lean_ctx/tools/registered/
ctx_agent.rs1use 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, stigmergic scent field. Actions: register (agent_type+role), \
19post (message+category), read (poll), status (active|idle|finished), handoff (transfer task+summary), \
20sync (agents + messages + scent: claims/stuck/hot), claim/release (atomic file/task claim, message=target), \
21brief (sub-agent briefing pack: message=task, priority=budget), \
22return (distill sub-agent report into knowledge: message='category/key: value' lines), \
23diary (log discovery/decision/blocker/progress/insight), recall_diary, diaries, list, info.",
24 json!({
25 "type": "object",
26 "properties": {
27 "action": {
28 "type": "string",
29 "enum": ["register", "list", "post", "read", "status", "info", "handoff", "sync", "claim", "release", "brief", "return", "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();
86 let current_agent_id = agent_id_handle
87 .map(|a| a.blocking_read().clone())
88 .unwrap_or_default();
89
90 let result = crate::tools::ctx_agent::handle(
91 &action,
92 agent_type.as_deref(),
93 role.as_deref(),
94 &project_root,
95 current_agent_id.as_deref(),
96 message.as_deref(),
97 category.as_deref(),
98 to_agent.as_deref(),
99 status.as_deref(),
100 privacy.as_deref(),
101 priority.as_deref(),
102 ttl_hours,
103 format.as_deref(),
104 write,
105 filename.as_deref(),
106 );
107
108 if action == "register" {
109 if let Some(id) = result.split(':').nth(1) {
110 let id = id.split_whitespace().next().unwrap_or("").to_string();
111 if !id.is_empty() {
112 if let Some(handle) = agent_id_handle {
113 let mut guard = handle.blocking_write();
114 *guard = Some(id);
115 }
116 }
117 }
118
119 let agent_role =
120 crate::core::agents::AgentRole::from_str_loose(role.as_deref().unwrap_or("coder"));
121 let depth = crate::core::agents::ContextDepthConfig::for_role(agent_role);
122 let depth_hint = format!(
123 "\n[context] role={:?} preferred_mode={} max_full={} max_sig={} budget_ratio={:.0}%",
124 agent_role,
125 depth.preferred_mode,
126 depth.max_files_full,
127 depth.max_files_signatures,
128 depth.context_budget_ratio * 100.0,
129 );
130 return Ok(ToolOutput {
131 text: format!("{result}{depth_hint}"),
132 original_tokens: 0,
133 saved_tokens: 0,
134 mode: Some(action),
135 path: None,
136 changed: false,
137 shell_outcome: None,
138 });
139 }
140
141 Ok(ToolOutput {
142 text: result,
143 original_tokens: 0,
144 saved_tokens: 0,
145 mode: Some(action),
146 path: None,
147 changed: false,
148 shell_outcome: None,
149 })
150 }
151}