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