use kernel::{
AxError, Completion, EventDraft, EventKind, Evidence, Ledger, Model, Payload, StopReason,
};
use crate::conversation::Conversation;
use crate::handoff::Handoff;
use crate::prefix::shape::PromptShape;
use crate::reminder::ContextGauge;
use crate::turn::{Generating, Interrupt, PhaseOutcome, RunPrompt, Turn, TurnReport};
use super::checkpoint::{CheckpointPolicy, Wave, WaveCheckpoint};
use super::{Active, Advance, Frozen, Run, RunHooks, RunPlan, SafePoint};
fn fold_steer(conversation: &mut Conversation, interrupt: &Interrupt) {
if let Interrupt::Steer { source, text } = interrupt {
conversation.push_steer(source, text);
}
}
fn concluded(report: &TurnReport) -> Result<Completion, AxError> {
if report.assistant().is_empty() || report.stop() == Some(StopReason::MaxTokens) {
return Ok(Completion::Limit);
}
Ok(Completion::Done(Evidence::new(vec![
*report.model_returned(),
])?))
}
impl Run<Active> {
pub fn dispatch(
plan: RunPlan,
ledger: &mut dyn Ledger,
hooks: &mut RunHooks<'_>,
) -> Result<Run<Active>, AxError> {
plan.charter().open(ledger, &mut *hooks.now)?;
let mut conversation = Conversation::new();
conversation.push_inherited(&plan.inherited);
conversation.push_task_lines(&plan.task, &plan.goal, plan.opening);
let gauge = ContextGauge::new(
kernel::Tokens::new(plan.shape.context_tokens),
plan.second_threshold,
);
Ok(Run {
plan,
state: Active {
conversation,
turns: 0,
last_turn_t: None,
gauge,
prior_shape: None,
prompt: crate::turn::PromptRecord::default(),
checkpoint: CheckpointPolicy::opening(),
},
})
}
pub fn advance(
&mut self,
ledger: &mut dyn Ledger,
model: &mut dyn Model,
hooks: &mut RunHooks<'_>,
) -> Result<Advance, AxError> {
let index = self.state.turns;
let t = (hooks.now)()?;
self.state.last_turn_t = Some(t);
let turn = Turn::begin(self.plan.run, self.plan.who.clone(), t, &mut *hooks.now);
let opening = (hooks.interrupt)(SafePoint::BeforeAssemble { turn: index });
fold_steer(&mut self.state.conversation, &opening);
let turn = match turn.assemble(
opening,
ledger,
RunPrompt::new(&self.plan.prefix, &mut self.state.prompt),
&self.state.conversation,
&self.plan.tools,
&self.plan.shape,
)? {
PhaseOutcome::Advanced(next) => next,
PhaseOutcome::Cancelled(_) => return Ok(Advance::Concluded(Completion::Cancelled)),
};
let shape = PromptShape::of(
&self.plan.prefix,
&self.plan.tools,
self.state.conversation.messages(),
)?;
let changed = shape.attribute(self.state.prior_shape.as_ref());
ledger.append(EventDraft {
run: self.plan.run,
t,
who: self.plan.who.clone(),
addr: Some(self.plan.addr.clone()),
kind: EventKind::PromptShapeCompared,
data: Payload::of(&shape.recorded(changed)?)?,
ig: false,
})?;
self.state.prior_shape = Some(shape);
let calling = (hooks.interrupt)(SafePoint::BeforeCall { turn: index });
let called = turn.call(
calling.clone(),
ledger,
model,
&self.plan.policy,
Generating::Speculating {
deltas: hooks.deltas.as_deref_mut(),
tools: &*hooks.invoke,
},
);
self.state.conversation.mark_sent();
fold_steer(&mut self.state.conversation, &calling);
let turn = match called? {
PhaseOutcome::Advanced(next) => next,
PhaseOutcome::Cancelled(_) => return Ok(Advance::Concluded(Completion::Cancelled)),
};
if let Some(usage) = turn.usage() {
self.plan.context.record(usage.input_tokens);
}
let touches = Wave::of(turn.calls(), hooks.writes);
let decided = self.state.checkpoint.for_wave(touches);
if let (WaveCheckpoint::Stage, Some(checkpoint)) = (decided, hooks.checkpoint.as_mut()) {
let committed = checkpoint(t)?;
ledger.append(EventDraft {
run: self.plan.run,
t,
who: self.plan.who.clone(),
addr: Some(self.plan.addr.clone()),
kind: EventKind::CheckpointCommitted,
data: committed,
ig: false,
})?;
}
self.state.checkpoint.record_wave(decided, touches);
let wave = (hooks.interrupt)(SafePoint::BeforeWave { turn: index });
fold_steer(&mut self.state.conversation, &wave);
let asking = &mut hooks.interrupt;
let conversation = &mut self.state.conversation;
let mut still_going = |call: u32| {
let arrived = asking(SafePoint::BeforeToolCall { turn: index, call });
fold_steer(conversation, &arrived);
arrived
};
let turn =
match turn.execute_concurrent(wave, ledger, &mut *hooks.invoke, &mut still_going)? {
PhaseOutcome::Advanced(next) => next,
PhaseOutcome::Cancelled(_) => return Ok(Advance::Concluded(Completion::Cancelled)),
};
let settling = (hooks.interrupt)(SafePoint::BeforeSpawn { turn: index });
fold_steer(&mut self.state.conversation, &settling);
let report = match turn.record(settling, ledger)? {
PhaseOutcome::Advanced(report) => report,
PhaseOutcome::Cancelled(_) => return Ok(Advance::Concluded(Completion::Cancelled)),
};
self.state.turns = self.state.turns.saturating_add(1);
self.state
.conversation
.push_assistant(report.assistant().to_vec());
self.state
.conversation
.push_tool_results(report.wave_results().to_vec());
if let Some(reminder) = report
.usage()
.and_then(|usage| self.state.gauge.observe(usage.input_tokens))
{
self.state.conversation.push_reminder(&reminder);
}
if report.calls_made() == 0 {
return Ok(Advance::Concluded(concluded(&report)?));
}
Ok(Advance::Turned)
}
pub fn freeze(
self,
ledger: &mut dyn Ledger,
handoff: &Handoff,
completion: Completion,
hooks: &mut RunHooks<'_>,
) -> Result<Run<Frozen>, AxError> {
let t = match (&completion, self.state.last_turn_t) {
(Completion::Cancelled, Some(turn_t)) => turn_t,
_ => (hooks.now)()?,
};
self.plan.charter().close(ledger, handoff, &completion, t)?;
let turns = self.state.turns;
Ok(Run {
plan: self.plan,
state: Frozen {
completion,
turns,
conversation: self.state.conversation,
},
})
}
pub fn turns_taken(&self) -> u32 {
self.state.turns
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
reason = "test code"
)]
mod tests;