1use anyhow::Result;
20use async_trait::async_trait;
21use chrono::Utc;
22use oxi_sdk::Agent;
23use oxios_ouroboros::Seed;
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::resource_monitor::ResourceMonitor;
34use crate::session_context::SessionContext;
35use crate::state_store::StateStore;
36use crate::types::{AgentId, AgentInfo, AgentStatus};
37use oxios_ouroboros::ExecutionResult;
38
39#[cfg(feature = "sqlite-memory")]
40use crate::agent_log_db::AgentLogDb;
41
42struct AgentHandle {
44 cancelled: Arc<AtomicBool>,
46 task: JoinHandle<Result<ExecutionResult>>,
48}
49
50#[derive(Default)]
57pub struct AgentPool {
58 agents: RwLock<HashMap<AgentId, Arc<Agent>>>,
59}
60
61impl AgentPool {
62 pub fn new() -> Self {
64 Self {
65 agents: RwLock::new(HashMap::new()),
66 }
67 }
68
69 pub fn insert(&self, id: AgentId, agent: Arc<Agent>) {
71 self.agents.write().insert(id, agent);
72 }
73
74 pub fn get(&self, id: &AgentId) -> Option<Arc<Agent>> {
76 self.agents.read().get(id).cloned()
77 }
78
79 pub fn remove(&self, id: &AgentId) -> Option<Arc<Agent>> {
81 self.agents.write().remove(id)
82 }
83
84 pub fn export_state(&self, id: &AgentId) -> Option<serde_json::Value> {
88 self.agents
89 .read()
90 .get(id)
91 .and_then(|agent| agent.export_state().ok())
92 }
93
94 pub fn import_state(&self, id: &AgentId, state: serde_json::Value) -> bool {
98 if let Some(agent) = self.agents.read().get(id) {
99 agent.import_state(state).is_ok()
100 } else {
101 false
102 }
103 }
104
105 pub fn len(&self) -> usize {
107 self.agents.read().len()
108 }
109
110 pub fn is_empty(&self) -> bool {
112 self.agents.read().is_empty()
113 }
114}
115
116#[async_trait]
118pub trait Supervisor: Send + Sync {
119 async fn fork(&self, spec: &Seed) -> Result<AgentId>;
121
122 async fn exec(&self, id: AgentId) -> Result<()>;
124
125 async fn run_with_seed(&self, id: AgentId, seed: &Seed) -> Result<ExecutionResult>;
128
129 async fn wait(&self, id: AgentId) -> Result<AgentStatus>;
131
132 async fn kill(&self, id: AgentId) -> Result<()>;
134
135 async fn list(&self) -> Result<Vec<AgentInfo>>;
137}
138
139pub struct BasicSupervisor {
141 agents: RwLock<HashMap<AgentId, AgentInfo>>,
142 handles: RwLock<HashMap<AgentId, AgentHandle>>,
144 agent_pool: AgentPool,
146 event_bus: EventBus,
147 runtime: Arc<AgentRuntime>,
148 resource_monitor: Option<Arc<ResourceMonitor>>,
149 session_context: Arc<tokio::sync::RwLock<SessionContext>>,
155 state_store: Option<Arc<StateStore>>,
157 #[cfg(feature = "sqlite-memory")]
159 agent_log_db: Option<Arc<AgentLogDb>>,
160 agent_log_config: AgentLogConfig,
162}
163
164impl BasicSupervisor {
165 pub fn new(event_bus: EventBus, runtime: AgentRuntime) -> Self {
167 Self {
168 agents: RwLock::new(HashMap::new()),
169 handles: RwLock::new(HashMap::new()),
170 agent_pool: AgentPool::new(),
171 event_bus,
172 runtime: Arc::new(runtime),
173 resource_monitor: None,
174 session_context: Arc::new(tokio::sync::RwLock::new(SessionContext::new())),
175 state_store: None,
176 #[cfg(feature = "sqlite-memory")]
177 agent_log_db: None,
178 agent_log_config: AgentLogConfig::default(),
179 }
180 }
181
182 pub fn set_state_store(&mut self, store: Arc<StateStore>) {
184 self.state_store = Some(store);
185 }
186
187 #[cfg(feature = "sqlite-memory")]
189 pub fn set_agent_log_db(&mut self, db: Arc<AgentLogDb>) {
190 self.agent_log_db = Some(db);
191 }
192
193 pub fn set_agent_log_config(&mut self, config: AgentLogConfig) {
195 self.agent_log_config = config;
196 }
197
198 pub fn set_resource_monitor(&mut self, rm: Arc<ResourceMonitor>) {
200 self.resource_monitor = Some(rm);
201 }
202
203 fn update_agent_count(&self) {
205 if let Some(ref rm) = self.resource_monitor {
206 let count = self.agents.read().len();
207 rm.set_active_agents(count);
208 }
209 }
210
211 pub fn pool(&self) -> &AgentPool {
213 &self.agent_pool
214 }
215}
216
217#[async_trait]
218impl Supervisor for BasicSupervisor {
219 async fn fork(&self, spec: &Seed) -> Result<AgentId> {
220 let id = AgentId::new_v4();
221 let info = AgentInfo {
222 id,
223 name: spec.goal.clone(),
224 status: AgentStatus::Starting,
225 created_at: Utc::now(),
226 seed_id: Some(spec.id),
227 project_id: spec.project_id,
228 started_at: None,
229 completed_at: None,
230 error: None,
231 steps_completed: 0,
232 steps_total: None,
233 tool_calls: vec![],
234 tokens_input: 0,
235 tokens_output: 0,
236 cost_usd: 0.0,
237 model_id: String::new(),
238 session_id: None,
239 };
240
241 {
242 let mut agents = self.agents.write();
243 agents.insert(id, info);
244 }
245
246 self.update_agent_count();
247
248 let _ = self
249 .event_bus
250 .publish(crate::event_bus::KernelEvent::AgentCreated {
251 id,
252 name: spec.goal.clone(),
253 });
254
255 tracing::info!(agent_id = %id, "Forked new agent from seed");
256 Ok(id)
257 }
258
259 async fn exec(&self, id: AgentId) -> Result<()> {
260 {
261 let mut agents = self.agents.write();
262 match agents.get_mut(&id) {
263 Some(agent) => {
264 agent.status = AgentStatus::Running;
265 }
266 None => anyhow::bail!("Agent {id} not found"),
267 }
268 }
269
270 self.update_agent_count();
271
272 let _ = self
273 .event_bus
274 .publish(crate::event_bus::KernelEvent::AgentStarted { id });
275 tracing::info!(agent_id = %id, "Agent execution started");
276
277 Ok(())
278 }
279
280 async fn run_with_seed(&self, id: AgentId, seed: &Seed) -> Result<ExecutionResult> {
281 {
283 let mut agents = self.agents.write();
284 match agents.get_mut(&id) {
285 Some(agent) => {
286 agent.status = AgentStatus::Running;
287 agent.started_at = Some(Utc::now());
288 }
289 None => anyhow::bail!("Agent {id} not found"),
290 }
291 }
292
293 let _ = self
294 .event_bus
295 .publish(crate::event_bus::KernelEvent::AgentStarted { id });
296
297 tracing::info!(agent_id = %id, seed_id = %seed.id, "Running agent task");
298
299 let cancelled = Arc::new(AtomicBool::new(false));
301 let runtime = Arc::clone(&self.runtime);
302 let seed = seed.clone();
303 let cancelled_clone = cancelled.clone();
304
305 let session_ctx = self.session_context.clone();
308
309 let handle: JoinHandle<Result<ExecutionResult>> = tokio::spawn(async move {
310 if cancelled_clone.load(Ordering::Relaxed) {
312 return Ok(ExecutionResult {
313 output: "Agent cancelled before execution".into(),
314 steps_completed: 0,
315 success: false,
316 tool_calls: vec![],
317 tokens_input: 0,
318 tokens_output: 0,
319 model_id: String::new(),
320 });
321 }
322 let mut ctx = session_ctx.write().await;
323 runtime.execute(id, &seed, &mut ctx).await
324 });
325
326 {
328 let mut handles = self.handles.write();
329 handles.insert(
330 id,
331 AgentHandle {
332 cancelled,
333 task: handle,
334 },
335 );
336 }
337
338 let result = {
340 let agent_handle = {
341 let mut handles = self.handles.write();
342 handles.remove(&id)
343 };
344 match agent_handle {
347 Some(ah) => match ah.task.await {
348 Ok(res) => res,
349 Err(join_err) => {
350 tracing::warn!(agent_id = %id, error = %join_err, "Agent task join error");
352 Ok(ExecutionResult {
353 output: format!("Agent task aborted: {join_err}"),
354 steps_completed: 0,
355 success: false,
356 tool_calls: vec![],
357 tokens_input: 0,
358 tokens_output: 0,
359 model_id: String::new(),
360 })
361 }
362 },
363 None => anyhow::bail!("Agent {id} handle disappeared"),
364 }
365 };
366
367 match result {
368 Ok(result) => {
369 tracing::info!(
370 agent_id = %id,
371 success = result.success,
372 steps = result.steps_completed,
373 "Agent task completed"
374 );
375
376 {
377 let mut agents = self.agents.write();
378 if let Some(agent) = agents.get_mut(&id) {
379 agent.status = if result.success {
380 AgentStatus::Idle
381 } else {
382 AgentStatus::Failed
383 };
384 agent.completed_at = Some(Utc::now());
385 agent.steps_completed = result.steps_completed;
386 agent.tool_calls = result
387 .tool_calls
388 .iter()
389 .map(|tc| crate::types::ToolCallRecord {
390 tool: tc.tool.clone(),
391 input: tc.input.clone(),
392 output: tc.output.clone(),
393 duration_ms: tc.duration_ms,
394 is_error: tc.is_error,
395 tool_call_id: tc.tool_call_id.clone(),
396 timestamp: tc.timestamp,
397 })
398 .collect();
399 agent.tokens_input = result.tokens_input;
400 agent.tokens_output = result.tokens_output;
401 agent.model_id = result.model_id.clone();
402 agent.cost_usd = if !result.model_id.is_empty() {
403 crate::kernel_handle::engine_api::estimate_cost(
404 &result.model_id,
405 result.tokens_input,
406 result.tokens_output,
407 )
408 } else {
409 0.0
410 };
411 if !result.success {
412 agent.error = Some(result.output.clone());
413 }
414 }
415 }
416
417 let _ = self
418 .event_bus
419 .publish(crate::event_bus::KernelEvent::AgentStopped { id });
420 self.update_agent_count();
421
422 self.persist_agent(id).await;
424
425 Ok(result)
426 }
427 Err(e) => {
428 tracing::error!(agent_id = %id, error = %e, "Agent task failed");
429
430 {
431 let mut agents = self.agents.write();
432 if let Some(agent) = agents.get_mut(&id) {
433 agent.status = AgentStatus::Failed;
434 agent.completed_at = Some(Utc::now());
435 agent.error = Some(e.to_string());
436 }
437 }
438
439 let _ = self
440 .event_bus
441 .publish(crate::event_bus::KernelEvent::AgentFailed {
442 id,
443 error: e.to_string(),
444 });
445 self.update_agent_count();
446
447 self.persist_agent(id).await;
449
450 Ok(ExecutionResult {
451 output: format!("Agent failed: {e}"),
452 steps_completed: 0,
453 success: false,
454 tool_calls: vec![],
455 tokens_input: 0,
456 tokens_output: 0,
457 model_id: String::new(),
458 })
459 }
460 }
461 }
462
463 async fn wait(&self, id: AgentId) -> Result<AgentStatus> {
464 let agents = self.agents.read();
465 match agents.get(&id) {
466 Some(info) => Ok(info.status),
467 None => anyhow::bail!("Agent {id} not found"),
468 }
469 }
470
471 async fn kill(&self, id: AgentId) -> Result<()> {
472 {
474 let mut handles = self.handles.write();
475 if let Some(agent_handle) = handles.remove(&id) {
476 agent_handle.cancelled.store(true, Ordering::Relaxed);
477 agent_handle.task.abort();
478 tracing::info!(agent_id = %id, "Agent task aborted");
479 }
480 }
481
482 {
483 let mut agents = self.agents.write();
484 if let Some(agent) = agents.get_mut(&id) {
485 agent.status = AgentStatus::Stopped;
486 agent.completed_at = Some(Utc::now());
487 } else {
488 anyhow::bail!("Agent {id} not found");
489 }
490 }
491
492 let _ = self
493 .event_bus
494 .publish(crate::event_bus::KernelEvent::AgentStopped { id });
495 self.update_agent_count();
496
497 self.persist_agent(id).await;
499
500 tracing::info!(agent_id = %id, "Agent killed");
501 Ok(())
502 }
503
504 async fn list(&self) -> Result<Vec<AgentInfo>> {
505 let agents = self.agents.read();
506 Ok(agents.values().cloned().collect())
507 }
508}
509
510impl BasicSupervisor {
511 async fn persist_agent(&self, id: AgentId) {
514 let info = {
516 let agents = self.agents.read();
517 agents.get(&id).cloned()
518 };
519
520 let Some(info) = info else { return };
521
522 if let Some(ref store) = self.state_store {
524 let store = store.clone();
525 let info = info.clone();
526 let max_entries = self.agent_log_config.max_entries;
527 let ttl_hours = self.agent_log_config.ttl_hours;
528 let batch_size = self.agent_log_config.prune_batch_size;
529 tokio::spawn(async move {
530 let _ = store
531 .save_json("agents", &id.to_string(), &info)
532 .await
533 .inspect_err(|e| tracing::warn!(agent_id = %id, error = %e, "Failed to persist agent to filesystem"));
534
535 if max_entries > 0 || ttl_hours > 0 {
537 let _ = store
538 .prune_agents_by_config(max_entries, ttl_hours, batch_size)
539 .await
540 .inspect_err(|e| tracing::warn!(error = %e, "Failed to prune agent log"));
541 }
542 });
543 }
544
545 #[cfg(feature = "sqlite-memory")]
547 if let Some(ref db) = self.agent_log_db {
548 let db = db.clone();
549 let info = info.clone();
550 let config = self.agent_log_config.clone();
551 tokio::spawn(async move {
552 let _ = db
553 .upsert_agent(&info)
554 .inspect_err(|e| tracing::warn!(agent_id = %id, error = %e, "Failed to upsert agent to SQLite"));
555
556 let _ = db
558 .prune(&config)
559 .inspect_err(|e| tracing::warn!(error = %e, "Failed to prune agent SQLite"));
560 });
561 }
562 }
563}
564
565pub struct NoOpSupervisor;
571
572#[async_trait::async_trait]
573impl Supervisor for NoOpSupervisor {
574 async fn fork(&self, _spec: &Seed) -> Result<AgentId> {
575 Err(anyhow::anyhow!(
576 "NoOpSupervisor: fork not available during build"
577 ))
578 }
579 async fn exec(&self, _id: AgentId) -> Result<()> {
580 Err(anyhow::anyhow!(
581 "NoOpSupervisor: exec not available during build"
582 ))
583 }
584 async fn run_with_seed(&self, _id: AgentId, _seed: &Seed) -> Result<ExecutionResult> {
585 Err(anyhow::anyhow!(
586 "NoOpSupervisor: run_with_seed not available during build"
587 ))
588 }
589 async fn wait(&self, _id: AgentId) -> Result<AgentStatus> {
590 Err(anyhow::anyhow!(
591 "NoOpSupervisor: wait not available during build"
592 ))
593 }
594 async fn kill(&self, _id: AgentId) -> Result<()> {
595 Err(anyhow::anyhow!(
596 "NoOpSupervisor: kill not available during build"
597 ))
598 }
599 async fn list(&self) -> Result<Vec<AgentInfo>> {
600 Ok(Vec::new())
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use crate::event_bus::EventBus;
608 use crate::types::AgentStatus;
609 use oxios_ouroboros::Seed;
611
612 async fn make_supervisor() -> BasicSupervisor {
617 let event_bus = EventBus::new(64);
618
619 let tmp = std::env::temp_dir().join(format!("oxios-test-{}", uuid::Uuid::new_v4()));
621 let _ = std::fs::create_dir_all(&tmp);
622
623 let state_store_2 =
624 Arc::new(crate::state_store::StateStore::new(tmp.join("state")).expect("state store"));
625 let state_store = state_store_2.clone();
626 let memory_manager = Arc::new({
627 let mut mm = crate::memory::MemoryManager::new(state_store.clone());
628 mm.set_git_layer(Arc::new(
629 crate::git_layer::GitLayer::new(tmp.join("git"), false).expect("git layer"),
630 ));
631 mm
632 });
633
634 let kernel_handle = Arc::new(crate::KernelHandle::new(
635 crate::kernel_handle::StateApi::new(state_store),
636 crate::kernel_handle::AgentApi::new(
637 Arc::new(crate::supervisor::NoOpSupervisor),
638 Arc::new(crate::budget::BudgetManager::new()),
639 memory_manager.clone(),
640 Some(event_bus.clone()),
641 ),
642 crate::kernel_handle::SecurityApi::new(
643 Arc::new(parking_lot::Mutex::new(crate::auth::AuthManager::new())),
644 Arc::new(oxi_sdk::observability::AuditTrail::new(100)),
645 Arc::new(parking_lot::Mutex::new(
646 crate::access_manager::AccessManager::new(),
647 )),
648 Arc::new(
649 crate::state_store::StateStore::new(tmp.join("state2")).expect("state store 2"),
650 ),
651 ),
652 crate::kernel_handle::PersonaApi::new(Arc::new(crate::persona::PersonaManager::new())),
653 crate::kernel_handle::ExtensionApi::new(Arc::new(crate::skill::SkillManager::new(
654 tmp.join("skills"),
655 tmp.join("share/skills"),
656 ))),
657 crate::kernel_handle::McpApi::new(Arc::new(crate::mcp::McpBridge::new())),
658 crate::kernel_handle::InfraApi::new(
659 Arc::new(
660 crate::git_layer::GitLayer::new(tmp.join("git"), false).expect("git layer"),
661 ),
662 Arc::new(crate::scheduler::AgentScheduler::new(4, 60, 300)),
663 Arc::new(crate::cron::CronScheduler::new(
664 Arc::new(
665 crate::state_store::StateStore::new(tmp.join("cron")).expect("cron state"),
666 ),
667 60,
668 )),
669 Arc::new(crate::resource_monitor::ResourceMonitor::new(60, 100)),
670 EventBus::new(64),
671 crate::config::OxiosConfig::default(),
672 std::time::Instant::now(),
673 ),
674 None,
675 crate::kernel_handle::ExecApi::new(
676 Arc::new(parking_lot::RwLock::new(
677 crate::config::ExecConfig::default(),
678 )),
679 Arc::new(parking_lot::Mutex::new(
680 crate::access_manager::AccessManager::new(),
681 )),
682 ),
683 crate::kernel_handle::A2aApi::new(Arc::new(crate::a2a::A2AProtocol::new(
684 EventBus::new(64),
685 ))),
686 crate::kernel_handle::EngineApi::new(
687 Arc::new(parking_lot::RwLock::new(
688 crate::config::OxiosConfig::default(),
689 )),
690 tmp.join("config.toml"),
691 Arc::new(crate::kernel_handle::RoutingStats::new()),
692 Arc::new(crate::engine::EngineHandle::new(Arc::new(
693 crate::OxiosEngine::new("anthropic/claude-sonnet-4-20250514"),
694 ))),
695 ),
696 Arc::new(oxios_markdown::KnowledgeBase::new(tmp.join("knowledge")).unwrap()),
697 Arc::new(
698 crate::kernel_handle::KnowledgeLens::new(
699 Arc::new(oxios_markdown::KnowledgeBase::new(tmp.join("knowledge")).unwrap()),
700 memory_manager.clone(),
701 )
702 .unwrap(),
703 ),
704 crate::kernel_handle::MarketplaceApi::new(
705 Arc::new(crate::skill::clawhub::ClawHubInstaller::new(
706 tmp.join("skills"),
707 tmp.join("state"),
708 None,
709 )),
710 Arc::new(
711 crate::skill::clawhub::ClawHubClient::new(None).expect("valid ClawHub client"),
712 ),
713 Arc::new(crate::skill::skills_sh::SkillsShInstaller::new(
714 tmp.join("skills"),
715 None,
716 None,
717 )),
718 Arc::new(
719 crate::skill::skills_sh::SkillsShClient::new(None, None)
720 .expect("valid Skills.sh client"),
721 ),
722 ),
723 None, None, ));
726
727 let engine = crate::OxiosEngine::new("mock/model");
728 let engine_handle = Arc::new(crate::engine::EngineHandle::new(Arc::new(engine)));
729 let runtime = AgentRuntime::new(engine_handle, kernel_handle, None);
730 BasicSupervisor::new(event_bus, runtime)
731 }
732
733 fn make_seed(goal: &str) -> Seed {
735 Seed {
736 id: uuid::Uuid::new_v4(),
737 goal: goal.to_string(),
738 constraints: vec![],
739 acceptance_criteria: vec![],
740 ontology: vec![],
741 created_at: chrono::Utc::now(),
742 generation: 0,
743 parent_seed_id: None,
744 cspace_hint: None,
745 original_request: String::new(),
746 output_schema: None,
747 project_id: None,
748 workspace_context: None,
749 mount_paths: Vec::new(),
750 }
751 }
752
753 #[tokio::test]
754 async fn test_fork_creates_agent() {
755 let supervisor = make_supervisor().await;
756 let seed = make_seed("Test agent");
757
758 let id = supervisor.fork(&seed).await.unwrap();
759
760 let agents = supervisor.list().await.unwrap();
761 assert_eq!(agents.len(), 1);
762 assert_eq!(agents[0].id, id);
763 assert_eq!(agents[0].name, "Test agent");
764 assert_eq!(agents[0].status, AgentStatus::Starting);
765 assert_eq!(agents[0].seed_id, Some(seed.id));
766 }
767
768 #[tokio::test]
769 async fn test_exec_updates_status_to_running() {
770 let supervisor = make_supervisor().await;
771 let seed = make_seed("Running agent");
772
773 let id = supervisor.fork(&seed).await.unwrap();
774 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Starting);
775
776 supervisor.exec(id).await.unwrap();
777 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Running);
778 }
779
780 #[tokio::test]
781 async fn test_kill_sets_stopped() {
782 let supervisor = make_supervisor().await;
783 let seed = make_seed("Doomed agent");
784
785 let id = supervisor.fork(&seed).await.unwrap();
786 supervisor.exec(id).await.unwrap();
787 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Running);
788
789 supervisor.kill(id).await.unwrap();
790 assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Stopped);
791 }
792
793 #[tokio::test]
794 async fn test_kill_unknown_agent_returns_error() {
795 let supervisor = make_supervisor().await;
796 let unknown_id = uuid::Uuid::new_v4();
797
798 let result = supervisor.kill(unknown_id).await;
799 assert!(result.is_err());
800 assert!(result.unwrap_err().to_string().contains("not found"));
801 }
802
803 #[tokio::test]
804 async fn test_list_returns_all_agents() {
805 let supervisor = make_supervisor().await;
806
807 let id1 = supervisor.fork(&make_seed("Agent 1")).await.unwrap();
808 let id2 = supervisor.fork(&make_seed("Agent 2")).await.unwrap();
809 let id3 = supervisor.fork(&make_seed("Agent 3")).await.unwrap();
810
811 let agents = supervisor.list().await.unwrap();
812 assert_eq!(agents.len(), 3);
813
814 let ids: std::collections::HashSet<AgentId> = agents.iter().map(|a| a.id).collect();
815 assert!(ids.contains(&id1));
816 assert!(ids.contains(&id2));
817 assert!(ids.contains(&id3));
818 }
819
820 #[tokio::test]
821 async fn test_exec_unknown_agent_returns_error() {
822 let supervisor = make_supervisor().await;
823 let unknown_id = uuid::Uuid::new_v4();
824
825 let result = supervisor.exec(unknown_id).await;
826 assert!(result.is_err());
827 assert!(result.unwrap_err().to_string().contains("not found"));
828 }
829
830 #[tokio::test]
831 async fn test_wait_unknown_agent_returns_error() {
832 let supervisor = make_supervisor().await;
833 let unknown_id = uuid::Uuid::new_v4();
834
835 let result = supervisor.wait(unknown_id).await;
836 assert!(result.is_err());
837 assert!(result.unwrap_err().to_string().contains("not found"));
838 }
839}