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>;
131
132 async fn run_with_directive(
136 &self,
137 id: AgentId,
138 directive: &Directive,
139 env: &ExecEnv,
140 ) -> Result<ExecutionResult>;
141
142 async fn wait(&self, id: AgentId) -> Result<AgentStatus>;
144
145 async fn kill(&self, id: AgentId) -> Result<()>;
147
148 async fn list(&self) -> Result<Vec<AgentInfo>>;
150}
151
152pub struct BasicSupervisor {
154 agents: RwLock<HashMap<AgentId, AgentInfo>>,
155 handles: RwLock<HashMap<AgentId, AgentHandle>>,
157 agent_pool: AgentPool,
159 event_bus: EventBus,
160 runtime: Arc<AgentRuntime>,
161 resource_monitor: Option<Arc<ResourceMonitor>>,
162 session_context: Arc<tokio::sync::RwLock<SessionContext>>,
168 state_store: Option<Arc<StateStore>>,
170 #[cfg(feature = "sqlite-memory")]
172 agent_log_db: Option<Arc<AgentLogDb>>,
173 agent_log_config: AgentLogConfig,
175}
176
177impl BasicSupervisor {
178 pub fn new(event_bus: EventBus, runtime: AgentRuntime) -> Self {
180 Self {
181 agents: RwLock::new(HashMap::new()),
182 handles: RwLock::new(HashMap::new()),
183 agent_pool: AgentPool::new(),
184 event_bus,
185 runtime: Arc::new(runtime),
186 resource_monitor: None,
187 session_context: Arc::new(tokio::sync::RwLock::new(SessionContext::new())),
188 state_store: None,
189 #[cfg(feature = "sqlite-memory")]
190 agent_log_db: None,
191 agent_log_config: AgentLogConfig::default(),
192 }
193 }
194
195 pub fn set_state_store(&mut self, store: Arc<StateStore>) {
197 self.state_store = Some(store);
198 }
199
200 #[cfg(feature = "sqlite-memory")]
202 pub fn set_agent_log_db(&mut self, db: Arc<AgentLogDb>) {
203 self.agent_log_db = Some(db);
204 }
205
206 pub fn set_agent_log_config(&mut self, config: AgentLogConfig) {
208 self.agent_log_config = config;
209 }
210
211 pub fn set_resource_monitor(&mut self, rm: Arc<ResourceMonitor>) {
213 self.resource_monitor = Some(rm);
214 }
215
216 fn update_agent_count(&self) {
218 if let Some(ref rm) = self.resource_monitor {
219 let count = self.agents.read().len();
220 rm.set_active_agents(count);
221 }
222 }
223
224 pub fn pool(&self) -> &AgentPool {
226 &self.agent_pool
227 }
228}
229
230#[async_trait]
231impl Supervisor for BasicSupervisor {
232 async fn fork_directive(&self, directive: &Directive, env: &ExecEnv) -> Result<AgentId> {
233 let id = AgentId::new_v4();
234 let info = AgentInfo {
235 id,
236 name: directive.goal.clone(),
237 status: AgentStatus::Starting,
238 created_at: Utc::now(),
239 seed_id: None,
242 project_id: env.project_id,
243 started_at: None,
244 completed_at: None,
245 error: None,
246 steps_completed: 0,
247 steps_total: None,
248 tool_calls: vec![],
249 tokens_input: 0,
250 tokens_output: 0,
251 cost_usd: 0.0,
252 model_id: String::new(),
253 session_id: None,
254 };
255
256 {
257 let mut agents = self.agents.write();
258 agents.insert(id, info);
259 }
260
261 self.update_agent_count();
262
263 let _ = self
264 .event_bus
265 .publish(crate::event_bus::KernelEvent::AgentCreated {
266 id,
267 name: directive.goal.clone(),
268 });
269
270 tracing::info!(agent_id = %id, "Forked new agent from directive");
271 Ok(id)
272 }
273
274 async fn exec(&self, id: AgentId) -> Result<()> {
275 {
276 let mut agents = self.agents.write();
277 match agents.get_mut(&id) {
278 Some(agent) => {
279 agent.status = AgentStatus::Running;
280 }
281 None => anyhow::bail!("Agent {id} not found"),
282 }
283 }
284
285 self.update_agent_count();
286
287 let _ = self
288 .event_bus
289 .publish(crate::event_bus::KernelEvent::AgentStarted { id });
290 tracing::info!(agent_id = %id, "Agent execution started");
291
292 Ok(())
293 }
294
295 async fn run_with_directive(
296 &self,
297 id: AgentId,
298 directive: &Directive,
299 env: &ExecEnv,
300 ) -> Result<ExecutionResult> {
301 {
303 let mut agents = self.agents.write();
304 match agents.get_mut(&id) {
305 Some(agent) => {
306 agent.status = AgentStatus::Running;
307 agent.started_at = Some(Utc::now());
308 }
309 None => anyhow::bail!("Agent {id} not found"),
310 }
311 }
312
313 let _ = self
314 .event_bus
315 .publish(crate::event_bus::KernelEvent::AgentStarted { id });
316
317 tracing::info!(agent_id = %id, "Running agent task from directive");
318
319 let cancelled = Arc::new(AtomicBool::new(false));
321 let runtime = Arc::clone(&self.runtime);
322 let directive = directive.clone();
323 let env = env.clone();
324
325 let session_ctx = self.session_context.clone();
328
329 let (done_tx, done_rx) = tokio::sync::oneshot::channel::<Result<ExecutionResult>>();
330 let cancelled_done = cancelled.clone();
331 let handle: JoinHandle<()> = tokio::spawn(async move {
332 let result = if cancelled_done.load(Ordering::Relaxed) {
334 Ok(ExecutionResult {
335 output: "Agent cancelled before execution".into(),
336 steps_completed: 0,
337 success: false,
338 tool_calls: vec![],
339 tokens_input: 0,
340 tokens_output: 0,
341 model_id: String::new(),
342 failure_class: None, restore_state: None,
344 reasoning_text: String::new(),
345 })
346 } else {
347 let mut ctx = session_ctx.write().await;
348 runtime
349 .execute_directive(id, &directive, &env, &mut ctx)
350 .await
351 };
352 let _ = done_tx.send(result);
354 });
355
356 {
358 let mut handles = self.handles.write();
359 handles.insert(
360 id,
361 AgentHandle {
362 cancelled,
363 task: handle,
364 },
365 );
366 }
367
368 let result = match done_rx.await {
372 Ok(res) => res,
373 Err(_) => {
374 let mut handles = self.handles.write();
375 handles.remove(&id);
376 Ok(ExecutionResult {
377 output: "Agent task aborted".into(),
378 steps_completed: 0,
379 success: false,
380 tool_calls: vec![],
381 tokens_input: 0,
382 tokens_output: 0,
383 model_id: String::new(),
384 failure_class: None, restore_state: None,
386 reasoning_text: String::new(),
387 })
388 }
389 };
390
391 {
393 let mut handles = self.handles.write();
394 handles.remove(&id);
395 }
396
397 match result {
398 Ok(result) => {
399 tracing::info!(
400 agent_id = %id,
401 success = result.success,
402 steps = result.steps_completed,
403 "Agent task completed (directive)"
404 );
405
406 {
407 let mut agents = self.agents.write();
408 if let Some(agent) = agents.get_mut(&id) {
409 agent.status = if result.success {
410 AgentStatus::Idle
411 } else {
412 AgentStatus::Failed
413 };
414 agent.completed_at = Some(Utc::now());
415 agent.steps_completed = result.steps_completed;
416 agent.tool_calls = result
417 .tool_calls
418 .iter()
419 .map(|tc| crate::types::ToolCallRecord {
420 tool: tc.tool.clone(),
421 input: tc.input.clone(),
422 output: tc.output.clone(),
423 duration_ms: tc.duration_ms,
424 is_error: tc.is_error,
425 tool_call_id: tc.tool_call_id.clone(),
426 timestamp: tc.timestamp,
427 })
428 .collect();
429 agent.tokens_input = result.tokens_input;
430 agent.tokens_output = result.tokens_output;
431 agent.model_id = result.model_id.clone();
432 agent.cost_usd = if !result.model_id.is_empty() {
433 crate::kernel_handle::engine_api::estimate_cost(
434 &result.model_id,
435 result.tokens_input,
436 result.tokens_output,
437 )
438 } else {
439 0.0
440 };
441 if !result.success {
442 agent.error = Some(result.output.clone());
443 }
444 }
445 }
446
447 let _ = self
448 .event_bus
449 .publish(crate::event_bus::KernelEvent::AgentStopped {
450 id,
451 success: result.success,
452 });
453 self.update_agent_count();
454
455 self.persist_agent(id).await;
457
458 Ok(result)
459 }
460 Err(e) => {
461 tracing::error!(agent_id = %id, error = %e, "Agent task failed (directive)");
462
463 {
464 let mut agents = self.agents.write();
465 if let Some(agent) = agents.get_mut(&id) {
466 agent.status = AgentStatus::Failed;
467 agent.completed_at = Some(Utc::now());
468 agent.error = Some(e.to_string());
469 }
470 }
471
472 let _ = self
473 .event_bus
474 .publish(crate::event_bus::KernelEvent::AgentFailed {
475 id,
476 error: e.to_string(),
477 });
478 self.update_agent_count();
479
480 self.persist_agent(id).await;
482
483 Ok(ExecutionResult {
484 output: format!("Agent failed: {e}"),
485 steps_completed: 0,
486 success: false,
487 tool_calls: vec![],
488 tokens_input: 0,
489 tokens_output: 0,
490 model_id: String::new(),
491 reasoning_text: String::new(),
492 failure_class: Some(classify(&e)),
498 restore_state: e
499 .downcast_ref::<crate::resilience::AgentRunError>()
500 .and_then(|err| err.restore_state.clone()),
501 })
502 }
503 }
504 }
505
506 async fn wait(&self, id: AgentId) -> Result<AgentStatus> {
507 let agents = self.agents.read();
508 match agents.get(&id) {
509 Some(info) => Ok(info.status),
510 None => anyhow::bail!("Agent {id} not found"),
511 }
512 }
513
514 async fn kill(&self, id: AgentId) -> Result<()> {
515 {
517 let mut handles = self.handles.write();
518 if let Some(agent_handle) = handles.remove(&id) {
519 agent_handle.cancelled.store(true, Ordering::Relaxed);
520 agent_handle.task.abort();
521 tracing::info!(agent_id = %id, "Agent task aborted");
522 }
523 }
524
525 {
526 let mut agents = self.agents.write();
527 if let Some(agent) = agents.get_mut(&id) {
528 agent.status = AgentStatus::Stopped;
529 agent.completed_at = Some(Utc::now());
530 } else {
531 anyhow::bail!("Agent {id} not found");
532 }
533 }
534
535 let _ = self
536 .event_bus
537 .publish(crate::event_bus::KernelEvent::AgentStopped { id, success: false });
538 self.update_agent_count();
539
540 self.persist_agent(id).await;
542
543 tracing::info!(agent_id = %id, "Agent killed");
544 Ok(())
545 }
546
547 async fn list(&self) -> Result<Vec<AgentInfo>> {
548 let agents = self.agents.read();
549 Ok(agents.values().cloned().collect())
550 }
551}
552
553impl BasicSupervisor {
554 async fn persist_agent(&self, id: AgentId) {
557 let info = {
559 let agents = self.agents.read();
560 agents.get(&id).cloned()
561 };
562
563 let Some(info) = info else { return };
564
565 if let Some(ref store) = self.state_store {
567 let store = store.clone();
568 let info = info.clone();
569 let max_entries = self.agent_log_config.max_entries;
570 let ttl_hours = self.agent_log_config.ttl_hours;
571 let batch_size = self.agent_log_config.prune_batch_size;
572 tokio::spawn(async move {
573 let _ = store
574 .save_json("agents", &id.to_string(), &info)
575 .await
576 .inspect_err(|e| tracing::warn!(agent_id = %id, error = %e, "Failed to persist agent to filesystem"));
577
578 if max_entries > 0 || ttl_hours > 0 {
580 let _ = store
581 .prune_agents_by_config(max_entries, ttl_hours, batch_size)
582 .await
583 .inspect_err(|e| tracing::warn!(error = %e, "Failed to prune agent log"));
584 }
585 });
586 }
587
588 #[cfg(feature = "sqlite-memory")]
590 if let Some(ref db) = self.agent_log_db {
591 let db = db.clone();
592 let info = info.clone();
593 let config = self.agent_log_config.clone();
594 tokio::spawn(async move {
595 let _ = db
596 .upsert_agent(&info)
597 .inspect_err(|e| tracing::warn!(agent_id = %id, error = %e, "Failed to upsert agent to SQLite"));
598
599 let _ = db
601 .prune(&config)
602 .inspect_err(|e| tracing::warn!(error = %e, "Failed to prune agent SQLite"));
603 });
604 }
605 }
606}
607
608pub struct NoOpSupervisor;
614
615#[async_trait::async_trait]
616impl Supervisor for NoOpSupervisor {
617 async fn exec(&self, _id: AgentId) -> Result<()> {
618 Err(anyhow::anyhow!(
619 "NoOpSupervisor: exec not available during build"
620 ))
621 }
622 async fn fork_directive(&self, _directive: &Directive, _env: &ExecEnv) -> Result<AgentId> {
623 Err(anyhow::anyhow!(
624 "NoOpSupervisor: fork_directive not available during build"
625 ))
626 }
627 async fn run_with_directive(
628 &self,
629 _id: AgentId,
630 _directive: &Directive,
631 _env: &ExecEnv,
632 ) -> Result<ExecutionResult> {
633 Err(anyhow::anyhow!(
634 "NoOpSupervisor: run_with_directive not available during build"
635 ))
636 }
637 async fn wait(&self, _id: AgentId) -> Result<AgentStatus> {
638 Err(anyhow::anyhow!(
639 "NoOpSupervisor: wait not available during build"
640 ))
641 }
642 async fn kill(&self, _id: AgentId) -> Result<()> {
643 Err(anyhow::anyhow!(
644 "NoOpSupervisor: kill not available during build"
645 ))
646 }
647 async fn list(&self) -> Result<Vec<AgentInfo>> {
648 Ok(Vec::new())
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655 use crate::event_bus::EventBus;
656 use crate::types::AgentStatus;
657
658 async fn make_supervisor() -> BasicSupervisor {
663 let event_bus = EventBus::new(64);
664
665 let tmp = std::env::temp_dir().join(format!("oxios-test-{}", uuid::Uuid::new_v4()));
667 let _ = std::fs::create_dir_all(&tmp);
668
669 let state_store_2 =
670 Arc::new(crate::state_store::StateStore::new(tmp.join("state")).expect("state store"));
671 let state_store = state_store_2.clone();
672 let memory_manager = Arc::new({
673 let mut mm = crate::memory::MemoryManager::new(state_store.clone());
674 mm.set_git_layer(Arc::new(
675 crate::git_layer::GitLayer::new(tmp.join("git"), false).expect("git layer"),
676 ));
677 mm
678 });
679
680 let kernel_handle = Arc::new(crate::KernelHandle::new(
681 crate::kernel_handle::StateApi::new(state_store),
682 crate::kernel_handle::AgentApi::new(
683 Arc::new(crate::supervisor::NoOpSupervisor),
684 Arc::new(crate::budget::BudgetManager::new()),
685 memory_manager.clone(),
686 Some(event_bus.clone()),
687 ),
688 crate::kernel_handle::SecurityApi::new(
689 Arc::new(parking_lot::Mutex::new(crate::auth::AuthManager::new())),
690 Arc::new(oxi_sdk::observability::AuditTrail::new(100)),
691 Arc::new(parking_lot::Mutex::new(
692 crate::access_manager::AccessManager::new(),
693 )),
694 Arc::new(
695 crate::state_store::StateStore::new(tmp.join("state2")).expect("state store 2"),
696 ),
697 ),
698 crate::kernel_handle::PersonaApi::new(Arc::new(crate::persona::PersonaManager::new())),
699 crate::kernel_handle::ExtensionApi::new(Arc::new(crate::skill::SkillManager::new(
700 tmp.join("skills"),
701 tmp.join("share/skills"),
702 ))),
703 crate::kernel_handle::McpApi::new(Arc::new(crate::mcp::McpBridge::new())),
704 crate::kernel_handle::InfraApi::new(
705 Arc::new(
706 crate::git_layer::GitLayer::new(tmp.join("git"), false).expect("git layer"),
707 ),
708 Arc::new(crate::cron::CronScheduler::new(
709 Arc::new(
710 crate::state_store::StateStore::new(tmp.join("cron")).expect("cron state"),
711 ),
712 60,
713 )),
714 Arc::new(crate::resource_monitor::ResourceMonitor::new(60, 100)),
715 EventBus::new(64),
716 crate::config::OxiosConfig::default(),
717 std::time::Instant::now(),
718 ),
719 None,
720 crate::kernel_handle::ExecApi::new(
721 Arc::new(parking_lot::RwLock::new(
722 crate::config::ExecConfig::default(),
723 )),
724 Arc::new(parking_lot::Mutex::new(
725 crate::access_manager::AccessManager::new(),
726 )),
727 ),
728 crate::kernel_handle::A2aApi::new(Arc::new(crate::a2a::A2AProtocol::new(
729 EventBus::new(64),
730 ))),
731 crate::kernel_handle::EngineApi::new(
732 Arc::new(parking_lot::RwLock::new(
733 crate::config::OxiosConfig::default(),
734 )),
735 tmp.join("config.toml"),
736 Arc::new(crate::kernel_handle::RoutingStats::new()),
737 Arc::new(crate::engine::EngineHandle::new(Arc::new(
738 crate::OxiosEngine::new("anthropic/claude-sonnet-4-20250514"),
739 ))),
740 ),
741 Arc::new(oxios_markdown::KnowledgeBase::new(tmp.join("knowledge")).unwrap()),
742 Arc::new(
743 crate::kernel_handle::KnowledgeLens::new(
744 Arc::new(oxios_markdown::KnowledgeBase::new(tmp.join("knowledge")).unwrap()),
745 memory_manager.clone(),
746 )
747 .unwrap(),
748 ),
749 crate::kernel_handle::MarketplaceApi::new(
750 Arc::new(crate::skill::clawhub::ClawHubInstaller::new(
751 tmp.join("skills"),
752 tmp.join("state"),
753 None,
754 )),
755 Arc::new(
756 crate::skill::clawhub::ClawHubClient::new(None).expect("valid ClawHub client"),
757 ),
758 Arc::new(crate::skill::skills_sh::SkillsShInstaller::new(
759 tmp.join("skills"),
760 None,
761 None,
762 )),
763 Arc::new(
764 crate::skill::skills_sh::SkillsShClient::new(None, None)
765 .expect("valid Skills.sh client"),
766 ),
767 ),
768 None, None, ));
771
772 let engine = crate::OxiosEngine::new("mock/model");
773 let engine_handle = Arc::new(crate::engine::EngineHandle::new(Arc::new(engine)));
774 let runtime = AgentRuntime::new(engine_handle, kernel_handle, None);
775 BasicSupervisor::new(event_bus, runtime)
776 }
777
778 fn make_directive(goal: &str) -> (Directive, ExecEnv) {
780 (Directive::from_message(goal), ExecEnv::default())
781 }
782
783 #[tokio::test]
784 async fn test_fork_creates_agent() {
785 let supervisor = make_supervisor().await;
786 let (directive, env) = make_directive("Test agent");
787
788 let id = supervisor.fork_directive(&directive, &env).await.unwrap();
789
790 let agents = supervisor.list().await.unwrap();
791 assert_eq!(agents.len(), 1);
792 assert_eq!(agents[0].id, id);
793 assert_eq!(agents[0].name, "Test agent");
794 assert_eq!(agents[0].status, AgentStatus::Starting);
795 assert_eq!(agents[0].seed_id, None);
796 }
797
798 #[tokio::test]
799 async fn test_exec_updates_status_to_running() {
800 let supervisor = make_supervisor().await;
801 let (directive, env) = make_directive("Running agent");
802
803 let id = supervisor.fork_directive(&directive, &env).await.unwrap();
804 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Starting);
805
806 supervisor.exec(id).await.unwrap();
807 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Running);
808 }
809
810 #[tokio::test]
811 async fn test_kill_sets_stopped() {
812 let supervisor = make_supervisor().await;
813 let (directive, env) = make_directive("Doomed agent");
814
815 let id = supervisor.fork_directive(&directive, &env).await.unwrap();
816 supervisor.exec(id).await.unwrap();
817 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Running);
818
819 supervisor.kill(id).await.unwrap();
820 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Stopped);
821 }
822
823 #[tokio::test]
824 async fn test_kill_unknown_agent_returns_error() {
825 let supervisor = make_supervisor().await;
826 let unknown_id = uuid::Uuid::new_v4();
827
828 let result = supervisor.kill(unknown_id).await;
829 assert!(result.is_err());
830 assert!(result.unwrap_err().to_string().contains("not found"));
831 }
832
833 #[tokio::test]
834 async fn test_list_returns_all_agents() {
835 let supervisor = make_supervisor().await;
836
837 let (d1, e1) = make_directive("Agent 1");
838 let id1 = supervisor.fork_directive(&d1, &e1).await.unwrap();
839 let (d2, e2) = make_directive("Agent 2");
840 let id2 = supervisor.fork_directive(&d2, &e2).await.unwrap();
841 let (d3, e3) = make_directive("Agent 3");
842 let id3 = supervisor.fork_directive(&d3, &e3).await.unwrap();
843
844 let agents = supervisor.list().await.unwrap();
845 assert_eq!(agents.len(), 3);
846
847 let ids: std::collections::HashSet<AgentId> = agents.iter().map(|a| a.id).collect();
848 assert!(ids.contains(&id1));
849 assert!(ids.contains(&id2));
850 assert!(ids.contains(&id3));
851 }
852
853 #[tokio::test]
854 async fn test_exec_unknown_agent_returns_error() {
855 let supervisor = make_supervisor().await;
856 let unknown_id = uuid::Uuid::new_v4();
857
858 let result = supervisor.exec(unknown_id).await;
859 assert!(result.is_err());
860 assert!(result.unwrap_err().to_string().contains("not found"));
861 }
862
863 #[tokio::test]
864 async fn test_wait_unknown_agent_returns_error() {
865 let supervisor = make_supervisor().await;
866 let unknown_id = uuid::Uuid::new_v4();
867
868 let result = supervisor.wait(unknown_id).await;
869 assert!(result.is_err());
870 assert!(result.unwrap_err().to_string().contains("not found"));
871 }
872}