Skip to main content

wire/answer/
rounds.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//! One session read as the rounds a person reads.
7//!
8//! These are the shapes only; the fold that produces them is
9//! `accounting::views::rounds`, and reading one payload into them is
10//! [`crate::reading`]. Three files because they are three shapes
11//! (ARCHITECTURE.md section 9): a value, a projection, and a decision.
12//!
13//! They live on the wire because a second client must be able to draw a
14//! session without folding the ledger itself.
15
16use kernel::{AxError, GitOid, Locator, RunId, Seq, TimeMs, Tokens, UsdMicros};
17use serde::{Deserialize, Serialize};
18
19/// What a tool call has come to so far.
20///
21/// Three states rather than a `bool` and an `Option`: a call still
22/// running and a call that failed are different things to a person
23/// deciding whether to step in, and the pair could spell a fourth state
24/// that cannot happen.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
28pub enum Outcome {
29    /// Called, and no result has arrived in this window.
30    Waiting,
31    /// Answered.
32    Answered,
33    /// Answered with an error. **Not an alert**: one failed call is a
34    /// fact, not a request for a person. If it actually stopped the
35    /// session, the freeze raises its own card.
36    Failed,
37}
38
39/// Whether the span between two times on a row is a measurement.
40///
41/// Two values because a page does one of two things with a row: draw
42/// how long it took, or draw no duration at all. The times themselves
43/// travel either way; they still give the row its order.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
47pub enum Timing {
48    /// Every time on the row is the moment its own record measured
49    /// (`kernel::EventRecord::moment`).
50    Measured,
51    /// At least one is not: a line written before each line recorded
52    /// its own moment carries its turn's stamp, which every line of that
53    /// turn shares, and an answer the city wrote itself after a restart
54    /// (`E_TOOL_OUTCOME_UNKNOWN`) carries when the city wrote it.
55    Unmeasured,
56}
57
58/// What a tool said, bounded so a wave of output cannot become the page.
59///
60/// The cut is counted rather than hinted at: a reader who cannot see how
61/// much was withheld is being dumped on slowly. Bytes already too large
62/// were replaced by `runtime::offload` before they reached the Ledger,
63/// and that substitute carries its own line naming the `Locator`.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
66pub struct Output {
67    /// The first [`crate::OUTPUT_LINES`] lines of it.
68    pub head: String,
69    /// How many lines this view cut. The rest is in the Ledger at the
70    /// call's own `at`.
71    pub cut: usize,
72    /// Where the whole of a call's output was stored when it left the
73    /// window: the bytes the command itself wrote, even when the sieve
74    /// cut them first and the cut was stored again. `None` when nothing
75    /// left the window, on a line written before results carried their
76    /// accounts, and always on a call's arguments, which never leave.
77    #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
78    pub pinned: Option<Locator>,
79}
80
81/// What a turn came to besides the calls it made.
82///
83/// **The criterion is closed on purpose**: an event earns a `Note` when
84/// it changed what this turn did, or what it is waiting on. Everything
85/// else stays in the event stream, which is the Ledger's shape rather
86/// than a reader's. Without that line this enum would grow to one arm
87/// per event kind and stop meaning anything.
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
91pub enum Note {
92    /// A door refused something. The error travels whole because the
93    /// interface has one place where a refusal becomes the three parts a
94    /// person needs; taking it apart here would be the second.
95    Refused { error: AxError, at: Seq },
96    /// A checkpoint went up, and this is the commit it made. It is
97    /// what a change list is addressed by.
98    Checkpointed { oid: GitOid, at: Seq },
99    /// This turn stopped for a person. What waits and who answers is the
100    /// approval queue's; copying it here would be a third authority.
101    /// `t` is when the Ledger recorded the request and `answered` when
102    /// it recorded the answer, paired back by approval id from the
103    /// city's own run; `None` when the answer is outside that window or
104    /// has not come, and the page then draws no guessed end.
105    Waiting {
106        at: Seq,
107        t: TimeMs,
108        answered: Option<TimeMs>,
109    },
110    /// A word arrived - from the person watching, or from another
111    /// address that reached this one.
112    Arrived { from: String, said: String, at: Seq },
113    /// Files went away. Every one carries its way back, which is the
114    /// Recycle Bin's to state.
115    Discarded { count: usize, at: Seq },
116    /// A record of a kind that earns a note, whose payload did not read
117    /// back as that kind. The failure stays visible here instead of the
118    /// turn reading as if nothing happened; `cause` names the kind and
119    /// what the reading stopped at.
120    Unreadable { cause: String, at: Seq },
121}
122
123impl Note {
124    /// Where in the Ledger this note is, so every row can be read
125    /// further.
126    #[must_use]
127    pub fn at(&self) -> Seq {
128        match *self {
129            Self::Refused { at, .. }
130            | Self::Checkpointed { at, .. }
131            | Self::Waiting { at, .. }
132            | Self::Arrived { at, .. }
133            | Self::Discarded { at, .. }
134            | Self::Unreadable { at, .. } => at,
135        }
136    }
137}
138
139/// What one turn cost in tokens.
140///
141/// Absolute counts and no ratio: the numerator is on the wire and the
142/// denominator - this model's context window - is not. A percentage with
143/// no denominator is the thing `UnplannedProgress` already refuses to
144/// spell, and it would be no more honest here.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
146#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
147pub struct Used {
148    pub input: Tokens,
149    pub output: Tokens,
150    /// The part of `input` read from the provider's cache
151    /// (`kernel::ModelUsage::cache_read_tokens`).
152    pub cached: Tokens,
153    /// The part of `input` written into the provider's cache
154    /// (`kernel::ModelUsage::cache_write_tokens`), priced apart from a
155    /// read. Always present beside the other three in this build; absent
156    /// only in a frame from a city written before it was answered
157    /// (`crates/wire/Spec.lean` D13).
158    #[serde(default)]
159    pub cache_write: Option<Tokens>,
160}
161
162/// One tool call inside a turn.
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
165pub struct Call {
166    /// The tool's own name, as the Ledger records it.
167    pub tool: String,
168    /// What it acted on, when the arguments name one thing. A display
169    /// reading rather than a field: the arguments are free JSON and this
170    /// picks the one a person recognises the call by.
171    pub subject: Option<String>,
172    /// What it was called with, bounded exactly the way [`Output`] is and
173    /// cut off at the same limit.
174    ///
175    /// A person judging a call needs what went in, not only what came
176    /// back: "the tool failed" and "the tool was asked for the wrong
177    /// path" are read from different halves of the same row. `subject`
178    /// stays one display reading of these rather than a replacement for
179    /// them. `None` when the recorded call carried no arguments this
180    /// build can read as text; the whole of them is in the Ledger at
181    /// `at`, which is where the cut lines went too.
182    pub arguments: Option<Output>,
183    pub outcome: Outcome,
184    /// Where in the Ledger the bytes are. The row shows a shape; this is
185    /// how somebody reads the rest.
186    pub at: Seq,
187    /// What it said, bounded. `None` when the call has not answered, or
188    /// when the result carried nothing this build can read as text.
189    pub output: Option<Output>,
190    /// When the Ledger wrote the call, read from that record the way
191    /// [`Turn::t`] is, so a replayed session keeps its original times.
192    pub called: TimeMs,
193    /// When the Ledger wrote the result paired with this call. `None`
194    /// exactly when `outcome` is [`Outcome::Waiting`]: the two are set by
195    /// one pairing, and an answer outside the window is not guessed.
196    pub answered: Option<TimeMs>,
197    /// Whether `answered - called` is how long the call took. One value
198    /// for both times: two records a build writes are both measured or
199    /// both not, and the one exception - an answer the city supplied
200    /// after a restart - makes the span unmeasured either way.
201    pub timing: Timing,
202    /// What boundary the tool was registered as crossing when it was
203    /// called, as its `tool_called` line recorded it. `None` for a tool
204    /// the bench did not know, and for a line written before the key
205    /// existed.
206    pub effect: Option<kernel::Effect>,
207    /// How the tool was registered to be drawn, from the same line: a
208    /// terminal, a diff, or the generic row. `None` in the same cases;
209    /// a page draws such a call as generic. A diff's `locations` are the
210    /// registration's and empty today - the file is [`Call::subject`].
211    pub render: Option<kernel::RenderIntent>,
212    /// The code the command of an `exec` call ended with, as its paired
213    /// result recorded it. `None` for every other tool, for a command a
214    /// signal stopped or the city never waited on - which have no code -
215    /// and for a call not yet answered: no code is drawn rather than an
216    /// invented one.
217    #[serde(default)]
218    pub exit_code: Option<i64>,
219}
220
221/// One turn: the model was asked, and this is what came of it.
222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
223#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
224pub struct Turn {
225    /// Counted from one, in the order the turns opened.
226    pub number: u32,
227    /// The event that opened it.
228    pub opened: Seq,
229    /// When the Ledger wrote the event `opened` names.
230    ///
231    /// Read from that record rather than from a clock consulted here:
232    /// the Ledger is the authority on when a thing happened and this
233    /// field is a projection of it, so a session replayed tomorrow
234    /// reports the times it originally had rather than the times it was
235    /// replayed.
236    ///
237    /// Whether it is the moment the model was asked, or the stamp a
238    /// ledger written before per-line moments gave every line of the
239    /// turn, is [`Turn::timing`].
240    ///
241    /// Not optional, unlike [`crate::LogLine::t`]. A log line is written
242    /// beside the Ledger and can find the clock unreadable; a turn is
243    /// folded from an `EventRecord`, which carries a reading in every
244    /// case. An `Option` here would be a state no fold can produce and
245    /// every reader would still have to answer.
246    pub t: TimeMs,
247    /// Whether [`Turn::t`] is the moment the model was asked.
248    pub timing: Timing,
249    /// When the reply's first content reached the city, as the turn's
250    /// `model_returned` recorded it; the time to first content is this
251    /// minus [`Turn::t`]. `None` when the reply had no stream, streamed
252    /// only tool calls, or was written before the key existed - the page
253    /// then draws no figure rather than a guessed one.
254    pub first_at: Option<TimeMs>,
255    /// When the reply that answered this turn was whole, as that
256    /// `model_returned` measured its own moment
257    /// (`kernel::EventRecord::moment`). The output rate is
258    /// `used.output` over `returned - first_at`. `None` when no reply
259    /// answered in this window and when the reply's line measured no
260    /// moment of its own - the envelope stamp of such a line is the
261    /// turn's, and a rate drawn from it would be invented.
262    #[serde(default)]
263    pub returned: Option<TimeMs>,
264    /// The endpoint's model id the `model_called` that opened this turn
265    /// recorded. Per turn because a session can change model midway,
266    /// and a name for the whole session would be wrong for half of it.
267    /// `None` when that record carries no model name as text; the page
268    /// then draws no name rather than a guessed one.
269    pub model: Option<String>,
270    /// What the model said in this turn: its prose, without the
271    /// reasoning that produced it.
272    pub said: Option<String>,
273    /// What the model reasoned in this turn, when the provider sent it.
274    /// A field of its own rather than part of `said`, because a page
275    /// folds the two differently and a person asked for an answer.
276    pub thought: Option<String>,
277    /// What this one turn was billed.
278    pub spent: Option<UsdMicros>,
279    pub used: Option<Used>,
280    /// Why the model stopped, in the provider's own word.
281    pub stopped: Option<String>,
282    pub calls: Vec<Call>,
283    /// What else happened inside this turn, oldest first.
284    pub notes: Vec<Note>,
285}
286
287/// How a session began: what the person asked for, in their words.
288///
289/// The rounds start at the first `model_called`, so without this the
290/// first thing said in a conversation - the task - is the one thing the
291/// answer never carried.
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
294pub struct Opening {
295    pub task: String,
296    pub goal: String,
297    pub at: TimeMs,
298    /// Who dispatched the run, as its `run_started` records it; `None`
299    /// for a ledger written before the key existed.
300    #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
301    pub dispatched_by: Option<kernel::event::Who>,
302    /// The run policy it was dispatched under - mode, write limit,
303    /// admission requirement and landing policy - as its `run_started`
304    /// records it; `None` for a line written before the key existed.
305    #[serde(default)]
306    pub policy: Option<kernel::RunPolicy>,
307    /// The effort the run's requests froze, as its `run_started`
308    /// records it (`crates/kernel/Spec.lean` ยง8-85); `None` when the
309    /// dispatch stated none, and for a line written before the key
310    /// existed.
311    #[serde(default)]
312    pub effort: Option<kernel::Effort>,
313    /// The names the run's session froze, read back from the version its
314    /// `run_started` names (`crates/wire/Spec.lean` D17). `None` for a
315    /// line written before the city recorded names, and for a version
316    /// the store no longer holds or cannot read: the page then falls
317    /// back to the address and the role's name, never to today's names.
318    #[serde(default)]
319    pub names: Option<FrozenNames>,
320}
321
322/// The names a session froze, as a page draws them.
323#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
324#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
325pub struct FrozenNames {
326    /// What the Mayor was called; `None` when nobody had named it, and
327    /// the page then draws the name its language table ships.
328    pub mayor: Option<String>,
329}
330
331/// How a session ended, in the word the run froze with: `done`,
332/// `limit` or `cancelled`.
333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
335pub struct Closing {
336    pub completion: String,
337    pub at: TimeMs,
338}
339
340/// One session's rounds, oldest first.
341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
342#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
343pub struct RoundsAnswer {
344    pub run: RunId,
345    pub turns: Vec<Turn>,
346    /// The checkpoint this session's changes are measured from: its
347    /// first checkpoint, because that is the tree as the work found it.
348    /// Measuring from the latest one would answer "what moved in the
349    /// last wave", which is a different question.
350    pub opened_at: Option<GitOid>,
351    /// Absent when the window this answer reads did not hold the
352    /// session's `run_started`; what the window does not hold is not
353    /// guessed.
354    pub opening: Option<Opening>,
355    /// Absent while the session is still going, or when the window did
356    /// not reach its `run_frozen`.
357    pub closing: Option<Closing>,
358    /// The name of the worktree this run was lent, from its own
359    /// `worktree_opened`: a name, never a path, which is a fact about one
360    /// machine. Absent for a run that worked in its building's own tree,
361    /// and when the window did not reach that line.
362    #[serde(default)]
363    pub worktree: Option<String>,
364}