Skip to main content

lean_ctx/tools/registered/
ctx_agent.rs

1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_bool, get_str};
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, stigmergic scent field.\n\
19            WORKFLOW: register agents first, then post/read messages, sync for state alignment.\n\
20            Actions: register (agent_type+role), post (message+category), read (poll),\n\
21            status (active|idle|finished), handoff (task+summary), sync (agents+messages+scent),\n\
22            claim/release (file/task), brief (sub-agent briefing),\n\
23            return (distill→knowledge), diary|recall_diary|diaries (agent journal),\n\
24            share_knowledge|receive_knowledge (cross-agent), list, info.\n\
25            ANTIPATTERN: NOT for single-agent workflows. Use ctx_compose for code understanding.",
26            json!({
27                "type": "object",
28                "properties": {
29                    "action": {
30                        "type": "string",
31                        "enum": ["register", "list", "post", "read", "status", "info", "handoff", "sync", "claim", "release", "brief", "return", "diary", "recall_diary", "diaries", "share_knowledge", "receive_knowledge"],
32                        "description": "register|list|post|read|status|info|handoff|sync|claim|release|brief|return|diary|recall_diary|diaries|share_knowledge|receive_knowledge"
33                    },
34                    "agent_type": {
35                        "type": "string",
36                        "description": "cursor|claude|codex|gemini|crush|subagent"
37                    },
38                    "role": {
39                        "type": "string",
40                        "description": "dev|review|test|plan"
41                    },
42                    "message": {
43                        "type": "string",
44                        "description": "Post text or status detail"
45                    },
46                    "category": {
47                        "type": "string",
48                        "description": "finding|warning|request|status"
49                    },
50                    "to_agent": {
51                        "type": "string",
52                        "description": "Target agent ID"
53                    },
54                    "status": {
55                        "type": "string",
56                        "enum": ["active", "idle", "finished"],
57                        "description": "active|idle|finished"
58                    }
59                },
60                "required": ["action"]
61            }),
62        )
63    }
64
65    fn handle(
66        &self,
67        args: &Map<String, Value>,
68        ctx: &ToolContext,
69    ) -> Result<ToolOutput, ErrorData> {
70        let action = get_str(args, "action")
71            .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
72        let agent_type = get_str(args, "agent_type");
73        let role = get_str(args, "role");
74        let message = get_str(args, "message");
75        let category = get_str(args, "category");
76        let to_agent = get_str(args, "to_agent");
77        let status = get_str(args, "status");
78        let privacy = get_str(args, "privacy");
79        let priority = get_str(args, "priority");
80        let ttl_hours: Option<u64> = args.get("ttl_hours").and_then(serde_json::Value::as_u64);
81        let format = get_str(args, "format");
82        let write = get_bool(args, "write").unwrap_or(false);
83        let filename = get_str(args, "filename");
84
85        let project_root = ctx.project_root.clone();
86
87        let agent_id_handle = ctx.agent_id.as_ref();
88        let current_agent_id = agent_id_handle
89            .map(|a| a.blocking_read().clone())
90            .unwrap_or_default();
91
92        let result = crate::tools::ctx_agent::handle(
93            &action,
94            agent_type.as_deref(),
95            role.as_deref(),
96            &project_root,
97            current_agent_id.as_deref(),
98            message.as_deref(),
99            category.as_deref(),
100            to_agent.as_deref(),
101            status.as_deref(),
102            privacy.as_deref(),
103            priority.as_deref(),
104            ttl_hours,
105            format.as_deref(),
106            write,
107            filename.as_deref(),
108        );
109
110        if action == "register" {
111            if let Some(id) = result.split(':').nth(1) {
112                let id = id.split_whitespace().next().unwrap_or("").to_string();
113                if !id.is_empty()
114                    && let Some(handle) = agent_id_handle
115                {
116                    let mut guard = handle.blocking_write();
117                    *guard = Some(id);
118                }
119            }
120
121            let agent_role =
122                crate::core::agents::AgentRole::from_str_loose(role.as_deref().unwrap_or("coder"));
123            let depth = crate::core::agents::ContextDepthConfig::for_role(agent_role);
124            let depth_hint = format!(
125                "\n[context] role={:?} preferred_mode={} max_full={} max_sig={} budget_ratio={:.0}%",
126                agent_role,
127                depth.preferred_mode,
128                depth.max_files_full,
129                depth.max_files_signatures,
130                depth.context_budget_ratio * 100.0,
131            );
132            return Ok(ToolOutput {
133                text: format!("{result}{depth_hint}"),
134                original_tokens: 0,
135                saved_tokens: 0,
136                mode: Some(action),
137                path: None,
138                changed: false,
139                shell_outcome: None,
140            });
141        }
142
143        Ok(ToolOutput {
144            text: result,
145            original_tokens: 0,
146            saved_tokens: 0,
147            mode: Some(action),
148            path: None,
149            changed: false,
150            shell_outcome: None,
151        })
152    }
153}