lean-ctx 3.8.5

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, 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 rmcp::model::Tool;
use rmcp::ErrorData;
use serde_json::{json, Map, Value};

use crate::server::tool_trait::{get_bool, get_str, McpTool, ToolContext, ToolOutput};
use crate::tool_defs::tool_def;

pub struct CtxAgentTool;

impl McpTool for CtxAgentTool {
    fn name(&self) -> &'static str {
        "ctx_agent"
    }

    fn tool_def(&self) -> Tool {
        tool_def(
            "ctx_agent",
            "Multi-agent coordination: shared message bus, persistent diaries, stigmergic scent field. Actions: register (agent_type+role), \
post (message+category), read (poll), status (active|idle|finished), handoff (transfer task+summary), \
sync (agents + messages + scent: claims/stuck/hot), claim/release (atomic file/task claim, message=target), \
brief (sub-agent briefing pack: message=task, priority=budget), \
return (distill sub-agent report into knowledge: message='category/key: value' lines), \
diary (log discovery/decision/blocker/progress/insight), recall_diary, diaries, list, info.",
            json!({
                "type": "object",
                "properties": {
                    "action": {
                        "type": "string",
                        "enum": ["register", "list", "post", "read", "status", "info", "handoff", "sync", "claim", "release", "brief", "return", "diary", "recall_diary", "diaries", "share_knowledge", "receive_knowledge"],
                        "description": "Agent operation."
                    },
                    "agent_type": {
                        "type": "string",
                        "description": "Agent type for register (cursor, claude, codex, gemini, crush, subagent)"
                    },
                    "role": {
                        "type": "string",
                        "description": "Agent role (dev, review, test, plan)"
                    },
                    "message": {
                        "type": "string",
                        "description": "Message text for post action, or status detail for status action"
                    },
                    "category": {
                        "type": "string",
                        "description": "Message category for post (finding, warning, request, status)"
                    },
                    "to_agent": {
                        "type": "string",
                        "description": "Target agent ID for direct message (omit for broadcast)"
                    },
                    "status": {
                        "type": "string",
                        "enum": ["active", "idle", "finished"],
                        "description": "New status for status action"
                    }
                },
                "required": ["action"]
            }),
        )
    }

    fn handle(
        &self,
        args: &Map<String, Value>,
        ctx: &ToolContext,
    ) -> Result<ToolOutput, ErrorData> {
        let action = get_str(args, "action")
            .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
        let agent_type = get_str(args, "agent_type");
        let role = get_str(args, "role");
        let message = get_str(args, "message");
        let category = get_str(args, "category");
        let to_agent = get_str(args, "to_agent");
        let status = get_str(args, "status");
        let privacy = get_str(args, "privacy");
        let priority = get_str(args, "priority");
        let ttl_hours: Option<u64> = args.get("ttl_hours").and_then(serde_json::Value::as_u64);
        let format = get_str(args, "format");
        let write = get_bool(args, "write").unwrap_or(false);
        let filename = get_str(args, "filename");

        let project_root = ctx.project_root.clone();

        let agent_id_handle = ctx.agent_id.as_ref();
        let current_agent_id = agent_id_handle
            .map(|a| a.blocking_read().clone())
            .unwrap_or_default();

        let result = crate::tools::ctx_agent::handle(
            &action,
            agent_type.as_deref(),
            role.as_deref(),
            &project_root,
            current_agent_id.as_deref(),
            message.as_deref(),
            category.as_deref(),
            to_agent.as_deref(),
            status.as_deref(),
            privacy.as_deref(),
            priority.as_deref(),
            ttl_hours,
            format.as_deref(),
            write,
            filename.as_deref(),
        );

        if action == "register" {
            if let Some(id) = result.split(':').nth(1) {
                let id = id.split_whitespace().next().unwrap_or("").to_string();
                if !id.is_empty() {
                    if let Some(handle) = agent_id_handle {
                        let mut guard = handle.blocking_write();
                        *guard = Some(id);
                    }
                }
            }

            let agent_role =
                crate::core::agents::AgentRole::from_str_loose(role.as_deref().unwrap_or("coder"));
            let depth = crate::core::agents::ContextDepthConfig::for_role(agent_role);
            let depth_hint = format!(
                "\n[context] role={:?} preferred_mode={} max_full={} max_sig={} budget_ratio={:.0}%",
                agent_role,
                depth.preferred_mode,
                depth.max_files_full,
                depth.max_files_signatures,
                depth.context_budget_ratio * 100.0,
            );
            return Ok(ToolOutput {
                text: format!("{result}{depth_hint}"),
                original_tokens: 0,
                saved_tokens: 0,
                mode: Some(action),
                path: None,
                changed: false,
                shell_outcome: None,
            });
        }

        Ok(ToolOutput {
            text: result,
            original_tokens: 0,
            saved_tokens: 0,
            mode: Some(action),
            path: None,
            changed: false,
            shell_outcome: None,
        })
    }
}