Skip to main content

agent_works/multi_agent/
mailbox.rs

1//! Asynchronous message passing between agents.
2//!
3//! The [`MailboxHub`] manages per-agent mailboxes and a global sequence number
4//! (`tokio::sync::watch<u64>`) that notifies waiters when any result arrives.
5//!
6//! # Architecture
7//!
8//! ```text
9//! Parent (LLM tools)                      Child (AgentRuntime task)
10//!        │                                         │
11//!        │  send_task() / send_message()           │
12//!        ├────────────────────────────────────────►│ task_rx
13//!        │                                         │
14//!        │                      post_result()      │
15//!        │◄────────────────────────────────────────┤ (via result_tx clone)
16//!        │                                         │
17//!        │  wait_for_result() watches seq_rx       │
18//!        │  (blocks until seq changes)             │
19//! ```
20//!
21//! Every `post_result()` increments the global sequence number, waking all
22//! `wait_for_result()` callers.
23
24use std::collections::HashMap;
25use std::sync::Mutex;
26
27use tokio::sync::{mpsc, watch};
28
29use super::path::AgentPath;
30
31// ---------------------------------------------------------------------------
32// Types
33// ---------------------------------------------------------------------------
34
35/// A task sent from parent to child agent.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct MailboxTask {
38    /// The task description / user input for the child agent.
39    pub task: String,
40    /// Whether to interrupt the child's current execution.
41    pub interrupt: bool,
42    /// Pending messages accumulated before this task (from `send_message`).
43    pub pending_messages: Vec<String>,
44}
45
46/// Status of a result posted from child to parent.
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum MailboxStatus {
49    /// Child agent completed its task successfully.
50    Ok,
51    /// Child agent encountered an error.
52    Error,
53    /// Child agent was closed.
54    Closed,
55}
56
57/// A result posted from child agent to parent.
58#[derive(Clone, Debug)]
59pub struct MailboxResult {
60    /// Which agent produced this result.
61    pub agent_path: AgentPath,
62    /// The status of the result.
63    pub status: MailboxStatus,
64    /// The result text (if any).
65    pub result: Option<String>,
66}
67
68// ---------------------------------------------------------------------------
69// Per-agent mailbox handle (child side)
70// ---------------------------------------------------------------------------
71
72/// The child-side handle to a mailbox.
73///
74/// Given to the spawned child agent task. The child reads tasks from `task_rx`
75/// and posts results via `hub.post_result()` using the agent path.
76#[derive(Debug)]
77pub struct ChildMailbox {
78    /// Receive tasks from parent.
79    pub task_rx: mpsc::Receiver<MailboxTask>,
80}
81
82// ---------------------------------------------------------------------------
83// Per-agent mailbox (internal)
84// ---------------------------------------------------------------------------
85
86struct MailboxEntry {
87    /// Send tasks to child.
88    task_tx: mpsc::Sender<MailboxTask>,
89    /// Results received from child (or posted by runtime).
90    results: Vec<MailboxResult>,
91    /// Pending messages (from `send_message`, no execution trigger).
92    pending: Vec<String>,
93}
94
95// ---------------------------------------------------------------------------
96// MailboxHub
97// ---------------------------------------------------------------------------
98
99/// Central hub for inter-agent message passing.
100///
101/// Manages per-agent mailboxes and a global sequence number. The sequence
102/// number increments every time a result is posted, allowing `wait_for_result`
103/// to efficiently block until new data arrives.
104///
105/// All methods use internal `Mutex` — the hub is designed to be shared
106/// via `Arc<MailboxHub>`.
107pub struct MailboxHub {
108    entries: Mutex<HashMap<AgentPath, MailboxEntry>>,
109    seq_tx: watch::Sender<u64>,
110    seq_rx: watch::Receiver<u64>,
111}
112
113impl MailboxHub {
114    /// Create a new empty mailbox hub.
115    pub fn new() -> Self {
116        let (seq_tx, seq_rx) = watch::channel(0);
117        Self {
118            entries: Mutex::new(HashMap::new()),
119            seq_tx,
120            seq_rx,
121        }
122    }
123
124    /// Register a new agent mailbox.
125    ///
126    /// Returns the child-side handle to be given to the spawned agent task.
127    /// Returns `None` if the agent_path is already registered.
128    pub fn register(&self, agent_path: &AgentPath) -> Option<ChildMailbox> {
129        let mut entries = self.entries.lock().unwrap();
130        if entries.contains_key(agent_path) {
131            return None;
132        }
133        let (task_tx, task_rx) = mpsc::channel(32);
134        entries.insert(
135            agent_path.clone(),
136            MailboxEntry {
137                task_tx,
138                results: Vec::new(),
139                pending: Vec::new(),
140            },
141        );
142        Some(ChildMailbox { task_rx })
143    }
144
145    /// Unregister an agent mailbox.
146    ///
147    /// Posts a `Closed` result first (to wake any waiters), then removes the entry.
148    /// Returns `true` if the agent was registered.
149    pub fn unregister(&self, agent_path: &AgentPath) -> bool {
150        let mut entries = self.entries.lock().unwrap();
151        if entries.get(agent_path).is_some() {
152            // Wake waiters with sequence bump
153            let current = *self.seq_rx.borrow();
154            let _ = self.seq_tx.send(current.wrapping_add(1));
155            entries.remove(agent_path);
156            true
157        } else {
158            false
159        }
160    }
161
162    /// Send a message to a sub-agent (no execution trigger).
163    ///
164    /// The message is appended to the agent's pending message buffer.
165    /// Returns `true` if the message was queued, `false` if the agent is not registered.
166    pub fn send_message(&self, agent_path: &AgentPath, message: String) -> bool {
167        let mut entries = self.entries.lock().unwrap();
168        match entries.get_mut(agent_path) {
169            Some(entry) => {
170                entry.pending.push(message);
171                true
172            }
173            None => false,
174        }
175    }
176
177    /// Send a task to a sub-agent (triggers execution).
178    ///
179    /// Drains pending messages and packages them with the task.
180    /// Returns `true` if the task was sent, `false` if the agent is not registered
181    /// or the channel is full.
182    pub fn send_task(&self, agent_path: &AgentPath, task: String, interrupt: bool) -> bool {
183        let mut entries = self.entries.lock().unwrap();
184        match entries.get_mut(agent_path) {
185            Some(entry) => {
186                let pending = std::mem::take(&mut entry.pending);
187                let mailbox_task = MailboxTask {
188                    task,
189                    interrupt,
190                    pending_messages: pending,
191                };
192                entry.task_tx.try_send(mailbox_task).is_ok()
193            }
194            None => false,
195        }
196    }
197
198    /// Check if an agent has pending (unread) messages.
199    pub fn has_pending(&self, agent_path: &AgentPath) -> bool {
200        let entries = self.entries.lock().unwrap();
201        entries
202            .get(agent_path)
203            .map(|e| !e.pending.is_empty())
204            .unwrap_or(false)
205    }
206
207    /// Post a result from a child agent.
208    ///
209    /// Increments the global sequence number, waking all `wait_for_result` callers.
210    pub fn post_result(&self, result: MailboxResult) {
211        let mut entries = self.entries.lock().unwrap();
212        if let Some(entry) = entries.get_mut(&result.agent_path) {
213            entry.results.push(result);
214            // Notify waiters
215            let current = *self.seq_rx.borrow();
216            let _ = self.seq_tx.send(current.wrapping_add(1));
217        }
218    }
219
220    /// Get a clone of the global sequence number receiver.
221    ///
222    /// Used by `wait_agent` to watch for changes before polling.
223    pub fn subscribe_seq(&self) -> watch::Receiver<u64> {
224        self.seq_rx.clone()
225    }
226
227    /// Try to receive a result for a specific agent (non-blocking).
228    ///
229    /// Returns the oldest unread result for the agent, or `None`.
230    pub fn try_recv_result(&self, agent_path: &AgentPath) -> Option<MailboxResult> {
231        let mut entries = self.entries.lock().unwrap();
232        entries.get_mut(agent_path).and_then(|e| {
233            if e.results.is_empty() {
234                None
235            } else {
236                Some(e.results.remove(0))
237            }
238        })
239    }
240
241    /// Try to receive any result (non-blocking).
242    ///
243    /// Returns the first available result from any agent mailbox.
244    pub fn try_recv_any(&self) -> Option<MailboxResult> {
245        let mut entries = self.entries.lock().unwrap();
246        for entry in entries.values_mut() {
247            if !entry.results.is_empty() {
248                return Some(entry.results.remove(0));
249            }
250        }
251        None
252    }
253
254    /// Check if an agent has unread results.
255    pub fn has_results(&self, agent_path: &AgentPath) -> bool {
256        let entries = self.entries.lock().unwrap();
257        entries
258            .get(agent_path)
259            .map(|e| !e.results.is_empty())
260            .unwrap_or(false)
261    }
262
263    /// Return the total number of unread results across all agents.
264    pub fn total_pending_results(&self) -> usize {
265        let entries = self.entries.lock().unwrap();
266        entries.values().map(|e| e.results.len()).sum()
267    }
268
269    /// Check if an agent is registered.
270    pub fn contains(&self, agent_path: &AgentPath) -> bool {
271        let entries = self.entries.lock().unwrap();
272        entries.contains_key(agent_path)
273    }
274
275    /// Return the number of registered agents.
276    pub fn len(&self) -> usize {
277        let entries = self.entries.lock().unwrap();
278        entries.len()
279    }
280
281    /// Return whether there are no registered agents.
282    pub fn is_empty(&self) -> bool {
283        self.len() == 0
284    }
285
286    /// Return all registered agent paths.
287    pub fn agent_paths(&self) -> Vec<AgentPath> {
288        let entries = self.entries.lock().unwrap();
289        entries.keys().cloned().collect()
290    }
291}
292
293impl Default for MailboxHub {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299// ---------------------------------------------------------------------------
300// Tests
301// ---------------------------------------------------------------------------
302
303#[cfg(test)]
304mod tests {
305    use std::sync::Arc;
306
307    use super::*;
308
309    fn test_path(name: &str) -> AgentPath {
310        AgentPath::root().join(name)
311    }
312
313    #[test]
314    fn register_and_unregister() {
315        let hub = MailboxHub::new();
316        let path = test_path("test-agent");
317
318        assert!(!hub.contains(&path));
319        assert_eq!(hub.len(), 0);
320
321        let child = hub.register(&path);
322        assert!(child.is_some());
323        assert!(hub.contains(&path));
324        assert_eq!(hub.len(), 1);
325
326        // Duplicate register fails
327        assert!(hub.register(&path).is_none());
328
329        assert!(hub.unregister(&path));
330        assert!(!hub.contains(&path));
331        assert_eq!(hub.len(), 0);
332
333        // Double unregister is a no-op
334        assert!(!hub.unregister(&path));
335    }
336
337    #[test]
338    fn send_message_and_task() {
339        let hub = MailboxHub::new();
340        let path = test_path("worker");
341
342        let mut child = hub.register(&path).unwrap();
343
344        // Messages accumulate without triggering
345        assert!(hub.send_message(&path, "hello".into()));
346        assert!(hub.send_message(&path, "world".into()));
347        assert!(hub.has_pending(&path));
348
349        // Send to non-existent agent
350        assert!(!hub.send_message(&test_path("ghost"), "nope".into()));
351
352        // Task drains pending
353        assert!(hub.send_task(&path, "do work".into(), true));
354        assert!(!hub.has_pending(&path));
355
356        // Child receives task with pending messages
357        let received = child.task_rx.try_recv().unwrap();
358        assert_eq!(received.task, "do work");
359        assert!(received.interrupt);
360        assert_eq!(received.pending_messages, vec!["hello", "world"]);
361    }
362
363    #[test]
364    fn post_and_receive_result() {
365        let hub = MailboxHub::new();
366        let path = test_path("worker");
367
368        hub.register(&path);
369
370        hub.post_result(MailboxResult {
371            agent_path: path.clone(),
372            status: MailboxStatus::Ok,
373            result: Some("done!".into()),
374        });
375
376        assert!(hub.has_results(&path));
377
378        let received = hub.try_recv_result(&path);
379        assert!(received.is_some());
380        let r = received.unwrap();
381        assert_eq!(r.agent_path, path);
382        assert_eq!(r.status, MailboxStatus::Ok);
383        assert_eq!(r.result.unwrap(), "done!");
384
385        assert!(!hub.has_results(&path));
386    }
387
388    #[test]
389    fn try_recv_any_returns_all() {
390        let hub = MailboxHub::new();
391        let a = test_path("a");
392        let b = test_path("b");
393
394        hub.register(&a);
395        hub.register(&b);
396
397        hub.post_result(MailboxResult {
398            agent_path: a.clone(),
399            status: MailboxStatus::Ok,
400            result: Some("first".into()),
401        });
402        hub.post_result(MailboxResult {
403            agent_path: b.clone(),
404            status: MailboxStatus::Error,
405            result: Some("second".into()),
406        });
407
408        // HashMap iteration order is non-deterministic, so just check we get both
409        let r1 = hub.try_recv_any().unwrap();
410        let r2 = hub.try_recv_any().unwrap();
411        assert!(hub.try_recv_any().is_none());
412
413        let mut paths = vec![r1.agent_path.to_string(), r2.agent_path.to_string()];
414        paths.sort();
415        assert_eq!(paths, vec!["root/a", "root/b"]);
416    }
417
418    #[test]
419    fn sequence_number_changes_on_post() {
420        let hub = MailboxHub::new();
421        let path = test_path("worker");
422        hub.register(&path);
423
424        let seq = hub.subscribe_seq();
425        let initial = *seq.borrow();
426
427        hub.post_result(MailboxResult {
428            agent_path: path.clone(),
429            status: MailboxStatus::Ok,
430            result: None,
431        });
432
433        assert!(seq.has_changed().unwrap());
434        assert_ne!(*seq.borrow(), initial);
435    }
436
437    #[test]
438    fn sequence_number_changes_on_unregister() {
439        let hub = MailboxHub::new();
440        let path = test_path("worker");
441        hub.register(&path);
442
443        let seq = hub.subscribe_seq();
444        let initial = *seq.borrow();
445
446        hub.unregister(&path);
447
448        assert!(seq.has_changed().unwrap());
449        assert_ne!(*seq.borrow(), initial);
450    }
451
452    #[test]
453    fn agent_paths() {
454        let hub = MailboxHub::new();
455        hub.register(&test_path("a"));
456        hub.register(&test_path("b"));
457
458        let mut paths = hub.agent_paths();
459        paths.sort();
460        assert_eq!(paths.len(), 2);
461    }
462
463    #[test]
464    fn total_pending_results() {
465        let hub = MailboxHub::new();
466        let a = test_path("a");
467        hub.register(&a);
468
469        assert_eq!(hub.total_pending_results(), 0);
470
471        hub.post_result(MailboxResult {
472            agent_path: a.clone(),
473            status: MailboxStatus::Ok,
474            result: None,
475        });
476        assert_eq!(hub.total_pending_results(), 1);
477
478        hub.post_result(MailboxResult {
479            agent_path: a.clone(),
480            status: MailboxStatus::Ok,
481            result: None,
482        });
483        assert_eq!(hub.total_pending_results(), 2);
484
485        hub.try_recv_any();
486        assert_eq!(hub.total_pending_results(), 1);
487    }
488
489    #[tokio::test]
490    async fn wait_for_result_pattern() {
491        let hub = Arc::new(MailboxHub::new());
492        let path = test_path("worker");
493        hub.register(&path);
494
495        let hub_clone = hub.clone();
496        let path_clone = path.clone();
497
498        // Spawn a task that posts a result after a short delay
499        tokio::spawn(async move {
500            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
501            hub_clone.post_result(MailboxResult {
502                agent_path: path_clone,
503                status: MailboxStatus::Ok,
504                result: Some("async result".into()),
505            });
506        });
507
508        // Wait for result using the seq pattern
509        let mut seq = hub.subscribe_seq();
510        loop {
511            match hub.try_recv_any() {
512                Some(r) => {
513                    assert_eq!(r.status, MailboxStatus::Ok);
514                    assert_eq!(r.result.unwrap(), "async result");
515                    break;
516                }
517                None => {
518                    let _ = seq.changed().await;
519                }
520            }
521        }
522    }
523}