use crate::types::{Ask, DecisionRecord, JournalKey, RunOutcome};
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum NodeState {
Waiting,
Ready,
Dispatched,
AwaitingClient,
DoneOk,
DoneFailed,
Dead,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum EdgeRes {
Fired,
Dead,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct Spent {
pub supersteps: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub usd_micros: u64,
pub wall_ms: u64,
pub storage_bytes: u64,
pub journal_grains: u64,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum Phase {
Idle,
Open {
outstanding: BTreeSet<JournalKey>,
results: BTreeMap<usize, Value>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
send_results: BTreeMap<String, Value>,
record: DecisionRecord,
},
Finished(RunOutcome),
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SendTask {
pub node_idx: usize,
pub input: Value,
pub attempt: u32,
pub state: SendTaskState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SendTaskState {
Queued,
Running,
Done,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PendingAsk {
pub key: JournalKey,
pub node_idx: usize,
pub ask: Ask,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PendingToolCall {
pub model_call_id: String,
pub tool_name: String,
pub arguments: Value,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum FlowNeed {
NextTurn,
Tools(Vec<PendingToolCall>),
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct FoldInFlight {
pub from: usize,
pub to: usize,
pub effect_seq: u32,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct AbstractFlow {
pub messages: Vec<Value>,
pub next_effect_seq: u32,
pub pending_tools: BTreeMap<u32, PendingToolCall>,
pub round_results: BTreeMap<u32, Value>,
pub need: Option<FlowNeed>,
pub unknown_strikes: u32,
#[serde(default, skip_serializing_if = "is_zero")]
pub last_prompt_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub folding: Option<FoldInFlight>,
#[serde(default, skip_serializing_if = "is_false")]
pub fold_forced: bool,
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub folds: u32,
}
fn is_zero(n: &u64) -> bool {
*n == 0
}
fn is_zero_u32(n: &u32) -> bool {
*n == 0
}
fn is_false(b: &bool) -> bool {
!*b
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SchedulerState {
pub run_id: String,
pub superstep: u64,
pub phase: Phase,
pub node_gen: Vec<u32>,
pub node_state: Vec<NodeState>,
pub in_res: Vec<BTreeMap<usize, EdgeRes>>,
pub edge_fired: Vec<u32>,
pub attempt: Vec<u32>,
pub attempt_base: Vec<u32>,
pub context: Value,
pub spent: Spent,
pub elapsed_ms: u64,
pub clock_ms: u64,
pub wall_open: Option<u64>,
pub paused_at: Option<u64>,
pub pending_asks: BTreeMap<String, PendingAsk>,
pub abstract_flows: BTreeMap<String, AbstractFlow>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub send_tasks: BTreeMap<String, SendTask>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub spawn_counter: BTreeMap<String, u32>,
pub announced_asks: BTreeSet<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inbox: Vec<Value>,
#[serde(default, skip_serializing_if = "is_zero")]
pub inputs_seen: u64,
pub cancel: Option<(String, String)>,
pub exhausted: Option<crate::error::BudgetAxis>,
pub failed: Option<(usize, String)>,
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub folds: u32,
}
impl SchedulerState {
pub fn new(run_id: &str, plan: &crate::plan::PlanGraph) -> Self {
let n = plan.nodes.len();
SchedulerState {
run_id: run_id.to_string(),
superstep: 0,
phase: Phase::Idle,
node_gen: vec![0; n],
node_state: vec![NodeState::Waiting; n],
in_res: vec![BTreeMap::new(); n],
edge_fired: vec![0; plan.edges.len()],
attempt: vec![0; n],
attempt_base: vec![0; n],
context: Value::Object(serde_json::Map::new()),
spent: Spent::default(),
elapsed_ms: 0,
clock_ms: 0,
wall_open: None,
paused_at: None,
pending_asks: BTreeMap::new(),
abstract_flows: BTreeMap::new(),
send_tasks: BTreeMap::new(),
spawn_counter: BTreeMap::new(),
announced_asks: BTreeSet::new(),
inbox: Vec::new(),
inputs_seen: 0,
cancel: None,
exhausted: None,
failed: None,
folds: 0,
}
}
pub fn is_terminal(&self) -> bool {
matches!(self.phase, Phase::Finished(_))
}
pub fn outcome(&self) -> Option<&RunOutcome> {
match &self.phase {
Phase::Finished(o) => Some(o),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bare_flow() -> AbstractFlow {
AbstractFlow {
messages: vec![serde_json::json!({"role": "user", "content": "go"})],
next_effect_seq: 1,
pending_tools: BTreeMap::new(),
round_results: BTreeMap::new(),
need: None,
unknown_strikes: 0,
last_prompt_tokens: 0,
folding: None,
fold_forced: false,
folds: 0,
}
}
#[test]
fn a_flow_with_no_fold_state_serializes_as_it_did_before_folds_existed() {
let json = serde_json::to_string(&bare_flow()).unwrap();
assert_eq!(
json,
r#"{"messages":[{"content":"go","role":"user"}],"next_effect_seq":1,"pending_tools":{},"round_results":{},"need":null,"unknown_strikes":0}"#
);
}
#[test]
fn fold_state_round_trips_byte_stably() {
let flow = AbstractFlow {
last_prompt_tokens: 120_000,
folding: Some(FoldInFlight { from: 1, to: 9, effect_seq: 9 }),
folds: 2,
..bare_flow()
};
let bytes = serde_json::to_string(&flow).unwrap();
let back: AbstractFlow = serde_json::from_str(&bytes).unwrap();
assert_eq!(back, flow);
assert_eq!(serde_json::to_string(&back).unwrap(), bytes);
}
}