use crate::error::{BudgetAxis, RunError};
use crate::plan::PlanGraph;
use crate::state::{EdgeRes, FoldInFlight, NodeState, PendingAsk, Phase, SchedulerState};
use crate::types::{
Ask, Budgets, Command, DecisionRecord, EdgeOutcome, EffectKind, EffectOutcome, EventIn,
FailCause, JournalKey, NodeExecutor, RunOutcome,
};
use serde_json::Value;
use std::collections::BTreeMap;
pub const DEFAULT_MAX_EFFECTS_PER_ATTEMPT: u32 = 16;
pub const KEEP_TAIL: usize = 4;
pub const FOLD_PROMPT: &str = "Summarize the working state of this task so far \
for someone who will continue it: what has been established, which tools were \
called and what they returned in substance, what remains to be done, and any \
errors or dead ends. Be concrete; keep identifiers, paths and values verbatim. \
Reply with the summary only.";
pub const FOLD_PROMPT_V: u32 = 1;
pub struct StepEnv<'a> {
pub plan: &'a PlanGraph,
pub executors: &'a [NodeExecutor],
pub budgets: Budgets,
pub reduce: &'a dyn Fn(&str, Option<&Value>, &Value) -> Value,
pub eval_cond: &'a dyn Fn(&crate::plan::PlanEdge, &Value) -> bool,
pub ask_ttl_sec: Option<i64>,
pub validate_args: &'a dyn Fn(&str, &Value) -> std::result::Result<(), String>,
pub llm_reserve_tokens: u64,
pub max_effects_per_attempt: u32,
pub llm_tool_result_chars: Option<usize>,
pub llm_context_tokens: Option<u64>,
}
pub struct StepOutcome {
pub commands: Vec<Command>,
pub state: SchedulerState,
}
pub fn step(env: &StepEnv<'_>, mut st: SchedulerState, events: &[EventIn]) -> StepOutcome {
let mut out: Vec<Command> = Vec::new();
for ev in events {
apply_event(env, &mut st, ev, &mut out);
}
progress(env, &mut st, &mut out);
StepOutcome { commands: out, state: st }
}
fn apply_event(
env: &StepEnv<'_>,
st: &mut SchedulerState,
ev: &EventIn,
out: &mut Vec<Command>,
) {
match ev {
EventIn::ClockReading { unix_ms } => {
st.clock_ms = (*unix_ms).max(st.clock_ms);
}
EventIn::Start { input } => {
if st.superstep == 0 && matches!(st.phase, Phase::Idle) {
st.context = match input {
Value::Object(_) => input.clone(),
other => serde_json::json!({ "input": other }),
};
if st.node_state[0] == NodeState::Waiting {
st.node_state[0] = NodeState::Ready;
}
}
}
EventIn::CancelSeen { principal, reason } => {
if st.cancel.is_none() && !st.is_terminal() {
st.cancel = Some((principal.clone(), reason.clone()));
}
}
EventIn::EffectResolved { key, outcome } => {
resolve_effect(env, st, key, outcome);
}
EventIn::Resumed => {
if matches!(st.phase, Phase::Idle) && st.paused_at.is_none() {
st.paused_at = Some(st.clock_ms);
}
}
EventIn::InputSeen { message } => {
st.inputs_seen += 1;
st.inbox.push(message.clone());
}
EventIn::AskForwarded { tool_call_id } => {
let Some(pending) = st.pending_asks.get(tool_call_id).cloned() else {
return;
};
drop_bubble(st, &pending.key);
st.node_state[pending.node_idx] = NodeState::Dispatched;
let executor = env.executors[pending.node_idx].clone();
let input = st.context.clone();
let (superstep, clock_ms) = (st.superstep, st.clock_ms);
out.push(Command::WriteIntent {
key: pending.key.clone(),
executor: executor.clone(),
input: input.clone(),
superstep,
clock_ms,
});
out.push(Command::Dispatch { key: pending.key, executor, input });
}
EventIn::ResponseSettled { tool_call_id, outcome } => {
let Some(pending) = st.pending_asks.remove(tool_call_id) else {
return;
};
st.announced_asks.remove(tool_call_id);
st.node_state[pending.node_idx] = NodeState::Dispatched;
resolve_effect(env, st, &pending.key, outcome);
}
}
}
fn resolve_effect(
env: &StepEnv<'_>,
st: &mut SchedulerState,
key: &JournalKey,
outcome: &EffectOutcome,
) {
let Some(node_idx) = env.plan.nodes.iter().position(|n| *n == key.node) else {
return;
};
{
let Phase::Open { outstanding, .. } = &mut st.phase else {
return; };
if !outstanding.remove(key) {
return; }
}
match outcome {
EffectOutcome::Completed {
journal_bytes,
input_tokens,
output_tokens,
usd_micros,
..
} => {
st.spent.journal_grains += 2; st.spent.storage_bytes += journal_bytes;
st.spent.input_tokens += input_tokens;
st.spent.output_tokens += output_tokens;
st.spent.usd_micros += usd_micros;
}
EffectOutcome::Failed { journal_bytes, .. } => {
st.spent.journal_grains += 2;
st.spent.storage_bytes += journal_bytes;
}
}
let flow = flow_key(node_idx, &key.task_path);
if st.abstract_flows.contains_key(&flow) {
match key.kind {
EffectKind::Llm => {
handle_llm_outcome(env, st, node_idx, &key.task_path, outcome);
return;
}
EffectKind::Tool
if st.abstract_flows[&flow].pending_tools.contains_key(&key.effect_seq) =>
{
handle_flow_tool_outcome(
env,
st,
node_idx,
&key.task_path,
key.effect_seq,
outcome,
);
return;
}
_ => {}
}
}
if !key.task_path.is_empty() {
handle_send_task_outcome(env, st, key, outcome);
return;
}
if matches!(
st.node_state[node_idx],
NodeState::DoneOk | NodeState::DoneFailed | NodeState::Dead
) {
return;
}
if let EffectOutcome::Completed { result, .. } = outcome {
if matches!(env.executors[node_idx], NodeExecutor::Subgraph { .. }) {
if let Some(asks) = result
.get(crate::types::PARKED_ASKS)
.and_then(|v| serde_json::from_value::<Vec<Ask>>(v.clone()).ok())
.filter(|a| !a.is_empty())
{
park_bubbled(st, node_idx, key, asks);
return;
}
}
}
let attempts_used = st.attempt[node_idx] - st.attempt_base[node_idx];
let retry_budget = env.plan.retries[node_idx];
let canceling = st.cancel.is_some();
match outcome {
EffectOutcome::Completed { result, .. } => {
st.node_state[node_idx] = NodeState::DoneOk;
if let Phase::Open { results, .. } = &mut st.phase {
results.insert(node_idx, result.clone());
}
}
EffectOutcome::Failed { cause, detail, .. } => {
let can_retry =
cause.retryable(key.kind) && attempts_used <= retry_budget && !canceling;
if can_retry {
st.node_state[node_idx] = NodeState::Ready;
} else {
st.node_state[node_idx] = NodeState::DoneFailed;
let named = st.failed.as_ref().map(|(i, _)| *i).unwrap_or(usize::MAX);
if node_idx < named {
st.failed = Some((node_idx, format!("{cause:?}: {detail}")));
}
}
}
}
}
pub fn flow_key(node_idx: usize, task_path: &str) -> String {
if task_path.is_empty() {
node_idx.to_string()
} else {
format!("{node_idx}@{task_path}")
}
}
fn flow_attempt(st: &SchedulerState, i: usize, path: &str) -> u32 {
match st.send_tasks.get(path) {
Some(task) => task.attempt,
None => st.attempt[i],
}
}
fn start_flow(env: &StepEnv<'_>, st: &mut SchedulerState, i: usize, path: &str, input: Value, out: &mut Vec<Command>) {
st.abstract_flows.insert(
flow_key(i, path),
crate::state::AbstractFlow {
messages: vec![serde_json::json!({
"role": "user",
"content": {
"instruction": env.plan.nodes[i],
"state": input,
}
})],
next_effect_seq: 0,
pending_tools: BTreeMap::new(),
round_results: BTreeMap::new(),
need: None,
unknown_strikes: 0,
last_prompt_tokens: 0,
folding: None,
fold_forced: false,
folds: 0,
},
);
dispatch_llm_turn(env, st, i, path, out);
}
fn flow_owners(st: &SchedulerState) -> Vec<(usize, String)> {
let mut owners: Vec<(usize, String)> = st
.abstract_flows
.iter()
.filter(|(_, f)| f.need.is_some())
.filter_map(|(k, _)| match k.split_once('@') {
Some((i, path)) => Some((i.parse().ok()?, path.to_string())),
None => Some((k.parse().ok()?, String::new())),
})
.collect();
owners.sort();
owners
}
fn apply_inbox(st: &mut SchedulerState) {
let Value::Object(o) = &mut st.context else {
return;
};
if st.inbox.is_empty() {
o.remove(crate::types::INBOX);
} else {
o.insert(crate::types::INBOX.into(), Value::Array(std::mem::take(&mut st.inbox)));
}
}
fn drop_bubble(st: &mut SchedulerState, key: &JournalKey) {
let ids: Vec<String> = st
.pending_asks
.iter()
.filter(|(_, p)| p.key == *key)
.map(|(id, _)| id.clone())
.collect();
for id in ids {
st.pending_asks.remove(&id);
st.announced_asks.remove(&id);
}
}
fn park_bubbled(st: &mut SchedulerState, i: usize, resolved: &JournalKey, asks: Vec<Ask>) {
if !matches!(st.phase, Phase::Open { .. }) {
return;
}
let stale: Vec<JournalKey> = st
.pending_asks
.values()
.filter(|p| p.node_idx == i)
.map(|p| p.key.clone())
.collect();
for key in stale {
drop_bubble(st, &key);
}
let key = JournalKey { effect_seq: resolved.effect_seq + 1, ..resolved.clone() };
if let Phase::Open { outstanding, .. } = &mut st.phase {
outstanding.insert(key.clone());
}
st.node_state[i] = NodeState::AwaitingClient;
for ask in asks {
st.pending_asks.insert(
ask.tool_call_id.clone(),
PendingAsk { key: key.clone(), node_idx: i, ask },
);
}
}
fn handle_send_task_outcome(
env: &StepEnv<'_>,
st: &mut SchedulerState,
key: &JournalKey,
outcome: &EffectOutcome,
) {
let Some(task) = st.send_tasks.get_mut(&key.task_path) else { return };
if task.state == crate::state::SendTaskState::Done {
return;
}
let target = task.node_idx;
match outcome {
EffectOutcome::Completed { result, .. } => {
settle_send_task(st, &key.task_path, result.clone());
}
EffectOutcome::Failed { cause, detail, .. } => {
let can_retry = cause.retryable(key.kind)
&& task.attempt <= env.plan.retries[target]
&& st.cancel.is_none();
if can_retry {
task.state = crate::state::SendTaskState::Queued;
return;
}
task.state = crate::state::SendTaskState::Done;
st.node_state[target] = NodeState::DoneFailed;
let named = st.failed.as_ref().map(|(i, _)| *i).unwrap_or(usize::MAX);
if target < named {
st.failed = Some((
target,
format!("{cause:?}: {detail} (task {})", key.task_path),
));
}
}
}
}
fn settle_send_task(st: &mut SchedulerState, path: &str, result: Value) {
let Some(task) = st.send_tasks.get_mut(path) else { return };
let target = task.node_idx;
task.state = crate::state::SendTaskState::Done;
if let Phase::Open { send_results, .. } = &mut st.phase {
send_results.insert(path.to_string(), result);
}
let all_done = st
.send_tasks
.values()
.filter(|t| t.node_idx == target)
.all(|t| t.state == crate::state::SendTaskState::Done);
if all_done && st.node_state[target] == NodeState::Dispatched {
st.node_state[target] = NodeState::DoneOk;
if let Phase::Open { results, .. } = &mut st.phase {
results.entry(target).or_insert(Value::Null);
}
}
}
fn handle_llm_outcome(
env: &StepEnv<'_>,
st: &mut SchedulerState,
i: usize,
path: &str,
outcome: &EffectOutcome,
) {
let NodeExecutor::Abstract { tools } = &env.executors[i] else { return };
let flow_id = flow_key(i, path);
let folding = st.abstract_flows.get_mut(&flow_id).and_then(|f| f.folding.take());
if let Some(f) = folding {
resolve_fold(env, st, i, path, f, outcome);
return;
}
let offered: Vec<&str> = tools.iter().map(|t| t.tool_name.as_str()).collect();
match outcome {
EffectOutcome::Failed { cause, detail, .. } => match cause {
FailCause::Timeout | FailCause::ExecutorError => {
if let Some(flow) = st.abstract_flows.get_mut(&flow_id) {
flow.need = Some(crate::state::FlowNeed::NextTurn);
}
}
FailCause::SchemaValidationFailed => {
if let Some(flow) = st.abstract_flows.get_mut(&flow_id) {
flow.messages.push(serde_json::json!({
"role": "user",
"content": format!(
"Your previous output failed validation: {detail}. \
Correct it and answer again."
),
}));
flow.need = Some(crate::state::FlowNeed::NextTurn);
}
}
FailCause::ContextOverflow => {
if let Some(flow) = st.abstract_flows.get_mut(&flow_id) {
flow.fold_forced = true;
flow.need = Some(crate::state::FlowNeed::NextTurn);
}
}
FailCause::UserAborted | FailCause::Unknown => {
fail_abstract(env, st, i, path, detail);
}
},
EffectOutcome::Completed { result, input_tokens, .. } => {
if let Some(flow) = st.abstract_flows.get_mut(&flow_id) {
flow.last_prompt_tokens = *input_tokens;
}
let text = result.get("text").and_then(|t| t.as_str()).map(str::to_string);
let calls: Vec<crate::state::PendingToolCall> = result
.get("tool_calls")
.and_then(|c| c.as_array())
.map(|arr| {
arr.iter()
.map(|c| crate::state::PendingToolCall {
model_call_id: c
.get("id")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
tool_name: c
.get("name")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
arguments: c.get("arguments").cloned().unwrap_or(Value::Null),
})
.collect()
})
.unwrap_or_default();
let unknown: Vec<String> = calls
.iter()
.map(|c| c.tool_name.clone())
.filter(|n| !offered.contains(&n.as_str()))
.collect();
if !unknown.is_empty() {
let strikes = st
.abstract_flows
.get(&flow_id)
.map(|f| f.unknown_strikes)
.unwrap_or(0);
if strikes >= 1 {
fail_abstract(
env,
st,
i,
path,
&format!("model called unknown tool(s) {unknown:?} after a corrective re-prompt"),
);
return;
}
let Some(flow) = st.abstract_flows.get_mut(&flow_id) else { return };
flow.unknown_strikes += 1;
flow.messages.push(serde_json::json!({
"role": "user",
"content": format!(
"Unknown tool(s) {:?}; choose among: {:?}",
unknown, offered
),
}));
flow.need = Some(crate::state::FlowNeed::NextTurn);
return;
}
let invalid: Option<(String, String)> = calls.iter().find_map(|c| {
if !c.arguments.is_object() {
return Some((
c.tool_name.clone(),
"arguments are not a JSON object".to_string(),
));
}
(env.validate_args)(&c.tool_name, &c.arguments)
.err()
.map(|e| (c.tool_name.clone(), e))
});
let Some(flow) = st.abstract_flows.get_mut(&flow_id) else { return };
if let Some((tool, why)) = invalid {
flow.messages.push(serde_json::json!({
"role": "user",
"content": format!(
"Arguments for tool '{tool}' failed validation ({why}). \
Correct the call and try again."
),
}));
flow.need = Some(crate::state::FlowNeed::NextTurn);
return;
}
if calls.is_empty() {
let final_text = text.unwrap_or_default();
let value = serde_json::from_str::<Value>(&final_text)
.ok()
.filter(|v| v.is_object())
.unwrap_or_else(|| {
serde_json::json!({ env.plan.nodes[i].clone(): final_text })
});
st.abstract_flows.remove(&flow_id);
if path.is_empty() {
st.node_state[i] = NodeState::DoneOk;
if let Phase::Open { results, .. } = &mut st.phase {
results.insert(i, value);
}
} else {
settle_send_task(st, path, value);
}
return;
}
flow.messages.push(serde_json::json!({
"role": "assistant",
"text": text,
"tool_calls": calls.iter().map(|c| serde_json::json!({
"id": c.model_call_id,
"name": c.tool_name,
"arguments": c.arguments,
})).collect::<Vec<_>>(),
}));
flow.round_results.clear();
flow.need = Some(crate::state::FlowNeed::Tools(calls));
}
}
}
pub fn bound_tool_content(
raw: &Value,
cap: Option<usize>,
attempt: u32,
effect_seq: u32,
) -> Value {
let Some(cap) = cap else { return raw.clone() };
let text = match raw {
Value::String(s) => s.clone(),
other => other.to_string(),
};
let total = text.chars().count();
if total <= cap {
return raw.clone();
}
let half = cap / 2;
serde_json::json!({
"truncated": true,
"chars": total,
"head": text.chars().take(half).collect::<String>(),
"tail": text.chars().skip(total - half).collect::<String>(),
"journal": { "attempt": attempt, "effect_seq": effect_seq },
})
}
fn resolve_fold(
env: &StepEnv<'_>,
st: &mut SchedulerState,
i: usize,
path: &str,
f: FoldInFlight,
outcome: &EffectOutcome,
) {
let flow_id = flow_key(i, path);
match outcome {
EffectOutcome::Completed { result, .. } => {
let summary =
result.get("text").and_then(Value::as_str).unwrap_or_default().to_string();
let attempt = flow_attempt(st, i, path);
let Some(flow) = st.abstract_flows.get_mut(&flow_id) else { return };
if f.from >= f.to || f.to > flow.messages.len() {
flow.need = Some(crate::state::FlowNeed::NextTurn);
return;
}
let n = flow.folds + 1;
let fold_msg = serde_json::json!({
"role": "user",
"content": format!(
"[Areev fold {n}: transcript entries {}..{} of attempt {attempt} were \
replaced by this summary, journaled at effect_seq {}. The full record \
— every turn and every tool result — is in this run's journal.]\n{summary}",
f.from, f.to, f.effect_seq,
),
});
flow.messages.splice(f.from..f.to, [fold_msg]);
flow.folds = n;
flow.last_prompt_tokens = 0;
flow.need = Some(crate::state::FlowNeed::NextTurn);
st.folds = st.folds.saturating_add(1);
}
EffectOutcome::Failed { cause, detail, .. } => match cause {
FailCause::Timeout | FailCause::ExecutorError => {
if let Some(flow) = st.abstract_flows.get_mut(&flow_id) {
flow.need = Some(crate::state::FlowNeed::NextTurn);
}
}
FailCause::ContextOverflow => {
let detail = RunError::ContextExceeded {
node: env.plan.nodes[i].clone(),
tokens: st
.abstract_flows
.get(&flow_id)
.map(|f| f.last_prompt_tokens)
.unwrap_or(0),
ceiling: env.llm_context_tokens,
}
.to_string();
fail_abstract_coded(env, st, i, path, &detail);
}
FailCause::Unknown
| FailCause::UserAborted
| FailCause::SchemaValidationFailed => {
fail_abstract(env, st, i, path, detail);
}
},
}
}
fn handle_flow_tool_outcome(
env: &StepEnv<'_>,
st: &mut SchedulerState,
i: usize,
path: &str,
effect_seq: u32,
outcome: &EffectOutcome,
) {
let attempt = flow_attempt(st, i, path);
let cap = env.llm_tool_result_chars;
let Some(flow) = st.abstract_flows.get_mut(&flow_key(i, path)) else { return };
let Some(call) = flow.pending_tools.remove(&effect_seq) else { return };
let entry = match outcome {
EffectOutcome::Completed { result, .. } => serde_json::json!({
"role": "tool",
"tool_call_id": call.model_call_id,
"content": bound_tool_content(result, cap, attempt, effect_seq),
"is_error": false,
}),
EffectOutcome::Failed { cause, detail, .. } => serde_json::json!({
"role": "tool",
"tool_call_id": call.model_call_id,
"content": bound_tool_content(
&Value::String(format!("{cause:?}: {detail}")),
cap,
attempt,
effect_seq,
),
"is_error": true,
}),
};
flow.round_results.insert(effect_seq, entry);
if flow.pending_tools.is_empty() {
let entries: Vec<Value> = flow.round_results.values().cloned().collect();
flow.messages.extend(entries);
flow.round_results.clear();
flow.need = Some(crate::state::FlowNeed::NextTurn);
}
}
fn progress(env: &StepEnv<'_>, st: &mut SchedulerState, out: &mut Vec<Command>) {
loop {
if st.is_terminal() {
return;
}
if matches!(st.phase, Phase::Open { .. }) {
if progress_open(env, st, out) {
continue;
}
return;
}
if !progress_idle(env, st, out) {
return;
}
}
}
fn progress_open(env: &StepEnv<'_>, st: &mut SchedulerState, out: &mut Vec<Command>) -> bool {
let draining = st.cancel.is_some() || st.exhausted.is_some();
if !draining {
let needy = flow_owners(st);
if !needy.is_empty() {
for (i, path) in needy {
let need =
st.abstract_flows.get_mut(&flow_key(i, &path)).and_then(|f| f.need.take());
match need {
Some(crate::state::FlowNeed::NextTurn) => {
dispatch_llm_turn(env, st, i, &path, out);
}
Some(crate::state::FlowNeed::Tools(calls)) => {
for call in calls {
dispatch_flow_tool(env, st, i, &path, call, out);
}
}
None => {}
}
}
return true;
}
}
let retry_nodes: Vec<usize> = (0..env.plan.nodes.len())
.filter(|&i| st.node_state[i] == NodeState::Ready)
.collect();
if !retry_nodes.is_empty() && !draining {
for i in retry_nodes {
dispatch_node(env, st, i, out);
}
return true;
}
let retry_tasks: Vec<String> = st
.send_tasks
.iter()
.filter(|(_, t)| t.state == crate::state::SendTaskState::Queued)
.map(|(p, _)| p.clone())
.collect();
if !retry_tasks.is_empty() && !draining {
for p in retry_tasks {
dispatch_send_task(env, st, &p, out);
}
return true;
}
let (outstanding_empty, only_asks_left) = match &st.phase {
Phase::Open { outstanding, .. } => {
let only_asks = !outstanding.is_empty()
&& outstanding
.iter()
.all(|k| st.pending_asks.values().any(|p| p.key == *k));
(outstanding.is_empty(), only_asks)
}
_ => unreachable!("progress_open requires Phase::Open"),
};
if outstanding_empty || (st.cancel.is_some() && only_asks_left) {
if st.cancel.is_some() && only_asks_left {
if let Phase::Open { outstanding, .. } = &mut st.phase {
outstanding.clear();
}
st.pending_asks.clear();
st.announced_asks.clear();
}
close_superstep(env, st, out);
return true;
}
if only_asks_left {
if let Some(open) = st.wall_open.take() {
st.spent.wall_ms += st.clock_ms.saturating_sub(open);
st.paused_at = Some(st.clock_ms);
}
park(st, out);
}
false
}
fn progress_idle(env: &StepEnv<'_>, st: &mut SchedulerState, out: &mut Vec<Command>) -> bool {
if let Some((by, reason)) = st.cancel.clone() {
finish(st, out, RunOutcome::Canceled { by, reason });
return false;
}
if let Some(axis) = st.exhausted {
finish(st, out, RunOutcome::BudgetExhausted { axis });
return false;
}
if let Some((idx, detail)) = st.failed.clone() {
finish(
st,
out,
RunOutcome::Failed { node: env.plan.nodes[idx].clone(), detail },
);
return false;
}
let ready: Vec<usize> = (0..env.plan.nodes.len())
.filter(|&i| st.node_state[i] == NodeState::Ready)
.collect();
let queued_tasks: Vec<String> = st
.send_tasks
.iter()
.filter(|(_, t)| t.state == crate::state::SendTaskState::Queued)
.map(|(p, _)| p.clone())
.collect();
let needy_flows = flow_owners(st);
if ready.is_empty() && queued_tasks.is_empty() && needy_flows.is_empty() {
if st.superstep == 0 && st.node_state[0] == NodeState::Waiting {
return false;
}
if st
.node_state
.iter()
.any(|s| matches!(s, NodeState::Dispatched | NodeState::AwaitingClient))
{
return false;
}
let completed = env
.plan
.terminals()
.iter()
.any(|&t| st.node_state[t] == NodeState::DoneOk);
if completed {
finish(st, out, RunOutcome::Completed);
} else {
let node = stalled_culprit(env, st);
finish(st, out, RunOutcome::Stalled { node });
}
return false;
}
let axes = [
(BudgetAxis::Supersteps, st.spent.supersteps, Some(env.budgets.max_supersteps)),
(BudgetAxis::WallMs, st.spent.wall_ms, env.budgets.max_wall_ms),
(
BudgetAxis::Tokens,
st.spent.input_tokens + st.spent.output_tokens,
env.budgets.max_tokens,
),
(BudgetAxis::Usd, st.spent.usd_micros, env.budgets.max_usd_micros),
(BudgetAxis::Storage, st.spent.storage_bytes, env.budgets.max_storage_bytes),
];
for (axis, spent, max) in axes {
if let Some(max) = max {
if spent >= max {
finish(st, out, RunOutcome::BudgetExhausted { axis });
return false;
}
}
}
st.superstep += 1;
st.spent.supersteps += 1;
let resumed_at = st.paused_at.take().map(|paused| {
st.elapsed_ms += st.clock_ms.saturating_sub(paused);
st.clock_ms
});
st.wall_open = Some(st.clock_ms);
st.phase = Phase::Open {
outstanding: Default::default(),
results: BTreeMap::new(),
send_results: BTreeMap::new(),
record: DecisionRecord {
superstep: st.superstep,
clock_open_ms: st.clock_ms,
resumed_at,
..DecisionRecord::default()
},
};
apply_inbox(st);
for i in ready {
dispatch_node(env, st, i, out);
}
for p in queued_tasks {
dispatch_send_task(env, st, &p, out);
}
for (i, path) in needy_flows {
let need = st.abstract_flows.get_mut(&flow_key(i, &path)).and_then(|fl| fl.need.take());
match need {
Some(crate::state::FlowNeed::NextTurn) => dispatch_llm_turn(env, st, i, &path, out),
Some(crate::state::FlowNeed::Tools(calls)) => {
for call in calls {
dispatch_flow_tool(env, st, i, &path, call, out);
}
}
None => {}
}
}
true
}
fn dispatch_send_task(
env: &StepEnv<'_>,
st: &mut SchedulerState,
path: &str,
out: &mut Vec<Command>,
) {
let Some(task) = st.send_tasks.get_mut(path) else { return };
task.attempt += 1;
task.state = crate::state::SendTaskState::Running;
let (target, attempt, input) = (task.node_idx, task.attempt, task.input.clone());
let executor = env.executors[target].clone();
if let NodeExecutor::Abstract { .. } = &executor {
start_flow(env, st, target, path, input, out);
return;
}
let key = JournalKey {
run_id: st.run_id.clone(),
task_path: path.to_string(),
node: env.plan.nodes[target].clone(),
attempt,
effect_seq: 0,
kind: EffectKind::Tool,
};
let clock = st.clock_ms;
let superstep = st.superstep;
if let Phase::Open { outstanding, record, .. } = &mut st.phase {
outstanding.insert(key.clone());
record.task_dispatched.push((path.to_string(), attempt));
}
out.push(Command::WriteIntent {
key: key.clone(),
executor: executor.clone(),
input: input.clone(),
superstep,
clock_ms: clock,
});
out.push(Command::Dispatch { key, executor, input });
}
fn dispatch_node(env: &StepEnv<'_>, st: &mut SchedulerState, i: usize, out: &mut Vec<Command>) {
st.attempt[i] += 1;
let executor = env.executors[i].clone();
if let NodeExecutor::Abstract { .. } = &executor {
st.node_state[i] = NodeState::Dispatched;
let input = st.context.clone();
start_flow(env, st, i, "", input, out);
return;
}
let key = JournalKey {
run_id: st.run_id.clone(),
task_path: String::new(),
node: env.plan.nodes[i].clone(),
attempt: st.attempt[i],
effect_seq: 0,
kind: EffectKind::Tool,
};
let input = st.context.clone();
let clock = st.clock_ms;
let superstep = st.superstep;
{
let Phase::Open { outstanding, record, .. } = &mut st.phase else {
return;
};
outstanding.insert(key.clone());
record.dispatched.push((i, st.attempt[i]));
}
out.push(Command::WriteIntent {
key: key.clone(),
executor: executor.clone(),
input: input.clone(),
superstep,
clock_ms: clock,
});
match &executor {
NodeExecutor::Host { .. } | NodeExecutor::Subgraph { .. } => {
st.node_state[i] = NodeState::Dispatched;
out.push(Command::Dispatch { key, executor, input });
}
NodeExecutor::Client { tool_name, .. } => {
st.node_state[i] = NodeState::AwaitingClient;
let ask = Ask {
tool_call_id: key.tool_call_id(),
node: env.plan.nodes[i].clone(),
tool_name: tool_name.clone(),
input,
expires_at_sec: env
.ask_ttl_sec
.map(|ttl| (st.clock_ms / 1000) as i64 + ttl),
approval: true,
};
st.pending_asks
.insert(ask.tool_call_id.clone(), PendingAsk { key, node_idx: i, ask });
}
NodeExecutor::Abstract { .. } => unreachable!("handled above"),
}
}
fn fold_range(messages: &[Value]) -> Option<(usize, usize)> {
let from = 1;
let mut to = messages.len().saturating_sub(KEEP_TAIL);
if to <= from {
return None;
}
while to < messages.len() && messages[to].get("role").and_then(Value::as_str) == Some("tool") {
to += 1;
}
if to <= from || to > messages.len() {
return None;
}
Some((from, to))
}
fn emit_fold_turn(
env: &StepEnv<'_>,
st: &mut SchedulerState,
i: usize,
path: &str,
from: usize,
to: usize,
out: &mut Vec<Command>,
) {
let flow_id = flow_key(i, path);
let Some(flow) = st.abstract_flows.get_mut(&flow_id) else { return };
if flow.next_effect_seq >= env.max_effects_per_attempt {
fail_abstract(env, st, i, path, "llm loop exceeded max_effects_per_attempt");
return;
}
let effect_seq = flow.next_effect_seq;
flow.next_effect_seq += 1;
flow.need = None;
let mut messages: Vec<Value> = flow.messages[from..to].to_vec();
messages.push(serde_json::json!({ "role": "user", "content": FOLD_PROMPT }));
flow.folding = Some(crate::state::FoldInFlight { from, to, effect_seq });
flow.fold_forced = false;
let key = JournalKey {
run_id: st.run_id.clone(),
task_path: path.to_string(),
node: env.plan.nodes[i].clone(),
attempt: flow_attempt(st, i, path),
effect_seq,
kind: EffectKind::Llm,
};
let input = serde_json::json!({
"messages": messages,
"fold": { "from": from, "to": to, "seq": effect_seq, "prompt_v": FOLD_PROMPT_V },
});
emit_effect(env, st, i, path, key, input, out);
}
fn dispatch_llm_turn(
env: &StepEnv<'_>,
st: &mut SchedulerState,
i: usize,
path: &str,
out: &mut Vec<Command>,
) {
let flow_id = flow_key(i, path);
if let Some(max) = env.budgets.max_tokens {
let spent = st.spent.input_tokens + st.spent.output_tokens;
if spent + env.llm_reserve_tokens > max {
st.exhausted = Some(crate::error::BudgetAxis::Tokens);
if let Some(flow) = st.abstract_flows.get_mut(&flow_id) {
flow.need = Some(crate::state::FlowNeed::NextTurn);
}
return;
}
}
{
let flow_now = st.abstract_flows.get(&flow_id);
let projected = flow_now
.map(|f| f.last_prompt_tokens)
.unwrap_or(0)
.saturating_add(env.llm_reserve_tokens);
let idle = flow_now.is_some_and(|f| f.folding.is_none());
let forced = flow_now.is_some_and(|f| f.fold_forced);
let over_ceiling = env.llm_context_tokens.is_some_and(|c| projected > c);
if idle && (forced || over_ceiling) {
match st.abstract_flows.get(&flow_id).and_then(|f| fold_range(&f.messages)) {
Some((from, to)) => {
emit_fold_turn(env, st, i, path, from, to, out);
return;
}
None => {
let tokens = st
.abstract_flows
.get(&flow_id)
.map(|f| f.last_prompt_tokens)
.unwrap_or(0);
let detail = RunError::ContextExceeded {
node: env.plan.nodes[i].clone(),
tokens,
ceiling: env.llm_context_tokens,
}
.to_string();
fail_abstract_coded(env, st, i, path, &detail);
return;
}
}
}
}
let Some(flow) = st.abstract_flows.get_mut(&flow_id) else { return };
if flow.next_effect_seq >= env.max_effects_per_attempt {
fail_abstract(env, st, i, path, "llm loop exceeded max_effects_per_attempt");
return;
}
let effect_seq = flow.next_effect_seq;
flow.next_effect_seq += 1;
flow.need = None;
let messages = flow.messages.clone();
let key = JournalKey {
run_id: st.run_id.clone(),
task_path: path.to_string(),
node: env.plan.nodes[i].clone(),
attempt: flow_attempt(st, i, path),
effect_seq,
kind: EffectKind::Llm,
};
let input = serde_json::json!({ "messages": messages });
emit_effect(env, st, i, path, key, input, out);
}
fn dispatch_flow_tool(
env: &StepEnv<'_>,
st: &mut SchedulerState,
i: usize,
path: &str,
call: crate::state::PendingToolCall,
out: &mut Vec<Command>,
) {
let flow_id = flow_key(i, path);
if st.exhausted.is_some() {
if let Some(flow) = st.abstract_flows.get_mut(&flow_id) {
match &mut flow.need {
Some(crate::state::FlowNeed::Tools(calls)) => calls.push(call),
other => *other = Some(crate::state::FlowNeed::Tools(vec![call])),
}
}
return;
}
let Some(flow) = st.abstract_flows.get_mut(&flow_id) else { return };
if flow.next_effect_seq >= env.max_effects_per_attempt {
fail_abstract(env, st, i, path, "llm loop exceeded max_effects_per_attempt");
return;
}
let NodeExecutor::Abstract { tools } = &env.executors[i] else { return };
let Some(tool) = tools.iter().find(|t| t.tool_name == call.tool_name).cloned() else {
fail_abstract(
env,
st,
i,
path,
&format!("offered tool '{}' missing from the manifest's executors", call.tool_name),
);
return;
};
let Some(flow) = st.abstract_flows.get_mut(&flow_id) else { return };
let effect_seq = flow.next_effect_seq;
flow.next_effect_seq += 1;
let arguments = call.arguments.clone();
flow.pending_tools.insert(effect_seq, call.clone());
let key = JournalKey {
run_id: st.run_id.clone(),
task_path: path.to_string(),
node: env.plan.nodes[i].clone(),
attempt: flow_attempt(st, i, path),
effect_seq,
kind: EffectKind::Tool,
};
let host = NodeExecutor::Host {
tool_hash: tool.tool_hash.clone(),
tool_name: tool.tool_name.clone(),
};
let clock = st.clock_ms;
let superstep = st.superstep;
if let Phase::Open { outstanding, .. } = &mut st.phase {
outstanding.insert(key.clone());
}
out.push(Command::WriteIntent {
key: key.clone(),
executor: host.clone(),
input: arguments.clone(),
superstep,
clock_ms: clock,
});
out.push(Command::Dispatch { key, executor: host, input: arguments });
}
fn emit_effect(
env: &StepEnv<'_>,
st: &mut SchedulerState,
i: usize,
path: &str,
key: JournalKey,
input: Value,
out: &mut Vec<Command>,
) {
let executor = env.executors[i].clone();
let clock = st.clock_ms;
let superstep = st.superstep;
if let Phase::Open { outstanding, record, .. } = &mut st.phase {
outstanding.insert(key.clone());
if key.effect_seq == 0 {
if path.is_empty() {
record.dispatched.push((i, key.attempt));
} else {
record.task_dispatched.push((path.to_string(), key.attempt));
}
}
}
out.push(Command::WriteIntent {
key: key.clone(),
executor: executor.clone(),
input: input.clone(),
superstep,
clock_ms: clock,
});
out.push(Command::Dispatch { key, executor, input });
}
fn fail_abstract(
env: &StepEnv<'_>,
st: &mut SchedulerState,
i: usize,
path: &str,
detail: &str,
) {
fail_abstract_coded(env, st, i, path, &format!("ExecutorError: {detail}"));
}
fn fail_abstract_coded(
env: &StepEnv<'_>,
st: &mut SchedulerState,
i: usize,
path: &str,
detail: &str,
) {
st.abstract_flows.remove(&flow_key(i, path));
if let Some(task) = st.send_tasks.get_mut(path) {
task.state = crate::state::SendTaskState::Done;
}
st.node_state[i] = NodeState::DoneFailed;
let named = st.failed.as_ref().map(|(n, _)| *n).unwrap_or(usize::MAX);
if i < named {
let at = if path.is_empty() { String::new() } else { format!(" (task {path})") };
st.failed = Some((i, format!("{detail}{at}")));
}
let _ = env;
}
fn park(st: &mut SchedulerState, out: &mut Vec<Command>) {
let fresh: Vec<Ask> = st
.pending_asks
.values()
.filter(|p| !st.announced_asks.contains(&p.ask.tool_call_id))
.map(|p| p.ask.clone())
.collect();
if fresh.is_empty() {
return;
}
for a in &fresh {
st.announced_asks.insert(a.tool_call_id.clone());
}
if let Phase::Open { record, .. } = &st.phase {
let mut record = record.clone();
record.clock_close_ms = st.clock_ms;
let state_json = serde_json::to_value(&*st).unwrap_or(Value::Null);
out.push(Command::WriteCheckpoint {
superstep: st.superstep,
decision_record: record,
state_json,
});
}
out.push(Command::EmitEnvelope { asks: fresh });
}
fn close_superstep(env: &StepEnv<'_>, st: &mut SchedulerState, out: &mut Vec<Command>) {
let Phase::Open { mut results, mut send_results, mut record, .. } =
std::mem::replace(&mut st.phase, Phase::Idle)
else {
return;
};
if st.failed.is_none() && st.cancel.is_none() {
let mut decisions: Vec<(String, usize, Vec<Value>)> = Vec::new();
for (i, r) in results.iter_mut() {
if let Some(list) = take_send(r) {
decisions.push((String::new(), *i, list));
}
}
let task_nodes: BTreeMap<String, usize> = st
.send_tasks
.iter()
.map(|(p, t)| (p.clone(), t.node_idx))
.collect();
for (path, r) in send_results.iter_mut() {
if let Some(list) = take_send(r) {
decisions.push((path.clone(), task_nodes[path], list));
}
}
apply_spawns(env, st, &mut record, decisions);
}
let send_merges: Vec<(usize, Value)> = send_results
.iter()
.map(|(p, v)| (st.send_tasks[p].node_idx, v.clone()))
.collect();
for (node_idx, result) in results
.iter()
.map(|(i, v)| (*i, v.clone()))
.chain(send_merges)
{
match result {
Value::Object(map) => {
for (k, v) in map {
let prev = st.context.get(&k).cloned();
let merged = (env.reduce)(&k, prev.as_ref(), &v);
st.context[k.as_str()] = merged;
}
}
Value::Null => {}
other => {
let key = env.plan.nodes[node_idx].clone();
let prev = st.context.get(&key).cloned();
let merged = (env.reduce)(&key, prev.as_ref(), &other);
st.context[key.as_str()] = merged;
}
}
}
if st.failed.is_none() && st.cancel.is_none() {
for i in results.keys().copied().collect::<Vec<_>>() {
for ei in env.plan.out_edges[i].clone() {
let edge = &env.plan.edges[ei];
let outcome = if edge.max_cycles.is_some_and(|m| st.edge_fired[ei] >= m) {
EdgeOutcome::CycleExhausted
} else {
let fires = (edge.cond.is_none() && edge.cond_raw.is_none())
|| (env.eval_cond)(edge, &st.context);
if fires { EdgeOutcome::Fired } else { EdgeOutcome::CondFalse }
};
if outcome == EdgeOutcome::Fired {
st.edge_fired[ei] += 1;
}
let gen = deliver(env, st, ei, outcome == EdgeOutcome::Fired);
record.edges.push((ei, gen, outcome));
}
}
propagate_death(env, st, &mut record);
}
record.clock_close_ms = st.clock_ms;
if let Some(open) = st.wall_open.take() {
st.spent.wall_ms += st.clock_ms.saturating_sub(open);
}
if let Some(paused) = st.paused_at.take() {
st.elapsed_ms += st.clock_ms.saturating_sub(paused);
}
let state_json = serde_json::to_value(&*st).unwrap_or(Value::Null);
out.push(Command::WriteCheckpoint {
superstep: st.superstep,
decision_record: record,
state_json,
});
}
fn take_send(result: &mut Value) -> Option<Vec<Value>> {
let obj = result.as_object_mut()?;
let taken = obj.remove("$send")?;
match taken {
Value::Array(list) => Some(list),
other => Some(vec![other]), }
}
const MAX_SPAWNS: usize = 4096;
fn apply_spawns(
env: &StepEnv<'_>,
st: &mut SchedulerState,
record: &mut DecisionRecord,
decisions: Vec<(String, usize, Vec<Value>)>,
) {
let fail = |st: &mut SchedulerState, spawner: usize, why: String| {
st.node_state[spawner] = NodeState::DoneFailed;
let named = st.failed.as_ref().map(|(i, _)| *i).unwrap_or(usize::MAX);
if spawner < named {
st.failed = Some((spawner, format!("ExecutorError: {why}")));
}
};
type ValidatedSpawn = (String, usize, Vec<(usize, Value)>);
let mut validated: Vec<ValidatedSpawn> = Vec::new();
for (parent, spawner, list) in decisions {
if list.len() > MAX_SPAWNS {
fail(st, spawner, format!("$send spawns {} tasks (cap {MAX_SPAWNS})", list.len()));
return;
}
let mut entries = Vec::with_capacity(list.len());
for e in &list {
let Some(target_name) = e.get("node").and_then(|n| n.as_str()) else {
fail(
st,
spawner,
"$send entries must be {\"node\": \"<name>\", \"input\": …}".into(),
);
return;
};
let Some(target) = env.plan.nodes.iter().position(|n| n == target_name) else {
fail(st, spawner, format!("$send targets unknown node '{target_name}'"));
return;
};
if !matches!(
env.executors[target],
NodeExecutor::Host { .. } | NodeExecutor::Abstract { .. }
) {
fail(
st,
spawner,
format!("$send target '{target_name}' is not a Host tool or abstract node"),
);
return;
}
if target == spawner {
fail(st, spawner, format!("$send target '{target_name}' is the spawner itself"));
return;
}
let input = e.get("input").cloned().unwrap_or_else(|| serde_json::json!({}));
entries.push((target, input));
}
validated.push((parent, spawner, entries));
}
for (parent, spawner, entries) in validated {
for (target, input) in entries {
let counter = st.spawn_counter.entry(parent.clone()).or_insert(0);
let ordinal = *counter;
if ordinal > 9999 {
fail(st, spawner, format!(
"spawn ordinal space exhausted under '{parent}' (9999 max per parent)"
));
return;
}
*counter += 1;
let path = format!("{parent}/{ordinal:04}");
st.send_tasks.insert(
path.clone(),
crate::state::SendTask {
node_idx: target,
input,
attempt: 0,
state: crate::state::SendTaskState::Queued,
},
);
st.node_state[target] = NodeState::Dispatched;
record.spawns.push((path, target));
}
}
}
fn deliver(env: &StepEnv<'_>, st: &mut SchedulerState, ei: usize, fired: bool) -> u32 {
let dst = env.plan.edges[ei].dst;
let resolved = matches!(
st.node_state[dst],
NodeState::DoneOk | NodeState::DoneFailed | NodeState::Dead
);
if resolved {
if !fired {
return st.node_gen[dst]; }
st.node_gen[dst] += 1;
st.node_state[dst] = NodeState::Waiting;
st.attempt_base[dst] = st.attempt[dst];
st.in_res[dst].clear();
for &other in &env.plan.in_edges[dst] {
if other == ei {
continue;
}
let src = env.plan.edges[other].src;
let src_unresolved = matches!(
st.node_state[src],
NodeState::Waiting
| NodeState::Ready
| NodeState::Dispatched
| NodeState::AwaitingClient
);
if !src_unresolved {
st.in_res[dst].insert(other, EdgeRes::Dead);
}
}
st.in_res[dst].insert(ei, EdgeRes::Fired);
} else {
st.in_res[dst]
.insert(ei, if fired { EdgeRes::Fired } else { EdgeRes::Dead });
}
refresh_readiness(env, st, dst);
st.node_gen[dst]
}
fn refresh_readiness(env: &StepEnv<'_>, st: &mut SchedulerState, i: usize) {
if st.node_state[i] != NodeState::Waiting {
return;
}
let ins = &env.plan.in_edges[i];
let gen0 = st.node_gen[i] == 0;
let all_gated_resolved = ins
.iter()
.all(|&e| (gen0 && env.plan.cycle_edge[e]) || st.in_res[i].contains_key(&e));
if !all_gated_resolved {
return;
}
let any_fired = ins.iter().any(|e| st.in_res[i].get(e) == Some(&EdgeRes::Fired));
st.node_state[i] = if any_fired { NodeState::Ready } else { NodeState::Dead };
}
fn propagate_death(env: &StepEnv<'_>, st: &mut SchedulerState, record: &mut DecisionRecord) {
loop {
let mut changed = false;
for i in 0..env.plan.nodes.len() {
if st.node_state[i] != NodeState::Dead {
continue;
}
for &ei in &env.plan.out_edges[i] {
let dst = env.plan.edges[ei].dst;
if st.node_state[dst] == NodeState::Waiting
&& !st.in_res[dst].contains_key(&ei)
{
st.in_res[dst].insert(ei, EdgeRes::Dead);
record.edges.push((ei, st.node_gen[dst], EdgeOutcome::DeadPath));
refresh_readiness(env, st, dst);
changed = true;
}
}
}
if !changed {
return;
}
}
}
fn stalled_culprit(env: &StepEnv<'_>, st: &SchedulerState) -> String {
for i in 0..env.plan.nodes.len() {
if st.node_state[i] == NodeState::DoneOk
&& !env.plan.out_edges[i].is_empty()
&& env.plan.out_edges[i].iter().all(|&ei| st.edge_fired[ei] == 0)
{
return env.plan.nodes[i].clone();
}
}
for i in 0..env.plan.nodes.len() {
if st.node_state[i] == NodeState::Waiting {
return env.plan.nodes[i].clone();
}
}
env.plan.nodes[0].clone()
}
fn finish(st: &mut SchedulerState, out: &mut Vec<Command>, outcome: RunOutcome) {
st.phase = Phase::Finished(outcome.clone());
out.push(Command::Finish { outcome });
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn no_cap_passes_the_result_through_verbatim() {
let big = json!({"rows": vec!["x"; 5_000]});
assert_eq!(bound_tool_content(&big, None, 3, 7), big);
}
#[test]
fn the_cap_is_inclusive() {
let at = Value::String("a".repeat(64));
assert_eq!(bound_tool_content(&at, Some(64), 0, 0), at);
let over = Value::String("a".repeat(65));
let bounded = bound_tool_content(&over, Some(64), 3, 7);
assert_eq!(bounded["truncated"], true);
assert_eq!(bounded["chars"], 65, "the TRUE length, not the kept length");
assert_eq!(bounded["head"].as_str().unwrap().chars().count(), 32);
assert_eq!(bounded["tail"].as_str().unwrap().chars().count(), 32);
assert_eq!(bounded["journal"], json!({"attempt": 3, "effect_seq": 7}));
}
#[test]
fn a_multibyte_result_is_cut_on_character_boundaries() {
let text: String = "日本語です".repeat(8);
assert_eq!(text.chars().count(), 40);
assert!(text.len() > 40, "the point of the case");
let bounded = bound_tool_content(&Value::String(text.clone()), Some(20), 1, 2);
assert_eq!(bounded["chars"], 40);
let head = bounded["head"].as_str().unwrap();
let tail = bounded["tail"].as_str().unwrap();
assert_eq!(head.chars().count(), 10);
assert_eq!(tail.chars().count(), 10);
assert!(text.starts_with(head), "the head is a real prefix");
assert!(text.ends_with(tail), "the tail is a real suffix");
}
#[test]
fn a_json_result_is_bounded_as_its_serialized_form() {
let big = json!({"k": "v".repeat(200)});
let bounded = bound_tool_content(&big, Some(40), 0, 1);
assert_eq!(bounded["chars"], big.to_string().chars().count());
assert!(bounded["head"].as_str().unwrap().starts_with(r#"{"k":"#), "{bounded}");
assert!(bounded["tail"].as_str().unwrap().ends_with(r#""}"#), "{bounded}");
}
#[test]
fn a_cap_of_zero_keeps_nothing_and_still_points_at_the_journal() {
let bounded = bound_tool_content(&Value::String("abc".into()), Some(0), 9, 4);
assert_eq!(bounded["chars"], 3);
assert_eq!(bounded["head"], "");
assert_eq!(bounded["tail"], "");
assert_eq!(bounded["journal"]["effect_seq"], 4);
}
fn msg(role: &str) -> Value {
json!({"role": role, "content": "x"})
}
#[test]
fn nothing_to_fold_below_the_kept_tail() {
for n in 0..=KEEP_TAIL + 1 {
let messages: Vec<Value> = (0..n).map(|_| msg("user")).collect();
assert_eq!(fold_range(&messages), None, "len {n} must not be foldable");
}
}
#[test]
fn the_nodes_input_is_never_folded() {
let messages: Vec<Value> = (0..12).map(|_| msg("user")).collect();
let (from, to) = fold_range(&messages).expect("a middle exists");
assert_eq!(from, 1, "messages[0] is the node's own input");
assert_eq!(to, 12 - KEEP_TAIL);
}
#[test]
fn the_cut_never_separates_a_tool_result_from_its_assistant_turn() {
let messages = vec![
msg("user"), msg("assistant"), msg("tool"), msg("user"), msg("assistant"), msg("tool"), msg("user"), msg("assistant"), msg("tool"), msg("tool"), msg("user"), msg("assistant"), ];
let (from, to) = fold_range(&messages).expect("a middle exists");
assert_eq!(from, 1);
assert_eq!(to, 10, "advanced past both tool entries");
assert_ne!(
messages[to].get("role").and_then(Value::as_str),
Some("tool"),
"the entry AFTER the fold must never be an orphaned tool result"
);
}
#[test]
fn a_round_that_fills_the_tail_folds_entirely_rather_than_orphaning_it() {
let mut messages = vec![msg("user"), msg("assistant")];
messages.extend((0..8).map(|_| msg("tool")));
assert_eq!(fold_range(&messages), Some((1, 10)));
}
}