1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
36pub struct SpawnArgs {
37 pub run_id: String,
39 pub blueprint_path: String,
41 pub task: String,
45 #[serde(default)]
49 pub regions: HashMap<String, String>,
50 #[serde(default)]
52 pub model: Option<String>,
53 pub workdir: String,
55 #[serde(default)]
57 pub metadata: HashMap<String, String>,
58 #[serde(default)]
60 pub callback_url: Option<String>,
61 #[serde(default)]
63 pub callback_secret: Option<String>,
64 #[serde(default)]
69 pub yolo: bool,
70 #[serde(default)]
75 pub no_seed_commands: bool,
76 #[serde(default)]
78 pub allow: Vec<String>,
79 #[serde(default)]
81 pub max_depth: Option<usize>,
82 #[serde(default)]
86 pub parent_run_id: Option<String>,
87}
88
89pub type Spawner = Box<dyn FnMut(&mut PipelineWorld, &SpawnArgs) -> Result<Entity, String> + Send>;
94
95pub type Reloader = Box<dyn FnMut(&mut PipelineWorld, &str) -> Option<Entity> + Send>;
103
104pub type ForceTerminator = Box<dyn FnMut(&str) -> bool + Send>;
117
118pub type Reaper = Box<dyn FnMut(&mut PipelineWorld, Entity) + Send>;
124
125pub type SpawnPreprocessor = Box<
133 dyn Fn(&SpawnArgs) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> + Send,
134>;
135
136pub enum SubAgentOp {
142 Spawn {
145 args: Box<SpawnArgs>,
148 parent_run_id: String,
150 max_depth: usize,
152 reply: oneshot::Sender<Result<String, String>>,
154 },
155 Check {
157 run_id: String,
159 reply: oneshot::Sender<Option<AgentStatus>>,
161 },
162 Send {
165 run_id: String,
167 caller_run_id: String,
170 content: String,
172 reply: oneshot::Sender<bool>,
174 },
175 Kill {
177 run_id: String,
179 caller_run_id: String,
182 reply: oneshot::Sender<bool>,
184 },
185}
186
187pub enum ControlOp {
190 Spawn {
192 args: Box<SpawnArgs>,
195 reply: oneshot::Sender<Result<String, String>>,
197 },
198 Status {
200 run_id: String,
202 reply: oneshot::Sender<Option<AgentStatus>>,
204 },
205 Pause {
207 run_id: String,
209 reply: oneshot::Sender<bool>,
211 },
212 Resume {
214 run_id: String,
216 reply: oneshot::Sender<bool>,
218 },
219 Cancel {
221 run_id: String,
223 reply: oneshot::Sender<bool>,
225 },
226 List {
228 reply: oneshot::Sender<Vec<(String, AgentStatus)>>,
230 },
231 Message {
234 agent_id: String,
236 content: String,
238 target_region: Option<String>,
240 reply: oneshot::Sender<bool>,
242 },
243 ListInteractions {
245 reply: oneshot::Sender<Vec<(String, InteractionRequest)>>,
247 },
248 AnswerInteraction {
250 response: InteractionResponse,
252 reply: oneshot::Sender<bool>,
254 },
255 CancelInteraction {
258 request_id: String,
260 reply: oneshot::Sender<bool>,
262 },
263 Shutdown {
266 reply: oneshot::Sender<bool>,
268 },
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
275#[serde(tag = "event", rename_all = "snake_case")]
276pub enum WorldEvent {
277 Spawned {
279 run_id: String,
281 agent_id: String,
283 blueprint: String,
285 },
286 Status {
288 run_id: String,
290 agent_id: String,
292 status: String,
294 stage: String,
296 iteration: usize,
298 tool_calls: usize,
300 accepts_messages: bool,
302 },
303 Tokens {
305 run_id: String,
307 agent_id: String,
309 prompt_tokens: usize,
311 completion_tokens: usize,
313 cached_tokens: usize,
315 cache_write_tokens: usize,
317 },
318 Context {
320 run_id: String,
322 agent_id: String,
324 total_tokens: usize,
326 max_tokens: usize,
328 },
329 Interaction {
331 run_id: String,
333 agent_id: String,
335 request: InteractionRequest,
337 },
338 Completed {
340 run_id: String,
342 agent_id: String,
344 status: String,
346 },
347 Log {
350 run_id: String,
352 agent_id: String,
354 line: String,
356 },
357}
358
359#[derive(bevy_ecs::resource::Resource, Clone)]
366pub struct WorldEventSink(pub broadcast::Sender<WorldEvent>);
367
368fn 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#[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
396pub 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 subagent_tx: UnboundedSender<SubAgentOp>,
412 subagent_rx: UnboundedReceiver<SubAgentOp>,
413}
414
415impl WorldHost {
416 pub fn new(world: PipelineWorld) -> Self {
418 Self::with_interactions(world, InteractionHub::new())
419 }
420
421 pub fn with_interactions(mut world: PipelineWorld, interactions: InteractionHub) -> Self {
424 let (events, _) = broadcast::channel(1024);
425 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 pub fn subagent_sender(&self) -> UnboundedSender<SubAgentOp> {
451 self.subagent_tx.clone()
452 }
453
454 pub fn subscribe(&self) -> broadcast::Receiver<WorldEvent> {
457 self.events.subscribe()
458 }
459
460 pub fn event_sender(&self) -> broadcast::Sender<WorldEvent> {
463 self.events.clone()
464 }
465
466 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 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; };
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 if cur.terminal && was_terminal && self.no_live_parent(entity) {
599 to_reap.push((run_id.clone(), entity));
600 }
601 self.emitted.insert(run_id, cur);
610 }
611
612 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 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 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 pub fn set_spawner(&mut self, spawner: Spawner) {
687 self.spawner = Some(spawner);
688 }
689
690 pub fn set_spawn_preprocessor(&mut self, pp: SpawnPreprocessor) {
693 self.spawn_preprocessor = Some(pp);
694 }
695
696 pub fn set_reloader(&mut self, reloader: Reloader) {
699 self.reloader = Some(reloader);
700 }
701
702 pub fn set_force_terminator(&mut self, force_terminator: ForceTerminator) {
706 self.force_terminator = Some(force_terminator);
707 }
708
709 pub fn set_reaper(&mut self, reaper: Reaper) {
713 self.reaper = Some(reaper);
714 }
715
716 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 pub fn interactions(&self) -> InteractionHub {
730 self.interactions.clone()
731 }
732
733 pub fn world_mut(&mut self) -> &mut PipelineWorld {
735 &mut self.world
736 }
737
738 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 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 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 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 fn spawn_child(
804 &mut self,
805 mut args: SpawnArgs,
806 parent_run_id: &str,
807 max_depth: usize,
808 ) -> Result<String, String> {
809 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 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 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 fn is_within_tree(&mut self, run_id: &str, ancestor: &str) -> bool {
884 if run_id == ancestor {
885 return true;
886 }
887 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 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 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 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 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 pub fn handle(&mut self, op: ControlOp) {
963 match op {
964 ControlOp::Spawn { args, reply } => {
965 let result = match self.spawner.as_mut() {
966 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 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 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 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 let _ = reply.send(true);
1068 self.world.shutdown();
1069 }
1070 }
1071 }
1072
1073 pub async fn flush_and_stop(&mut self) {
1078 self.world.flush_and_stop().await;
1079 }
1080
1081 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 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, }
1114 }
1115 Some(sub) = self.subagent_rx.recv() => {
1117 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 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 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 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 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 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 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 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 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 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 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 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 let pref = host.world.world().get::<ParentRef>(child).unwrap();
1527 assert_eq!(pref.parent_entity, parent);
1528 assert_eq!(pref.depth, 1);
1529 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, 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 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 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 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 #[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 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 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 #[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 #[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 #[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 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 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 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 #[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 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 #[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 #[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 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 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 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 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 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 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 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 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 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); 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 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 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 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 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 handle.await.unwrap();
2238 }
2239
2240 #[tokio::test]
2241 async fn flush_and_stop_delegates_to_the_world() {
2242 let mut host = host_with(vec![]);
2245 host.flush_and_stop().await;
2246 host.flush_and_stop().await; }
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 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 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 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 host.emit_events();
2331 assert!(rx.try_recv().is_err());
2332
2333 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 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 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 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 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 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 static SEEN_LIVE: AtomicUsize = AtomicUsize::new(0);
2458 SEEN_LIVE.store(0, Ordering::SeqCst);
2459 host.set_reaper(Box::new(|world, entity| {
2460 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(); assert_eq!(SEEN_LIVE.load(Ordering::SeqCst), 0);
2474 host.emit_events(); 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 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 #[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 let asking = register_waiting(&mut host, "asking");
2507 host.world
2508 .world_mut()
2509 .entity_mut(asking)
2510 .insert(AwaitingInteraction);
2511 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 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 assert!(host.resolve_or_reload("ghost").is_none());
2536
2537 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 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 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 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 host.emit_events();
2603 assert!(rx.try_recv().is_err());
2604
2605 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 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); host.serve(op_rx).await; }
2644
2645 #[tokio::test]
2646 async fn mock_helpers_are_exercised() {
2647 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()); 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 host.world_mut().world_mut().despawn(e);
2686
2687 let list = ask(&mut host, |reply| ControlOp::List { reply }).await;
2688 assert!(list.is_empty()); 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}