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    /// The ACP agent that took this harness run (`crates/kernel/spec/Event/Record.lean`
99    /// §8-87). Absent on a run this city's own model loop drives, and in a
100    /// record written before the key existed.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub agent: Option<AgentRunIdentity>,
103}
104
105/// Which ACP agent ran a harness run, said without a command line or a
106/// path: those are one machine's facts, and a ledger is exported and
107/// replayed by anybody.
108///
109/// The digest is the one the city recomputed when the person consented,
110/// so a run started from a launch spec that changed after consent shows
111/// a different digest than the consent did.
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
114pub struct AgentRunIdentity {
115    /// The agent entry's id: an `[[agent]]` row's, or a built-in entry's.
116    pub id: String,
117    /// The version the agent reported in `initialize`; absent when it
118    /// reported none.
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub version: Option<String>,
121    /// The digest of the launch spec the person consented to.
122    #[cfg_attr(feature = "schema", schemars(with = "String"))]
123    pub launch_digest: B3Hash,
124}
125
126/// How a run's first user message opens.
127///
128/// Exhaustive, and the choice is made once by the city that wrote (or
129/// did not write) the job file. It is not a formatting preference: a
130/// session working from an assignment and a session talking with the
131/// person want different first words, and inferring which from an empty
132/// string would make the emptiness of a goal mean two things.
133///
134/// It lives here rather than beside the conversation that writes it
135/// because `run_started` records it, and the ledger's payloads are this
136/// crate's; `runtime::conversation` re-exports it.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(rename_all = "snake_case")]
139#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
140pub enum Opening {
141    /// Somebody wrote the task down; the job file's text is in the prefix.
142    FromJob,
143    /// Somebody wrote the task down for a mother, and a branch is
144    /// rebuilding her conversation: her job file lives in her room, not
145    /// in the branch's prefix, so the task travels in the message.
146    Inherited,
147    /// Nobody did; the person is on the other side of this message.
148    WithPerson,
149}
150
151/// `run_frozen`: how a run ended, and what it cites for having ended
152/// that way.
153///
154/// The three endings are spelled by [`Completion::name`], which stays
155/// the authority for the words: this struct carries whichever word that
156/// method printed rather than an enum that would have to agree with it.
157/// A run that ended any other way cites nothing, and the key is absent
158/// rather than empty, exactly as the hand-written writer left it.
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
161pub struct RunFrozen {
162    pub completion: String,
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub evidence: Option<Vec<EvidenceCite>>,
165    /// Why the line was written by something other than the run's own
166    /// driver (`crates/kernel/spec/Event/Record.lean` §8-82-2). Absent on every freeze a run wrote
167    /// for itself, so those lines keep the bytes they always had.
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub cause: Option<FreezeCause>,
170}
171
172/// Why a run was frozen by something other than its own driver.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case")]
175#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
176pub enum FreezeCause {
177    /// The process driving it died; the next `resume` froze it.
178    ProcessDied,
179}
180
181impl RunFrozen {
182    /// The one projection from a verdict to the line that records it.
183    ///
184    /// Reading is deliberately not the inverse: a [`Completion::Done`]
185    /// holds [`EventRef`](crate::EventRef)s, and minting those from a
186    /// line would forge the evidence the type exists to guarantee.
187    pub fn of(completion: &Completion) -> RunFrozen {
188        let evidence = match completion {
189            Completion::Done(evidence) => Some(
190                evidence
191                    .refs()
192                    .iter()
193                    .map(|cited| EvidenceCite {
194                        seq: cited.seq(),
195                        kind: cited.kind(),
196                    })
197                    .collect(),
198            ),
199            Completion::Limit | Completion::Cancelled => None,
200        };
201        RunFrozen {
202            completion: completion.name().to_owned(),
203            evidence,
204            cause: None,
205        }
206    }
207
208    /// The freeze of a run whose process died before it froze itself:
209    /// the one place that says which ending such a run has.
210    ///
211    /// `cancelled`, because the run stopped without finishing - `done`
212    /// needs evidence the city recorded and it has none - and no ceiling
213    /// cut it, which is what `limit` says. The cause tells it apart from
214    /// a cancel somebody asked for (kernel D14).
215    pub fn lost() -> RunFrozen {
216        RunFrozen {
217            completion: Completion::Cancelled.name().to_owned(),
218            evidence: None,
219            cause: Some(FreezeCause::ProcessDied),
220        }
221    }
222}
223
224/// One line a finished run cites as the evidence it finished.
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
226#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
227pub struct EvidenceCite {
228    pub seq: Seq,
229    pub kind: EventKind,
230}
231
232/// `session_opened`: a new session began at this address.
233///
234/// The room is the subject rather than the run, because what changed is
235/// what the *next* run there is governed by: the model and the effort
236/// the previous session froze are gone, and whether the summary it left
237/// travels with this one is the one thing the caller chose. A replay
238/// reads the answer to "why did this run start fresh" from here;
239/// neither fact is visible in the room's files afterwards, because both
240/// were removals.
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
243pub struct SessionOpened {
244    /// What the session branched from, when it is a branch.
245    ///
246    /// `#[serde(default)]` and absent when there is none, so a line
247    /// written before this key existed reads as a session that began
248    /// without one - which is what it was.
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub from: Option<Origin>,
251    /// Whether the previous session's handoff travelled with it.
252    ///
253    /// **The room itself is the record's own `addr` field**, not a copy
254    /// here: a line that belongs to an address says so in the envelope,
255    /// and a payload copy would be a second place the same address is
256    /// spelled (`crates/kernel/spec/Event.lean` §8-4).
257    pub carried: bool,
258}
259
260/// `run_forked`: which run this one continues, and from where in it.
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
263pub struct RunForked {
264    /// The run whose history this one resumes.
265    pub from: RunId,
266    /// The last sequence number of `from` that this run inherits.
267    pub at_seq: Seq,
268}
269
270/// `eval_run`: the handoff probe asked of a successor, compared with
271/// what its predecessor answered.
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
274pub struct EvalRun {
275    /// The probe's name and version, which together fix its questions.
276    pub probe: String,
277    pub version: u32,
278    /// The run whose answers are the reference.
279    pub predecessor: RunId,
280    /// How many answers the successor kept.
281    pub kept: u32,
282    /// The question indices whose answers differ, in order.
283    pub lost: Vec<u32>,
284    /// The predecessor's answers and the successor's, one per question.
285    pub before: Vec<String>,
286    pub after: Vec<String>,
287}
288
289/// `run_policy_changed`: the room's run policy, changed while a session
290/// is open (`crates/kernel/spec/Event/Record.lean` D21).
291///
292/// The run under way reads it at its next safe point and passes the
293/// gate under it from that step on; the next run's `run_started.policy`
294/// is the room's last change. The model and the effort stay what the
295/// session froze.
296#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
297#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
298pub struct RunPolicyChanged {
299    pub policy: RunPolicy,
300    #[cfg_attr(feature = "schema", schemars(with = "String"))]
301    pub by: Who,
302}
303
304/// `session_named`: the name a person gave one session of a room
305/// (`crates/kernel/spec/Event/Record.lean` D22).
306///
307/// The address stays the session's identity; the name is what a page
308/// shows. `began` is the seq that opened the session, the same value
309/// `wire::SessionLine.began` carries; an empty name takes the name back,
310/// and the last line for a session is the one that holds.
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
313pub struct SessionNamed {
314    pub began: Seq,
315    pub name: String,
316}
317
318#[cfg(test)]
319#[allow(
320    clippy::unwrap_used,
321    clippy::expect_used,
322    clippy::panic,
323    clippy::indexing_slicing,
324    reason = "test code"
325)]
326mod tests;