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