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 }
309 }
310
311 fn si(model: &str) -> StageInference {
312 StageInference {
313 provider_name: "p".to_string(),
314 model: model.to_string(),
315 tools: vec![],
316 tool_filter: None,
317 fallbacks: Vec::new(),
318 }
319 }
320
321 fn agent_world() -> (World, Entity) {
324 let mut world = World::new();
325 let mut window = ContextWindow::new(10_000);
326 window.add_region(Region::new(
327 "conversation".to_string(),
328 RegionKind::Clearable,
329 10_000,
330 ));
331 let _ = window.add_to_region("conversation", "fresh task seed".to_string(), 3);
332 let entity = world
333 .spawn((
334 window,
335 StageCursor { index: 0 },
336 AgentState {
337 agent_id: "a".to_string(),
338 current_stage: "s0".to_string(),
339 iteration: 0,
340 status: AgentStatus::Active,
341 spawned_children_ids: vec![],
342 pending_wait: None,
343 accepts_messages: true,
344 },
345 StageInferences(vec![si("m0"), si("m1")]),
346 StageSetups(vec![setup(None), setup(Some(0.5))]),
347 si("m0"),
348 setup(None).inference_config,
349 TokenTotals::default(),
350 ReadyToInfer,
351 ))
352 .id();
353 (world, entity)
354 }
355
356 fn snapshot() -> ContextSnapshot {
357 ContextSnapshot {
358 stage_name: "s1".to_string(),
359 total_tokens: 8,
360 max_tokens: 10_000,
361 regions: vec![
362 RegionSnapshot {
363 name: "conversation".to_string(),
364 kind: "clearable".to_string(),
365 current_tokens: 8,
366 max_tokens: 10_000,
367 entries: vec![
368 RegionEntrySnapshot {
369 content: "prior user turn".to_string(),
370 tokens: 5,
371 kind: EntryKind::UserMessage,
372 metadata: None,
373 key: None,
374 taint: Default::default(),
375 },
376 RegionEntrySnapshot {
377 content: "prior assistant".to_string(),
378 tokens: 3,
379 kind: EntryKind::AssistantTurn { tool_calls: vec![] },
380 metadata: None,
381 key: None,
382 taint: Default::default(),
383 },
384 ],
385 },
386 RegionSnapshot {
388 name: "ghost".to_string(),
389 kind: "pinned".to_string(),
390 current_tokens: 1,
391 max_tokens: 10,
392 entries: vec![RegionEntrySnapshot {
393 content: "orphan".to_string(),
394 tokens: 1,
395 kind: EntryKind::Text,
396 metadata: None,
397 key: None,
398 taint: Default::default(),
399 }],
400 },
401 ],
402 }
403 }
404
405 #[test]
411 fn restore_rebuilds_region_taint_from_the_persisted_entries() {
412 use leviath_core::taint::TaintLevel;
413
414 let mut snap = snapshot();
415 snap.regions[0].entries[0].taint = TaintLevel::Private;
416 snap.regions[0].entries[1].taint = TaintLevel::Public;
417
418 let (mut world, entity) = agent_world();
420 restore_agent(&mut world, entity, &snap, 1, 7, TokenTotals::default());
421 assert!(
422 world
423 .get::<ContextWindow>(entity)
424 .unwrap()
425 .get_region("conversation")
426 .unwrap()
427 .taint
428 .is_none()
429 );
430
431 let (mut world, entity) = agent_world();
433 world
434 .get_mut::<ContextWindow>(entity)
435 .unwrap()
436 .get_region_mut("conversation")
437 .unwrap()
438 .enable_taint_tracking();
439 restore_agent(&mut world, entity, &snap, 1, 7, TokenTotals::default());
440
441 let window = world.get::<ContextWindow>(entity).unwrap();
442 let region = window.get_region("conversation").unwrap();
443 assert_eq!(region.taint_level(), Some(TaintLevel::Private));
444 let taint = region.taint.as_ref().unwrap();
445 assert_eq!(taint.entry_taint(0), Some(TaintLevel::Private));
446 assert_eq!(taint.entry_taint(1), Some(TaintLevel::Public));
447 }
448
449 #[test]
450 fn restore_overlays_context_and_jumps_to_stage() {
451 let (mut world, entity) = agent_world();
452 restore_agent(
453 &mut world,
454 entity,
455 &snapshot(),
456 1,
457 7,
458 TokenTotals {
459 prompt_tokens: 100,
460 ..Default::default()
461 },
462 );
463
464 let window = world.get::<ContextWindow>(entity).unwrap();
466 let region = window.get_region("conversation").unwrap();
467 assert_eq!(region.content.len(), 2);
468 assert_eq!(region.content[0].content, "prior user turn");
469 assert_eq!(region.content[0].kind, EntryKind::UserMessage);
470 assert_eq!(region.current_tokens, 8);
471
472 assert_eq!(world.get::<StageCursor>(entity).unwrap().index, 1);
474 let state = world.get::<AgentState>(entity).unwrap();
475 assert_eq!(state.current_stage, "s1");
476 assert_eq!(state.iteration, 7);
477 assert_eq!(state.status, AgentStatus::Active);
478 assert_eq!(
479 world.get::<InferenceConfig>(entity).unwrap().temperature,
480 Some(0.5)
481 );
482 assert_eq!(world.get::<StageInference>(entity).unwrap().model, "m1");
483 assert_eq!(world.get::<TokenTotals>(entity).unwrap().prompt_tokens, 100);
484 assert!(world.get::<ReadyToInfer>(entity).is_some());
486 }
487
488 fn pending_call(
491 id: &str,
492 name: &str,
493 result: Option<&str>,
494 ) -> leviath_core::run_archive::ToolCallRecord {
495 leviath_core::run_archive::ToolCallRecord {
496 id: id.to_string(),
497 name: name.to_string(),
498 arguments: r#"{"path":"x.txt"}"#.to_string(),
499 result: result.map(str::to_string),
500 thought_signature: None,
501 }
502 }
503
504 fn pending_batch(
505 calls: Vec<leviath_core::run_archive::ToolCallRecord>,
506 ) -> leviath_core::run_archive::PendingToolBatch {
507 leviath_core::run_archive::PendingToolBatch {
508 stage_index: 1,
509 iteration: 7,
510 response: "writing then checking".to_string(),
511 calls,
512 }
513 }
514
515 fn conv_entries(world: &World, entity: Entity) -> Vec<RegionEntry> {
517 world
518 .get::<ContextWindow>(entity)
519 .unwrap()
520 .get_region("conversation")
521 .unwrap()
522 .content
523 .clone()
524 }
525
526 #[test]
527 fn pending_batch_replays_real_results_and_synthesizes_interrupted_ones() {
528 let (mut world, entity) = agent_world();
529 restore_agent(
530 &mut world,
531 entity,
532 &snapshot(),
533 1,
534 7,
535 TokenTotals::default(),
536 );
537 restore_pending_batch(
538 &mut world,
539 entity,
540 &pending_batch(vec![
541 pending_call("c1", "write_file", Some("Wrote 42 bytes to x.txt")),
542 pending_call("c2", "shell", None),
543 ]),
544 &[],
545 );
546
547 let entries = conv_entries(&world, entity);
548 let turn = entries
551 .iter()
552 .find_map(|e| match &e.kind {
553 EntryKind::AssistantTurn { tool_calls } if !tool_calls.is_empty() => {
554 Some(tool_calls.clone())
555 }
556 _ => None,
557 })
558 .expect("assistant turn appended");
559 assert_eq!(turn.len(), 2);
560 assert_eq!(turn[0].id, "c1");
561 assert_eq!(
562 turn[0].arguments,
563 serde_json::json!({"path": "x.txt"}),
564 "journaled arguments parsed back to JSON"
565 );
566 let result_of = |id: &str| {
567 entries
568 .iter()
569 .find(|e| {
570 matches!(&e.kind, EntryKind::ToolResult { tool_call_id, .. } if tool_call_id == id)
571 })
572 .map(|e| e.content.clone())
573 .expect("a result per call")
574 };
575 assert_eq!(result_of("c1"), "Wrote 42 bytes to x.txt");
576 assert!(result_of("c2").contains("interrupted"));
577 assert!(result_of("c2").contains("Verify whether it took effect"));
578 }
579
580 #[test]
581 fn pending_batch_survives_request_assembly_unstripped() {
582 let (mut world, entity) = agent_world();
587 world
588 .get_mut::<ContextWindow>(entity)
589 .unwrap()
590 .get_region_mut("conversation")
591 .unwrap()
592 .kind = RegionKind::SlidingWindow {
593 max_items: 100,
594 eviction_strategy: Default::default(),
595 };
596 restore_agent(
597 &mut world,
598 entity,
599 &snapshot(),
600 1,
601 7,
602 TokenTotals::default(),
603 );
604 restore_pending_batch(
605 &mut world,
606 entity,
607 &pending_batch(vec![pending_call("c1", "shell", None)]),
608 &[],
609 );
610
611 let assembled = world.get::<ContextWindow>(entity).unwrap().assemble();
612 let mut tool_uses = 0;
613 let mut tool_results = 0;
614 for msg in &assembled.messages {
615 if let leviath_providers::MessageContent::Blocks(blocks) = &msg.content {
616 for block in blocks {
617 match block {
618 leviath_providers::ContentBlock::ToolUse { id, .. } => {
619 assert_eq!(id, "c1");
620 tool_uses += 1;
621 }
622 leviath_providers::ContentBlock::ToolResult { tool_use_id, .. } => {
623 assert_eq!(tool_use_id, "c1");
624 tool_results += 1;
625 }
626 _ => {}
627 }
628 }
629 }
630 }
631 assert_eq!((tool_uses, tool_results), (1, 1), "nothing stripped");
632 }
633
634 #[test]
635 fn pending_batch_routes_results_through_the_restored_stage_routing() {
636 let (mut world, entity) = agent_world();
640 world
641 .get_mut::<ContextWindow>(entity)
642 .unwrap()
643 .add_region(Region::new(
644 "knowledge".to_string(),
645 RegionKind::Pinned,
646 10_000,
647 ));
648 world
649 .get_mut::<StageSetups>(entity)
650 .unwrap()
651 .0
652 .get_mut(1)
653 .unwrap()
654 .routing = Some(leviath_core::ToolResultRouting {
655 default_region: "knowledge".to_string(),
656 ..Default::default()
657 });
658 restore_agent(
659 &mut world,
660 entity,
661 &snapshot(),
662 1,
663 7,
664 TokenTotals::default(),
665 );
666 restore_pending_batch(
667 &mut world,
668 entity,
669 &pending_batch(vec![pending_call("c1", "read_file", Some("the file body"))]),
670 &[],
671 );
672
673 let window = world.get::<ContextWindow>(entity).unwrap();
674 let knowledge = window.get_region("knowledge").unwrap();
675 assert!(
676 knowledge
677 .content
678 .iter()
679 .any(|e| e.content.contains("the file body")),
680 "full text routed to the knowledge region"
681 );
682 assert!(
683 conv_entries(&world, entity).iter().any(
684 |e| matches!(&e.kind, EntryKind::ToolResult { tool_call_id, .. } if tool_call_id == "c1")
685 ),
686 "conversation keeps the paired pointer result"
687 );
688 }
689
690 #[test]
691 fn pending_batch_taints_results_per_tool_sensitivity() {
692 use leviath_core::taint::TaintLevel;
693 let (mut world, entity) = agent_world();
694 world
695 .get_mut::<ContextWindow>(entity)
696 .unwrap()
697 .get_region_mut("conversation")
698 .unwrap()
699 .enable_taint_tracking();
700 world
701 .entity_mut(entity)
702 .insert(crate::pipeline::ToolSensitivities(
703 [("read_file".to_string(), TaintLevel::Private)]
704 .into_iter()
705 .collect(),
706 ));
707 restore_agent(
708 &mut world,
709 entity,
710 &snapshot(),
711 1,
712 7,
713 TokenTotals::default(),
714 );
715 restore_pending_batch(
716 &mut world,
717 entity,
718 &pending_batch(vec![pending_call("c1", "read_file", Some("secret body"))]),
719 &[],
720 );
721
722 let window = world.get::<ContextWindow>(entity).unwrap();
723 assert_eq!(
724 window.get_region("conversation").unwrap().taint_level(),
725 Some(TaintLevel::Private),
726 "replayed result tainted like a live one"
727 );
728 }
729
730 #[test]
731 fn unparseable_journaled_arguments_survive_as_a_raw_string() {
732 let (mut world, entity) = agent_world();
733 restore_agent(
734 &mut world,
735 entity,
736 &snapshot(),
737 1,
738 7,
739 TokenTotals::default(),
740 );
741 let mut call = pending_call("c1", "shell", None);
742 call.arguments = "not json {".to_string();
743 restore_pending_batch(&mut world, entity, &pending_batch(vec![call]), &[]);
744
745 let entries = conv_entries(&world, entity);
746 let turn = entries
747 .iter()
748 .find_map(|e| match &e.kind {
749 EntryKind::AssistantTurn { tool_calls } if !tool_calls.is_empty() => {
750 Some(tool_calls.clone())
751 }
752 _ => None,
753 })
754 .expect("turn still lands");
755 assert_eq!(
756 turn[0].arguments,
757 serde_json::Value::String("not json {".to_string())
758 );
759 }
760
761 #[test]
762 fn interrupted_subagent_calls_point_at_known_children() {
763 let kids = vec!["run-kid-1".to_string(), "run-kid-2".to_string()];
767 let enriched = interrupted_result("spawn_agent", &kids);
768 assert!(enriched.contains("run-kid-1, run-kid-2"));
769 assert!(enriched.contains("check_agent"));
770 assert_eq!(interrupted_result("shell", &kids), INTERRUPTED_TOOL_RESULT);
771 assert_eq!(
772 interrupted_result("spawn_agent", &[]),
773 INTERRUPTED_TOOL_RESULT
774 );
775
776 let (mut world, entity) = agent_world();
778 restore_agent(
779 &mut world,
780 entity,
781 &snapshot(),
782 1,
783 7,
784 TokenTotals::default(),
785 );
786 restore_pending_batch(
787 &mut world,
788 entity,
789 &pending_batch(vec![pending_call("c1", "spawn_agent", None)]),
790 &kids,
791 );
792 assert!(
793 conv_entries(&world, entity)
794 .iter()
795 .any(|e| e.content.contains("already has child agent runs")),
796 "the synthesized sub-agent note lands in the window"
797 );
798 }
799
800 #[test]
801 fn restore_swaps_in_the_stage_routing_and_clears_stale() {
802 use crate::components::ToolResultRoutingComponent;
803
804 let (mut world, entity) = agent_world();
806 let routed = leviath_core::ToolResultRouting {
807 default_region: "knowledge".to_string(),
808 ..Default::default()
809 };
810 world
811 .get_mut::<StageSetups>(entity)
812 .unwrap()
813 .0
814 .get_mut(1)
815 .unwrap()
816 .routing = Some(routed);
817 restore_agent(
818 &mut world,
819 entity,
820 &snapshot(),
821 1,
822 7,
823 TokenTotals::default(),
824 );
825 assert_eq!(
826 world
827 .get::<ToolResultRoutingComponent>(entity)
828 .expect("stage 1's routing swapped in")
829 .routing
830 .default_region,
831 "knowledge"
832 );
833
834 let (mut world, entity) = agent_world();
837 world.entity_mut(entity).insert(ToolResultRoutingComponent {
838 routing: leviath_core::ToolResultRouting::default(),
839 });
840 restore_agent(
841 &mut world,
842 entity,
843 &snapshot(),
844 1,
845 7,
846 TokenTotals::default(),
847 );
848 assert!(world.get::<ToolResultRoutingComponent>(entity).is_none());
849 }
850
851 fn meta_with(run_id: &str, status: RunStatus, updated_at: i64) -> RunMeta {
852 let mut m = RunMeta::new(
853 run_id.to_string(),
854 "a".to_string(),
855 "/p".to_string(),
856 "t".to_string(),
857 None,
858 "/w".to_string(),
859 1,
860 );
861 m.status = status;
862 m.updated_at = updated_at;
863 m
864 }
865
866 #[test]
867 fn classify_restore_skips_terminal_and_ranks_the_rest() {
868 assert_eq!(classify_restore(&RunStatus::Complete, false), None);
870 assert_eq!(classify_restore(&RunStatus::Error, false), None);
871 assert_eq!(classify_restore(&RunStatus::Cancelled, false), None);
872 assert_eq!(
874 classify_restore(&RunStatus::Running, false),
875 Some(RestorePriority::Active)
876 );
877 assert_eq!(
878 classify_restore(&RunStatus::Starting, false),
879 Some(RestorePriority::Active)
880 );
881 assert_eq!(
883 classify_restore(&RunStatus::WaitingInput, false),
884 Some(RestorePriority::Blocked)
885 );
886 assert_eq!(
887 classify_restore(&RunStatus::Paused, false),
888 Some(RestorePriority::Blocked)
889 );
890 assert_eq!(
891 classify_restore(&RunStatus::CompleteInteractive, false),
892 Some(RestorePriority::Blocked)
893 );
894 assert_eq!(
896 classify_restore(&RunStatus::Running, true),
897 Some(RestorePriority::Blocked)
898 );
899 assert_eq!(classify_restore(&RunStatus::Complete, true), None);
901 }
902
903 #[test]
904 fn triage_orders_actionable_first_then_by_recency_and_drops_terminal() {
905 let candidates = vec![
906 (
907 meta_with("blocked-old", RunStatus::WaitingInput, 100),
908 false,
909 ),
910 (meta_with("active-old", RunStatus::Running, 200), false),
911 (meta_with("terminal", RunStatus::Complete, 999), false),
912 (meta_with("active-new", RunStatus::Starting, 300), false),
913 (meta_with("parked", RunStatus::Running, 999), true), (
915 meta_with("blocked-new", RunStatus::WaitingInput, 400),
916 false,
917 ),
918 ];
919 let order: Vec<String> = triage_restores(candidates)
920 .into_iter()
921 .map(|m| m.run_id)
922 .collect();
923 assert_eq!(
926 order,
927 vec![
928 "active-new".to_string(), "active-old".to_string(), "parked".to_string(), "blocked-new".to_string(), "blocked-old".to_string(), ]
934 );
935 }
936
937 #[test]
938 fn restore_with_out_of_range_stage_keeps_spawn_config() {
939 let (mut world, entity) = agent_world();
940 let mut snap = snapshot();
941 snap.stage_name = "s0".to_string();
942 restore_agent(&mut world, entity, &snap, 9, 2, TokenTotals::default());
944
945 assert_eq!(world.get::<StageCursor>(entity).unwrap().index, 0);
947 assert_eq!(world.get::<StageInference>(entity).unwrap().model, "m0");
948 assert_eq!(world.get::<AgentState>(entity).unwrap().iteration, 2);
950 assert_eq!(
951 world
952 .get::<ContextWindow>(entity)
953 .unwrap()
954 .get_region("conversation")
955 .unwrap()
956 .content
957 .len(),
958 2
959 );
960 }
961}