Skip to main content

lean_ctx/tools/registered/
ctx_agent.rs

1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_bool, get_str, McpTool, ToolContext, ToolOutput};
6use crate::tool_defs::tool_def;
7
8pub struct CtxAgentTool;
9
10impl McpTool for CtxAgentTool {
11    fn name(&self) -> &'static str {
12        "ctx_agent"
13    }
14
15    fn tool_def(&self) -> Tool {
16        tool_def(
17            "ctx_agent",
18            "Multi-agent coordination (shared message bus + persistent diaries). Actions: register (join with agent_type+role), \
19post (broadcast or direct message with category), read (poll messages), status (update state: active|idle|finished), \
20handoff (transfer task to another agent with summary), sync (overview of all agents + pending messages + shared contexts), \
21diary (log discovery/decision/blocker/progress/insight — persisted across sessions), \
22recall_diary (read agent diary), diaries (list all agent diaries), \
23list, info.",
24            json!({
25                "type": "object",
26                "properties": {
27                    "action": {
28                        "type": "string",
29                        "enum": ["register", "list", "post", "read", "status", "info", "handoff", "sync", "diary", "recall_diary", "diaries", "share_knowledge", "receive_knowledge"],
30                        "description": "Agent operation."
31                    },
32                    "agent_type": {
33                        "type": "string",
34                        "description": "Agent type for register (cursor, claude, codex, gemini, crush, subagent)"
35                    },
36                    "role": {
37                        "type": "string",
38                        "description": "Agent role (dev, review, test, plan)"
39                    },
40                    "message": {
41                        "type": "string",
42                        "description": "Message text for post action, or status detail for status action"
43                    },
44                    "category": {
45                        "type": "string",
46                        "description": "Message category for post (finding, warning, request, status)"
47                    },
48                    "to_agent": {
49                        "type": "string",
50                        "description": "Target agent ID for direct message (omit for broadcast)"
51                    },
52                    "status": {
53                        "type": "string",
54                        "enum": ["active", "idle", "finished"],
55                        "description": "New status for status action"
56                    }
57                },
58                "required": ["action"]
59            }),
60        )
61    }
62
63    fn handle(
64        &self,
65        args: &Map<String, Value>,
66        ctx: &ToolContext,
67    ) -> Result<ToolOutput, ErrorData> {
68        let action = get_str(args, "action")
69            .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
70        let agent_type = get_str(args, "agent_type");
71        let role = get_str(args, "role");
72        let message = get_str(args, "message");
73        let category = get_str(args, "category");
74        let to_agent = get_str(args, "to_agent");
75        let status = get_str(args, "status");
76        let privacy = get_str(args, "privacy");
77        let priority = get_str(args, "priority");
78        let ttl_hours: Option<u64> = args.get("ttl_hours").and_then(serde_json::Value::as_u64);
79        let format = get_str(args, "format");
80        let write = get_bool(args, "write").unwrap_or(false);
81        let filename = get_str(args, "filename");
82
83        let project_root = ctx.project_root.clone();
84
85        let agent_id_handle = ctx.agent_id.as_ref().unwrap();
86        let current_agent_id = {
87            let guard = agent_id_handle.blocking_read();
88            guard.clone()
89        };
90
91        let result = crate::tools::ctx_agent::handle(
92            &action,
93            agent_type.as_deref(),
94            role.as_deref(),
95            &project_root,
96            current_agent_id.as_deref(),
97            message.as_deref(),
98            category.as_deref(),
99            to_agent.as_deref(),
100            status.as_deref(),
101            privacy.as_deref(),
102            priority.as_deref(),
103            ttl_hours,
104            format.as_deref(),
105            write,
106            filename.as_deref(),
107        );
108
109        if action == "register" {
110            if let Some(id) = result.split(':').nth(1) {
111                let id = id.split_whitespace().next().unwrap_or("").to_string();
112                if !id.is_empty() {
113                    let mut guard = agent_id_handle.blocking_write();
114                    *guard = Some(id);
115                }
116            }
117
118            let agent_role =
119                crate::core::agents::AgentRole::from_str_loose(role.as_deref().unwrap_or("coder"));
120            let depth = crate::core::agents::ContextDepthConfig::for_role(agent_role);
121            let depth_hint = format!(
122                "\n[context] role={:?} preferred_mode={} max_full={} max_sig={} budget_ratio={:.0}%",
123                agent_role,
124                depth.preferred_mode,
125                depth.max_files_full,
126                depth.max_files_signatures,
127                depth.context_budget_ratio * 100.0,
128            );
129            return Ok(ToolOutput {
130                text: format!("{result}{depth_hint}"),
131                original_tokens: 0,
132                saved_tokens: 0,
133                mode: Some(action),
134                path: None,
135            });
136        }
137
138        Ok(ToolOutput {
139            text: result,
140            original_tokens: 0,
141            saved_tokens: 0,
142            mode: Some(action),
143            path: None,
144        })
145    }
146}