1use async_trait::async_trait;
18use serde_json::{json, Value};
19use std::collections::VecDeque;
20use std::sync::Arc;
21use tokio::sync::RwLock;
22
23use crate::error::{Error, Result};
24use crate::llm::ToolSpec;
25use crate::tasks::TaskRegistry;
26use crate::tools::{Tool, ToolSideEffect};
27
28#[derive(Clone, Default)]
37pub struct WorkerMailbox {
38 queue: Arc<tokio::sync::Mutex<VecDeque<String>>>,
39}
40
41impl WorkerMailbox {
42 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub async fn push(&self, msg: String) {
48 self.queue.lock().await.push_back(msg);
49 }
50
51 pub async fn pop(&self) -> Option<String> {
53 self.queue.lock().await.pop_front()
54 }
55
56 pub async fn drain_all(&self) -> Vec<String> {
58 let mut q = self.queue.lock().await;
59 q.drain(..).collect()
60 }
61
62 pub async fn len(&self) -> usize {
64 self.queue.lock().await.len()
65 }
66
67 pub async fn is_empty(&self) -> bool {
68 self.queue.lock().await.is_empty()
69 }
70}
71
72#[derive(Clone, Default)]
78pub struct WorkerRegistry {
79 inner: Arc<RwLock<std::collections::HashMap<String, WorkerMailbox>>>,
80}
81
82impl WorkerRegistry {
83 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub async fn register(&self, worker_id: &str) -> WorkerMailbox {
89 let mailbox = WorkerMailbox::new();
90 self.inner
91 .write()
92 .await
93 .insert(worker_id.to_string(), mailbox.clone());
94 mailbox
95 }
96
97 pub async fn deregister(&self, worker_id: &str) {
99 self.inner.write().await.remove(worker_id);
100 }
101
102 pub async fn get(&self, worker_id: &str) -> Option<WorkerMailbox> {
104 self.inner.read().await.get(worker_id).cloned()
105 }
106
107 pub async fn active_workers(&self) -> Vec<String> {
109 self.inner.read().await.keys().cloned().collect()
110 }
111}
112
113pub struct SendMessageTool {
120 registry: WorkerRegistry,
121 task_registry: Arc<TaskRegistry>,
122 workers: crate::tools::agent::WorkerTable,
127}
128
129impl SendMessageTool {
130 pub fn new(
131 registry: WorkerRegistry,
132 task_registry: Arc<TaskRegistry>,
133 workers: crate::tools::agent::WorkerTable,
134 ) -> Self {
135 Self {
136 registry,
137 task_registry,
138 workers,
139 }
140 }
141}
142
143#[async_trait]
144impl Tool for SendMessageTool {
145 fn is_deferred(&self) -> bool {
146 true
147 }
148
149 fn spec(&self) -> ToolSpec {
150 ToolSpec {
151 name: "send_message".into(),
152 description: concat!(
153 "Send a follow-up message to a running worker. Two addressing ",
154 "modes: 'task_id' (preferred, returned by task_create / agent ",
155 "tool) or 'worker_id' (legacy, from spawn_worker). Messages ",
156 "are queued and delivered to the worker on its next step."
157 )
158 .into(),
159 parameters: json!({
160 "type": "object",
161 "properties": {
162 "task_id": {
163 "type": "string",
164 "description": "The task ID (preferred, returned by task_create or the agent tool when run_in_background=true or team_name+name are set)."
165 },
166 "worker_id": {
167 "type": "string",
168 "description": "Legacy: the worker ID returned by spawn_worker."
169 },
170 "message": {
171 "type": "string",
172 "description": "The message to send to the worker."
173 }
174 },
175 "required": ["message"]
176 }),
177 }
178 }
179
180 fn side_effect_class(&self) -> ToolSideEffect {
181 ToolSideEffect::External
183 }
184
185 async fn execute(&self, arguments: Value) -> Result<String> {
186 let message = arguments["message"]
187 .as_str()
188 .ok_or_else(|| Error::BadToolArgs {
189 name: "send_message".into(),
190 message: "missing required parameter: message".to_string(),
191 })?
192 .to_string();
193
194 if let Some(task_id_str) = arguments.get("task_id").and_then(|v| v.as_str()) {
196 let task_id = crate::tasks::TaskId(task_id_str.to_string());
197 let task = self
198 .task_registry
199 .get(&task_id)
200 .await
201 .ok_or_else(|| Error::NotFound(format!("task '{task_id_str}'")))?;
202 let _ = task.output_tx.send(message.clone());
205
206 let workers = self.workers.lock().unwrap_or_else(|e| e.into_inner());
211 for handle in workers.values() {
212 if handle.task_id == task_id {
213 match handle.tx.send(message.clone()) {
214 Ok(()) => {
215 return Ok(format!(
216 "Message delivered to task '{task_id_str}'; worker will run it as a new turn."
217 ));
218 }
219 Err(_) => {
220 return Ok(format!(
221 "Message queued for task '{task_id_str}', but the worker has exited and cannot continue."
222 ));
223 }
224 }
225 }
226 }
227 return Ok(format!("Message delivered to task '{task_id_str}'."));
228 }
229
230 if let Some(worker_id) = arguments.get("worker_id").and_then(|v| v.as_str()) {
234 {
240 let workers = self.workers.lock().unwrap_or_else(|e| e.into_inner());
241 if let Some(handle) = workers.get(worker_id) {
242 match handle.tx.send(message.clone()) {
243 Ok(()) => {
244 return Ok(format!(
245 "Message delivered to worker '{worker_id}'; worker will run it as a new turn."
246 ));
247 }
248 Err(_) => {
249 return Ok(format!(
250 "Message queued for worker '{worker_id}', but it has exited and cannot continue."
251 ));
252 }
253 }
254 }
255 }
256
257 match self.registry.get(worker_id).await {
259 Some(mailbox) => {
260 mailbox.push(message).await;
261 return Ok(format!("Message delivered to worker '{worker_id}'."));
262 }
263 None => {
264 let active = self.registry.active_workers().await;
265 let tasks = self.task_registry.list().await;
266 if active.is_empty() && tasks.is_empty() {
267 return Ok(format!(
268 "Worker '{worker_id}' not found. No active workers or tasks currently registered."
269 ));
270 }
271 let mut sorted = active;
272 sorted.sort_unstable();
273 let task_ids: Vec<String> =
274 tasks.into_iter().map(|t| t.id.to_string()).collect();
275 return Ok(format!(
276 "Worker '{worker_id}' not found. Active workers: [{}]. Active task IDs: [{}].",
277 sorted.join(", "),
278 task_ids.join(", ")
279 ));
280 }
281 }
282 }
283
284 Err(Error::BadToolArgs {
286 name: "send_message".into(),
287 message: "must provide one of: task_id, worker_id".to_string(),
288 })
289 }
290}
291
292pub struct ListWorkersTool {
298 registry: WorkerRegistry,
299 task_registry: Arc<TaskRegistry>,
300}
301
302impl ListWorkersTool {
303 pub fn new(registry: WorkerRegistry, task_registry: Arc<TaskRegistry>) -> Self {
304 Self {
305 registry,
306 task_registry,
307 }
308 }
309}
310
311#[async_trait]
312impl Tool for ListWorkersTool {
313 fn spec(&self) -> ToolSpec {
314 ToolSpec {
315 name: "list_workers".into(),
316 description: concat!(
317 "List all currently active worker IDs and task IDs. Use this to ",
318 "discover peer workers and tasks you can communicate with via ",
319 "send_message."
320 )
321 .into(),
322 parameters: json!({
323 "type": "object",
324 "properties": {}
325 }),
326 }
327 }
328
329 fn side_effect_class(&self) -> ToolSideEffect {
330 ToolSideEffect::ReadOnly
331 }
332
333 async fn execute(&self, _arguments: Value) -> Result<String> {
334 let mut workers = self.registry.active_workers().await;
335 workers.sort_unstable();
336 let tasks = self.task_registry.list().await;
337 let mut out = String::new();
338 if workers.is_empty() {
339 out.push_str("Active workers: (none)\n");
340 } else {
341 out.push_str(&format!("Active workers ({}):\n", workers.len()));
342 for w in &workers {
343 out.push_str(&format!(" {w}\n"));
344 }
345 }
346 if tasks.is_empty() {
347 out.push_str("Active tasks: (none)\n");
348 } else {
349 out.push_str(&format!("Active tasks ({}):\n", tasks.len()));
350 for t in tasks {
351 let s = t.status().await;
352 out.push_str(&format!(" {} [{}] {}\n", t.id, s.as_str(), t.description));
353 }
354 }
355 Ok(out.trim_end().to_string())
356 }
357}
358
359#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::tasks::TaskState;
367
368 #[test]
369 fn send_message_tool_is_not_readonly() {
370 use crate::tools::Tool;
371 let reg = WorkerRegistry::new();
372 let tr = Arc::new(TaskRegistry::new());
373 let tool = SendMessageTool::new(reg, tr, crate::tools::agent::WorkerTable::default());
374 assert!(
375 !tool.is_readonly(),
376 "SendMessageTool must not be ReadOnly: it pushes messages to worker mailboxes"
377 );
378 assert_eq!(
379 tool.side_effect_class(),
380 super::ToolSideEffect::External,
381 "SendMessageTool side_effect_class must be External"
382 );
383 }
384
385 #[tokio::test]
386 async fn mailbox_push_pop() {
387 let mb = WorkerMailbox::new();
388 mb.push("hello".into()).await;
389 mb.push("world".into()).await;
390 assert_eq!(mb.len().await, 2);
391 assert_eq!(mb.pop().await, Some("hello".into()));
392 assert_eq!(mb.pop().await, Some("world".into()));
393 assert_eq!(mb.pop().await, None);
394 }
395
396 #[tokio::test]
397 async fn mailbox_drain_all() {
398 let mb = WorkerMailbox::new();
399 mb.push("a".into()).await;
400 mb.push("b".into()).await;
401 let msgs = mb.drain_all().await;
402 assert_eq!(msgs, vec!["a", "b"]);
403 assert!(mb.is_empty().await);
404 }
405
406 #[tokio::test]
407 async fn registry_register_and_get() {
408 let reg = WorkerRegistry::new();
409 reg.register("w1").await;
410 assert!(reg.get("w1").await.is_some());
411 assert!(reg.get("nonexistent").await.is_none());
412 }
413
414 #[tokio::test]
415 async fn registry_deregister() {
416 let reg = WorkerRegistry::new();
417 reg.register("w1").await;
418 reg.deregister("w1").await;
419 assert!(reg.get("w1").await.is_none());
420 }
421
422 #[tokio::test]
423 async fn send_message_delivers_to_worker() {
424 let reg = WorkerRegistry::new();
425 let tr = Arc::new(TaskRegistry::new());
426 let mailbox = reg.register("w1").await;
427
428 let tool = SendMessageTool::new(reg, tr, crate::tools::agent::WorkerTable::default());
429 let result = tool
430 .execute(json!({
431 "worker_id": "w1",
432 "message": "please add error handling"
433 }))
434 .await
435 .unwrap();
436
437 assert!(result.contains("w1"));
438 assert!(result.contains("delivered"));
439 assert_eq!(
440 mailbox.pop().await.as_deref(),
441 Some("please add error handling")
442 );
443 }
444
445 #[tokio::test]
446 async fn send_message_to_task() {
447 let reg = WorkerRegistry::new();
448 let tr = Arc::new(TaskRegistry::new());
449 let (state, id) = TaskState::new("t", "alpha", "r");
450 tr.register(state).await;
451
452 let tool = SendMessageTool::new(reg, tr, crate::tools::agent::WorkerTable::default());
453 let result = tool
454 .execute(json!({
455 "task_id": id.to_string(),
456 "message": "do thing"
457 }))
458 .await
459 .unwrap();
460 assert!(result.contains(&id.to_string()));
461 assert!(result.contains("delivered"));
462 }
463
464 #[tokio::test]
465 async fn send_message_unknown_task_errors() {
466 let reg = WorkerRegistry::new();
467 let tr = Arc::new(TaskRegistry::new());
468 let tool = SendMessageTool::new(reg, tr, crate::tools::agent::WorkerTable::default());
469 let res = tool
470 .execute(json!({
471 "task_id": "task-bogus",
472 "message": "hi"
473 }))
474 .await;
475 assert!(matches!(res, Err(Error::NotFound(_))));
476 }
477
478 #[tokio::test]
479 async fn send_message_requires_one_of_ids() {
480 let reg = WorkerRegistry::new();
481 let tr = Arc::new(TaskRegistry::new());
482 let tool = SendMessageTool::new(reg, tr, crate::tools::agent::WorkerTable::default());
483 let res = tool.execute(json!({ "message": "hi" })).await;
484 assert!(matches!(res, Err(Error::BadToolArgs { .. })));
485 }
486
487 #[tokio::test]
488 async fn send_message_unknown_worker() {
489 let reg = WorkerRegistry::new();
490 let tr = Arc::new(TaskRegistry::new());
491 let tool = SendMessageTool::new(reg, tr, crate::tools::agent::WorkerTable::default());
492 let result = tool
493 .execute(json!({"worker_id": "ghost", "message": "hi"}))
494 .await
495 .unwrap();
496 assert!(result.contains("ghost"));
497 assert!(result.contains("not found"));
498 }
499
500 #[tokio::test]
501 async fn send_message_shows_active_workers() {
502 let reg = WorkerRegistry::new();
503 let tr = Arc::new(TaskRegistry::new());
504 reg.register("active-1").await;
505 let tool = SendMessageTool::new(reg, tr, crate::tools::agent::WorkerTable::default());
506 let result = tool
507 .execute(json!({"worker_id": "missing", "message": "hello"}))
508 .await
509 .unwrap();
510 assert!(result.contains("active-1"));
511 }
512
513 #[tokio::test]
514 async fn list_workers_includes_tasks() {
515 let reg = WorkerRegistry::new();
516 let tr = Arc::new(TaskRegistry::new());
517 reg.register("w1").await;
518 let (state, id) = TaskState::new("t", "alpha", "r");
519 tr.register(state).await;
520 let tool = ListWorkersTool::new(reg, tr);
521 let out = tool.execute(json!({})).await.unwrap();
522 assert!(out.contains("w1"));
523 assert!(out.contains(&id.to_string()));
524 }
525
526 #[tokio::test]
529 async fn mailbox_is_empty_false_when_nonempty() {
530 let mb = WorkerMailbox::new();
532 assert!(mb.is_empty().await, "fresh mailbox must be empty");
533 mb.push("msg".into()).await;
534 assert!(
535 !mb.is_empty().await,
536 "mailbox with a message must NOT be empty"
537 );
538 }
539
540 #[test]
541 fn send_message_tool_is_deferred() {
542 let reg = WorkerRegistry::new();
544 let tr = Arc::new(TaskRegistry::new());
545 let tool = SendMessageTool::new(reg, tr, crate::tools::agent::WorkerTable::default());
546 assert!(
547 tool.is_deferred(),
548 "SendMessageTool must be deferred (depends on available workers)"
549 );
550 }
551
552 #[tokio::test]
553 async fn registry_active_workers_lists_all_registered() {
554 let reg = WorkerRegistry::new();
556 reg.register("w-alpha").await;
557 reg.register("w-beta").await;
558 let workers = reg.active_workers().await;
559 assert!(
560 workers.contains(&"w-alpha".to_string()),
561 "active_workers must include w-alpha"
562 );
563 assert!(
564 workers.contains(&"w-beta".to_string()),
565 "active_workers must include w-beta"
566 );
567 assert_eq!(workers.len(), 2, "expected exactly 2 active workers");
568 }
569}