lean_ctx/tools/
ctx_agent.rs1use crate::core::agents::{AgentRegistry, AgentStatus};
2
3#[allow(clippy::too_many_arguments)]
4pub fn handle(
5 action: &str,
6 agent_type: Option<&str>,
7 role: Option<&str>,
8 project_root: &str,
9 current_agent_id: Option<&str>,
10 message: Option<&str>,
11 category: Option<&str>,
12 to_agent: Option<&str>,
13 status: Option<&str>,
14) -> String {
15 match action {
16 "register" => {
17 let atype = agent_type.unwrap_or("unknown");
18 let mut registry = AgentRegistry::load_or_create();
19 registry.cleanup_stale(24);
20 let agent_id = registry.register(atype, role, project_root);
21 match registry.save() {
22 Ok(()) => format!(
23 "Agent registered: {agent_id} (type: {atype}, role: {})",
24 role.unwrap_or("none")
25 ),
26 Err(e) => format!("Registered as {agent_id} but save failed: {e}"),
27 }
28 }
29
30 "list" => {
31 let mut registry = AgentRegistry::load_or_create();
32 registry.cleanup_stale(24);
33 let _ = registry.save();
34
35 let agents = registry.list_active(Some(project_root));
36 if agents.is_empty() {
37 return "No active agents for this project.".to_string();
38 }
39
40 let mut out = format!("Active agents ({}):\n", agents.len());
41 for a in agents {
42 let role_str = a.role.as_deref().unwrap_or("-");
43 let status_msg = a
44 .status_message
45 .as_deref()
46 .map(|m| format!(" — {m}"))
47 .unwrap_or_default();
48 let age = (chrono::Utc::now() - a.last_active).num_minutes();
49 out.push_str(&format!(
50 " {} [{}] role={} status={}{} (last active: {}m ago, pid: {})\n",
51 a.agent_id, a.agent_type, role_str, a.status, status_msg, age, a.pid
52 ));
53 }
54 out
55 }
56
57 "post" => {
58 let msg = match message {
59 Some(m) => m,
60 None => return "Error: message is required for post".to_string(),
61 };
62 let cat = category.unwrap_or("status");
63 let from = current_agent_id.unwrap_or("anonymous");
64 let mut registry = AgentRegistry::load_or_create();
65 let msg_id = registry.post_message(from, to_agent, cat, msg);
66 match registry.save() {
67 Ok(()) => {
68 let target = to_agent.unwrap_or("all agents (broadcast)");
69 format!("Posted [{cat}] to {target}: {msg} (id: {msg_id})")
70 }
71 Err(e) => format!("Posted but save failed: {e}"),
72 }
73 }
74
75 "read" => {
76 let agent_id = match current_agent_id {
77 Some(id) => id,
78 None => {
79 return "Error: agent must be registered first (use action=register)"
80 .to_string()
81 }
82 };
83 let mut registry = AgentRegistry::load_or_create();
84 let messages = registry.read_unread(agent_id);
85
86 if messages.is_empty() {
87 let _ = registry.save();
88 return "No new messages.".to_string();
89 }
90
91 let mut out = format!("New messages ({}):\n", messages.len());
92 for m in &messages {
93 let age = (chrono::Utc::now() - m.timestamp).num_minutes();
94 out.push_str(&format!(
95 " [{}] from {} ({}m ago): {}\n",
96 m.category, m.from_agent, age, m.message
97 ));
98 }
99 let _ = registry.save();
100 out
101 }
102
103 "status" => {
104 let agent_id = match current_agent_id {
105 Some(id) => id,
106 None => return "Error: agent must be registered first".to_string(),
107 };
108 let new_status = match status {
109 Some("active") => AgentStatus::Active,
110 Some("idle") => AgentStatus::Idle,
111 Some("finished") => AgentStatus::Finished,
112 Some(other) => {
113 return format!("Unknown status: {other}. Use: active, idle, finished")
114 }
115 None => return "Error: status value is required".to_string(),
116 };
117 let status_msg = message;
118
119 let mut registry = AgentRegistry::load_or_create();
120 registry.set_status(agent_id, new_status.clone(), status_msg);
121 match registry.save() {
122 Ok(()) => format!(
123 "Status updated: {} → {}{}",
124 agent_id,
125 new_status,
126 status_msg.map(|m| format!(" ({m})")).unwrap_or_default()
127 ),
128 Err(e) => format!("Status set but save failed: {e}"),
129 }
130 }
131
132 "info" => {
133 let registry = AgentRegistry::load_or_create();
134 let total = registry.agents.len();
135 let active = registry
136 .agents
137 .iter()
138 .filter(|a| a.status == AgentStatus::Active)
139 .count();
140 let messages = registry.scratchpad.len();
141 format!(
142 "Agent Registry: {total} total, {active} active, {messages} scratchpad entries\nLast updated: {}",
143 registry.updated_at.format("%Y-%m-%d %H:%M UTC")
144 )
145 }
146
147 "handoff" => {
148 let from = match current_agent_id {
149 Some(id) => id,
150 None => return "Error: agent must be registered first".to_string(),
151 };
152 let target = match to_agent {
153 Some(id) => id,
154 None => return "Error: to_agent is required for handoff".to_string(),
155 };
156 let summary = message.unwrap_or("(no summary provided)");
157
158 let mut registry = AgentRegistry::load_or_create();
159
160 registry.post_message(
161 from,
162 Some(target),
163 "handoff",
164 &format!("HANDOFF from {from}: {summary}"),
165 );
166
167 registry.set_status(from, AgentStatus::Finished, Some("handed off"));
168 let _ = registry.save();
169
170 format!("Handoff complete: {from} → {target}\nSummary: {summary}")
171 }
172
173 "sync" => {
174 let registry = AgentRegistry::load_or_create();
175 let agents: Vec<&crate::core::agents::AgentEntry> = registry
176 .agents
177 .iter()
178 .filter(|a| a.status != AgentStatus::Finished)
179 .collect();
180
181 if agents.is_empty() {
182 return "No active agents to sync with.".to_string();
183 }
184
185 let pending_count = registry
186 .scratchpad
187 .iter()
188 .filter(|e| {
189 if let Some(ref id) = current_agent_id {
190 !e.read_by.contains(&id.to_string()) && e.from_agent != *id
191 } else {
192 false
193 }
194 })
195 .count();
196
197 let shared_dir = dirs::home_dir()
198 .unwrap_or_default()
199 .join(".lean-ctx")
200 .join("agents")
201 .join("shared");
202
203 let shared_count = if shared_dir.exists() {
204 std::fs::read_dir(&shared_dir)
205 .map(|rd| rd.count())
206 .unwrap_or(0)
207 } else {
208 0
209 };
210
211 let mut out = "Multi-Agent Sync Status:\n".to_string();
212 out.push_str(&format!(" Active agents: {}\n", agents.len()));
213 for a in &agents {
214 let role = a.role.as_deref().unwrap_or("-");
215 let age = (chrono::Utc::now() - a.last_active).num_minutes();
216 out.push_str(&format!(
217 " {} [{}] role={} ({}m ago)\n",
218 a.agent_id, a.agent_type, role, age
219 ));
220 }
221 out.push_str(&format!(" Pending messages: {pending_count}\n"));
222 out.push_str(&format!(" Shared contexts: {shared_count}\n"));
223 out
224 }
225
226 _ => format!("Unknown action: {action}. Use: register, list, post, read, status, info, handoff, sync"),
227 }
228}