Skip to main content

leviath_runtime/
host.rs

1//! The world host: the daemon-side wrapper that owns a single [`PipelineWorld`],
2//! maps stable **run ids** to ECS entities, and interleaves external **control
3//! operations** with driving the world - all on one task, so there is never any
4//! locking around the world.
5//!
6//! Clients (a control socket, the TUI, the CLI) don't hold entities - those are
7//! generational indices meaningful only inside the world. They address agents by
8//! run id. The host keeps the `run_id → Entity` map and turns each
9//! [`ControlOp`] into the corresponding [`PipelineWorld`] call, replying on the
10//! op's oneshot channel.
11//!
12//! The serve loop drives the world to quiescence, then parks until either an
13//! async result wakes it, a control op arrives, or shutdown is signalled -
14//! handling a control op and then re-driving to quiescence so its effect (a
15//! resume, a delivered message) is applied immediately.
16
17use std::collections::{HashMap, HashSet};
18
19use bevy_ecs::entity::Entity;
20use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
21use tokio::sync::{broadcast, oneshot};
22
23use crate::components::{
24    AgentMessage, AgentState, AgentStatus, ContextWindow, ParentRef, SubAgentChildren,
25};
26use crate::interaction_hub::InteractionHub;
27use crate::persistence::{RunMetadata, TokenTotals};
28use crate::world::PipelineWorld;
29use leviath_core::interaction::{InteractionRequest, InteractionResponse};
30use serde::{Deserialize, Serialize};
31
32/// The parameters for spawning an agent into the world. The runtime doesn't know
33/// how to load blueprints or resolve tools - that policy lives in the
34/// [`Spawner`] the daemon installs - so this just carries the raw request.
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
36pub struct SpawnArgs {
37    /// The run id to give the new agent (its directory / control key).
38    pub run_id: String,
39    /// Path to the agent manifest directory or bundle.
40    pub blueprint_path: String,
41    /// The task prompt. Seeded into the region keyed `task` (see
42    /// [`crate::context_setup::init_window_seeded`]); a matching `regions`
43    /// entry, if present, overrides it.
44    pub task: String,
45    /// Literal seed content for named caller-input regions, keyed by the
46    /// region's caller-input name. Merged over `task` at spawn. `#[serde(default)]`
47    /// keeps older requests (which never sent this) deserializing to an empty map.
48    #[serde(default)]
49    pub regions: HashMap<String, String>,
50    /// Optional model override (`provider/model` or `model`).
51    #[serde(default)]
52    pub model: Option<String>,
53    /// Working directory for tool execution.
54    pub workdir: String,
55    /// Custom key/value metadata from the request.
56    #[serde(default)]
57    pub metadata: HashMap<String, String>,
58    /// Webhook to POST on completion/error (surfaced in the run metadata).
59    #[serde(default)]
60    pub callback_url: Option<String>,
61    /// Optional shared secret for HMAC-SHA256 signing the webhook body.
62    #[serde(default)]
63    pub callback_secret: Option<String>,
64    /// Run this agent unattended (the `--yolo` launch override): approve every
65    /// tool call, waive the taint gate, and auto-answer the agent's own prompts
66    /// (`ask_user_*`, blueprint interaction points) rather than parking on the
67    /// interaction hub for a person who isn't there.
68    #[serde(default)]
69    pub yolo: bool,
70    /// Refuse this run's `seed = { command = ... }` regions (the
71    /// `--no-seed-commands` launch override). Command seeds execute at spawn,
72    /// before any approval prompt, so this is the per-run counterpart to the
73    /// `[security] allow_seed_commands` config switch.
74    #[serde(default)]
75    pub no_seed_commands: bool,
76    /// Tools to allow outright for this run (the `--allow` launch override).
77    #[serde(default)]
78    pub allow: Vec<String>,
79    /// Override the blueprint's max sub-agent tree depth.
80    #[serde(default)]
81    pub max_depth: Option<usize>,
82    /// The run id of this agent's parent, when it is a sub-agent / fan-out
83    /// worker. Persisted in the run metadata so observers (dashboard, `serve`
84    /// tree) can nest children under their parent. `None` for a top-level run.
85    #[serde(default)]
86    pub parent_run_id: Option<String>,
87}
88
89/// The daemon-installed function that turns [`SpawnArgs`] into a live agent:
90/// loads the blueprint, resolves stages/tools, spawns into the world, and
91/// returns the new entity (the host records the run-id mapping). Returns `Err`
92/// with a human-readable message on failure.
93pub type Spawner = Box<dyn FnMut(&mut PipelineWorld, &SpawnArgs) -> Result<Entity, String> + Send>;
94
95/// The daemon-installed function that pages a previously-unloaded run back into
96/// the world from its on-disk state: given a run id, it reloads the agent (its
97/// blueprint, tool state, context, stage) and returns the new entity, or `None`
98/// if there is no such resumable run on disk. Used for reload-on-demand - a
99/// control/sub-agent op targeting a run that isn't currently in memory pages it
100/// in first via the host's internal resolve-or-reload step. Installed with
101/// [`WorldHost::set_reloader`].
102pub type Reloader = Box<dyn FnMut(&mut PipelineWorld, &str) -> Option<Entity> + Send>;
103
104/// The daemon-installed last resort for cancelling a run the world cannot hold:
105/// given a run id, it forces that run's **on-disk** state to a terminal status
106/// and reports whether a run directory existed to act on.
107///
108/// This is what makes a cancel unconditional. [`Reloader`] declines whenever a
109/// run can't be rebuilt - its blueprint was moved or deleted, its metadata is
110/// unreadable, it died mid-spawn before any agent existed - and before this seam
111/// a cancel in that state replied `false` and wrote nothing, so `meta.json` kept
112/// claiming `running`/`starting` forever and the run could never be got rid of.
113/// The runtime has no notion of the on-disk layout, so the daemon supplies the
114/// writer. Installed with [`WorldHost::set_force_terminator`]; without one, a
115/// cancel that misses in the world simply misses (the prior behavior).
116pub type ForceTerminator = Box<dyn FnMut(&str) -> bool + Send>;
117
118/// The daemon-installed hook run just before a terminal agent's entity is
119/// despawned (reaped). It receives the world and the entity while both are still
120/// valid, so the daemon can release per-agent resources the runtime doesn't know
121/// about - tearing down the agent's sandbox and dropping its tool state.
122/// Installed with [`WorldHost::set_reaper`]; a no-op when none is set.
123pub type Reaper = Box<dyn FnMut(&mut PipelineWorld, Entity) + Send>;
124
125/// An async hook the host awaits *before* servicing a top-level `Spawn` control
126/// op, so the daemon can do async preparation the sync spawner can't - e.g.
127/// lazily connecting the blueprint's MCP servers into the shared pool so
128/// they're warm by the time [`Spawner`] reads them. The returned future is
129/// `'static` (it must clone anything it needs from the `SpawnArgs`). Installed
130/// with [`WorldHost::set_spawn_preprocessor`]; when none is set, spawns proceed
131/// straight to the spawner.
132pub type SpawnPreprocessor = Box<
133    dyn Fn(&SpawnArgs) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> + Send,
134>;
135
136/// A world-access request from an agent's tool lane. The sub-agent tools
137/// (`spawn_agent`/`check_agent`/`send_to_agent`/`kill_agent`) need the world and
138/// the [`Spawner`], which only the host holds - the tool lane runs async, off the
139/// world. Each carries a oneshot reply, so the (sequential) tool lane blocks on
140/// the host applying it, mirroring the interaction hub.
141pub enum SubAgentOp {
142    /// Spawn a child agent from `args`, linked as a child of `parent_run_id`.
143    /// Rejected if the child would exceed `max_depth`. Reply is the child run id.
144    Spawn {
145        /// The child's spawn parameters (blueprint path, task, etc.). Boxed
146        /// because it is much larger than the other variants' payloads.
147        args: Box<SpawnArgs>,
148        /// The run id of the agent doing the spawning.
149        parent_run_id: String,
150        /// Maximum allowed sub-agent tree depth (root = 0).
151        max_depth: usize,
152        /// Reply: the child's run id, or an error message.
153        reply: oneshot::Sender<Result<String, String>>,
154    },
155    /// Report a run's current status (`None` if the host has no such live run).
156    Check {
157        /// The run to query.
158        run_id: String,
159        /// Reply: the run's status.
160        reply: oneshot::Sender<Option<AgentStatus>>,
161    },
162    /// Deliver a message into a running agent's inbox. Reply is whether a live
163    /// agent accepted it.
164    Send {
165        /// The target run.
166        run_id: String,
167        /// The run doing the sending. The target must be it or one of its
168        /// descendants - see `WorldHost::is_within_tree`.
169        caller_run_id: String,
170        /// The message body.
171        content: String,
172        /// Reply: whether the message was accepted.
173        reply: oneshot::Sender<bool>,
174    },
175    /// Cancel a run and its whole sub-tree. Reply is whether any agent was found.
176    Kill {
177        /// The run to cancel (with its descendants).
178        run_id: String,
179        /// The run doing the cancelling. The target must be it or one of its
180        /// descendants - see `WorldHost::is_within_tree`.
181        caller_run_id: String,
182        /// Reply: whether anything was cancelled.
183        reply: oneshot::Sender<bool>,
184    },
185}
186
187/// A control operation addressed to the host, each carrying a oneshot channel the
188/// host replies on. Agents are addressed by run id.
189pub enum ControlOp {
190    /// Spawn a new agent. Reply is the run id on success, or an error message.
191    Spawn {
192        /// The spawn request. Boxed because it is much larger than the other
193        /// variants' payloads.
194        args: Box<SpawnArgs>,
195        /// Reply channel.
196        reply: oneshot::Sender<Result<String, String>>,
197    },
198    /// The status of a run, or `None` if there is no such run.
199    Status {
200        /// The run to query.
201        run_id: String,
202        /// Reply channel.
203        reply: oneshot::Sender<Option<AgentStatus>>,
204    },
205    /// Pause a run. Reply is `false` if there is no such (live) run.
206    Pause {
207        /// The run to pause.
208        run_id: String,
209        /// Reply channel.
210        reply: oneshot::Sender<bool>,
211    },
212    /// Resume a paused run. Reply is `false` if there is no such (live) run.
213    Resume {
214        /// The run to resume.
215        run_id: String,
216        /// Reply channel.
217        reply: oneshot::Sender<bool>,
218    },
219    /// Cancel a run. Reply is `false` if there is no such (live) run.
220    Cancel {
221        /// The run to cancel.
222        run_id: String,
223        /// Reply channel.
224        reply: oneshot::Sender<bool>,
225    },
226    /// List every known live run and its status.
227    List {
228        /// Reply channel.
229        reply: oneshot::Sender<Vec<(String, AgentStatus)>>,
230    },
231    /// Deliver a message to a running agent (by agent id). Reply is `false` if the
232    /// world's message channel is closed.
233    Message {
234        /// Target agent id.
235        agent_id: String,
236        /// Message body.
237        content: String,
238        /// Optional target region (defaults to the conversation region).
239        target_region: Option<String>,
240        /// Reply channel.
241        reply: oneshot::Sender<bool>,
242    },
243    /// List every open interaction awaiting an answer, as `(agent_id, request)`.
244    ListInteractions {
245        /// Reply channel.
246        reply: oneshot::Sender<Vec<(String, InteractionRequest)>>,
247    },
248    /// Answer an open interaction. Reply is `false` if no such request is open.
249    AnswerInteraction {
250        /// The answer (its `request_id` selects the interaction).
251        response: InteractionResponse,
252        /// Reply channel.
253        reply: oneshot::Sender<bool>,
254    },
255    /// Cancel an open interaction (its asker wakes with a neutral response).
256    /// Reply is `false` if no such request is open.
257    CancelInteraction {
258        /// The interaction id to cancel.
259        request_id: String,
260        /// Reply channel.
261        reply: oneshot::Sender<bool>,
262    },
263    /// Shut the daemon down: signal the world's shutdown so the serve loop
264    /// returns. Reply is sent (`true`) before the shutdown is triggered.
265    Shutdown {
266        /// Reply channel.
267        reply: oneshot::Sender<bool>,
268    },
269}
270
271/// A change in the world, broadcast to subscribers (the HTTP/WS gateway) so they
272/// get pushed updates instead of polling. Emitted by the host as it drives the
273/// world; streamed over the control transport via `ControlRequest::Subscribe`.
274#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
275#[serde(tag = "event", rename_all = "snake_case")]
276pub enum WorldEvent {
277    /// A run first appeared in the world.
278    Spawned {
279        /// The run id.
280        run_id: String,
281        /// The agent id.
282        agent_id: String,
283        /// The blueprint / agent name.
284        blueprint: String,
285    },
286    /// A run's status, stage, iteration, or tool-call count changed.
287    Status {
288        /// The run id.
289        run_id: String,
290        /// The agent id.
291        agent_id: String,
292        /// Short status label (`active`, `waiting`, `complete`, …).
293        status: String,
294        /// The current stage name.
295        stage: String,
296        /// The current iteration.
297        iteration: usize,
298        /// Cumulative tool calls.
299        tool_calls: usize,
300        /// Whether the current stage accepts messages.
301        accepts_messages: bool,
302    },
303    /// A run's token totals changed.
304    Tokens {
305        /// The run id.
306        run_id: String,
307        /// The agent id.
308        agent_id: String,
309        /// Cumulative prompt tokens.
310        prompt_tokens: usize,
311        /// Cumulative completion tokens.
312        completion_tokens: usize,
313        /// Cumulative cached tokens.
314        cached_tokens: usize,
315        /// Cumulative cache-write tokens.
316        cache_write_tokens: usize,
317    },
318    /// A run's context-window token usage changed.
319    Context {
320        /// The run id.
321        run_id: String,
322        /// The agent id.
323        agent_id: String,
324        /// Current context tokens.
325        total_tokens: usize,
326        /// Max context tokens.
327        max_tokens: usize,
328    },
329    /// A run raised a new interaction awaiting an answer.
330    Interaction {
331        /// The run id.
332        run_id: String,
333        /// The agent id.
334        agent_id: String,
335        /// The interaction request.
336        request: InteractionRequest,
337    },
338    /// A run reached a terminal status.
339    Completed {
340        /// The run id.
341        run_id: String,
342        /// The agent id.
343        agent_id: String,
344        /// The terminal status label.
345        status: String,
346    },
347    /// A run produced a per-agent log/output line (readable assistant output or
348    /// an operational `[Tokens: …]` / `[tool] …` / `[error] …` line).
349    Log {
350        /// The run id.
351        run_id: String,
352        /// The agent id.
353        agent_id: String,
354        /// The log line text.
355        line: String,
356    },
357}
358
359/// A world resource holding a clone of the host's [`WorldEvent`] broadcast
360/// sender, so ECS systems (e.g. the persistence drain) can push events - notably
361/// per-agent [`WorldEvent::Log`] lines - into the same stream the control
362/// transport serves. Absent in worlds that don't stream (test / `lev run`), where
363/// systems that depend on it become no-ops.
364// `Resource` moved from `bevy_ecs::system` to `bevy_ecs::resource` in 0.19.
365#[derive(bevy_ecs::resource::Resource, Clone)]
366pub struct WorldEventSink(pub broadcast::Sender<WorldEvent>);
367
368/// A short, stable status label for [`WorldEvent`].
369fn status_str(status: &AgentStatus) -> &'static str {
370    match status {
371        AgentStatus::Idle => "idle",
372        AgentStatus::Active => "active",
373        AgentStatus::Waiting => "waiting",
374        AgentStatus::Complete => "complete",
375        AgentStatus::Error { .. } => "error",
376        AgentStatus::Cancelled => "cancelled",
377    }
378}
379
380/// The last-emitted snapshot of an agent, for change detection.
381#[derive(Clone)]
382struct Emitted {
383    status: &'static str,
384    stage: String,
385    iteration: usize,
386    tool_calls: usize,
387    accepts_messages: bool,
388    prompt_tokens: usize,
389    completion_tokens: usize,
390    cached_tokens: usize,
391    cache_write_tokens: usize,
392    context_tokens: usize,
393    terminal: bool,
394}
395
396/// Owns the world and the run-id map; drives the world and services control ops.
397pub struct WorldHost {
398    world: PipelineWorld,
399    by_run_id: HashMap<String, Entity>,
400    interactions: InteractionHub,
401    spawner: Option<Spawner>,
402    spawn_preprocessor: Option<SpawnPreprocessor>,
403    reloader: Option<Reloader>,
404    force_terminator: Option<ForceTerminator>,
405    reaper: Option<Reaper>,
406    events: broadcast::Sender<WorldEvent>,
407    emitted: HashMap<String, Emitted>,
408    emitted_interactions: HashSet<String>,
409    /// Sub-agent world-access requests from tool lanes. The host holds a `tx`
410    /// clone so the receiver never closes (its `recv` never yields `None`).
411    subagent_tx: UnboundedSender<SubAgentOp>,
412    subagent_rx: UnboundedReceiver<SubAgentOp>,
413}
414
415impl WorldHost {
416    /// Wrap a world with a fresh interaction hub.
417    pub fn new(world: PipelineWorld) -> Self {
418        Self::with_interactions(world, InteractionHub::new())
419    }
420
421    /// Wrap a world with a specific interaction hub - the daemon shares one hub
422    /// between the tool service's per-agent backends and this host.
423    pub fn with_interactions(mut world: PipelineWorld, interactions: InteractionHub) -> Self {
424        let (events, _) = broadcast::channel(1024);
425        // Let ECS systems (the persistence drain) push events - per-agent log
426        // lines - into the same stream the control transport serves.
427        world
428            .world_mut()
429            .insert_resource(WorldEventSink(events.clone()));
430        let (subagent_tx, subagent_rx) = tokio::sync::mpsc::unbounded_channel();
431        Self {
432            world,
433            by_run_id: HashMap::new(),
434            interactions,
435            spawner: None,
436            spawn_preprocessor: None,
437            reloader: None,
438            force_terminator: None,
439            reaper: None,
440            events,
441            emitted: HashMap::new(),
442            emitted_interactions: HashSet::new(),
443            subagent_tx,
444            subagent_rx,
445        }
446    }
447
448    /// A sender for [`SubAgentOp`]s. The daemon hands a clone to each agent's tool
449    /// state so the sub-agent tools can reach the world through the host.
450    pub fn subagent_sender(&self) -> UnboundedSender<SubAgentOp> {
451        self.subagent_tx.clone()
452    }
453
454    /// Subscribe to [`WorldEvent`]s. The HTTP/WS gateway uses this (via the
455    /// control transport's `Subscribe`) to push updates instead of polling.
456    pub fn subscribe(&self) -> broadcast::Receiver<WorldEvent> {
457        self.events.subscribe()
458    }
459
460    /// The world-event sender, handed to the control transport so a `Subscribe`
461    /// connection can stream events.
462    pub fn event_sender(&self) -> broadcast::Sender<WorldEvent> {
463        self.events.clone()
464    }
465
466    /// Diff every registered run against its last-emitted snapshot and broadcast
467    /// what changed (status/tokens/context/completion) plus any new interaction.
468    /// Called after each drive to quiescence, so subscribers see every change.
469    fn emit_events(&mut self) {
470        self.adopt_unregistered_runs();
471        let pairs: Vec<(String, Entity)> = self
472            .by_run_id
473            .iter()
474            .map(|(k, &v)| (k.clone(), v))
475            .collect();
476        // Terminal agents to unload from memory this pass (their disk state is
477        // preserved and still viewable). Collected during the loop, reaped after.
478        let mut to_reap: Vec<(String, Entity)> = Vec::new();
479        for (run_id, entity) in pairs {
480            let Some(state) = self.world.world().get::<AgentState>(entity) else {
481                continue; // reaped between registration and now
482            };
483            let agent_id = state.agent_id.clone();
484            let status = status_str(&state.status);
485            let terminal = matches!(
486                state.status,
487                AgentStatus::Complete | AgentStatus::Error { .. } | AgentStatus::Cancelled
488            );
489            let cur = {
490                let totals = self
491                    .world
492                    .world()
493                    .get::<TokenTotals>(entity)
494                    .copied()
495                    .unwrap_or_default();
496                let (context_tokens, _) = self
497                    .world
498                    .world()
499                    .get::<ContextWindow>(entity)
500                    .map(|w| (w.current_tokens, w.max_tokens))
501                    .unwrap_or((0, 0));
502                Emitted {
503                    status,
504                    stage: state.current_stage.clone(),
505                    iteration: state.iteration,
506                    tool_calls: totals.tool_calls,
507                    accepts_messages: state.accepts_messages,
508                    prompt_tokens: totals.prompt_tokens,
509                    completion_tokens: totals.completion_tokens,
510                    cached_tokens: totals.cached_tokens,
511                    cache_write_tokens: totals.cache_write_tokens,
512                    context_tokens,
513                    terminal,
514                }
515            };
516            let max_tokens = self
517                .world
518                .world()
519                .get::<ContextWindow>(entity)
520                .map(|w| w.max_tokens)
521                .unwrap_or(0);
522            let prev = self.emitted.get(&run_id).cloned();
523
524            if prev.is_none() {
525                let blueprint = self
526                    .world
527                    .world()
528                    .get::<RunMetadata>(entity)
529                    .map(|m| m.agent_name.clone())
530                    .unwrap_or_default();
531                let _ = self.events.send(WorldEvent::Spawned {
532                    run_id: run_id.clone(),
533                    agent_id: agent_id.clone(),
534                    blueprint,
535                });
536            }
537
538            let status_key = |e: &Emitted| {
539                (
540                    e.status,
541                    e.stage.clone(),
542                    e.iteration,
543                    e.tool_calls,
544                    e.accepts_messages,
545                )
546            };
547            if prev.as_ref().map(status_key) != Some(status_key(&cur)) {
548                let _ = self.events.send(WorldEvent::Status {
549                    run_id: run_id.clone(),
550                    agent_id: agent_id.clone(),
551                    status: status.to_string(),
552                    stage: cur.stage.clone(),
553                    iteration: cur.iteration,
554                    tool_calls: cur.tool_calls,
555                    accepts_messages: cur.accepts_messages,
556                });
557            }
558
559            let token_key = |e: &Emitted| {
560                (
561                    e.prompt_tokens,
562                    e.completion_tokens,
563                    e.cached_tokens,
564                    e.cache_write_tokens,
565                )
566            };
567            if prev.as_ref().map(token_key) != Some(token_key(&cur)) {
568                let _ = self.events.send(WorldEvent::Tokens {
569                    run_id: run_id.clone(),
570                    agent_id: agent_id.clone(),
571                    prompt_tokens: cur.prompt_tokens,
572                    completion_tokens: cur.completion_tokens,
573                    cached_tokens: cur.cached_tokens,
574                    cache_write_tokens: cur.cache_write_tokens,
575                });
576            }
577
578            if prev.as_ref().map(|e| e.context_tokens) != Some(cur.context_tokens) {
579                let _ = self.events.send(WorldEvent::Context {
580                    run_id: run_id.clone(),
581                    agent_id: agent_id.clone(),
582                    total_tokens: cur.context_tokens,
583                    max_tokens,
584                });
585            }
586
587            let was_terminal = prev.as_ref().map(|e| e.terminal) == Some(true);
588            if cur.terminal && !was_terminal {
589                let _ = self.events.send(WorldEvent::Completed {
590                    run_id: run_id.clone(),
591                    agent_id: agent_id.clone(),
592                    status: status.to_string(),
593                });
594            }
595            // Unload a terminal agent once its terminal state has been emitted (a
596            // prior pass already saw it terminal, so the event went out and the
597            // persistence lane captured it) and no live parent still needs it.
598            if cur.terminal && was_terminal && self.no_live_parent(entity) {
599                to_reap.push((run_id.clone(), entity));
600            }
601            // NOTE: non-terminal `Waiting` agents are intentionally NOT unloaded.
602            // Every `Waiting` state carries a live, unpersisted continuation - a
603            // blocked `ask` future (`AwaitingInteraction`), running fan-out workers
604            // (`FanOutWaiting`), or pending children (`WaitingForChildren`) - so
605            // flushing one to disk and paging it back cannot resume it (in-flight
606            // interactions aren't persisted; the blocked future is gone). Only
607            // terminal agents, whose full state is on disk, are safe to reap.
608
609            self.emitted.insert(run_id, cur);
610        }
611
612        // Reap: run the daemon's reap hook (sandbox teardown + tool-state drop)
613        // while the entity is still valid, then despawn it and erase its host-map
614        // entries. Iterating a snapshot of `by_run_id` above means removing here
615        // is safe. The reaper is moved out for the loop to avoid borrowing `self`
616        // twice, then restored.
617        let mut reaper = self.reaper.take();
618        for (run_id, entity) in to_reap {
619            if let Some(reaper) = reaper.as_mut() {
620                reaper(&mut self.world, entity);
621            }
622            self.world.world_mut().despawn(entity);
623            self.by_run_id.remove(&run_id);
624            self.emitted.remove(&run_id);
625        }
626        self.reaper = reaper;
627
628        for (agent_id, request) in self.interactions.pending() {
629            if self.emitted_interactions.insert(request.id.clone()) {
630                let _ = self.events.send(WorldEvent::Interaction {
631                    run_id: agent_id.clone(),
632                    agent_id,
633                    request,
634                });
635            }
636        }
637    }
638
639    /// Register any agent that exists in the world but is missing from the run-id
640    /// map, so the host's view is the world's view.
641    ///
642    /// Not every agent arrives through a `Spawn` control op: fan-out workers are
643    /// built straight into the world by the fan-out spawner, which has no handle
644    /// on the host to register them. An unregistered agent is invisible to `list`
645    /// (so `lev ps` never showed a worker), never reaped (its sandbox and tool
646    /// state leak), and - worst - un-cancellable, because a cancel by its run id
647    /// misses the map, falls through to the reloader, and pages a **second** live
648    /// entity in from that run's on-disk state while the original keeps running.
649    /// Adopting them here is idempotent and keeps a stale mapping from winning:
650    /// a registered id whose entity has been despawned is re-pointed.
651    fn adopt_unregistered_runs(&mut self) {
652        let live: Vec<(String, Entity)> = self
653            .world
654            .world_mut()
655            .query::<(Entity, &RunMetadata)>()
656            .iter(self.world.world())
657            .map(|(entity, md)| (md.run_id.clone(), entity))
658            .collect();
659        for (run_id, entity) in live {
660            if self.live_entity(&run_id) != Some(entity) {
661                self.by_run_id.insert(run_id, entity);
662            }
663        }
664    }
665
666    /// Whether a terminal agent is safe to unload: it has no **live** parent that
667    /// might still be waiting on it. True for a root (no `ParentRef`), or when its
668    /// parent has been despawned or is itself terminal; false while a non-terminal
669    /// parent could still be gating on this child.
670    fn no_live_parent(&self, entity: Entity) -> bool {
671        let world = self.world.world();
672        match world.get::<crate::components::ParentRef>(entity) {
673            None => true,
674            Some(parent_ref) => match world.get::<AgentState>(parent_ref.parent_entity) {
675                None => true,
676                Some(state) => matches!(
677                    state.status,
678                    AgentStatus::Complete | AgentStatus::Error { .. } | AgentStatus::Cancelled
679                ),
680            },
681        }
682    }
683
684    /// Install the spawner used to service `Spawn` control ops. Without one, a
685    /// `Spawn` op replies with an error.
686    pub fn set_spawner(&mut self, spawner: Spawner) {
687        self.spawner = Some(spawner);
688    }
689
690    /// Install the async hook awaited before each top-level `Spawn` (see
691    /// [`SpawnPreprocessor`]).
692    pub fn set_spawn_preprocessor(&mut self, pp: SpawnPreprocessor) {
693        self.spawn_preprocessor = Some(pp);
694    }
695
696    /// Install the reloader used to page an unloaded run back in on demand.
697    /// Without one, an op targeting a run that isn't in memory just misses.
698    pub fn set_reloader(&mut self, reloader: Reloader) {
699        self.reloader = Some(reloader);
700    }
701
702    /// Install the [`ForceTerminator`] used to terminate a run on disk when the
703    /// world cannot hold it. Without one, a cancel that misses in the world and
704    /// can't be reloaded just misses.
705    pub fn set_force_terminator(&mut self, force_terminator: ForceTerminator) {
706        self.force_terminator = Some(force_terminator);
707    }
708
709    /// Install the reap hook run just before each terminal agent is despawned,
710    /// so the daemon can tear down that agent's sandbox and drop its tool state.
711    /// Without one, reaping just despawns the entity (the prior behavior).
712    pub fn set_reaper(&mut self, reaper: Reaper) {
713        self.reaper = Some(reaper);
714    }
715
716    /// Resolve a run id to a live entity, paging it in from disk if it has been
717    /// unloaded (and a reloader is installed). Returns `None` if the run is
718    /// neither live nor resumable from disk. Newly-reloaded runs are registered.
719    fn resolve_or_reload(&mut self, run_id: &str) -> Option<Entity> {
720        if let Some(entity) = self.live_entity(run_id) {
721            return Some(entity);
722        }
723        let entity = (self.reloader.as_mut()?)(&mut self.world, run_id)?;
724        self.by_run_id.insert(run_id.to_string(), entity);
725        Some(entity)
726    }
727
728    /// A clone of the interaction hub, for building per-agent backends.
729    pub fn interactions(&self) -> InteractionHub {
730        self.interactions.clone()
731    }
732
733    /// Mutable access to the underlying world (for the spawner to add agents).
734    pub fn world_mut(&mut self) -> &mut PipelineWorld {
735        &mut self.world
736    }
737
738    /// Record the run-id → entity mapping for a freshly-spawned agent.
739    pub fn register(&mut self, run_id: impl Into<String>, entity: Entity) {
740        self.by_run_id.insert(run_id.into(), entity);
741    }
742
743    /// Resolve a run id to a **live** entity (one that still exists in the world).
744    fn live_entity(&self, run_id: &str) -> Option<Entity> {
745        let entity = *self.by_run_id.get(run_id)?;
746        self.world.world().get::<AgentState>(entity).map(|_| entity)
747    }
748
749    /// Service one [`SubAgentOp`] from a tool lane, replying on its oneshot.
750    fn handle_subagent(&mut self, op: SubAgentOp) {
751        match op {
752            SubAgentOp::Spawn {
753                args,
754                parent_run_id,
755                max_depth,
756                reply,
757            } => {
758                let _ = reply.send(self.spawn_child(*args, &parent_run_id, max_depth));
759            }
760            SubAgentOp::Check { run_id, reply } => {
761                let status = self
762                    .live_entity(&run_id)
763                    .and_then(|e| self.world.agent_status(e));
764                let _ = reply.send(status);
765            }
766            SubAgentOp::Send {
767                run_id,
768                caller_run_id,
769                content,
770                reply,
771            } => {
772                if !self.is_within_tree(&run_id, &caller_run_id) {
773                    let _ = reply.send(false);
774                    return;
775                }
776                // Page the target in if it was unloaded, so delivery finds it.
777                self.resolve_or_reload(&run_id);
778                let ok = self
779                    .world
780                    .send_message(AgentMessage {
781                        agent_id: run_id,
782                        content,
783                        target_region: None,
784                        priority: 0,
785                    })
786                    .is_ok();
787                let _ = reply.send(ok);
788            }
789            SubAgentOp::Kill {
790                run_id,
791                caller_run_id,
792                reply,
793            } => {
794                let within = self.is_within_tree(&run_id, &caller_run_id);
795                let _ = reply.send(within && self.cancel_tree(&run_id));
796            }
797        }
798    }
799
800    /// Spawn a child agent under `parent_run_id`, linking `ParentRef` /
801    /// `SubAgentChildren` and registering its run id. `Err` if the parent is not
802    /// live, the depth limit is reached, or the spawner rejects it.
803    fn spawn_child(
804        &mut self,
805        mut args: SpawnArgs,
806        parent_run_id: &str,
807        max_depth: usize,
808    ) -> Result<String, String> {
809        // Record the parentage so the child's run metadata nests it in the tree.
810        args.parent_run_id = Some(parent_run_id.to_string());
811        let parent = self
812            .live_entity(parent_run_id)
813            .ok_or_else(|| format!("parent run '{parent_run_id}' is not live"))?;
814        let parent_depth = self
815            .world
816            .world()
817            .get::<ParentRef>(parent)
818            .map_or(0, |p| p.depth);
819        let child_depth = parent_depth + 1;
820        if child_depth > max_depth {
821            return Err(format!(
822                "sub-agent depth limit ({max_depth}) reached; not spawning deeper"
823            ));
824        }
825        let run_id = args.run_id.clone();
826        let child = match self.spawner.as_mut() {
827            Some(spawner) => spawner(&mut self.world, &args)?,
828            None => return Err("this daemon cannot spawn agents".to_string()),
829        };
830        let world = self.world.world_mut();
831        world.entity_mut(child).insert(ParentRef {
832            parent_entity: parent,
833            parent_agent_id: parent_run_id.to_string(),
834            depth: child_depth,
835        });
836        match world.get_mut::<SubAgentChildren>(parent) {
837            Some(mut kids) => kids.children.push(child),
838            None => {
839                world.entity_mut(parent).insert(SubAgentChildren {
840                    children: vec![child],
841                    max_child_depth: max_depth,
842                });
843            }
844        }
845        // Record the child's run-id on the parent's serializable state so the
846        // tree is persisted (and restart can rebuild `SubAgentChildren`). A
847        // spawning parent always carries `AgentState`.
848        world
849            .get_mut::<crate::components::AgentState>(parent)
850            .expect("a spawning parent always has AgentState")
851            .spawned_children_ids
852            .push(run_id.clone());
853        // Seed the child's context from the parent per any declared blueprint
854        // context transform (planner→coder region mapping, etc.).
855        crate::context_transform::apply_context_transforms(world, parent, child);
856        self.by_run_id.insert(run_id.clone(), child);
857        Ok(run_id)
858    }
859
860    /// Cancel a run and every descendant, paging the root in from disk first if it
861    /// had been unloaded. Returns whether the run was found in the world.
862    ///
863    /// Cancelling only the root would leave its sub-agents and fan-out workers
864    /// running - they are independent agents the schedule keeps driving, so they
865    /// would carry on spending tokens with no parent to report to. Each cancelled
866    /// agent's open interactions are closed too, so nothing is left blocked on a
867    /// prompt for a run that is going away.
868    /// Whether `run_id` is `ancestor` itself or one of its descendants.
869    ///
870    /// `send_to_agent` and `kill_agent` took any run id at all. Nothing tied the
871    /// target to the caller, so an agent could cancel an unrelated run, inject
872    /// text into its context, or - worst - hand it data: a message is added to
873    /// the target as `Public` regardless of the sender's taint, so an agent
874    /// holding `Private` context whose own outbound tools were gated could pass
875    /// it to a sibling whose tools were not. That is a laundering channel
876    /// straight through the middle of taint tracking.
877    ///
878    /// A downward walk from the caller, the same shape [`cancel_tree`] uses:
879    /// parentage is recorded as `SubAgentChildren`, so "is it mine" is "is it in
880    /// my subtree".
881    ///
882    /// [`cancel_tree`]: Self::cancel_tree
883    fn is_within_tree(&mut self, run_id: &str, ancestor: &str) -> bool {
884        if run_id == ancestor {
885            return true;
886        }
887        // Both ends as entities: the host already maps run ids to them, and
888        // comparing entities avoids re-reading an id component per node.
889        let (Some(target), Some(root)) = (
890            self.resolve_or_reload(run_id),
891            self.resolve_or_reload(ancestor),
892        ) else {
893            return false;
894        };
895        let mut stack = vec![root];
896        while let Some(e) = stack.pop() {
897            if e == target {
898                return true;
899            }
900            if let Some(kids) = self.world.world().get::<SubAgentChildren>(e) {
901                stack.extend(kids.children.iter().copied());
902            }
903        }
904        false
905    }
906
907    fn cancel_tree(&mut self, run_id: &str) -> bool {
908        let Some(root) = self.resolve_or_reload(run_id) else {
909            return false;
910        };
911        // Collect the subtree (parent before children), then cancel each.
912        let mut subtree = Vec::new();
913        let mut stack = vec![root];
914        while let Some(e) = stack.pop() {
915            subtree.push(e);
916            if let Some(kids) = self.world.world().get::<SubAgentChildren>(e) {
917                stack.extend(kids.children.iter().copied());
918            }
919        }
920        let mut cancelled = false;
921        for e in subtree {
922            // Read the agent id before cancelling - the entity stays valid until
923            // it is reaped, but reading first keeps this independent of that.
924            let agent_id = self
925                .world
926                .world()
927                .get::<AgentState>(e)
928                .map(|s| s.agent_id.clone());
929            cancelled |= self.world.cancel(e);
930            if let Some(agent_id) = agent_id {
931                self.interactions.cancel_for_agent(&agent_id);
932                // The hub is keyed by agent id but the emitted-interaction set is
933                // keyed by request id, so drop the ids that are no longer pending.
934                let still_open: HashSet<String> = self
935                    .interactions
936                    .pending()
937                    .into_iter()
938                    .map(|(_, req)| req.id)
939                    .collect();
940                self.emitted_interactions
941                    .retain(|id| still_open.contains(id));
942            }
943        }
944        cancelled
945    }
946
947    /// List every known live run and its status.
948    fn list(&self) -> Vec<(String, AgentStatus)> {
949        self.by_run_id
950            .iter()
951            .filter_map(|(run_id, &entity)| {
952                self.world
953                    .world()
954                    .get::<AgentState>(entity)
955                    .map(|s| (run_id.clone(), s.status.clone()))
956            })
957            .collect()
958    }
959
960    /// Apply one control op and reply on its channel. A dropped reply receiver is
961    /// harmless (the requester went away).
962    pub fn handle(&mut self, op: ControlOp) {
963        match op {
964            ControlOp::Spawn { args, reply } => {
965                let result = match self.spawner.as_mut() {
966                    // Spawning runs outside the pipeline schedule, so it isn't
967                    // covered by `run_isolated`'s panic guard: a panic while
968                    // parsing a blueprint or building a sandbox would otherwise
969                    // unwind the whole serve task and take the daemon with it.
970                    // As with `run_isolated`, the world may be left holding a
971                    // partially-built entity - the run just never registers.
972                    Some(spawner) => {
973                        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
974                            spawner(&mut self.world, &args)
975                        })) {
976                            Ok(Ok(entity)) => {
977                                self.by_run_id.insert(args.run_id.clone(), entity);
978                                Ok(args.run_id.clone())
979                            }
980                            Ok(Err(e)) => Err(e),
981                            Err(_) => Err("agent spawn panicked".to_string()),
982                        }
983                    }
984                    None => Err("this daemon cannot spawn agents".to_string()),
985                };
986                // A failed spawn must leave a trace daemon-side: the error goes
987                // back over the socket to a client that may have already exited,
988                // and nothing is written to disk, so without this log line the
989                // failure is invisible (issue #107).
990                if let Err(error) = &result {
991                    tracing::error!(
992                        run_id = %args.run_id,
993                        blueprint = %args.blueprint_path,
994                        workdir = %args.workdir,
995                        error = %error,
996                        "agent spawn failed"
997                    );
998                }
999                let _ = reply.send(result);
1000            }
1001            ControlOp::Status { run_id, reply } => {
1002                let status = self
1003                    .live_entity(&run_id)
1004                    .and_then(|e| self.world.agent_status(e));
1005                let _ = reply.send(status);
1006            }
1007            ControlOp::Pause { run_id, reply } => {
1008                let ok = self
1009                    .resolve_or_reload(&run_id)
1010                    .is_some_and(|e| self.world.pause(e));
1011                let _ = reply.send(ok);
1012            }
1013            ControlOp::Resume { run_id, reply } => {
1014                let ok = self
1015                    .resolve_or_reload(&run_id)
1016                    .is_some_and(|e| self.world.resume(e));
1017                let _ = reply.send(ok);
1018            }
1019            ControlOp::Cancel { run_id, reply } => {
1020                // Cancel is unconditional: it either takes effect in the world
1021                // (root plus every descendant) or, when the run can't be held
1022                // there at all, is forced onto its on-disk state. It reports
1023                // `false` only when there is genuinely no such run anywhere -
1024                // otherwise a run whose blueprint had moved stayed `running` on
1025                // disk forever with no way to get rid of it.
1026                let ok = self.cancel_tree(&run_id)
1027                    || self
1028                        .force_terminator
1029                        .as_mut()
1030                        .is_some_and(|terminate| terminate(&run_id));
1031                let _ = reply.send(ok);
1032            }
1033            ControlOp::List { reply } => {
1034                let _ = reply.send(self.list());
1035            }
1036            ControlOp::Message {
1037                agent_id,
1038                content,
1039                target_region,
1040                reply,
1041            } => {
1042                // Page the target in if it was unloaded, so delivery finds it.
1043                self.resolve_or_reload(&agent_id);
1044                let ok = self
1045                    .world
1046                    .send_message(AgentMessage {
1047                        agent_id,
1048                        content,
1049                        target_region,
1050                        priority: 0,
1051                    })
1052                    .is_ok();
1053                let _ = reply.send(ok);
1054            }
1055            ControlOp::ListInteractions { reply } => {
1056                let _ = reply.send(self.interactions.pending());
1057            }
1058            ControlOp::AnswerInteraction { response, reply } => {
1059                let _ = reply.send(self.interactions.answer(response));
1060            }
1061            ControlOp::CancelInteraction { request_id, reply } => {
1062                let _ = reply.send(self.interactions.cancel(&request_id));
1063            }
1064            ControlOp::Shutdown { reply } => {
1065                // Reply first (best effort), then trigger the world's shutdown so
1066                // the serve loop's next `select!` returns.
1067                let _ = reply.send(true);
1068                self.world.shutdown();
1069            }
1070        }
1071    }
1072
1073    /// Flush all queued persistence and stop the hosted world, guaranteeing every
1074    /// dirty agent's final snapshot reaches disk (see
1075    /// [`PipelineWorld::flush_and_stop`]). Invoked automatically when [`Self::serve`]
1076    /// returns; also exposed directly for callers that drive the world themselves.
1077    pub async fn flush_and_stop(&mut self) {
1078        self.world.flush_and_stop().await;
1079    }
1080
1081    /// Run the host: drive the world to quiescence, then park until an async
1082    /// result wakes it, a control op arrives, or shutdown is signalled. Returns
1083    /// when shutdown fires or the control channel closes - and before returning,
1084    /// **flushes all queued persistence to disk** ([`Self::flush_and_stop`]) so a
1085    /// clean daemon shutdown never loses a dirty agent's final snapshot.
1086    pub async fn serve(&mut self, mut control_rx: UnboundedReceiver<ControlOp>) {
1087        let wake = self.world.wake_handle();
1088        let shutdown = self.world.shutdown_handle();
1089        'serve: loop {
1090            self.world.run_to_fixed_point();
1091            self.emit_events();
1092            tokio::select! {
1093                _ = wake.notified() => {}
1094                _ = shutdown.notified() => break 'serve,
1095                op = control_rx.recv() => {
1096                    match op {
1097                        // Await the spawn preprocessor (e.g. lazy MCP connect) before
1098                        // the sync spawner runs, so the pool is warm. The returned
1099                        // future is `'static`, so no borrow of `self`/`op` outlives it.
1100                        Some(op) => {
1101                            let pre = match &op {
1102                                ControlOp::Spawn { args, .. } => {
1103                                    self.spawn_preprocessor.as_ref().map(|pp| pp(args))
1104                                }
1105                                _ => None,
1106                            };
1107                            if let Some(fut) = pre {
1108                                fut.await;
1109                            }
1110                            self.handle(op);
1111                        }
1112                        None => break 'serve, // all control senders dropped
1113                    }
1114                }
1115                // The host holds a `subagent_tx`, so this only yields `Some`.
1116                Some(sub) = self.subagent_rx.recv() => {
1117                    // Warm a spawning sub-agent's MCP servers first, same as a
1118                    // top-level Spawn (both run in this async loop).
1119                    let pre = match &sub {
1120                        SubAgentOp::Spawn { args, .. } => {
1121                            self.spawn_preprocessor.as_ref().map(|pp| pp(args))
1122                        }
1123                        _ => None,
1124                    };
1125                    if let Some(fut) = pre {
1126                        fut.await;
1127                    }
1128                    self.handle_subagent(sub);
1129                }
1130            }
1131        }
1132        // Shutting down: drain the persistence lane before the world is dropped.
1133        self.flush_and_stop().await;
1134    }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139    use super::*;
1140    use crate::dynamic_interaction::InteractionBackend;
1141    use crate::inference_pool::InferencePoolConfig;
1142    use crate::pipeline::{
1143        AgentBlueprint, ReadyToInfer, StageCursor, StageInference, StageInferences, StageProgress,
1144        StageSetup, StageSetups, ToolService, VisitCounts, WaitingForChildren,
1145    };
1146    use crate::tool_bridge::BoxedToolExec;
1147    use leviath_core::{Region, RegionKind};
1148    use leviath_providers::{
1149        FinishReason, InferenceRequest, InferenceResponse, ModelCapabilities, Provider,
1150        ProviderError, TokenUsage,
1151    };
1152    use std::sync::Arc;
1153    use std::sync::Mutex;
1154    use tokio::runtime::Handle;
1155    use tokio::sync::mpsc;
1156
1157    struct Script {
1158        responses: Mutex<std::collections::VecDeque<InferenceResponse>>,
1159    }
1160    #[async_trait::async_trait]
1161    impl Provider for Script {
1162        async fn infer(
1163            &self,
1164            _req: InferenceRequest,
1165        ) -> leviath_providers::Result<InferenceResponse> {
1166            self.responses
1167                .lock()
1168                .unwrap()
1169                .pop_front()
1170                .ok_or_else(|| ProviderError::Other("exhausted".to_string()))
1171        }
1172        async fn count_tokens(&self, _t: &str, _m: &str) -> usize {
1173            1
1174        }
1175        fn max_context_tokens(&self, _m: &str) -> usize {
1176            100_000
1177        }
1178        fn name(&self) -> &str {
1179            "script"
1180        }
1181        fn capabilities(&self, _m: &str) -> ModelCapabilities {
1182            ModelCapabilities::default()
1183        }
1184    }
1185
1186    struct NoTools;
1187    impl ToolService for NoTools {
1188        fn exec_for(&self, _e: Entity, calls: Vec<leviath_providers::ToolCall>) -> BoxedToolExec {
1189            Box::new(move || {
1190                Box::pin(async move { calls.into_iter().map(|c| (c.id, String::new())).collect() })
1191            })
1192        }
1193    }
1194
1195    fn text(content: &str) -> InferenceResponse {
1196        InferenceResponse {
1197            content: content.to_string(),
1198            tool_calls: vec![],
1199            tokens_used: TokenUsage {
1200                prompt_tokens: 1,
1201                completion_tokens: 1,
1202                total_tokens: 2,
1203                cached_tokens: 0,
1204                cache_write_tokens: 0,
1205            },
1206            finish_reason: FinishReason::Complete,
1207        }
1208    }
1209
1210    fn host_with(responses: Vec<InferenceResponse>) -> WorldHost {
1211        let mut registry = crate::providers::ProviderRegistry::new();
1212        registry.register(
1213            "script".to_string(),
1214            Arc::new(Script {
1215                responses: Mutex::new(responses.into_iter().collect()),
1216            }),
1217        );
1218        let world = PipelineWorld::new(
1219            registry,
1220            Arc::new(NoTools),
1221            InferencePoolConfig::new(),
1222            1,
1223            std::env::temp_dir(),
1224            Handle::current(),
1225        );
1226        WorldHost::new(world)
1227    }
1228
1229    fn blueprint() -> leviath_core::Blueprint {
1230        let layout = leviath_core::layout::ContextLayout::new(
1231            vec![leviath_core::layout::RegionDefinition::new(
1232                "conversation".to_string(),
1233                RegionKind::Clearable,
1234                10_000,
1235            )],
1236            12_000,
1237        );
1238        let s = leviath_core::Stage::new(
1239            "s".to_string(),
1240            leviath_core::blueprint::ModelConfig::new("script".to_string(), "m".to_string()),
1241        );
1242        leviath_core::Blueprint::new("t".to_string(), "d".to_string(), vec![s], layout)
1243    }
1244
1245    fn window() -> crate::components::ContextWindow {
1246        let mut w = crate::components::ContextWindow::new(10_000);
1247        w.add_region(Region::new(
1248            "conversation".to_string(),
1249            RegionKind::Clearable,
1250            10_000,
1251        ));
1252        w
1253    }
1254
1255    fn agent_state(agent_id: &str) -> AgentState {
1256        AgentState {
1257            agent_id: agent_id.to_string(),
1258            current_stage: "s".to_string(),
1259            iteration: 0,
1260            status: AgentStatus::Active,
1261            spawned_children_ids: vec![],
1262            pending_wait: None,
1263            accepts_messages: true,
1264        }
1265    }
1266
1267    fn si() -> StageInference {
1268        StageInference {
1269            provider_name: "script".to_string(),
1270            model: "m".to_string(),
1271            tools: vec![],
1272            tool_filter: None,
1273        }
1274    }
1275
1276    fn setup() -> StageSetup {
1277        StageSetup {
1278            inference_config: crate::components::InferenceConfig {
1279                temperature: None,
1280                max_output_tokens: None,
1281                extra_params: Default::default(),
1282                batch_tool_hint: false,
1283                request_timeout_secs: None,
1284            },
1285            routing: None,
1286            accepts_messages: true,
1287            context_layout: None,
1288            system_prompt: None,
1289        }
1290    }
1291
1292    /// Spawn a simple agent into the host and register it under `run_id`.
1293    fn spawn(host: &mut WorldHost, run_id: &str, agent_id: &str) -> Entity {
1294        let e = host.world_mut().spawn_agent((
1295            AgentBlueprint(blueprint()),
1296            StageCursor { index: 0 },
1297            agent_state(agent_id),
1298            crate::components::MessageInbox::default(),
1299            StageProgress::default(),
1300            StageInferences(vec![si()]),
1301            StageSetups(vec![setup()]),
1302            VisitCounts::default(),
1303            window(),
1304            si(),
1305            setup().inference_config,
1306            ReadyToInfer,
1307        ));
1308        host.register(run_id, e);
1309        e
1310    }
1311
1312    /// A [`ForceTerminator`] that records each run id it was asked to terminate
1313    /// and reports success for everything but `"never-existed"`. Shared by the
1314    /// tests that expect it to fire and the ones that expect it not to, so its
1315    /// body is exercised rather than existing only to go unused.
1316    fn recording_terminator(seen: Arc<Mutex<Vec<String>>>) -> ForceTerminator {
1317        Box::new(move |run_id| {
1318            seen.lock().unwrap().push(run_id.to_string());
1319            run_id != "never-existed"
1320        })
1321    }
1322
1323    /// A [`Reloader`] that pages any run id in as a fresh agent.
1324    fn paging_reloader() -> Reloader {
1325        Box::new(|world, run_id| Some(world.spawn_agent((agent_state(run_id),))))
1326    }
1327
1328    async fn ask<T>(host: &mut WorldHost, make: impl FnOnce(oneshot::Sender<T>) -> ControlOp) -> T {
1329        let (tx, rx) = oneshot::channel();
1330        host.handle(make(tx));
1331        rx.await.unwrap()
1332    }
1333
1334    #[tokio::test]
1335    async fn status_and_list_reflect_registered_runs() {
1336        let mut host = host_with(vec![]);
1337        spawn(&mut host, "run-a", "agent-a");
1338
1339        let status = ask(&mut host, |reply| ControlOp::Status {
1340            run_id: "run-a".to_string(),
1341            reply,
1342        })
1343        .await;
1344        assert_eq!(status, Some(AgentStatus::Active));
1345
1346        let list = ask(&mut host, |reply| ControlOp::List { reply }).await;
1347        assert_eq!(list, vec![("run-a".to_string(), AgentStatus::Active)]);
1348
1349        // Unknown run.
1350        let none = ask(&mut host, |reply| ControlOp::Status {
1351            run_id: "ghost".to_string(),
1352            reply,
1353        })
1354        .await;
1355        assert_eq!(none, None);
1356    }
1357
1358    #[tokio::test]
1359    async fn pause_resume_cancel_by_run_id() {
1360        let mut host = host_with(vec![]);
1361        spawn(&mut host, "run-a", "agent-a");
1362
1363        assert!(
1364            ask(&mut host, |reply| ControlOp::Pause {
1365                run_id: "run-a".to_string(),
1366                reply
1367            })
1368            .await
1369        );
1370        assert_eq!(
1371            host.world.agent_status(host.by_run_id["run-a"]),
1372            Some(AgentStatus::Idle)
1373        );
1374
1375        assert!(
1376            ask(&mut host, |reply| ControlOp::Resume {
1377                run_id: "run-a".to_string(),
1378                reply
1379            })
1380            .await
1381        );
1382        assert!(
1383            ask(&mut host, |reply| ControlOp::Cancel {
1384                run_id: "run-a".to_string(),
1385                reply
1386            })
1387            .await
1388        );
1389        assert_eq!(
1390            host.world.agent_status(host.by_run_id["run-a"]),
1391            Some(AgentStatus::Cancelled)
1392        );
1393
1394        // Unknown run ⇒ false.
1395        assert!(
1396            !ask(&mut host, |reply| ControlOp::Pause {
1397                run_id: "ghost".to_string(),
1398                reply
1399            })
1400            .await
1401        );
1402        assert!(
1403            !ask(&mut host, |reply| ControlOp::Resume {
1404                run_id: "ghost".to_string(),
1405                reply
1406            })
1407            .await
1408        );
1409        assert!(
1410            !ask(&mut host, |reply| ControlOp::Cancel {
1411                run_id: "ghost".to_string(),
1412                reply
1413            })
1414            .await
1415        );
1416    }
1417
1418    #[tokio::test]
1419    async fn spawn_op_uses_installed_spawner_and_registers() {
1420        let mut host = host_with(vec![]);
1421        host.set_spawner(Box::new(|world, args| {
1422            Ok(world.spawn_agent((agent_state(&args.run_id),)))
1423        }));
1424
1425        let result = ask(&mut host, |reply| ControlOp::Spawn {
1426            args: Box::new(SpawnArgs {
1427                run_id: "r1".to_string(),
1428                ..Default::default()
1429            }),
1430            reply,
1431        })
1432        .await;
1433        assert_eq!(result, Ok("r1".to_string()));
1434
1435        // The run is now registered, so Status resolves it.
1436        let status = ask(&mut host, |reply| ControlOp::Status {
1437            run_id: "r1".to_string(),
1438            reply,
1439        })
1440        .await;
1441        assert_eq!(status, Some(AgentStatus::Active));
1442    }
1443
1444    #[tokio::test]
1445    async fn spawn_op_propagates_spawner_error() {
1446        let mut host = host_with(vec![]);
1447        host.set_spawner(Box::new(|_world, _args| Err("bad blueprint".to_string())));
1448        let result = ask(&mut host, |reply| ControlOp::Spawn {
1449            args: Box::new(SpawnArgs::default()),
1450            reply,
1451        })
1452        .await;
1453        assert_eq!(result, Err("bad blueprint".to_string()));
1454    }
1455
1456    #[tokio::test]
1457    async fn spawn_op_contains_a_panicking_spawner() {
1458        // A panic while building an agent (bad manifest, sandbox blow-up) must
1459        // not unwind the daemon's serve task - the run just fails to start.
1460        let mut host = host_with(vec![]);
1461        host.set_spawner(Box::new(|_world, _args| panic!("simulated spawn panic")));
1462        let (tx, rx) = oneshot::channel();
1463        crate::test_support::with_silenced_panics(|| {
1464            host.handle(ControlOp::Spawn {
1465                args: Box::new(SpawnArgs::default()),
1466                reply: tx,
1467            });
1468        });
1469        assert_eq!(rx.await.unwrap(), Err("agent spawn panicked".to_string()));
1470        // The host is still usable afterwards, and the run never registered.
1471        let status = ask(&mut host, |reply| ControlOp::Status {
1472            run_id: SpawnArgs::default().run_id,
1473            reply,
1474        })
1475        .await;
1476        assert!(status.is_none());
1477    }
1478
1479    #[tokio::test]
1480    async fn spawn_op_errors_without_a_spawner() {
1481        let mut host = host_with(vec![]);
1482        let result = ask(&mut host, |reply| ControlOp::Spawn {
1483            args: Box::new(SpawnArgs::default()),
1484            reply,
1485        })
1486        .await;
1487        assert!(result.unwrap_err().contains("cannot spawn"));
1488    }
1489
1490    // ─── sub-agent bridge ──────────────────────────────────────────────────
1491
1492    async fn ask_sub<T>(
1493        host: &mut WorldHost,
1494        make: impl FnOnce(oneshot::Sender<T>) -> SubAgentOp,
1495    ) -> T {
1496        let (tx, rx) = oneshot::channel();
1497        host.handle_subagent(make(tx));
1498        rx.await.unwrap()
1499    }
1500
1501    /// A spawner that adds a bare child agent and returns it.
1502    fn child_spawner() -> Spawner {
1503        Box::new(|world, args| Ok(world.spawn_agent((agent_state(&args.run_id),))))
1504    }
1505
1506    #[tokio::test]
1507    async fn subagent_spawn_links_child_and_registers() {
1508        let mut host = host_with(vec![]);
1509        host.set_spawner(child_spawner());
1510        let parent = spawn(&mut host, "parent", "parent");
1511
1512        let result = ask_sub(&mut host, |reply| SubAgentOp::Spawn {
1513            args: Box::new(SpawnArgs {
1514                run_id: "child".to_string(),
1515                ..Default::default()
1516            }),
1517            parent_run_id: "parent".to_string(),
1518            max_depth: 3,
1519            reply,
1520        })
1521        .await;
1522        assert_eq!(result, Ok("child".to_string()));
1523
1524        let child = host.by_run_id["child"];
1525        // The child links back to the parent at depth 1.
1526        let pref = host.world.world().get::<ParentRef>(child).unwrap();
1527        assert_eq!(pref.parent_entity, parent);
1528        assert_eq!(pref.depth, 1);
1529        // The parent tracks the child.
1530        let kids = host.world.world().get::<SubAgentChildren>(parent).unwrap();
1531        assert_eq!(kids.children, vec![child]);
1532    }
1533
1534    #[tokio::test]
1535    async fn subagent_spawn_appends_to_existing_children() {
1536        let mut host = host_with(vec![]);
1537        host.set_spawner(child_spawner());
1538        spawn(&mut host, "parent", "parent");
1539        for id in ["c1", "c2"] {
1540            let r = ask_sub(&mut host, |reply| SubAgentOp::Spawn {
1541                args: Box::new(SpawnArgs {
1542                    run_id: id.to_string(),
1543                    ..Default::default()
1544                }),
1545                parent_run_id: "parent".to_string(),
1546                max_depth: 3,
1547                reply,
1548            })
1549            .await;
1550            assert!(r.is_ok());
1551        }
1552        let parent = host.by_run_id["parent"];
1553        let kids = host.world.world().get::<SubAgentChildren>(parent).unwrap();
1554        assert_eq!(kids.children.len(), 2);
1555    }
1556
1557    #[tokio::test]
1558    async fn subagent_spawn_rejects_beyond_max_depth() {
1559        let mut host = host_with(vec![]);
1560        host.set_spawner(child_spawner());
1561        spawn(&mut host, "parent", "parent");
1562        let result = ask_sub(&mut host, |reply| SubAgentOp::Spawn {
1563            args: Box::new(SpawnArgs {
1564                run_id: "child".to_string(),
1565                ..Default::default()
1566            }),
1567            parent_run_id: "parent".to_string(),
1568            max_depth: 0, // child would be depth 1 > 0
1569            reply,
1570        })
1571        .await;
1572        assert!(result.unwrap_err().contains("depth limit"));
1573        assert!(!host.by_run_id.contains_key("child"));
1574    }
1575
1576    #[tokio::test]
1577    async fn subagent_spawn_unknown_parent_and_no_spawner_and_spawner_error() {
1578        // Unknown parent.
1579        let mut host = host_with(vec![]);
1580        host.set_spawner(child_spawner());
1581        let r = ask_sub(&mut host, |reply| SubAgentOp::Spawn {
1582            args: Box::new(SpawnArgs::default()),
1583            parent_run_id: "ghost".to_string(),
1584            max_depth: 3,
1585            reply,
1586        })
1587        .await;
1588        assert!(r.unwrap_err().contains("not live"));
1589
1590        // No spawner installed.
1591        let mut host2 = host_with(vec![]);
1592        spawn(&mut host2, "parent", "parent");
1593        let r = ask_sub(&mut host2, |reply| SubAgentOp::Spawn {
1594            args: Box::new(SpawnArgs::default()),
1595            parent_run_id: "parent".to_string(),
1596            max_depth: 3,
1597            reply,
1598        })
1599        .await;
1600        assert!(r.unwrap_err().contains("cannot spawn"));
1601
1602        // Spawner rejects.
1603        let mut host3 = host_with(vec![]);
1604        host3.set_spawner(Box::new(|_w, _a| Err("bad blueprint".to_string())));
1605        spawn(&mut host3, "parent", "parent");
1606        let r = ask_sub(&mut host3, |reply| SubAgentOp::Spawn {
1607            args: Box::new(SpawnArgs::default()),
1608            parent_run_id: "parent".to_string(),
1609            max_depth: 3,
1610            reply,
1611        })
1612        .await;
1613        assert_eq!(r, Err("bad blueprint".to_string()));
1614    }
1615
1616    #[tokio::test]
1617    async fn subagent_check_reports_status_or_none() {
1618        let mut host = host_with(vec![]);
1619        spawn(&mut host, "run-a", "run-a");
1620        let status = ask_sub(&mut host, |reply| SubAgentOp::Check {
1621            run_id: "run-a".to_string(),
1622            reply,
1623        })
1624        .await;
1625        assert_eq!(status, Some(AgentStatus::Active));
1626
1627        let none = ask_sub(&mut host, |reply| SubAgentOp::Check {
1628            run_id: "ghost".to_string(),
1629            reply,
1630        })
1631        .await;
1632        assert_eq!(none, None);
1633    }
1634
1635    /// `send_to_agent` and `kill_agent` took any run id at all, so an agent
1636    /// could reach into an unrelated run - cancel it, inject text, or hand it
1637    /// data that arrives `Public` regardless of the sender's taint. That last
1638    /// one is a laundering channel straight through taint tracking.
1639    /// The converse of the refusal: a run the caller *did* spawn is reachable,
1640    /// so scoping did not simply block everything. This also walks the
1641    /// `SubAgentChildren` link rather than matching the caller itself.
1642    #[tokio::test]
1643    async fn subagent_ops_reach_a_run_the_caller_spawned() {
1644        let mut host = host_with(vec![]);
1645        let parent = spawn(&mut host, "parent", "parent");
1646        let child = spawn(&mut host, "child", "child");
1647        host.world_mut()
1648            .world_mut()
1649            .entity_mut(parent)
1650            .insert(SubAgentChildren {
1651                children: vec![child],
1652                max_child_depth: 3,
1653            });
1654
1655        let delivered = ask_sub(&mut host, |reply| SubAgentOp::Send {
1656            run_id: "child".to_string(),
1657            caller_run_id: "parent".to_string(),
1658            content: "carry on".to_string(),
1659            reply,
1660        })
1661        .await;
1662        assert!(delivered, "a run we spawned is ours to message");
1663    }
1664
1665    #[tokio::test]
1666    async fn subagent_ops_refuse_a_run_outside_the_callers_tree() {
1667        let mut host = host_with(vec![]);
1668        spawn(&mut host, "run-a", "run-a");
1669        spawn(&mut host, "outsider", "outsider");
1670
1671        let delivered = ask_sub(&mut host, |reply| SubAgentOp::Send {
1672            run_id: "outsider".to_string(),
1673            caller_run_id: "run-a".to_string(),
1674            content: "take this".to_string(),
1675            reply,
1676        })
1677        .await;
1678        assert!(!delivered, "a run we did not spawn is not ours to message");
1679
1680        let killed = ask_sub(&mut host, |reply| SubAgentOp::Kill {
1681            run_id: "outsider".to_string(),
1682            caller_run_id: "run-a".to_string(),
1683            reply,
1684        })
1685        .await;
1686        assert!(!killed, "nor ours to cancel");
1687
1688        // A run id that resolves to nothing at all is likewise not ours - the
1689        // walk never starts, rather than defaulting to reachable.
1690        let phantom = ask_sub(&mut host, |reply| SubAgentOp::Send {
1691            run_id: "no-such-run".to_string(),
1692            caller_run_id: "run-a".to_string(),
1693            content: "hello?".to_string(),
1694            reply,
1695        })
1696        .await;
1697        assert!(!phantom, "an unknown run id is in nobody's tree");
1698    }
1699
1700    #[tokio::test]
1701    async fn subagent_send_delivers_to_inbox() {
1702        let mut host = host_with(vec![]);
1703        spawn(&mut host, "run-a", "run-a");
1704        let ok = ask_sub(&mut host, |reply| SubAgentOp::Send {
1705            run_id: "run-a".to_string(),
1706            caller_run_id: "run-a".to_string(),
1707            content: "hello child".to_string(),
1708            reply,
1709        })
1710        .await;
1711        assert!(ok);
1712    }
1713
1714    #[tokio::test]
1715    async fn subagent_kill_cancels_the_whole_tree() {
1716        let mut host = host_with(vec![]);
1717        host.set_spawner(child_spawner());
1718        spawn(&mut host, "parent", "parent");
1719        ask_sub(&mut host, |reply| SubAgentOp::Spawn {
1720            args: Box::new(SpawnArgs {
1721                run_id: "child".to_string(),
1722                ..Default::default()
1723            }),
1724            parent_run_id: "parent".to_string(),
1725            max_depth: 3,
1726            reply,
1727        })
1728        .await
1729        .unwrap();
1730
1731        let ok = ask_sub(&mut host, |reply| SubAgentOp::Kill {
1732            run_id: "parent".to_string(),
1733            caller_run_id: "parent".to_string(),
1734            reply,
1735        })
1736        .await;
1737        assert!(ok);
1738        assert_eq!(
1739            host.world.agent_status(host.by_run_id["parent"]),
1740            Some(AgentStatus::Cancelled)
1741        );
1742        assert_eq!(
1743            host.world.agent_status(host.by_run_id["child"]),
1744            Some(AgentStatus::Cancelled)
1745        );
1746
1747        // Killing an unknown run is a no-op.
1748        let miss = ask_sub(&mut host, |reply| SubAgentOp::Kill {
1749            run_id: "ghost".to_string(),
1750            caller_run_id: "ghost".to_string(),
1751            reply,
1752        })
1753        .await;
1754        assert!(!miss);
1755    }
1756
1757    /// A user-facing cancel must reach the sub-agent tree, not just the root -
1758    /// otherwise the children keep running with nobody to report to. Before this,
1759    /// only the model-facing `kill_agent` tool cascaded.
1760    #[tokio::test]
1761    async fn cancel_cascades_to_the_whole_tree() {
1762        let mut host = host_with(vec![]);
1763        host.set_spawner(child_spawner());
1764        spawn(&mut host, "parent", "parent");
1765        ask_sub(&mut host, |reply| SubAgentOp::Spawn {
1766            args: Box::new(SpawnArgs {
1767                run_id: "child".to_string(),
1768                ..Default::default()
1769            }),
1770            parent_run_id: "parent".to_string(),
1771            max_depth: 3,
1772            reply,
1773        })
1774        .await
1775        .unwrap();
1776
1777        assert!(
1778            ask(&mut host, |reply| ControlOp::Cancel {
1779                run_id: "parent".to_string(),
1780                reply
1781            })
1782            .await
1783        );
1784        assert_eq!(
1785            host.world.agent_status(host.by_run_id["child"]),
1786            Some(AgentStatus::Cancelled),
1787            "cancelling the parent cancels its children"
1788        );
1789    }
1790
1791    /// A child that was already reaped is skipped rather than tripping the
1792    /// cancel: `SubAgentChildren` still names it, but the entity is gone, so
1793    /// there is no agent id to close interactions for.
1794    #[tokio::test]
1795    async fn cancel_tolerates_a_child_that_has_already_been_reaped() {
1796        let mut host = host_with(vec![]);
1797        let parent = spawn(&mut host, "parent", "parent");
1798        let ghost = host.world_mut().spawn_agent((agent_state("ghost"),));
1799        host.world_mut()
1800            .world_mut()
1801            .entity_mut(parent)
1802            .insert(SubAgentChildren {
1803                children: vec![ghost],
1804                max_child_depth: 3,
1805            });
1806        host.world_mut().world_mut().despawn(ghost);
1807
1808        assert!(
1809            ask(&mut host, |reply| ControlOp::Cancel {
1810                run_id: "parent".to_string(),
1811                reply
1812            })
1813            .await,
1814            "the parent is still cancelled"
1815        );
1816        assert_eq!(
1817            host.world.agent_status(parent),
1818            Some(AgentStatus::Cancelled)
1819        );
1820    }
1821
1822    /// Cancelling a run closes its open prompts. The blocked `ask` occupies a
1823    /// tool-lane worker, and the lane has a fixed worker count - leaving it
1824    /// parked forever is what starves every other agent's tool batches.
1825    #[tokio::test]
1826    async fn cancel_closes_the_runs_open_interactions() {
1827        let mut host = host_with(vec![]);
1828        let hub = host.interactions();
1829        spawn(&mut host, "run-a", "agent-a");
1830
1831        let backend = hub.backend_for("agent-a");
1832        let asking = tokio::spawn(async move {
1833            backend
1834                .ask(InteractionRequest::free_text("q", "ask", "stage", true))
1835                .await
1836        });
1837        // Wait for the ask to register, then let the host emit it - so the
1838        // emitted-interaction set is non-empty and the cancel has something to
1839        // prune, rather than pruning an empty set.
1840        while hub.pending().is_empty() {
1841            tokio::task::yield_now().await;
1842        }
1843        host.emit_events();
1844        assert!(
1845            !host.emitted_interactions.is_empty(),
1846            "the open request was emitted"
1847        );
1848
1849        ask(&mut host, |reply| ControlOp::Cancel {
1850            run_id: "run-a".to_string(),
1851            reply,
1852        })
1853        .await;
1854
1855        // The blocked future is released rather than parked forever. Bounded,
1856        // because the regression this guards *is* an unbounded wait: without the
1857        // per-agent cancel this await simply never returns, and a test that hangs
1858        // rather than fails is worse than no test.
1859        tokio::time::timeout(std::time::Duration::from_secs(5), asking)
1860            .await
1861            .expect("cancelling the run releases its blocked ask")
1862            .expect("the ask task did not panic");
1863        // ...and the request stops being advertised to `lev respond` / the
1864        // dashboard for a run that is going away.
1865        assert!(hub.pending().is_empty(), "no orphaned prompt is left open");
1866        assert!(
1867            host.emitted_interactions.is_empty(),
1868            "and it is pruned from the emitted set, not re-announced forever"
1869        );
1870    }
1871
1872    /// The floor under every kill: a run the reloader can't rebuild must still be
1873    /// terminated, via the daemon's on-disk force-terminator. Replying `false` and
1874    /// writing nothing is what made such a run permanent.
1875    #[tokio::test]
1876    async fn cancel_falls_back_to_the_force_terminator_when_the_world_cannot_hold_the_run() {
1877        let mut host = host_with(vec![]);
1878        // A reloader that always declines - the deleted-blueprint case.
1879        host.set_reloader(Box::new(|_world, _run_id| None));
1880        let terminated = Arc::new(Mutex::new(Vec::new()));
1881        host.set_force_terminator(recording_terminator(terminated.clone()));
1882
1883        assert!(
1884            ask(&mut host, |reply| ControlOp::Cancel {
1885                run_id: "unreloadable".to_string(),
1886                reply
1887            })
1888            .await,
1889            "a run that can't be reloaded is still terminated"
1890        );
1891        assert!(
1892            !ask(&mut host, |reply| ControlOp::Cancel {
1893                run_id: "never-existed".to_string(),
1894                reply
1895            })
1896            .await,
1897            "`false` is reserved for a run that exists nowhere"
1898        );
1899        assert_eq!(
1900            *terminated.lock().unwrap(),
1901            vec!["unreloadable".to_string(), "never-existed".to_string()]
1902        );
1903    }
1904
1905    /// A live run is cancelled in the world; the on-disk fallback is not consulted
1906    /// (the persistence lane records the status change).
1907    #[tokio::test]
1908    async fn cancel_does_not_force_terminate_a_run_it_could_cancel() {
1909        let mut host = host_with(vec![]);
1910        spawn(&mut host, "run-a", "agent-a");
1911        let terminated = Arc::new(Mutex::new(Vec::new()));
1912        host.set_force_terminator(recording_terminator(terminated.clone()));
1913
1914        assert!(
1915            ask(&mut host, |reply| ControlOp::Cancel {
1916                run_id: "run-a".to_string(),
1917                reply
1918            })
1919            .await
1920        );
1921        assert_eq!(
1922            host.world.agent_status(host.by_run_id["run-a"]),
1923            Some(AgentStatus::Cancelled)
1924        );
1925        assert!(
1926            terminated.lock().unwrap().is_empty(),
1927            "the disk fallback stayed unused"
1928        );
1929    }
1930
1931    /// Agents that enter the world outside a `Spawn` op (fan-out workers, built
1932    /// directly by the fan-out spawner) are adopted into the run-id map, so they
1933    /// are listed, reaped and - the point here - cancellable by id. Left
1934    /// unregistered, a cancel missed the map and paged a *second* copy of the run
1935    /// in from disk while the original kept going.
1936    #[tokio::test]
1937    async fn unregistered_world_agents_are_adopted_and_become_cancellable() {
1938        let mut host = host_with(vec![]);
1939        let entity = host.world_mut().spawn_agent((
1940            agent_state("worker"),
1941            RunMetadata {
1942                run_id: "worker-run".to_string(),
1943                agent_name: "w".to_string(),
1944                agent_path: String::new(),
1945                task: String::new(),
1946                model: None,
1947                workdir: String::new(),
1948                num_stages: 1,
1949                started_at: 0,
1950                parent_run_id: None,
1951                metadata: Default::default(),
1952                callback_url: None,
1953                callback_secret: None,
1954                title: None,
1955            },
1956        ));
1957        assert!(
1958            !host.by_run_id.contains_key("worker-run"),
1959            "not registered by the spawn itself"
1960        );
1961
1962        host.emit_events();
1963
1964        assert_eq!(host.live_entity("worker-run"), Some(entity), "adopted");
1965        // A reloader that would mint a duplicate if the map were still missing it.
1966        host.set_reloader(paging_reloader());
1967        assert!(
1968            ask(&mut host, |reply| ControlOp::Cancel {
1969                run_id: "worker-run".to_string(),
1970                reply
1971            })
1972            .await
1973        );
1974        assert_eq!(
1975            host.world.agent_status(entity),
1976            Some(AgentStatus::Cancelled),
1977            "the original entity is cancelled, not a reloaded copy"
1978        );
1979    }
1980
1981    #[tokio::test]
1982    async fn interaction_ops_list_answer_and_cancel() {
1983        let mut host = host_with(vec![]);
1984        let hub = host.interactions();
1985        let backend = hub.backend_for("agent-a");
1986
1987        // An agent's ask is registered on the hub.
1988        let asking = tokio::spawn(async move {
1989            backend
1990                .ask(leviath_core::interaction::InteractionRequest::free_text(
1991                    "q1", "prompt?", "stage", true,
1992                ))
1993                .await
1994        });
1995        for _ in 0..8 {
1996            tokio::task::yield_now().await;
1997        }
1998
1999        // ListInteractions surfaces it.
2000        let list = ask(&mut host, |reply| ControlOp::ListInteractions { reply }).await;
2001        assert_eq!(list.len(), 1);
2002        assert_eq!(list[0].0, "agent-a");
2003
2004        // AnswerInteraction fulfils it.
2005        let ok = ask(&mut host, |reply| ControlOp::AnswerInteraction {
2006            response: leviath_core::interaction::InteractionResponse::text("q1", "hi"),
2007            reply,
2008        })
2009        .await;
2010        assert!(ok);
2011        assert_eq!(asking.await.unwrap().value.as_deref(), Some("hi"));
2012
2013        // CancelInteraction on an unknown id ⇒ false.
2014        let cancelled = ask(&mut host, |reply| ControlOp::CancelInteraction {
2015            request_id: "gone".to_string(),
2016            reply,
2017        })
2018        .await;
2019        assert!(!cancelled);
2020    }
2021
2022    #[tokio::test]
2023    async fn cancel_interaction_op_wakes_asker() {
2024        let mut host = host_with(vec![]);
2025        let backend = host.interactions().backend_for("agent-a");
2026        let asking = tokio::spawn(async move {
2027            backend
2028                .ask(leviath_core::interaction::InteractionRequest::free_text(
2029                    "q2", "p", "s", true,
2030                ))
2031                .await
2032        });
2033        for _ in 0..8 {
2034            tokio::task::yield_now().await;
2035        }
2036
2037        let ok = ask(&mut host, |reply| ControlOp::CancelInteraction {
2038            request_id: "q2".to_string(),
2039            reply,
2040        })
2041        .await;
2042        assert!(ok);
2043        assert_eq!(asking.await.unwrap().request_id, "q2");
2044    }
2045
2046    #[tokio::test]
2047    async fn message_op_is_delivered() {
2048        let mut host = host_with(vec![]);
2049        let e = spawn(&mut host, "run-a", "agent-a");
2050
2051        let ok = ask(&mut host, |reply| ControlOp::Message {
2052            agent_id: "agent-a".to_string(),
2053            content: "hi".to_string(),
2054            target_region: Some("conversation".to_string()),
2055            reply,
2056        })
2057        .await;
2058        assert!(ok);
2059
2060        // One tick delivers the message into context.
2061        host.world_mut().tick();
2062        assert!(
2063            host.world
2064                .world()
2065                .get::<crate::components::ContextWindow>(e)
2066                .unwrap()
2067                .get_region("conversation")
2068                .unwrap()
2069                .current_tokens
2070                > 0
2071        );
2072    }
2073
2074    #[tokio::test]
2075    async fn serve_drives_agents_and_handles_ops_until_shutdown() {
2076        let mut host = host_with(vec![text("t1"), text("t2"), text("t3"), text("t4")]);
2077        let e = spawn(&mut host, "run-a", "agent-a");
2078        let shutdown = host.world_mut().shutdown_handle();
2079        let (op_tx, op_rx) = mpsc::unbounded_channel();
2080
2081        let handle = tokio::spawn(async move {
2082            host.serve(op_rx).await;
2083            host
2084        });
2085
2086        // Query status via the live serve loop.
2087        let (tx, rx) = oneshot::channel();
2088        op_tx
2089            .send(ControlOp::Status {
2090                run_id: "run-a".to_string(),
2091                reply: tx,
2092            })
2093            .unwrap();
2094        let _ = rx.await.unwrap();
2095
2096        shutdown.notify_one();
2097        let host = handle.await.unwrap();
2098        // The agent ran to completion under the serve loop.
2099        assert_eq!(host.world.agent_status(e), Some(AgentStatus::Complete));
2100    }
2101
2102    #[tokio::test]
2103    async fn serve_awaits_spawn_preprocessor_before_spawning() {
2104        use std::sync::atomic::{AtomicBool, Ordering};
2105        let mut host = host_with(vec![]);
2106        let ran = Arc::new(AtomicBool::new(false));
2107        let ran_pp = ran.clone();
2108        host.set_spawn_preprocessor(Box::new(move |_args| {
2109            let ran = ran_pp.clone();
2110            Box::pin(async move {
2111                ran.store(true, Ordering::SeqCst);
2112            })
2113        }));
2114        let ran_spawn = ran.clone();
2115        host.set_spawner(Box::new(move |world, args| {
2116            // The preprocessor must have completed before the spawner runs.
2117            assert!(ran_spawn.load(Ordering::SeqCst));
2118            Ok(world.spawn_agent((agent_state(&args.run_id),)))
2119        }));
2120        let (op_tx, op_rx) = mpsc::unbounded_channel();
2121        let handle = tokio::spawn(async move {
2122            host.serve(op_rx).await;
2123        });
2124        let (tx, rx) = oneshot::channel();
2125        op_tx
2126            .send(ControlOp::Spawn {
2127                args: Box::new(SpawnArgs {
2128                    run_id: "rp".to_string(),
2129                    ..Default::default()
2130                }),
2131                reply: tx,
2132            })
2133            .unwrap();
2134        let result = rx.await.unwrap();
2135        drop(op_tx); // close the channel so serve() returns
2136        handle.await.unwrap();
2137        assert_eq!(result, Ok("rp".to_string()));
2138        assert!(ran.load(Ordering::SeqCst), "preprocessor ran");
2139    }
2140
2141    #[tokio::test]
2142    async fn serve_awaits_preprocessor_for_subagent_spawn() {
2143        use std::sync::atomic::{AtomicUsize, Ordering};
2144        let mut host = host_with(vec![]);
2145        host.set_spawner(child_spawner());
2146        let _parent = spawn(&mut host, "parent", "parent");
2147        // Count preprocessor invocations: it must fire for the sub-agent Spawn,
2148        // and NOT for the non-Spawn Check op (the `_ => None` arm).
2149        let calls = Arc::new(AtomicUsize::new(0));
2150        let calls_pp = calls.clone();
2151        host.set_spawn_preprocessor(Box::new(move |_args| {
2152            let calls = calls_pp.clone();
2153            Box::pin(async move {
2154                calls.fetch_add(1, Ordering::SeqCst);
2155            })
2156        }));
2157        let sub_tx = host.subagent_sender();
2158        let shutdown = host.world_mut().shutdown_handle();
2159        let (op_tx, op_rx) = mpsc::unbounded_channel();
2160        let handle = tokio::spawn(async move {
2161            host.serve(op_rx).await;
2162        });
2163
2164        // A non-Spawn sub-agent op does not invoke the preprocessor.
2165        let (ctx, crx) = oneshot::channel();
2166        sub_tx
2167            .send(SubAgentOp::Check {
2168                run_id: "parent".to_string(),
2169                reply: ctx,
2170            })
2171            .unwrap();
2172        let _ = crx.await.unwrap();
2173
2174        // A sub-agent Spawn does.
2175        let (stx, srx) = oneshot::channel();
2176        sub_tx
2177            .send(SubAgentOp::Spawn {
2178                args: Box::new(SpawnArgs {
2179                    run_id: "child".to_string(),
2180                    ..Default::default()
2181                }),
2182                parent_run_id: "parent".to_string(),
2183                max_depth: 3,
2184                reply: stx,
2185            })
2186            .unwrap();
2187        assert_eq!(srx.await.unwrap(), Ok("child".to_string()));
2188
2189        shutdown.notify_one();
2190        drop(op_tx);
2191        handle.await.unwrap();
2192        assert_eq!(
2193            calls.load(Ordering::SeqCst),
2194            1,
2195            "only the Spawn preprocessed"
2196        );
2197    }
2198
2199    #[tokio::test]
2200    async fn serve_spawns_without_a_preprocessor() {
2201        // A Spawn op through serve() with no preprocessor installed exercises the
2202        // `None` arm of the preprocessor branch.
2203        let mut host = host_with(vec![]);
2204        host.set_spawner(Box::new(|world, args| {
2205            Ok(world.spawn_agent((agent_state(&args.run_id),)))
2206        }));
2207        let (op_tx, op_rx) = mpsc::unbounded_channel();
2208        let handle = tokio::spawn(async move {
2209            host.serve(op_rx).await;
2210        });
2211        let (tx, rx) = oneshot::channel();
2212        op_tx
2213            .send(ControlOp::Spawn {
2214                args: Box::new(SpawnArgs {
2215                    run_id: "np".to_string(),
2216                    ..Default::default()
2217                }),
2218                reply: tx,
2219            })
2220            .unwrap();
2221        let result = rx.await.unwrap();
2222        drop(op_tx);
2223        handle.await.unwrap();
2224        assert_eq!(result, Ok("np".to_string()));
2225    }
2226
2227    #[tokio::test]
2228    async fn shutdown_op_stops_the_serve_loop() {
2229        let mut host = host_with(vec![]);
2230        let (op_tx, op_rx) = mpsc::unbounded_channel();
2231        let handle = tokio::spawn(async move { host.serve(op_rx).await });
2232
2233        let (tx, rx) = oneshot::channel();
2234        op_tx.send(ControlOp::Shutdown { reply: tx }).unwrap();
2235        assert!(rx.await.unwrap());
2236        // The serve loop returns once the world's shutdown is signalled.
2237        handle.await.unwrap();
2238    }
2239
2240    #[tokio::test]
2241    async fn flush_and_stop_delegates_to_the_world() {
2242        // The host's flush-and-stop drains the world's persistence lane; calling it
2243        // (even with no agents) returns cleanly and is idempotent.
2244        let mut host = host_with(vec![]);
2245        host.flush_and_stop().await;
2246        host.flush_and_stop().await; // second call is a no-op
2247    }
2248
2249    #[tokio::test]
2250    async fn serve_loop_services_subagent_ops_via_the_sender() {
2251        let mut host = host_with(vec![]);
2252        spawn(&mut host, "run-a", "run-a");
2253        let sub_tx = host.subagent_sender();
2254        let (op_tx, op_rx) = mpsc::unbounded_channel();
2255        let handle = tokio::spawn(async move { host.serve(op_rx).await });
2256
2257        // A Check submitted on the sub-agent channel is serviced by the serve loop.
2258        let (tx, rx) = oneshot::channel();
2259        sub_tx
2260            .send(SubAgentOp::Check {
2261                run_id: "run-a".to_string(),
2262                reply: tx,
2263            })
2264            .unwrap();
2265        assert!(rx.await.unwrap().is_some());
2266
2267        let (stx, srx) = oneshot::channel();
2268        op_tx.send(ControlOp::Shutdown { reply: stx }).unwrap();
2269        assert!(srx.await.unwrap());
2270        handle.await.unwrap();
2271    }
2272
2273    #[test]
2274    fn status_str_covers_all_variants() {
2275        assert_eq!(status_str(&AgentStatus::Idle), "idle");
2276        assert_eq!(status_str(&AgentStatus::Active), "active");
2277        assert_eq!(status_str(&AgentStatus::Waiting), "waiting");
2278        assert_eq!(status_str(&AgentStatus::Complete), "complete");
2279        assert_eq!(
2280            status_str(&AgentStatus::Error {
2281                message: "x".to_string()
2282            }),
2283            "error"
2284        );
2285        assert_eq!(status_str(&AgentStatus::Cancelled), "cancelled");
2286    }
2287
2288    #[tokio::test]
2289    async fn emit_events_broadcasts_agent_changes() {
2290        let mut host = host_with(vec![text("done")]);
2291        let mut rx = host.subscribe();
2292        let entity = spawn(&mut host, "run-a", "agent-a");
2293        // Attach run metadata so the `Spawned` event carries the blueprint name.
2294        host.world_mut()
2295            .world_mut()
2296            .entity_mut(entity)
2297            .insert(RunMetadata {
2298                run_id: "run-a".to_string(),
2299                agent_name: "coder".to_string(),
2300                agent_path: "/a".to_string(),
2301                task: "t".to_string(),
2302                model: None,
2303                workdir: "/w".to_string(),
2304                num_stages: 1,
2305                started_at: 0,
2306                parent_run_id: None,
2307                metadata: std::collections::HashMap::new(),
2308                callback_url: None,
2309                callback_secret: None,
2310                title: None,
2311            });
2312
2313        // First emission after spawn: Spawned + Status + Tokens + Context.
2314        host.emit_events();
2315        let first: Vec<WorldEvent> = std::iter::from_fn(|| rx.try_recv().ok()).collect();
2316        assert!(
2317            first
2318                .iter()
2319                .any(|e| matches!(e, WorldEvent::Spawned { .. }))
2320        );
2321        assert!(first.iter().any(|e| matches!(e, WorldEvent::Status { .. })));
2322        assert!(first.iter().any(|e| matches!(e, WorldEvent::Tokens { .. })));
2323        assert!(
2324            first
2325                .iter()
2326                .any(|e| matches!(e, WorldEvent::Context { .. }))
2327        );
2328
2329        // A second emission with nothing changed emits nothing (skip branches).
2330        host.emit_events();
2331        assert!(rx.try_recv().is_err());
2332
2333        // Drive to completion, then emit: a terminal `Completed` fires.
2334        host.world_mut().run_until_idle(20).await;
2335        host.emit_events();
2336        let done: Vec<WorldEvent> = std::iter::from_fn(|| rx.try_recv().ok()).collect();
2337        assert!(
2338            done.iter()
2339                .any(|e| matches!(e, WorldEvent::Completed { .. }))
2340        );
2341
2342        // Once terminal and unchanged, a further emission fires nothing.
2343        host.emit_events();
2344        assert!(
2345            std::iter::from_fn(|| rx.try_recv().ok())
2346                .collect::<Vec<_>>()
2347                .is_empty()
2348        );
2349    }
2350
2351    #[tokio::test]
2352    async fn emit_events_unloads_terminal_agents_when_safe() {
2353        let mut host = host_with(vec![]);
2354
2355        // A terminal root: emitted on the first pass, unloaded on the second.
2356        let root = {
2357            let mut s = agent_state("root");
2358            s.status = AgentStatus::Complete;
2359            host.world.world_mut().spawn(s).id()
2360        };
2361        host.register("root", root);
2362        host.emit_events();
2363        assert!(
2364            host.live_entity("root").is_some(),
2365            "not reaped on the first terminal pass (event must go out first)"
2366        );
2367        host.emit_events();
2368        assert!(host.live_entity("root").is_none(), "reaped after emit");
2369        assert!(
2370            host.world.world().get::<AgentState>(root).is_none(),
2371            "entity despawned"
2372        );
2373
2374        // A terminal child under a LIVE (Active) parent is deferred.
2375        let parent = host.world.world_mut().spawn(agent_state("parent")).id();
2376        host.register("parent", parent);
2377        let child = {
2378            let mut s = agent_state("child");
2379            s.status = AgentStatus::Complete;
2380            host.world
2381                .world_mut()
2382                .spawn((
2383                    s,
2384                    ParentRef {
2385                        parent_entity: parent,
2386                        parent_agent_id: "parent".to_string(),
2387                        depth: 1,
2388                    },
2389                ))
2390                .id()
2391        };
2392        host.register("child", child);
2393        host.emit_events();
2394        host.emit_events();
2395        assert!(
2396            host.live_entity("child").is_some(),
2397            "not reaped while its parent is live"
2398        );
2399
2400        // Once the parent is terminal, the child becomes reapable.
2401        host.world
2402            .world_mut()
2403            .get_mut::<AgentState>(parent)
2404            .unwrap()
2405            .status = AgentStatus::Complete;
2406        host.emit_events();
2407        host.emit_events();
2408        assert!(
2409            host.live_entity("child").is_none(),
2410            "reaped once its parent is terminal"
2411        );
2412
2413        // A terminal child whose parent entity was despawned is also reapable.
2414        let ghost = host.world.world_mut().spawn_empty().id();
2415        host.world.world_mut().despawn(ghost);
2416        let orphan = {
2417            let mut s = agent_state("orphan");
2418            s.status = AgentStatus::Complete;
2419            host.world
2420                .world_mut()
2421                .spawn((
2422                    s,
2423                    ParentRef {
2424                        parent_entity: ghost,
2425                        parent_agent_id: "gone".to_string(),
2426                        depth: 1,
2427                    },
2428                ))
2429                .id()
2430        };
2431        host.register("orphan", orphan);
2432        host.emit_events();
2433        host.emit_events();
2434        assert!(
2435            host.live_entity("orphan").is_none(),
2436            "reaped: parent entity despawned"
2437        );
2438    }
2439
2440    #[tokio::test]
2441    async fn emit_events_does_not_reap_non_terminal_agents() {
2442        let mut host = host_with(vec![]);
2443        let active = host.world.world_mut().spawn(agent_state("active")).id();
2444        host.register("active", active);
2445        host.emit_events();
2446        host.emit_events();
2447        assert!(host.live_entity("active").is_some());
2448    }
2449
2450    #[tokio::test]
2451    async fn reaper_runs_once_per_agent_before_despawn() {
2452        use std::sync::atomic::{AtomicUsize, Ordering};
2453        let mut host = host_with(vec![]);
2454
2455        // The reap hook records that it saw a still-live entity, proving it runs
2456        // before despawn. A `static` counter dodges the `'static` closure bound.
2457        static SEEN_LIVE: AtomicUsize = AtomicUsize::new(0);
2458        SEEN_LIVE.store(0, Ordering::SeqCst);
2459        host.set_reaper(Box::new(|world, entity| {
2460            // Branch-free (`live as usize`) so the whole closure body is covered
2461            // by a single firing; the assertion below confirms `live` was true.
2462            let live = world.world().get::<AgentState>(entity).is_some();
2463            SEEN_LIVE.fetch_add(live as usize, Ordering::SeqCst);
2464        }));
2465
2466        let root = {
2467            let mut s = agent_state("root");
2468            s.status = AgentStatus::Complete;
2469            host.world.world_mut().spawn(s).id()
2470        };
2471        host.register("root", root);
2472        host.emit_events(); // first pass: emit terminal event, not yet reaped
2473        assert_eq!(SEEN_LIVE.load(Ordering::SeqCst), 0);
2474        host.emit_events(); // second pass: reaper fires, then despawn
2475        assert!(host.live_entity("root").is_none(), "reaped after emit");
2476        assert_eq!(
2477            SEEN_LIVE.load(Ordering::SeqCst),
2478            1,
2479            "reaper ran exactly once, while the entity was still live"
2480        );
2481    }
2482
2483    /// Spawn a `Waiting` agent (optionally with an extra marker component) and
2484    /// register it under `run_id`.
2485    fn register_waiting(host: &mut WorldHost, run_id: &str) -> Entity {
2486        let mut s = agent_state(run_id);
2487        s.status = AgentStatus::Waiting;
2488        let e = host.world.world_mut().spawn(s).id();
2489        host.register(run_id, e);
2490        e
2491    }
2492
2493    /// Regression: a `Waiting` agent must NEVER be unloaded. Every `Waiting`
2494    /// state carries a live, unpersisted continuation, so flushing it to disk
2495    /// strands the run. The worst case is an agent parked on a human approval
2496    /// (`AwaitingInteraction`): unloading it means the answer has no entity to
2497    /// wake and the run hangs in "waiting" forever.
2498    #[tokio::test]
2499    async fn emit_events_never_unloads_waiting_agents() {
2500        use crate::components::AwaitingInteraction;
2501
2502        let mut host = host_with(vec![]);
2503
2504        // Parked on a human prompt (`AwaitingInteraction`) - the reported bug:
2505        // the blocked `ask` future is unpersisted, so unloading strands the run.
2506        let asking = register_waiting(&mut host, "asking");
2507        host.world
2508            .world_mut()
2509            .entity_mut(asking)
2510            .insert(AwaitingInteraction);
2511        // Gated on children, and a plain parked agent.
2512        let gated = register_waiting(&mut host, "gated");
2513        host.world
2514            .world_mut()
2515            .entity_mut(gated)
2516            .insert(WaitingForChildren);
2517        register_waiting(&mut host, "parked");
2518
2519        // Many serve passes - none of them may reap a Waiting agent.
2520        for _ in 0..5 {
2521            host.emit_events();
2522        }
2523        for run_id in ["asking", "gated", "parked"] {
2524            assert!(
2525                host.live_entity(run_id).is_some(),
2526                "a Waiting agent was unloaded and can no longer be resumed"
2527            );
2528        }
2529    }
2530
2531    #[tokio::test]
2532    async fn resolve_or_reload_pages_in_and_registers() {
2533        let mut host = host_with(vec![]);
2534        // No reloader installed → a miss stays a miss.
2535        assert!(host.resolve_or_reload("ghost").is_none());
2536
2537        // A reloader that declines (run not resumable from disk) → still a miss,
2538        // and nothing gets registered.
2539        host.set_reloader(Box::new(|_world, _run_id| None));
2540        assert!(host.resolve_or_reload("gone").is_none());
2541        assert!(
2542            host.live_entity("gone").is_none(),
2543            "a declined reload registers nothing"
2544        );
2545
2546        // With a reloader that resolves → an unloaded run is paged in and registered.
2547        host.set_reloader(Box::new(|world, run_id| {
2548            Some(world.spawn_agent((agent_state(run_id),)))
2549        }));
2550        let paged = host.resolve_or_reload("paged").expect("reloaded");
2551        assert_eq!(
2552            host.live_entity("paged"),
2553            Some(paged),
2554            "registered after reload"
2555        );
2556
2557        // A live run is returned without invoking the reloader (no re-spawn).
2558        assert_eq!(host.resolve_or_reload("paged"), Some(paged));
2559    }
2560
2561    #[tokio::test]
2562    async fn cancel_pages_in_an_unloaded_run() {
2563        let mut host = host_with(vec![]);
2564        host.set_reloader(paging_reloader());
2565        // Cancelling a run that isn't in memory pages it in, then cancels it.
2566        let cancelled = ask(&mut host, |reply| ControlOp::Cancel {
2567            run_id: "unloaded".to_string(),
2568            reply,
2569        })
2570        .await;
2571        assert!(cancelled, "reloaded then cancelled");
2572        assert_eq!(
2573            host.world
2574                .agent_status(host.live_entity("unloaded").unwrap()),
2575            Some(AgentStatus::Cancelled)
2576        );
2577    }
2578
2579    #[tokio::test]
2580    async fn emit_events_broadcasts_new_interactions_once() {
2581        let mut host = host_with(vec![]);
2582        let mut rx = host.subscribe();
2583        let backend = host.interactions().backend_for("agent-a");
2584        let asking = tokio::spawn(async move {
2585            backend
2586                .ask(leviath_core::interaction::InteractionRequest::free_text(
2587                    "q1", "p", "s", true,
2588                ))
2589                .await
2590        });
2591        for _ in 0..8 {
2592            tokio::task::yield_now().await;
2593        }
2594
2595        host.emit_events();
2596        let evs: Vec<WorldEvent> = std::iter::from_fn(|| rx.try_recv().ok()).collect();
2597        assert!(
2598            evs.iter()
2599                .any(|e| matches!(e, WorldEvent::Interaction { .. }))
2600        );
2601        // A second emission does not re-broadcast the same interaction.
2602        host.emit_events();
2603        assert!(rx.try_recv().is_err());
2604
2605        // Answer it so the asking task finishes cleanly.
2606        assert!(
2607            host.interactions()
2608                .answer(leviath_core::interaction::InteractionResponse::text(
2609                    "q1", "ok"
2610                ))
2611        );
2612        let _ = asking.await;
2613    }
2614
2615    #[tokio::test]
2616    async fn event_sender_feeds_subscribers() {
2617        let host = host_with(vec![]);
2618        let mut rx = host.subscribe();
2619        let event = WorldEvent::Completed {
2620            run_id: "r".to_string(),
2621            agent_id: "a".to_string(),
2622            status: "complete".to_string(),
2623        };
2624        host.event_sender().send(event.clone()).unwrap();
2625        assert_eq!(rx.try_recv().unwrap(), event);
2626    }
2627
2628    #[tokio::test]
2629    async fn emit_events_skips_despawned_agents() {
2630        let mut host = host_with(vec![]);
2631        let e = spawn(&mut host, "run-a", "agent-a");
2632        host.world_mut().world_mut().despawn(e);
2633        // The stale run-id mapping is skipped; must not panic.
2634        host.emit_events();
2635    }
2636
2637    #[tokio::test]
2638    async fn serve_returns_when_control_channel_closes() {
2639        let mut host = host_with(vec![text("done")]);
2640        let (op_tx, op_rx) = mpsc::unbounded_channel();
2641        drop(op_tx); // close immediately
2642        host.serve(op_rx).await; // must return, not hang
2643    }
2644
2645    #[tokio::test]
2646    async fn mock_helpers_are_exercised() {
2647        // Keep the test mocks' non-driven methods measured (metadata, the
2648        // exhausted-infer error path, and the no-op tool exec).
2649        let p = Script {
2650            responses: Mutex::new(std::collections::VecDeque::new()),
2651        };
2652        assert_eq!(p.name(), "script");
2653        assert_eq!(p.count_tokens("t", "m").await, 1);
2654        assert_eq!(p.max_context_tokens("m"), 100_000);
2655        let _ = p.capabilities("m");
2656        let req = InferenceRequest {
2657            system: vec![],
2658            messages: vec![],
2659            model: "m".to_string(),
2660            max_tokens: 1,
2661            temperature: 0.0,
2662            tools: vec![],
2663            extra: serde_json::Value::Null,
2664            request_timeout_secs: None,
2665        };
2666        assert!(p.infer(req).await.is_err()); // exhausted
2667
2668        let exec = NoTools.exec_for(
2669            Entity::from_raw_u32(1).expect("a small literal index is always a valid entity id"),
2670            vec![leviath_providers::ToolCall {
2671                id: "c".to_string(),
2672                name: "n".to_string(),
2673                arguments: serde_json::Value::Null,
2674                thought_signature: None,
2675            }],
2676        );
2677        assert_eq!(exec().await, vec![("c".to_string(), String::new())]);
2678    }
2679
2680    #[tokio::test]
2681    async fn list_skips_despawned_entity() {
2682        let mut host = host_with(vec![]);
2683        let e = spawn(&mut host, "run-a", "agent-a");
2684        // Despawn the entity behind the world's back; the run-id map is now stale.
2685        host.world_mut().world_mut().despawn(e);
2686
2687        let list = ask(&mut host, |reply| ControlOp::List { reply }).await;
2688        assert!(list.is_empty()); // stale mapping filtered out
2689        let status = ask(&mut host, |reply| ControlOp::Status {
2690            run_id: "run-a".to_string(),
2691            reply,
2692        })
2693        .await;
2694        assert_eq!(status, None);
2695    }
2696}