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