Skip to main content

oxios_kernel/
supervisor.rs

1//! Supervisor: agent lifecycle management.
2//!
3//! The supervisor handles forking, executing, monitoring, and
4//! terminating agent instances. It is the "init" of Oxios.
5//!
6//! When an agent is forked and executed, the supervisor delegates
7//! the actual tool-calling loop to the [`AgentRuntime`].
8//!
9//! # Agent Pool & Session Persistence
10//!
11//! Agents are retained in an [`AgentPool`] after execution for:
12//! - **Session continuation** via `Agent::continue_with()` — multi-turn
13//!   conversations without re-creating the agent.
14//! - **State export/import** — serialize agent conversation history to
15//!   JSON for crash recovery, migration, or debugging.
16//! - **Provider rate limiting** — all agents share a [`ProviderPool`] to
17//!   respect per-provider RPM/concurrency limits.
18
19use 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
44/// Tracks the runtime handles needed to cancel a running agent.
45struct AgentHandle {
46    /// Flag set on `kill()` to cooperatively signal cancellation.
47    cancelled: Arc<AtomicBool>,
48    /// The tokio task running the agent execution. Aborted on `kill()`.
49    task: JoinHandle<()>,
50}
51
52/// Pool of live `Agent` instances, keyed by AgentId.
53///
54/// Retains agents after execution for:
55/// - **State persistence** — `export_state()` serializes conversation history
56///   to JSON for crash recovery, migration, or debugging.
57/// - **State restoration** — `import_state()` restores a previous session.
58#[derive(Default)]
59pub struct AgentPool {
60    agents: RwLock<HashMap<AgentId, Arc<Agent>>>,
61}
62
63impl AgentPool {
64    /// Create an empty agent pool.
65    pub fn new() -> Self {
66        Self {
67            agents: RwLock::new(HashMap::new()),
68        }
69    }
70
71    /// Insert an agent into the pool.
72    pub fn insert(&self, id: AgentId, agent: Arc<Agent>) {
73        self.agents.write().insert(id, agent);
74    }
75
76    /// Get a pooled agent by ID.
77    pub fn get(&self, id: &AgentId) -> Option<Arc<Agent>> {
78        self.agents.read().get(id).cloned()
79    }
80
81    /// Remove an agent from the pool.
82    pub fn remove(&self, id: &AgentId) -> Option<Arc<Agent>> {
83        self.agents.write().remove(id)
84    }
85
86    /// Export an agent's state as JSON.
87    ///
88    /// Returns `None` if the agent is not in the pool or export fails.
89    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    /// Import agent state from JSON.
97    ///
98    /// Returns `false` if the agent is not in the pool or import fails.
99    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    /// Number of agents currently in the pool.
108    pub fn len(&self) -> usize {
109        self.agents.read().len()
110    }
111
112    /// Whether the pool is empty.
113    pub fn is_empty(&self) -> bool {
114        self.agents.read().is_empty()
115    }
116}
117
118/// Supervisor trait for managing agent lifecycles.
119#[async_trait]
120pub trait Supervisor: Send + Sync {
121    /// Start executing an agent.
122    async fn exec(&self, id: AgentId) -> Result<()>;
123
124    /// Fork a new agent from a Directive and ExecEnv (RFC-027).
125    ///
126    /// Reads the goal / project_id from the unified-intent Directive.
127    async fn fork_directive(&self, directive: &Directive, env: &ExecEnv) -> Result<AgentId>;
128
129    /// Fork and execute an agent with a Directive + ExecEnv, running to completion.
130    ///
131    /// Dispatches to `AgentRuntime::execute_directive_with_session`.
132    async fn run_with_directive(
133        &self,
134        id: AgentId,
135        directive: &Directive,
136        env: &ExecEnv,
137    ) -> Result<ExecutionResult>;
138
139    /// Wait for an agent to complete and return its final status.
140    async fn wait(&self, id: AgentId) -> Result<AgentStatus>;
141
142    /// Terminate an agent.
143    async fn kill(&self, id: AgentId) -> Result<()>;
144
145    /// List all known agents.
146    async fn list(&self) -> Result<Vec<AgentInfo>>;
147}
148
149/// Basic in-memory supervisor implementation with AgentRuntime integration.
150pub struct BasicSupervisor {
151    agents: RwLock<HashMap<AgentId, AgentInfo>>,
152    /// Per-agent cancellation tokens and join handles for task abortion.
153    handles: RwLock<HashMap<AgentId, AgentHandle>>,
154    /// Pool of live Agent instances for session continuation.
155    agent_pool: AgentPool,
156    event_bus: EventBus,
157    runtime: Arc<AgentRuntime>,
158    resource_monitor: Option<Arc<ResourceMonitor>>,
159    /// Session context for proactive recall timing (RFC-020).
160    /// Shared across all agent executions within this supervisor's lifetime
161    /// so that RecallTiming can track message count and topic changes.
162    /// Uses tokio::sync::RwLock (not parking_lot) so the guard is Send,
163    /// allowing it to be held across .await in tokio::spawn.
164    session_context: Arc<tokio::sync::RwLock<SessionContext>>,
165    /// Filesystem state store for agent persistence (JSON files).
166    state_store: Option<Arc<StateStore>>,
167    /// SQLite-backed agent history query index.
168    #[cfg(feature = "sqlite-memory")]
169    agent_log_db: Option<Arc<AgentLogDb>>,
170    /// Agent log retention configuration.
171    agent_log_config: AgentLogConfig,
172}
173
174impl BasicSupervisor {
175    /// Creates a new supervisor with the given event bus and agent runtime.
176    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    /// Attach a filesystem state store for agent history persistence.
193    pub fn set_state_store(&mut self, store: Arc<StateStore>) {
194        self.state_store = Some(store);
195    }
196
197    /// Attach a SQLite-backed agent history log database.
198    #[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    /// Set agent log retention configuration.
204    pub fn set_agent_log_config(&mut self, config: AgentLogConfig) {
205        self.agent_log_config = config;
206    }
207
208    /// Attach a resource monitor for agent count tracking.
209    pub fn set_resource_monitor(&mut self, rm: Arc<ResourceMonitor>) {
210        self.resource_monitor = Some(rm);
211    }
212
213    /// Update the resource monitor with current active agent count.
214    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    /// Access the agent pool for session continuation.
222    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        // Mark as running.
296        {
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        // Spawn the execution as a tokio task so we can track and abort it.
314        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        // Share the session context so RecallTiming persists across directives.
320        // Uses tokio::sync::RwLock so the guard is Send-safe across .await.
321        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            // Check for cancellation before starting.
327            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, // cancellation, not a provider failure
337                    restore_state: None,
338                    reasoning_text: String::new(),
339                    reasoning_segments: Vec::new(),
340                })
341            } else {
342                // Snapshot recall_timing under a brief read lock, execute
343                // WITHOUT holding any lock, then write back. SessionContext's
344                // only mutation during execution is recall_with_proactive
345                // (agent_runtime.rs:426); project IDs are read-only. Holding
346                // the write lock for the entire multi-minute execution was
347                // serializing all agents in the same session.
348                let mut temp_ctx = {
349                    let ctx = session_ctx.read().await;
350                    let mut c = crate::session_context::SessionContext::new();
351                    c.recall_timing = ctx.recall_timing.clone();
352                    c
353                };
354                let exec_result = runtime
355                    .execute_directive(id, &directive, &env, &mut temp_ctx)
356                    .await;
357                // Write back the mutated recall_timing (last-write-wins for
358                // concurrent agents — recall_timing is a heuristic tracker).
359                session_ctx.write().await.recall_timing = temp_ctx.recall_timing;
360                exec_result
361            };
362            // Receiver gone (run_with_directive returned early) → ignore error.
363            let _ = done_tx.send(result);
364        });
365
366        // Store the handle so kill() can abort the task.
367        {
368            let mut handles = self.handles.write();
369            handles.insert(
370                id,
371                AgentHandle {
372                    cancelled,
373                    task: handle,
374                },
375            );
376        }
377
378        // Await completion via the oneshot channel. If kill() aborts the task
379        // (or it panics), done_tx is dropped and this returns Err — treat as
380        // cancellation.
381        let result = match done_rx.await {
382            Ok(res) => res,
383            Err(_) => {
384                let mut handles = self.handles.write();
385                handles.remove(&id);
386                Ok(ExecutionResult {
387                    output: "Agent task aborted".into(),
388                    steps_completed: 0,
389                    success: false,
390                    tool_calls: vec![],
391                    tokens_input: 0,
392                    tokens_output: 0,
393                    model_id: String::new(),
394                    failure_class: None, // abort (kill/panic), not a provider failure
395                    restore_state: None,
396                    reasoning_text: String::new(),
397                    reasoning_segments: Vec::new(),
398                })
399            }
400        };
401
402        // Natural completion — remove the handle.
403        {
404            let mut handles = self.handles.write();
405            handles.remove(&id);
406        }
407
408        match result {
409            Ok(result) => {
410                tracing::info!(
411                    agent_id = %id,
412                    success = result.success,
413                    steps = result.steps_completed,
414                    "Agent task completed (directive)"
415                );
416
417                {
418                    let mut agents = self.agents.write();
419                    if let Some(agent) = agents.get_mut(&id) {
420                        agent.status = if result.success {
421                            AgentStatus::Completed
422                        } else {
423                            AgentStatus::Failed
424                        };
425                        agent.completed_at = Some(Utc::now());
426                        agent.steps_completed = result.steps_completed;
427                        agent.tool_calls = result
428                            .tool_calls
429                            .iter()
430                            .map(|tc| crate::types::ToolCallRecord {
431                                tool: tc.tool.clone(),
432                                input: tc.input.clone(),
433                                output: tc.output.clone(),
434                                duration_ms: tc.duration_ms,
435                                is_error: tc.is_error,
436                                tool_call_id: tc.tool_call_id.clone(),
437                                timestamp: tc.timestamp,
438                            })
439                            .collect();
440                        agent.tokens_input = result.tokens_input;
441                        agent.tokens_output = result.tokens_output;
442                        agent.model_id = result.model_id.clone();
443                        agent.cost_usd = if !result.model_id.is_empty() {
444                            crate::kernel_handle::engine_api::estimate_cost(
445                                &result.model_id,
446                                result.tokens_input,
447                                result.tokens_output,
448                            )
449                        } else {
450                            0.0
451                        };
452                        if !result.success {
453                            agent.error = Some(result.output.clone());
454                        }
455                    }
456                }
457
458                let _ = self
459                    .event_bus
460                    .publish(crate::event_bus::KernelEvent::AgentStopped {
461                        id,
462                        success: result.success,
463                    });
464                self.update_agent_count();
465
466                // Persist to agent history log (async, non-blocking)
467                self.persist_agent(id).await;
468
469                Ok(result)
470            }
471            Err(e) => {
472                tracing::error!(agent_id = %id, error = %e, "Agent task failed (directive)");
473
474                {
475                    let mut agents = self.agents.write();
476                    if let Some(agent) = agents.get_mut(&id) {
477                        agent.status = AgentStatus::Failed;
478                        agent.completed_at = Some(Utc::now());
479                        agent.error = Some(e.to_string());
480                    }
481                }
482
483                let _ = self
484                    .event_bus
485                    .publish(crate::event_bus::KernelEvent::AgentFailed {
486                        id,
487                        error: e.to_string(),
488                    });
489                self.update_agent_count();
490
491                // Persist to agent history log (async, non-blocking)
492                self.persist_agent(id).await;
493
494                Ok(ExecutionResult {
495                    output: format!("Agent failed: {e}"),
496                    steps_completed: 0,
497                    success: false,
498                    tool_calls: vec![],
499                    tokens_input: 0,
500                    tokens_output: 0,
501                    model_id: String::new(),
502                    reasoning_text: String::new(),
503                    reasoning_segments: Vec::new(),
504                    // (P2 RecoveryCoordinator, gateway user-facing
505                    // messages) can see whether this is a transient
506                    // retry, a quota/auth that needs provider swap,
507                    // context overflow, etc. Conservative: Unknown
508                    // when no pattern matches.
509                    failure_class: Some(classify(&e)),
510                    restore_state: e
511                        .downcast_ref::<crate::resilience::AgentRunError>()
512                        .and_then(|err| err.restore_state.clone()),
513                })
514            }
515        }
516    }
517
518    async fn wait(&self, id: AgentId) -> Result<AgentStatus> {
519        let agents = self.agents.read();
520        match agents.get(&id) {
521            Some(info) => Ok(info.status),
522            None => anyhow::bail!("Agent {id} not found"),
523        }
524    }
525
526    async fn kill(&self, id: AgentId) -> Result<()> {
527        // Cancel and abort the running task, if any.
528        {
529            let mut handles = self.handles.write();
530            if let Some(agent_handle) = handles.remove(&id) {
531                agent_handle.cancelled.store(true, Ordering::Relaxed);
532                agent_handle.task.abort();
533                tracing::info!(agent_id = %id, "Agent task aborted");
534            }
535        }
536
537        {
538            let mut agents = self.agents.write();
539            if let Some(agent) = agents.get_mut(&id) {
540                agent.status = AgentStatus::Stopped;
541                agent.completed_at = Some(Utc::now());
542            } else {
543                anyhow::bail!("Agent {id} not found");
544            }
545        }
546
547        let _ = self
548            .event_bus
549            .publish(crate::event_bus::KernelEvent::AgentStopped { id, success: false });
550        self.update_agent_count();
551
552        // Persist to agent history log (async, non-blocking)
553        self.persist_agent(id).await;
554
555        tracing::info!(agent_id = %id, "Agent killed");
556        Ok(())
557    }
558
559    async fn list(&self) -> Result<Vec<AgentInfo>> {
560        let agents = self.agents.read();
561        Ok(agents.values().cloned().collect())
562    }
563}
564
565impl BasicSupervisor {
566    /// Persist a terminated agent to both filesystem JSON and SQLite.
567    /// Non-blocking: spawns a tokio task for the actual persistence.
568    async fn persist_agent(&self, id: AgentId) {
569        // Snapshot the agent info from the in-memory map
570        let info = {
571            let agents = self.agents.read();
572            agents.get(&id).cloned()
573        };
574
575        let Some(info) = info else { return };
576
577        // 1. Filesystem JSON (source of truth)
578        if let Some(ref store) = self.state_store {
579            let store = store.clone();
580            let info = info.clone();
581            let max_entries = self.agent_log_config.max_entries;
582            let ttl_hours = self.agent_log_config.ttl_hours;
583            let batch_size = self.agent_log_config.prune_batch_size;
584            tokio::spawn(async move {
585                let _ = store
586                    .save_json("agents", &id.to_string(), &info)
587                    .await
588                    .inspect_err(|e| tracing::warn!(agent_id = %id, error = %e, "Failed to persist agent to filesystem"));
589
590                // Prune old records (async, best-effort)
591                if max_entries > 0 || ttl_hours > 0 {
592                    let _ = store
593                        .prune_agents_by_config(max_entries, ttl_hours, batch_size)
594                        .await
595                        .inspect_err(|e| tracing::warn!(error = %e, "Failed to prune agent log"));
596                }
597            });
598        }
599
600        // 2. SQLite (query index)
601        #[cfg(feature = "sqlite-memory")]
602        if let Some(ref db) = self.agent_log_db {
603            let db = db.clone();
604            let info = info.clone();
605            let config = self.agent_log_config.clone();
606            tokio::spawn(async move {
607                let _ = db
608                    .upsert_agent(&info)
609                    .inspect_err(|e| tracing::warn!(agent_id = %id, error = %e, "Failed to upsert agent to SQLite"));
610
611                // Prune old records
612                let _ = db
613                    .prune(&config)
614                    .inspect_err(|e| tracing::warn!(error = %e, "Failed to prune agent SQLite"));
615            });
616        }
617    }
618}
619
620/// A no-op supervisor used during KernelBuilder::build() to break the
621/// KernelHandle → AgentRuntime → Supervisor → KernelHandle cycle.
622///
623/// AgentApi.supervisor is only used for list/kill operations, not during
624/// tool registration, so this placeholder is safe during build time.
625pub struct NoOpSupervisor;
626
627#[async_trait::async_trait]
628impl Supervisor for NoOpSupervisor {
629    async fn exec(&self, _id: AgentId) -> Result<()> {
630        Err(anyhow::anyhow!(
631            "NoOpSupervisor: exec not available during build"
632        ))
633    }
634    async fn fork_directive(&self, _directive: &Directive, _env: &ExecEnv) -> Result<AgentId> {
635        Err(anyhow::anyhow!(
636            "NoOpSupervisor: fork_directive not available during build"
637        ))
638    }
639    async fn run_with_directive(
640        &self,
641        _id: AgentId,
642        _directive: &Directive,
643        _env: &ExecEnv,
644    ) -> Result<ExecutionResult> {
645        Err(anyhow::anyhow!(
646            "NoOpSupervisor: run_with_directive not available during build"
647        ))
648    }
649    async fn wait(&self, _id: AgentId) -> Result<AgentStatus> {
650        Err(anyhow::anyhow!(
651            "NoOpSupervisor: wait not available during build"
652        ))
653    }
654    async fn kill(&self, _id: AgentId) -> Result<()> {
655        Err(anyhow::anyhow!(
656            "NoOpSupervisor: kill not available during build"
657        ))
658    }
659    async fn list(&self) -> Result<Vec<AgentInfo>> {
660        Ok(Vec::new())
661    }
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667    use crate::event_bus::EventBus;
668    use crate::types::AgentStatus;
669
670    // Note: MockProvider no longer needed — OxiosEngine handles provider resolution.
671    // The engine resolves models internally, so tests just use OxiosEngine::new().
672
673    /// Helper to create a real BasicSupervisor wired to a real EventBus.
674    async fn make_supervisor() -> BasicSupervisor {
675        let event_bus = EventBus::new(64);
676
677        // Build a mock KernelHandle with temp dirs.
678        let tmp = std::env::temp_dir().join(format!("oxios-test-{}", uuid::Uuid::new_v4()));
679        let _ = std::fs::create_dir_all(&tmp);
680
681        let state_store_2 =
682            Arc::new(crate::state_store::StateStore::new(tmp.join("state")).expect("state store"));
683        let state_store = state_store_2.clone();
684        let memory_manager = Arc::new({
685            let mut mm = crate::memory::MemoryManager::new(state_store.clone());
686            mm.set_git_layer(Arc::new(
687                crate::git_layer::GitLayer::new(tmp.join("git"), false).expect("git layer"),
688            ));
689            mm
690        });
691
692        let kernel_handle = Arc::new(crate::KernelHandle::new(
693            crate::kernel_handle::StateApi::new(state_store),
694            crate::kernel_handle::AgentApi::new(
695                Arc::new(crate::supervisor::NoOpSupervisor),
696                Arc::new(crate::budget::BudgetManager::new()),
697                memory_manager.clone(),
698                Some(event_bus.clone()),
699            ),
700            crate::kernel_handle::SecurityApi::new(
701                Arc::new(parking_lot::Mutex::new(crate::auth::AuthManager::new())),
702                Arc::new(oxi_sdk::observability::AuditTrail::new(100)),
703                Arc::new(parking_lot::Mutex::new(
704                    crate::access_manager::AccessManager::new(),
705                )),
706                Arc::new(
707                    crate::state_store::StateStore::new(tmp.join("state2")).expect("state store 2"),
708                ),
709            ),
710            crate::kernel_handle::PersonaApi::new(Arc::new(crate::persona::PersonaManager::new())),
711            crate::kernel_handle::ExtensionApi::new(Arc::new(crate::skill::SkillManager::new(
712                tmp.join("skills"),
713                tmp.join("share/skills"),
714            ))),
715            crate::kernel_handle::McpApi::new(Arc::new(crate::mcp::McpBridge::new())),
716            crate::kernel_handle::InfraApi::new(
717                Arc::new(
718                    crate::git_layer::GitLayer::new(tmp.join("git"), false).expect("git layer"),
719                ),
720                Arc::new(crate::cron::CronScheduler::new(
721                    Arc::new(
722                        crate::state_store::StateStore::new(tmp.join("cron")).expect("cron state"),
723                    ),
724                    60,
725                )),
726                Arc::new(crate::resource_monitor::ResourceMonitor::new(60, 100)),
727                EventBus::new(64),
728                crate::config::OxiosConfig::default(),
729                std::time::Instant::now(),
730                std::sync::Arc::new(crate::tools::PendingToolApprovals::new()),
731                std::sync::Arc::new(crate::tools::PendingAskUser::new()),
732                std::sync::Arc::new(parking_lot::RwLock::new(
733                    crate::approval::ApprovalConfig::default(),
734                )),
735                std::sync::Arc::new(crate::tools::PendingPathAccess::new()),
736            ),
737            None,
738            crate::kernel_handle::ExecApi::new(
739                Arc::new(parking_lot::RwLock::new(
740                    crate::config::ExecConfig::default(),
741                )),
742                Arc::new(parking_lot::Mutex::new(
743                    crate::access_manager::AccessManager::new(),
744                )),
745            ),
746            crate::kernel_handle::A2aApi::new(Arc::new(crate::a2a::A2AProtocol::new(
747                EventBus::new(64),
748            ))),
749            crate::kernel_handle::EngineApi::new(
750                Arc::new(parking_lot::RwLock::new(
751                    crate::config::OxiosConfig::default(),
752                )),
753                tmp.join("config.toml"),
754                Arc::new(crate::kernel_handle::RoutingStats::new()),
755                Arc::new(crate::engine::EngineHandle::new(Arc::new(
756                    crate::OxiosEngine::new("anthropic/claude-sonnet-4-20250514"),
757                ))),
758            ),
759            Arc::new(oxios_markdown::KnowledgeBase::new(tmp.join("knowledge")).unwrap()),
760            Arc::new(
761                crate::kernel_handle::KnowledgeLens::new(
762                    Arc::new(oxios_markdown::KnowledgeBase::new(tmp.join("knowledge")).unwrap()),
763                    memory_manager.clone(),
764                )
765                .unwrap(),
766            ),
767            crate::kernel_handle::MarketplaceApi::new(
768                Arc::new(crate::skill::clawhub::ClawHubInstaller::new(
769                    tmp.join("skills"),
770                    tmp.join("state"),
771                    None,
772                )),
773                Arc::new(
774                    crate::skill::clawhub::ClawHubClient::new(None).expect("valid ClawHub client"),
775                ),
776                Arc::new(crate::skill::skills_sh::SkillsShInstaller::new(
777                    tmp.join("skills"),
778                    None,
779                    None,
780                )),
781                Arc::new(
782                    crate::skill::skills_sh::SkillsShClient::new(None, None)
783                        .expect("valid Skills.sh client"),
784                ),
785            ),
786            None,                                     // calendar (not configured in test)
787            Arc::new(parking_lot::RwLock::new(None)), // email (not configured in test)
788        ));
789
790        let engine = crate::OxiosEngine::new("mock/model");
791        let engine_handle = Arc::new(crate::engine::EngineHandle::new(Arc::new(engine)));
792        let runtime = AgentRuntime::new(engine_handle, kernel_handle, None);
793        BasicSupervisor::new(event_bus, runtime)
794    }
795
796    /// Helper to create a minimal (Directive, ExecEnv) pair for testing.
797    fn make_directive(goal: &str) -> (Directive, ExecEnv) {
798        (Directive::from_message(goal), ExecEnv::default())
799    }
800
801    #[tokio::test]
802    async fn test_fork_creates_agent() {
803        let supervisor = make_supervisor().await;
804        let (directive, env) = make_directive("Test agent");
805
806        let id = supervisor.fork_directive(&directive, &env).await.unwrap();
807
808        let agents = supervisor.list().await.unwrap();
809        assert_eq!(agents.len(), 1);
810        assert_eq!(agents[0].id, id);
811        assert_eq!(agents[0].name, "Test agent");
812        assert_eq!(agents[0].status, AgentStatus::Starting);
813    }
814
815    #[tokio::test]
816    async fn test_exec_updates_status_to_running() {
817        let supervisor = make_supervisor().await;
818        let (directive, env) = make_directive("Running agent");
819
820        let id = supervisor.fork_directive(&directive, &env).await.unwrap();
821        assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Starting);
822
823        supervisor.exec(id).await.unwrap();
824        assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Running);
825    }
826
827    #[tokio::test]
828    async fn test_kill_sets_stopped() {
829        let supervisor = make_supervisor().await;
830        let (directive, env) = make_directive("Doomed agent");
831
832        let id = supervisor.fork_directive(&directive, &env).await.unwrap();
833        supervisor.exec(id).await.unwrap();
834        assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Running);
835
836        supervisor.kill(id).await.unwrap();
837        assert_eq!(supervisor.wait(id).await.unwrap(), AgentStatus::Stopped);
838    }
839
840    #[tokio::test]
841    async fn test_kill_unknown_agent_returns_error() {
842        let supervisor = make_supervisor().await;
843        let unknown_id = uuid::Uuid::new_v4();
844
845        let result = supervisor.kill(unknown_id).await;
846        assert!(result.is_err());
847        assert!(result.unwrap_err().to_string().contains("not found"));
848    }
849
850    #[tokio::test]
851    async fn test_list_returns_all_agents() {
852        let supervisor = make_supervisor().await;
853
854        let (d1, e1) = make_directive("Agent 1");
855        let id1 = supervisor.fork_directive(&d1, &e1).await.unwrap();
856        let (d2, e2) = make_directive("Agent 2");
857        let id2 = supervisor.fork_directive(&d2, &e2).await.unwrap();
858        let (d3, e3) = make_directive("Agent 3");
859        let id3 = supervisor.fork_directive(&d3, &e3).await.unwrap();
860
861        let agents = supervisor.list().await.unwrap();
862        assert_eq!(agents.len(), 3);
863
864        let ids: std::collections::HashSet<AgentId> = agents.iter().map(|a| a.id).collect();
865        assert!(ids.contains(&id1));
866        assert!(ids.contains(&id2));
867        assert!(ids.contains(&id3));
868    }
869
870    #[tokio::test]
871    async fn test_exec_unknown_agent_returns_error() {
872        let supervisor = make_supervisor().await;
873        let unknown_id = uuid::Uuid::new_v4();
874
875        let result = supervisor.exec(unknown_id).await;
876        assert!(result.is_err());
877        assert!(result.unwrap_err().to_string().contains("not found"));
878    }
879
880    #[tokio::test]
881    async fn test_wait_unknown_agent_returns_error() {
882        let supervisor = make_supervisor().await;
883        let unknown_id = uuid::Uuid::new_v4();
884
885        let result = supervisor.wait(unknown_id).await;
886        assert!(result.is_err());
887        assert!(result.unwrap_err().to_string().contains("not found"));
888    }
889}