1use crate::error::{SdkError, SdkResult};
7use crate::lifecycle::snapshot::SnapshotStore;
8use crate::lifecycle::{AgentLifecycleEvent, AgentSnapshot, AgentStatus, MetricsSnapshot};
9use crate::routing::RoutingControl;
10use oxicode_agent::{AgentConfig, AgentTool, ProviderResolver, ToolRegistry};
11use parking_lot::RwLock;
12use std::collections::HashMap;
13use std::sync::Arc;
14use std::sync::atomic::{AtomicU8, Ordering};
15use tokio::sync::broadcast;
16
17const STATUS_CREATED: u8 = 0;
20const STATUS_RUNNING: u8 = 1;
21const STATUS_SUSPENDED: u8 = 2;
22const STATUS_TERMINATED: u8 = 3;
23const STATUS_FAILED: u8 = 4;
24
25fn u8_to_status(v: u8) -> AgentStatus {
26 match v {
27 STATUS_CREATED => AgentStatus::Created,
28 STATUS_RUNNING => AgentStatus::Running,
29 STATUS_SUSPENDED => AgentStatus::Suspended,
30 STATUS_TERMINATED => AgentStatus::Terminated,
31 _ => AgentStatus::Failed,
32 }
33}
34
35#[derive(Debug, Clone)]
39pub struct SupervisorPolicy {
40 pub max_restarts: usize,
42 pub restart_window_secs: u64,
44 pub backoff: RestartBackoff,
46}
47
48impl Default for SupervisorPolicy {
49 fn default() -> Self {
50 Self {
51 max_restarts: 3,
52 restart_window_secs: 60,
53 backoff: RestartBackoff::Exponential {
54 base_ms: 1000,
55 max_ms: 30_000,
56 },
57 }
58 }
59}
60
61impl SupervisorPolicy {
62 pub fn no_restart() -> Self {
64 Self {
65 max_restarts: 0,
66 restart_window_secs: 0,
67 backoff: RestartBackoff::None,
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub enum RestartBackoff {
75 None,
77 Fixed {
79 delay_ms: u64,
81 },
82 Exponential {
84 base_ms: u64,
86 max_ms: u64,
88 },
89}
90
91#[derive(Clone)]
98pub struct AgentHandle {
99 agent_id: String,
100 status: Arc<AtomicU8>,
101 agent: Arc<oxicode_agent::Agent>,
102 config: Arc<RwLock<AgentConfig>>,
103 metrics: Arc<crate::metrics::AgentMetrics>,
104 lifecycle_tx: broadcast::Sender<AgentLifecycleEvent>,
105 created_at_ms: u64,
106 parent_id: Option<String>,
107 routing: RoutingControl,
108}
109
110impl AgentHandle {
111 pub(crate) fn new(
113 agent: oxicode_agent::Agent,
114 config: AgentConfig,
115 parent_id: Option<String>,
116 lifecycle_tx: broadcast::Sender<AgentLifecycleEvent>,
117 ) -> Self {
118 let routing = RoutingControl::new(crate::routing::RoutingConfig::default());
119 Self {
120 agent_id: if config.name.is_empty() {
121 uuid::Uuid::new_v4().to_string()
122 } else {
123 config.name.clone()
124 },
125 status: Arc::new(AtomicU8::new(STATUS_CREATED)),
126 agent: Arc::new(agent),
127 config: Arc::new(RwLock::new(config)),
128 metrics: Arc::new(crate::metrics::AgentMetrics::new()),
129 lifecycle_tx,
130 created_at_ms: AgentLifecycleEvent::now_ms(),
131 parent_id,
132 routing,
133 }
134 }
135
136 pub fn agent_id(&self) -> &str {
140 &self.agent_id
141 }
142
143 pub fn parent_id(&self) -> Option<&str> {
145 self.parent_id.as_deref()
146 }
147
148 pub fn created_at_ms(&self) -> u64 {
150 self.created_at_ms
151 }
152
153 pub fn metrics(&self) -> MetricsSnapshot {
155 self.metrics.snapshot()
156 }
157
158 pub fn is_running(&self) -> bool {
160 self.status() == AgentStatus::Running
161 }
162
163 pub fn status(&self) -> AgentStatus {
165 u8_to_status(self.status.load(Ordering::SeqCst))
166 }
167
168 pub async fn run(
175 &self,
176 prompt: String,
177 ) -> SdkResult<(
178 oxicode_agent::types::Response,
179 Vec<oxicode_agent::AgentEvent>,
180 )> {
181 let prev = self
183 .status
184 .compare_exchange(
185 STATUS_CREATED,
186 STATUS_RUNNING,
187 Ordering::SeqCst,
188 Ordering::SeqCst,
189 )
190 .or_else(|_| {
191 self.status.compare_exchange(
192 STATUS_SUSPENDED,
193 STATUS_RUNNING,
194 Ordering::SeqCst,
195 Ordering::SeqCst,
196 )
197 });
198
199 if prev.is_err() {
200 return Err(SdkError::AgentNotRunnable {
201 agent_id: self.agent_id.clone(),
202 status: self.status().to_string(),
203 });
204 }
205
206 self.emit(AgentLifecycleEvent::RunStart {
207 agent_id: self.agent_id.clone(),
208 timestamp_ms: AgentLifecycleEvent::now_ms(),
209 });
210
211 let start = std::time::Instant::now();
212 let result = self.agent.run(prompt).await;
213 let elapsed = start.elapsed();
214
215 match result {
216 Ok((response, events)) => {
217 let agent_state = self.agent.state();
218 let input_tokens = agent_state.input_tokens as u64;
219 let output_tokens = agent_state.output_tokens as u64;
220 let tool_count = events
221 .iter()
222 .filter(|e| matches!(e, oxicode_agent::AgentEvent::ToolExecutionStart { .. }))
223 .count() as u64;
224 self.metrics.record_success(
225 elapsed.as_millis() as u64,
226 input_tokens,
227 output_tokens,
228 tool_count,
229 );
230 self.transition(STATUS_CREATED);
231 self.emit(AgentLifecycleEvent::RunEnd {
232 agent_id: self.agent_id.clone(),
233 timestamp_ms: AgentLifecycleEvent::now_ms(),
234 success: true,
235 });
236 Ok((response, events))
237 }
238 Err(e) => {
239 self.transition(STATUS_FAILED);
240 self.emit(AgentLifecycleEvent::RunEnd {
241 agent_id: self.agent_id.clone(),
242 timestamp_ms: AgentLifecycleEvent::now_ms(),
243 success: false,
244 });
245 Err(SdkError::ExecutionFailed {
246 reason: e.to_string(),
247 })
248 }
249 }
250 }
251
252 pub async fn continue_with(
256 &self,
257 prompt: String,
258 ) -> SdkResult<(
259 oxicode_agent::types::Response,
260 Vec<oxicode_agent::AgentEvent>,
261 )> {
262 self.run(prompt).await
263 }
264
265 pub fn cancel(&self) {
267 self.agent.cancel();
268 }
269
270 pub async fn suspend(&self) -> SdkResult<AgentSnapshot> {
276 let cur = self.status();
277 if !cur.is_runnable() && cur != AgentStatus::Running {
278 return Err(SdkError::AgentNotRunnable {
279 agent_id: self.agent_id.clone(),
280 status: cur.to_string(),
281 });
282 }
283
284 if cur == AgentStatus::Running {
286 self.cancel();
287 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
288 }
289
290 let snapshot = AgentSnapshot::from_agent(
291 self.agent_id.clone(),
292 &self.config.read(),
293 &self.agent.state(),
294 &self.agent.tools(),
295 self.parent_id.clone(),
296 HashMap::new(),
297 );
298
299 self.transition(STATUS_SUSPENDED);
300 self.emit(AgentLifecycleEvent::Suspended {
301 agent_id: self.agent_id.clone(),
302 snapshot: Box::new(snapshot.clone()),
303 timestamp_ms: AgentLifecycleEvent::now_ms(),
304 });
305
306 Ok(snapshot)
307 }
308
309 pub fn terminate(&self) -> SdkResult<()> {
311 if self.status().is_terminal() {
312 return Err(SdkError::AgentNotRunnable {
313 agent_id: self.agent_id.clone(),
314 status: self.status().to_string(),
315 });
316 }
317 self.transition(STATUS_TERMINATED);
318 self.emit(AgentLifecycleEvent::Terminated {
319 agent_id: self.agent_id.clone(),
320 timestamp_ms: AgentLifecycleEvent::now_ms(),
321 });
322 Ok(())
323 }
324
325 pub fn snapshot(&self) -> SdkResult<AgentSnapshot> {
327 Ok(AgentSnapshot::from_agent(
328 self.agent_id.clone(),
329 &self.config.read(),
330 &self.agent.state(),
331 &self.agent.tools(),
332 self.parent_id.clone(),
333 HashMap::new(),
334 ))
335 }
336
337 pub fn switch_model(&self, model_id: &str) -> anyhow::Result<()> {
344 let old = self.config.read().model_id.clone();
345 self.agent.switch_model(model_id)?;
346 self.config.write().model_id = model_id.to_string();
347 self.emit(AgentLifecycleEvent::ModelSwitched {
348 agent_id: self.agent_id.clone(),
349 from_model: old,
350 to_model: model_id.to_string(),
351 timestamp_ms: AgentLifecycleEvent::now_ms(),
352 });
353 Ok(())
354 }
355
356 pub fn set_system_prompt(&self, prompt: String) {
358 self.config.write().system_prompt = Some(prompt.clone());
359 self.agent.set_system_prompt(prompt);
360 }
361
362 pub fn add_tool(&self, tool: impl AgentTool + 'static) {
364 self.agent.add_tool(tool);
365 }
366
367 pub fn routing(&self) -> &RoutingControl {
374 &self.routing
375 }
376
377 pub fn disable_routing(&self) {
379 self.routing.set_enabled(false);
380 }
381
382 pub fn enable_routing(&self) {
384 self.routing.set_enabled(true);
385 }
386
387 pub fn exclude_route_model(&self, model_id: &str) {
389 self.routing.exclude_model(model_id);
390 }
391
392 fn transition(&self, new_status: u8) {
395 self.status.store(new_status, Ordering::SeqCst);
396 }
397
398 fn emit(&self, event: AgentLifecycleEvent) {
399 let _ = self.lifecycle_tx.send(event);
400 }
401}
402
403impl std::fmt::Debug for AgentHandle {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 f.debug_struct("AgentHandle")
406 .field("agent_id", &self.agent_id)
407 .field("status", &self.status())
408 .finish()
409 }
410}
411
412#[derive(Clone)]
422pub struct AgentSupervisor {
423 agents: Arc<RwLock<HashMap<String, AgentHandle>>>,
424 lifecycle_tx: broadcast::Sender<AgentLifecycleEvent>,
425 snapshot_store: Arc<dyn SnapshotStore>,
426 policy: SupervisorPolicy,
427 restart_log: Arc<RwLock<HashMap<String, Vec<u64>>>>,
429 resolver: Arc<dyn ProviderResolver>,
430 oxicode: Option<Arc<crate::Oxicode>>,
435 agent_decorator: Option<Arc<dyn crate::observability::AgentDecorator>>,
438}
439
440impl AgentSupervisor {
441 pub fn new(
443 resolver: Arc<dyn ProviderResolver>,
444 snapshot_store: Arc<dyn SnapshotStore>,
445 ) -> Self {
446 Self::with_policy(resolver, snapshot_store, SupervisorPolicy::default())
447 }
448 pub fn with_policy(
450 resolver: Arc<dyn ProviderResolver>,
451 snapshot_store: Arc<dyn SnapshotStore>,
452 policy: SupervisorPolicy,
453 ) -> Self {
454 let (tx, _) = broadcast::channel(1024);
455 Self {
456 agents: Arc::new(RwLock::new(HashMap::new())),
457 lifecycle_tx: tx,
458 snapshot_store,
459 policy,
460 restart_log: Arc::new(RwLock::new(HashMap::new())),
461 resolver,
462 oxicode: None,
463 agent_decorator: None,
464 }
465 }
466
467 pub fn with_agent_decorator(
478 mut self,
479 oxicode: Arc<crate::Oxicode>,
480 decorator: Arc<dyn crate::observability::AgentDecorator>,
481 ) -> Self {
482 self.oxicode = Some(oxicode);
483 self.agent_decorator = Some(decorator);
484 self
485 }
486
487 pub fn subscribe(&self) -> broadcast::Receiver<AgentLifecycleEvent> {
489 self.lifecycle_tx.subscribe()
490 }
491
492 pub fn spawn(&self, config: AgentConfig) -> anyhow::Result<AgentHandle> {
506 let agent = if let (Some(oxicode), Some(decorator)) =
507 (self.oxicode.as_ref(), self.agent_decorator.as_ref())
508 {
509 let builder = oxicode.agent(config.clone());
510 let builder = decorator.decorate(builder);
511 builder.build()?
512 } else {
513 let model = self
514 .resolver
515 .resolve_model(&config.model_id)
516 .ok_or_else(|| SdkError::ModelNotFound {
517 model_id: config.model_id.clone(),
518 })?;
519 let provider = self
520 .resolver
521 .resolve_provider(&model.provider)
522 .ok_or_else(|| SdkError::ProviderNotFound {
523 provider: model.provider.clone(),
524 })?;
525 let tools = Arc::new(ToolRegistry::new());
526 oxicode_agent::Agent::new(provider, config.clone(), tools)
527 };
528
529 let handle = AgentHandle::new(agent, config.clone(), None, self.lifecycle_tx.clone());
530
531 self.agents
532 .write()
533 .insert(handle.agent_id().to_string(), handle.clone());
534
535 self.emit(AgentLifecycleEvent::Spawned {
536 agent_id: handle.agent_id().to_string(),
537 parent_id: None,
538 model_id: config.model_id.clone(),
539 timestamp_ms: AgentLifecycleEvent::now_ms(),
540 });
541
542 Ok(handle)
543 }
544
545 pub fn spawn_child(&self, parent_id: &str, config: AgentConfig) -> anyhow::Result<AgentHandle> {
547 let model = self
548 .resolver
549 .resolve_model(&config.model_id)
550 .ok_or_else(|| SdkError::ModelNotFound {
551 model_id: config.model_id.clone(),
552 })?;
553 let provider = self
554 .resolver
555 .resolve_provider(&model.provider)
556 .ok_or_else(|| SdkError::ProviderNotFound {
557 provider: model.provider.clone(),
558 })?;
559
560 let tools = Arc::new(ToolRegistry::new());
561 let agent = oxicode_agent::Agent::new(provider, config.clone(), tools);
562
563 let handle = AgentHandle::new(
564 agent,
565 config.clone(),
566 Some(parent_id.to_string()),
567 self.lifecycle_tx.clone(),
568 );
569
570 self.agents
571 .write()
572 .insert(handle.agent_id().to_string(), handle.clone());
573
574 self.emit(AgentLifecycleEvent::Spawned {
575 agent_id: handle.agent_id().to_string(),
576 parent_id: Some(parent_id.to_string()),
577 model_id: config.model_id.clone(),
578 timestamp_ms: AgentLifecycleEvent::now_ms(),
579 });
580
581 Ok(handle)
582 }
583
584 pub fn get(&self, agent_id: &str) -> Option<AgentHandle> {
586 self.agents.read().get(agent_id).cloned()
587 }
588
589 pub fn list(&self) -> Vec<(String, AgentStatus)> {
591 self.agents
592 .read()
593 .iter()
594 .map(|(id, h)| (id.clone(), h.status()))
595 .collect()
596 }
597
598 pub fn count_by_status(&self) -> HashMap<AgentStatus, usize> {
600 let mut counts = HashMap::new();
601 for handle in self.agents.read().values() {
602 *counts.entry(handle.status()).or_insert(0) += 1;
603 }
604 counts
605 }
606
607 pub async fn suspend(&self, agent_id: &str) -> anyhow::Result<AgentSnapshot> {
611 let handle = self
612 .get(agent_id)
613 .ok_or_else(|| SdkError::SnapshotNotFound {
614 agent_id: agent_id.to_string(),
615 })?;
616 let snapshot = handle.suspend().await?;
617 self.snapshot_store.save(&snapshot).await?;
618 Ok(snapshot)
619 }
620
621 pub async fn restore(&self, agent_id: &str) -> anyhow::Result<AgentHandle> {
623 if let Some(handle) = self.get(agent_id) {
625 return Ok(handle);
626 }
627
628 let snapshot = self.snapshot_store.load(agent_id).await?.ok_or_else(|| {
629 SdkError::SnapshotNotFound {
630 agent_id: agent_id.to_string(),
631 }
632 })?;
633
634 self.restore_from_snapshot(snapshot).await
635 }
636
637 pub async fn restore_from_snapshot(
639 &self,
640 snapshot: AgentSnapshot,
641 ) -> anyhow::Result<AgentHandle> {
642 let model = self
643 .resolver
644 .resolve_model(&snapshot.config.model_id)
645 .ok_or_else(|| SdkError::ModelNotFound {
646 model_id: snapshot.config.model_id.clone(),
647 })?;
648 let provider = self
649 .resolver
650 .resolve_provider(&model.provider)
651 .ok_or_else(|| SdkError::ProviderNotFound {
652 provider: model.provider.clone(),
653 })?;
654
655 let tools = Arc::new(ToolRegistry::new());
656 let agent = oxicode_agent::Agent::new(provider, snapshot.config.clone(), tools);
657
658 let state_json = serde_json::to_value(&snapshot.state)?;
660 agent.import_state(state_json)?;
661
662 let handle = AgentHandle::new(
663 agent,
664 snapshot.config.clone(),
665 snapshot.parent_id.clone(),
666 self.lifecycle_tx.clone(),
667 );
668
669 self.agents
670 .write()
671 .insert(handle.agent_id().to_string(), handle.clone());
672
673 self.emit(AgentLifecycleEvent::Resumed {
674 agent_id: handle.agent_id().to_string(),
675 from_snapshot_id: Some(snapshot.agent_id.clone()),
676 timestamp_ms: AgentLifecycleEvent::now_ms(),
677 });
678
679 Ok(handle)
680 }
681
682 pub fn terminate(&self, agent_id: &str) -> anyhow::Result<()> {
684 let handle = self
685 .get(agent_id)
686 .ok_or_else(|| SdkError::SnapshotNotFound {
687 agent_id: agent_id.to_string(),
688 })?;
689
690 if handle.is_running() {
691 return Err(SdkError::AgentNotRunnable {
692 agent_id: agent_id.to_string(),
693 status: "running".to_string(),
694 }
695 .into());
696 }
697
698 handle.terminate()?;
699 self.agents.write().remove(agent_id);
700 self.restart_log.write().remove(agent_id);
701
702 Ok(())
703 }
704
705 pub fn can_restart(&self, agent_id: &str) -> bool {
713 if self.policy.max_restarts == 0 {
714 return false;
715 }
716 let now = AgentLifecycleEvent::now_ms();
717 let window_ms = self.policy.restart_window_secs * 1000;
718 let log = self.restart_log.read();
719 let restarts = log
720 .get(agent_id)
721 .map(|ts| {
722 ts.iter()
723 .filter(|&&t| now.saturating_sub(t) <= window_ms)
724 .count()
725 })
726 .unwrap_or(0);
727 restarts < self.policy.max_restarts
728 }
729
730 pub async fn restart(&self, agent_id: &str) -> SdkResult<AgentHandle> {
735 if !self.can_restart(agent_id) {
736 return Err(SdkError::InvalidState {
737 entity: "agent".into(),
738 reason: format!(
739 "agent '{}' exceeded max restarts ({})",
740 agent_id, self.policy.max_restarts
741 ),
742 });
743 }
744
745 let old = self.agents.read().get(agent_id).cloned();
747 let config = match &old {
748 Some(h) => h.config.read().clone(),
749 None => {
750 return Err(SdkError::SnapshotNotFound {
751 agent_id: agent_id.to_string(),
752 });
753 }
754 };
755
756 if let Some(delay) = self.compute_backoff(agent_id) {
758 tokio::time::sleep(delay).await;
759 }
760
761 self.restart_log
763 .write()
764 .entry(agent_id.to_string())
765 .or_default()
766 .push(AgentLifecycleEvent::now_ms());
767
768 self.agents.write().remove(agent_id);
770
771 self.spawn(config).map_err(SdkError::from)
773 }
774
775 fn compute_backoff(&self, agent_id: &str) -> Option<std::time::Duration> {
777 let count = self
778 .restart_log
779 .read()
780 .get(agent_id)
781 .map(|ts| ts.len())
782 .unwrap_or(0);
783 if count == 0 {
784 return None;
785 }
786 match &self.policy.backoff {
787 RestartBackoff::None => None,
788 RestartBackoff::Fixed { delay_ms } => Some(std::time::Duration::from_millis(*delay_ms)),
789 RestartBackoff::Exponential { base_ms, max_ms } => {
790 let delay = (*base_ms).saturating_mul(2u64.saturating_pow(count as u32));
791 Some(std::time::Duration::from_millis(delay.min(*max_ms)))
792 }
793 }
794 }
795
796 fn emit(&self, event: AgentLifecycleEvent) {
799 let _ = self.lifecycle_tx.send(event);
800 }
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806 use std::future::Future;
807 use std::pin::Pin;
808 use std::sync::Arc;
809
810 fn mock_resolver() -> Arc<dyn ProviderResolver> {
813 struct MockProvider;
814 impl oxicode_ai::Provider for MockProvider {
815 fn stream<'a>(
816 &'a self,
817 _model: &'a oxicode_ai::Model,
818 _context: &'a oxicode_ai::Context,
819 _options: Option<oxicode_ai::StreamOptions>,
820 ) -> Pin<Box<dyn Future<Output = oxicode_ai::StreamResult> + Send + 'a>> {
821 Box::pin(
822 async move { Err(oxicode_ai::ProviderError::NotImplemented("mock".into())) },
823 )
824 }
825 }
826
827 struct Mock;
828 impl ProviderResolver for Mock {
829 fn resolve_provider(&self, _name: &str) -> Option<Arc<dyn oxicode_ai::Provider>> {
830 Some(Arc::new(MockProvider))
831 }
832 fn resolve_model(&self, _model_id: &str) -> Option<oxicode_ai::Model> {
833 Some(oxicode_ai::Model::new(
834 "anthropic/claude-sonnet-4-20250514",
835 "Claude",
836 oxicode_ai::Api::AnthropicMessages,
837 "anthropic",
838 "https://api.anthropic.com",
839 ))
840 }
841 }
842 Arc::new(Mock)
843 }
844
845 struct NoopStore;
846
847 impl SnapshotStore for NoopStore {
848 fn save<'a>(
849 &'a self,
850 _snapshot: &'a AgentSnapshot,
851 ) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + 'a>> {
852 Box::pin(async { Ok(()) })
853 }
854 fn load<'a>(
855 &'a self,
856 _agent_id: &'a str,
857 ) -> Pin<Box<dyn Future<Output = anyhow::Result<Option<AgentSnapshot>>> + Send + 'a>>
858 {
859 Box::pin(async { Ok(None) })
860 }
861 fn list(&self) -> Pin<Box<dyn Future<Output = anyhow::Result<Vec<String>>> + Send + '_>> {
862 Box::pin(async { Ok(vec![]) })
863 }
864 fn delete<'a>(
865 &'a self,
866 _agent_id: &'a str,
867 ) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + 'a>> {
868 Box::pin(async { Ok(()) })
869 }
870 }
871
872 fn make_supervisor() -> AgentSupervisor {
873 AgentSupervisor::new(
874 mock_resolver(),
875 Arc::new(NoopStore) as Arc<dyn SnapshotStore>,
876 )
877 }
878
879 fn test_config() -> AgentConfig {
880 AgentConfig {
881 model_id: "anthropic/claude-sonnet-4-20250514".into(),
882 name: uuid::Uuid::new_v4().to_string(),
883 ..Default::default()
884 }
885 }
886
887 #[test]
890 fn supervisor_policy_default() {
891 let policy = SupervisorPolicy::default();
892 assert_eq!(policy.max_restarts, 3);
893 assert!(matches!(policy.backoff, RestartBackoff::Exponential { .. }));
894 }
895
896 #[test]
897 fn supervisor_policy_no_restart() {
898 let policy = SupervisorPolicy::no_restart();
899 assert_eq!(policy.max_restarts, 0);
900 assert!(matches!(policy.backoff, RestartBackoff::None));
901 }
902
903 #[test]
904 fn supervisor_spawn_and_get() {
905 let supervisor = make_supervisor();
906 let handle = supervisor.spawn(test_config()).unwrap();
907 assert!(!handle.agent_id().is_empty());
908 assert_eq!(handle.status(), AgentStatus::Created);
909 assert_eq!(handle.parent_id(), None);
910 }
911
912 #[test]
913 fn supervisor_spawn_child() {
914 let supervisor = make_supervisor();
915 let parent = supervisor.spawn(test_config()).unwrap();
916 let child = supervisor
917 .spawn_child(parent.agent_id(), test_config())
918 .unwrap();
919 assert_eq!(child.parent_id(), Some(parent.agent_id()));
920 }
921
922 #[test]
923 fn supervisor_terminate() {
924 let supervisor = make_supervisor();
925 let handle = supervisor.spawn(test_config()).unwrap();
926 let id = handle.agent_id().to_string();
927 supervisor.terminate(&id).unwrap();
928 assert!(supervisor.get(&id).is_none());
929 }
930
931 #[test]
932 fn supervisor_list_and_count() {
933 let supervisor = make_supervisor();
934 supervisor.spawn(test_config()).unwrap();
935 supervisor.spawn(test_config()).unwrap();
936
937 let list = supervisor.list();
938 assert_eq!(list.len(), 2);
939
940 let counts = supervisor.count_by_status();
941 assert_eq!(counts.get(&AgentStatus::Created), Some(&2));
942 }
943
944 #[test]
945 fn handle_status_transitions() {
946 let supervisor = make_supervisor();
947 let handle = supervisor.spawn(test_config()).unwrap();
948
949 handle.terminate().unwrap();
951 assert_eq!(handle.status(), AgentStatus::Terminated);
952 assert!(handle.status().is_terminal());
953
954 assert!(handle.terminate().is_err());
956 }
957
958 #[test]
959 fn handle_switch_model() {
960 let supervisor = make_supervisor();
961 let handle = supervisor.spawn(test_config()).unwrap();
962 let result = handle.switch_model("openai/gpt-4o");
964 let _ = result;
967 }
968
969 #[test]
970 fn handle_set_system_prompt() {
971 let supervisor = make_supervisor();
972 let handle = supervisor.spawn(test_config()).unwrap();
973 handle.set_system_prompt("You are a test agent.".into());
974 assert_eq!(
976 handle.config.read().system_prompt,
977 Some("You are a test agent.".into())
978 );
979 }
980
981 #[test]
982 fn handle_snapshot() {
983 let supervisor = make_supervisor();
984 let handle = supervisor.spawn(test_config()).unwrap();
985 let snap = handle.snapshot().unwrap();
986 assert_eq!(snap.agent_id, handle.agent_id());
987 }
988
989 #[test]
990 fn lifecycle_events_received() {
991 let supervisor = make_supervisor();
992 let mut rx = supervisor.subscribe();
993 supervisor.spawn(test_config()).unwrap();
994
995 let event = rx.try_recv().expect("should receive Spawned event");
996 match event {
997 AgentLifecycleEvent::Spawned { agent_id, .. } => {
998 assert!(!agent_id.is_empty());
999 }
1000 _ => panic!("Expected Spawned event"),
1001 }
1002 }
1003
1004 #[test]
1007 fn handle_has_routing_control() {
1008 let supervisor = make_supervisor();
1009 let handle = supervisor.spawn(test_config()).unwrap();
1010 assert!(handle.routing().is_enabled());
1012 }
1013
1014 #[test]
1015 fn handle_routing_toggle() {
1016 let supervisor = make_supervisor();
1017 let handle = supervisor.spawn(test_config()).unwrap();
1018 handle.disable_routing();
1019 assert!(!handle.routing().is_enabled());
1020 handle.enable_routing();
1021 assert!(handle.routing().is_enabled());
1022 }
1023
1024 #[test]
1025 fn handle_routing_exclude_model() {
1026 let supervisor = make_supervisor();
1027 let handle = supervisor.spawn(test_config()).unwrap();
1028 handle.exclude_route_model("openai/gpt-4o");
1029 assert!(
1030 handle
1031 .routing()
1032 .excluded_models()
1033 .contains(&"openai/gpt-4o".to_string())
1034 );
1035 }
1036
1037 #[test]
1038 fn handle_routing_fallback_models() {
1039 let supervisor = make_supervisor();
1040 let handle = supervisor.spawn(test_config()).unwrap();
1041 handle
1042 .routing()
1043 .set_fallback_models(vec!["anthropic/claude-sonnet-4-20250514".into()]);
1044 assert_eq!(handle.routing().fallback_models().len(), 1);
1045 }
1046
1047 #[test]
1048 fn supervisor_can_restart_default_policy() {
1049 let supervisor = make_supervisor();
1050 let handle = supervisor.spawn(test_config()).unwrap();
1051 let id = handle.agent_id().to_string();
1052 assert!(supervisor.can_restart(&id));
1054 }
1055
1056 #[test]
1057 fn supervisor_cannot_restart_no_restart_policy() {
1058 let supervisor = AgentSupervisor::with_policy(
1059 mock_resolver(),
1060 Arc::new(NoopStore) as Arc<dyn SnapshotStore>,
1061 SupervisorPolicy::no_restart(),
1062 );
1063 let handle = supervisor.spawn(test_config()).unwrap();
1064 let id = handle.agent_id().to_string();
1065 assert!(!supervisor.can_restart(&id));
1066 }
1067
1068 #[tokio::test]
1069 async fn supervisor_restart_with_no_restart_policy_fails() {
1070 let supervisor = AgentSupervisor::with_policy(
1071 mock_resolver(),
1072 Arc::new(NoopStore) as Arc<dyn SnapshotStore>,
1073 SupervisorPolicy::no_restart(),
1074 );
1075 let handle = supervisor.spawn(test_config()).unwrap();
1076 let id = handle.agent_id().to_string();
1077 let result = supervisor.restart(&id).await;
1078 assert!(result.is_err());
1079 }
1080
1081 #[tokio::test]
1082 async fn supervisor_restart_spawns_new_agent() {
1083 let policy = SupervisorPolicy {
1085 max_restarts: 3,
1086 restart_window_secs: 60,
1087 backoff: RestartBackoff::Fixed { delay_ms: 0 },
1088 };
1089 let supervisor = AgentSupervisor::with_policy(
1090 mock_resolver(),
1091 Arc::new(NoopStore) as Arc<dyn SnapshotStore>,
1092 policy,
1093 );
1094 let handle = supervisor.spawn(test_config()).unwrap();
1095 let old_id = handle.agent_id().to_string();
1096
1097 let new_handle = supervisor.restart(&old_id).await.unwrap();
1098 assert!(supervisor.get(new_handle.agent_id()).is_some());
1100 assert_eq!(new_handle.status(), AgentStatus::Created);
1101 let log = supervisor.restart_log.read();
1104 assert!(log.values().any(|ts| !ts.is_empty()));
1105 }
1106
1107 #[tokio::test]
1108 async fn supervisor_restart_respects_max_restarts() {
1109 let policy = SupervisorPolicy {
1110 max_restarts: 1,
1111 restart_window_secs: 60,
1112 backoff: RestartBackoff::None,
1113 };
1114 let supervisor = AgentSupervisor::with_policy(
1115 mock_resolver(),
1116 Arc::new(NoopStore) as Arc<dyn SnapshotStore>,
1117 policy,
1118 );
1119 let handle = supervisor.spawn(test_config()).unwrap();
1120 let id = handle.agent_id().to_string();
1121
1122 let first = supervisor.restart(&id).await.unwrap();
1124 let first_id = first.agent_id().to_string();
1125
1126 assert!(!supervisor.can_restart(&first_id));
1128 let result = supervisor.restart(&first_id).await;
1129 assert!(result.is_err());
1130 }
1131}