Skip to main content

lean_ctx/tools/
ctx_agent.rs

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}