lean-ctx 3.1.5

Context Runtime for AI Agents with CCP. 42 MCP tools, 10 read modes, 90+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing + diaries, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24 AI tools. Reduces LLM token consumption by up to 99%.
Documentation
use crate::core::agents::{AgentDiary, AgentRegistry, AgentStatus, DiaryEntryType};

#[allow(clippy::too_many_arguments)]
pub fn handle(
    action: &str,
    agent_type: Option<&str>,
    role: Option<&str>,
    project_root: &str,
    current_agent_id: Option<&str>,
    message: Option<&str>,
    category: Option<&str>,
    to_agent: Option<&str>,
    status: Option<&str>,
) -> String {
    match action {
        "register" => {
            let atype = agent_type.unwrap_or("unknown");
            let mut registry = AgentRegistry::load_or_create();
            registry.cleanup_stale(24);
            let agent_id = registry.register(atype, role, project_root);
            match registry.save() {
                Ok(()) => format!(
                    "Agent registered: {agent_id} (type: {atype}, role: {})",
                    role.unwrap_or("none")
                ),
                Err(e) => format!("Registered as {agent_id} but save failed: {e}"),
            }
        }

        "list" => {
            let mut registry = AgentRegistry::load_or_create();
            registry.cleanup_stale(24);
            let _ = registry.save();

            let agents = registry.list_active(Some(project_root));
            if agents.is_empty() {
                return "No active agents for this project.".to_string();
            }

            let mut out = format!("Active agents ({}):\n", agents.len());
            for a in agents {
                let role_str = a.role.as_deref().unwrap_or("-");
                let status_msg = a
                    .status_message
                    .as_deref()
                    .map(|m| format!(" — {m}"))
                    .unwrap_or_default();
                let age = (chrono::Utc::now() - a.last_active).num_minutes();
                out.push_str(&format!(
                    "  {} [{}] role={} status={}{} (last active: {}m ago, pid: {})\n",
                    a.agent_id, a.agent_type, role_str, a.status, status_msg, age, a.pid
                ));
            }
            out
        }

        "post" => {
            let msg = match message {
                Some(m) => m,
                None => return "Error: message is required for post".to_string(),
            };
            let cat = category.unwrap_or("status");
            let from = current_agent_id.unwrap_or("anonymous");
            let mut registry = AgentRegistry::load_or_create();
            let msg_id = registry.post_message(from, to_agent, cat, msg);
            match registry.save() {
                Ok(()) => {
                    let target = to_agent.unwrap_or("all agents (broadcast)");
                    format!("Posted [{cat}] to {target}: {msg} (id: {msg_id})")
                }
                Err(e) => format!("Posted but save failed: {e}"),
            }
        }

        "read" => {
            let agent_id = match current_agent_id {
                Some(id) => id,
                None => {
                    return "Error: agent must be registered first (use action=register)"
                        .to_string()
                }
            };
            let mut registry = AgentRegistry::load_or_create();
            let messages = registry.read_unread(agent_id);

            if messages.is_empty() {
                let _ = registry.save();
                return "No new messages.".to_string();
            }

            let mut out = format!("New messages ({}):\n", messages.len());
            for m in &messages {
                let age = (chrono::Utc::now() - m.timestamp).num_minutes();
                out.push_str(&format!(
                    "  [{}] from {} ({}m ago): {}\n",
                    m.category, m.from_agent, age, m.message
                ));
            }
            let _ = registry.save();
            out
        }

        "status" => {
            let agent_id = match current_agent_id {
                Some(id) => id,
                None => return "Error: agent must be registered first".to_string(),
            };
            let new_status = match status {
                Some("active") => AgentStatus::Active,
                Some("idle") => AgentStatus::Idle,
                Some("finished") => AgentStatus::Finished,
                Some(other) => {
                    return format!("Unknown status: {other}. Use: active, idle, finished")
                }
                None => return "Error: status value is required".to_string(),
            };
            let status_msg = message;

            let mut registry = AgentRegistry::load_or_create();
            registry.set_status(agent_id, new_status.clone(), status_msg);
            match registry.save() {
                Ok(()) => format!(
                    "Status updated: {} → {}{}",
                    agent_id,
                    new_status,
                    status_msg.map(|m| format!(" ({m})")).unwrap_or_default()
                ),
                Err(e) => format!("Status set but save failed: {e}"),
            }
        }

        "info" => {
            let registry = AgentRegistry::load_or_create();
            let total = registry.agents.len();
            let active = registry
                .agents
                .iter()
                .filter(|a| a.status == AgentStatus::Active)
                .count();
            let messages = registry.scratchpad.len();
            format!(
                "Agent Registry: {total} total, {active} active, {messages} scratchpad entries\nLast updated: {}",
                registry.updated_at.format("%Y-%m-%d %H:%M UTC")
            )
        }

        "handoff" => {
            let from = match current_agent_id {
                Some(id) => id,
                None => return "Error: agent must be registered first".to_string(),
            };
            let target = match to_agent {
                Some(id) => id,
                None => return "Error: to_agent is required for handoff".to_string(),
            };
            let summary = message.unwrap_or("(no summary provided)");

            let mut registry = AgentRegistry::load_or_create();

            registry.post_message(
                from,
                Some(target),
                "handoff",
                &format!("HANDOFF from {from}: {summary}"),
            );

            registry.set_status(from, AgentStatus::Finished, Some("handed off"));
            let _ = registry.save();

            format!("Handoff complete: {from} → {target}\nSummary: {summary}")
        }

        "sync" => {
            let registry = AgentRegistry::load_or_create();
            let agents: Vec<&crate::core::agents::AgentEntry> = registry
                .agents
                .iter()
                .filter(|a| a.status != AgentStatus::Finished)
                .collect();

            if agents.is_empty() {
                return "No active agents to sync with.".to_string();
            }

            let pending_count = registry
                .scratchpad
                .iter()
                .filter(|e| {
                    if let Some(ref id) = current_agent_id {
                        !e.read_by.contains(&id.to_string()) && e.from_agent != *id
                    } else {
                        false
                    }
                })
                .count();

            let shared_dir = dirs::home_dir()
                .unwrap_or_default()
                .join(".lean-ctx")
                .join("agents")
                .join("shared");

            let shared_count = if shared_dir.exists() {
                std::fs::read_dir(&shared_dir)
                    .map(|rd| rd.count())
                    .unwrap_or(0)
            } else {
                0
            };

            let mut out = "Multi-Agent Sync Status:\n".to_string();
            out.push_str(&format!("  Active agents: {}\n", agents.len()));
            for a in &agents {
                let role = a.role.as_deref().unwrap_or("-");
                let age = (chrono::Utc::now() - a.last_active).num_minutes();
                out.push_str(&format!(
                    "    {} [{}] role={} ({}m ago)\n",
                    a.agent_id, a.agent_type, role, age
                ));
            }
            out.push_str(&format!("  Pending messages: {pending_count}\n"));
            out.push_str(&format!("  Shared contexts: {shared_count}\n"));
            out
        }

        "diary" => {
            let agent_id = match current_agent_id {
                Some(id) => id,
                None => return "Error: agent must be registered first".to_string(),
            };
            let content = match message {
                Some(m) => m,
                None => return "Error: message is required for diary entry".to_string(),
            };
            let entry_type = match category.unwrap_or("progress") {
                "discovery" | "found" => DiaryEntryType::Discovery,
                "decision" | "decided" => DiaryEntryType::Decision,
                "blocker" | "blocked" => DiaryEntryType::Blocker,
                "progress" | "done" => DiaryEntryType::Progress,
                "insight" => DiaryEntryType::Insight,
                other => return format!("Unknown diary type: {other}. Use: discovery, decision, blocker, progress, insight"),
            };
            let atype = agent_type.unwrap_or("unknown");
            let mut diary = AgentDiary::load_or_create(agent_id, atype, project_root);
            let context_str = to_agent;
            diary.add_entry(entry_type.clone(), content, context_str);
            match diary.save() {
                Ok(()) => format!("Diary entry [{entry_type}] added: {content}"),
                Err(e) => format!("Diary entry added but save failed: {e}"),
            }
        }

        "recall_diary" | "diary_recall" => {
            let agent_id = match current_agent_id {
                Some(id) => id,
                None => {
                    let diaries = AgentDiary::list_all();
                    if diaries.is_empty() {
                        return "No agent diaries found.".to_string();
                    }
                    let mut out = format!("Agent Diaries ({}):\n", diaries.len());
                    for (id, count, updated) in &diaries {
                        let age = (chrono::Utc::now() - *updated).num_minutes();
                        out.push_str(&format!("  {id}: {count} entries ({age}m ago)\n"));
                    }
                    return out;
                }
            };
            match AgentDiary::load(agent_id) {
                Some(diary) => diary.format_summary(),
                None => format!("No diary found for agent '{agent_id}'."),
            }
        }

        "diaries" => {
            let diaries = AgentDiary::list_all();
            if diaries.is_empty() {
                return "No agent diaries found.".to_string();
            }
            let mut out = format!("Agent Diaries ({}):\n", diaries.len());
            for (id, count, updated) in &diaries {
                let age = (chrono::Utc::now() - *updated).num_minutes();
                out.push_str(&format!("  {id}: {count} entries ({age}m ago)\n"));
            }
            out
        }

        _ => format!("Unknown action: {action}. Use: register, list, post, read, status, info, handoff, sync, diary, recall_diary, diaries"),
    }
}