cloudllm 0.15.9

A batteries-included Rust toolkit for building intelligent agents with LLM integration, multi-protocol tool support, multi-agent orchestration, and MentisDB-backed durable memory.
Documentation
//! Persistent CLI chat agent backed by MentisDB over MCP.
//!
//! By default this example starts a local MentisDB MCP server on an
//! ephemeral localhost port, then creates a GPT-5.4 CloudLLM agent with:
//! - remote MentisDB memory tools over MCP
//! - local memory, bash, HTTP, calculator, and filesystem tools
//!
//! The agent restores prior memory on startup and persists each completed turn
//! back into MentisDB so it can remember previous sessions.

#[path = "support/persistent_agent_tools.rs"]
mod persistent_agent_tools;

use std::env;
use std::io::{self, Write};
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use cloudllm::clients::openai::{Model, OpenAIClient};
use cloudllm::tool_protocol::ToolProtocol;
use cloudllm::Agent;
use mentisdb::server::{default_mentisdb_dir, start_mcp_server, MentisDbServiceConfig};
use persistent_agent_tools::build_persistent_agent_registry;
use serde_json::json;

const DEFAULT_CHAIN_KEY: &str = "borganism-brain";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    cloudllm::init_logger();

    let api_key = env::var("OPEN_AI_SECRET")
        .map_err(|_| "Please set OPEN_AI_SECRET to run persistent_agent_chat")?;

    let chain_key =
        env::var("MENTISDB_CHAIN_KEY").unwrap_or_else(|_| DEFAULT_CHAIN_KEY.to_string());
    let chain_dir = env::var("MENTISDB_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|_| default_mentisdb_dir());
    let filesystem_root = env::var("CLOUDLLM_CHAT_FS_ROOT")
        .map(PathBuf::from)
        .unwrap_or(env::current_dir()?);

    let mut embedded_server = None;
    let mentisdb_endpoint = if let Ok(endpoint) = env::var("MENTISDB_MCP_ENDPOINT") {
        endpoint
    } else {
        let server = start_mcp_server(
            SocketAddr::from(([127, 0, 0, 1], 0)),
            MentisDbServiceConfig::new(
                chain_dir.clone(),
                chain_key.clone(),
                mentisdb::StorageAdapterKind::Jsonl,
            ),
        )
        .await?;
        let endpoint = format!("http://{}", server.local_addr());
        embedded_server = Some(server);
        endpoint
    };

    let (registry, mentisdb_protocol) =
        build_persistent_agent_registry(&mentisdb_endpoint, filesystem_root.clone()).await?;

    bootstrap_chain(&mentisdb_protocol, &chain_key).await?;
    let restored_memory = load_memory_markdown(&mentisdb_protocol, &chain_key).await?;
    append_session_checkpoint(
        &mentisdb_protocol,
        &chain_key,
        "Session started for the persistent CLI chat agent.",
    )
    .await?;

    let client = Arc::new(OpenAIClient::new_with_model_enum(&api_key, Model::GPT54));
    let mut agent = Agent::new("persistent-chat", "Persistent Chat Agent", client)
        .with_expertise(
            "Long-running user collaboration, durable memory management, coding, shell, HTTP, and file operations",
        )
        .with_personality("Direct, pragmatic, memory-aware, and concise")
        .with_tools(registry);

    let system_prompt = build_system_prompt(&chain_key, &filesystem_root, &restored_memory);
    agent.set_system_prompt(&system_prompt);

    println!("Persistent Agent Chat");
    println!("Model: gpt-5.4");
    println!("MentisDB MCP endpoint: {}", mentisdb_endpoint);
    println!("MentisDB directory: {}", chain_dir.display());
    println!("MentisDB chain key: {}", chain_key);
    println!("Filesystem root: {}", filesystem_root.display());
    if embedded_server.is_some() {
        println!("MentisDB MCP server mode: embedded local server");
    } else {
        println!("MentisDB MCP server mode: external endpoint");
    }
    println!("Commands: /help, /tools, /memory, /recent, /search <text>, /remember <note>, /exit");
    println!("Input: press Enter to send. End a line with \\ to continue onto the next line.");

    loop {
        print!("\nYou:\n");
        io::stdout().flush()?;

        let user_input = read_continuation_input()?;
        let trimmed = user_input.trim();
        if trimmed.is_empty() {
            continue;
        }

        if trimmed == "/exit" {
            break;
        }
        if trimmed == "/help" {
            print_help();
            continue;
        }
        if trimmed == "/tools" {
            let tools = agent.list_tools().await;
            println!("Available tools:");
            for tool in tools {
                println!("  - {}", tool);
            }
            continue;
        }
        if trimmed == "/memory" {
            let markdown = load_memory_markdown(&mentisdb_protocol, &chain_key).await?;
            println!("{markdown}");
            continue;
        }
        if trimmed == "/recent" {
            let result = mentisdb_protocol
                .execute(
                    "mentisdb_recent_context",
                    json!({"chain_key": chain_key, "last_n": 12}),
                )
                .await?;
            println!(
                "{}",
                result.output["prompt"]
                    .as_str()
                    .unwrap_or("(no recent context)")
            );
            continue;
        }
        if let Some(query) = trimmed.strip_prefix("/search ") {
            let result = mentisdb_protocol
                .execute(
                    "mentisdb_search",
                    json!({"chain_key": chain_key, "text": query, "limit": 8}),
                )
                .await?;
            println!("{}", serde_json::to_string_pretty(&result.output)?);
            continue;
        }
        if let Some(note) = trimmed.strip_prefix("/remember ") {
            let result = mentisdb_protocol
                .execute(
                    "mentisdb_append",
                    json!({
                        "chain_key": chain_key,
                        "thought_type": "Insight",
                        "role": "Memory",
                        "importance": 0.9,
                        "tags": ["manual-note"],
                        "content": note,
                    }),
                )
                .await?;
            println!("{}", serde_json::to_string_pretty(&result.output)?);
            continue;
        }

        println!("Assistant is thinking...");
        let response = agent.send(&user_input).await?;
        println!("\nAssistant:\n{}\n", response.content);

        persist_turn(
            &mentisdb_protocol,
            &chain_key,
            &user_input,
            &response.content,
        )
        .await?;
    }

    println!("Session ended.");
    Ok(())
}

fn build_system_prompt(chain_key: &str, filesystem_root: &Path, restored_memory: &str) -> String {
    format!(
        "You are a persistent GPT-5.4 powered CloudLLM agent in a terminal chat.\n\
Your durable memory lives in MentisDB and is exposed over MCP tools.\n\
Chain key: {chain_key}\n\
Filesystem root: {}\n\n\
Behavior rules:\n\
- Use mentisdb_search when a user request may depend on prior sessions.\n\
- Use mentisdb_append whenever you learn durable user preferences, constraints, decisions, plans, corrections, insights, or surprises.\n\
- Use mentisdb_append_retrospective after a repeated failure, a long debugging snag, or a non-obvious fix that future agents should not rediscover the hard way.\n\
- Keep stored memories concise, factual, and semantically typed.\n\
- Do not store secrets unless the user explicitly asks you to remember them.\n\
- Use other tools normally for coding, shell, filesystem, HTTP, and calculations.\n\n\
Restored durable memory:\n{}\n",
        filesystem_root.display(),
        restored_memory
    )
}

fn read_continuation_input() -> Result<String, io::Error> {
    let mut user_input = String::new();
    loop {
        let mut line = String::new();
        io::stdin().read_line(&mut line)?;

        if let Some(stripped) = line.strip_suffix("\\\n") {
            user_input.push_str(stripped);
            user_input.push('\n');
            print!("> ");
            io::stdout().flush()?;
            continue;
        }
        if let Some(stripped) = line.strip_suffix("\\\r\n") {
            user_input.push_str(stripped);
            user_input.push('\n');
            print!("> ");
            io::stdout().flush()?;
            continue;
        }

        user_input.push_str(&line);
        break;
    }
    Ok(user_input)
}

fn print_help() {
    println!("Commands:");
    println!("  /help            Show this help");
    println!("  /tools           List tools available to the agent");
    println!("  /memory          Print MEMORY.md exported from MentisDB");
    println!("  /recent          Print recent MentisDB catch-up context");
    println!("  /search <text>   Search MentisDB memories by text");
    println!("  /remember <note> Store a manual durable memory");
    println!("  /exit            Quit the chat");
    println!("\nInput behavior:");
    println!("  Press Enter to send the current message");
    println!("  End a line with \\ to continue onto the next line");
}

async fn bootstrap_chain(
    mentisdb_protocol: &Arc<cloudllm::tool_protocols::McpClientProtocol>,
    chain_key: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    mentisdb_protocol
        .execute(
            "mentisdb_bootstrap",
            json!({
                "chain_key": chain_key,
                "content": "Bootstrap memory for the persistent CloudLLM CLI chat agent. Preserve durable user preferences, constraints, plans, decisions, insights, corrections, and summaries across sessions.",
                "importance": 1.0,
                "tags": ["bootstrap", "system"],
                "concepts": ["persistence", "semantic-memory", "cli-chat"]
            }),
        )
        .await?;
    Ok(())
}

async fn append_session_checkpoint(
    mentisdb_protocol: &Arc<cloudllm::tool_protocols::McpClientProtocol>,
    chain_key: &str,
    content: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    mentisdb_protocol
        .execute(
            "mentisdb_append",
            json!({
                "chain_key": chain_key,
                "thought_type": "Checkpoint",
                "role": "Checkpoint",
                "importance": 0.4,
                "tags": ["session"],
                "content": content,
            }),
        )
        .await?;
    Ok(())
}

async fn load_memory_markdown(
    mentisdb_protocol: &Arc<cloudllm::tool_protocols::McpClientProtocol>,
    chain_key: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
    let result = mentisdb_protocol
        .execute(
            "mentisdb_memory_markdown",
            json!({
                "chain_key": chain_key,
                "limit": 80,
            }),
        )
        .await?;

    Ok(result.output["markdown"]
        .as_str()
        .unwrap_or("# MEMORY\n\n")
        .to_string())
}

async fn persist_turn(
    mentisdb_protocol: &Arc<cloudllm::tool_protocols::McpClientProtocol>,
    chain_key: &str,
    user_input: &str,
    assistant_output: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let content = format!(
        "Conversation turn summary.\nUser: {}\nAssistant: {}",
        truncate_for_memory(user_input, 800),
        truncate_for_memory(assistant_output, 1200)
    );

    mentisdb_protocol
        .execute(
            "mentisdb_append",
            json!({
                "chain_key": chain_key,
                "thought_type": "Summary",
                "role": "Memory",
                "importance": 0.6,
                "tags": ["conversation-turn"],
                "concepts": ["session-memory"],
                "content": content,
            }),
        )
        .await?;

    Ok(())
}

fn truncate_for_memory(input: &str, max_chars: usize) -> String {
    let mut truncated = input.trim().chars().take(max_chars).collect::<String>();
    if input.trim().chars().count() > max_chars {
        truncated.push_str("...");
    }
    truncated
}