1use bevy_ecs::prelude::*;
19use leviath_core::region::RegionEntry;
20use leviath_core::run_meta::{ContextSnapshot, RunMeta, RunStatus};
21
22use crate::components::{AgentState, AgentStatus, ContextWindow};
23use crate::persistence::TokenTotals;
24use crate::pipeline::{StageCursor, StageInferences, StageSetups};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
29pub enum RestorePriority {
30 Blocked,
34 Active,
38}
39
40pub fn classify_restore(status: &RunStatus, parked_on_fanout: bool) -> Option<RestorePriority> {
49 match status {
50 RunStatus::Complete | RunStatus::Error | RunStatus::Cancelled => None,
51 _ if parked_on_fanout => Some(RestorePriority::Blocked),
52 RunStatus::Starting | RunStatus::Running => Some(RestorePriority::Active),
53 RunStatus::WaitingInput | RunStatus::CompleteInteractive | RunStatus::Paused => {
54 Some(RestorePriority::Blocked)
55 }
56 }
57}
58
59pub fn triage_restores(candidates: Vec<(RunMeta, bool)>) -> Vec<RunMeta> {
71 let mut ranked: Vec<(RestorePriority, RunMeta)> = candidates
72 .into_iter()
73 .filter_map(|(meta, parked)| {
74 classify_restore(&meta.status, parked).map(|prio| (prio, meta))
75 })
76 .collect();
77 ranked.sort_by(|(a_prio, a), (b_prio, b)| {
80 b_prio
81 .cmp(a_prio)
82 .then_with(|| b.updated_at.cmp(&a.updated_at))
83 });
84 ranked.into_iter().map(|(_, meta)| meta).collect()
85}
86
87pub fn restore_agent(
98 world: &mut World,
99 entity: Entity,
100 snapshot: &ContextSnapshot,
101 stage_index: usize,
102 iteration: usize,
103 totals: TokenTotals,
104) {
105 {
107 let mut window = world
108 .get_mut::<ContextWindow>(entity)
109 .expect("a spawned agent has a context window");
110 for snap_region in &snapshot.regions {
111 if let Some(region) = window
112 .regions
113 .iter_mut()
114 .find(|r| r.name == snap_region.name)
115 {
116 region.content = snap_region
117 .entries
118 .iter()
119 .map(|e| RegionEntry {
120 content: e.content.clone(),
121 tokens: e.tokens,
122 timestamp: 0,
123 metadata: e.metadata.clone(),
124 kind: e.kind.clone(),
125 key: e.key.clone(),
126 })
127 .collect();
128 if region.taint.is_some() {
136 region.taint = Some(leviath_core::taint::RegionTaint::from_entry_taints(
137 snap_region.entries.iter().map(|e| e.taint).collect(),
138 ));
139 }
140 region.current_tokens = region.content.iter().map(|e| e.tokens).sum();
141 }
142 }
143 window.current_tokens = window.calculate_tokens();
144 }
145
146 if let Some(inf) = world
149 .get::<StageInferences>(entity)
150 .expect("a spawned agent has stage inferences")
151 .0
152 .get(stage_index)
153 .cloned()
154 {
155 let setup = &world
156 .get::<StageSetups>(entity)
157 .expect("a spawned agent has stage setups")
158 .0[stage_index];
159 let cfg = setup.inference_config.clone();
160 let routing = setup.routing.clone();
161 world.entity_mut(entity).insert((inf, cfg));
162 match routing {
166 Some(routing) => {
167 world
168 .entity_mut(entity)
169 .insert(crate::components::ToolResultRoutingComponent { routing });
170 }
171 None => {
172 world
173 .entity_mut(entity)
174 .remove::<crate::components::ToolResultRoutingComponent>();
175 }
176 }
177 world
178 .get_mut::<StageCursor>(entity)
179 .expect("a spawned agent has a stage cursor")
180 .index = stage_index;
181 }
182
183 {
185 let mut state = world
186 .get_mut::<AgentState>(entity)
187 .expect("a spawned agent has state");
188 state.current_stage = snapshot.stage_name.clone();
189 state.iteration = iteration;
190 state.status = AgentStatus::Active;
191 }
192 world.entity_mut(entity).insert(totals);
193}
194
195pub const INTERRUPTED_TOOL_RESULT: &str = "[error] interrupted: the daemon restarted while this tool call was executing and its \
199 result was lost. Verify whether it took effect before re-running side-effecting work.";
200
201fn interrupted_result(tool_name: &str, children: &[String]) -> String {
206 if leviath_tools::is_subagent_tool(tool_name) && !children.is_empty() {
207 format!(
208 "{INTERRUPTED_TOOL_RESULT} This run already has child agent runs: {}; check them \
209 with check_agent before spawning again.",
210 children.join(", ")
211 )
212 } else {
213 INTERRUPTED_TOOL_RESULT.to_string()
214 }
215}
216
217pub fn restore_pending_batch(
236 world: &mut World,
237 entity: Entity,
238 batch: &leviath_core::run_archive::PendingToolBatch,
239 children: &[String],
240) {
241 let calls: Vec<crate::components::ToolCall> = batch
242 .calls
243 .iter()
244 .map(|c| crate::components::ToolCall {
245 tool_id: c.id.clone(),
246 name: c.name.clone(),
247 arguments: serde_json::from_str(&c.arguments)
251 .unwrap_or_else(|_| serde_json::Value::String(c.arguments.clone())),
252 thought_signature: c.thought_signature.clone(),
253 })
254 .collect();
255 let merged: Vec<(String, String)> = batch
256 .calls
257 .iter()
258 .map(|c| {
259 let result = c
260 .result
261 .clone()
262 .unwrap_or_else(|| interrupted_result(&c.name, children));
263 (c.id.clone(), result)
264 })
265 .collect();
266 let routing = world
267 .get::<crate::components::ToolResultRoutingComponent>(entity)
268 .map(|c| c.routing.clone());
269 let sensitivities = world
270 .get::<crate::pipeline::ToolSensitivities>(entity)
271 .map(|s| s.0.clone());
272 let mut window = world
273 .get_mut::<ContextWindow>(entity)
274 .expect("a spawned agent has a context window");
275 crate::pipeline::apply_tool_results(
276 &mut window,
277 &batch.response,
278 &calls,
279 &merged,
280 routing.as_ref(),
281 sensitivities.as_ref(),
282 );
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::components::InferenceConfig;
289 use crate::pipeline::{ReadyToInfer, StageInference, StageSetup};
290 use leviath_core::region::EntryKind;
291 use leviath_core::run_meta::{RegionEntrySnapshot, RegionSnapshot};
292 use leviath_core::{Region, RegionKind};
293
294 fn setup(temp: Option<f32>) -> StageSetup {
295 StageSetup {
296 inference_config: InferenceConfig {
297 temperature: temp,
298 max_output_tokens: None,
299 extra_params: Default::default(),
300 batch_tool_hint: false,
301 shell_hint: false,
302 request_timeout_secs: None,
303 },
304 routing: None,
305 accepts_messages: true,
306 context_layout: None,
307 system_prompt: None,
308 output: None,
309 }
310 }
311
312 fn si(model: &str) -> StageInference {
313 StageInference {
314 provider_name: "p".to_string(),
315 model: model.to_string(),
316 tools: vec![],
317 tool_filter: None,
318 fallbacks: Vec::new(),
319 output: None,
320 }
321 }
322
323 fn agent_world() -> (World, Entity) {
326 let mut world = World::new();
327 let mut window = ContextWindow::new(10_000);
328 window.add_region(Region::new(
329 "conversation".to_string(),
330 RegionKind::Clearable,
331 10_000,
332 ));
333 let _ = window.add_to_region("conversation", "fresh task seed".to_string(), 3);
334 let entity = world
335 .spawn((
336 window,
337 StageCursor { index: 0 },
338 AgentState {
339 agent_id: "a".to_string(),
340 current_stage: "s0".to_string(),
341 iteration: 0,
342 status: AgentStatus::Active,
343 spawned_children_ids: vec![],
344 pending_wait: None,
345 accepts_messages: true,
346 },
347 StageInferences(vec![si("m0"), si("m1")]),
348 StageSetups(vec![setup(None), setup(Some(0.5))]),
349 si("m0"),
350 setup(None).inference_config,
351 TokenTotals::default(),
352 ReadyToInfer,
353 ))
354 .id();
355 (world, entity)
356 }
357
358 fn snapshot() -> ContextSnapshot {
359 ContextSnapshot {
360 stage_name: "s1".to_string(),
361 total_tokens: 8,
362 max_tokens: 10_000,
363 regions: vec![
364 RegionSnapshot {
365 name: "conversation".to_string(),
366 kind: "clearable".to_string(),
367 current_tokens: 8,
368 max_tokens: 10_000,
369 entries: vec![
370 RegionEntrySnapshot {
371 content: "prior user turn".to_string(),
372 tokens: 5,
373 kind: EntryKind::UserMessage,
374 metadata: None,
375 key: None,
376 taint: Default::default(),
377 },
378 RegionEntrySnapshot {
379 content: "prior assistant".to_string(),
380 tokens: 3,
381 kind: EntryKind::AssistantTurn { tool_calls: vec![] },
382 metadata: None,
383 key: None,
384 taint: Default::default(),
385 },
386 ],
387 },
388 RegionSnapshot {
390 name: "ghost".to_string(),
391 kind: "pinned".to_string(),
392 current_tokens: 1,
393 max_tokens: 10,
394 entries: vec![RegionEntrySnapshot {
395 content: "orphan".to_string(),
396 tokens: 1,
397 kind: EntryKind::Text,
398 metadata: None,
399 key: None,
400 taint: Default::default(),
401 }],
402 },
403 ],
404 }
405 }
406
407 #[test]
413 fn restore_rebuilds_region_taint_from_the_persisted_entries() {
414 use leviath_core::taint::TaintLevel;
415
416 let mut snap = snapshot();
417 snap.regions[0].entries[0].taint = TaintLevel::Private;
418 snap.regions[0].entries[1].taint = TaintLevel::Public;
419
420 let (mut world, entity) = agent_world();
422 restore_agent(&mut world, entity, &snap, 1, 7, TokenTotals::default());
423 assert!(
424 world
425 .get::<ContextWindow>(entity)
426 .unwrap()
427 .get_region("conversation")
428 .unwrap()
429 .taint
430 .is_none()
431 );
432
433 let (mut world, entity) = agent_world();
435 world
436 .get_mut::<ContextWindow>(entity)
437 .unwrap()
438 .get_region_mut("conversation")
439 .unwrap()
440 .enable_taint_tracking();
441 restore_agent(&mut world, entity, &snap, 1, 7, TokenTotals::default());
442
443 let window = world.get::<ContextWindow>(entity).unwrap();
444 let region = window.get_region("conversation").unwrap();
445 assert_eq!(region.taint_level(), Some(TaintLevel::Private));
446 let taint = region.taint.as_ref().unwrap();
447 assert_eq!(taint.entry_taint(0), Some(TaintLevel::Private));
448 assert_eq!(taint.entry_taint(1), Some(TaintLevel::Public));
449 }
450
451 #[test]
452 fn restore_overlays_context_and_jumps_to_stage() {
453 let (mut world, entity) = agent_world();
454 restore_agent(
455 &mut world,
456 entity,
457 &snapshot(),
458 1,
459 7,
460 TokenTotals {
461 prompt_tokens: 100,
462 ..Default::default()
463 },
464 );
465
466 let window = world.get::<ContextWindow>(entity).unwrap();
468 let region = window.get_region("conversation").unwrap();
469 assert_eq!(region.content.len(), 2);
470 assert_eq!(region.content[0].content, "prior user turn");
471 assert_eq!(region.content[0].kind, EntryKind::UserMessage);
472 assert_eq!(region.current_tokens, 8);
473
474 assert_eq!(world.get::<StageCursor>(entity).unwrap().index, 1);
476 let state = world.get::<AgentState>(entity).unwrap();
477 assert_eq!(state.current_stage, "s1");
478 assert_eq!(state.iteration, 7);
479 assert_eq!(state.status, AgentStatus::Active);
480 assert_eq!(
481 world.get::<InferenceConfig>(entity).unwrap().temperature,
482 Some(0.5)
483 );
484 assert_eq!(world.get::<StageInference>(entity).unwrap().model, "m1");
485 assert_eq!(world.get::<TokenTotals>(entity).unwrap().prompt_tokens, 100);
486 assert!(world.get::<ReadyToInfer>(entity).is_some());
488 }
489
490 fn pending_call(
493 id: &str,
494 name: &str,
495 result: Option<&str>,
496 ) -> leviath_core::run_archive::ToolCallRecord {
497 leviath_core::run_archive::ToolCallRecord {
498 id: id.to_string(),
499 name: name.to_string(),
500 arguments: r#"{"path":"x.txt"}"#.to_string(),
501 result: result.map(str::to_string),
502 thought_signature: None,
503 }
504 }
505
506 fn pending_batch(
507 calls: Vec<leviath_core::run_archive::ToolCallRecord>,
508 ) -> leviath_core::run_archive::PendingToolBatch {
509 leviath_core::run_archive::PendingToolBatch {
510 stage_index: 1,
511 iteration: 7,
512 response: "writing then checking".to_string(),
513 calls,
514 }
515 }
516
517 fn conv_entries(world: &World, entity: Entity) -> Vec<RegionEntry> {
519 world
520 .get::<ContextWindow>(entity)
521 .unwrap()
522 .get_region("conversation")
523 .unwrap()
524 .content
525 .clone()
526 }
527
528 #[test]
529 fn pending_batch_replays_real_results_and_synthesizes_interrupted_ones() {
530 let (mut world, entity) = agent_world();
531 restore_agent(
532 &mut world,
533 entity,
534 &snapshot(),
535 1,
536 7,
537 TokenTotals::default(),
538 );
539 restore_pending_batch(
540 &mut world,
541 entity,
542 &pending_batch(vec![
543 pending_call("c1", "write_file", Some("Wrote 42 bytes to x.txt")),
544 pending_call("c2", "shell", None),
545 ]),
546 &[],
547 );
548
549 let entries = conv_entries(&world, entity);
550 let turn = entries
553 .iter()
554 .find_map(|e| match &e.kind {
555 EntryKind::AssistantTurn { tool_calls } if !tool_calls.is_empty() => {
556 Some(tool_calls.clone())
557 }
558 _ => None,
559 })
560 .expect("assistant turn appended");
561 assert_eq!(turn.len(), 2);
562 assert_eq!(turn[0].id, "c1");
563 assert_eq!(
564 turn[0].arguments,
565 serde_json::json!({"path": "x.txt"}),
566 "journaled arguments parsed back to JSON"
567 );
568 let result_of = |id: &str| {
569 entries
570 .iter()
571 .find(|e| {
572 matches!(&e.kind, EntryKind::ToolResult { tool_call_id, .. } if tool_call_id == id)
573 })
574 .map(|e| e.content.clone())
575 .expect("a result per call")
576 };
577 assert_eq!(result_of("c1"), "Wrote 42 bytes to x.txt");
578 assert!(result_of("c2").contains("interrupted"));
579 assert!(result_of("c2").contains("Verify whether it took effect"));
580 }
581
582 #[test]
583 fn pending_batch_survives_request_assembly_unstripped() {
584 let (mut world, entity) = agent_world();
589 world
590 .get_mut::<ContextWindow>(entity)
591 .unwrap()
592 .get_region_mut("conversation")
593 .unwrap()
594 .kind = RegionKind::SlidingWindow {
595 max_items: 100,
596 eviction_strategy: Default::default(),
597 };
598 restore_agent(
599 &mut world,
600 entity,
601 &snapshot(),
602 1,
603 7,
604 TokenTotals::default(),
605 );
606 restore_pending_batch(
607 &mut world,
608 entity,
609 &pending_batch(vec![pending_call("c1", "shell", None)]),
610 &[],
611 );
612
613 let assembled = world.get::<ContextWindow>(entity).unwrap().assemble();
614 let mut tool_uses = 0;
615 let mut tool_results = 0;
616 for msg in &assembled.messages {
617 if let leviath_providers::MessageContent::Blocks(blocks) = &msg.content {
618 for block in blocks {
619 match block {
620 leviath_providers::ContentBlock::ToolUse { id, .. } => {
621 assert_eq!(id, "c1");
622 tool_uses += 1;
623 }
624 leviath_providers::ContentBlock::ToolResult { tool_use_id, .. } => {
625 assert_eq!(tool_use_id, "c1");
626 tool_results += 1;
627 }
628 _ => {}
629 }
630 }
631 }
632 }
633 assert_eq!((tool_uses, tool_results), (1, 1), "nothing stripped");
634 }
635
636 #[test]
637 fn pending_batch_routes_results_through_the_restored_stage_routing() {
638 let (mut world, entity) = agent_world();
642 world
643 .get_mut::<ContextWindow>(entity)
644 .unwrap()
645 .add_region(Region::new(
646 "knowledge".to_string(),
647 RegionKind::Pinned,
648 10_000,
649 ));
650 world
651 .get_mut::<StageSetups>(entity)
652 .unwrap()
653 .0
654 .get_mut(1)
655 .unwrap()
656 .routing = Some(leviath_core::ToolResultRouting {
657 default_region: "knowledge".to_string(),
658 ..Default::default()
659 });
660 restore_agent(
661 &mut world,
662 entity,
663 &snapshot(),
664 1,
665 7,
666 TokenTotals::default(),
667 );
668 restore_pending_batch(
669 &mut world,
670 entity,
671 &pending_batch(vec![pending_call("c1", "read_file", Some("the file body"))]),
672 &[],
673 );
674
675 let window = world.get::<ContextWindow>(entity).unwrap();
676 let knowledge = window.get_region("knowledge").unwrap();
677 assert!(
678 knowledge
679 .content
680 .iter()
681 .any(|e| e.content.contains("the file body")),
682 "full text routed to the knowledge region"
683 );
684 assert!(
685 conv_entries(&world, entity).iter().any(
686 |e| matches!(&e.kind, EntryKind::ToolResult { tool_call_id, .. } if tool_call_id == "c1")
687 ),
688 "conversation keeps the paired pointer result"
689 );
690 }
691
692 #[test]
693 fn pending_batch_taints_results_per_tool_sensitivity() {
694 use leviath_core::taint::TaintLevel;
695 let (mut world, entity) = agent_world();
696 world
697 .get_mut::<ContextWindow>(entity)
698 .unwrap()
699 .get_region_mut("conversation")
700 .unwrap()
701 .enable_taint_tracking();
702 world
703 .entity_mut(entity)
704 .insert(crate::pipeline::ToolSensitivities(
705 [("read_file".to_string(), TaintLevel::Private)]
706 .into_iter()
707 .collect(),
708 ));
709 restore_agent(
710 &mut world,
711 entity,
712 &snapshot(),
713 1,
714 7,
715 TokenTotals::default(),
716 );
717 restore_pending_batch(
718 &mut world,
719 entity,
720 &pending_batch(vec![pending_call("c1", "read_file", Some("secret body"))]),
721 &[],
722 );
723
724 let window = world.get::<ContextWindow>(entity).unwrap();
725 assert_eq!(
726 window.get_region("conversation").unwrap().taint_level(),
727 Some(TaintLevel::Private),
728 "replayed result tainted like a live one"
729 );
730 }
731
732 #[test]
733 fn unparseable_journaled_arguments_survive_as_a_raw_string() {
734 let (mut world, entity) = agent_world();
735 restore_agent(
736 &mut world,
737 entity,
738 &snapshot(),
739 1,
740 7,
741 TokenTotals::default(),
742 );
743 let mut call = pending_call("c1", "shell", None);
744 call.arguments = "not json {".to_string();
745 restore_pending_batch(&mut world, entity, &pending_batch(vec![call]), &[]);
746
747 let entries = conv_entries(&world, entity);
748 let turn = entries
749 .iter()
750 .find_map(|e| match &e.kind {
751 EntryKind::AssistantTurn { tool_calls } if !tool_calls.is_empty() => {
752 Some(tool_calls.clone())
753 }
754 _ => None,
755 })
756 .expect("turn still lands");
757 assert_eq!(
758 turn[0].arguments,
759 serde_json::Value::String("not json {".to_string())
760 );
761 }
762
763 #[test]
764 fn interrupted_subagent_calls_point_at_known_children() {
765 let kids = vec!["run-kid-1".to_string(), "run-kid-2".to_string()];
769 let enriched = interrupted_result("spawn_agent", &kids);
770 assert!(enriched.contains("run-kid-1, run-kid-2"));
771 assert!(enriched.contains("check_agent"));
772 assert_eq!(interrupted_result("shell", &kids), INTERRUPTED_TOOL_RESULT);
773 assert_eq!(
774 interrupted_result("spawn_agent", &[]),
775 INTERRUPTED_TOOL_RESULT
776 );
777
778 let (mut world, entity) = agent_world();
780 restore_agent(
781 &mut world,
782 entity,
783 &snapshot(),
784 1,
785 7,
786 TokenTotals::default(),
787 );
788 restore_pending_batch(
789 &mut world,
790 entity,
791 &pending_batch(vec![pending_call("c1", "spawn_agent", None)]),
792 &kids,
793 );
794 assert!(
795 conv_entries(&world, entity)
796 .iter()
797 .any(|e| e.content.contains("already has child agent runs")),
798 "the synthesized sub-agent note lands in the window"
799 );
800 }
801
802 #[test]
803 fn restore_swaps_in_the_stage_routing_and_clears_stale() {
804 use crate::components::ToolResultRoutingComponent;
805
806 let (mut world, entity) = agent_world();
808 let routed = leviath_core::ToolResultRouting {
809 default_region: "knowledge".to_string(),
810 ..Default::default()
811 };
812 world
813 .get_mut::<StageSetups>(entity)
814 .unwrap()
815 .0
816 .get_mut(1)
817 .unwrap()
818 .routing = Some(routed);
819 restore_agent(
820 &mut world,
821 entity,
822 &snapshot(),
823 1,
824 7,
825 TokenTotals::default(),
826 );
827 assert_eq!(
828 world
829 .get::<ToolResultRoutingComponent>(entity)
830 .expect("stage 1's routing swapped in")
831 .routing
832 .default_region,
833 "knowledge"
834 );
835
836 let (mut world, entity) = agent_world();
839 world.entity_mut(entity).insert(ToolResultRoutingComponent {
840 routing: leviath_core::ToolResultRouting::default(),
841 });
842 restore_agent(
843 &mut world,
844 entity,
845 &snapshot(),
846 1,
847 7,
848 TokenTotals::default(),
849 );
850 assert!(world.get::<ToolResultRoutingComponent>(entity).is_none());
851 }
852
853 fn meta_with(run_id: &str, status: RunStatus, updated_at: i64) -> RunMeta {
854 let mut m = RunMeta::new(
855 run_id.to_string(),
856 "a".to_string(),
857 "/p".to_string(),
858 "t".to_string(),
859 None,
860 "/w".to_string(),
861 1,
862 );
863 m.status = status;
864 m.updated_at = updated_at;
865 m
866 }
867
868 #[test]
869 fn classify_restore_skips_terminal_and_ranks_the_rest() {
870 assert_eq!(classify_restore(&RunStatus::Complete, false), None);
872 assert_eq!(classify_restore(&RunStatus::Error, false), None);
873 assert_eq!(classify_restore(&RunStatus::Cancelled, false), None);
874 assert_eq!(
876 classify_restore(&RunStatus::Running, false),
877 Some(RestorePriority::Active)
878 );
879 assert_eq!(
880 classify_restore(&RunStatus::Starting, false),
881 Some(RestorePriority::Active)
882 );
883 assert_eq!(
885 classify_restore(&RunStatus::WaitingInput, false),
886 Some(RestorePriority::Blocked)
887 );
888 assert_eq!(
889 classify_restore(&RunStatus::Paused, false),
890 Some(RestorePriority::Blocked)
891 );
892 assert_eq!(
893 classify_restore(&RunStatus::CompleteInteractive, false),
894 Some(RestorePriority::Blocked)
895 );
896 assert_eq!(
898 classify_restore(&RunStatus::Running, true),
899 Some(RestorePriority::Blocked)
900 );
901 assert_eq!(classify_restore(&RunStatus::Complete, true), None);
903 }
904
905 #[test]
906 fn triage_orders_actionable_first_then_by_recency_and_drops_terminal() {
907 let candidates = vec![
908 (
909 meta_with("blocked-old", RunStatus::WaitingInput, 100),
910 false,
911 ),
912 (meta_with("active-old", RunStatus::Running, 200), false),
913 (meta_with("terminal", RunStatus::Complete, 999), false),
914 (meta_with("active-new", RunStatus::Starting, 300), false),
915 (meta_with("parked", RunStatus::Running, 999), true), (
917 meta_with("blocked-new", RunStatus::WaitingInput, 400),
918 false,
919 ),
920 ];
921 let order: Vec<String> = triage_restores(candidates)
922 .into_iter()
923 .map(|m| m.run_id)
924 .collect();
925 assert_eq!(
928 order,
929 vec![
930 "active-new".to_string(), "active-old".to_string(), "parked".to_string(), "blocked-new".to_string(), "blocked-old".to_string(), ]
936 );
937 }
938
939 #[test]
940 fn restore_with_out_of_range_stage_keeps_spawn_config() {
941 let (mut world, entity) = agent_world();
942 let mut snap = snapshot();
943 snap.stage_name = "s0".to_string();
944 restore_agent(&mut world, entity, &snap, 9, 2, TokenTotals::default());
946
947 assert_eq!(world.get::<StageCursor>(entity).unwrap().index, 0);
949 assert_eq!(world.get::<StageInference>(entity).unwrap().model, "m0");
950 assert_eq!(world.get::<AgentState>(entity).unwrap().iteration, 2);
952 assert_eq!(
953 world
954 .get::<ContextWindow>(entity)
955 .unwrap()
956 .get_region("conversation")
957 .unwrap()
958 .content
959 .len(),
960 2
961 );
962 }
963}