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;