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}