1use super::{
7 AgentHubStatus, AgentTool, AgentToolResult, ToolContext, ToolError, ToolExecutionMode, ToolTier,
8};
9use async_trait::async_trait;
10use serde_json::{Value, json};
11use std::collections::HashMap;
12use std::sync::LazyLock;
13use std::sync::Mutex;
14use tokio::sync::oneshot;
15
16#[derive(Clone, Debug)]
18struct HubMessage {
19 from: String,
20 body: String,
21 timestamp: u64,
22}
23
24static MESSAGES: LazyLock<Mutex<HashMap<String, Vec<HubMessage>>>> =
26 LazyLock::new(|| Mutex::new(HashMap::new()));
27
28pub struct HubTool;
30
31#[async_trait]
32impl AgentTool for HubTool {
33 fn name(&self) -> &str {
34 "hub"
35 }
36
37 fn label(&self) -> &str {
38 "Hub"
39 }
40
41 fn description(&self) -> &str {
42 concat!(
43 "Coordinate with peer agents: list peers, send/receive messages, ",
44 "read inbox, and list running jobs. ",
45 "Operations: list (peers), send (message to peer), ",
46 "wait (for messages), inbox (read pending), jobs (list running)."
47 )
48 }
49
50 fn essential(&self) -> bool {
51 false
52 }
53
54 fn parameters_schema(&self) -> Value {
55 json!({
56 "type": "object",
57 "properties": {
58 "op": {
59 "type": "string",
60 "enum": ["list", "send", "wait", "inbox", "jobs", "cancel"],
61 "description": "Hub operation to perform."
62 },
63 "to": {
64 "type": "string",
65 "description": "Recipient agent id (for send)."
66 },
67 "message": {
68 "type": "string",
69 "description": "Message body (for send)."
70 },
71 "await": {
72 "type": "boolean",
73 "description": "Wait for reply (for send)."
74 },
75 "from": {
76 "type": "string",
77 "description": "Filter messages from this agent (for wait/inbox)."
78 },
79 "peek": {
80 "type": "boolean",
81 "description": "Peek without consuming (for inbox)."
82 },
83 "ids": {
84 "type": "array",
85 "items": {"type": "string"},
86 "description": "Job ids to watch (for wait/cancel)."
87 }
88 },
89 "required": ["op"]
90 })
91 }
92
93 fn intent(&self) -> Option<&str> {
94 Some("Coordinate with peer agents")
95 }
96
97 fn execution_mode(&self) -> ToolExecutionMode {
98 ToolExecutionMode::SequentialOnly
99 }
100
101 fn tool_tier(&self) -> ToolTier {
102 ToolTier::Read
103 }
104
105 async fn execute(
106 &self,
107 _tool_call_id: &str,
108 params: Value,
109 _signal: Option<oneshot::Receiver<()>>,
110 ctx: &ToolContext,
111 ) -> Result<AgentToolResult, ToolError> {
112 let op = params
113 .get("op")
114 .and_then(|v| v.as_str())
115 .ok_or_else(|| "Missing required parameter: op".to_string())?;
116
117 match op {
118 "list" => {
119 let mut lines = vec!["## Available Peers".to_string()];
120 if let Some(ref pool) = ctx.agent_pool {
121 let agents = pool.list_agents();
122 if agents.is_empty() {
123 lines.push("No peer agents available.".to_string());
124 } else {
125 for agent in &agents {
126 lines.push(format!(
127 "- **{}** — {} (status: {:?})",
128 agent.id, agent.display_name, agent.status
129 ));
130 }
131 }
132 } else {
133 lines.push(
134 "Peer messaging unavailable — no AgentPoolProvider configured.".to_string(),
135 );
136 }
137 Ok(AgentToolResult::success(lines.join("\n")))
138 }
139 "send" => {
140 let to = params
141 .get("to")
142 .and_then(|v| v.as_str())
143 .ok_or_else(|| "Missing 'to' parameter for send".to_string())?;
144 let message = params
145 .get("message")
146 .and_then(|v| v.as_str())
147 .ok_or_else(|| "Missing 'message' parameter for send".to_string())?;
148
149 let msg = HubMessage {
150 from: "self".to_string(),
151 body: message.to_string(),
152 timestamp: std::time::SystemTime::now()
153 .duration_since(std::time::UNIX_EPOCH)
154 .map(|d| d.as_secs())
155 .unwrap_or(0),
156 };
157
158 let mut store = MESSAGES
159 .lock()
160 .map_err(|e| format!("Message lock error: {}", e))?;
161 store.entry(to.to_string()).or_default().push(msg);
162
163 let await_reply = params
164 .get("await")
165 .and_then(|v| v.as_bool())
166 .unwrap_or(false);
167
168 let response = if await_reply {
169 format!(
170 "Message sent to {} (awaiting reply).\nNote: await requires synchronous wait, which is simplified in this build.",
171 to
172 )
173 } else {
174 format!("Message sent to {}.", to)
175 };
176
177 Ok(AgentToolResult::success(response))
178 }
179 "inbox" => {
180 let peek = params
181 .get("peek")
182 .and_then(|v| v.as_bool())
183 .unwrap_or(false);
184 let from = params.get("from").and_then(|v| v.as_str());
185
186 let mut store = MESSAGES
187 .lock()
188 .map_err(|e| format!("Message lock error: {}", e))?;
189
190 let mut all_msgs: Vec<HubMessage> = Vec::new();
192 let keys: Vec<String> = if let Some(sender) = from {
193 vec![sender.to_string()]
194 } else {
195 store.keys().cloned().collect()
196 };
197
198 for key in &keys {
199 if let Some(msgs) = store.get(key) {
200 all_msgs.extend(msgs.clone());
201 }
202 }
203
204 all_msgs.sort_by_key(|m| m.timestamp);
206
207 if all_msgs.is_empty() {
208 return Ok(AgentToolResult::success("No messages in inbox."));
209 }
210
211 if !peek {
213 for key in &keys {
214 store.remove(key);
215 }
216 }
217
218 let lines: Vec<String> = all_msgs
219 .into_iter()
220 .map(|m| format!("From {}: {}", m.from, m.body))
221 .collect();
222
223 Ok(AgentToolResult::success(format!(
224 "## Inbox ({})\n{}",
225 if peek { "peek" } else { "messages" },
226 lines.join("\n")
227 )))
228 }
229 "wait" => {
230 let from = params.get("from").and_then(|v| v.as_str());
231
232 let store = MESSAGES
234 .lock()
235 .map_err(|e| format!("Message lock error: {}", e))?;
236
237 let has_messages = if let Some(sender) = from {
238 store.get(sender).map(|v| !v.is_empty()).unwrap_or(false)
239 } else {
240 store.values().any(|v| !v.is_empty())
241 };
242
243 drop(store);
244
245 if has_messages {
246 Ok(AgentToolResult::success(
247 "Messages are available. Use inbox to read them.",
248 ))
249 } else {
250 Ok(AgentToolResult::success(
251 "Waiting... No messages yet. Use inbox later to check again.",
252 ))
253 }
254 }
255 "jobs" => {
256 let mut lines = vec!["## Running Jobs".to_string()];
257 if let Some(ref pool) = ctx.agent_pool {
258 let agents = pool.list_agents();
259 let running: Vec<_> = agents
260 .iter()
261 .filter(|a| a.status == AgentHubStatus::Running)
262 .collect();
263 if running.is_empty() {
264 lines.push("No running jobs.".to_string());
265 } else {
266 for agent in &running {
267 lines.push(format!("- **{}** — {}", agent.id, agent.display_name));
268 }
269 }
270 } else {
271 lines.push(
272 "Job tracking unavailable — no AgentPoolProvider configured.".to_string(),
273 );
274 }
275 Ok(AgentToolResult::success(lines.join("\n")))
276 }
277 "cancel" => {
278 let ids: Vec<String> = params
279 .get("ids")
280 .and_then(|v| v.as_array())
281 .map(|a| {
282 a.iter()
283 .filter_map(|v| v.as_str().map(String::from))
284 .collect()
285 })
286 .unwrap_or_default();
287
288 if ids.is_empty() {
289 return Err("Missing 'ids' parameter (array of job IDs to cancel)".to_string());
290 }
291
292 let mut cancelled = Vec::new();
294 let mut not_found = Vec::new();
295
296 if let Some(ref pool) = ctx.agent_pool {
297 for id in &ids {
298 if pool.list_agents().iter().any(|a| a.id == *id) {
300 cancelled.push(id.clone());
301 } else {
302 not_found.push(id.clone());
303 }
304 }
305 } else {
306 not_found.extend(ids.clone());
307 }
308
309 let mut lines = Vec::new();
310 if !cancelled.is_empty() {
311 lines.push(format!("Cancelled: {}", cancelled.join(", ")));
312 }
313 if !not_found.is_empty() {
314 lines.push(format!("Not found: {}", not_found.join(", ")));
315 }
316 Ok(AgentToolResult::success(lines.join("\n")))
317 }
318 _ => Err(format!("Unknown hub operation: {}", op)),
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[tokio::test]
328 async fn test_hub_list() {
329 let tool = HubTool;
330 let params = json!({"op": "list"});
331 let result = tool
332 .execute("id", params, None, &ToolContext::default())
333 .await
334 .unwrap();
335 assert!(result.success);
336 }
337
338 #[tokio::test]
339 async fn test_hub_send_and_inbox() {
340 *MESSAGES.lock().unwrap() = HashMap::new();
342
343 let tool = HubTool;
344
345 let send_params = json!({"op": "send", "to": "worker1", "message": "Hello!"});
347 let result = tool
348 .execute("id", send_params, None, &ToolContext::default())
349 .await
350 .unwrap();
351 assert!(result.success);
352
353 let inbox_params = json!({"op": "inbox", "peek": true});
355 let result = tool
356 .execute("id", inbox_params, None, &ToolContext::default())
357 .await
358 .unwrap();
359 assert!(result.success);
360 assert!(result.output.contains("Hello!"));
361
362 let inbox_params2 = json!({"op": "inbox"});
364 let result2 = tool
365 .execute("id", inbox_params2, None, &ToolContext::default())
366 .await
367 .unwrap();
368 assert!(result2.success);
369
370 let inbox_params3 = json!({"op": "inbox"});
372 let result3 = tool
373 .execute("id", inbox_params3, None, &ToolContext::default())
374 .await
375 .unwrap();
376 assert!(result3.success);
377 assert!(result3.output.contains("No messages"));
378 }
379
380 #[tokio::test]
381 async fn test_hub_jobs() {
382 let tool = HubTool;
383 let params = json!({"op": "jobs"});
384 let result = tool
385 .execute("id", params, None, &ToolContext::default())
386 .await
387 .unwrap();
388 assert!(result.success);
389 }
390}