lean_ctx/tools/registered/
ctx_agent.rs1use 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|lease_acquire|lease_release", "lease_acquire", "lease_release"],
32 "description": "register|list|post|read|status|info|handoff|sync|claim|release|brief|return|diary|recall_diary|diaries|share_knowledge|receive_knowledge|lease_acquire|lease_release"
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 "allOf": [
61 { "if": { "properties": { "action": { "const": "post" } }, "required": ["action"] }, "then": { "required": ["action", "message"] } },
62 { "if": { "properties": { "action": { "const": "status" } }, "required": ["action"] }, "then": { "required": ["action", "status"] } },
63 { "if": { "properties": { "action": { "const": "handoff" } }, "required": ["action"] }, "then": { "required": ["action", "to_agent"] } },
64 { "if": { "properties": { "action": { "const": "claim" } }, "required": ["action"] }, "then": { "required": ["action", "message"] } },
65 { "if": { "properties": { "action": { "const": "release" } }, "required": ["action"] }, "then": { "required": ["action", "message"] } },
66 { "if": { "properties": { "action": { "const": "brief" } }, "required": ["action"] }, "then": { "required": ["action", "message"] } },
67 { "if": { "properties": { "action": { "const": "return" } }, "required": ["action"] }, "then": { "required": ["action", "message"] } },
68 { "if": { "properties": { "action": { "const": "diary" } }, "required": ["action"] }, "then": { "required": ["action", "message"] } },
69 { "if": { "properties": { "action": { "const": "share_knowledge" } }, "required": ["action"] }, "then": { "required": ["action", "message"] } }
70 ],
71 "required": ["action"]
72 }),
73 )
74 }
75
76 fn handle(
77 &self,
78 args: &Map<String, Value>,
79 ctx: &ToolContext,
80 ) -> Result<ToolOutput, ErrorData> {
81 let action = get_str(args, "action")
82 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
83 let agent_type = get_str(args, "agent_type");
84 let role = get_str(args, "role");
85 let message = get_str(args, "message");
86 let category = get_str(args, "category");
87 let to_agent = get_str(args, "to_agent");
88 let status = get_str(args, "status");
89 let privacy = get_str(args, "privacy");
90 let priority = get_str(args, "priority");
91 let ttl_hours: Option<u64> = args.get("ttl_hours").and_then(serde_json::Value::as_u64);
92 let format = get_str(args, "format");
93 let write = get_bool(args, "write").unwrap_or(false);
94 let filename = get_str(args, "filename");
95
96 let project_root = ctx.project_root.clone();
97
98 let agent_id_handle = ctx.agent_id.as_ref();
99 let current_agent_id = agent_id_handle
100 .map(|a| a.blocking_read().clone())
101 .unwrap_or_default();
102
103 let result = crate::tools::ctx_agent::handle(
104 &action,
105 agent_type.as_deref(),
106 role.as_deref(),
107 &project_root,
108 current_agent_id.as_deref(),
109 message.as_deref(),
110 category.as_deref(),
111 to_agent.as_deref(),
112 status.as_deref(),
113 privacy.as_deref(),
114 priority.as_deref(),
115 ttl_hours,
116 format.as_deref(),
117 write,
118 filename.as_deref(),
119 );
120
121 if action == "register" {
122 if let Some(id) = result.split(':').nth(1) {
123 let id = id.split_whitespace().next().unwrap_or("").to_string();
124 if !id.is_empty()
125 && let Some(handle) = agent_id_handle
126 {
127 let mut guard = handle.blocking_write();
128 *guard = Some(id);
129 }
130 }
131
132 let agent_role =
133 crate::core::agents::AgentRole::from_str_loose(role.as_deref().unwrap_or("coder"));
134 let depth = crate::core::agents::ContextDepthConfig::for_role(agent_role);
135 let depth_hint = format!(
136 "\n[context] role={:?} preferred_mode={} max_full={} max_sig={} budget_ratio={:.0}%",
137 agent_role,
138 depth.preferred_mode,
139 depth.max_files_full,
140 depth.max_files_signatures,
141 depth.context_budget_ratio * 100.0,
142 );
143 return Ok(ToolOutput {
144 text: format!("{result}{depth_hint}"),
145 original_tokens: 0,
146 saved_tokens: 0,
147 mode: Some(action),
148 path: None,
149 changed: false,
150 shell_outcome: None,
151 content_blocks: None,
152 });
153 }
154
155 Ok(ToolOutput {
156 text: result,
157 original_tokens: 0,
158 saved_tokens: 0,
159 mode: Some(action),
160 path: None,
161 changed: false,
162 shell_outcome: None,
163 content_blocks: None,
164 })
165 }
166}