1use kernel::{
10 AxError, Carrier, Completion, EventDraft, Evidence, Ledger, Model, Payload, StopReason, TimeMs,
11};
12
13use crate::conversation::Conversation;
14use crate::handoff::Handoff;
15use crate::mode::{PolicyCell, policy_note};
16use crate::prefix::shape::PromptShape;
17use crate::reminder::ContextGauge;
18use crate::turn::{
19 Generating, HeldLines, Interrupt, PhaseOutcome, RunLine, RunPrompt, Turn, TurnReport,
20};
21
22use super::checkpoint::{CheckpointPolicy, Wave, WaveCheckpoint};
23use super::{Active, Advance, Frozen, Run, RunHooks, RunPlan, SafePoint};
24
25fn fold_steer(conversation: &mut Conversation, interrupt: &Interrupt) {
34 if let Interrupt::Steer { speaker, text } = interrupt {
35 conversation.push_steer(speaker, text);
36 }
37}
38
39fn hold_policy(cell: &mut PolicyCell, interrupt: &Interrupt) {
43 if let Interrupt::Policy { policy } = interrupt {
44 cell.arrive(*policy);
45 }
46}
47
48fn fold_arrival(conversation: &mut Conversation, policy: &mut PolicyCell, interrupt: &Interrupt) {
52 fold_steer(conversation, interrupt);
53 hold_policy(policy, interrupt);
54}
55
56fn concluded(report: &TurnReport, lines: &HeldLines) -> Result<Completion, AxError> {
71 if report.assistant().is_empty() || report.stop() == Some(StopReason::MaxTokens) {
72 return Ok(Completion::Limit);
73 }
74 Ok(Completion::Done(Evidence::new(vec![
75 lines.durable(report.model_returned())?,
76 ])?))
77}
78
79impl Run<Active> {
80 pub(super) fn line(&self, t: TimeMs, kind: kernel::EventKind, data: Payload) -> EventDraft {
82 EventDraft {
83 run: self.plan.run,
84 t,
85 who: self.plan.who.clone(),
86 addr: Some(self.plan.addr.clone()),
87 kind,
88 data,
89 ig: false,
90 }
91 }
92
93 pub(super) fn record_fired(
97 &self,
98 ledger: &mut dyn Ledger,
99 t: TimeMs,
100 disposal: &crate::Disposal,
101 ) -> Result<(), AxError> {
102 let data = self.state.watchdog.fired_payload(disposal)?;
103 ledger
104 .append(self.line(t, kernel::EventKind::WatchdogFired, data))
105 .map(|_| ())
106 }
107
108 pub(super) fn record_refusal(
111 &self,
112 ledger: &mut dyn Ledger,
113 t: TimeMs,
114 refusal: &AxError,
115 ) -> Result<(), AxError> {
116 let Carrier::Event(kind) = refusal.code().carrier() else {
117 return Err(refusal.clone());
118 };
119 ledger
120 .append(self.line(t, kind, Payload::of(refusal)?))
121 .map(|_| ())
122 }
123
124 pub fn dispatch(
133 plan: RunPlan,
134 ledger: &mut dyn Ledger,
135 hooks: &mut RunHooks<'_>,
136 ) -> Result<Run<Active>, AxError> {
137 plan.charter().open(ledger, &mut *hooks.now)?;
138 let lines = HeldLines::open(plan.run, plan.who.clone());
139
140 let mut conversation = Conversation::new();
141 conversation.push_inherited(&plan.inherited);
145 conversation.push_task_lines(
146 &plan.task,
147 &plan.goal,
148 plan.opening,
149 &plan
150 .dispatched_by
151 .handed_down_by(plan.predecessor, plan.parent),
152 );
153 let gauge = ContextGauge::new(
154 kernel::Tokens::new(plan.shape.context_tokens),
155 plan.second_threshold,
156 );
157 let watchdog = crate::Watchdog::new(plan.retries, plan.run);
158 Ok(Run {
159 plan,
160 state: Active {
161 conversation,
162 turns: 0,
163 last_turn_t: None,
164 gauge,
165 prior_shape: None,
166 prompt: crate::turn::PromptRecord::default(),
167 checkpoint: CheckpointPolicy::opening(),
168 lines,
169 watchdog,
170 },
171 })
172 }
173
174 pub fn advance(
182 &mut self,
183 ledger: &mut dyn Ledger,
184 model: &mut dyn Model,
185 hooks: &mut RunHooks<'_>,
186 ) -> Result<Advance, AxError> {
187 let index = self.state.turns;
188 let t = (hooks.now)()?;
189 self.state.last_turn_t = Some(t);
190
191 let turn =
192 Turn::begin(&mut self.state.lines, t, &mut *hooks.now).timed(&mut *hooks.monotonic_us);
193 let opening = (hooks.interrupt)(SafePoint::BeforeAssemble { turn: index });
194 fold_arrival(
195 &mut self.state.conversation,
196 &mut self.plan.run_policy,
197 &opening,
198 );
199 let mut turn = match turn.assemble(
200 opening,
201 ledger,
202 RunPrompt::new(&self.plan.prefix, &mut self.state.prompt),
203 &self.state.conversation,
204 &self.plan.tools,
205 &self.plan.shape,
206 )? {
207 PhaseOutcome::Advanced(next) => next,
208 PhaseOutcome::Cancelled(_) => return Ok(Advance::Concluded(Completion::Cancelled)),
209 };
210 let shape = PromptShape::of(
218 &self.plan.prefix,
219 &self.plan.tools,
220 self.state.conversation.messages(),
221 )?;
222 let changed = shape.attribute(self.state.prior_shape.as_ref());
223 turn.hold_run_line(
224 RunLine::PromptShapeCompared,
225 self.plan.addr.clone(),
226 Payload::of(&shape.recorded(changed)?)?,
227 );
228 self.state.prior_shape = Some(shape);
229 let calling = (hooks.interrupt)(SafePoint::BeforeCall { turn: index });
230 let called = turn.under(&mut self.state.watchdog).call(
231 calling.clone(),
232 ledger,
233 model,
234 &self.plan.policy,
235 Generating::Speculating {
238 deltas: hooks.deltas.as_deref_mut(),
239 tools: &*hooks.invoke,
240 },
241 );
242 self.state.conversation.mark_sent();
247 fold_arrival(
248 &mut self.state.conversation,
249 &mut self.plan.run_policy,
250 &calling,
251 );
252 let mut turn = match called? {
253 PhaseOutcome::Advanced(next) => next,
254 PhaseOutcome::Cancelled(_) => return Ok(Advance::Concluded(Completion::Cancelled)),
255 };
256
257 if let Some(usage) = turn.usage() {
260 self.plan.context.record(usage.input_tokens);
261 }
262 let touches = Wave::of(turn.calls(), hooks.writes);
265 let decided = self.state.checkpoint.for_wave(touches);
266 if let (WaveCheckpoint::Stage, Some(checkpoint)) = (decided, hooks.checkpoint.as_mut()) {
267 turn.hold_run_line(
268 RunLine::CheckpointCommitted,
269 self.plan.addr.clone(),
270 checkpoint(t)?,
271 );
272 }
273
274 self.state.checkpoint.record_wave(decided, touches);
275
276 let wave = (hooks.interrupt)(SafePoint::BeforeWave { turn: index });
277 fold_arrival(
278 &mut self.state.conversation,
279 &mut self.plan.run_policy,
280 &wave,
281 );
282 let taken = self.plan.run_policy.take_at_wave();
285 let asking = &mut hooks.interrupt;
292 let state = &mut self.state.conversation;
293 let cell = &mut self.plan.run_policy;
294 let mut still_going = |call: u32| {
295 let arrived = asking(SafePoint::BeforeToolCall { turn: index, call });
296 fold_arrival(state, cell, &arrived);
297 arrived
298 };
299 let turn =
300 match turn.execute_concurrent(wave, ledger, &mut *hooks.invoke, &mut still_going)? {
301 PhaseOutcome::Advanced(next) => next,
302 PhaseOutcome::Cancelled(_) => return Ok(Advance::Concluded(Completion::Cancelled)),
303 };
304
305 let settling = (hooks.interrupt)(SafePoint::BeforeSpawn { turn: index });
306 fold_arrival(
307 &mut self.state.conversation,
308 &mut self.plan.run_policy,
309 &settling,
310 );
311 let report = match turn.record(settling, ledger)? {
312 PhaseOutcome::Advanced(report) => report,
313 PhaseOutcome::Cancelled(_) => return Ok(Advance::Concluded(Completion::Cancelled)),
314 };
315 self.state.turns = self.state.turns.saturating_add(1);
316 self.state
317 .conversation
318 .push_assistant(report.assistant().to_vec());
319 self.state
320 .conversation
321 .push_tool_results(report.wave_results().to_vec());
322 if let Some(policy) = taken {
326 self.state
327 .conversation
328 .push_steer(&crate::conversation::Speaker::City, &policy_note(&policy));
329 }
330 if let Some(reminder) = report
334 .usage()
335 .and_then(|usage| self.state.gauge.observe(usage.input_tokens))
336 {
337 self.state.conversation.push_reminder(&reminder);
338 }
339 if report.calls_made() == 0 {
340 self.state.lines.barrier(ledger)?;
341 return Ok(Advance::Concluded(concluded(&report, &self.state.lines)?));
342 }
343 Ok(Advance::Turned)
344 }
345
346 pub fn freeze(
359 self,
360 ledger: &mut dyn Ledger,
361 handoff: &Handoff,
362 completion: Completion,
363 hooks: &mut RunHooks<'_>,
364 ) -> Result<Run<Frozen>, AxError> {
365 let mut lines = self.state.lines;
366 lines.barrier(ledger)?;
367 let t = match (&completion, self.state.last_turn_t) {
368 (Completion::Cancelled, Some(turn_t)) => turn_t,
369 _ => (hooks.now)()?,
370 };
371 self.plan.charter().close(ledger, handoff, &completion, t)?;
372 let turns = self.state.turns;
373 Ok(Run {
374 plan: self.plan,
375 state: Frozen {
376 completion,
377 turns,
378 conversation: self.state.conversation,
379 },
380 })
381 }
382
383 pub fn turns_taken(&self) -> u32 {
384 self.state.turns
385 }
386}
387
388#[cfg(test)]
389#[allow(
390 clippy::unwrap_used,
391 clippy::expect_used,
392 clippy::panic,
393 clippy::indexing_slicing,
394 reason = "test code"
395)]
396mod tests;