Skip to main content

salvor_replay/
state.rs

1//! State derivation: a pure fold from an event log to the run state it
2//! implies.
3//!
4//! This is the projection behind `replay --dry-run` and, later, the
5//! dashboard: events in, state out, nothing executed. It is total over every
6//! prefix of a valid log, because a crash can happen at any event boundary
7//! and every prefix is therefore a state something might resume from.
8//!
9//! # Purity
10//!
11//! No IO, no clock, no randomness, no dependency on storage or executors.
12//! Like the replay cursor, this fold lives in the pure `salvor-replay` crate,
13//! which builds for wasm32 so the v0.3 browser inspector can derive a run's
14//! state from its log in-browser, from this same code.
15//!
16//! # The write rule
17//!
18//! A log whose last recorded tool intent has [`Effect::Write`] and no
19//! completion derives to [`RunStatus::NeedsReconciliation`], never to
20//! anything retryable. The write may or may not have reached the provider;
21//! the fold refuses to guess, and so must everything built on it.
22
23use serde_json::Value;
24
25use crate::effect::Effect;
26use crate::event::{Budget, Event, EventEnvelope, UnresolvedWrite};
27use crate::id::SequenceNumber;
28
29/// Token usage accumulated across every completed model call in a log.
30///
31/// Wider than the per-call [`crate::TokenUsage`] counters on purpose: a long
32/// run sums many calls, and the fold must stay total, so accumulation uses
33/// `u64` and saturating arithmetic instead of trusting the sum to fit.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub struct TokenTotals {
36    /// Total input (prompt) tokens across the run.
37    pub input_tokens: u64,
38    /// Total output (completion) tokens across the run.
39    pub output_tokens: u64,
40}
41
42/// A recorded call intent with no recorded completion.
43///
44/// Carries everything needed to act on the gap: re-issue a model call,
45/// retry an idempotent call under its recorded key, or show a human the
46/// evidence for reconciling a write.
47#[derive(Debug, Clone, PartialEq)]
48pub enum PendingCall {
49    /// A model call was requested and never completed. Safe to re-issue.
50    Model {
51        /// The log position of the intent.
52        seq: SequenceNumber,
53        /// The recorded hash of the request.
54        request_hash: String,
55    },
56    /// A tool call was requested and never completed. What may be done next
57    /// depends on `effect`; see the [`Effect`] docs for the table.
58    Tool {
59        /// The log position of the intent.
60        seq: SequenceNumber,
61        /// The tool that was called.
62        tool: String,
63        /// The recorded input.
64        input: Value,
65        /// The declared effect class.
66        effect: Effect,
67        /// The recorded idempotency key. For an idempotent retry this exact
68        /// key must be reused, so the provider collapses the attempts.
69        idempotency_key: Option<String>,
70    },
71}
72
73/// Where a run stands, as derived from its log alone.
74#[derive(Debug, Clone, PartialEq)]
75pub enum RunStatus {
76    /// The log is empty: nothing has been recorded yet.
77    NotStarted,
78    /// The run is between recorded steps and can continue.
79    Running,
80    /// A model call intent is recorded with no completion. The call can be
81    /// re-issued.
82    AwaitingModel,
83    /// A read or idempotent tool intent is recorded with no completion. The
84    /// call can be re-executed (reads freely, idempotent calls under their
85    /// recorded key).
86    AwaitingTool,
87    /// The run parked awaiting input. Carries what a resume needs: why it
88    /// parked and the schema the input must satisfy.
89    Suspended {
90        /// The recorded suspension reason.
91        reason: String,
92        /// The JSON Schema the resume input is validated against.
93        input_schema: Value,
94    },
95    /// A declared budget was crossed and the run parked. A human can raise
96    /// the limit and resume.
97    BudgetExceeded {
98        /// The budget that was crossed.
99        budget: Budget,
100        /// The observed value, in the units of the budget's kind.
101        observed: f64,
102    },
103    /// A write intent is recorded with no completion. The write may or may
104    /// not have happened; only a human may decide what happens next. Never
105    /// derived as retryable, by design.
106    NeedsReconciliation,
107    /// The run finished with this output.
108    Completed {
109        /// The recorded final output.
110        output: Value,
111    },
112    /// The run failed with this error.
113    Failed {
114        /// The recorded failure description.
115        error: String,
116    },
117    /// The run was abandoned by an operator: a terminal resting state in its
118    /// own right, distinct from [`RunStatus::Failed`]. Abandonment is a
119    /// deliberate retirement, not a failure, so it reads as its own muted
120    /// terminal everywhere downstream rather than borrowing the failure ink.
121    Abandoned {
122        /// The operator's optional note for why the run was abandoned.
123        reason: Option<String>,
124        /// The write intent left unsettled when a needs-reconciliation run was
125        /// abandoned, when there was one. Present only for a run abandoned from
126        /// [`RunStatus::NeedsReconciliation`]; the abandonment records it so the
127        /// terminal state never claims the write question was answered.
128        unresolved_write: Option<UnresolvedWrite>,
129    },
130}
131
132/// Everything a log prefix implies about a run.
133#[derive(Debug, Clone, PartialEq)]
134pub struct RunState {
135    /// Where the run stands.
136    pub status: RunStatus,
137    /// The position the next appended event will occupy.
138    pub next_seq: SequenceNumber,
139    /// Token usage accumulated over every completed model call.
140    pub usage: TokenTotals,
141    /// The dangling call intent, when one exists. `Some` whenever `status`
142    /// is [`RunStatus::AwaitingModel`], [`RunStatus::AwaitingTool`], or
143    /// [`RunStatus::NeedsReconciliation`]. Kept through a terminal event
144    /// too, so an explicitly failed run still shows the unresolved call.
145    pub pending_call: Option<PendingCall>,
146}
147
148/// Folds an event log into the [`RunState`] it implies.
149///
150/// Total over every prefix of a valid log: it never panics and never
151/// executes anything, whatever boundary the log was cut at. Later events
152/// simply overwrite what earlier ones implied, so feeding it prefixes of
153/// growing length walks the run's whole state history (which is exactly what
154/// a dashboard scrubber will do).
155#[must_use]
156pub fn derive_state(log: &[EventEnvelope]) -> RunState {
157    let mut state = RunState {
158        status: RunStatus::NotStarted,
159        next_seq: SequenceNumber::new(0),
160        usage: TokenTotals::default(),
161        pending_call: None,
162    };
163    for envelope in log {
164        state.next_seq = envelope.seq.next();
165        match &envelope.event {
166            Event::RunStarted { .. } => {
167                state.status = RunStatus::Running;
168            }
169            Event::ModelCallRequested {
170                seq, request_hash, ..
171            } => {
172                state.pending_call = Some(PendingCall::Model {
173                    seq: *seq,
174                    request_hash: request_hash.clone(),
175                });
176                state.status = RunStatus::AwaitingModel;
177            }
178            Event::ModelCallCompleted { usage, .. } => {
179                state.usage.input_tokens = state
180                    .usage
181                    .input_tokens
182                    .saturating_add(u64::from(usage.input_tokens));
183                state.usage.output_tokens = state
184                    .usage
185                    .output_tokens
186                    .saturating_add(u64::from(usage.output_tokens));
187                state.pending_call = None;
188                state.status = RunStatus::Running;
189            }
190            Event::ToolCallRequested {
191                seq,
192                tool,
193                input,
194                effect,
195                idempotency_key,
196            } => {
197                state.pending_call = Some(PendingCall::Tool {
198                    seq: *seq,
199                    tool: tool.clone(),
200                    input: input.clone(),
201                    effect: *effect,
202                    idempotency_key: idempotency_key.clone(),
203                });
204                // The write rule: an uncompleted write intent is
205                // needs-reconciliation the moment it is the log's last word,
206                // and every prefix ending here is exactly that log.
207                state.status = match effect {
208                    Effect::Write => RunStatus::NeedsReconciliation,
209                    Effect::Read | Effect::Idempotent => RunStatus::AwaitingTool,
210                };
211            }
212            Event::ToolCallCompleted { .. } => {
213                state.pending_call = None;
214                state.status = RunStatus::Running;
215            }
216            // Deterministic-context observations change no run status; they
217            // only exist so replay can hand the same values back.
218            Event::NowObserved { .. } | Event::RandomObserved { .. } => {}
219            Event::Suspended {
220                reason,
221                input_schema,
222            } => {
223                state.status = RunStatus::Suspended {
224                    reason: reason.clone(),
225                    input_schema: input_schema.clone(),
226                };
227            }
228            Event::Resumed { .. } => {
229                state.status = RunStatus::Running;
230            }
231            Event::BudgetExceeded { budget, observed } => {
232                state.status = RunStatus::BudgetExceeded {
233                    budget: *budget,
234                    observed: *observed,
235                };
236            }
237            Event::RunCompleted { output } => {
238                state.status = RunStatus::Completed {
239                    output: output.clone(),
240                };
241            }
242            Event::RunFailed { error } => {
243                state.status = RunStatus::Failed {
244                    error: error.clone(),
245                };
246            }
247            // An operator-appended terminal. Abandonment gets its own resting
248            // status, never `Failed`: the two are different facts and read
249            // differently downstream. The `pending_call` is left as the log's
250            // last dangling intent implied (kept through terminal events, like
251            // `Failed`), so an abandoned needs-reconciliation run still surfaces
252            // the write; `unresolved_write` is the durable, recorded copy of
253            // that evidence carried on the status itself.
254            Event::RunAbandoned {
255                reason,
256                unresolved_write,
257            } => {
258                state.status = RunStatus::Abandoned {
259                    reason: reason.clone(),
260                    unresolved_write: unresolved_write.clone(),
261                };
262            }
263            // A graph run's head. It stands where an agent run's `RunStarted`
264            // does: the run is now under way, so the status becomes `Running`.
265            // No new `RunStatus` variant is minted for graph runs — the whole
266            // point of this fold's graph handling is that a graph run reads
267            // through the same agent-run status vocabulary. Between its
268            // recorded steps it is `Running`; a dangling model or tool call
269            // inside one of its agent or tool nodes still arrives as an
270            // ordinary `ModelCallRequested`/`ToolCallRequested`, so the arms
271            // above already carry it to `AwaitingModel`, `AwaitingTool`, or
272            // `NeedsReconciliation` with no graph-specific code. `usage`
273            // accumulates across every node's model calls; `pending_call`
274            // surfaces whichever node's call is dangling. The per-node picture
275            // (which node is current, which branch fired, map fan-out) is a
276            // separate projection, `crate::graph_state`, deliberately kept out
277            // of this run-level status.
278            Event::GraphRunStarted { .. } => {
279                state.status = RunStatus::Running;
280            }
281            // The graph node/branch/map/fold markers narrate the walk for the
282            // per-node projection; at the run level they are structural notes
283            // that change no status, exactly like the context observations
284            // above. A graph run sits at `Running` across all of them (the
285            // real call boundaries are the model/tool intents they bracket).
286            Event::NodeEntered { .. }
287            | Event::NodeExited { .. }
288            | Event::NodeSkipped { .. }
289            | Event::BranchTaken { .. }
290            | Event::MapFannedOut { .. }
291            | Event::MapIterationStarted { .. }
292            | Event::MapIterationJoined { .. }
293            | Event::FoldIterationStarted { .. }
294            | Event::FoldIterationJoined { .. }
295            | Event::FoldConverged { .. } => {}
296        }
297    }
298    state
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::event::TokenUsage;
305    use crate::id::RunId;
306    use time::macros::datetime;
307    use uuid::Uuid;
308
309    fn log(events: Vec<Event>) -> Vec<EventEnvelope> {
310        let run_id =
311            RunId::from_uuid(Uuid::parse_str("00000000-0000-4000-8000-000000000002").unwrap());
312        events
313            .into_iter()
314            .enumerate()
315            .map(|(i, event)| {
316                EventEnvelope::new(
317                    run_id,
318                    SequenceNumber::new(i as u64),
319                    datetime!(2026-07-09 12:00:00 UTC),
320                    event,
321                )
322            })
323            .collect()
324    }
325
326    fn started() -> Event {
327        Event::RunStarted {
328            agent_def_hash: "sha256:agent".into(),
329            input: serde_json::json!({"topic": "otters"}),
330            labels: None,
331        }
332    }
333
334    /// The empty prefix is a state too: nothing recorded, next position 0.
335    #[test]
336    fn empty_log_derives_not_started() {
337        let state = derive_state(&[]);
338        assert_eq!(state.status, RunStatus::NotStarted);
339        assert_eq!(state.next_seq, SequenceNumber::new(0));
340        assert_eq!(state.usage, TokenTotals::default());
341        assert_eq!(state.pending_call, None);
342    }
343
344    /// A log ending between steps derives to running.
345    #[test]
346    fn run_started_derives_running() {
347        let state = derive_state(&log(vec![started()]));
348        assert_eq!(state.status, RunStatus::Running);
349        assert_eq!(state.next_seq, SequenceNumber::new(1));
350    }
351
352    /// A dangling model intent derives to awaiting-model with the pending
353    /// call carrying what a re-issue needs.
354    #[test]
355    fn dangling_model_intent_derives_awaiting_model() {
356        let state = derive_state(&log(vec![
357            started(),
358            Event::ModelCallRequested {
359                seq: SequenceNumber::new(1),
360                request_hash: "sha256:req".into(),
361                request_body: None,
362            },
363        ]));
364        assert_eq!(state.status, RunStatus::AwaitingModel);
365        assert_eq!(
366            state.pending_call,
367            Some(PendingCall::Model {
368                seq: SequenceNumber::new(1),
369                request_hash: "sha256:req".into(),
370            })
371        );
372    }
373
374    /// A dangling read intent derives to awaiting-tool: reads re-execute
375    /// freely, so the state is retryable.
376    #[test]
377    fn dangling_read_intent_derives_awaiting_tool() {
378        let state = derive_state(&log(vec![
379            started(),
380            Event::ToolCallRequested {
381                seq: SequenceNumber::new(1),
382                tool: "search".into(),
383                input: serde_json::json!({"q": "otters"}),
384                effect: Effect::Read,
385                idempotency_key: None,
386            },
387        ]));
388        assert_eq!(state.status, RunStatus::AwaitingTool);
389    }
390
391    /// A dangling idempotent intent is retryable and surfaces its recorded
392    /// idempotency key, so the retry collapses at the provider.
393    #[test]
394    fn dangling_idempotent_intent_surfaces_recorded_key() {
395        let state = derive_state(&log(vec![
396            started(),
397            Event::ToolCallRequested {
398                seq: SequenceNumber::new(1),
399                tool: "store".into(),
400                input: serde_json::json!({"doc": 1}),
401                effect: Effect::Idempotent,
402                idempotency_key: Some("key-7".into()),
403            },
404        ]));
405        assert_eq!(state.status, RunStatus::AwaitingTool);
406        match state.pending_call {
407            Some(PendingCall::Tool {
408                idempotency_key, ..
409            }) => assert_eq!(idempotency_key.as_deref(), Some("key-7")),
410            other => panic!("expected pending tool call, got {other:?}"),
411        }
412    }
413
414    /// The write rule: a dangling write intent derives to
415    /// needs-reconciliation, never to anything retryable.
416    #[test]
417    fn dangling_write_intent_derives_needs_reconciliation() {
418        let state = derive_state(&log(vec![
419            started(),
420            Event::ToolCallRequested {
421                seq: SequenceNumber::new(1),
422                tool: "create_ticket".into(),
423                input: serde_json::json!({"title": "bug"}),
424                effect: Effect::Write,
425                idempotency_key: None,
426            },
427        ]));
428        assert_eq!(state.status, RunStatus::NeedsReconciliation);
429        assert!(matches!(
430            state.pending_call,
431            Some(PendingCall::Tool {
432                effect: Effect::Write,
433                ..
434            })
435        ));
436    }
437
438    /// A completed write intent is history, not a hazard.
439    #[test]
440    fn completed_write_derives_running() {
441        let state = derive_state(&log(vec![
442            started(),
443            Event::ToolCallRequested {
444                seq: SequenceNumber::new(1),
445                tool: "create_ticket".into(),
446                input: serde_json::json!({"title": "bug"}),
447                effect: Effect::Write,
448                idempotency_key: None,
449            },
450            Event::ToolCallCompleted {
451                seq: SequenceNumber::new(1),
452                output: serde_json::json!({"id": "TICKET-1"}),
453            },
454        ]));
455        assert_eq!(state.status, RunStatus::Running);
456        assert_eq!(state.pending_call, None);
457    }
458
459    /// A log ending at a suspension derives to suspended, carrying what a
460    /// resume needs.
461    #[test]
462    fn suspension_without_resume_derives_suspended() {
463        let schema = serde_json::json!({"type": "object"});
464        let state = derive_state(&log(vec![
465            started(),
466            Event::Suspended {
467                reason: "awaiting approval".into(),
468                input_schema: schema.clone(),
469            },
470        ]));
471        assert_eq!(
472            state.status,
473            RunStatus::Suspended {
474                reason: "awaiting approval".into(),
475                input_schema: schema,
476            }
477        );
478    }
479
480    /// A recorded resume puts the run back in running.
481    #[test]
482    fn resume_derives_running() {
483        let state = derive_state(&log(vec![
484            started(),
485            Event::Suspended {
486                reason: "awaiting approval".into(),
487                input_schema: serde_json::json!({"type": "object"}),
488            },
489            Event::Resumed {
490                input: serde_json::json!({"approved": true}),
491            },
492        ]));
493        assert_eq!(state.status, RunStatus::Running);
494    }
495
496    /// A log ending at a budget crossing derives to budget-exceeded, parked
497    /// rather than dead.
498    #[test]
499    fn budget_crossing_derives_budget_exceeded() {
500        let budget = Budget {
501            kind: crate::event::BudgetKind::CostUsd,
502            limit: 2.0,
503        };
504        let state = derive_state(&log(vec![
505            started(),
506            Event::BudgetExceeded {
507                budget,
508                observed: 2.5,
509            },
510        ]));
511        assert_eq!(
512            state.status,
513            RunStatus::BudgetExceeded {
514                budget,
515                observed: 2.5,
516            }
517        );
518    }
519
520    /// Terminal events derive to completed and failed, carrying their
521    /// recorded payloads.
522    #[test]
523    fn terminal_events_derive_terminal_statuses() {
524        let completed = derive_state(&log(vec![
525            started(),
526            Event::RunCompleted {
527                output: serde_json::json!({"summary": "done"}),
528            },
529        ]));
530        assert_eq!(
531            completed.status,
532            RunStatus::Completed {
533                output: serde_json::json!({"summary": "done"}),
534            }
535        );
536
537        let failed = derive_state(&log(vec![
538            started(),
539            Event::RunFailed {
540                error: "provider timeout".into(),
541            },
542        ]));
543        assert_eq!(
544            failed.status,
545            RunStatus::Failed {
546                error: "provider timeout".into(),
547            }
548        );
549    }
550
551    /// An abandonment derives to the abandoned terminal, carrying its recorded
552    /// reason. A bare abandonment (no dangling write) carries no
553    /// unresolved-write evidence.
554    #[test]
555    fn abandonment_derives_abandoned() {
556        let state = derive_state(&log(vec![
557            started(),
558            Event::RunAbandoned {
559                reason: Some("husk is dead forever".into()),
560                unresolved_write: None,
561            },
562        ]));
563        assert_eq!(
564            state.status,
565            RunStatus::Abandoned {
566                reason: Some("husk is dead forever".into()),
567                unresolved_write: None,
568            }
569        );
570    }
571
572    /// Abandoning a needs-reconciliation run derives to the abandoned terminal
573    /// carrying the unresolved-write evidence, and the dangling write is still
574    /// surfaced through `pending_call` (kept through the terminal event), so the
575    /// abandoned state never claims the write question was answered.
576    #[test]
577    fn abandonment_of_needs_reconciliation_records_unresolved_write() {
578        let state = derive_state(&log(vec![
579            started(),
580            Event::ToolCallRequested {
581                seq: SequenceNumber::new(1),
582                tool: "create_ticket".into(),
583                input: serde_json::json!({"title": "bug"}),
584                effect: Effect::Write,
585                idempotency_key: None,
586            },
587            Event::RunAbandoned {
588                reason: None,
589                unresolved_write: Some(UnresolvedWrite {
590                    seq: SequenceNumber::new(1),
591                    tool: "create_ticket".into(),
592                }),
593            },
594        ]));
595        assert_eq!(
596            state.status,
597            RunStatus::Abandoned {
598                reason: None,
599                unresolved_write: Some(UnresolvedWrite {
600                    seq: SequenceNumber::new(1),
601                    tool: "create_ticket".into(),
602                }),
603            }
604        );
605        assert!(matches!(
606            state.pending_call,
607            Some(PendingCall::Tool {
608                effect: Effect::Write,
609                ..
610            })
611        ));
612    }
613
614    /// Context observations leave the status untouched.
615    #[test]
616    fn context_events_do_not_change_status() {
617        let state = derive_state(&log(vec![
618            started(),
619            Event::NowObserved {
620                now: datetime!(2026-07-09 12:00:00.123456789 UTC),
621            },
622            Event::RandomObserved { value: u64::MAX },
623        ]));
624        assert_eq!(state.status, RunStatus::Running);
625        assert_eq!(state.next_seq, SequenceNumber::new(3));
626    }
627
628    fn graph_started() -> Event {
629        Event::GraphRunStarted {
630            graph_hash: "sha256:graph".into(),
631            input: serde_json::json!({"topic": "otters"}),
632            labels: None,
633            forked_from: None,
634        }
635    }
636
637    /// A graph run's head derives to running, exactly as an agent run's does:
638    /// no new status is minted for graph runs.
639    #[test]
640    fn graph_run_started_derives_running() {
641        let state = derive_state(&log(vec![graph_started()]));
642        assert_eq!(state.status, RunStatus::Running);
643        assert_eq!(state.next_seq, SequenceNumber::new(1));
644    }
645
646    /// The graph node/branch/map markers change no run status: a graph run
647    /// reads as running across them, and `next_seq` still advances.
648    #[test]
649    fn graph_markers_do_not_change_status() {
650        let state = derive_state(&log(vec![
651            graph_started(),
652            Event::NodeEntered {
653                node: "research".into(),
654            },
655            Event::BranchTaken {
656                node: "gate".into(),
657                case: "approved".into(),
658            },
659            Event::MapFannedOut {
660                node: "fanout".into(),
661                items: serde_json::json!([1, 2]),
662            },
663            Event::MapIterationStarted {
664                node: "fanout".into(),
665                index: 0,
666                child_run: "sha256:child".into(),
667            },
668            Event::MapIterationJoined {
669                node: "fanout".into(),
670                index: 0,
671            },
672            Event::NodeExited {
673                node: "research".into(),
674            },
675            Event::NodeSkipped {
676                node: "publish".into(),
677                reason: "unreached".into(),
678            },
679        ]));
680        assert_eq!(state.status, RunStatus::Running);
681        assert_eq!(state.next_seq, SequenceNumber::new(8));
682        assert_eq!(state.pending_call, None);
683    }
684
685    /// A dangling model call inside a graph run's agent node surfaces through
686    /// the same `AwaitingModel` status as an agent run: the graph markers add
687    /// no new call-boundary status, they only bracket the real intents.
688    #[test]
689    fn dangling_model_call_inside_a_node_derives_awaiting_model() {
690        let state = derive_state(&log(vec![
691            graph_started(),
692            Event::NodeEntered {
693                node: "research".into(),
694            },
695            Event::ModelCallRequested {
696                seq: SequenceNumber::new(2),
697                request_hash: "sha256:req".into(),
698                request_body: None,
699            },
700        ]));
701        assert_eq!(state.status, RunStatus::AwaitingModel);
702        assert_eq!(
703            state.pending_call,
704            Some(PendingCall::Model {
705                seq: SequenceNumber::new(2),
706                request_hash: "sha256:req".into(),
707            })
708        );
709    }
710
711    /// Usage accumulates across every completed model call, widened to u64.
712    #[test]
713    fn usage_accumulates_across_model_calls() {
714        let state = derive_state(&log(vec![
715            started(),
716            Event::ModelCallRequested {
717                seq: SequenceNumber::new(1),
718                request_hash: "sha256:a".into(),
719                request_body: None,
720            },
721            Event::ModelCallCompleted {
722                seq: SequenceNumber::new(1),
723                response: serde_json::json!({"text": "one"}),
724                usage: TokenUsage {
725                    input_tokens: 100,
726                    output_tokens: 40,
727                },
728            },
729            Event::ModelCallRequested {
730                seq: SequenceNumber::new(3),
731                request_hash: "sha256:b".into(),
732                request_body: None,
733            },
734            Event::ModelCallCompleted {
735                seq: SequenceNumber::new(3),
736                response: serde_json::json!({"text": "two"}),
737                usage: TokenUsage {
738                    input_tokens: u32::MAX,
739                    output_tokens: 2,
740                },
741            },
742        ]));
743        assert_eq!(
744            state.usage,
745            TokenTotals {
746                input_tokens: 100 + u64::from(u32::MAX),
747                output_tokens: 42,
748            }
749        );
750    }
751}