1use anyhow::Result;
20use async_trait::async_trait;
21use chrono::Utc;
22use oxi_sdk::Agent;
23use oxios_ouroboros::{Directive, ExecEnv};
24use parking_lot::RwLock;
25use std::collections::HashMap;
26use std::sync::Arc;
27use std::sync::atomic::{AtomicBool, Ordering};
28use tokio::task::JoinHandle;
29
30use crate::agent_runtime::AgentRuntime;
31use crate::config::AgentLogConfig;
32use crate::event_bus::EventBus;
33use crate::resilience::classify;
34use crate::resource_monitor::ResourceMonitor;
35use crate::session_context::SessionContext;
36use crate::state_store::StateStore;
37use crate::types::{AgentId, AgentInfo, AgentStatus};
38
39use oxios_ouroboros::ExecutionResult;
40
41#[cfg(feature = "sqlite-memory")]
42use crate::agent_log_db::AgentLogDb;
43
44struct AgentHandle {
46 cancelled: Arc<AtomicBool>,
48 task: JoinHandle<()>,
50}
51
52#[derive(Default)]
59pub struct AgentPool {
60 agents: RwLock<HashMap<AgentId, Arc<Agent>>>,
61}
62
63impl AgentPool {
64 pub fn new() -> Self {
66 Self {
67 agents: RwLock::new(HashMap::new()),
68 }
69 }
70
71 pub fn insert(&self, id: AgentId, agent: Arc<Agent>) {
73 self.agents.write().insert(id, agent);
74 }
75
76 pub fn get(&self, id: &AgentId) -> Option<Arc<Agent>> {
78 self.agents.read().get(id).cloned()
79 }
80
81 pub fn remove(&self, id: &AgentId) -> Option<Arc<Agent>> {
83 self.agents.write().remove(id)
84 }
85
86 pub fn export_state(&self, id: &AgentId) -> Option<serde_json::Value> {
90 self.agents
91 .read()
92 .get(id)
93 .and_then(|agent| agent.export_state().ok())
94 }
95
96 pub fn import_state(&self, id: &AgentId, state: serde_json::Value) -> bool {
100 if let Some(agent) = self.agents.read().get(id) {
101 agent.import_state(state).is_ok()
102 } else {
103 false
104 }
105 }
106
107 pub fn len(&self) -> usize {
109 self.agents.read().len()
110 }
111
112 pub fn is_empty(&self) -> bool {
114 self.agents.read().is_empty()
115 }
116}
117
118#[async_trait]
120pub trait Supervisor: Send + Sync {
121 async fn exec(&self, id: AgentId) -> Result<()>;
123
124 async fn fork_directive(&self, directive: &Directive, env: &ExecEnv) -> Result<AgentId>;
128
129 async fn run_with_directive(
133 &self,
134 id: AgentId,
135 directive: &Directive,
136 env: &ExecEnv,
137 ) -> Result<ExecutionResult>;
138
139 async fn wait(&self, id: AgentId) -> Result<AgentStatus>;
141
142 async fn kill(&self, id: AgentId) -> Result<()>;
144
145 async fn list(&self) -> Result<Vec<AgentInfo>>;
147}
148
149pub struct BasicSupervisor {
151 agents: RwLock<HashMap<AgentId, AgentInfo>>,
152 handles: RwLock<HashMap<AgentId, AgentHandle>>,
154 agent_pool: AgentPool,
156 event_bus: EventBus,
157 runtime: Arc<AgentRuntime>,
158 resource_monitor: Option<Arc<ResourceMonitor>>,
159 session_context: Arc<tokio::sync::RwLock<SessionContext>>,
165 state_store: Option<Arc<StateStore>>,
167 #[cfg(feature = "sqlite-memory")]
169 agent_log_db: Option<Arc<AgentLogDb>>,
170 agent_log_config: AgentLogConfig,
172}
173
174impl BasicSupervisor {
175 pub fn new(event_bus: EventBus, runtime: AgentRuntime) -> Self {
177 Self {
178 agents: RwLock::new(HashMap::new()),
179 handles: RwLock::new(HashMap::new()),
180 agent_pool: AgentPool::new(),
181 event_bus,
182 runtime: Arc::new(runtime),
183 resource_monitor: None,
184 session_context: Arc::new(tokio::sync::RwLock::new(SessionContext::new())),
185 state_store: None,
186 #[cfg(feature = "sqlite-memory")]
187 agent_log_db: None,
188 agent_log_config: AgentLogConfig::default(),
189 }
190 }
191
192 pub fn set_state_store(&mut self, store: Arc<StateStore>) {
194 self.state_store = Some(store);
195 }
196
197 #[cfg(feature = "sqlite-memory")]
199 pub fn set_agent_log_db(&mut self, db: Arc<AgentLogDb>) {
200 self.agent_log_db = Some(db);
201 }
202
203 pub fn set_agent_log_config(&mut self, config: AgentLogConfig) {
205 self.agent_log_config = config;
206 }
207
208 pub fn set_resource_monitor(&mut self, rm: Arc<ResourceMonitor>) {
210 self.resource_monitor = Some(rm);
211 }
212
213 fn update_agent_count(&self) {
215 if let Some(ref rm) = self.resource_monitor {
216 let count = self.agents.read().len();
217 rm.set_active_agents(count);
218 }
219 }
220
221 pub fn pool(&self) -> &AgentPool {
223 &self.agent_pool
224 }
225}
226
227#[async_trait]
228impl Supervisor for BasicSupervisor {
229 async fn fork_directive(&self, directive: &Directive, env: &ExecEnv) -> Result<AgentId> {
230 let id = AgentId::new_v4();
231 let info = AgentInfo {
232 id,
233 name: directive.goal.clone(),
234 status: AgentStatus::Starting,
235 created_at: Utc::now(),
236 project_id: env.project_id,
237 started_at: None,
238 completed_at: None,
239 error: None,
240 steps_completed: 0,
241 steps_total: None,
242 tool_calls: vec![],
243 tokens_input: 0,
244 tokens_output: 0,
245 cost_usd: 0.0,
246 model_id: String::new(),
247 session_id: None,
248 };
249
250 {
251 let mut agents = self.agents.write();
252 agents.insert(id, info);
253 }
254
255 self.update_agent_count();
256
257 let _ = self
258 .event_bus
259 .publish(crate::event_bus::KernelEvent::AgentCreated {
260 id,
261 name: directive.goal.clone(),
262 });
263
264 tracing::info!(agent_id = %id, "Forked new agent from directive");
265 Ok(id)
266 }
267
268 async fn exec(&self, id: AgentId) -> Result<()> {
269 {
270 let mut agents = self.agents.write();
271 match agents.get_mut(&id) {
272 Some(agent) => {
273 agent.status = AgentStatus::Running;
274 }
275 None => anyhow::bail!("Agent {id} not found"),
276 }
277 }
278
279 self.update_agent_count();
280
281 let _ = self
282 .event_bus
283 .publish(crate::event_bus::KernelEvent::AgentStarted { id });
284 tracing::info!(agent_id = %id, "Agent execution started");
285
286 Ok(())
287 }
288
289 async fn run_with_directive(
290 &self,
291 id: AgentId,
292 directive: &Directive,
293 env: &ExecEnv,
294 ) -> Result<ExecutionResult> {
295 {
297 let mut agents = self.agents.write();
298 match agents.get_mut(&id) {
299 Some(agent) => {
300 agent.status = AgentStatus::Running;
301 agent.started_at = Some(Utc::now());
302 }
303 None => anyhow::bail!("Agent {id} not found"),
304 }
305 }
306
307 let _ = self
308 .event_bus
309 .publish(crate::event_bus::KernelEvent::AgentStarted { id });
310
311 tracing::info!(agent_id = %id, "Running agent task from directive");
312
313 let cancelled = Arc::new(AtomicBool::new(false));
315 let runtime = Arc::clone(&self.runtime);
316 let directive = directive.clone();
317 let env = env.clone();
318
319 let session_ctx = self.session_context.clone();
322
323 let (done_tx, done_rx) = tokio::sync::oneshot::channel::<Result<ExecutionResult>>();
324 let cancelled_done = cancelled.clone();
325 let handle: JoinHandle<()> = tokio::spawn(async move {
326 let result = if cancelled_done.load(Ordering::Relaxed) {
328 Ok(ExecutionResult {
329 output: "Agent cancelled before execution".into(),
330 steps_completed: 0,
331 success: false,
332 tool_calls: vec![],
333 tokens_input: 0,
334 tokens_output: 0,
335 model_id: String::new(),
336 failure_class: None, restore_state: None,
338 reasoning_text: String::new(),
339 reasoning_segments: Vec::new(),
340 })
341 } else {
342 let mut temp_ctx = {
349 let ctx = session_ctx.read().await;
350 let mut c = crate::session_context::SessionContext::new();
351 c.recall_timing = ctx.recall_timing.clone();
352 c
353 };
354 let exec_result = runtime
355 .execute_directive(id, &directive, &env, &mut temp_ctx)
356 .await;
357 session_ctx.write().await.recall_timing = temp_ctx.recall_timing;
360 exec_result
361 };
362 let _ = done_tx.send(result);
364 });
365
366 {
368 let mut handles = self.handles.write();
369 handles.insert(
370 id,
371 AgentHandle {
372 cancelled,
373 task: handle,
374 },
375 );
376 }
377
378 let result = match done_rx.await {
382 Ok(res) => res,
383 Err(_) => {
384 let mut handles = self.handles.write();
385 handles.remove(&id);
386 Ok(ExecutionResult {
387 output: "Agent task aborted".into(),
388 steps_completed: 0,
389 success: false,
390 tool_calls: vec![],
391 tokens_input: 0,
392 tokens_output: 0,
393 model_id: String::new(),
394 failure_class: None, restore_state: None,
396 reasoning_text: String::new(),
397 reasoning_segments: Vec::new(),
398 })
399 }
400 };
401
402 {
404 let mut handles = self.handles.write();
405 handles.remove(&id);
406 }
407
408 match result {
409 Ok(result) => {
410 tracing::info!(
411 agent_id = %id,
412 success = result.success,
413 steps = result.steps_completed,
414 "Agent task completed (directive)"
415 );
416
417 {
418 let mut agents = self.agents.write();
419 if let Some(agent) = agents.get_mut(&id) {
420 agent.status = if result.success {
421 AgentStatus::Completed
422 } else {
423 AgentStatus::Failed
424 };
425 agent.completed_at = Some(Utc::now());
426 agent.steps_completed = result.steps_completed;
427 agent.tool_calls = result
428 .tool_calls
429 .iter()
430 .map(|tc| crate::types::ToolCallRecord {
431 tool: tc.tool.clone(),
432 input: tc.input.clone(),
433 output: tc.output.clone(),
434 duration_ms: tc.duration_ms,
435 is_error: tc.is_error,
436 tool_call_id: tc.tool_call_id.clone(),
437 timestamp: tc.timestamp,
438 })
439 .collect();
440 agent.tokens_input = result.tokens_input;
441 agent.tokens_output = result.tokens_output;
442 agent.model_id = result.model_id.clone();
443 agent.cost_usd = if !result.model_id.is_empty() {
444 crate::kernel_handle::engine_api::estimate_cost(
445 &result.model_id,
446 result.tokens_input,
447 result.tokens_output,
448 )
449 } else {
450 0.0
451 };
452 if !result.success {
453 agent.error = Some(result.output.clone());
454 }
455 }
456 }
457
458 let _ = self
459 .event_bus
460 .publish(crate::event_bus::KernelEvent::AgentStopped {
461 id,
462 success: result.success,
463 });
464 self.update_agent_count();
465
466 self.persist_agent(id).await;
468
469 Ok(result)
470 }
471 Err(e) => {
472 tracing::error!(agent_id = %id, error = %e, "Agent task failed (directive)");
473
474 {
475 let mut agents = self.agents.write();
476 if let Some(agent) = agents.get_mut(&id) {
477 agent.status = AgentStatus::Failed;
478 agent.completed_at = Some(Utc::now());
479 agent.error = Some(e.to_string());
480 }
481 }
482
483 let _ = self
484 .event_bus
485 .publish(crate::event_bus::KernelEvent::AgentFailed {
486 id,
487 error: e.to_string(),
488 });
489 self.update_agent_count();
490
491 self.persist_agent(id).await;
493
494 Ok(ExecutionResult {
495 output: format!("Agent failed: {e}"),
496 steps_completed: 0,
497 success: false,
498 tool_calls: vec![],
499 tokens_input: 0,
500 tokens_output: 0,
501 model_id: String::new(),
502 reasoning_text: String::new(),
503 reasoning_segments: Vec::new(),
504 failure_class: Some(classify(&e)),
510 restore_state: e
511 .downcast_ref::<crate::resilience::AgentRunError>()
512 .and_then(|err| err.restore_state.clone()),
513 })
514 }
515 }
516 }
517
518 async fn wait(&self, id: AgentId) -> Result<AgentStatus> {
519 let agents = self.agents.read();
520 match agents.get(&id) {
521 Some(info) => Ok(info.status),
522 None => anyhow::bail!("Agent {id} not found"),
523 }
524 }
525
526 async fn kill(&self, id: AgentId) -> Result<()> {
527 {
529 let mut handles = self.handles.write();
530 if let Some(agent_handle) = handles.remove(&id) {
531 agent_handle.cancelled.store(true, Ordering::Relaxed);
532 agent_handle.task.abort();
533 tracing::info!(agent_id = %id, "Agent task aborted");
534 }
535 }
536
537 {
538 let mut agents = self.agents.write();
539 if let Some(agent) = agents.get_mut(&id) {
540 agent.status = AgentStatus::Stopped;
541 agent.completed_at = Some(Utc::now());
542 } else {
543 anyhow::bail!("Agent {id} not found");
544 }
545 }
546
547 let _ = self
548 .event_bus
549 .publish(crate::event_bus::KernelEvent::AgentStopped { id, success: false });
550 self.update_agent_count();
551
552 self.persist_agent(id).await;
554
555 tracing::info!(agent_id = %id, "Agent killed");
556 Ok(())
557 }
558
559 async fn list(&self) -> Result<Vec<AgentInfo>> {
560 let agents = self.agents.read();
561 Ok(agents.values().cloned().collect())
562 }
563}
564
565impl BasicSupervisor {
566 async fn persist_agent(&self, id: AgentId) {
569 let info = {
571 let agents = self.agents.read();
572 agents.get(&id).cloned()
573 };
574
575 let Some(info) = info else { return };
576
577 if let Some(ref store) = self.state_store {
579 let store = store.clone();
580 let info = info.clone();
581 let max_entries = self.agent_log_config.max_entries;
582 let ttl_hours = self.agent_log_config.ttl_hours;
583 let batch_size = self.agent_log_config.prune_batch_size;
584 tokio::spawn(async move {
585 let _ = store
586 .save_json("agents", &id.to_string(), &info)
587 .await
588 .inspect_err(|e| tracing::warn!(agent_id = %id, error = %e, "Failed to persist agent to filesystem"));
589
590 if max_entries > 0 || ttl_hours > 0 {
592 let _ = store
593 .prune_agents_by_config(max_entries, ttl_hours, batch_size)
594 .await
595 .inspect_err(|e| tracing::warn!(error = %e, "Failed to prune agent log"));
596 }
597 });
598 }
599
600 #[cfg(feature = "sqlite-memory")]
602 if let Some(ref db) = self.agent_log_db {
603 let db = db.clone();
604 let info = info.clone();
605 let config = self.agent_log_config.clone();
606 tokio::spawn(async move {
607 let _ = db
608 .upsert_agent(&info)
609 .inspect_err(|e| tracing::warn!(agent_id = %id, error = %e, "Failed to upsert agent to SQLite"));
610
611 let _ = db
613 .prune(&config)
614 .inspect_err(|e| tracing::warn!(error = %e, "Failed to prune agent SQLite"));
615 });
616 }
617 }
618}
619
620pub struct NoOpSupervisor;
626
627#[async_trait::async_trait]
628impl Supervisor for NoOpSupervisor {
629 async fn exec(&self, _id: AgentId) -> Result<()> {
630 Err(anyhow::anyhow!(
631 "NoOpSupervisor: exec not available during build"
632 ))
633 }
634 async fn fork_directive(&self, _directive: &Directive, _env: &ExecEnv) -> Result<AgentId> {
635 Err(anyhow::anyhow!(
636 "NoOpSupervisor: fork_directive not available during build"
637 ))
638 }
639 async fn run_with_directive(
640 &self,
641 _id: AgentId,
642 _directive: &Directive,
643 _env: &ExecEnv,
644 ) -> Result<ExecutionResult> {
645 Err(anyhow::anyhow!(
646 "NoOpSupervisor: run_with_directive not available during build"
647 ))
648 }
649 async fn wait(&self, _id: AgentId) -> Result<AgentStatus> {
650 Err(anyhow::anyhow!(
651 "NoOpSupervisor: wait not available during build"
652 ))
653 }
654 async fn kill(&self, _id: AgentId) -> Result<()> {
655 Err(anyhow::anyhow!(
656 "NoOpSupervisor: kill not available during build"
657 ))
658 }
659 async fn list(&self) -> Result<Vec<AgentInfo>> {
660 Ok(Vec::new())
661 }
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use crate::event_bus::EventBus;
668 use crate::types::AgentStatus;
669
670 async fn make_supervisor() -> BasicSupervisor {
675 let event_bus = EventBus::new(64);
676
677 let tmp = std::env::temp_dir().join(format!("oxios-test-{}", uuid::Uuid::new_v4()));
679 let _ = std::fs::create_dir_all(&tmp);
680
681 let state_store_2 =
682 Arc::new(crate::state_store::StateStore::new(tmp.join("state")).expect("state store"));
683 let state_store = state_store_2.clone();
684 let memory_manager = Arc::new({
685 let mut mm = crate::memory::MemoryManager::new(state_store.clone());
686 mm.set_git_layer(Arc::new(
687 crate::git_layer::GitLayer::new(tmp.join("git"), false).expect("git layer"),
688 ));
689 mm
690 });
691
692 let kernel_handle = Arc::new(crate::KernelHandle::new(
693 crate::kernel_handle::StateApi::new(state_store),
694 crate::kernel_handle::AgentApi::new(
695 Arc::new(crate::supervisor::NoOpSupervisor),
696 Arc::new(crate::budget::BudgetManager::new()),
697 memory_manager.clone(),
698 Some(event_bus.clone()),
699 ),
700 crate::kernel_handle::SecurityApi::new(
701 Arc::new(parking_lot::Mutex::new(crate::auth::AuthManager::new())),
702 Arc::new(oxi_sdk::observability::AuditTrail::new(100)),
703 Arc::new(parking_lot::Mutex::new(
704 crate::access_manager::AccessManager::new(),
705 )),
706 Arc::new(
707 crate::state_store::StateStore::new(tmp.join("state2")).expect("state store 2"),
708 ),
709 ),
710 crate::kernel_handle::PersonaApi::new(Arc::new(crate::persona::PersonaManager::new())),
711 crate::kernel_handle::ExtensionApi::new(Arc::new(crate::skill::SkillManager::new(
712 tmp.join("skills"),
713 tmp.join("share/skills"),
714 ))),
715 crate::kernel_handle::McpApi::new(Arc::new(crate::mcp::McpBridge::new())),
716 crate::kernel_handle::InfraApi::new(
717 Arc::new(
718 crate::git_layer::GitLayer::new(tmp.join("git"), false).expect("git layer"),
719 ),
720 Arc::new(crate::cron::CronScheduler::new(
721 Arc::new(
722 crate::state_store::StateStore::new(tmp.join("cron")).expect("cron state"),
723 ),
724 60,
725 )),
726 Arc::new(crate::resource_monitor::ResourceMonitor::new(60, 100)),
727 EventBus::new(64),
728 crate::config::OxiosConfig::default(),
729 std::time::Instant::now(),
730 std::sync::Arc::new(crate::tools::PendingToolApprovals::new()),
731 std::sync::Arc::new(crate::tools::PendingAskUser::new()),
732 std::sync::Arc::new(parking_lot::RwLock::new(
733 crate::approval::ApprovalConfig::default(),
734 )),
735 std::sync::Arc::new(crate::tools::PendingPathAccess::new()),
736 ),
737 None,
738 crate::kernel_handle::ExecApi::new(
739 Arc::new(parking_lot::RwLock::new(
740 crate::config::ExecConfig::default(),
741 )),
742 Arc::new(parking_lot::Mutex::new(
743 crate::access_manager::AccessManager::new(),
744 )),
745 ),
746 crate::kernel_handle::A2aApi::new(Arc::new(crate::a2a::A2AProtocol::new(
747 EventBus::new(64),
748 ))),
749 crate::kernel_handle::EngineApi::new(
750 Arc::new(parking_lot::RwLock::new(
751 crate::config::OxiosConfig::default(),
752 )),
753 tmp.join("config.toml"),
754 Arc::new(crate::kernel_handle::RoutingStats::new()),
755 Arc::new(crate::engine::EngineHandle::new(Arc::new(
756 crate::OxiosEngine::new("anthropic/claude-sonnet-4-20250514"),
757 ))),
758 ),
759 Arc::new(oxios_markdown::KnowledgeBase::new(tmp.join("knowledge")).unwrap()),
760 Arc::new(
761 crate::kernel_handle::KnowledgeLens::new(
762 Arc::new(oxios_markdown::KnowledgeBase::new(tmp.join("knowledge")).unwrap()),
763 memory_manager.clone(),
764 )
765 .unwrap(),
766 ),
767 crate::kernel_handle::MarketplaceApi::new(
768 Arc::new(crate::skill::clawhub::ClawHubInstaller::new(
769 tmp.join("skills"),
770 tmp.join("state"),
771 None,
772 )),
773 Arc::new(
774 crate::skill::clawhub::ClawHubClient::new(None).expect("valid ClawHub client"),
775 ),
776 Arc::new(crate::skill::skills_sh::SkillsShInstaller::new(
777 tmp.join("skills"),
778 None,
779 None,
780 )),
781 Arc::new(
782 crate::skill::skills_sh::SkillsShClient::new(None, None)
783 .expect("valid Skills.sh client"),
784 ),
785 ),
786 None, Arc::new(parking_lot::RwLock::new(None)), ));
789
790 let engine = crate::OxiosEngine::new("mock/model");
791 let engine_handle = Arc::new(crate::engine::EngineHandle::new(Arc::new(engine)));
792 let runtime = AgentRuntime::new(engine_handle, kernel_handle, None);
793 BasicSupervisor::new(event_bus, runtime)
794 }
795
796 fn make_directive(goal: &str) -> (Directive, ExecEnv) {
798 (Directive::from_message(goal), ExecEnv::default())
799 }
800
801 #[tokio::test]
802 async fn test_fork_creates_agent() {
803 let supervisor = make_supervisor().await;
804 let (directive, env) = make_directive("Test agent");
805
806 let id = supervisor.fork_directive(&directive, &env).await.unwrap();
807
808 let agents = supervisor.list().await.unwrap();
809 assert_eq!(agents.len(), 1);
810 assert_eq!(agents[0].id, id);
811 assert_eq!(agents[0].name, "Test agent");
812 assert_eq!(agents[0].status, AgentStatus::Starting);
813 }
814
815 #[tokio::test]
816 async fn test_exec_updates_status_to_running() {
817 let supervisor = make_supervisor().await;
818 let (directive, env) = make_directive("Running agent");
819
820 let id = supervisor.fork_directive(&directive, &env).await.unwrap();
821 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Starting);
822
823 supervisor.exec(id).await.unwrap();
824 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Running);
825 }
826
827 #[tokio::test]
828 async fn test_kill_sets_stopped() {
829 let supervisor = make_supervisor().await;
830 let (directive, env) = make_directive("Doomed agent");
831
832 let id = supervisor.fork_directive(&directive, &env).await.unwrap();
833 supervisor.exec(id).await.unwrap();
834 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Running);
835
836 supervisor.kill(id).await.unwrap();
837 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Stopped);
838 }
839
840 #[tokio::test]
841 async fn test_kill_unknown_agent_returns_error() {
842 let supervisor = make_supervisor().await;
843 let unknown_id = uuid::Uuid::new_v4();
844
845 let result = supervisor.kill(unknown_id).await;
846 assert!(result.is_err());
847 assert!(result.unwrap_err().to_string().contains("not found"));
848 }
849
850 #[tokio::test]
851 async fn test_list_returns_all_agents() {
852 let supervisor = make_supervisor().await;
853
854 let (d1, e1) = make_directive("Agent 1");
855 let id1 = supervisor.fork_directive(&d1, &e1).await.unwrap();
856 let (d2, e2) = make_directive("Agent 2");
857 let id2 = supervisor.fork_directive(&d2, &e2).await.unwrap();
858 let (d3, e3) = make_directive("Agent 3");
859 let id3 = supervisor.fork_directive(&d3, &e3).await.unwrap();
860
861 let agents = supervisor.list().await.unwrap();
862 assert_eq!(agents.len(), 3);
863
864 let ids: std::collections::HashSet<AgentId> = agents.iter().map(|a| a.id).collect();
865 assert!(ids.contains(&id1));
866 assert!(ids.contains(&id2));
867 assert!(ids.contains(&id3));
868 }
869
870 #[tokio::test]
871 async fn test_exec_unknown_agent_returns_error() {
872 let supervisor = make_supervisor().await;
873 let unknown_id = uuid::Uuid::new_v4();
874
875 let result = supervisor.exec(unknown_id).await;
876 assert!(result.is_err());
877 assert!(result.unwrap_err().to_string().contains("not found"));
878 }
879
880 #[tokio::test]
881 async fn test_wait_unknown_agent_returns_error() {
882 let supervisor = make_supervisor().await;
883 let unknown_id = uuid::Uuid::new_v4();
884
885 let result = supervisor.wait(unknown_id).await;
886 assert!(result.is_err());
887 assert!(result.unwrap_err().to_string().contains("not found"));
888 }
889}