1use std::collections::HashMap;
2use std::sync::{Arc, RwLock};
3use tokio::sync::{mpsc, oneshot};
4use serde_json::Value;
5
6#[derive(Debug)]
9pub struct SubagentResult {
10 pub text: String,
11 pub model: String,
12 pub input_tokens: u64,
13 pub output_tokens: u64,
14 pub cache_read: u64,
15 pub cache_creation: u64,
16 pub cache_creation_5m: Option<u64>,
19 pub cache_creation_1h: Option<u64>,
20 pub tool_count: u32,
21}
22
23#[derive(Debug, Clone, PartialEq)]
26pub enum SubagentStatus {
27 Running,
28 Completed,
29 Cancelled,
31 TimedOut,
32 Failed(String),
33}
34
35#[derive(Debug)]
40pub struct SubagentState {
41 pub status: SubagentStatus,
42 pub partial_text: String,
43 pub tool_log: Vec<String>,
44 pub conversation_state: Vec<Value>,
45 pub finished_at: Option<std::time::Instant>,
47 pub cancel_requested: bool,
50}
51
52impl SubagentState {
53 pub fn new() -> Self {
54 Self {
55 status: SubagentStatus::Running,
56 partial_text: String::new(),
57 tool_log: Vec::new(),
58 conversation_state: Vec::new(),
59 finished_at: None,
60 cancel_requested: false,
61 }
62 }
63}
64
65impl Default for SubagentState {
66 fn default() -> Self { Self::new() }
67}
68
69#[derive(Debug, Clone)]
74pub struct SubagentDisplayRow {
75 pub subagent_id: u64,
76 pub agent_name: String,
77 pub status: SubagentStatus,
78 pub cancel_requested: bool,
79 pub elapsed_secs: f64,
80 pub finished_elapsed: Option<std::time::Duration>,
81}
82
83pub struct SubagentHandle {
86 pub id: String,
87 pub subagent_id: u64,
90 pub agent_name: String,
91 pub task_preview: String,
92 pub model: String,
93 pub system_prompt: String,
94 pub started_at: std::time::Instant,
95 pub timeout_secs: u64,
96
97 state: Arc<RwLock<SubagentState>>,
99
100 steer_tx: Option<mpsc::UnboundedSender<String>>,
102 shutdown_tx: Option<oneshot::Sender<()>>,
103 pub(crate) thread_handle: Option<std::thread::JoinHandle<()>>,
106
107 result_rx: Option<oneshot::Receiver<SubagentResult>>,
109
110 collected: bool,
112}
113
114impl std::fmt::Debug for SubagentHandle {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct("SubagentHandle")
117 .field("id", &self.id)
118 .field("subagent_id", &self.subagent_id)
119 .field("agent_name", &self.agent_name)
120 .field("model", &self.model)
121 .finish_non_exhaustive()
122 }
123}
124
125impl SubagentHandle {
126 #[allow(clippy::too_many_arguments)]
128 pub fn new(
129 id: String,
130 subagent_id: u64,
131 agent_name: String,
132 task_preview: String,
133 model: String,
134 system_prompt: String,
135 timeout_secs: u64,
136 state: Arc<RwLock<SubagentState>>,
137 steer_tx: Option<mpsc::UnboundedSender<String>>,
138 shutdown_tx: Option<oneshot::Sender<()>>,
139 result_rx: Option<oneshot::Receiver<SubagentResult>>,
140 ) -> Self {
141 Self {
142 id,
143 subagent_id,
144 agent_name,
145 task_preview,
146 model,
147 system_prompt,
148 started_at: std::time::Instant::now(),
149 timeout_secs,
150 state,
151 steer_tx,
152 shutdown_tx,
153 thread_handle: None,
154 result_rx,
155 collected: false,
156 }
157 }
158
159 pub fn status(&self) -> SubagentStatus {
161 self.state.read().unwrap_or_else(|p| p.into_inner()).status.clone()
162 }
163
164 pub fn partial_output(&self) -> String {
166 self.state.read().unwrap_or_else(|p| p.into_inner()).partial_text.clone()
167 }
168
169 pub fn tool_log(&self) -> Vec<String> {
171 self.state.read().unwrap_or_else(|p| p.into_inner()).tool_log.clone()
172 }
173
174 pub fn conversation_state(&self) -> Vec<Value> {
176 self.state.read().unwrap_or_else(|p| p.into_inner()).conversation_state.clone()
177 }
178
179 pub fn elapsed_secs(&self) -> f64 {
181 self.started_at.elapsed().as_secs_f64()
182 }
183
184 pub fn steer(&self, message: &str) -> Result<(), String> {
186 match &self.steer_tx {
187 Some(tx) => tx
188 .send(message.to_string())
189 .map_err(|e| format!("steer channel closed: {e}")),
190 None => Err("no steer channel on this handle".to_string()),
191 }
192 }
193
194 pub fn set_thread_handle(&mut self, handle: std::thread::JoinHandle<()>) {
197 self.thread_handle = Some(handle);
198 }
199
200 pub fn cancel(&mut self) {
201 self.state.write().unwrap_or_else(|p| p.into_inner()).cancel_requested = true;
204 if let Some(tx) = self.shutdown_tx.take() {
205 let _ = tx.send(());
206 }
207 }
208
209 pub fn is_finished(&self) -> bool {
211 !matches!(self.status(), SubagentStatus::Running)
212 }
213
214 pub fn mark_collected(&mut self) {
216 self.collected = true;
217 }
218
219 pub fn is_collected(&self) -> bool {
221 self.collected
222 }
223
224 pub fn finished_elapsed(&self) -> Option<std::time::Duration> {
227 self.state.read().unwrap_or_else(|p| p.into_inner()).finished_at.map(|t| t.elapsed())
228 }
229
230 pub async fn collect(mut self) -> Result<SubagentResult, String> {
232 match self.result_rx.take() {
233 Some(rx) => rx.await.map_err(|_| "subagent result channel dropped".to_string()),
234 None => Err("no result receiver — already collected or never set".to_string()),
235 }
236 }
237}
238
239pub const FINISHED_HANDLE_TTL: std::time::Duration = std::time::Duration::from_secs(900); #[derive(Debug)]
245pub struct SubagentRegistry {
246 pub(crate) handles: HashMap<String, SubagentHandle>,
247}
248
249impl SubagentRegistry {
250 pub fn new() -> Self {
251 Self {
252 handles: HashMap::new(),
253 }
254 }
255
256 pub fn register(&mut self, handle: SubagentHandle) -> String {
258 let id = handle.id.clone();
259 self.handles.insert(id.clone(), handle);
260 id
261 }
262
263 pub fn get(&self, id: &str) -> Option<&SubagentHandle> {
264 self.handles.get(id)
265 }
266
267 pub fn get_mut(&mut self, id: &str) -> Option<&mut SubagentHandle> {
268 self.handles.get_mut(id)
269 }
270
271 pub fn remove(&mut self, id: &str) -> Option<SubagentHandle> {
272 self.handles.remove(id)
273 }
274
275 pub fn list_active(&self) -> Vec<(String, String, SubagentStatus)> {
277 self.handles
278 .values()
279 .map(|h| (h.id.clone(), h.agent_name.clone(), h.status()))
280 .collect()
281 }
282
283 pub fn display_rows(&self) -> Vec<SubagentDisplayRow> {
287 self.handles
288 .values()
289 .map(|h| {
290 let s = h.state.read().unwrap_or_else(|p| p.into_inner());
291 SubagentDisplayRow {
292 subagent_id: h.subagent_id,
293 agent_name: h.agent_name.clone(),
294 status: s.status.clone(),
295 cancel_requested: s.cancel_requested,
296 elapsed_secs: h.started_at.elapsed().as_secs_f64(),
297 finished_elapsed: s.finished_at.map(|t| t.elapsed()),
298 }
299 })
300 .collect()
301 }
302
303 pub fn iter_mut_handles(&mut self) -> impl Iterator<Item = &mut SubagentHandle> {
306 self.handles.values_mut()
307 }
308
309 pub fn cleanup_finished_with_ttl(&mut self, ttl: std::time::Duration) {
319 let reap_ids: Vec<String> = self.handles.iter()
320 .filter(|(_, h)| h.is_finished()
321 && h.thread_handle.as_ref().map_or(true, |t| t.is_finished())
325 && (h.is_collected()
326 || h.finished_elapsed().is_some_and(|d| d >= ttl)))
327 .map(|(id, _)| id.clone())
328 .collect();
329 for id in reap_ids {
330 if let Some(mut handle) = self.handles.remove(&id) {
331 if let Some(th) = handle.thread_handle.take() {
332 let _ = th.join();
333 }
334 }
335 }
336 }
337
338 pub fn cleanup_finished(&mut self) {
341 self.cleanup_finished_with_ttl(FINISHED_HANDLE_TTL)
342 }
343}
344
345pub fn reap_finished(registry: &Arc<std::sync::Mutex<SubagentRegistry>>) {
352 match registry.lock() {
353 Ok(mut guard) => guard.cleanup_finished(),
354 Err(poisoned) => poisoned.into_inner().cleanup_finished(),
355 }
356}
357
358impl Default for SubagentRegistry {
359 fn default() -> Self {
360 Self::new()
361 }
362}
363
364impl SubagentStatus {
365 pub fn as_str(&self) -> &str {
366 match self {
367 SubagentStatus::Running => "running",
368 SubagentStatus::Completed => "completed",
369 SubagentStatus::Cancelled => "cancelled",
370 SubagentStatus::TimedOut => "timed_out",
371 SubagentStatus::Failed(_) => "failed",
372 }
373 }
374
375 pub fn failure_reason(&self) -> Option<&str> {
377 match self {
378 SubagentStatus::Failed(r) => Some(r.as_str()),
379 _ => None,
380 }
381 }
382}
383
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use tokio::sync::{mpsc, oneshot};
389
390 struct TestHandle {
392 handle: SubagentHandle,
393 _steer_rx: mpsc::UnboundedReceiver<String>,
394 _shutdown_rx: oneshot::Receiver<()>,
395 }
396
397 fn make_test_handle(id: &str) -> TestHandle {
398 let state = Arc::new(RwLock::new(SubagentState::new()));
399 let (steer_tx, steer_rx) = mpsc::unbounded_channel();
400 let (shutdown_tx, shutdown_rx) = oneshot::channel();
401 let (_result_tx, result_rx) = oneshot::channel();
402 let numeric_id: u64 = id.strip_prefix("sa_").and_then(|n| n.parse().ok()).unwrap_or(0);
404 TestHandle {
405 handle: SubagentHandle::new(
406 id.to_string(),
407 numeric_id,
408 "test-agent".to_string(),
409 "test task".to_string(),
410 "claude-sonnet-4-6".to_string(),
411 "You are a test agent.".to_string(),
412 300,
413 state,
414 Some(steer_tx),
415 Some(shutdown_tx),
416 Some(result_rx),
417 ),
418 _steer_rx: steer_rx,
419 _shutdown_rx: shutdown_rx,
420 }
421 }
422
423 fn make_handle(id: &str) -> SubagentHandle {
424 make_test_handle(id).handle
425 }
426
427 fn make_finished_handle(id: &str) -> SubagentHandle {
428 let h = make_handle(id);
429 {
430 let mut s = h.state.write().unwrap();
431 s.status = SubagentStatus::Completed;
432 s.finished_at = Some(std::time::Instant::now());
433 }
434 h
435 }
436
437 #[test]
438 fn handle_initial_status_is_running() {
439 let h = make_handle("sa_1");
440 assert_eq!(h.status(), SubagentStatus::Running);
441 assert!(!h.is_finished());
442 }
443
444 #[test]
445 fn handle_partial_output_empty_initially() {
446 let h = make_handle("sa_1");
447 assert_eq!(h.partial_output(), "");
448 assert!(h.tool_log().is_empty());
449 assert!(h.conversation_state().is_empty());
450 }
451
452 #[test]
453 fn handle_status_reflects_state_change() {
454 let h = make_handle("sa_1");
455 {
456 let mut s = h.state.write().unwrap();
457 s.status = SubagentStatus::Completed;
458 s.partial_text = "done!".to_string();
459 }
460 assert_eq!(h.status(), SubagentStatus::Completed);
461 assert!(h.is_finished());
462 assert_eq!(h.partial_output(), "done!");
463 }
464
465 #[test]
466 fn handle_steer_sends_message() {
467 let th = make_test_handle("sa_1");
468 assert!(th.handle.steer("redirect").is_ok());
469 }
470
471 #[test]
472 fn handle_steer_fails_without_channel() {
473 let state = Arc::new(RwLock::new(SubagentState::new()));
474 let (_shutdown_tx, _) = oneshot::channel::<()>();
475 let (_, result_rx) = oneshot::channel();
476 let h = SubagentHandle::new(
477 "sa_1".into(), 1, "test".into(), "task".into(),
478 "model".into(), "prompt".into(), 300, state, None, None, Some(result_rx),
479 );
480 assert!(h.steer("msg").is_err());
481 }
482
483 #[test]
484 fn handle_cancel_consumes_shutdown() {
485 let mut h = make_handle("sa_1");
486 h.cancel(); h.cancel(); }
489
490 #[test]
491 fn handle_elapsed_increases() {
492 let h = make_handle("sa_1");
493 std::thread::sleep(std::time::Duration::from_millis(10));
494 assert!(h.elapsed_secs() > 0.0);
495 }
496
497 #[test]
498 fn registry_register_and_get() {
499 let mut reg = SubagentRegistry::new();
500 let h = make_handle("sa_1");
501 reg.register(h);
502 assert!(reg.get("sa_1").is_some());
503 assert!(reg.get("sa_99").is_none());
504 }
505
506 #[test]
507 fn registry_remove() {
508 let mut reg = SubagentRegistry::new();
509 reg.register(make_handle("sa_1"));
510 assert!(reg.remove("sa_1").is_some());
511 assert!(reg.get("sa_1").is_none());
512 }
513
514 #[test]
515 fn registry_list_active() {
516 let mut reg = SubagentRegistry::new();
517 reg.register(make_handle("sa_1"));
518 reg.register(make_handle("sa_2"));
519 let active = reg.list_active();
520 assert_eq!(active.len(), 2);
521 }
522
523 #[test]
526 fn registry_cleanup_finished() {
527 let mut reg = SubagentRegistry::new();
528 let mut h = make_finished_handle("sa_1");
529 h.mark_collected(); reg.register(h);
531 reg.register(make_handle("sa_2")); reg.cleanup_finished();
534
535 assert!(reg.get("sa_1").is_none(), "collected finished handle must be reaped");
536 assert!(reg.get("sa_2").is_some(), "running handle must survive cleanup");
537 }
538
539 #[test]
541 fn reaper_retains_finished_uncollected_within_ttl() {
542 let mut reg = SubagentRegistry::new();
543 let h = make_finished_handle("sa_1"); reg.register(h);
545
546 reg.cleanup_finished_with_ttl(std::time::Duration::from_secs(900));
547
548 assert!(reg.get("sa_1").is_some(), "finished-uncollected handle within TTL must be retained");
549 }
550
551 #[test]
553 fn reaper_reaps_finished_and_collected() {
554 let mut reg = SubagentRegistry::new();
555 let mut h = make_finished_handle("sa_1");
556 h.mark_collected();
557 reg.register(h);
558
559 reg.cleanup_finished_with_ttl(std::time::Duration::from_secs(900));
560
561 assert!(reg.get("sa_1").is_none(), "finished+collected handle must be reaped");
562 }
563
564 #[test]
566 fn reaper_reaps_abandoned_after_ttl() {
567 let mut reg = SubagentRegistry::new();
568 let h = make_finished_handle("sa_1"); reg.register(h);
570
571 reg.cleanup_finished_with_ttl(std::time::Duration::ZERO);
572
573 assert!(reg.get("sa_1").is_none(), "handle past TTL must be reaped even if uncollected");
574 }
575
576 #[test]
579 fn reaper_defers_handle_with_live_thread() {
580 let mut reg = SubagentRegistry::new();
581 let mut h = make_finished_handle("sa_live");
582 h.mark_collected(); let barrier = Arc::new(std::sync::Barrier::new(2));
586 let barrier_c = Arc::clone(&barrier);
587 let thread = std::thread::spawn(move || {
588 barrier_c.wait();
590 std::thread::sleep(std::time::Duration::from_millis(20));
592 });
593 h.set_thread_handle(thread);
594 reg.register(h);
595
596 barrier.wait();
598
599 reg.cleanup_finished_with_ttl(std::time::Duration::ZERO);
605 std::thread::sleep(std::time::Duration::from_millis(50));
610
611 reg.cleanup_finished_with_ttl(std::time::Duration::ZERO);
613 assert!(reg.get("sa_live").is_none(), "handle must be reaped once OS thread exits");
614 }
615
616 #[test]
618 fn reaper_never_touches_running() {
619 let mut reg = SubagentRegistry::new();
620 reg.register(make_handle("sa_1")); reg.register(make_handle("sa_2")); reg.cleanup_finished_with_ttl(std::time::Duration::ZERO);
624 assert!(reg.get("sa_1").is_some());
625 assert!(reg.get("sa_2").is_some());
626
627 reg.cleanup_finished(); assert!(reg.get("sa_1").is_some());
629 assert!(reg.get("sa_2").is_some());
630 }
631
632 #[test]
633 fn subagent_state_new_defaults() {
634 let s = SubagentState::new();
635 assert_eq!(s.status, SubagentStatus::Running);
636 assert!(s.partial_text.is_empty());
637 assert!(s.tool_log.is_empty());
638 assert!(s.conversation_state.is_empty());
639 assert!(s.finished_at.is_none());
640 assert!(!s.cancel_requested, "cancel_requested must default to false");
641 }
642
643 #[test]
644 fn subagent_status_as_str() {
645 assert_eq!(SubagentStatus::Running.as_str(), "running");
646 assert_eq!(SubagentStatus::Completed.as_str(), "completed");
647 assert_eq!(SubagentStatus::Cancelled.as_str(), "cancelled");
648 assert_eq!(SubagentStatus::TimedOut.as_str(), "timed_out");
649 assert_eq!(SubagentStatus::Failed("oops".into()).as_str(), "failed");
650 }
651
652 #[test]
654 fn cancel_sets_cancel_requested_flag() {
655 let mut h = make_handle("sa_cancel");
656 {
657 let s = h.state.read().unwrap();
658 assert!(!s.cancel_requested, "cancel_requested must start false");
659 }
660 h.cancel();
661 {
662 let s = h.state.read().unwrap();
663 assert!(s.cancel_requested, "cancel_requested must be true after cancel()");
664 }
665 h.cancel();
667 }
668
669 #[test]
671 fn cancelled_status_is_finished() {
672 let h = make_handle("sa_c1");
673 {
674 let mut s = h.state.write().unwrap();
675 s.status = SubagentStatus::Cancelled;
676 }
677 assert!(h.is_finished(), "Cancelled must count as finished");
678 }
679
680 #[test]
682 fn display_rows_running_handle() {
683 let mut reg = SubagentRegistry::new();
684 reg.register(make_handle("sa_1"));
685 reg.register(make_handle("sa_2"));
686 let rows = reg.display_rows();
687 assert_eq!(rows.len(), 2);
688 for row in &rows {
689 assert_eq!(row.status, SubagentStatus::Running);
690 assert!(!row.cancel_requested);
691 assert!(row.elapsed_secs >= 0.0);
692 assert!(row.finished_elapsed.is_none());
693 }
694 }
695
696 #[test]
698 fn display_rows_finished_and_cancel_requested() {
699 let mut reg = SubagentRegistry::new();
700 let h = make_finished_handle("sa_1");
701 reg.register(h);
702
703 let th = make_test_handle("sa_2");
705 {
706 let mut s = th.handle.state.write().unwrap();
707 s.cancel_requested = true;
708 }
709 reg.register(th.handle);
710
711 let rows = reg.display_rows();
712 assert_eq!(rows.len(), 2);
713
714 let finished = rows.iter().find(|r| r.subagent_id == 1).unwrap();
715 assert_eq!(finished.status, SubagentStatus::Completed);
716 assert!(!finished.cancel_requested);
717 assert!(finished.finished_elapsed.is_some());
718
719 let cancelling = rows.iter().find(|r| r.subagent_id == 2).unwrap();
720 assert!(cancelling.cancel_requested);
721 }
722
723 #[test]
725 fn display_rows_empty_registry() {
726 let reg = SubagentRegistry::new();
727 assert!(reg.display_rows().is_empty());
728 }
729
730 #[test]
734 fn reap_finished_headless_reaps_collected() {
735 let registry = Arc::new(std::sync::Mutex::new(SubagentRegistry::new()));
736 {
737 let mut reg = registry.lock().unwrap();
738 let mut h = make_finished_handle("sa_r1");
739 h.mark_collected();
740 reg.register(h);
741 }
742 super::reap_finished(®istry);
743 assert!(registry.lock().unwrap().get("sa_r1").is_none(),
744 "reap_finished must reap collected+finished handle");
745 }
746
747 #[test]
749 fn reap_finished_headless_retains_running() {
750 let registry = Arc::new(std::sync::Mutex::new(SubagentRegistry::new()));
751 {
752 let mut reg = registry.lock().unwrap();
753 reg.register(make_handle("sa_running"));
754 }
755 super::reap_finished(®istry);
756 assert!(registry.lock().unwrap().get("sa_running").is_some(),
757 "reap_finished must not touch running handles");
758 }
759
760 #[test]
762 fn reap_finished_poisoned_lock_recovery() {
763 let registry = Arc::new(std::sync::Mutex::new(SubagentRegistry::new()));
764 {
765 let mut reg = registry.lock().unwrap();
766 let mut h = make_finished_handle("sa_poison");
767 h.mark_collected();
768 reg.register(h);
769 }
770
771 let registry_c = Arc::clone(®istry);
773 let _ = std::panic::catch_unwind(|| {
774 let _guard = registry_c.lock().unwrap();
775 panic!("deliberately poison the mutex");
776 });
777
778 super::reap_finished(®istry);
780
781 let result = registry.lock()
783 .unwrap_or_else(|p| p.into_inner())
784 .get("sa_poison")
785 .is_none();
786 assert!(result, "reap_finished must run cleanup even after poison");
787 }
788}
789
790#[cfg(test)]
792mod cancelled_wake_tests {
793 use super::*;
794 use std::sync::{Arc, RwLock};
795
796 #[test]
800 fn cancelled_suppresses_wake() {
801 use crate::events::EventQueue;
802 use crate::tools::finalize_subagent;
803
804 let state = Arc::new(RwLock::new(SubagentState::new()));
805 {
806 let mut s = state.write().unwrap();
807 s.status = SubagentStatus::Completed; s.cancel_requested = true; }
810 let queue = Arc::new(EventQueue::new(100));
811
812 finalize_subagent(
813 &state,
814 Some(&queue),
815 "sa_v3", 42, "test-agent",
816 std::time::Instant::now(),
817 None,
818 );
819
820 assert!(queue.is_empty(), "cancelled subagent must not publish a wake event");
822
823 let s = state.read().unwrap();
825 assert_eq!(s.status, SubagentStatus::Cancelled, "status must be Cancelled when cancel_requested");
826
827 assert!(s.finished_at.is_some(), "finished_at must be stamped by finalizer");
829 }
830}