Skip to main content

runtime/
run.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The run driver: dispatch, turns, freeze — the single authority for the
7//! event sequence a run leaves behind. The real city and citysim call this
8//! same code, so the simulator's byte fixtures are evidence about
9//! production rather than about a second implementation of the same idea.
10//!
11//! Time arrives through the `now` hook and is never sampled here. The
12//! caller decides what a clock is: a counter in the simulator, the one
13//! sanctioned wall-clock sample in `bin::assembly`.
14//!
15//! `crates/runtime/spec/Run.lean` proves what the event sequence must
16//! hold (§8-15): `handoff_written` and `run_frozen` close every run, once.
17
18use kernel::{
19    Address, AxError, BuildingPolicy, Carrier, Completion, EventDraft, Ledger, Locator, Model,
20    Payload, RunId, TimeMs, ToolCall, ToolDef,
21};
22
23use kernel::ChatMessage;
24
25use crate::catalog::SkillPin;
26use crate::conversation::{Conversation, Opening};
27use crate::handoff::Handoff;
28use crate::prefix::FrozenPrefix;
29use crate::reminder::ContextGauge;
30use crate::turn::{CallShape, Interrupt};
31
32mod charter;
33mod checkpoint;
34mod harness;
35mod lifecycle;
36
37pub use charter::Charter;
38pub use harness::{Conclusion, Cut, HarnessRun};
39
40/// Everything constant about one run. Assembled by the caller, because
41/// what a prefix contains and which tools exist are decisions of the city
42/// that dispatches, not of the loop that runs.
43pub struct RunPlan {
44    pub run: RunId,
45    pub who: String,
46    pub addr: Address,
47    pub task: String,
48    pub goal: String,
49    /// Whether this session was handed a written task or a person.
50    /// Decided by the city when it laid the brief down, carried here so
51    /// the window and the prefix's run segment cannot disagree about
52    /// which situation the agent is in.
53    pub opening: Opening,
54    pub job: Locator,
55    /// The run that handed this work down, when one did. Written into
56    /// `run_started` so the tree is a fact anybody folding the ledger
57    /// can read, rather than an inference from two lines happening to
58    /// be next to each other.
59    pub parent: Option<RunId>,
60    /// The run this one replaces, when a resident succeeded itself.
61    /// Written into `run_started` beside `parent`, so a lineage is a
62    /// fact folded from the ledger rather than inferred from two runs
63    /// sharing an address.
64    pub predecessor: Option<RunId>,
65    /// Who dispatched this run, written into `run_started`: the line's
66    /// author is always the city's desk, so only the dispatch site
67    /// knows whether the person, the city or a resident sent it.
68    pub dispatched_by: kernel::event::Who,
69    /// The run policy this run was dispatched under, written into
70    /// `run_started` as it was chosen (`crates/kernel/Spec.lean` §8-77). Named apart
71    /// from `policy`, which is the building's, because the two answer
72    /// different questions: what the building allows, and what this
73    /// dispatch asked for inside that.
74    pub run_policy: kernel::RunPolicy,
75    /// The conversation this run starts from, when it is the first run of
76    /// a session that branched off another. Empty for every other run.
77    ///
78    /// It is the window's material and not the prefix's: a prefix segment
79    /// is a document a person can read and edit, while this is what the
80    /// model said and what the tools answered, and the ledger is where
81    /// those live (`runtime::fork::inherited_indexed` rebuilds them from it).
82    pub inherited: Vec<ChatMessage>,
83    pub shape: CallShape,
84    /// Where the context reminder's second rung sits for this run: the
85    /// configuration ladder's answer, or `None` when no layer stated one
86    /// and the policy default sounds. Frozen with the run like every
87    /// setting here, so "each rung sounds once per run" cannot depend on
88    /// when somebody edited a file.
89    pub second_threshold: Option<kernel::SecondThreshold>,
90    /// Where this run records the provider's count after each call. The
91    /// caller keeps a clone for whatever reports the reading, which is
92    /// `status`: the run writes it and nothing else does.
93    pub context: crate::ContextReading,
94    pub prefix: FrozenPrefix,
95    pub policy: BuildingPolicy,
96    pub tools: Vec<ToolDef>,
97    /// The skills this run's reading room admitted, and what each one
98    /// hashed to when the shelf was read. Written into `run_started`
99    /// beside `parent`, and for the same reason: once the process that
100    /// read the shelf is gone, the ledger is the only thing that can
101    /// still say which bytes this run was given.
102    pub skills: Vec<SkillPin>,
103    /// How many times this run may make a failed call again, as the
104    /// person set it on the endpoint it calls. Frozen with the run for
105    /// the reason every other setting here is: a number changed halfway
106    /// through would make the account of what this run did depend on
107    /// when somebody looked at a form.
108    pub retries: kernel::Retries,
109    /// The identity version this run's session froze, which
110    /// `run_started` records as it is (`crates/runtime/spec/Run.lean` §8-56).
111    pub naming: Option<kernel::B3Hash>,
112}
113
114/// Where the driver stops to ask whether anything arrived. The turn layer
115/// owns three cancellation-safe points; this enum is how the driver names
116/// them to a caller that knows nothing about turn internals.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum SafePoint {
119    BeforeAssemble {
120        turn: u32,
121    },
122    BeforeCall {
123        turn: u32,
124    },
125    BeforeWave {
126        turn: u32,
127    },
128    /// Before one call of the wave, asked once per call. A wave is as
129    /// many effects as the model asked for, so a halt that arrives
130    /// while the third of eight runs stops the fourth rather than the
131    /// next turn.
132    BeforeToolCall {
133        turn: u32,
134        call: u32,
135    },
136    /// After the wave, before the run acts on what the turn decided.
137    /// The one boundary a run that concluded still passes through, so a
138    /// cancel arriving that late stops the work the turn handed down
139    /// instead of arriving at a run that has already ended.
140    BeforeSpawn {
141        turn: u32,
142    },
143}
144
145/// What one turn did. Exhaustive on purpose: a new ending has to make
146/// every caller say what it does about it.
147#[derive(Debug)]
148pub enum Advance {
149    Turned,
150    Concluded(Completion),
151}
152
153/// The things the driver cannot decide for itself. Closures rather
154/// than traits: no second implementation exists yet, and this
155/// library introduces a trait at a seam that already has one.
156pub struct RunHooks<'a> {
157    /// The clock, called by the driver on its own thread and nowhere else:
158    /// once per dispatch line, once per turn, once when each model attempt
159    /// is sent and once when its reply is whole, once when each tool call
160    /// starts and once when it answers, and once per freeze that is not a
161    /// cancellation.
162    pub now: &'a mut dyn FnMut() -> Result<TimeMs, AxError>,
163    /// Answers what arrived at a safe point.
164    pub interrupt: &'a mut dyn FnMut(SafePoint) -> Interrupt,
165    /// The pre-wave checkpoint. `None` runs without a net, which
166    /// the tool layer refuses for anything that can delete.
167    pub checkpoint: Option<&'a mut dyn FnMut(TimeMs) -> Result<Payload, AxError>>,
168    /// What a call may write, by its declared effect, asked before the
169    /// wave runs: a wave whose every call answers `Nothing` changes no
170    /// file, so it needs no checkpoint of its own (§8-45).
171    pub writes: &'a dyn Fn(&ToolCall) -> kernel::Writes,
172    /// Runs a wave's tool calls in three stages (see
173    /// [`crate::ConcurrentInvoke`]). `admit` receives the turn's stamp; a
174    /// call's own moments are read by the turn from `now`, never by the
175    /// tool face, which holds no clock of its own.
176    pub invoke: &'a mut dyn crate::ConcurrentInvoke,
177    /// Holds the run until the moment the watchdog set for the next call
178    /// to a provider that failed, and answers whether that call may go.
179    ///
180    /// `Halted` comes back as soon as a halt reaches the run, without
181    /// waiting the rest out: a backoff grows to a minute, and a brake
182    /// that engages a minute late is not one. A counted clock answers
183    /// `Allowed` at once, because nothing it replays waits in real time.
184    pub wait: &'a mut dyn FnMut(TimeMs) -> crate::NextCall,
185    /// Where text goes while the model is still saying it.
186    ///
187    /// `None` runs the model without asking for a stream, which is what
188    /// citysim and every offline replay do: an increment changes nothing
189    /// a run decides, so a driver with nowhere to put one asks for none
190    /// and the call is byte-identical to what it always was.
191    ///
192    /// It takes no result, because nothing here may fail a call. What
193    /// arrives is a thing to look at; the record of what was said is
194    /// written from `model_returned`, once, afterwards.
195    pub deltas: Option<&'a mut (dyn FnMut(&kernel::Increment) + 'a)>,
196}
197
198/// An active run: turns may still be taken.
199pub struct Active {
200    conversation: Conversation,
201    turns: u32,
202    last_turn_t: Option<TimeMs>,
203    /// How full the window is, read off each call's reported usage.
204    gauge: ContextGauge,
205    /// The cache shape of the request this run sent last, which is what
206    /// the next one is compared against. `None` before the first
207    /// request, and that absence is the `FirstRequest` a record states.
208    prior_shape: Option<crate::prefix::shape::PromptShape>,
209    /// The `prompt_assembled` payload this run wrote last, which a turn
210    /// does not write again (`crates/runtime/spec/Prefix.lean` §8-39, item 5).
211    prompt: crate::turn::PromptRecord,
212    /// Whether the next wave needs a checkpoint (§8-45).
213    checkpoint: checkpoint::CheckpointPolicy,
214}
215
216/// A frozen run. There is no method back to [`Active`]: waking an old run
217/// is not something this type can spell, and `resume` takes a [`Handoff`]
218/// rather than a run.
219pub struct Frozen {
220    completion: Completion,
221    turns: u32,
222    /// Kept past the freeze for one reader: the transcript written
223    /// beside the room, which is what this run's model actually saw.
224    conversation: Conversation,
225}
226
227pub struct Run<S> {
228    plan: RunPlan,
229    state: S,
230}
231
232impl Run<Frozen> {
233    pub fn completion(&self) -> &Completion {
234        &self.state.completion
235    }
236
237    /// What this run was dispatched with, for a successor that takes
238    /// the same work on.
239    pub fn plan(&self) -> &RunPlan {
240        &self.plan
241    }
242
243    /// What the model saw, scanned and ready to write beside the room.
244    ///
245    /// # Errors
246    /// Propagates a message that will not serialise.
247    pub fn transcript(&self) -> Result<crate::transcript::Transcript, AxError> {
248        crate::transcript::Transcript::of(self.plan.run, &self.state.conversation)
249    }
250
251    pub fn turns(&self) -> u32 {
252        self.state.turns
253    }
254
255    pub fn run_id(&self) -> RunId {
256        self.plan.run
257    }
258}
259
260/// Dispatch, turn until an ending, freeze. The loop is here rather than in
261/// each caller because the ending rules — an empty wave concludes, a
262/// cancellation is an ending too — are the part that must not drift
263/// between the city and the simulator.
264///
265/// **The loop counts no turns.** There is no ceiling to reach, because
266/// nobody can price a piece of work before it runs. What
267/// ends a run is what it did: a turn that concluded, a failure with a
268/// carrier event, or an interruption a safe point delivered — and what
269/// stops one from outside is `Halt`, which shuts the scope and kills the
270/// backlog members inside it.
271///
272/// # Errors
273/// Propagates ledger and provider failures.
274pub fn drive(
275    plan: RunPlan,
276    ledger: &mut dyn Ledger,
277    model: &mut dyn Model,
278    hooks: &mut RunHooks<'_>,
279    handoff: &Handoff,
280) -> Result<Run<Frozen>, AxError> {
281    let mut watchdog = crate::Watchdog::new(plan.retries, plan.run);
282    let mut run = Run::dispatch(plan, ledger, hooks)?;
283    let ending = loop {
284        match run.advance(ledger, model, hooks) {
285            Ok(Advance::Turned) => watchdog.on_provider_answered(),
286            Ok(Advance::Concluded(completion)) => break completion,
287            // A run always ends. A mid-turn failure whose code has a
288            // carrier event (provider down, budget, watchdog) is written
289            // into history under that carrier and the run freezes as
290            // cancelled, so a 401 from a provider never leaves a run
291            // "started" with an event stream that simply went quiet.
292            //
293            // A loadtime code names no carrier, and it leaves by the
294            // same door: the verdict is written first and the diagnosis
295            // travels afterwards, so the two facts do not compete.
296            // `E_WIRE_MISMATCH` means a provider spelled its dialect
297            // wrong while the ledger is in perfect health. Where the
298            // store really is the casualty, `freeze` fails on its own
299            // append and that failure is what travels, which is more
300            // honest than deciding in advance that nothing can be
301            // written.
302            Err(err) => {
303                let Carrier::Event(kind) = err.code().carrier() else {
304                    run.freeze(ledger, handoff, Completion::Cancelled, hooks)?;
305                    return Err(err);
306                };
307                let t = (hooks.now)()?;
308                // A provider that failed for a reason worth retrying is
309                // asked again, and the attempt is recorded rather than
310                // repeated silently: the second `model_called` in the
311                // history is what a person reads the retry off.
312                // The watchdog decides whether there is a next call and
313                // when; the line records that moment before the wait,
314                // and the wait is where a halt reaches a run that has
315                // no turn in flight to stop.
316                let disposal = watchdog.on_provider_failure(&err, t);
317                if let crate::Disposal::BackOff { until, .. } = disposal {
318                    ledger.append(EventDraft {
319                        run: run.plan.run,
320                        t,
321                        who: run.plan.who.clone(),
322                        addr: Some(run.plan.addr.clone()),
323                        kind: kernel::EventKind::WatchdogFired,
324                        data: watchdog.fired_payload(&disposal)?,
325                        ig: false,
326                    })?;
327                    match (hooks.wait)(until) {
328                        crate::NextCall::Allowed => continue,
329                        crate::NextCall::Halted => break Completion::Cancelled,
330                    }
331                }
332                // The failure itself is the payload, through the one
333                // door: an encoding that fails travels as a refusal
334                // rather than as an empty object.
335                let data = Payload::of(&err)?;
336                ledger.append(EventDraft {
337                    run: run.plan.run,
338                    t,
339                    who: run.plan.who.clone(),
340                    addr: Some(run.plan.addr.clone()),
341                    kind,
342                    data,
343                    ig: false,
344                })?;
345                break Completion::Cancelled;
346            }
347        }
348    };
349    run.freeze(ledger, handoff, ending, hooks)
350}