1use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9
10use agent_base::{
11 AgentBuilder, AgentResult, AgentRuntime, DenyAllApprovalHandler, Language, LlmClient,
12 RunOutcome, RuntimeEvent, SessionId, Tool, UserEvent,
13};
14use tokio::task::JoinSet;
15use tokio_util::sync::CancellationToken;
16
17use super::config::MultiAgentConfig;
18use super::mailbox::{ChildMailbox, MailboxHub, MailboxResult, MailboxStatus, MailboxTask};
19use super::path::AgentPath;
20use super::registry::{AgentRegistry, AgentStatus};
21
22pub struct MultiAgentRuntime {
32 registry: Mutex<AgentRegistry>,
34
35 mailbox: Arc<MailboxHub>,
37
38 client: Arc<dyn LlmClient>,
40
41 business_tools: Vec<Arc<dyn Tool>>,
43
44 event_tx: Mutex<Option<tokio::sync::mpsc::UnboundedSender<RuntimeEvent>>>,
46
47 root_cancel: CancellationToken,
49
50 join_set: Mutex<JoinSet<()>>,
52
53 child_cancels: Mutex<HashMap<AgentPath, CancellationToken>>,
55
56 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
58
59 language: Language,
61}
62
63impl MultiAgentRuntime {
64 pub fn new(
68 config: MultiAgentConfig,
69 client: Arc<dyn LlmClient>,
70 business_tools: Vec<Arc<dyn Tool>>,
71 root_cancel: CancellationToken,
72 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
73 language: Language,
74 ) -> Self {
75 Self {
76 registry: Mutex::new(AgentRegistry::new(config)),
77 mailbox: Arc::new(MailboxHub::new()),
78 client,
79 business_tools,
80 event_tx: Mutex::new(None),
81 root_cancel,
82 join_set: Mutex::new(JoinSet::new()),
83 child_cancels: Mutex::new(HashMap::new()),
84 error_recovery,
85 language,
86 }
87 }
88
89 pub fn set_event_sender(&self, tx: tokio::sync::mpsc::UnboundedSender<RuntimeEvent>) {
93 *self.event_tx.lock().unwrap() = Some(tx);
94 }
95
96 pub async fn spawn_child(
110 &self,
111 name: &str,
112 system_prompt: String,
113 depth: i32,
114 tool_count: usize,
115 ) -> Result<String, String> {
116 let path = AgentPath::root().join(name);
117
118 {
120 let mut registry = self.registry.lock().unwrap();
121 registry.can_spawn(depth).map_err(|e| e.to_string())?;
122 registry
123 .register(&path, depth, tool_count)
124 .map_err(|e| e.to_string())?;
125 }
126
127 let child_mailbox = self
129 .mailbox
130 .register(&path)
131 .ok_or_else(|| "mailbox already exists".to_string())?;
132
133 let child_runtime = self.build_child_runtime(system_prompt).map_err(|e| {
135 self.registry.lock().unwrap().close(&path);
136 self.mailbox.unregister(&path);
137 format!("failed to build child runtime: {}", e)
138 })?;
139
140 let session_id = child_runtime.create_session().await;
142
143 let child_cancel = self.root_cancel.child_token();
145 {
146 let mut cancels = self.child_cancels.lock().unwrap();
147 cancels.insert(path.clone(), child_cancel.clone());
148 }
149
150 let agent_path = path.clone();
152 let mailbox_for_task = self.mailbox.clone();
153 let mailbox_for_close = self.mailbox.clone();
154 let event_tx = self.event_tx.lock().unwrap().clone();
155 let registry_agent_path = path.clone();
156
157 self.join_set.lock().unwrap().spawn(async move {
158 run_child_loop(
159 child_mailbox,
160 child_runtime,
161 session_id,
162 agent_path.clone(),
163 mailbox_for_task,
164 event_tx,
165 child_cancel,
166 )
167 .await;
168
169 mailbox_for_close.post_result(MailboxResult {
171 agent_path,
172 status: MailboxStatus::Closed,
173 result: None,
174 });
175 });
176
177 self.registry
178 .lock()
179 .unwrap()
180 .set_status(®istry_agent_path, AgentStatus::Idle);
181
182 Ok(path.to_string())
183 }
184
185 pub fn send_message(&self, agent_path: &str, message: String) -> Result<bool, String> {
189 let path = self.parse_path(agent_path)?;
190 Ok(self.mailbox.send_message(&path, message))
191 }
192
193 pub fn send_task(
197 &self,
198 agent_path: &str,
199 task: String,
200 interrupt: bool,
201 ) -> Result<bool, String> {
202 let path = self.parse_path(agent_path)?;
203 if !self.mailbox.contains(&path) {
204 return Err("agent not found".to_string());
205 }
206 let sent = self.mailbox.send_task(&path, task, interrupt);
207 if sent {
208 self.registry
209 .lock()
210 .unwrap()
211 .set_status(&path, AgentStatus::Running);
212 }
213 Ok(sent)
214 }
215
216 pub async fn wait_for_result(&self, agent_path: Option<&str>, timeout_ms: u64) -> WaitResult {
220 let filter_path = match agent_path {
221 Some(s) => match AgentPath::parse(s) {
222 Some(p) => Some(p),
223 None => {
224 return WaitResult {
225 status: "error".to_string(),
226 result: Some(format!("invalid agent path: {}", s)),
227 agent_path: None,
228 has_more: false,
229 };
230 }
231 },
232 None => None,
233 };
234
235 let mut seq = self.mailbox.subscribe_seq();
236 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
237
238 loop {
239 let result = match &filter_path {
241 Some(path) => self.mailbox.try_recv_result(path),
242 None => self.mailbox.try_recv_any(),
243 };
244
245 if let Some(r) = result {
246 let has_more = self.mailbox.total_pending_results() > 0;
247 let (status_str, result_text) = match r.status {
248 MailboxStatus::Ok => ("ok".to_string(), r.result),
249 MailboxStatus::Error => ("error".to_string(), r.result),
250 MailboxStatus::Closed => ("closed".to_string(), r.result),
251 };
252 return WaitResult {
253 status: status_str,
254 result: result_text,
255 agent_path: Some(r.agent_path.to_string()),
256 has_more,
257 };
258 }
259
260 let now = tokio::time::Instant::now();
262 if now >= deadline {
263 return WaitResult {
264 status: "timeout".to_string(),
265 result: None,
266 agent_path: None,
267 has_more: false,
268 };
269 }
270
271 let remaining = deadline - now;
272 tokio::select! {
273 _ = seq.changed() => {
274 continue;
276 }
277 _ = tokio::time::sleep(remaining) => {
278 return WaitResult {
279 status: "timeout".to_string(),
280 result: None,
281 agent_path: None,
282 has_more: false,
283 };
284 }
285 }
286 }
287 }
288
289 pub fn close_agent(&self, agent_path: &str) -> Result<CloseResult, String> {
294 let path = self.parse_path(agent_path)?;
295
296 let previous_status = {
298 let registry = self.registry.lock().unwrap();
299 registry
300 .get(&path)
301 .map(|e| format!("{:?}", e.status).to_lowercase())
302 .unwrap_or_else(|| "unknown".to_string())
303 };
304
305 {
307 let mut cancels = self.child_cancels.lock().unwrap();
308 if let Some(token) = cancels.remove(&path) {
309 token.cancel();
310 }
311 }
312
313 let existed = { self.registry.lock().unwrap().close(&path).is_some() };
315
316 self.mailbox.unregister(&path);
318
319 Ok(CloseResult {
320 closed: existed,
321 previous_status,
322 message: if existed {
323 "agent closed".to_string()
324 } else {
325 "agent not found".to_string()
326 },
327 })
328 }
329
330 pub fn list_agents(&self) -> Vec<AgentInfo> {
334 let registry = self.registry.lock().unwrap();
335 registry
336 .list()
337 .into_iter()
338 .map(|e| AgentInfo {
339 agent_path: e.path.to_string(),
340 status: format!("{:?}", e.status).to_lowercase(),
341 tool_count: e.tool_count,
342 })
343 .collect()
344 }
345
346 pub fn mailbox(&self) -> &Arc<MailboxHub> {
348 &self.mailbox
349 }
350
351 pub fn registry(&self) -> &Mutex<AgentRegistry> {
353 &self.registry
354 }
355
356 pub fn cancel_all(&self) {
358 let mut cancels = self.child_cancels.lock().unwrap();
359 for (_, token) in cancels.drain() {
360 token.cancel();
361 }
362 }
363}
364
365impl Drop for MultiAgentRuntime {
366 fn drop(&mut self) {
367 self.cancel_all();
368 let mut js = self.join_set.lock().unwrap();
370 while let Some(result) = js.try_join_next() {
371 if let Err(e) = result
372 && e.is_panic()
373 {
374 tracing::error!(
375 error = %e,
376 "child agent task panicked"
377 );
378 }
379 }
380 }
381}
382
383impl MultiAgentRuntime {
384 fn parse_path(&self, s: &str) -> Result<AgentPath, String> {
385 AgentPath::parse(s).ok_or_else(|| format!("invalid agent path: '{}'", s))
386 }
387
388 fn build_child_runtime(&self, system_prompt: String) -> AgentResult<AgentRuntime> {
389 let mut builder = AgentBuilder::new(self.client.clone())
390 .system_prompt(system_prompt)
391 .approval_handler(Arc::new(DenyAllApprovalHandler))
392 .language(self.language.clone());
393
394 for tool in &self.business_tools {
396 builder = builder.register_tool_arc(tool.clone());
397 }
398
399 if let Some(ref recovery) = self.error_recovery {
400 builder = builder.error_recovery(recovery.clone());
401 }
402
403 builder.build()
404 }
405}
406
407#[derive(Clone, Debug)]
413pub struct WaitResult {
414 pub status: String,
415 pub result: Option<String>,
416 pub agent_path: Option<String>,
417 pub has_more: bool,
418}
419
420#[derive(Clone, Debug)]
422pub struct CloseResult {
423 pub closed: bool,
424 pub previous_status: String,
425 pub message: String,
426}
427
428#[derive(Clone, Debug, serde::Serialize)]
430pub struct AgentInfo {
431 pub agent_path: String,
432 pub status: String,
433 pub tool_count: usize,
434}
435
436async fn run_child_loop(
449 child_mailbox: ChildMailbox,
450 child_runtime: AgentRuntime,
451 session_id: SessionId,
452 agent_path: AgentPath,
453 mailbox: Arc<MailboxHub>,
454 event_tx: Option<tokio::sync::mpsc::UnboundedSender<RuntimeEvent>>,
455 child_cancel: CancellationToken,
456) {
457 let mut task_rx = child_mailbox.task_rx;
458
459 if let Some(tx) = event_tx {
461 let mut child_events = child_runtime.subscribe_runtime_events();
462 let bridge_path = agent_path.to_string();
463 let bridge_cancel = child_cancel.clone();
464
465 tokio::spawn(async move {
466 loop {
467 tokio::select! {
468 _ = bridge_cancel.cancelled() => break,
469 event = child_events.recv() => {
470 match event {
471 Ok(event) => {
472 if matches!(event, RuntimeEvent::RunFinished { .. } | RuntimeEvent::RunCancelled { .. }) {
473 continue;
474 }
475 let _ = tx.send(RuntimeEvent::UserEvent {
476 session_id: SessionId::new(0),
477 event: UserEvent::SubAgentEvent {
478 subagent: bridge_path.clone(),
479 event: Box::new(event),
480 },
481 agent_id: None,
482 trace_id: None,
483 });
484 }
485 Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
486 tracing::warn!(
487 subagent = %bridge_path,
488 lagged = n,
489 "child event bridge lagged"
490 );
491 }
492 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
493 }
494 }
495 }
496 }
497 });
498 }
499
500 loop {
502 tokio::select! {
503 _ = child_cancel.cancelled() => {
504 break;
505 }
506 task = task_rx.recv() => {
507 match task {
508 Some(task) => {
509 let input = build_child_input(&task);
510 let result = child_runtime.run_turn_collect(
511 session_id.clone(),
512 &input,
513 ).await;
514
515 match result {
516 Ok((_events, outcome)) => {
517 let summary = summarize_outcome(&outcome);
518 mailbox.post_result(MailboxResult {
519 agent_path: agent_path.clone(),
520 status: MailboxStatus::Ok,
521 result: Some(summary),
522 });
523 }
524 Err(e) => {
525 mailbox.post_result(MailboxResult {
526 agent_path: agent_path.clone(),
527 status: MailboxStatus::Error,
528 result: Some(e.to_string()),
529 });
530 }
531 }
532 }
533 None => break, }
535 }
536 }
537 }
538}
539
540fn build_child_input(task: &MailboxTask) -> String {
542 if task.pending_messages.is_empty() {
543 task.task.clone()
544 } else {
545 let mut parts: Vec<String> = Vec::new();
546 for msg in &task.pending_messages {
547 parts.push(format!("[Message]: {}", msg));
548 }
549 parts.push(format!("[Task]: {}", task.task));
550 parts.join("\n\n")
551 }
552}
553
554fn summarize_outcome(outcome: &RunOutcome) -> String {
556 match outcome {
557 RunOutcome::Completed => "task completed".to_string(),
558 RunOutcome::Failed { error } => format!("task failed: {}", error),
559 RunOutcome::MaxTurnsExceeded { turns } => {
560 format!("max turns exceeded ({} turns)", turns)
561 }
562 RunOutcome::Cancelled => "cancelled".to_string(),
563 }
564}
565
566#[cfg(test)]
571mod tests {
572 use super::*;
573 use agent_base::RunOutcome;
574
575 #[test]
578 fn test_summarize_completed() {
579 let s = summarize_outcome(&RunOutcome::Completed);
580 assert_eq!(s, "task completed");
581 }
582
583 #[test]
584 fn test_summarize_failed() {
585 let outcome = RunOutcome::Failed {
586 error: "connection refused".to_string(),
587 };
588 let s = summarize_outcome(&outcome);
589 assert_eq!(s, "task failed: connection refused");
590 }
591
592 #[test]
593 fn test_summarize_max_turns() {
594 let outcome = RunOutcome::MaxTurnsExceeded { turns: 42 };
595 let s = summarize_outcome(&outcome);
596 assert!(s.contains("max turns exceeded"));
597 assert!(s.contains("42"));
598 }
599
600 #[test]
601 fn test_summarize_cancelled() {
602 let s = summarize_outcome(&RunOutcome::Cancelled);
603 assert_eq!(s, "cancelled");
604 }
605
606 #[test]
609 fn test_build_child_input_task_only() {
610 let task = MailboxTask {
611 task: "do work".into(),
612 interrupt: true,
613 pending_messages: vec![],
614 };
615 let out = build_child_input(&task);
616 assert_eq!(out, "do work");
617 }
618
619 #[test]
620 fn test_build_child_input_with_pending_messages() {
621 let task = MailboxTask {
622 task: "do work".into(),
623 interrupt: false,
624 pending_messages: vec!["context 1".into(), "context 2".into()],
625 };
626 let out = build_child_input(&task);
627 assert!(out.contains("[Message]: context 1"));
628 assert!(out.contains("[Message]: context 2"));
629 assert!(out.contains("[Task]: do work"));
630 let msg_pos = out.find("[Message]:").unwrap();
632 let task_pos = out.find("[Task]:").unwrap();
633 assert!(msg_pos < task_pos, "messages should precede task");
634 }
635
636 #[test]
637 fn test_build_child_input_single_message() {
638 let task = MailboxTask {
639 task: "final task".into(),
640 interrupt: true,
641 pending_messages: vec!["hint".into()],
642 };
643 let out = build_child_input(&task);
644 assert_eq!(out, "[Message]: hint\n\n[Task]: final task");
645 }
646}