1use std::path::Path;
40use std::sync::Arc;
41
42use bevy_ecs::entity::Entity;
43use leviath_core::run_archive;
44use leviath_core::run_meta::{ContextSnapshot, RunMeta, RunStatus};
45use leviath_mcp::ToolExecutor;
46use leviath_providers::Tool;
47use leviath_runtime::host::{SpawnArgs, SubAgentOp};
48use leviath_runtime::interaction_hub::InteractionHub;
49use leviath_runtime::interaction_points::InteractionPointState;
50use leviath_runtime::persistence::{RunMetadata, TokenTotals};
51use leviath_runtime::restore::restore_agent;
52use leviath_runtime::world::PipelineWorld;
53use tokio::sync::Mutex;
54use tokio::sync::mpsc::UnboundedSender;
55
56use crate::config::Config;
57use crate::daemon::spawn::build_agent_for_reload;
58use crate::daemon::tool_service::CliToolService;
59
60#[allow(clippy::too_many_arguments)]
64pub fn reload_persisted_agents(
65 world: &mut PipelineWorld,
66 tool_service: &CliToolService,
67 config: &Config,
68 shared_mcp: Arc<Mutex<ToolExecutor>>,
69 mcp_tool_defs: &[Tool],
70 hub: &InteractionHub,
71 runs_dir: &Path,
72 now_secs: i64,
73 subagent_tx: &UnboundedSender<SubAgentOp>,
74) -> Vec<(String, Entity)> {
75 let mut reloaded: Vec<(RunMeta, Entity)> = Vec::new();
76 let Ok(dir_entries) = std::fs::read_dir(runs_dir) else {
77 return Vec::new(); };
79 let candidates: Vec<(RunMeta, bool)> = dir_entries
82 .flatten()
83 .filter_map(|dir_entry| {
84 let run_dir = dir_entry.path();
85 let meta = read_meta(&run_dir)?; let parked_on_fanout = run_dir.join("fanout.json").exists();
87 Some((meta, parked_on_fanout))
88 })
89 .collect();
90 let ordered = leviath_runtime::restore::triage_restores(candidates);
94 for meta in ordered {
95 let run_dir = runs_dir.join(&meta.run_id);
96 match reload_one(
97 world,
98 tool_service,
99 config,
100 shared_mcp.clone(),
101 mcp_tool_defs,
102 hub,
103 &meta,
104 &run_dir,
105 now_secs,
106 subagent_tx,
107 ) {
108 Ok(entity) => reloaded.push((meta, entity)),
109 Err(e) => {
110 tracing::warn!(run_id = %meta.run_id, error = %e, "skipping un-reloadable run");
111 mark_crashed(&run_dir, meta, &e.to_string(), now_secs);
112 }
113 }
114 }
115 relink_tree(world, &reloaded);
119 restore_fan_outs(world, &reloaded, runs_dir);
120 reloaded
121 .into_iter()
122 .map(|(meta, entity)| (meta.run_id, entity))
123 .collect()
124}
125
126#[allow(clippy::too_many_arguments)]
132pub fn reload_run(
133 world: &mut PipelineWorld,
134 tool_service: &CliToolService,
135 config: &Config,
136 shared_mcp: Arc<Mutex<ToolExecutor>>,
137 mcp_tool_defs: &[Tool],
138 hub: &InteractionHub,
139 run_id: &str,
140 runs_dir: &std::path::Path,
141 now_secs: i64,
142 subagent_tx: &UnboundedSender<SubAgentOp>,
143) -> Option<Entity> {
144 let run_dir = runs_dir.join(run_id);
145 let meta = read_meta(&run_dir)?;
146 if is_terminal(&meta.status) {
147 return None; }
149 reload_one(
150 world,
151 tool_service,
152 config,
153 shared_mcp,
154 mcp_tool_defs,
155 hub,
156 &meta,
157 &run_dir,
158 now_secs,
159 subagent_tx,
160 )
161 .ok()
162}
163
164fn restore_fan_outs(world: &mut PipelineWorld, reloaded: &[(RunMeta, Entity)], runs_dir: &Path) {
170 let by_run_id: std::collections::HashMap<&str, Entity> = reloaded
171 .iter()
172 .map(|(m, e)| (m.run_id.as_str(), *e))
173 .collect();
174 for (meta, entity) in reloaded {
175 let path = runs_dir.join(&meta.run_id).join("fanout.json");
176 let Some(state) = std::fs::read_to_string(&path)
177 .ok()
178 .and_then(|s| serde_json::from_str::<leviath_runtime::fanout::FanOutState>(&s).ok())
179 else {
180 continue;
181 };
182 leviath_runtime::fanout::restore_fan_out_waiting(
183 world.world_mut(),
184 *entity,
185 state,
186 &|rid| by_run_id.get(rid).copied(),
187 );
188 }
189}
190
191fn relink_tree(world: &mut PipelineWorld, reloaded: &[(RunMeta, Entity)]) {
197 use leviath_runtime::components::{AgentState, ParentRef, SubAgentChildren};
198
199 let by_run_id: std::collections::HashMap<&str, Entity> = reloaded
200 .iter()
201 .map(|(m, e)| (m.run_id.as_str(), *e))
202 .collect();
203 let w = world.world_mut();
204 for (meta, entity) in reloaded {
205 if let Some(parent_id) = &meta.parent_run_id {
207 match by_run_id.get(parent_id.as_str()) {
208 Some(&parent_entity) => {
209 w.entity_mut(*entity).insert(ParentRef {
210 parent_entity,
211 parent_agent_id: parent_id.clone(),
212 depth: meta.depth,
213 });
214 }
215 None => tracing::warn!(
216 run_id = %meta.run_id, parent = %parent_id,
217 "parent run did not reload; leaving child unlinked"
218 ),
219 }
220 }
221 if !meta.children.is_empty() {
223 let children: Vec<Entity> = meta
224 .children
225 .iter()
226 .filter_map(|cid| by_run_id.get(cid.as_str()).copied())
227 .collect();
228 if !children.is_empty() {
229 w.entity_mut(*entity).insert(SubAgentChildren {
230 children,
231 max_child_depth: meta.max_child_depth,
232 });
233 }
234 w.get_mut::<AgentState>(*entity)
238 .expect("a reloaded agent always has AgentState")
239 .spawned_children_ids = meta.children.clone();
240 }
241 }
242}
243
244fn read_meta(run_dir: &Path) -> Option<RunMeta> {
247 let text = std::fs::read_to_string(run_dir.join("meta.json")).ok()?;
248 serde_json::from_str(&text).ok()
249}
250
251fn totals_from(meta: &RunMeta) -> TokenTotals {
253 TokenTotals {
254 prompt_tokens: meta.prompt_tokens,
255 completion_tokens: meta.completion_tokens,
256 cached_tokens: meta.cached_tokens,
257 cache_write_tokens: meta.cache_write_tokens,
258 tool_calls: meta.tool_calls,
259 }
260}
261
262fn mark_crashed(run_dir: &Path, meta: RunMeta, reason: &str, now_secs: i64) {
274 let crashed = RunMeta {
275 status: RunStatus::Error,
276 error: Some(format!(
277 "the daemon exited while this run was active and it could not be recovered: {reason}"
278 )),
279 updated_at: now_secs,
280 ..meta
281 };
282 if let Err(e) = crate::runstate::write_meta_to(run_dir, &crashed) {
283 tracing::warn!(
284 run_id = %crashed.run_id,
285 error = %e,
286 "could not record an un-reloadable run as crashed"
287 );
288 }
289}
290
291fn is_terminal(status: &RunStatus) -> bool {
293 matches!(
294 status,
295 RunStatus::Complete | RunStatus::Cancelled | RunStatus::Error
296 )
297}
298
299#[allow(clippy::too_many_arguments)]
302fn reload_one(
303 world: &mut PipelineWorld,
304 tool_service: &CliToolService,
305 config: &Config,
306 shared_mcp: Arc<Mutex<ToolExecutor>>,
307 mcp_tool_defs: &[Tool],
308 hub: &InteractionHub,
309 meta: &RunMeta,
310 run_dir: &Path,
311 now_secs: i64,
312 subagent_tx: &UnboundedSender<SubAgentOp>,
313) -> Result<Entity, String> {
314 let args = SpawnArgs {
315 run_id: meta.run_id.clone(),
316 blueprint_path: meta.agent_path.clone(),
317 task: meta.task.clone(),
318 regions: Default::default(),
322 model: meta.model.clone(),
323 workdir: meta.workdir.clone(),
324 metadata: meta.metadata.clone(),
325 callback_url: meta.callback_url.clone(),
326 callback_secret: meta.callback_secret.clone(),
327 yolo: meta.yolo,
338 no_seed_commands: true,
341 allow: Vec::new(),
342 max_depth: None,
343 parent_run_id: meta.parent_run_id.clone(),
344 };
345 let entity = build_agent_for_reload(
346 world.world_mut(),
347 tool_service,
348 config,
349 shared_mcp,
350 mcp_tool_defs,
351 hub,
352 &args,
353 now_secs,
354 subagent_tx.clone(),
355 )?;
356
357 let folded = std::fs::read(run_dir.join("run.lvr"))
368 .ok()
369 .and_then(|bytes| run_archive::read_archive_lenient(&mut bytes.as_slice()).ok())
370 .and_then(|(_version, records)| run_archive::fold(&records));
371 let (snapshot, stage_index, iteration, totals, pending_batch) = match folded {
372 Some(folded) => {
373 let totals = totals_from(&folded.meta);
374 (
375 folded.context,
376 folded.meta.stage_index,
377 folded.meta.iteration,
378 totals,
379 folded.pending_batch,
380 )
381 }
382 None => {
383 let snapshot = std::fs::read_to_string(run_dir.join("context.json"))
384 .ok()
385 .and_then(|s| serde_json::from_str::<ContextSnapshot>(&s).ok())
386 .unwrap_or_else(|| ContextSnapshot {
387 stage_name: meta.current_stage.clone(),
388 total_tokens: 0,
389 max_tokens: 0,
390 regions: Vec::new(),
391 });
392 (
393 snapshot,
394 meta.stage_index,
395 meta.iteration,
396 totals_from(meta),
397 None,
400 )
401 }
402 };
403 restore_agent(
404 world.world_mut(),
405 entity,
406 &snapshot,
407 stage_index,
408 iteration,
409 totals,
410 );
411
412 if let Some(batch) = pending_batch {
419 leviath_runtime::restore::restore_pending_batch(
420 world.world_mut(),
421 entity,
422 &batch,
423 &meta.children,
424 );
425 }
426
427 {
429 let mut md = world
430 .world_mut()
431 .get_mut::<RunMetadata>(entity)
432 .expect("build_agent attached run metadata");
433 md.started_at = meta.started_at;
434 md.title = meta.title.clone();
435 md.callback_url = meta.callback_url.clone();
436 md.callback_secret = meta.callback_secret.clone();
437 }
439
440 {
443 let mut flags = world
444 .world_mut()
445 .get_mut::<leviath_runtime::persistence::RunOutcomeFlags>(entity)
446 .expect("build_agent attached run outcome flags");
447 flags.0 = meta.flags.clone();
448 }
449
450 if let Some(state) = std::fs::read_to_string(run_dir.join("interactions.json"))
456 .ok()
457 .and_then(|s| serde_json::from_str::<InteractionPointState>(&s).ok())
458 {
459 leviath_runtime::interaction_points::restore_interaction_point(
460 world.world_mut(),
461 entity,
462 state,
463 );
464 }
465
466 if meta.status == RunStatus::Paused {
469 world.pause(entity);
470 }
471
472 Ok(entity)
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use leviath_runtime::ProviderRegistry;
479 use leviath_runtime::components::AgentStatus;
480 use leviath_runtime::inference_pool::InferencePoolConfig;
481 use tokio::runtime::Handle;
482
483 fn sub_tx() -> UnboundedSender<SubAgentOp> {
484 tokio::sync::mpsc::unbounded_channel().0
485 }
486
487 struct FakeProvider;
488 #[async_trait::async_trait]
489 impl leviath_providers::Provider for FakeProvider {
490 async fn infer(
491 &self,
492 _r: leviath_providers::InferenceRequest,
493 ) -> leviath_providers::Result<leviath_providers::InferenceResponse> {
494 Err(leviath_providers::ProviderError::Other("t".to_string()))
495 }
496 async fn count_tokens(&self, _t: &str, _m: &str) -> usize {
497 1
498 }
499 fn max_context_tokens(&self, _m: &str) -> usize {
500 1000
501 }
502 fn name(&self) -> &str {
503 "fake"
504 }
505 fn capabilities(&self, _m: &str) -> leviath_providers::ModelCapabilities {
506 leviath_providers::ModelCapabilities::default()
507 }
508 }
509
510 fn test_world() -> (PipelineWorld, Arc<CliToolService>) {
511 let cli = Arc::new(CliToolService::new());
512 let mut registry = ProviderRegistry::new();
513 for p in ["anthropic", "openai", "ollama"] {
514 registry.register(p.to_string(), Arc::new(FakeProvider));
515 }
516 let world = PipelineWorld::new(
517 registry,
518 cli.clone(),
519 InferencePoolConfig::new(),
520 1,
521 None,
522 Handle::current(),
523 );
524 (world, cli)
525 }
526
527 fn coder_manifest() -> String {
528 crate::test_support::inline_coder_manifest()
530 }
531
532 fn write_run(
535 runs_dir: &Path,
536 run_id: &str,
537 agent_path: &str,
538 status: RunStatus,
539 context: Option<&ContextSnapshot>,
540 ) {
541 write_run_tree(
542 runs_dir,
543 run_id,
544 agent_path,
545 status,
546 context,
547 None,
548 &[],
549 0,
550 0,
551 );
552 }
553
554 #[allow(clippy::too_many_arguments)]
557 fn write_run_tree(
558 runs_dir: &Path,
559 run_id: &str,
560 agent_path: &str,
561 status: RunStatus,
562 context: Option<&ContextSnapshot>,
563 parent_run_id: Option<&str>,
564 children: &[&str],
565 depth: usize,
566 max_child_depth: usize,
567 ) {
568 let dir = runs_dir.join(run_id);
569 std::fs::create_dir_all(&dir).unwrap();
570 let meta = RunMeta {
571 run_id: run_id.to_string(),
572 agent_name: "coder".to_string(),
573 agent_path: agent_path.to_string(),
574 task: "resume me".to_string(),
575 model: None,
576 pid: 0,
577 status,
578 current_stage: "implement".to_string(),
579 stage_index: 0,
580 num_stages: 1,
581 iteration: 5,
582 prompt_tokens: 42,
583 completion_tokens: 7,
584 cached_tokens: 0,
585 cache_write_tokens: 0,
586 tool_calls: 3,
587 workdir: std::env::temp_dir().to_string_lossy().to_string(),
588 started_at: 111,
589 updated_at: 222,
590 last_progress_at: None,
591 error: None,
592 title: Some("Resume Me".to_string()),
593 metadata: std::collections::HashMap::new(),
594 callback_url: Some("http://cb".to_string()),
595 callback_secret: None,
596 parent_run_id: parent_run_id.map(str::to_string),
597 children: children.iter().map(|s| s.to_string()).collect(),
598 depth,
599 max_child_depth,
600 flags: leviath_core::run_meta::RunFlags {
603 modified_files: vec!["src/a.rs".to_string()],
604 modified_file_count: 1,
605 no_output_tools: true,
610 ..Default::default()
611 },
612 yolo: false,
613 read_paths: None,
614 };
615 std::fs::write(dir.join("meta.json"), serde_json::to_string(&meta).unwrap()).unwrap();
616 if let Some(ctx) = context {
617 std::fs::write(
618 dir.join("context.json"),
619 serde_json::to_string(ctx).unwrap(),
620 )
621 .unwrap();
622 }
623 }
624
625 fn agent_dir() -> tempfile::TempDir {
626 let dir = tempfile::tempdir().unwrap();
627 std::fs::write(dir.path().join("agent.leviath"), coder_manifest()).unwrap();
628 dir
629 }
630
631 fn write_run_archive(
635 runs_dir: &Path,
636 run_id: &str,
637 agent_path: &str,
638 stage_index: usize,
639 iteration: usize,
640 prompt_tokens: usize,
641 context: &ContextSnapshot,
642 ) {
643 use leviath_core::run_archive::{self, RunIdentity, RunRecord};
644 let dir = runs_dir.join(run_id);
645 std::fs::create_dir_all(&dir).unwrap();
646 let mut meta = RunMeta::new(
647 run_id.to_string(),
648 "coder".to_string(),
649 agent_path.to_string(),
650 "resume me".to_string(),
651 None,
652 std::env::temp_dir().to_string_lossy().to_string(),
653 1,
654 );
655 meta.status = RunStatus::Running;
656 meta.current_stage = "implement".to_string();
657 meta.stage_index = stage_index;
658 meta.iteration = iteration;
659 meta.prompt_tokens = prompt_tokens;
660 let mut buf = Vec::new();
661 run_archive::write_archive_start(&mut buf, run_archive::RUN_ARCHIVE_VERSION).unwrap();
662 run_archive::write_record(
663 &mut buf,
664 &RunRecord::Header {
665 identity: RunIdentity {
666 run_id: run_id.to_string(),
667 machine_id: "m".to_string(),
668 world_id: "w".to_string(),
669 created_at: 1,
670 },
671 meta: Box::new(meta),
672 },
673 )
674 .unwrap();
675 run_archive::write_record(
676 &mut buf,
677 &RunRecord::ContextCheckpoint {
678 snapshot: context.clone(),
679 at: 2,
680 },
681 )
682 .unwrap();
683 std::fs::write(dir.join("run.lvr"), &buf).unwrap();
684 }
685
686 #[tokio::test]
689 async fn reload_keeps_a_paused_run_paused() {
690 let agent = agent_dir();
691 let manifest = agent.path().join("agent.leviath");
692 let runs = tempfile::tempdir().unwrap();
693 write_run(
694 runs.path(),
695 "run-paused",
696 manifest.to_str().unwrap(),
697 RunStatus::Paused,
698 None,
699 );
700
701 let (mut world, cli) = test_world();
702 let hub = InteractionHub::new();
703 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
704 let restored = reload_persisted_agents(
705 &mut world,
706 cli.as_ref(),
707 &Config::default(),
708 mcp,
709 &[],
710 &hub,
711 runs.path(),
712 999,
713 &sub_tx(),
714 );
715
716 assert_eq!(restored.len(), 1);
717 let (run_id, entity) = &restored[0];
718 assert_eq!(run_id, "run-paused");
719 assert_eq!(world.agent_status(*entity), Some(AgentStatus::Paused));
720 }
721
722 async fn reload_single(runs: &Path, run_id: &str) -> (PipelineWorld, Entity) {
724 let (mut world, cli) = test_world();
725 let restored = reload_persisted_agents(
726 &mut world,
727 cli.as_ref(),
728 &Config::default(),
729 Arc::new(Mutex::new(ToolExecutor::new())),
730 &[],
731 &InteractionHub::new(),
732 runs,
733 999,
734 &sub_tx(),
735 );
736 assert_eq!(restored.len(), 1);
737 assert_eq!(restored[0].0, run_id);
738 let entity = restored[0].1;
739 (world, entity)
740 }
741
742 #[tokio::test]
746 async fn reload_keeps_an_unattended_run_unattended() {
747 let agent = agent_dir();
748 let manifest = agent.path().join("agent.leviath");
749 let runs = tempfile::tempdir().unwrap();
750 write_run(
751 runs.path(),
752 "run-yolo",
753 manifest.to_str().unwrap(),
754 RunStatus::Running,
755 None,
756 );
757 let meta_path = runs.path().join("run-yolo").join("meta.json");
759 let mut meta: RunMeta =
760 serde_json::from_str(&std::fs::read_to_string(&meta_path).unwrap()).unwrap();
761 meta.yolo = true;
762 std::fs::write(&meta_path, serde_json::to_string(&meta).unwrap()).unwrap();
763
764 let (world, entity) = reload_single(runs.path(), "run-yolo").await;
765 assert!(
766 world
767 .world()
768 .get::<RunMetadata>(entity)
769 .expect("reloaded run has metadata")
770 .unattended
771 );
772 assert!(
773 world
774 .world()
775 .get::<leviath_runtime::components::InteractionAutoApprove>(entity)
776 .is_some(),
777 "an unattended reload still auto-approves its checkpoints"
778 );
779 }
780
781 #[tokio::test]
784 async fn reload_does_not_invent_unattended() {
785 let agent = agent_dir();
786 let manifest = agent.path().join("agent.leviath");
787 let runs = tempfile::tempdir().unwrap();
788 write_run(
789 runs.path(),
790 "run-plain",
791 manifest.to_str().unwrap(),
792 RunStatus::Running,
793 None,
794 );
795 let meta_path = runs.path().join("run-plain").join("meta.json");
797 let mut raw: serde_json::Value =
798 serde_json::from_str(&std::fs::read_to_string(&meta_path).unwrap()).unwrap();
799 raw.as_object_mut().unwrap().remove("yolo");
800 std::fs::write(&meta_path, serde_json::to_string(&raw).unwrap()).unwrap();
801
802 let (world, entity) = reload_single(runs.path(), "run-plain").await;
803 assert!(
804 !world
805 .world()
806 .get::<RunMetadata>(entity)
807 .expect("reloaded run has metadata")
808 .unattended
809 );
810 }
811
812 #[tokio::test]
813 async fn reloads_nonterminal_runs_and_restores_state() {
814 let agent = agent_dir();
815 let manifest = agent.path().join("agent.leviath");
816 let runs = tempfile::tempdir().unwrap();
817
818 let ctx = ContextSnapshot {
820 stage_name: "implement".to_string(),
821 total_tokens: 4,
822 max_tokens: 100_000,
823 regions: vec![leviath_core::run_meta::RegionSnapshot {
824 name: "conversation".to_string(),
825 kind: "clearable".to_string(),
826 current_tokens: 4,
827 max_tokens: 100_000,
828 entries: vec![leviath_core::run_meta::RegionEntrySnapshot {
829 content: "earlier turn".to_string(),
830 tokens: 4,
831 kind: leviath_core::region::EntryKind::UserMessage,
832 metadata: None,
833 key: None,
834 taint: Default::default(),
835 }],
836 }],
837 };
838 write_run(
839 runs.path(),
840 "run-live",
841 manifest.to_str().unwrap(),
842 RunStatus::Running,
843 Some(&ctx),
844 );
845 write_run(
847 runs.path(),
848 "run-done",
849 manifest.to_str().unwrap(),
850 RunStatus::Complete,
851 None,
852 );
853
854 let (mut world, cli) = test_world();
855 let hub = InteractionHub::new();
856 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
857 let restored = reload_persisted_agents(
858 &mut world,
859 cli.as_ref(),
860 &Config::default(),
861 mcp,
862 &[],
863 &hub,
864 runs.path(),
865 999,
866 &sub_tx(),
867 );
868
869 assert_eq!(restored.len(), 1);
870 let (run_id, entity) = &restored[0];
871 assert_eq!(run_id, "run-live");
872 assert_eq!(world.agent_status(*entity), Some(AgentStatus::Active));
873 let md = world.world().get::<RunMetadata>(*entity).unwrap();
875 assert_eq!(md.started_at, 111);
876 assert_eq!(md.title.as_deref(), Some("Resume Me"));
877 assert_eq!(md.callback_url.as_deref(), Some("http://cb"));
878 let totals = world.world().get::<TokenTotals>(*entity).unwrap();
879 assert_eq!(totals.prompt_tokens, 42);
880 assert_eq!(totals.tool_calls, 3);
881 let flags = world
884 .world()
885 .get::<leviath_runtime::persistence::RunOutcomeFlags>(*entity)
886 .unwrap();
887 assert_eq!(flags.0.modified_files, vec!["src/a.rs".to_string()]);
888 assert_eq!(flags.0.modified_file_count, 1);
889 assert!(flags.0.no_output_tools);
892 }
893
894 fn assert_restored_from_archive(world: &PipelineWorld, entity: Entity) {
897 use leviath_runtime::components::AgentState;
898 let state = world.world().get::<AgentState>(entity).unwrap();
899 assert_eq!(state.current_stage, "fresh-stage");
902 assert_eq!(state.iteration, 9);
903 let totals = world.world().get::<TokenTotals>(entity).unwrap();
905 assert_eq!(totals.prompt_tokens, 99);
906 }
907
908 #[tokio::test]
913 async fn reload_prefers_the_atomic_journal_over_a_stale_context_json() {
914 let agent = agent_dir();
915 let manifest = agent.path().join("agent.leviath");
916 let mpath = manifest.to_str().unwrap();
917 let runs = tempfile::tempdir().unwrap();
918
919 let stale = ContextSnapshot {
923 stage_name: "stale-stage".to_string(),
924 total_tokens: 1,
925 max_tokens: 100,
926 regions: vec![],
927 };
928 write_run(
929 runs.path(),
930 "run-torn",
931 mpath,
932 RunStatus::Running,
933 Some(&stale),
934 );
935 let fresh = ContextSnapshot {
938 stage_name: "fresh-stage".to_string(),
939 total_tokens: 4,
940 max_tokens: 100_000,
941 regions: vec![],
942 };
943 write_run_archive(runs.path(), "run-torn", mpath, 0, 9, 99, &fresh);
944
945 let (mut world, cli) = test_world();
946 let hub = InteractionHub::new();
947 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
948 let restored = reload_persisted_agents(
949 &mut world,
950 cli.as_ref(),
951 &Config::default(),
952 mcp,
953 &[],
954 &hub,
955 runs.path(),
956 999,
957 &sub_tx(),
958 );
959
960 assert_eq!(restored.len(), 1);
961 assert_restored_from_archive(&world, restored[0].1);
962 }
963
964 #[tokio::test]
968 async fn reload_tolerates_a_torn_journal_tail() {
969 let agent = agent_dir();
970 let manifest = agent.path().join("agent.leviath");
971 let mpath = manifest.to_str().unwrap();
972 let runs = tempfile::tempdir().unwrap();
973
974 let stale = ContextSnapshot {
975 stage_name: "stale-stage".to_string(),
976 total_tokens: 1,
977 max_tokens: 100,
978 regions: vec![],
979 };
980 write_run(
981 runs.path(),
982 "run-torn2",
983 mpath,
984 RunStatus::Running,
985 Some(&stale),
986 );
987 let fresh = ContextSnapshot {
988 stage_name: "fresh-stage".to_string(),
989 total_tokens: 4,
990 max_tokens: 100_000,
991 regions: vec![],
992 };
993 write_run_archive(runs.path(), "run-torn2", mpath, 0, 9, 99, &fresh);
994 {
996 use std::io::Write;
997 let mut f = std::fs::OpenOptions::new()
998 .append(true)
999 .open(runs.path().join("run-torn2/run.lvr"))
1000 .unwrap();
1001 f.write_all(&[0, 0, 0, 0, 0, 0, 0, 10, 1, 2]).unwrap();
1002 }
1003
1004 let (mut world, cli) = test_world();
1005 let hub = InteractionHub::new();
1006 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1007 let restored = reload_persisted_agents(
1008 &mut world,
1009 cli.as_ref(),
1010 &Config::default(),
1011 mcp,
1012 &[],
1013 &hub,
1014 runs.path(),
1015 999,
1016 &sub_tx(),
1017 );
1018
1019 assert_eq!(restored.len(), 1);
1020 assert_restored_from_archive(&world, restored[0].1);
1022 }
1023
1024 fn append_archive_records(
1027 runs_dir: &Path,
1028 run_id: &str,
1029 records: &[leviath_core::run_archive::RunRecord],
1030 ) {
1031 use std::io::Write;
1032 let mut buf = Vec::new();
1033 for r in records {
1034 leviath_core::run_archive::write_record(&mut buf, r).unwrap();
1035 }
1036 let mut f = std::fs::OpenOptions::new()
1037 .append(true)
1038 .open(runs_dir.join(run_id).join("run.lvr"))
1039 .unwrap();
1040 f.write_all(&buf).unwrap();
1041 }
1042
1043 fn batch_call(
1044 id: &str,
1045 name: &str,
1046 result: Option<&str>,
1047 ) -> leviath_core::run_archive::ToolCallRecord {
1048 leviath_core::run_archive::ToolCallRecord {
1049 id: id.to_string(),
1050 name: name.to_string(),
1051 arguments: "{}".to_string(),
1052 result: result.map(str::to_string),
1053 thought_signature: None,
1054 }
1055 }
1056
1057 fn conversation_of(world: &PipelineWorld, entity: Entity) -> Vec<leviath_core::RegionEntry> {
1059 world
1060 .world()
1061 .get::<leviath_runtime::components::ContextWindow>(entity)
1062 .unwrap()
1063 .get_region("conversation")
1064 .unwrap()
1065 .content
1066 .clone()
1067 }
1068
1069 #[tokio::test]
1075 async fn reload_replays_a_pending_tool_batch_instead_of_reexecuting() {
1076 use leviath_core::run_archive::RunRecord;
1077 let agent = agent_dir();
1078 let manifest = agent.path().join("agent.leviath");
1079 let mpath = manifest.to_str().unwrap();
1080 let runs = tempfile::tempdir().unwrap();
1081
1082 write_run(runs.path(), "run-batch", mpath, RunStatus::Running, None);
1083 let ctx = ContextSnapshot {
1084 stage_name: "implement".to_string(),
1085 total_tokens: 0,
1086 max_tokens: 100_000,
1087 regions: vec![],
1088 };
1089 write_run_archive(runs.path(), "run-batch", mpath, 0, 9, 99, &ctx);
1090 append_archive_records(
1091 runs.path(),
1092 "run-batch",
1093 &[
1094 RunRecord::ToolBatch {
1095 calls: vec![
1096 batch_call("c_done", "write_file", None),
1097 batch_call("c_lost", "shell", None),
1098 ],
1099 at: 3,
1100 stage_index: 0,
1101 iteration: 9,
1102 response: "writing then running".to_string(),
1103 },
1104 RunRecord::ToolCallDone {
1105 iteration: 9,
1106 call_id: "c_done".to_string(),
1107 result: "Wrote 42 bytes to x.txt".to_string(),
1108 at: 4,
1109 },
1110 ],
1111 );
1112
1113 let (mut world, cli) = test_world();
1114 let hub = InteractionHub::new();
1115 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1116 let restored = reload_persisted_agents(
1117 &mut world,
1118 cli.as_ref(),
1119 &Config::default(),
1120 mcp,
1121 &[],
1122 &hub,
1123 runs.path(),
1124 999,
1125 &sub_tx(),
1126 );
1127
1128 assert_eq!(restored.len(), 1);
1129 let entity = restored[0].1;
1130 let entries = conversation_of(&world, entity);
1131 assert!(entries.iter().any(|e| matches!(
1133 &e.kind,
1134 leviath_core::region::EntryKind::AssistantTurn { tool_calls } if tool_calls.len() == 2
1135 )));
1136 assert!(
1138 entries
1139 .iter()
1140 .any(|e| e.content == "Wrote 42 bytes to x.txt")
1141 );
1142 assert!(entries.iter().any(|e| e.content.contains("interrupted")
1144 && e.content.contains("Verify whether it took effect")));
1145 assert!(
1147 world
1148 .world()
1149 .get::<leviath_runtime::pipeline::ReadyToInfer>(entity)
1150 .is_some()
1151 );
1152 }
1153
1154 #[tokio::test]
1158 async fn reload_does_not_replay_a_batch_already_in_the_window() {
1159 use leviath_core::region::EntryKind;
1160 use leviath_core::run_archive::RunRecord;
1161 let agent = agent_dir();
1162 let manifest = agent.path().join("agent.leviath");
1163 let mpath = manifest.to_str().unwrap();
1164 let runs = tempfile::tempdir().unwrap();
1165
1166 write_run(runs.path(), "run-applied", mpath, RunStatus::Running, None);
1167 let ctx = ContextSnapshot {
1169 stage_name: "implement".to_string(),
1170 total_tokens: 2,
1171 max_tokens: 100_000,
1172 regions: vec![leviath_core::run_meta::RegionSnapshot {
1173 name: "conversation".to_string(),
1174 kind: "clearable".to_string(),
1175 current_tokens: 2,
1176 max_tokens: 100_000,
1177 entries: vec![
1178 leviath_core::run_meta::RegionEntrySnapshot {
1179 content: "done".to_string(),
1180 tokens: 1,
1181 kind: EntryKind::AssistantTurn {
1182 tool_calls: vec![leviath_core::region::SerializedToolCall {
1183 id: "c1".to_string(),
1184 name: "write_file".to_string(),
1185 arguments: serde_json::Value::Null,
1186 thought_signature: None,
1187 }],
1188 },
1189 metadata: None,
1190 key: None,
1191 taint: Default::default(),
1192 },
1193 leviath_core::run_meta::RegionEntrySnapshot {
1194 content: "Wrote it".to_string(),
1195 tokens: 1,
1196 kind: EntryKind::ToolResult {
1197 tool_call_id: "c1".to_string(),
1198 tool_name: "write_file".to_string(),
1199 is_error: false,
1200 },
1201 metadata: None,
1202 key: None,
1203 taint: Default::default(),
1204 },
1205 ],
1206 }],
1207 };
1208 write_run_archive(runs.path(), "run-applied", mpath, 0, 9, 99, &ctx);
1209 append_archive_records(
1210 runs.path(),
1211 "run-applied",
1212 &[RunRecord::ToolBatch {
1213 calls: vec![batch_call("c1", "write_file", None)],
1214 at: 3,
1215 stage_index: 0,
1216 iteration: 9,
1217 response: "done".to_string(),
1218 }],
1219 );
1220
1221 let (mut world, cli) = test_world();
1222 let hub = InteractionHub::new();
1223 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1224 let restored = reload_persisted_agents(
1225 &mut world,
1226 cli.as_ref(),
1227 &Config::default(),
1228 mcp,
1229 &[],
1230 &hub,
1231 runs.path(),
1232 999,
1233 &sub_tx(),
1234 );
1235
1236 assert_eq!(restored.len(), 1);
1237 let entries = conversation_of(&world, restored[0].1);
1238 assert_eq!(
1240 entries
1241 .iter()
1242 .filter(|e| matches!(&e.kind, EntryKind::AssistantTurn { tool_calls } if !tool_calls.is_empty()))
1243 .count(),
1244 1
1245 );
1246 assert!(!entries.iter().any(|e| e.content.contains("interrupted")));
1247 }
1248
1249 fn interactive_agent_dir() -> tempfile::TempDir {
1252 let dir = tempfile::tempdir().unwrap();
1253 std::fs::write(
1254 dir.path().join("agent.leviath"),
1255 crate::test_support::inline_interactive_manifest(),
1256 )
1257 .unwrap();
1258 dir
1259 }
1260
1261 #[tokio::test]
1262 async fn reload_resumes_a_blocked_interaction_point_in_the_waiting_state() {
1263 let agent = interactive_agent_dir();
1264 let manifest = agent.path().join("agent.leviath");
1265 let runs = tempfile::tempdir().unwrap();
1266
1267 write_run(
1269 runs.path(),
1270 "run-await",
1271 manifest.to_str().unwrap(),
1272 RunStatus::WaitingInput,
1273 None,
1274 );
1275 std::fs::write(
1277 runs.path().join("run-await/interactions.json"),
1278 serde_json::to_string(&InteractionPointState {
1279 cursor: 0,
1280 round: 0,
1281 body: "## Plan\n1. do it".to_string(),
1282 })
1283 .unwrap(),
1284 )
1285 .unwrap();
1286
1287 let (mut world, cli) = test_world();
1288 let hub = InteractionHub::new();
1289 world.insert_interaction_hub(hub.clone()); let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1291 let restored = reload_persisted_agents(
1292 &mut world,
1293 cli.as_ref(),
1294 &Config::default(),
1295 mcp,
1296 &[],
1297 &hub,
1298 runs.path(),
1299 999,
1300 &sub_tx(),
1301 );
1302
1303 assert_eq!(restored.len(), 1);
1304 let (run_id, entity) = &restored[0];
1305 assert_eq!(run_id, "run-await");
1306 assert_eq!(world.agent_status(*entity), Some(AgentStatus::Waiting));
1309 assert!(
1310 world
1311 .world()
1312 .get::<leviath_runtime::interaction_points::AwaitingInteractionPoint>(*entity)
1313 .is_some()
1314 );
1315 assert!(
1316 world
1317 .world()
1318 .get::<leviath_runtime::pipeline::ReadyToInfer>(*entity)
1319 .is_none(),
1320 "the spawn-set ReadyToInfer is cleared so the inference lane won't fire"
1321 );
1322
1323 for _ in 0..8 {
1325 tokio::task::yield_now().await;
1326 }
1327 let pending = hub.pending();
1328 assert_eq!(pending.len(), 1);
1329 assert_eq!(pending[0].0, "run-await");
1330 assert_eq!(pending[0].1.body.as_deref(), Some("## Plan\n1. do it"));
1331 }
1332
1333 #[tokio::test]
1334 async fn reload_restores_actionable_runs_before_blocked_and_skips_terminal() {
1335 let agent = agent_dir();
1336 let mpath = agent.path().join("agent.leviath");
1337 let mpath = mpath.to_str().unwrap();
1338 let runs = tempfile::tempdir().unwrap();
1339 write_run(
1342 runs.path(),
1343 "aaa-blocked",
1344 mpath,
1345 RunStatus::WaitingInput,
1346 None,
1347 );
1348 write_run(runs.path(), "zzz-active", mpath, RunStatus::Running, None);
1349 write_run(runs.path(), "mmm-done", mpath, RunStatus::Complete, None);
1350
1351 let (mut world, cli) = test_world();
1352 let hub = InteractionHub::new();
1353 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1354 let restored = reload_persisted_agents(
1355 &mut world,
1356 cli.as_ref(),
1357 &Config::default(),
1358 mcp,
1359 &[],
1360 &hub,
1361 runs.path(),
1362 999,
1363 &sub_tx(),
1364 );
1365
1366 let order: Vec<&str> = restored.iter().map(|(id, _)| id.as_str()).collect();
1368 assert_eq!(order, vec!["zzz-active", "aaa-blocked"]);
1369 }
1370
1371 #[tokio::test]
1372 async fn reload_run_pages_in_nonterminal_only() {
1373 let agent = agent_dir();
1374 let manifest = agent.path().join("agent.leviath");
1375 let mpath = manifest.to_str().unwrap();
1376 let runs = tempfile::tempdir().unwrap();
1377 write_run(runs.path(), "live", mpath, RunStatus::Running, None);
1378 write_run(runs.path(), "done", mpath, RunStatus::Complete, None);
1379
1380 let (mut world, cli) = test_world();
1381 let hub = InteractionHub::new();
1382 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1383
1384 assert!(
1386 reload_run(
1387 &mut world,
1388 cli.as_ref(),
1389 &Config::default(),
1390 mcp.clone(),
1391 &[],
1392 &hub,
1393 "live",
1394 runs.path(),
1395 1,
1396 &sub_tx(),
1397 )
1398 .is_some()
1399 );
1400 assert!(
1402 reload_run(
1403 &mut world,
1404 cli.as_ref(),
1405 &Config::default(),
1406 mcp.clone(),
1407 &[],
1408 &hub,
1409 "done",
1410 runs.path(),
1411 1,
1412 &sub_tx(),
1413 )
1414 .is_none()
1415 );
1416 assert!(
1418 reload_run(
1419 &mut world,
1420 cli.as_ref(),
1421 &Config::default(),
1422 mcp,
1423 &[],
1424 &hub,
1425 "no-such-run",
1426 runs.path(),
1427 1,
1428 &sub_tx(),
1429 )
1430 .is_none()
1431 );
1432 }
1433
1434 #[tokio::test]
1435 async fn resumes_a_parent_parked_mid_fan_out() {
1436 use leviath_core::blueprint::{FanOutConfig, WorkerFailurePolicy};
1437 use leviath_runtime::fanout::{FanOutState, FanOutWaiting};
1438
1439 let agent = agent_dir();
1440 let manifest = agent.path().join("agent.leviath");
1441 let mpath = manifest.to_str().unwrap();
1442 let runs = tempfile::tempdir().unwrap();
1443
1444 write_run(
1446 runs.path(),
1447 "parent-fo",
1448 mpath,
1449 RunStatus::WaitingInput,
1450 None,
1451 );
1452 let state = FanOutState {
1453 config: FanOutConfig {
1454 worker_agent: None,
1455 worker_stage: Some("w".to_string()),
1456 worker_query: None,
1457 merge_stage: None,
1458 max_workers: 1,
1459 on_worker_failure: WorkerFailurePolicy::Continue,
1460 split_prompt: "s".to_string(),
1461 },
1462 max_workers: 1,
1463 pending: vec![],
1464 active: vec![("item-1".to_string(), "worker-fo".to_string())],
1467 summaries: vec![],
1468 failures: vec![],
1469 };
1470 std::fs::write(
1471 runs.path().join("parent-fo").join("fanout.json"),
1472 serde_json::to_string(&state).unwrap(),
1473 )
1474 .unwrap();
1475 write_run(runs.path(), "worker-fo", mpath, RunStatus::Running, None);
1477
1478 write_run(runs.path(), "bad-fo", mpath, RunStatus::WaitingInput, None);
1480 std::fs::write(runs.path().join("bad-fo").join("fanout.json"), b"garbage").unwrap();
1481
1482 let (mut world, cli) = test_world();
1483 let hub = InteractionHub::new();
1484 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1485 let restored = reload_persisted_agents(
1486 &mut world,
1487 cli.as_ref(),
1488 &Config::default(),
1489 mcp,
1490 &[],
1491 &hub,
1492 runs.path(),
1493 999,
1494 &sub_tx(),
1495 );
1496 let by_id: std::collections::HashMap<_, _> =
1497 restored.iter().map(|(r, e)| (r.clone(), *e)).collect();
1498
1499 assert!(
1501 world
1502 .world()
1503 .get::<FanOutWaiting>(by_id["parent-fo"])
1504 .is_some()
1505 );
1506 assert!(
1507 world
1508 .world()
1509 .get::<FanOutWaiting>(by_id["bad-fo"])
1510 .is_none()
1511 );
1512 }
1513
1514 #[tokio::test]
1515 async fn rebuilds_parent_child_tree_on_reload() {
1516 use leviath_runtime::components::{ParentRef, SubAgentChildren};
1517
1518 let agent = agent_dir();
1519 let manifest = agent.path().join("agent.leviath");
1520 let mpath = manifest.to_str().unwrap();
1521 let runs = tempfile::tempdir().unwrap();
1522
1523 write_run_tree(
1525 runs.path(),
1526 "parent",
1527 mpath,
1528 RunStatus::WaitingInput,
1529 None,
1530 None,
1531 &["child-a", "child-b"],
1532 0,
1533 4,
1534 );
1535 write_run_tree(
1536 runs.path(),
1537 "child-a",
1538 mpath,
1539 RunStatus::Running,
1540 None,
1541 Some("parent"),
1542 &[],
1543 1,
1544 0,
1545 );
1546 write_run_tree(
1547 runs.path(),
1548 "child-b",
1549 mpath,
1550 RunStatus::Running,
1551 None,
1552 Some("parent"),
1553 &[],
1554 1,
1555 0,
1556 );
1557
1558 let (mut world, cli) = test_world();
1559 let hub = InteractionHub::new();
1560 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1561 let restored = reload_persisted_agents(
1562 &mut world,
1563 cli.as_ref(),
1564 &Config::default(),
1565 mcp,
1566 &[],
1567 &hub,
1568 runs.path(),
1569 999,
1570 &sub_tx(),
1571 );
1572 assert_eq!(restored.len(), 3);
1573 let by_id: std::collections::HashMap<_, _> =
1574 restored.iter().map(|(r, e)| (r.clone(), *e)).collect();
1575 let parent = by_id["parent"];
1576 let child_a = by_id["child-a"];
1577 let child_b = by_id["child-b"];
1578
1579 let kids = world.world().get::<SubAgentChildren>(parent).unwrap();
1581 assert_eq!(kids.max_child_depth, 4);
1582 assert_eq!(kids.children.len(), 2);
1583 assert!(kids.children.contains(&child_a) && kids.children.contains(&child_b));
1584 let pr = world.world().get::<ParentRef>(child_a).unwrap();
1586 assert_eq!(pr.parent_entity, parent);
1587 assert_eq!(pr.parent_agent_id, "parent");
1588 assert_eq!(pr.depth, 1);
1589 let state = world
1591 .world()
1592 .get::<leviath_runtime::components::AgentState>(parent)
1593 .unwrap();
1594 assert_eq!(state.spawned_children_ids, vec!["child-a", "child-b"]);
1595 }
1596
1597 #[tokio::test]
1598 async fn relink_skips_children_and_parents_that_did_not_reload() {
1599 use leviath_runtime::components::{ParentRef, SubAgentChildren};
1600
1601 let agent = agent_dir();
1602 let manifest = agent.path().join("agent.leviath");
1603 let mpath = manifest.to_str().unwrap();
1604 let runs = tempfile::tempdir().unwrap();
1605
1606 write_run_tree(
1608 runs.path(),
1609 "lonely-parent",
1610 mpath,
1611 RunStatus::WaitingInput,
1612 None,
1613 None,
1614 &["gone-child"],
1615 0,
1616 2,
1617 );
1618 write_run_tree(
1619 runs.path(),
1620 "gone-child",
1621 mpath,
1622 RunStatus::Complete, None,
1624 Some("lonely-parent"),
1625 &[],
1626 1,
1627 0,
1628 );
1629 write_run_tree(
1631 runs.path(),
1632 "orphan",
1633 mpath,
1634 RunStatus::Running,
1635 None,
1636 Some("gone-parent"),
1637 &[],
1638 1,
1639 0,
1640 );
1641 write_run_tree(
1642 runs.path(),
1643 "gone-parent",
1644 mpath,
1645 RunStatus::Error,
1646 None,
1647 None,
1648 &["orphan"],
1649 0,
1650 2,
1651 );
1652
1653 let (mut world, cli) = test_world();
1654 let hub = InteractionHub::new();
1655 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1656 let restored = reload_persisted_agents(
1657 &mut world,
1658 cli.as_ref(),
1659 &Config::default(),
1660 mcp,
1661 &[],
1662 &hub,
1663 runs.path(),
1664 999,
1665 &sub_tx(),
1666 );
1667 assert_eq!(restored.len(), 2);
1669 let by_id: std::collections::HashMap<_, _> =
1670 restored.iter().map(|(r, e)| (r.clone(), *e)).collect();
1671 assert!(
1673 world
1674 .world()
1675 .get::<SubAgentChildren>(by_id["lonely-parent"])
1676 .is_none()
1677 );
1678 assert!(world.world().get::<ParentRef>(by_id["orphan"]).is_none());
1680 }
1681
1682 #[tokio::test]
1683 async fn reload_without_context_json_still_resumes() {
1684 let agent = agent_dir();
1685 let manifest = agent.path().join("agent.leviath");
1686 let runs = tempfile::tempdir().unwrap();
1687 write_run(
1688 runs.path(),
1689 "run-nocontext",
1690 manifest.to_str().unwrap(),
1691 RunStatus::WaitingInput,
1692 None, );
1694
1695 let (mut world, cli) = test_world();
1696 let hub = InteractionHub::new();
1697 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1698 let restored = reload_persisted_agents(
1699 &mut world,
1700 cli.as_ref(),
1701 &Config::default(),
1702 mcp,
1703 &[],
1704 &hub,
1705 runs.path(),
1706 999,
1707 &sub_tx(),
1708 );
1709 assert_eq!(restored.len(), 1);
1710 assert!(world.world().get::<TokenTotals>(restored[0].1).is_some());
1711 }
1712
1713 #[tokio::test]
1714 async fn skips_missing_dir_junk_and_unreloadable_runs() {
1715 let (mut world, cli) = test_world();
1717 let hub = InteractionHub::new();
1718 let mcp = Arc::new(Mutex::new(ToolExecutor::new()));
1719 assert!(
1720 reload_persisted_agents(
1721 &mut world,
1722 cli.as_ref(),
1723 &Config::default(),
1724 mcp.clone(),
1725 &[],
1726 &hub,
1727 std::path::Path::new("/no/such/runs/dir"),
1728 1,
1729 &sub_tx(),
1730 )
1731 .is_empty()
1732 );
1733
1734 let runs = tempfile::tempdir().unwrap();
1737 std::fs::create_dir_all(runs.path().join("no-meta")).unwrap();
1738 let corrupt = runs.path().join("corrupt");
1739 std::fs::create_dir_all(&corrupt).unwrap();
1740 std::fs::write(corrupt.join("meta.json"), "not json").unwrap();
1741 write_run(
1742 runs.path(),
1743 "run-badpath",
1744 "/no/such/agent.leviath",
1745 RunStatus::Running,
1746 None,
1747 );
1748
1749 let restored = reload_persisted_agents(
1750 &mut world,
1751 cli.as_ref(),
1752 &Config::default(),
1753 mcp,
1754 &[],
1755 &hub,
1756 runs.path(),
1757 1,
1758 &sub_tx(),
1759 );
1760 assert!(restored.is_empty()); let meta: RunMeta = serde_json::from_str(
1766 &std::fs::read_to_string(runs.path().join("run-badpath").join("meta.json")).unwrap(),
1767 )
1768 .unwrap();
1769 assert_eq!(meta.status, RunStatus::Error);
1770 let error = meta.error.unwrap_or_default();
1771 assert!(error.contains("could not be recovered"), "got: {error}");
1772 assert_eq!(meta.updated_at, 1);
1773 assert!(!runs.path().join("no-meta").join("meta.json").exists());
1775 assert_eq!(
1776 std::fs::read_to_string(corrupt.join("meta.json")).unwrap(),
1777 "not json"
1778 );
1779 }
1780
1781 #[test]
1782 fn marking_a_crash_is_best_effort() {
1783 let runs = tempfile::tempdir().unwrap();
1787 write_run(
1788 runs.path(),
1789 "run-x",
1790 "/no/such/agent.leviath",
1791 RunStatus::Running,
1792 None,
1793 );
1794 let meta = read_meta(&runs.path().join("run-x")).expect("written above");
1795 mark_crashed(&runs.path().join("gone"), meta, "boom", 7);
1796 assert!(!runs.path().join("gone").exists());
1797 }
1798
1799 #[tokio::test]
1800 async fn fake_provider_methods_are_exercised() {
1801 use leviath_providers::Provider;
1802 let p = FakeProvider;
1803 assert_eq!(p.name(), "fake");
1804 assert_eq!(p.count_tokens("t", "m").await, 1);
1805 assert_eq!(p.max_context_tokens("m"), 1000);
1806 let _ = p.capabilities("m");
1807 assert!(
1808 p.infer(leviath_providers::InferenceRequest {
1809 system: vec![],
1810 messages: vec![],
1811 model: "m".to_string(),
1812 max_tokens: 1,
1813 temperature: 0.0,
1814 tools: vec![],
1815 extra: serde_json::Value::Null,
1816 request_timeout_secs: None,
1817 })
1818 .await
1819 .is_err()
1820 );
1821 }
1822
1823 #[test]
1824 fn is_terminal_covers_all_statuses() {
1825 assert!(is_terminal(&RunStatus::Complete));
1826 assert!(is_terminal(&RunStatus::Cancelled));
1827 assert!(is_terminal(&RunStatus::Error));
1828 assert!(!is_terminal(&RunStatus::Running));
1829 assert!(!is_terminal(&RunStatus::WaitingInput));
1830 }
1831}