1use std::path::Path;
40
41use bevy_ecs::entity::Entity;
42use leviath_core::run_archive;
43use leviath_core::run_meta::{ContextSnapshot, RunMeta, RunStatus};
44use leviath_runtime::host::SpawnArgs;
45use leviath_runtime::interaction_points::InteractionPointState;
46use leviath_runtime::persistence::{RunMetadata, TokenTotals};
47use leviath_runtime::restore::restore_agent;
48use leviath_runtime::world::PipelineWorld;
49
50use crate::daemon::spawn::{SpawnDeps, build_agent_for_reload};
53
54pub fn reload_persisted_agents(
58 world: &mut PipelineWorld,
59 deps: SpawnDeps<'_>,
60 runs_dir: &Path,
61) -> Vec<(String, leviath_runtime::world::AgentId)> {
62 let mut reloaded: Vec<(RunMeta, Entity)> = Vec::new();
63 let Ok(dir_entries) = std::fs::read_dir(runs_dir) else {
64 return Vec::new(); };
66 let candidates: Vec<(RunMeta, bool)> = dir_entries
69 .flatten()
70 .filter_map(|dir_entry| {
71 let run_dir = dir_entry.path();
72 let meta = read_meta(&run_dir)?; let parked_on_fanout = run_dir.join("fanout.json").exists();
74 Some((meta, parked_on_fanout))
75 })
76 .collect();
77 let ordered = leviath_runtime::restore::triage_restores(candidates);
81 for meta in ordered {
82 let run_dir = runs_dir.join(&meta.run_id);
83 match reload_one(world, deps.clone(), &meta, &run_dir) {
84 Ok(entity) => reloaded.push((meta, entity)),
85 Err(e) => {
86 tracing::warn!(run_id = %meta.run_id, error = %e, "skipping un-reloadable run");
87 mark_crashed(&run_dir, meta, &e.to_string(), deps.now_secs);
88 }
89 }
90 }
91 relink_tree(world, &reloaded);
95 restore_fan_outs(world, &reloaded, runs_dir);
96 reloaded
99 .into_iter()
100 .map(|(meta, entity)| (meta.run_id, world.own_agent(entity)))
101 .collect()
102}
103
104pub fn reload_run(
110 world: &mut PipelineWorld,
111 deps: SpawnDeps<'_>,
112 run_id: &str,
113 runs_dir: &std::path::Path,
114) -> Option<leviath_runtime::world::AgentId> {
115 let run_dir = runs_dir.join(run_id);
116 let meta = read_meta(&run_dir)?;
117 if is_terminal(&meta.status) {
118 return None; }
120 let entity = reload_one(world, deps, &meta, &run_dir).ok()?;
121 Some(world.own_agent(entity))
122}
123
124fn restore_fan_outs(world: &mut PipelineWorld, reloaded: &[(RunMeta, Entity)], runs_dir: &Path) {
130 let by_run_id: std::collections::HashMap<&str, Entity> = reloaded
131 .iter()
132 .map(|(m, e)| (m.run_id.as_str(), *e))
133 .collect();
134 for (meta, entity) in reloaded {
135 let path = runs_dir.join(&meta.run_id).join("fanout.json");
136 let Some(state) = std::fs::read_to_string(&path)
137 .ok()
138 .and_then(|s| serde_json::from_str::<leviath_runtime::fanout::FanOutState>(&s).ok())
139 else {
140 continue;
141 };
142 leviath_runtime::fanout::restore_fan_out_waiting(
143 world.world_mut(),
144 *entity,
145 state,
146 &|rid| by_run_id.get(rid).copied(),
147 );
148 }
149}
150
151fn relink_tree(world: &mut PipelineWorld, reloaded: &[(RunMeta, Entity)]) {
157 use leviath_runtime::components::{AgentState, ParentRef, SubAgentChildren};
158
159 let by_run_id: std::collections::HashMap<&str, Entity> = reloaded
160 .iter()
161 .map(|(m, e)| (m.run_id.as_str(), *e))
162 .collect();
163 let w = world.world_mut();
164 for (meta, entity) in reloaded {
165 if let Some(parent_id) = &meta.parent_run_id {
167 match by_run_id.get(parent_id.as_str()) {
168 Some(&parent_entity) => {
169 w.entity_mut(*entity).insert(ParentRef {
170 parent_entity,
171 parent_agent_id: parent_id.clone(),
172 depth: meta.depth,
173 });
174 }
175 None => tracing::warn!(
176 run_id = %meta.run_id, parent = %parent_id,
177 "parent run did not reload; leaving child unlinked"
178 ),
179 }
180 }
181 if !meta.children.is_empty() {
183 let children: Vec<Entity> = meta
184 .children
185 .iter()
186 .filter_map(|cid| by_run_id.get(cid.as_str()).copied())
187 .collect();
188 if !children.is_empty() {
189 w.entity_mut(*entity).insert(SubAgentChildren {
190 children,
191 max_child_depth: meta.max_child_depth,
192 });
193 }
194 w.get_mut::<AgentState>(*entity)
198 .expect("a reloaded agent always has AgentState")
199 .spawned_children_ids = meta.children.clone();
200 }
201 }
202}
203
204fn read_meta(run_dir: &Path) -> Option<RunMeta> {
207 let text = std::fs::read_to_string(run_dir.join("meta.json")).ok()?;
208 serde_json::from_str(&text).ok()
209}
210
211fn totals_from(meta: &RunMeta) -> TokenTotals {
213 TokenTotals {
214 prompt_tokens: meta.prompt_tokens,
215 completion_tokens: meta.completion_tokens,
216 cached_tokens: meta.cached_tokens,
217 cache_write_tokens: meta.cache_write_tokens,
218 tool_calls: meta.tool_calls,
219 }
220}
221
222fn mark_crashed(run_dir: &Path, meta: RunMeta, reason: &str, now_secs: i64) {
234 let crashed = RunMeta {
235 status: RunStatus::Error,
236 error: Some(format!(
237 "the daemon exited while this run was active and it could not be recovered: {reason}"
238 )),
239 updated_at: now_secs,
240 ..meta
241 };
242 if let Err(e) = crate::runstate::write_meta_to(run_dir, &crashed) {
243 tracing::warn!(
244 run_id = %crashed.run_id,
245 error = %e,
246 "could not record an un-reloadable run as crashed"
247 );
248 }
249}
250
251fn is_terminal(status: &RunStatus) -> bool {
253 matches!(
254 status,
255 RunStatus::Complete | RunStatus::Cancelled | RunStatus::Error
256 )
257}
258
259fn reload_one(
262 world: &mut PipelineWorld,
263 deps: SpawnDeps<'_>,
264 meta: &RunMeta,
265 run_dir: &Path,
266) -> Result<Entity, String> {
267 let args = SpawnArgs {
268 run_id: meta.run_id.clone(),
269 blueprint_path: meta.agent_path.clone(),
270 task: meta.task.clone(),
271 regions: Default::default(),
275 model: meta.model.clone(),
276 workdir: meta.workdir.clone(),
277 metadata: meta.metadata.clone(),
278 callback_url: meta.callback_url.clone(),
279 callback_secret: meta.callback_secret.clone(),
280 yolo: meta.yolo,
291 no_seed_commands: true,
294 allow: Vec::new(),
295 max_depth: None,
296 parent_run_id: meta.parent_run_id.clone(),
297 output: meta.output_request.clone(),
301 };
302 let entity = build_agent_for_reload(world.world_mut(), deps, &args)?;
303
304 let folded = std::fs::read(run_dir.join("run.lvr"))
315 .ok()
316 .and_then(|bytes| run_archive::read_archive_lenient(&mut bytes.as_slice()).ok())
317 .and_then(|(_version, records)| run_archive::fold(&records));
318 let (snapshot, stage_index, iteration, totals, pending_batch) = match folded {
319 Some(folded) => {
320 let totals = totals_from(&folded.meta);
321 (
322 folded.context,
323 folded.meta.stage_index,
324 folded.meta.iteration,
325 totals,
326 folded.pending_batch,
327 )
328 }
329 None => {
330 let snapshot = std::fs::read_to_string(run_dir.join("context.json"))
331 .ok()
332 .and_then(|s| serde_json::from_str::<ContextSnapshot>(&s).ok())
333 .unwrap_or_else(|| ContextSnapshot {
334 stage_name: meta.current_stage.clone(),
335 total_tokens: 0,
336 max_tokens: 0,
337 regions: Vec::new(),
338 });
339 (
340 snapshot,
341 meta.stage_index,
342 meta.iteration,
343 totals_from(meta),
344 None,
347 )
348 }
349 };
350 restore_agent(
351 world.world_mut(),
352 entity,
353 &snapshot,
354 stage_index,
355 iteration,
356 totals,
357 );
358
359 if let Some(batch) = pending_batch {
366 leviath_runtime::restore::restore_pending_batch(
367 world.world_mut(),
368 entity,
369 &batch,
370 &meta.children,
371 );
372 }
373
374 {
376 let mut md = world
377 .world_mut()
378 .get_mut::<RunMetadata>(entity)
379 .expect("build_agent attached run metadata");
380 md.started_at = meta.started_at;
381 md.title = meta.title.clone();
382 md.callback_url = meta.callback_url.clone();
383 md.callback_secret = meta.callback_secret.clone();
384 }
386
387 {
390 let mut flags = world
391 .world_mut()
392 .get_mut::<leviath_runtime::persistence::RunOutcomeFlags>(entity)
393 .expect("build_agent attached run outcome flags");
394 flags.0 = meta.flags.clone();
395 }
396
397 if let Some(output) = read_final_output_from(run_dir, meta) {
405 world
406 .world_mut()
407 .entity_mut(entity)
408 .insert(leviath_runtime::persistence::FinalOutput(output));
409 }
410
411 if let Some(state) = std::fs::read_to_string(run_dir.join("interactions.json"))
417 .ok()
418 .and_then(|s| serde_json::from_str::<InteractionPointState>(&s).ok())
419 {
420 let agent = world.own_agent(entity);
422 leviath_runtime::interaction_points::restore_interaction_point(
423 world.world_mut(),
424 agent,
425 state,
426 );
427 }
428
429 if meta.status == RunStatus::Paused {
432 world.pause(world.own_agent(entity));
434 }
435
436 Ok(entity)
437}
438
439fn read_final_output_from(dir: &Path, meta: &RunMeta) -> Option<leviath_core::FinalOutput> {
447 let descriptor = meta.final_output.clone()?;
448 let content = std::fs::read_to_string(dir.join(leviath_core::FINAL_OUTPUT_FILE)).ok()?;
449 Some(leviath_core::FinalOutput {
450 content,
451 format: descriptor.format,
452 stage: descriptor.stage,
453 submitted_at: descriptor.submitted_at,
454 truncated: descriptor.truncated,
455 artifacts: descriptor.artifacts,
456 })
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use std::sync::Arc;
466
467 use leviath_mcp::ToolExecutor;
468 use leviath_runtime::host::SubAgentOp;
469 use leviath_runtime::interaction_hub::InteractionHub;
470 use tokio::sync::Mutex;
471 use tokio::sync::mpsc::UnboundedSender;
472
473 use crate::config::Config;
474 use crate::daemon::tool_service::CliToolService;
475
476 use leviath_runtime::ProviderRegistry;
477 use leviath_runtime::components::AgentStatus;
478 use leviath_runtime::inference_pool::InferencePoolConfig;
479 use tokio::runtime::Handle;
480
481 fn sub_tx() -> UnboundedSender<SubAgentOp> {
482 tokio::sync::mpsc::unbounded_channel().0
483 }
484
485 struct FakeProvider;
486 #[async_trait::async_trait]
487 impl leviath_providers::Provider for FakeProvider {
488 async fn infer(
489 &self,
490 _r: &leviath_providers::InferenceRequest,
491 ) -> leviath_providers::Result<leviath_providers::InferenceResponse> {
492 Err(leviath_providers::ProviderError::Other("t".to_string()))
493 }
494 async fn count_tokens(&self, _t: &str, _m: &str) -> usize {
495 1
496 }
497 fn max_context_tokens(&self, _m: &str) -> usize {
498 1000
499 }
500 fn name(&self) -> &str {
501 "fake"
502 }
503 fn capabilities(&self, _m: &str) -> leviath_providers::ModelCapabilities {
504 leviath_providers::ModelCapabilities::default()
505 }
506 }
507
508 fn test_world() -> (PipelineWorld, Arc<CliToolService>) {
509 let cli = Arc::new(CliToolService::new());
510 let mut registry = ProviderRegistry::new();
511 for p in ["anthropic", "openai", "ollama"] {
512 registry.register(p.to_string(), Arc::new(FakeProvider));
513 }
514 let world = PipelineWorld::new(
515 registry,
516 cli.clone(),
517 InferencePoolConfig::new(),
518 1,
519 None,
520 Handle::current(),
521 );
522 (world, cli)
523 }
524
525 fn coder_manifest() -> String {
526 crate::test_support::inline_coder_manifest()
528 }
529
530 fn write_run(
533 runs_dir: &Path,
534 run_id: &str,
535 agent_path: &str,
536 status: RunStatus,
537 context: Option<&ContextSnapshot>,
538 ) {
539 write_run_tree(RunFixture {
540 runs_dir,
541 run_id,
542 agent_path,
543 status,
544 context,
545 parent_run_id: None,
546 children: &[],
547 depth: 0,
548 max_child_depth: 0,
549 });
550 }
551
552 struct RunFixture<'a> {
561 runs_dir: &'a Path,
562 run_id: &'a str,
563 agent_path: &'a str,
564 status: RunStatus,
565 context: Option<&'a ContextSnapshot>,
566 parent_run_id: Option<&'a str>,
567 children: &'a [&'a str],
568 depth: usize,
569 max_child_depth: usize,
570 }
571
572 fn write_run_tree(f: RunFixture<'_>) {
573 let RunFixture {
574 runs_dir,
575 run_id,
576 agent_path,
577 status,
578 context,
579 parent_run_id,
580 children,
581 depth,
582 max_child_depth,
583 } = f;
584 let dir = runs_dir.join(run_id);
585 std::fs::create_dir_all(&dir).unwrap();
586 let meta = RunMeta {
587 run_id: run_id.to_string(),
588 agent_name: "coder".to_string(),
589 agent_path: agent_path.to_string(),
590 task: "resume me".to_string(),
591 model: None,
592 pid: 0,
593 status,
594 current_stage: "implement".to_string(),
595 stage_index: 0,
596 num_stages: 1,
597 iteration: 5,
598 prompt_tokens: 42,
599 completion_tokens: 7,
600 cached_tokens: 0,
601 cache_write_tokens: 0,
602 tool_calls: 3,
603 workdir: std::env::temp_dir().to_string_lossy().to_string(),
604 started_at: 111,
605 updated_at: 222,
606 last_progress_at: None,
607 error: None,
608 title: Some("Resume Me".to_string()),
609 metadata: std::collections::HashMap::new(),
610 callback_url: Some("http://cb".to_string()),
611 callback_secret: None,
612 parent_run_id: parent_run_id.map(str::to_string),
613 children: children.iter().map(|s| s.to_string()).collect(),
614 depth,
615 max_child_depth,
616 flags: leviath_core::run_meta::RunFlags {
619 modified_files: vec!["src/a.rs".to_string()],
620 modified_file_count: 1,
621 no_output_tools: true,
626 ..Default::default()
627 },
628 yolo: false,
629 read_paths: None,
630 final_output: Some(
634 leviath_core::output::FinalOutput::new(
635 "already answered",
636 Some("markdown".to_string()),
637 "implement".to_string(),
638 777,
639 )
640 .descriptor(),
641 ),
642 output_request: Some(leviath_core::output::OutputSpec {
643 format: Some("a2ui".to_string()),
644 ..Default::default()
645 }),
646 };
647 std::fs::write(dir.join("meta.json"), serde_json::to_string(&meta).unwrap()).unwrap();
648 std::fs::write(
651 dir.join(leviath_core::FINAL_OUTPUT_FILE),
652 "already answered",
653 )
654 .unwrap();
655 if let Some(ctx) = context {
656 std::fs::write(
657 dir.join("context.json"),
658 serde_json::to_string(ctx).unwrap(),
659 )
660 .unwrap();
661 }
662 }
663
664 fn agent_dir() -> tempfile::TempDir {
665 let dir = tempfile::tempdir().unwrap();
666 std::fs::write(dir.path().join("agent.leviath"), coder_manifest()).unwrap();
667 dir
668 }
669
670 fn write_run_archive(
674 runs_dir: &Path,
675 run_id: &str,
676 agent_path: &str,
677 stage_index: usize,
678 iteration: usize,
679 prompt_tokens: usize,
680 context: &ContextSnapshot,
681 ) {
682 use leviath_core::run_archive::{self, RunIdentity, RunRecord};
683 let dir = runs_dir.join(run_id);
684 std::fs::create_dir_all(&dir).unwrap();
685 let mut meta = RunMeta::new(
686 run_id.to_string(),
687 "coder".to_string(),
688 agent_path.to_string(),
689 "resume me".to_string(),
690 None,
691 std::env::temp_dir().to_string_lossy().to_string(),
692 1,
693 );
694 meta.status = RunStatus::Running;
695 meta.current_stage = "implement".to_string();
696 meta.stage_index = stage_index;
697 meta.iteration = iteration;
698 meta.prompt_tokens = prompt_tokens;
699 let mut buf = Vec::new();
700 run_archive::write_archive_start(&mut buf, run_archive::RUN_ARCHIVE_VERSION).unwrap();
701 run_archive::write_record(
702 &mut buf,
703 &RunRecord::Header {
704 identity: RunIdentity {
705 run_id: run_id.to_string(),
706 machine_id: "m".to_string(),
707 world_id: "w".to_string(),
708 created_at: 1,
709 },
710 meta: Box::new(meta),
711 },
712 )
713 .unwrap();
714 run_archive::write_record(
715 &mut buf,
716 &RunRecord::ContextCheckpoint {
717 snapshot: context.clone(),
718 at: 2,
719 },
720 )
721 .unwrap();
722 std::fs::write(dir.join("run.lvr"), &buf).unwrap();
723 }
724
725 #[tokio::test]
728 async fn reload_keeps_a_paused_run_paused() {
729 let agent = agent_dir();
730 let manifest = agent.path().join("agent.leviath");
731 let runs = tempfile::tempdir().unwrap();
732 write_run(
733 runs.path(),
734 "run-paused",
735 manifest.to_str().unwrap(),
736 RunStatus::Paused,
737 None,
738 );
739
740 let (mut world, cli) = test_world();
741 let hub = InteractionHub::new();
742 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
743 let restored = reload_persisted_agents(
744 &mut world,
745 crate::daemon::spawn::SpawnDeps {
746 tool_service: cli.as_ref(),
747 config: &Config::default(),
748 shared_mcp: mcp,
749 mcp_tool_defs: &[],
750 hub: &hub,
751 now_secs: 999,
752 subagent_tx: sub_tx().clone(),
753 },
754 runs.path(),
755 );
756
757 assert_eq!(restored.len(), 1);
758 let (run_id, entity) = &restored[0];
759 assert_eq!(run_id, "run-paused");
760 assert_eq!(world.agent_status(*entity), Some(AgentStatus::Paused));
761 }
762
763 #[test]
769 fn a_descriptor_without_its_sidecar_restores_nothing() {
770 let dir = tempfile::tempdir().unwrap();
771 let mut meta = RunMeta::new(
772 "run-1".to_string(),
773 "a".to_string(),
774 "/p".to_string(),
775 "t".to_string(),
776 None,
777 "/w".to_string(),
778 1,
779 );
780
781 assert!(read_final_output_from(dir.path(), &meta).is_none());
783
784 let answer = leviath_core::output::FinalOutput::new(
786 "already answered",
787 Some("markdown".to_string()),
788 "implement".to_string(),
789 777,
790 );
791 meta.final_output = Some(answer.descriptor());
792 assert!(read_final_output_from(dir.path(), &meta).is_none());
793
794 std::fs::write(
796 dir.path().join(leviath_core::FINAL_OUTPUT_FILE),
797 &answer.content,
798 )
799 .unwrap();
800 let restored = read_final_output_from(dir.path(), &meta).expect("both halves");
801 assert_eq!(restored.content, "already answered");
802 assert_eq!(restored.stage, "implement");
803 }
804
805 async fn reload_single(runs: &Path, run_id: &str) -> (PipelineWorld, Entity) {
806 let (mut world, cli) = test_world();
807 let restored = reload_persisted_agents(
808 &mut world,
809 crate::daemon::spawn::SpawnDeps {
810 tool_service: cli.as_ref(),
811 config: &Config::default(),
812 shared_mcp: Arc::new(Mutex::new(ToolExecutor::new())),
813 mcp_tool_defs: &[],
814 hub: &InteractionHub::new(),
815 now_secs: 999,
816 subagent_tx: sub_tx().clone(),
817 },
818 runs,
819 );
820 assert_eq!(restored.len(), 1);
821 assert_eq!(restored[0].0, run_id);
822 let entity = restored[0].1;
823 (world, entity.entity())
824 }
825
826 #[tokio::test]
830 async fn reload_keeps_an_unattended_run_unattended() {
831 let agent = agent_dir();
832 let manifest = agent.path().join("agent.leviath");
833 let runs = tempfile::tempdir().unwrap();
834 write_run(
835 runs.path(),
836 "run-yolo",
837 manifest.to_str().unwrap(),
838 RunStatus::Running,
839 None,
840 );
841 let meta_path = runs.path().join("run-yolo").join("meta.json");
843 let mut meta: RunMeta =
844 serde_json::from_str(&std::fs::read_to_string(&meta_path).unwrap()).unwrap();
845 meta.yolo = true;
846 std::fs::write(&meta_path, serde_json::to_string(&meta).unwrap()).unwrap();
847
848 let (world, entity) = reload_single(runs.path(), "run-yolo").await;
849 assert!(
850 world
851 .world()
852 .get::<RunMetadata>(entity)
853 .expect("reloaded run has metadata")
854 .unattended
855 );
856 assert!(
857 world
858 .world()
859 .get::<leviath_runtime::components::InteractionAutoApprove>(entity)
860 .is_some(),
861 "an unattended reload still auto-approves its checkpoints"
862 );
863 }
864
865 #[tokio::test]
868 async fn reload_does_not_invent_unattended() {
869 let agent = agent_dir();
870 let manifest = agent.path().join("agent.leviath");
871 let runs = tempfile::tempdir().unwrap();
872 write_run(
873 runs.path(),
874 "run-plain",
875 manifest.to_str().unwrap(),
876 RunStatus::Running,
877 None,
878 );
879 let meta_path = runs.path().join("run-plain").join("meta.json");
881 let mut raw: serde_json::Value =
882 serde_json::from_str(&std::fs::read_to_string(&meta_path).unwrap()).unwrap();
883 raw.as_object_mut().unwrap().remove("yolo");
884 std::fs::write(&meta_path, serde_json::to_string(&raw).unwrap()).unwrap();
885
886 let (world, entity) = reload_single(runs.path(), "run-plain").await;
887 assert!(
888 !world
889 .world()
890 .get::<RunMetadata>(entity)
891 .expect("reloaded run has metadata")
892 .unattended
893 );
894 }
895
896 #[tokio::test]
897 async fn reloads_nonterminal_runs_and_restores_state() {
898 let agent = agent_dir();
899 let manifest = agent.path().join("agent.leviath");
900 let runs = tempfile::tempdir().unwrap();
901
902 let ctx = ContextSnapshot {
904 stage_name: "implement".to_string(),
905 total_tokens: 4,
906 max_tokens: 100_000,
907 regions: vec![leviath_core::run_meta::RegionSnapshot {
908 name: "conversation".to_string(),
909 kind: "clearable".to_string(),
910 current_tokens: 4,
911 max_tokens: 100_000,
912 entries: vec![leviath_core::run_meta::RegionEntrySnapshot {
913 content: "earlier turn".to_string(),
914 tokens: 4,
915 kind: leviath_core::region::EntryKind::UserMessage,
916 metadata: None,
917 key: None,
918 taint: Default::default(),
919 }],
920 }],
921 };
922 write_run(
923 runs.path(),
924 "run-live",
925 manifest.to_str().unwrap(),
926 RunStatus::Running,
927 Some(&ctx),
928 );
929 write_run(
931 runs.path(),
932 "run-done",
933 manifest.to_str().unwrap(),
934 RunStatus::Complete,
935 None,
936 );
937
938 let (mut world, cli) = test_world();
939 let hub = InteractionHub::new();
940 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
941 let restored = reload_persisted_agents(
942 &mut world,
943 crate::daemon::spawn::SpawnDeps {
944 tool_service: cli.as_ref(),
945 config: &Config::default(),
946 shared_mcp: mcp,
947 mcp_tool_defs: &[],
948 hub: &hub,
949 now_secs: 999,
950 subagent_tx: sub_tx().clone(),
951 },
952 runs.path(),
953 );
954
955 assert_eq!(restored.len(), 1);
956 let (run_id, entity) = &restored[0];
957 assert_eq!(run_id, "run-live");
958 assert_eq!(world.agent_status(*entity), Some(AgentStatus::Active));
959 let md = world.world().get::<RunMetadata>(entity.entity()).unwrap();
961 assert_eq!(md.started_at, 111);
962 assert_eq!(md.title.as_deref(), Some("Resume Me"));
963 assert_eq!(md.callback_url.as_deref(), Some("http://cb"));
964 let totals = world.world().get::<TokenTotals>(entity.entity()).unwrap();
965 assert_eq!(totals.prompt_tokens, 42);
966 assert_eq!(totals.tool_calls, 3);
967 let flags = world
970 .world()
971 .get::<leviath_runtime::persistence::RunOutcomeFlags>(entity.entity())
972 .unwrap();
973 assert_eq!(flags.0.modified_files, vec!["src/a.rs".to_string()]);
974 assert_eq!(flags.0.modified_file_count, 1);
975 assert!(flags.0.no_output_tools);
978 let output = world
982 .world()
983 .get::<leviath_runtime::persistence::FinalOutput>(entity.entity())
984 .expect("a submitted answer survives the restart");
985 assert_eq!(output.0.content, "already answered");
986 assert_eq!(output.0.stage, "implement");
987 assert_eq!(
990 md.output_request.as_ref().and_then(|s| s.format.as_deref()),
991 Some("a2ui")
992 );
993 }
994
995 fn assert_restored_from_archive(world: &PipelineWorld, entity: Entity) {
998 use leviath_runtime::components::AgentState;
999 let state = world.world().get::<AgentState>(entity).unwrap();
1000 assert_eq!(state.current_stage, "fresh-stage");
1003 assert_eq!(state.iteration, 9);
1004 let totals = world.world().get::<TokenTotals>(entity).unwrap();
1006 assert_eq!(totals.prompt_tokens, 99);
1007 }
1008
1009 #[tokio::test]
1014 async fn reload_prefers_the_atomic_journal_over_a_stale_context_json() {
1015 let agent = agent_dir();
1016 let manifest = agent.path().join("agent.leviath");
1017 let mpath = manifest.to_str().unwrap();
1018 let runs = tempfile::tempdir().unwrap();
1019
1020 let stale = ContextSnapshot {
1024 stage_name: "stale-stage".to_string(),
1025 total_tokens: 1,
1026 max_tokens: 100,
1027 regions: vec![],
1028 };
1029 write_run(
1030 runs.path(),
1031 "run-torn",
1032 mpath,
1033 RunStatus::Running,
1034 Some(&stale),
1035 );
1036 let fresh = ContextSnapshot {
1039 stage_name: "fresh-stage".to_string(),
1040 total_tokens: 4,
1041 max_tokens: 100_000,
1042 regions: vec![],
1043 };
1044 write_run_archive(runs.path(), "run-torn", mpath, 0, 9, 99, &fresh);
1045
1046 let (mut world, cli) = test_world();
1047 let hub = InteractionHub::new();
1048 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1049 let restored = reload_persisted_agents(
1050 &mut world,
1051 crate::daemon::spawn::SpawnDeps {
1052 tool_service: cli.as_ref(),
1053 config: &Config::default(),
1054 shared_mcp: mcp,
1055 mcp_tool_defs: &[],
1056 hub: &hub,
1057 now_secs: 999,
1058 subagent_tx: sub_tx().clone(),
1059 },
1060 runs.path(),
1061 );
1062
1063 assert_eq!(restored.len(), 1);
1064 assert_restored_from_archive(&world, restored[0].1.entity());
1065 }
1066
1067 #[tokio::test]
1071 async fn reload_tolerates_a_torn_journal_tail() {
1072 let agent = agent_dir();
1073 let manifest = agent.path().join("agent.leviath");
1074 let mpath = manifest.to_str().unwrap();
1075 let runs = tempfile::tempdir().unwrap();
1076
1077 let stale = ContextSnapshot {
1078 stage_name: "stale-stage".to_string(),
1079 total_tokens: 1,
1080 max_tokens: 100,
1081 regions: vec![],
1082 };
1083 write_run(
1084 runs.path(),
1085 "run-torn2",
1086 mpath,
1087 RunStatus::Running,
1088 Some(&stale),
1089 );
1090 let fresh = ContextSnapshot {
1091 stage_name: "fresh-stage".to_string(),
1092 total_tokens: 4,
1093 max_tokens: 100_000,
1094 regions: vec![],
1095 };
1096 write_run_archive(runs.path(), "run-torn2", mpath, 0, 9, 99, &fresh);
1097 {
1099 use std::io::Write;
1100 let mut f = std::fs::OpenOptions::new()
1101 .append(true)
1102 .open(runs.path().join("run-torn2/run.lvr"))
1103 .unwrap();
1104 f.write_all(&[0, 0, 0, 0, 0, 0, 0, 10, 1, 2]).unwrap();
1105 }
1106
1107 let (mut world, cli) = test_world();
1108 let hub = InteractionHub::new();
1109 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1110 let restored = reload_persisted_agents(
1111 &mut world,
1112 crate::daemon::spawn::SpawnDeps {
1113 tool_service: cli.as_ref(),
1114 config: &Config::default(),
1115 shared_mcp: mcp,
1116 mcp_tool_defs: &[],
1117 hub: &hub,
1118 now_secs: 999,
1119 subagent_tx: sub_tx().clone(),
1120 },
1121 runs.path(),
1122 );
1123
1124 assert_eq!(restored.len(), 1);
1125 assert_restored_from_archive(&world, restored[0].1.entity());
1127 }
1128
1129 fn append_archive_records(
1132 runs_dir: &Path,
1133 run_id: &str,
1134 records: &[leviath_core::run_archive::RunRecord],
1135 ) {
1136 use std::io::Write;
1137 let mut buf = Vec::new();
1138 for r in records {
1139 leviath_core::run_archive::write_record(&mut buf, r).unwrap();
1140 }
1141 let mut f = std::fs::OpenOptions::new()
1142 .append(true)
1143 .open(runs_dir.join(run_id).join("run.lvr"))
1144 .unwrap();
1145 f.write_all(&buf).unwrap();
1146 }
1147
1148 fn batch_call(
1149 id: &str,
1150 name: &str,
1151 result: Option<&str>,
1152 ) -> leviath_core::run_archive::ToolCallRecord {
1153 leviath_core::run_archive::ToolCallRecord {
1154 id: id.to_string(),
1155 name: name.to_string(),
1156 arguments: "{}".to_string(),
1157 result: result.map(str::to_string),
1158 thought_signature: None,
1159 }
1160 }
1161
1162 fn conversation_of(world: &PipelineWorld, entity: Entity) -> Vec<leviath_core::RegionEntry> {
1164 world
1165 .world()
1166 .get::<leviath_runtime::components::ContextWindow>(entity)
1167 .unwrap()
1168 .get_region("conversation")
1169 .unwrap()
1170 .content
1171 .clone()
1172 }
1173
1174 #[tokio::test]
1180 async fn reload_replays_a_pending_tool_batch_instead_of_reexecuting() {
1181 use leviath_core::run_archive::RunRecord;
1182 let agent = agent_dir();
1183 let manifest = agent.path().join("agent.leviath");
1184 let mpath = manifest.to_str().unwrap();
1185 let runs = tempfile::tempdir().unwrap();
1186
1187 write_run(runs.path(), "run-batch", mpath, RunStatus::Running, None);
1188 let ctx = ContextSnapshot {
1189 stage_name: "implement".to_string(),
1190 total_tokens: 0,
1191 max_tokens: 100_000,
1192 regions: vec![],
1193 };
1194 write_run_archive(runs.path(), "run-batch", mpath, 0, 9, 99, &ctx);
1195 append_archive_records(
1196 runs.path(),
1197 "run-batch",
1198 &[
1199 RunRecord::ToolBatch {
1200 calls: vec![
1201 batch_call("c_done", "write_file", None),
1202 batch_call("c_lost", "shell", None),
1203 ],
1204 at: 3,
1205 stage_index: 0,
1206 iteration: 9,
1207 response: "writing then running".to_string(),
1208 },
1209 RunRecord::ToolCallDone {
1210 iteration: 9,
1211 call_id: "c_done".to_string(),
1212 result: "Wrote 42 bytes to x.txt".to_string(),
1213 at: 4,
1214 },
1215 ],
1216 );
1217
1218 let (mut world, cli) = test_world();
1219 let hub = InteractionHub::new();
1220 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1221 let restored = reload_persisted_agents(
1222 &mut world,
1223 crate::daemon::spawn::SpawnDeps {
1224 tool_service: cli.as_ref(),
1225 config: &Config::default(),
1226 shared_mcp: mcp,
1227 mcp_tool_defs: &[],
1228 hub: &hub,
1229 now_secs: 999,
1230 subagent_tx: sub_tx().clone(),
1231 },
1232 runs.path(),
1233 );
1234
1235 assert_eq!(restored.len(), 1);
1236 let entity = restored[0].1;
1237 let entries = conversation_of(&world, entity.entity());
1238 assert!(entries.iter().any(|e| matches!(
1240 &e.kind,
1241 leviath_core::region::EntryKind::AssistantTurn { tool_calls } if tool_calls.len() == 2
1242 )));
1243 assert!(
1245 entries
1246 .iter()
1247 .any(|e| e.content == "Wrote 42 bytes to x.txt")
1248 );
1249 assert!(entries.iter().any(|e| e.content.contains("interrupted")
1251 && e.content.contains("Verify whether it took effect")));
1252 assert!(
1254 world
1255 .world()
1256 .get::<leviath_runtime::pipeline::ReadyToInfer>(entity.entity())
1257 .is_some()
1258 );
1259 }
1260
1261 #[tokio::test]
1265 async fn reload_does_not_replay_a_batch_already_in_the_window() {
1266 use leviath_core::region::EntryKind;
1267 use leviath_core::run_archive::RunRecord;
1268 let agent = agent_dir();
1269 let manifest = agent.path().join("agent.leviath");
1270 let mpath = manifest.to_str().unwrap();
1271 let runs = tempfile::tempdir().unwrap();
1272
1273 write_run(runs.path(), "run-applied", mpath, RunStatus::Running, None);
1274 let ctx = ContextSnapshot {
1276 stage_name: "implement".to_string(),
1277 total_tokens: 2,
1278 max_tokens: 100_000,
1279 regions: vec![leviath_core::run_meta::RegionSnapshot {
1280 name: "conversation".to_string(),
1281 kind: "clearable".to_string(),
1282 current_tokens: 2,
1283 max_tokens: 100_000,
1284 entries: vec![
1285 leviath_core::run_meta::RegionEntrySnapshot {
1286 content: "done".to_string(),
1287 tokens: 1,
1288 kind: EntryKind::AssistantTurn {
1289 tool_calls: vec![leviath_core::region::SerializedToolCall {
1290 id: "c1".to_string(),
1291 name: "write_file".to_string(),
1292 arguments: serde_json::Value::Null,
1293 thought_signature: None,
1294 }],
1295 },
1296 metadata: None,
1297 key: None,
1298 taint: Default::default(),
1299 },
1300 leviath_core::run_meta::RegionEntrySnapshot {
1301 content: "Wrote it".to_string(),
1302 tokens: 1,
1303 kind: EntryKind::ToolResult {
1304 tool_call_id: "c1".to_string(),
1305 tool_name: "write_file".to_string(),
1306 is_error: false,
1307 },
1308 metadata: None,
1309 key: None,
1310 taint: Default::default(),
1311 },
1312 ],
1313 }],
1314 };
1315 write_run_archive(runs.path(), "run-applied", mpath, 0, 9, 99, &ctx);
1316 append_archive_records(
1317 runs.path(),
1318 "run-applied",
1319 &[RunRecord::ToolBatch {
1320 calls: vec![batch_call("c1", "write_file", None)],
1321 at: 3,
1322 stage_index: 0,
1323 iteration: 9,
1324 response: "done".to_string(),
1325 }],
1326 );
1327
1328 let (mut world, cli) = test_world();
1329 let hub = InteractionHub::new();
1330 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1331 let restored = reload_persisted_agents(
1332 &mut world,
1333 crate::daemon::spawn::SpawnDeps {
1334 tool_service: cli.as_ref(),
1335 config: &Config::default(),
1336 shared_mcp: mcp,
1337 mcp_tool_defs: &[],
1338 hub: &hub,
1339 now_secs: 999,
1340 subagent_tx: sub_tx().clone(),
1341 },
1342 runs.path(),
1343 );
1344
1345 assert_eq!(restored.len(), 1);
1346 let entries = conversation_of(&world, restored[0].1.entity());
1347 assert_eq!(
1349 entries
1350 .iter()
1351 .filter(|e| matches!(&e.kind, EntryKind::AssistantTurn { tool_calls } if !tool_calls.is_empty()))
1352 .count(),
1353 1
1354 );
1355 assert!(!entries.iter().any(|e| e.content.contains("interrupted")));
1356 }
1357
1358 fn interactive_agent_dir() -> tempfile::TempDir {
1361 let dir = tempfile::tempdir().unwrap();
1362 std::fs::write(
1363 dir.path().join("agent.leviath"),
1364 crate::test_support::inline_interactive_manifest(),
1365 )
1366 .unwrap();
1367 dir
1368 }
1369
1370 #[tokio::test]
1371 async fn reload_resumes_a_blocked_interaction_point_in_the_waiting_state() {
1372 let agent = interactive_agent_dir();
1373 let manifest = agent.path().join("agent.leviath");
1374 let runs = tempfile::tempdir().unwrap();
1375
1376 write_run(
1378 runs.path(),
1379 "run-await",
1380 manifest.to_str().unwrap(),
1381 RunStatus::WaitingInput,
1382 None,
1383 );
1384 std::fs::write(
1386 runs.path().join("run-await/interactions.json"),
1387 serde_json::to_string(&InteractionPointState {
1388 cursor: 0,
1389 round: 0,
1390 body: "## Plan\n1. do it".to_string(),
1391 })
1392 .unwrap(),
1393 )
1394 .unwrap();
1395
1396 let (mut world, cli) = test_world();
1397 let hub = InteractionHub::new();
1398 world.insert_interaction_hub(hub.clone()); let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1400 let restored = reload_persisted_agents(
1401 &mut world,
1402 crate::daemon::spawn::SpawnDeps {
1403 tool_service: cli.as_ref(),
1404 config: &Config::default(),
1405 shared_mcp: mcp,
1406 mcp_tool_defs: &[],
1407 hub: &hub,
1408 now_secs: 999,
1409 subagent_tx: sub_tx().clone(),
1410 },
1411 runs.path(),
1412 );
1413
1414 assert_eq!(restored.len(), 1);
1415 let (run_id, entity) = &restored[0];
1416 assert_eq!(run_id, "run-await");
1417 assert_eq!(world.agent_status(*entity), Some(AgentStatus::Waiting));
1420 assert!(
1421 world
1422 .world()
1423 .get::<leviath_runtime::interaction_points::AwaitingInteractionPoint>(
1424 entity.entity()
1425 )
1426 .is_some()
1427 );
1428 assert!(
1429 world
1430 .world()
1431 .get::<leviath_runtime::pipeline::ReadyToInfer>(entity.entity())
1432 .is_none(),
1433 "the spawn-set ReadyToInfer is cleared so the inference lane won't fire"
1434 );
1435
1436 for _ in 0..8 {
1438 tokio::task::yield_now().await;
1439 }
1440 let pending = hub.pending();
1441 assert_eq!(pending.len(), 1);
1442 assert_eq!(pending[0].0, "run-await");
1443 assert_eq!(pending[0].1.body.as_deref(), Some("## Plan\n1. do it"));
1444 }
1445
1446 #[tokio::test]
1447 async fn reload_restores_actionable_runs_before_blocked_and_skips_terminal() {
1448 let agent = agent_dir();
1449 let mpath = agent.path().join("agent.leviath");
1450 let mpath = mpath.to_str().unwrap();
1451 let runs = tempfile::tempdir().unwrap();
1452 write_run(
1455 runs.path(),
1456 "aaa-blocked",
1457 mpath,
1458 RunStatus::WaitingInput,
1459 None,
1460 );
1461 write_run(runs.path(), "zzz-active", mpath, RunStatus::Running, None);
1462 write_run(runs.path(), "mmm-done", mpath, RunStatus::Complete, None);
1463
1464 let (mut world, cli) = test_world();
1465 let hub = InteractionHub::new();
1466 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1467 let restored = reload_persisted_agents(
1468 &mut world,
1469 crate::daemon::spawn::SpawnDeps {
1470 tool_service: cli.as_ref(),
1471 config: &Config::default(),
1472 shared_mcp: mcp,
1473 mcp_tool_defs: &[],
1474 hub: &hub,
1475 now_secs: 999,
1476 subagent_tx: sub_tx().clone(),
1477 },
1478 runs.path(),
1479 );
1480
1481 let order: Vec<&str> = restored.iter().map(|(id, _)| id.as_str()).collect();
1483 assert_eq!(order, vec!["zzz-active", "aaa-blocked"]);
1484 }
1485
1486 #[tokio::test]
1487 async fn reload_run_pages_in_nonterminal_only() {
1488 let agent = agent_dir();
1489 let manifest = agent.path().join("agent.leviath");
1490 let mpath = manifest.to_str().unwrap();
1491 let runs = tempfile::tempdir().unwrap();
1492 write_run(runs.path(), "live", mpath, RunStatus::Running, None);
1493 write_run(runs.path(), "done", mpath, RunStatus::Complete, None);
1494
1495 let (mut world, cli) = test_world();
1496 let hub = InteractionHub::new();
1497 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1498
1499 assert!(
1501 reload_run(
1502 &mut world,
1503 crate::daemon::spawn::SpawnDeps {
1504 tool_service: cli.as_ref(),
1505 config: &Config::default(),
1506 shared_mcp: mcp.clone(),
1507 mcp_tool_defs: &[],
1508 hub: &hub,
1509 now_secs: 1,
1510 subagent_tx: sub_tx().clone(),
1511 },
1512 "live",
1513 runs.path(),
1514 )
1515 .is_some()
1516 );
1517 assert!(
1519 reload_run(
1520 &mut world,
1521 crate::daemon::spawn::SpawnDeps {
1522 tool_service: cli.as_ref(),
1523 config: &Config::default(),
1524 shared_mcp: mcp.clone(),
1525 mcp_tool_defs: &[],
1526 hub: &hub,
1527 now_secs: 1,
1528 subagent_tx: sub_tx().clone(),
1529 },
1530 "done",
1531 runs.path(),
1532 )
1533 .is_none()
1534 );
1535 assert!(
1537 reload_run(
1538 &mut world,
1539 crate::daemon::spawn::SpawnDeps {
1540 tool_service: cli.as_ref(),
1541 config: &Config::default(),
1542 shared_mcp: mcp,
1543 mcp_tool_defs: &[],
1544 hub: &hub,
1545 now_secs: 1,
1546 subagent_tx: sub_tx().clone(),
1547 },
1548 "no-such-run",
1549 runs.path(),
1550 )
1551 .is_none()
1552 );
1553 }
1554
1555 #[tokio::test]
1556 async fn resumes_a_parent_parked_mid_fan_out() {
1557 use leviath_core::blueprint::{FanOutConfig, WorkerFailurePolicy};
1558 use leviath_runtime::fanout::{FanOutState, FanOutWaiting};
1559
1560 let agent = agent_dir();
1561 let manifest = agent.path().join("agent.leviath");
1562 let mpath = manifest.to_str().unwrap();
1563 let runs = tempfile::tempdir().unwrap();
1564
1565 write_run(
1567 runs.path(),
1568 "parent-fo",
1569 mpath,
1570 RunStatus::WaitingInput,
1571 None,
1572 );
1573 let state = FanOutState {
1574 config: FanOutConfig {
1575 worker_agent: None,
1576 worker_stage: Some("w".to_string()),
1577 worker_query: None,
1578 merge_stage: None,
1579 max_workers: 1,
1580 on_worker_failure: WorkerFailurePolicy::Continue,
1581 split_prompt: "s".to_string(),
1582 results_region: None,
1583 max_items: None,
1584 },
1585 max_workers: 1,
1586 pending: vec![],
1587 active: vec![("item-1".to_string(), "worker-fo".to_string())],
1590 summaries: vec![],
1591 failures: vec![],
1592 };
1593 std::fs::write(
1594 runs.path().join("parent-fo").join("fanout.json"),
1595 serde_json::to_string(&state).unwrap(),
1596 )
1597 .unwrap();
1598 write_run(runs.path(), "worker-fo", mpath, RunStatus::Running, None);
1600
1601 write_run(runs.path(), "bad-fo", mpath, RunStatus::WaitingInput, None);
1603 std::fs::write(runs.path().join("bad-fo").join("fanout.json"), b"garbage").unwrap();
1604
1605 let (mut world, cli) = test_world();
1606 let hub = InteractionHub::new();
1607 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1608 let restored = reload_persisted_agents(
1609 &mut world,
1610 crate::daemon::spawn::SpawnDeps {
1611 tool_service: cli.as_ref(),
1612 config: &Config::default(),
1613 shared_mcp: mcp,
1614 mcp_tool_defs: &[],
1615 hub: &hub,
1616 now_secs: 999,
1617 subagent_tx: sub_tx().clone(),
1618 },
1619 runs.path(),
1620 );
1621 let by_id: std::collections::HashMap<_, _> =
1622 restored.iter().map(|(r, e)| (r.clone(), *e)).collect();
1623
1624 assert!(
1626 world
1627 .world()
1628 .get::<FanOutWaiting>(by_id["parent-fo"].entity())
1629 .is_some()
1630 );
1631 assert!(
1632 world
1633 .world()
1634 .get::<FanOutWaiting>(by_id["bad-fo"].entity())
1635 .is_none()
1636 );
1637 }
1638
1639 #[tokio::test]
1640 async fn rebuilds_parent_child_tree_on_reload() {
1641 use leviath_runtime::components::{ParentRef, SubAgentChildren};
1642
1643 let agent = agent_dir();
1644 let manifest = agent.path().join("agent.leviath");
1645 let mpath = manifest.to_str().unwrap();
1646 let runs = tempfile::tempdir().unwrap();
1647
1648 write_run_tree(RunFixture {
1650 runs_dir: runs.path(),
1651 run_id: "parent",
1652 agent_path: mpath,
1653 status: RunStatus::WaitingInput,
1654 context: None,
1655 parent_run_id: None,
1656 children: &["child-a", "child-b"],
1657 depth: 0,
1658 max_child_depth: 4,
1659 });
1660 write_run_tree(RunFixture {
1661 runs_dir: runs.path(),
1662 run_id: "child-a",
1663 agent_path: mpath,
1664 status: RunStatus::Running,
1665 context: None,
1666 parent_run_id: Some("parent"),
1667 children: &[],
1668 depth: 1,
1669 max_child_depth: 0,
1670 });
1671 write_run_tree(RunFixture {
1672 runs_dir: runs.path(),
1673 run_id: "child-b",
1674 agent_path: mpath,
1675 status: RunStatus::Running,
1676 context: None,
1677 parent_run_id: Some("parent"),
1678 children: &[],
1679 depth: 1,
1680 max_child_depth: 0,
1681 });
1682
1683 let (mut world, cli) = test_world();
1684 let hub = InteractionHub::new();
1685 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1686 let restored = reload_persisted_agents(
1687 &mut world,
1688 crate::daemon::spawn::SpawnDeps {
1689 tool_service: cli.as_ref(),
1690 config: &Config::default(),
1691 shared_mcp: mcp,
1692 mcp_tool_defs: &[],
1693 hub: &hub,
1694 now_secs: 999,
1695 subagent_tx: sub_tx().clone(),
1696 },
1697 runs.path(),
1698 );
1699 assert_eq!(restored.len(), 3);
1700 let by_id: std::collections::HashMap<_, _> =
1701 restored.iter().map(|(r, e)| (r.clone(), *e)).collect();
1702 let parent = by_id["parent"];
1703 let child_a = by_id["child-a"];
1704 let child_b = by_id["child-b"];
1705
1706 let kids = world
1708 .world()
1709 .get::<SubAgentChildren>(parent.entity())
1710 .unwrap();
1711 assert_eq!(kids.max_child_depth, 4);
1712 assert_eq!(kids.children.len(), 2);
1713 assert!(
1714 kids.children.contains(&child_a.entity()) && kids.children.contains(&child_b.entity())
1715 );
1716 let pr = world.world().get::<ParentRef>(child_a.entity()).unwrap();
1718 assert_eq!(pr.parent_entity, parent.entity());
1719 assert_eq!(pr.parent_agent_id, "parent");
1720 assert_eq!(pr.depth, 1);
1721 let state = world
1723 .world()
1724 .get::<leviath_runtime::components::AgentState>(parent.entity())
1725 .unwrap();
1726 assert_eq!(state.spawned_children_ids, vec!["child-a", "child-b"]);
1727 }
1728
1729 #[tokio::test]
1730 async fn relink_skips_children_and_parents_that_did_not_reload() {
1731 use leviath_runtime::components::{ParentRef, SubAgentChildren};
1732
1733 let agent = agent_dir();
1734 let manifest = agent.path().join("agent.leviath");
1735 let mpath = manifest.to_str().unwrap();
1736 let runs = tempfile::tempdir().unwrap();
1737
1738 write_run_tree(RunFixture {
1740 runs_dir: runs.path(),
1741 run_id: "lonely-parent",
1742 agent_path: mpath,
1743 status: RunStatus::WaitingInput,
1744 context: None,
1745 parent_run_id: None,
1746 children: &["gone-child"],
1747 depth: 0,
1748 max_child_depth: 2,
1749 });
1750 write_run_tree(RunFixture {
1751 runs_dir: runs.path(),
1752 run_id: "gone-child",
1753 agent_path: mpath,
1754 status: RunStatus::Complete,
1755 context: None,
1757 parent_run_id: Some("lonely-parent"),
1758 children: &[],
1759 depth: 1,
1760 max_child_depth: 0,
1761 });
1762 write_run_tree(RunFixture {
1764 runs_dir: runs.path(),
1765 run_id: "orphan",
1766 agent_path: mpath,
1767 status: RunStatus::Running,
1768 context: None,
1769 parent_run_id: Some("gone-parent"),
1770 children: &[],
1771 depth: 1,
1772 max_child_depth: 0,
1773 });
1774 write_run_tree(RunFixture {
1775 runs_dir: runs.path(),
1776 run_id: "gone-parent",
1777 agent_path: mpath,
1778 status: RunStatus::Error,
1779 context: None,
1780 parent_run_id: None,
1781 children: &["orphan"],
1782 depth: 0,
1783 max_child_depth: 2,
1784 });
1785
1786 let (mut world, cli) = test_world();
1787 let hub = InteractionHub::new();
1788 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1789 let restored = reload_persisted_agents(
1790 &mut world,
1791 crate::daemon::spawn::SpawnDeps {
1792 tool_service: cli.as_ref(),
1793 config: &Config::default(),
1794 shared_mcp: mcp,
1795 mcp_tool_defs: &[],
1796 hub: &hub,
1797 now_secs: 999,
1798 subagent_tx: sub_tx().clone(),
1799 },
1800 runs.path(),
1801 );
1802 assert_eq!(restored.len(), 2);
1804 let by_id: std::collections::HashMap<_, _> =
1805 restored.iter().map(|(r, e)| (r.clone(), *e)).collect();
1806 assert!(
1808 world
1809 .world()
1810 .get::<SubAgentChildren>(by_id["lonely-parent"].entity())
1811 .is_none()
1812 );
1813 assert!(
1815 world
1816 .world()
1817 .get::<ParentRef>(by_id["orphan"].entity())
1818 .is_none()
1819 );
1820 }
1821
1822 #[tokio::test]
1823 async fn reload_without_context_json_still_resumes() {
1824 let agent = agent_dir();
1825 let manifest = agent.path().join("agent.leviath");
1826 let runs = tempfile::tempdir().unwrap();
1827 write_run(
1828 runs.path(),
1829 "run-nocontext",
1830 manifest.to_str().unwrap(),
1831 RunStatus::WaitingInput,
1832 None, );
1834
1835 let (mut world, cli) = test_world();
1836 let hub = InteractionHub::new();
1837 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1838 let restored = reload_persisted_agents(
1839 &mut world,
1840 crate::daemon::spawn::SpawnDeps {
1841 tool_service: cli.as_ref(),
1842 config: &Config::default(),
1843 shared_mcp: mcp,
1844 mcp_tool_defs: &[],
1845 hub: &hub,
1846 now_secs: 999,
1847 subagent_tx: sub_tx().clone(),
1848 },
1849 runs.path(),
1850 );
1851 assert_eq!(restored.len(), 1);
1852 assert!(
1853 world
1854 .world()
1855 .get::<TokenTotals>(restored[0].1.entity())
1856 .is_some()
1857 );
1858 }
1859
1860 #[tokio::test]
1861 async fn skips_missing_dir_junk_and_unreloadable_runs() {
1862 let (mut world, cli) = test_world();
1864 let hub = InteractionHub::new();
1865 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1866 assert!(
1867 reload_persisted_agents(
1868 &mut world,
1869 crate::daemon::spawn::SpawnDeps {
1870 tool_service: cli.as_ref(),
1871 config: &Config::default(),
1872 shared_mcp: mcp.clone(),
1873 mcp_tool_defs: &[],
1874 hub: &hub,
1875 now_secs: 1,
1876 subagent_tx: sub_tx().clone(),
1877 },
1878 std::path::Path::new("/no/such/runs/dir"),
1879 )
1880 .is_empty()
1881 );
1882
1883 let runs = tempfile::tempdir().unwrap();
1886 std::fs::create_dir_all(runs.path().join("no-meta")).unwrap();
1887 let corrupt = runs.path().join("corrupt");
1888 std::fs::create_dir_all(&corrupt).unwrap();
1889 std::fs::write(corrupt.join("meta.json"), "not json").unwrap();
1890 write_run(
1891 runs.path(),
1892 "run-badpath",
1893 "/no/such/agent.leviath",
1894 RunStatus::Running,
1895 None,
1896 );
1897
1898 let restored = reload_persisted_agents(
1899 &mut world,
1900 crate::daemon::spawn::SpawnDeps {
1901 tool_service: cli.as_ref(),
1902 config: &Config::default(),
1903 shared_mcp: mcp,
1904 mcp_tool_defs: &[],
1905 hub: &hub,
1906 now_secs: 1,
1907 subagent_tx: sub_tx().clone(),
1908 },
1909 runs.path(),
1910 );
1911 assert!(restored.is_empty()); let meta: RunMeta = serde_json::from_str(
1917 &std::fs::read_to_string(runs.path().join("run-badpath").join("meta.json")).unwrap(),
1918 )
1919 .unwrap();
1920 assert_eq!(meta.status, RunStatus::Error);
1921 let error = meta.error.unwrap_or_default();
1922 assert!(error.contains("could not be recovered"), "got: {error}");
1923 assert_eq!(meta.updated_at, 1);
1924 assert!(!runs.path().join("no-meta").join("meta.json").exists());
1926 assert_eq!(
1927 std::fs::read_to_string(corrupt.join("meta.json")).unwrap(),
1928 "not json"
1929 );
1930 }
1931
1932 #[test]
1933 fn marking_a_crash_is_best_effort() {
1934 let runs = tempfile::tempdir().unwrap();
1938 write_run(
1939 runs.path(),
1940 "run-x",
1941 "/no/such/agent.leviath",
1942 RunStatus::Running,
1943 None,
1944 );
1945 let meta = read_meta(&runs.path().join("run-x")).expect("written above");
1946 mark_crashed(&runs.path().join("gone"), meta, "boom", 7);
1947 assert!(!runs.path().join("gone").exists());
1948 }
1949
1950 #[tokio::test]
1951 async fn fake_provider_methods_are_exercised() {
1952 use leviath_providers::Provider;
1953 let p = FakeProvider;
1954 assert_eq!(p.name(), "fake");
1955 assert_eq!(p.count_tokens("t", "m").await, 1);
1956 assert_eq!(p.max_context_tokens("m"), 1000);
1957 let _ = p.capabilities("m");
1958 assert!(
1959 p.infer(&leviath_providers::InferenceRequest {
1960 system: vec![],
1961 messages: vec![],
1962 model: "m".to_string(),
1963 max_tokens: 1,
1964 temperature: 0.0,
1965 tools: vec![],
1966 extra: serde_json::Value::Null,
1967 request_timeout_secs: None,
1968 })
1969 .await
1970 .is_err()
1971 );
1972 }
1973
1974 #[test]
1975 fn is_terminal_covers_all_statuses() {
1976 assert!(is_terminal(&RunStatus::Complete));
1977 assert!(is_terminal(&RunStatus::Cancelled));
1978 assert!(is_terminal(&RunStatus::Error));
1979 assert!(!is_terminal(&RunStatus::Running));
1980 assert!(!is_terminal(&RunStatus::WaitingInput));
1981 }
1982}