Skip to main content

kernel/event/record/
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//! What a dispatch, a fork and a freeze record about a run.
7
8use serde::{Deserialize, Serialize};
9
10use crate::completion::Completion;
11use crate::event::identity::{RunId, Seq};
12use crate::event::kind::EventKind;
13use crate::event::who::Who;
14use crate::locator::{B3Hash, Locator};
15use crate::model::{Effort, RunPolicy};
16use crate::origin::Origin;
17
18/// One skill a run was dispatched with, pinned to the bytes it read.
19///
20/// The hash is the point: a page answers "which runs read these exact
21/// bytes" from it, and a pin naming a name alone cannot answer that.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
24pub struct SkillPin {
25    pub name: String,
26    #[cfg_attr(feature = "schema", schemars(with = "String"))]
27    pub hash: B3Hash,
28}
29
30/// `run_started`: the work a run was given, and where it came from.
31///
32/// Every field defaults, because the golden fixtures hold a
33/// `run_started` whose payload is `{}` and a projection that refused it
34/// would refuse a ledger this repository ships.
35#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37pub struct RunStarted {
38    /// What the person asked for, in their words.
39    #[serde(default)]
40    pub task: String,
41    /// What finishing looks like, in their words.
42    #[serde(default)]
43    pub goal: String,
44    /// The job this dispatch came out of. Absent only in a record
45    /// written before the key existed.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
48    pub job: Option<Locator>,
49    /// The run that forked this one, when one did.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub parent: Option<RunId>,
52    /// The run this one replaced, when it is a successor. Spelled the
53    /// same way here and on `checkpoint_committed`, because it is one
54    /// fact with one spelling.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub predecessor: Option<RunId>,
57    /// Written even when empty, and deliberately: a key that comes and
58    /// goes is a shape a reader has to guess at, and "this building
59    /// admits no skills" is a fact worth recording rather than an
60    /// absence to infer.
61    #[serde(default)]
62    pub skills: Vec<SkillPin>,
63    /// Who dispatched this run: the person, the city's own desk (a
64    /// plan, the schedule, an arrival), or the resident that delegated,
65    /// succeeded itself or knocked. The line's author cannot say it,
66    /// because the city's desk writes every `run_started`. Absent only
67    /// in a record written before the key existed.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
70    pub dispatched_by: Option<Who>,
71    /// The run policy this run was dispatched under (`crates/kernel/spec/Model.lean` §8-77).
72    /// Absent in a record written before the policy was recorded; the
73    /// six mode words before it never reached a typed payload, so
74    /// there is no older shape to read.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub policy: Option<RunPolicy>,
77    /// The identity version this run's session froze: the key a page
78    /// reads the names the request carried back by, through the content
79    /// store (`crates/kernel/spec/Event/Record.lean` §8-79). Absent in a record written before the
80    /// key existed, which a page answers with the address rather than
81    /// with today's names.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
84    pub naming: Option<B3Hash>,
85    /// How the run's first user message was written (`crates/kernel/spec/Event/Record.lean` §8-82-1):
86    /// what a fork reads to rebuild that message the way the mother sent
87    /// it. Absent in a record written before the key existed, and on a
88    /// harness run, whose first words are the harness's own prompt.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub opening: Option<Opening>,
91    /// The effort the run's requests froze (`crates/kernel/spec/Event/Record.lean`
92    /// §8-85): the session's, which no turn may change. Absent when the
93    /// dispatch stated none and the provider chooses - which is not
94    /// [`Effort::None`], asking it not to think - on a harness run, and
95    /// in a record written before the key existed.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub effort: Option<Effort>,
98}
99
100/// How a run's first user message opens.
101///
102/// Exhaustive, and the choice is made once by the city that wrote (or
103/// did not write) the job file. It is not a formatting preference: a
104/// session working from an assignment and a session talking with the
105/// person want different first words, and inferring which from an empty
106/// string would make the emptiness of a goal mean two things.
107///
108/// It lives here rather than beside the conversation that writes it
109/// because `run_started` records it, and the ledger's payloads are this
110/// crate's; `runtime::conversation` re-exports it.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
114pub enum Opening {
115    /// Somebody wrote the task down; the job file's text is in the prefix.
116    FromJob,
117    /// Somebody wrote the task down for a mother, and a branch is
118    /// rebuilding her conversation: her job file lives in her room, not
119    /// in the branch's prefix, so the task travels in the message.
120    Inherited,
121    /// Nobody did; the person is on the other side of this message.
122    WithPerson,
123}
124
125/// `run_frozen`: how a run ended, and what it cites for having ended
126/// that way.
127///
128/// The three endings are spelled by [`Completion::name`], which stays
129/// the authority for the words: this struct carries whichever word that
130/// method printed rather than an enum that would have to agree with it.
131/// A run that ended any other way cites nothing, and the key is absent
132/// rather than empty, exactly as the hand-written writer left it.
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
135pub struct RunFrozen {
136    pub completion: String,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub evidence: Option<Vec<EvidenceCite>>,
139    /// Why the line was written by something other than the run's own
140    /// driver (`crates/kernel/spec/Event/Record.lean` §8-82-2). Absent on every freeze a run wrote
141    /// for itself, so those lines keep the bytes they always had.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub cause: Option<FreezeCause>,
144}
145
146/// Why a run was frozen by something other than its own driver.
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(rename_all = "snake_case")]
149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
150pub enum FreezeCause {
151    /// The process driving it died; the next `resume` froze it.
152    ProcessDied,
153}
154
155impl RunFrozen {
156    /// The one projection from a verdict to the line that records it.
157    ///
158    /// Reading is deliberately not the inverse: a [`Completion::Done`]
159    /// holds [`EventRef`](crate::EventRef)s, and minting those from a
160    /// line would forge the evidence the type exists to guarantee.
161    pub fn of(completion: &Completion) -> RunFrozen {
162        let evidence = match completion {
163            Completion::Done(evidence) => Some(
164                evidence
165                    .refs()
166                    .iter()
167                    .map(|cited| EvidenceCite {
168                        seq: cited.seq(),
169                        kind: cited.kind(),
170                    })
171                    .collect(),
172            ),
173            Completion::Limit | Completion::Cancelled => None,
174        };
175        RunFrozen {
176            completion: completion.name().to_owned(),
177            evidence,
178            cause: None,
179        }
180    }
181
182    /// The freeze of a run whose process died before it froze itself:
183    /// the one place that says which ending such a run has.
184    ///
185    /// `cancelled`, because the run stopped without finishing - `done`
186    /// needs evidence the city recorded and it has none - and no ceiling
187    /// cut it, which is what `limit` says. The cause tells it apart from
188    /// a cancel somebody asked for (kernel D14).
189    pub fn lost() -> RunFrozen {
190        RunFrozen {
191            completion: Completion::Cancelled.name().to_owned(),
192            evidence: None,
193            cause: Some(FreezeCause::ProcessDied),
194        }
195    }
196}
197
198/// One line a finished run cites as the evidence it finished.
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
201pub struct EvidenceCite {
202    pub seq: Seq,
203    pub kind: EventKind,
204}
205
206/// `session_opened`: a new session began at this address.
207///
208/// The room is the subject rather than the run, because what changed is
209/// what the *next* run there is governed by: the model and the effort
210/// the previous session froze are gone, and whether the summary it left
211/// travels with this one is the one thing the caller chose. A replay
212/// reads the answer to "why did this run start fresh" from here;
213/// neither fact is visible in the room's files afterwards, because both
214/// were removals.
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
217pub struct SessionOpened {
218    /// What the session branched from, when it is a branch.
219    ///
220    /// `#[serde(default)]` and absent when there is none, so a line
221    /// written before this key existed reads as a session that began
222    /// without one - which is what it was.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub from: Option<Origin>,
225    /// Whether the previous session's handoff travelled with it.
226    ///
227    /// **The room itself is the record's own `addr` field**, not a copy
228    /// here: a line that belongs to an address says so in the envelope,
229    /// and a payload copy would be a second place the same address is
230    /// spelled (`crates/kernel/spec/Event.lean` §8-4).
231    pub carried: bool,
232}
233
234/// `run_forked`: which run this one continues, and from where in it.
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
237pub struct RunForked {
238    /// The run whose history this one resumes.
239    pub from: RunId,
240    /// The last sequence number of `from` that this run inherits.
241    pub at_seq: Seq,
242}
243
244/// `eval_run`: the handoff probe asked of a successor, compared with
245/// what its predecessor answered.
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
248pub struct EvalRun {
249    /// The probe's name and version, which together fix its questions.
250    pub probe: String,
251    pub version: u32,
252    /// The run whose answers are the reference.
253    pub predecessor: RunId,
254    /// How many answers the successor kept.
255    pub kept: u32,
256    /// The question indices whose answers differ, in order.
257    pub lost: Vec<u32>,
258    /// The predecessor's answers and the successor's, one per question.
259    pub before: Vec<String>,
260    pub after: Vec<String>,
261}
262
263#[cfg(test)]
264#[allow(
265    clippy::unwrap_used,
266    clippy::expect_used,
267    clippy::panic,
268    clippy::indexing_slicing,
269    reason = "test code"
270)]
271mod tests;