use bevy_ecs::prelude::*;
use leviath_core::region::RegionEntry;
use leviath_core::run_meta::{ContextSnapshot, RunMeta, RunStatus};
use crate::components::{AgentState, AgentStatus, ContextWindow};
use crate::persistence::TokenTotals;
use crate::pipeline::{StageCursor, StageInferences, StageSetups};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RestorePriority {
Blocked,
Active,
}
pub fn classify_restore(status: &RunStatus, parked_on_fanout: bool) -> Option<RestorePriority> {
match status {
RunStatus::Complete | RunStatus::Error | RunStatus::Cancelled => None,
_ if parked_on_fanout => Some(RestorePriority::Blocked),
RunStatus::Starting | RunStatus::Running => Some(RestorePriority::Active),
RunStatus::WaitingInput | RunStatus::CompleteInteractive => Some(RestorePriority::Blocked),
}
}
pub fn triage_restores(candidates: Vec<(RunMeta, bool)>) -> Vec<RunMeta> {
let mut ranked: Vec<(RestorePriority, RunMeta)> = candidates
.into_iter()
.filter_map(|(meta, parked)| {
classify_restore(&meta.status, parked).map(|prio| (prio, meta))
})
.collect();
ranked.sort_by(|(a_prio, a), (b_prio, b)| {
b_prio
.cmp(a_prio)
.then_with(|| b.updated_at.cmp(&a.updated_at))
});
ranked.into_iter().map(|(_, meta)| meta).collect()
}
pub fn restore_agent(
world: &mut World,
entity: Entity,
snapshot: &ContextSnapshot,
stage_index: usize,
iteration: usize,
totals: TokenTotals,
) {
{
let mut window = world
.get_mut::<ContextWindow>(entity)
.expect("a spawned agent has a context window");
for snap_region in &snapshot.regions {
if let Some(region) = window
.regions
.iter_mut()
.find(|r| r.name == snap_region.name)
{
region.content = snap_region
.entries
.iter()
.map(|e| RegionEntry {
content: e.content.clone(),
tokens: e.tokens,
timestamp: 0,
metadata: e.metadata.clone(),
kind: e.kind.clone(),
key: e.key.clone(),
})
.collect();
if region.taint.is_some() {
region.taint = Some(leviath_core::taint::RegionTaint::from_entry_taints(
snap_region.entries.iter().map(|e| e.taint).collect(),
));
}
region.current_tokens = region.content.iter().map(|e| e.tokens).sum();
}
}
window.current_tokens = window.calculate_tokens();
}
if let Some(inf) = world
.get::<StageInferences>(entity)
.expect("a spawned agent has stage inferences")
.0
.get(stage_index)
.cloned()
{
let cfg = world
.get::<StageSetups>(entity)
.expect("a spawned agent has stage setups")
.0[stage_index]
.inference_config
.clone();
world.entity_mut(entity).insert((inf, cfg));
world
.get_mut::<StageCursor>(entity)
.expect("a spawned agent has a stage cursor")
.index = stage_index;
}
{
let mut state = world
.get_mut::<AgentState>(entity)
.expect("a spawned agent has state");
state.current_stage = snapshot.stage_name.clone();
state.iteration = iteration;
state.status = AgentStatus::Active;
}
world.entity_mut(entity).insert(totals);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::InferenceConfig;
use crate::pipeline::{ReadyToInfer, StageInference, StageSetup};
use leviath_core::region::EntryKind;
use leviath_core::run_meta::{RegionEntrySnapshot, RegionSnapshot};
use leviath_core::{Region, RegionKind};
fn setup(temp: Option<f32>) -> StageSetup {
StageSetup {
inference_config: InferenceConfig {
temperature: temp,
max_output_tokens: None,
extra_params: Default::default(),
batch_tool_hint: false,
request_timeout_secs: None,
},
routing: None,
accepts_messages: true,
context_layout: None,
system_prompt: None,
}
}
fn si(model: &str) -> StageInference {
StageInference {
provider_name: "p".to_string(),
model: model.to_string(),
tools: vec![],
tool_filter: None,
}
}
fn agent_world() -> (World, Entity) {
let mut world = World::new();
let mut window = ContextWindow::new(10_000);
window.add_region(Region::new(
"conversation".to_string(),
RegionKind::Clearable,
10_000,
));
let _ = window.add_to_region("conversation", "fresh task seed".to_string(), 3);
let entity = world
.spawn((
window,
StageCursor { index: 0 },
AgentState {
agent_id: "a".to_string(),
current_stage: "s0".to_string(),
iteration: 0,
status: AgentStatus::Active,
spawned_children_ids: vec![],
pending_wait: None,
accepts_messages: true,
},
StageInferences(vec![si("m0"), si("m1")]),
StageSetups(vec![setup(None), setup(Some(0.5))]),
si("m0"),
setup(None).inference_config,
TokenTotals::default(),
ReadyToInfer,
))
.id();
(world, entity)
}
fn snapshot() -> ContextSnapshot {
ContextSnapshot {
stage_name: "s1".to_string(),
total_tokens: 8,
max_tokens: 10_000,
regions: vec![
RegionSnapshot {
name: "conversation".to_string(),
kind: "clearable".to_string(),
current_tokens: 8,
max_tokens: 10_000,
entries: vec![
RegionEntrySnapshot {
content: "prior user turn".to_string(),
tokens: 5,
kind: EntryKind::UserMessage,
metadata: None,
key: None,
taint: Default::default(),
},
RegionEntrySnapshot {
content: "prior assistant".to_string(),
tokens: 3,
kind: EntryKind::AssistantTurn { tool_calls: vec![] },
metadata: None,
key: None,
taint: Default::default(),
},
],
},
RegionSnapshot {
name: "ghost".to_string(),
kind: "pinned".to_string(),
current_tokens: 1,
max_tokens: 10,
entries: vec![RegionEntrySnapshot {
content: "orphan".to_string(),
tokens: 1,
kind: EntryKind::Text,
metadata: None,
key: None,
taint: Default::default(),
}],
},
],
}
}
#[test]
fn restore_rebuilds_region_taint_from_the_persisted_entries() {
use leviath_core::taint::TaintLevel;
let mut snap = snapshot();
snap.regions[0].entries[0].taint = TaintLevel::Private;
snap.regions[0].entries[1].taint = TaintLevel::Public;
let (mut world, entity) = agent_world();
restore_agent(&mut world, entity, &snap, 1, 7, TokenTotals::default());
assert!(
world
.get::<ContextWindow>(entity)
.unwrap()
.get_region("conversation")
.unwrap()
.taint
.is_none()
);
let (mut world, entity) = agent_world();
world
.get_mut::<ContextWindow>(entity)
.unwrap()
.get_region_mut("conversation")
.unwrap()
.enable_taint_tracking();
restore_agent(&mut world, entity, &snap, 1, 7, TokenTotals::default());
let window = world.get::<ContextWindow>(entity).unwrap();
let region = window.get_region("conversation").unwrap();
assert_eq!(region.taint_level(), Some(TaintLevel::Private));
let taint = region.taint.as_ref().unwrap();
assert_eq!(taint.entry_taint(0), Some(TaintLevel::Private));
assert_eq!(taint.entry_taint(1), Some(TaintLevel::Public));
}
#[test]
fn restore_overlays_context_and_jumps_to_stage() {
let (mut world, entity) = agent_world();
restore_agent(
&mut world,
entity,
&snapshot(),
1,
7,
TokenTotals {
prompt_tokens: 100,
..Default::default()
},
);
let window = world.get::<ContextWindow>(entity).unwrap();
let region = window.get_region("conversation").unwrap();
assert_eq!(region.content.len(), 2);
assert_eq!(region.content[0].content, "prior user turn");
assert_eq!(region.content[0].kind, EntryKind::UserMessage);
assert_eq!(region.current_tokens, 8);
assert_eq!(world.get::<StageCursor>(entity).unwrap().index, 1);
let state = world.get::<AgentState>(entity).unwrap();
assert_eq!(state.current_stage, "s1");
assert_eq!(state.iteration, 7);
assert_eq!(state.status, AgentStatus::Active);
assert_eq!(
world.get::<InferenceConfig>(entity).unwrap().temperature,
Some(0.5)
);
assert_eq!(world.get::<StageInference>(entity).unwrap().model, "m1");
assert_eq!(world.get::<TokenTotals>(entity).unwrap().prompt_tokens, 100);
assert!(world.get::<ReadyToInfer>(entity).is_some());
}
fn meta_with(run_id: &str, status: RunStatus, updated_at: i64) -> RunMeta {
let mut m = RunMeta::new(
run_id.to_string(),
"a".to_string(),
"/p".to_string(),
"t".to_string(),
None,
"/w".to_string(),
1,
);
m.status = status;
m.updated_at = updated_at;
m
}
#[test]
fn classify_restore_skips_terminal_and_ranks_the_rest() {
assert_eq!(classify_restore(&RunStatus::Complete, false), None);
assert_eq!(classify_restore(&RunStatus::Error, false), None);
assert_eq!(classify_restore(&RunStatus::Cancelled, false), None);
assert_eq!(
classify_restore(&RunStatus::Running, false),
Some(RestorePriority::Active)
);
assert_eq!(
classify_restore(&RunStatus::Starting, false),
Some(RestorePriority::Active)
);
assert_eq!(
classify_restore(&RunStatus::WaitingInput, false),
Some(RestorePriority::Blocked)
);
assert_eq!(
classify_restore(&RunStatus::CompleteInteractive, false),
Some(RestorePriority::Blocked)
);
assert_eq!(
classify_restore(&RunStatus::Running, true),
Some(RestorePriority::Blocked)
);
assert_eq!(classify_restore(&RunStatus::Complete, true), None);
}
#[test]
fn triage_orders_actionable_first_then_by_recency_and_drops_terminal() {
let candidates = vec![
(
meta_with("blocked-old", RunStatus::WaitingInput, 100),
false,
),
(meta_with("active-old", RunStatus::Running, 200), false),
(meta_with("terminal", RunStatus::Complete, 999), false),
(meta_with("active-new", RunStatus::Starting, 300), false),
(meta_with("parked", RunStatus::Running, 999), true), (
meta_with("blocked-new", RunStatus::WaitingInput, 400),
false,
),
];
let order: Vec<String> = triage_restores(candidates)
.into_iter()
.map(|m| m.run_id)
.collect();
assert_eq!(
order,
vec![
"active-new".to_string(), "active-old".to_string(), "parked".to_string(), "blocked-new".to_string(), "blocked-old".to_string(), ]
);
}
#[test]
fn restore_with_out_of_range_stage_keeps_spawn_config() {
let (mut world, entity) = agent_world();
let mut snap = snapshot();
snap.stage_name = "s0".to_string();
restore_agent(&mut world, entity, &snap, 9, 2, TokenTotals::default());
assert_eq!(world.get::<StageCursor>(entity).unwrap().index, 0);
assert_eq!(world.get::<StageInference>(entity).unwrap().model, "m0");
assert_eq!(world.get::<AgentState>(entity).unwrap().iteration, 2);
assert_eq!(
world
.get::<ContextWindow>(entity)
.unwrap()
.get_region("conversation")
.unwrap()
.content
.len(),
2
);
}
}