1use crate::core::agents::{AgentDiary, AgentRegistry, AgentStatus, DiaryEntryType};
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 Some(msg) = message else { return "Error: message is required for post".to_string() };
59 let cat = category.unwrap_or("status");
60 let from = current_agent_id.unwrap_or("anonymous");
61 let mut registry = AgentRegistry::load_or_create();
62 let msg_id = registry.post_message(from, to_agent, cat, msg);
63 match registry.save() {
64 Ok(()) => {
65 let target = to_agent.unwrap_or("all agents (broadcast)");
66 format!("Posted [{cat}] to {target}: {msg} (id: {msg_id})")
67 }
68 Err(e) => format!("Posted but save failed: {e}"),
69 }
70 }
71
72 "read" => {
73 let Some(agent_id) = current_agent_id else {
74 return "Error: agent must be registered first (use action=register)"
75 .to_string()
76 };
77 let mut registry = AgentRegistry::load_or_create();
78 let messages = registry.read_unread(agent_id);
79
80 if messages.is_empty() {
81 let _ = registry.save();
82 return "No new messages.".to_string();
83 }
84
85 let mut out = format!("New messages ({}):\n", messages.len());
86 for m in &messages {
87 let age = (chrono::Utc::now() - m.timestamp).num_minutes();
88 out.push_str(&format!(
89 " [{}] from {} ({}m ago): {}\n",
90 m.category, m.from_agent, age, m.message
91 ));
92 }
93 let _ = registry.save();
94 out
95 }
96
97 "status" => {
98 let Some(agent_id) = current_agent_id else { return "Error: agent must be registered first".to_string() };
99 let new_status = match status {
100 Some("active") => AgentStatus::Active,
101 Some("idle") => AgentStatus::Idle,
102 Some("finished") => AgentStatus::Finished,
103 Some(other) => {
104 return format!("Unknown status: {other}. Use: active, idle, finished")
105 }
106 None => return "Error: status value is required".to_string(),
107 };
108 let status_msg = message;
109
110 let mut registry = AgentRegistry::load_or_create();
111 registry.set_status(agent_id, new_status.clone(), status_msg);
112 match registry.save() {
113 Ok(()) => format!(
114 "Status updated: {} → {}{}",
115 agent_id,
116 new_status,
117 status_msg.map(|m| format!(" ({m})")).unwrap_or_default()
118 ),
119 Err(e) => format!("Status set but save failed: {e}"),
120 }
121 }
122
123 "info" => {
124 let registry = AgentRegistry::load_or_create();
125 let total = registry.agents.len();
126 let active = registry
127 .agents
128 .iter()
129 .filter(|a| a.status == AgentStatus::Active)
130 .count();
131 let messages = registry.scratchpad.len();
132 format!(
133 "Agent Registry: {total} total, {active} active, {messages} scratchpad entries\nLast updated: {}",
134 registry.updated_at.format("%Y-%m-%d %H:%M UTC")
135 )
136 }
137
138 "handoff" => {
139 let Some(from) = current_agent_id else { return "Error: agent must be registered first".to_string() };
140 let Some(target) = to_agent else { return "Error: to_agent is required for handoff".to_string() };
141 let summary = message.unwrap_or("(no summary provided)");
142
143 let mut registry = AgentRegistry::load_or_create();
144
145 registry.post_message(
146 from,
147 Some(target),
148 "handoff",
149 &format!("HANDOFF from {from}: {summary}"),
150 );
151
152 registry.set_status(from, AgentStatus::Finished, Some("handed off"));
153 let _ = registry.save();
154
155 format!("Handoff complete: {from} → {target}\nSummary: {summary}")
156 }
157
158 "sync" => {
159 let registry = AgentRegistry::load_or_create();
160 let agents: Vec<&crate::core::agents::AgentEntry> = registry
161 .agents
162 .iter()
163 .filter(|a| a.status != AgentStatus::Finished)
164 .collect();
165
166 if agents.is_empty() {
167 return "No active agents to sync with.".to_string();
168 }
169
170 let pending_count = registry
171 .scratchpad
172 .iter()
173 .filter(|e| {
174 if let Some(ref id) = current_agent_id {
175 !e.read_by.contains(&id.to_string()) && e.from_agent != *id
176 } else {
177 false
178 }
179 })
180 .count();
181
182 let shared_dir = crate::core::data_dir::lean_ctx_data_dir()
183 .unwrap_or_default()
184 .join("agents")
185 .join("shared");
186
187 let shared_count = if shared_dir.exists() {
188 std::fs::read_dir(&shared_dir)
189 .map_or(0, std::iter::Iterator::count)
190 } else {
191 0
192 };
193
194 let mut out = "Multi-Agent Sync Status:\n".to_string();
195 out.push_str(&format!(" Active agents: {}\n", agents.len()));
196 for a in &agents {
197 let role = a.role.as_deref().unwrap_or("-");
198 let age = (chrono::Utc::now() - a.last_active).num_minutes();
199 out.push_str(&format!(
200 " {} [{}] role={} ({}m ago)\n",
201 a.agent_id, a.agent_type, role, age
202 ));
203 }
204 out.push_str(&format!(" Pending messages: {pending_count}\n"));
205 out.push_str(&format!(" Shared contexts: {shared_count}\n"));
206 out
207 }
208
209 "diary" => {
210 let Some(agent_id) = current_agent_id else { return "Error: agent must be registered first".to_string() };
211 let Some(content) = message else { return "Error: message is required for diary entry".to_string() };
212 let entry_type = match category.unwrap_or("progress") {
213 "discovery" | "found" => DiaryEntryType::Discovery,
214 "decision" | "decided" => DiaryEntryType::Decision,
215 "blocker" | "blocked" => DiaryEntryType::Blocker,
216 "progress" | "done" => DiaryEntryType::Progress,
217 "insight" => DiaryEntryType::Insight,
218 other => return format!("Unknown diary type: {other}. Use: discovery, decision, blocker, progress, insight"),
219 };
220 let atype = agent_type.unwrap_or("unknown");
221 let mut diary = AgentDiary::load_or_create(agent_id, atype, project_root);
222 let context_str = to_agent;
223 diary.add_entry(entry_type.clone(), content, context_str);
224 match diary.save() {
225 Ok(()) => format!("Diary entry [{entry_type}] added: {content}"),
226 Err(e) => format!("Diary entry added but save failed: {e}"),
227 }
228 }
229
230 "recall_diary" | "diary_recall" => {
231 let Some(agent_id) = current_agent_id else {
232 let diaries = AgentDiary::list_all();
233 if diaries.is_empty() {
234 return "No agent diaries found.".to_string();
235 }
236 let mut out = format!("Agent Diaries ({}):\n", diaries.len());
237 for (id, count, updated) in &diaries {
238 let age = (chrono::Utc::now() - *updated).num_minutes();
239 out.push_str(&format!(" {id}: {count} entries ({age}m ago)\n"));
240 }
241 return out;
242 };
243 match AgentDiary::load(agent_id) {
244 Some(diary) => diary.format_summary(),
245 None => format!("No diary found for agent '{agent_id}'."),
246 }
247 }
248
249 "diaries" => {
250 let diaries = AgentDiary::list_all();
251 if diaries.is_empty() {
252 return "No agent diaries found.".to_string();
253 }
254 let mut out = format!("Agent Diaries ({}):\n", diaries.len());
255 for (id, count, updated) in &diaries {
256 let age = (chrono::Utc::now() - *updated).num_minutes();
257 out.push_str(&format!(" {id}: {count} entries ({age}m ago)\n"));
258 }
259 out
260 }
261
262 "share_knowledge" => {
263 let cat = category.unwrap_or("general");
264 let Some(msg_text) = message else { return "Error: message required (format: key1=value1;key2=value2)".to_string() };
265 let facts: Vec<(String, String)> = msg_text
266 .split(';')
267 .filter_map(|kv| {
268 let (k, v) = kv.split_once('=')?;
269 Some((k.trim().to_string(), v.trim().to_string()))
270 })
271 .collect();
272 if facts.is_empty() {
273 return "Error: no valid key=value pairs found".to_string();
274 }
275 let from = current_agent_id.unwrap_or("anonymous");
276 let mut registry = AgentRegistry::load_or_create();
277 registry.share_knowledge(from, cat, &facts);
278 match registry.save() {
279 Ok(()) => format!("Shared {} facts in category '{}'", facts.len(), cat),
280 Err(e) => format!("Share failed: {e}"),
281 }
282 }
283
284 "receive_knowledge" => {
285 let Some(agent_id) = current_agent_id else { return "Error: agent must be registered first".to_string() };
286 let mut registry = AgentRegistry::load_or_create();
287 let facts = registry.receive_shared_knowledge(agent_id);
288 let _ = registry.save();
289 if facts.is_empty() {
290 return "No new shared knowledge.".to_string();
291 }
292 let mut out = format!("Received {} facts:\n", facts.len());
293 for f in &facts {
294 let age = (chrono::Utc::now() - f.timestamp).num_minutes();
295 out.push_str(&format!(
296 " [{}] {}={} (from {}, {}m ago)\n",
297 f.category, f.key, f.value, f.from_agent, age
298 ));
299 }
300 out
301 }
302
303 _ => format!("Unknown action: {action}. Use: register, list, post, read, status, info, handoff, sync, diary, recall_diary, diaries, share_knowledge, receive_knowledge"),
304 }
305}