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 })
340 } else {
341 let mut temp_ctx = {
348 let ctx = session_ctx.read().await;
349 let mut c = crate::session_context::SessionContext::new();
350 c.recall_timing = ctx.recall_timing.clone();
351 c
352 };
353 let exec_result = runtime
354 .execute_directive(id, &directive, &env, &mut temp_ctx)
355 .await;
356 session_ctx.write().await.recall_timing = temp_ctx.recall_timing;
359 exec_result
360 };
361 let _ = done_tx.send(result);
363 });
364
365 {
367 let mut handles = self.handles.write();
368 handles.insert(
369 id,
370 AgentHandle {
371 cancelled,
372 task: handle,
373 },
374 );
375 }
376
377 let result = match done_rx.await {
381 Ok(res) => res,
382 Err(_) => {
383 let mut handles = self.handles.write();
384 handles.remove(&id);
385 Ok(ExecutionResult {
386 output: "Agent task aborted".into(),
387 steps_completed: 0,
388 success: false,
389 tool_calls: vec![],
390 tokens_input: 0,
391 tokens_output: 0,
392 model_id: String::new(),
393 failure_class: None, restore_state: None,
395 reasoning_text: String::new(),
396 })
397 }
398 };
399
400 {
402 let mut handles = self.handles.write();
403 handles.remove(&id);
404 }
405
406 match result {
407 Ok(result) => {
408 tracing::info!(
409 agent_id = %id,
410 success = result.success,
411 steps = result.steps_completed,
412 "Agent task completed (directive)"
413 );
414
415 {
416 let mut agents = self.agents.write();
417 if let Some(agent) = agents.get_mut(&id) {
418 agent.status = if result.success {
419 AgentStatus::Completed
420 } else {
421 AgentStatus::Failed
422 };
423 agent.completed_at = Some(Utc::now());
424 agent.steps_completed = result.steps_completed;
425 agent.tool_calls = result
426 .tool_calls
427 .iter()
428 .map(|tc| crate::types::ToolCallRecord {
429 tool: tc.tool.clone(),
430 input: tc.input.clone(),
431 output: tc.output.clone(),
432 duration_ms: tc.duration_ms,
433 is_error: tc.is_error,
434 tool_call_id: tc.tool_call_id.clone(),
435 timestamp: tc.timestamp,
436 })
437 .collect();
438 agent.tokens_input = result.tokens_input;
439 agent.tokens_output = result.tokens_output;
440 agent.model_id = result.model_id.clone();
441 agent.cost_usd = if !result.model_id.is_empty() {
442 crate::kernel_handle::engine_api::estimate_cost(
443 &result.model_id,
444 result.tokens_input,
445 result.tokens_output,
446 )
447 } else {
448 0.0
449 };
450 if !result.success {
451 agent.error = Some(result.output.clone());
452 }
453 }
454 }
455
456 let _ = self
457 .event_bus
458 .publish(crate::event_bus::KernelEvent::AgentStopped {
459 id,
460 success: result.success,
461 });
462 self.update_agent_count();
463
464 self.persist_agent(id).await;
466
467 Ok(result)
468 }
469 Err(e) => {
470 tracing::error!(agent_id = %id, error = %e, "Agent task failed (directive)");
471
472 {
473 let mut agents = self.agents.write();
474 if let Some(agent) = agents.get_mut(&id) {
475 agent.status = AgentStatus::Failed;
476 agent.completed_at = Some(Utc::now());
477 agent.error = Some(e.to_string());
478 }
479 }
480
481 let _ = self
482 .event_bus
483 .publish(crate::event_bus::KernelEvent::AgentFailed {
484 id,
485 error: e.to_string(),
486 });
487 self.update_agent_count();
488
489 self.persist_agent(id).await;
491
492 Ok(ExecutionResult {
493 output: format!("Agent failed: {e}"),
494 steps_completed: 0,
495 success: false,
496 tool_calls: vec![],
497 tokens_input: 0,
498 tokens_output: 0,
499 model_id: String::new(),
500 reasoning_text: String::new(),
501 failure_class: Some(classify(&e)),
507 restore_state: e
508 .downcast_ref::<crate::resilience::AgentRunError>()
509 .and_then(|err| err.restore_state.clone()),
510 })
511 }
512 }
513 }
514
515 async fn wait(&self, id: AgentId) -> Result<AgentStatus> {
516 let agents = self.agents.read();
517 match agents.get(&id) {
518 Some(info) => Ok(info.status),
519 None => anyhow::bail!("Agent {id} not found"),
520 }
521 }
522
523 async fn kill(&self, id: AgentId) -> Result<()> {
524 {
526 let mut handles = self.handles.write();
527 if let Some(agent_handle) = handles.remove(&id) {
528 agent_handle.cancelled.store(true, Ordering::Relaxed);
529 agent_handle.task.abort();
530 tracing::info!(agent_id = %id, "Agent task aborted");
531 }
532 }
533
534 {
535 let mut agents = self.agents.write();
536 if let Some(agent) = agents.get_mut(&id) {
537 agent.status = AgentStatus::Stopped;
538 agent.completed_at = Some(Utc::now());
539 } else {
540 anyhow::bail!("Agent {id} not found");
541 }
542 }
543
544 let _ = self
545 .event_bus
546 .publish(crate::event_bus::KernelEvent::AgentStopped { id, success: false });
547 self.update_agent_count();
548
549 self.persist_agent(id).await;
551
552 tracing::info!(agent_id = %id, "Agent killed");
553 Ok(())
554 }
555
556 async fn list(&self) -> Result<Vec<AgentInfo>> {
557 let agents = self.agents.read();
558 Ok(agents.values().cloned().collect())
559 }
560}
561
562impl BasicSupervisor {
563 async fn persist_agent(&self, id: AgentId) {
566 let info = {
568 let agents = self.agents.read();
569 agents.get(&id).cloned()
570 };
571
572 let Some(info) = info else { return };
573
574 if let Some(ref store) = self.state_store {
576 let store = store.clone();
577 let info = info.clone();
578 let max_entries = self.agent_log_config.max_entries;
579 let ttl_hours = self.agent_log_config.ttl_hours;
580 let batch_size = self.agent_log_config.prune_batch_size;
581 tokio::spawn(async move {
582 let _ = store
583 .save_json("agents", &id.to_string(), &info)
584 .await
585 .inspect_err(|e| tracing::warn!(agent_id = %id, error = %e, "Failed to persist agent to filesystem"));
586
587 if max_entries > 0 || ttl_hours > 0 {
589 let _ = store
590 .prune_agents_by_config(max_entries, ttl_hours, batch_size)
591 .await
592 .inspect_err(|e| tracing::warn!(error = %e, "Failed to prune agent log"));
593 }
594 });
595 }
596
597 #[cfg(feature = "sqlite-memory")]
599 if let Some(ref db) = self.agent_log_db {
600 let db = db.clone();
601 let info = info.clone();
602 let config = self.agent_log_config.clone();
603 tokio::spawn(async move {
604 let _ = db
605 .upsert_agent(&info)
606 .inspect_err(|e| tracing::warn!(agent_id = %id, error = %e, "Failed to upsert agent to SQLite"));
607
608 let _ = db
610 .prune(&config)
611 .inspect_err(|e| tracing::warn!(error = %e, "Failed to prune agent SQLite"));
612 });
613 }
614 }
615}
616
617pub struct NoOpSupervisor;
623
624#[async_trait::async_trait]
625impl Supervisor for NoOpSupervisor {
626 async fn exec(&self, _id: AgentId) -> Result<()> {
627 Err(anyhow::anyhow!(
628 "NoOpSupervisor: exec not available during build"
629 ))
630 }
631 async fn fork_directive(&self, _directive: &Directive, _env: &ExecEnv) -> Result<AgentId> {
632 Err(anyhow::anyhow!(
633 "NoOpSupervisor: fork_directive not available during build"
634 ))
635 }
636 async fn run_with_directive(
637 &self,
638 _id: AgentId,
639 _directive: &Directive,
640 _env: &ExecEnv,
641 ) -> Result<ExecutionResult> {
642 Err(anyhow::anyhow!(
643 "NoOpSupervisor: run_with_directive not available during build"
644 ))
645 }
646 async fn wait(&self, _id: AgentId) -> Result<AgentStatus> {
647 Err(anyhow::anyhow!(
648 "NoOpSupervisor: wait not available during build"
649 ))
650 }
651 async fn kill(&self, _id: AgentId) -> Result<()> {
652 Err(anyhow::anyhow!(
653 "NoOpSupervisor: kill not available during build"
654 ))
655 }
656 async fn list(&self) -> Result<Vec<AgentInfo>> {
657 Ok(Vec::new())
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664 use crate::event_bus::EventBus;
665 use crate::types::AgentStatus;
666
667 async fn make_supervisor() -> BasicSupervisor {
672 let event_bus = EventBus::new(64);
673
674 let tmp = std::env::temp_dir().join(format!("oxios-test-{}", uuid::Uuid::new_v4()));
676 let _ = std::fs::create_dir_all(&tmp);
677
678 let state_store_2 =
679 Arc::new(crate::state_store::StateStore::new(tmp.join("state")).expect("state store"));
680 let state_store = state_store_2.clone();
681 let memory_manager = Arc::new({
682 let mut mm = crate::memory::MemoryManager::new(state_store.clone());
683 mm.set_git_layer(Arc::new(
684 crate::git_layer::GitLayer::new(tmp.join("git"), false).expect("git layer"),
685 ));
686 mm
687 });
688
689 let kernel_handle = Arc::new(crate::KernelHandle::new(
690 crate::kernel_handle::StateApi::new(state_store),
691 crate::kernel_handle::AgentApi::new(
692 Arc::new(crate::supervisor::NoOpSupervisor),
693 Arc::new(crate::budget::BudgetManager::new()),
694 memory_manager.clone(),
695 Some(event_bus.clone()),
696 ),
697 crate::kernel_handle::SecurityApi::new(
698 Arc::new(parking_lot::Mutex::new(crate::auth::AuthManager::new())),
699 Arc::new(oxi_sdk::observability::AuditTrail::new(100)),
700 Arc::new(parking_lot::Mutex::new(
701 crate::access_manager::AccessManager::new(),
702 )),
703 Arc::new(
704 crate::state_store::StateStore::new(tmp.join("state2")).expect("state store 2"),
705 ),
706 ),
707 crate::kernel_handle::PersonaApi::new(Arc::new(crate::persona::PersonaManager::new())),
708 crate::kernel_handle::ExtensionApi::new(Arc::new(crate::skill::SkillManager::new(
709 tmp.join("skills"),
710 tmp.join("share/skills"),
711 ))),
712 crate::kernel_handle::McpApi::new(Arc::new(crate::mcp::McpBridge::new())),
713 crate::kernel_handle::InfraApi::new(
714 Arc::new(
715 crate::git_layer::GitLayer::new(tmp.join("git"), false).expect("git layer"),
716 ),
717 Arc::new(crate::cron::CronScheduler::new(
718 Arc::new(
719 crate::state_store::StateStore::new(tmp.join("cron")).expect("cron state"),
720 ),
721 60,
722 )),
723 Arc::new(crate::resource_monitor::ResourceMonitor::new(60, 100)),
724 EventBus::new(64),
725 crate::config::OxiosConfig::default(),
726 std::time::Instant::now(),
727 ),
728 None,
729 crate::kernel_handle::ExecApi::new(
730 Arc::new(parking_lot::RwLock::new(
731 crate::config::ExecConfig::default(),
732 )),
733 Arc::new(parking_lot::Mutex::new(
734 crate::access_manager::AccessManager::new(),
735 )),
736 ),
737 crate::kernel_handle::A2aApi::new(Arc::new(crate::a2a::A2AProtocol::new(
738 EventBus::new(64),
739 ))),
740 crate::kernel_handle::EngineApi::new(
741 Arc::new(parking_lot::RwLock::new(
742 crate::config::OxiosConfig::default(),
743 )),
744 tmp.join("config.toml"),
745 Arc::new(crate::kernel_handle::RoutingStats::new()),
746 Arc::new(crate::engine::EngineHandle::new(Arc::new(
747 crate::OxiosEngine::new("anthropic/claude-sonnet-4-20250514"),
748 ))),
749 ),
750 Arc::new(oxios_markdown::KnowledgeBase::new(tmp.join("knowledge")).unwrap()),
751 Arc::new(
752 crate::kernel_handle::KnowledgeLens::new(
753 Arc::new(oxios_markdown::KnowledgeBase::new(tmp.join("knowledge")).unwrap()),
754 memory_manager.clone(),
755 )
756 .unwrap(),
757 ),
758 crate::kernel_handle::MarketplaceApi::new(
759 Arc::new(crate::skill::clawhub::ClawHubInstaller::new(
760 tmp.join("skills"),
761 tmp.join("state"),
762 None,
763 )),
764 Arc::new(
765 crate::skill::clawhub::ClawHubClient::new(None).expect("valid ClawHub client"),
766 ),
767 Arc::new(crate::skill::skills_sh::SkillsShInstaller::new(
768 tmp.join("skills"),
769 None,
770 None,
771 )),
772 Arc::new(
773 crate::skill::skills_sh::SkillsShClient::new(None, None)
774 .expect("valid Skills.sh client"),
775 ),
776 ),
777 None, None, ));
780
781 let engine = crate::OxiosEngine::new("mock/model");
782 let engine_handle = Arc::new(crate::engine::EngineHandle::new(Arc::new(engine)));
783 let runtime = AgentRuntime::new(engine_handle, kernel_handle, None);
784 BasicSupervisor::new(event_bus, runtime)
785 }
786
787 fn make_directive(goal: &str) -> (Directive, ExecEnv) {
789 (Directive::from_message(goal), ExecEnv::default())
790 }
791
792 #[tokio::test]
793 async fn test_fork_creates_agent() {
794 let supervisor = make_supervisor().await;
795 let (directive, env) = make_directive("Test agent");
796
797 let id = supervisor.fork_directive(&directive, &env).await.unwrap();
798
799 let agents = supervisor.list().await.unwrap();
800 assert_eq!(agents.len(), 1);
801 assert_eq!(agents[0].id, id);
802 assert_eq!(agents[0].name, "Test agent");
803 assert_eq!(agents[0].status, AgentStatus::Starting);
804 }
805
806 #[tokio::test]
807 async fn test_exec_updates_status_to_running() {
808 let supervisor = make_supervisor().await;
809 let (directive, env) = make_directive("Running agent");
810
811 let id = supervisor.fork_directive(&directive, &env).await.unwrap();
812 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Starting);
813
814 supervisor.exec(id).await.unwrap();
815 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Running);
816 }
817
818 #[tokio::test]
819 async fn test_kill_sets_stopped() {
820 let supervisor = make_supervisor().await;
821 let (directive, env) = make_directive("Doomed agent");
822
823 let id = supervisor.fork_directive(&directive, &env).await.unwrap();
824 supervisor.exec(id).await.unwrap();
825 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Running);
826
827 supervisor.kill(id).await.unwrap();
828 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Stopped);
829 }
830
831 #[tokio::test]
832 async fn test_kill_unknown_agent_returns_error() {
833 let supervisor = make_supervisor().await;
834 let unknown_id = uuid::Uuid::new_v4();
835
836 let result = supervisor.kill(unknown_id).await;
837 assert!(result.is_err());
838 assert!(result.unwrap_err().to_string().contains("not found"));
839 }
840
841 #[tokio::test]
842 async fn test_list_returns_all_agents() {
843 let supervisor = make_supervisor().await;
844
845 let (d1, e1) = make_directive("Agent 1");
846 let id1 = supervisor.fork_directive(&d1, &e1).await.unwrap();
847 let (d2, e2) = make_directive("Agent 2");
848 let id2 = supervisor.fork_directive(&d2, &e2).await.unwrap();
849 let (d3, e3) = make_directive("Agent 3");
850 let id3 = supervisor.fork_directive(&d3, &e3).await.unwrap();
851
852 let agents = supervisor.list().await.unwrap();
853 assert_eq!(agents.len(), 3);
854
855 let ids: std::collections::HashSet<AgentId> = agents.iter().map(|a| a.id).collect();
856 assert!(ids.contains(&id1));
857 assert!(ids.contains(&id2));
858 assert!(ids.contains(&id3));
859 }
860
861 #[tokio::test]
862 async fn test_exec_unknown_agent_returns_error() {
863 let supervisor = make_supervisor().await;
864 let unknown_id = uuid::Uuid::new_v4();
865
866 let result = supervisor.exec(unknown_id).await;
867 assert!(result.is_err());
868 assert!(result.unwrap_err().to_string().contains("not found"));
869 }
870
871 #[tokio::test]
872 async fn test_wait_unknown_agent_returns_error() {
873 let supervisor = make_supervisor().await;
874 let unknown_id = uuid::Uuid::new_v4();
875
876 let result = supervisor.wait(unknown_id).await;
877 assert!(result.is_err());
878 assert!(result.unwrap_err().to_string().contains("not found"));
879 }
880}