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