Skip to main content

claude_codes/io/
result.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4
5/// Result message for completed queries
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ResultMessage {
8    pub subtype: ResultSubtype,
9    pub is_error: bool,
10    pub duration_ms: u64,
11    pub duration_api_ms: u64,
12
13    /// Time to first token, in milliseconds.
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub ttft_ms: Option<u64>,
16
17    /// Time to first streamed token, in milliseconds.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub ttft_stream_ms: Option<u64>,
20
21    /// Time from session start until the first request was issued, in milliseconds.
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub time_to_request_ms: Option<u64>,
24
25    /// How [`time_to_request_ms`](Self::time_to_request_ms) was spent, in
26    /// integer milliseconds per phase, summing to it exactly (CLI 2.1.285+):
27    /// `system_prompt`, `system_prompt_rebuild` (a second assembly of the
28    /// system prompt, run when the tool pool re-read after the message was
29    /// handled differs from the turn-start pool; CLIs before 2.1.287 report
30    /// it inside `system_prompt`), `resume_other` (settling, before the
31    /// turn's own prompt, tool calls that an earlier run left open or the
32    /// session's client handed over — see
33    /// [`turn_start_resume_kind`](Self::turn_start_resume_kind) — excluding
34    /// four parts that have keys of their own: `resume_store_confirm`,
35    /// `resume_staged_files_wait`, `resume_connector_wait` and
36    /// `turn_start_tool_run`; CLIs before 2.1.287 report these five as one
37    /// `turn_start_resume` key), `process_user_input` (excluding
38    /// `input_images`, `input_attachments`, `input_command` and
39    /// `input_hooks`, which have keys of their own on 2.1.287+),
40    /// `transcript_persist`, `init_message`, `engine_pickup`,
41    /// `memory_recall_wait`, `memory_context_wait`, `autocompact`,
42    /// `query_setup`, `tool_schema_build`, `message_normalization`,
43    /// `client_creation`, (CLI 2.1.316+) `conversation_append` (from the end
44    /// of `process_user_input` to the start of `tool_pool_refresh`, less
45    /// `transcript_persist`), `init_frame_send` (from the end of
46    /// `init_message` to the start of `engine_pickup`), `loop_entry` (from the
47    /// end of `engine_pickup` to the start of `message_prep`, less the memory
48    /// waits), `request_announcements` (from the end of `autocompact` to the
49    /// start of `query_setup`, less the model choice) and `other` (the
50    /// remainder no phase covers; producers before 2.1.316 count those four
51    /// stretches here). A phase that did not run is absent; `other` is always
52    /// present. Open set —
53    /// keep unrecognized phase names. Present where `time_to_request_ms` is,
54    /// on `CLAUDE_CODE_REMOTE` sessions and hosts that set
55    /// `CLAUDE_CODE_EMIT_STARTUP_TIMING`; absent elsewhere.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub time_to_request_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
58
59    /// What the turn was given to settle before its own prompt; the
60    /// `resume_*` phases and `turn_start_tool_run` in
61    /// [`time_to_request_phases_ms`](Self::time_to_request_phases_ms) say how
62    /// long that took (CLI 2.1.287+). A turn given more than one kind reports
63    /// the first in [`TurnStartResumeKind`] order. Present where
64    /// `time_to_request_phases_ms` is.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub turn_start_resume_kind: Option<TurnStartResumeKind>,
67
68    /// On a turn the session's client handed over (`turn_start_resume_kind`
69    /// is `turn_handoff`), how long its `turn_handoff` control request waited
70    /// from its arrival on the session server's SSE stream until the input
71    /// loop read it, in integer ms, split into `startup_wait`,
72    /// `control_requests_ahead` and `user_frames_ahead` (each absent when 0)
73    /// and `before_read` (the rest, always present). The values sum to the
74    /// wait. Open set — keep unrecognized part names. Present only on
75    /// results that carry `turn_start_resume_kind` (CLI 2.1.300+).
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub handoff_intake_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
78
79    /// Number of earlier turns of this CLI process that sent a
80    /// main-conversation model request: 0 is the process's first turn to
81    /// reach the model, not the conversation's first. Turns stopped or
82    /// failed after sending count but carry no value, so values can skip.
83    /// Where one process hosts several sessions, each has its own count.
84    /// Present where
85    /// [`time_to_request_phases_ms`](Self::time_to_request_phases_ms) is
86    /// (CLI 2.1.285+).
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub process_turn_index: Option<u64>,
89
90    /// CPU time (user plus system) the CLI process used from turn start
91    /// until it sent the turn's first main-conversation model request, in
92    /// milliseconds (CLI 2.1.285+). Near `time_to_request_ms`, the window
93    /// was mostly computing; near 0, mostly waiting. It adds up all threads,
94    /// so it can exceed the window, and leaves out child processes. Present
95    /// where `time_to_request_phases_ms` is, unless the runtime reports no
96    /// resource usage or the process hosts other sessions too.
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub time_to_request_cpu_ms: Option<u64>,
99
100    /// The longest single stall of the session's thread between turn start
101    /// and the turn's first main-conversation model request, in
102    /// milliseconds, measured as how late a 50 ms timer fired (CLI
103    /// 2.1.285+): the real stall can be up to 50 ms longer, and a few ms is
104    /// timer noise. Only the first two minutes are measured. Present where
105    /// `time_to_request_phases_ms` is.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub time_to_request_loop_lag_ms: Option<u64>,
108
109    /// Major page faults (waits for memory to be read from disk) by the
110    /// process in the
111    /// [`time_to_request_cpu_ms`](Self::time_to_request_cpu_ms) window;
112    /// Windows also counts faults needing no disk read. Present where
113    /// `time_to_request_cpu_ms` is (CLI 2.1.285+).
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub time_to_request_major_faults: Option<u64>,
116
117    /// Resident memory of the CLI process at turn start, in whole MiB
118    /// (`VmRSS`), child processes left out. Sent only by a worker Anthropic
119    /// hosts, with telemetry on, on Linux (not WSL); there it is present,
120    /// always with [`process_rss_mib_result`](Self::process_rss_mib_result),
121    /// where `process_turn_index` is, unless the process hosts other
122    /// sessions too (CLI 2.1.300+).
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub process_rss_mib_turn_start: Option<u64>,
125
126    /// The anonymous part (`RssAnon`: heaps, stacks, JIT code) of
127    /// [`process_rss_mib_turn_start`](Self::process_rss_mib_turn_start), in
128    /// whole MiB (CLI 2.1.300+).
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub process_anon_mib_turn_start: Option<u64>,
131
132    /// As [`process_rss_mib_turn_start`](Self::process_rss_mib_turn_start),
133    /// read again just before this result is built: the level the turn
134    /// leaves behind (CLI 2.1.300+).
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub process_rss_mib_result: Option<u64>,
137
138    /// The anonymous part of
139    /// [`process_rss_mib_result`](Self::process_rss_mib_result), in whole
140    /// MiB (CLI 2.1.300+).
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub process_anon_mib_result: Option<u64>,
143
144    /// The most memory the process has had resident from its start until
145    /// this result (`VmHWM`), in whole MiB — not this turn's peak alone
146    /// (CLI 2.1.300+).
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub process_peak_rss_mib: Option<u64>,
149
150    /// CPU time (user plus system, all threads, no child processes) the CLI
151    /// process used from the end of its previous turn to this turn's start,
152    /// in ms. Divide sums of it by sums of
153    /// [`process_wall_ms_between_turns`](Self::process_wall_ms_between_turns),
154    /// which always comes with it, for CPU per second. Present where
155    /// [`process_cpu_ms_turn`](Self::process_cpu_ms_turn) is, from the
156    /// process's second turn on (CLI 2.1.300+).
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub process_cpu_ms_between_turns: Option<u64>,
159
160    /// Monotonic wall time of the
161    /// [`process_cpu_ms_between_turns`](Self::process_cpu_ms_between_turns)
162    /// stretch, in ms (CLI 2.1.300+).
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub process_wall_ms_between_turns: Option<u64>,
165
166    /// CPU time, counted as in
167    /// [`process_cpu_ms_between_turns`](Self::process_cpu_ms_between_turns),
168    /// from this turn's start until just before this result is built,
169    /// including the waits for the model and tools. Always comes with
170    /// [`process_wall_ms_turn`](Self::process_wall_ms_turn). Sent only by the
171    /// workers named under
172    /// [`process_rss_mib_turn_start`](Self::process_rss_mib_turn_start)
173    /// (CLI 2.1.300+).
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub process_cpu_ms_turn: Option<u64>,
176
177    /// How many times the session built its main tool list, first build
178    /// included, since the last result that carried this count (or since the
179    /// session started). A read that reused the stored list counts as none;
180    /// builds between turns and in turns whose result carried no count are
181    /// included, a subagent's list is not. Sent with
182    /// [`process_cpu_ms_turn`](Self::process_cpu_ms_turn) (CLI 2.1.321+).
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub tool_pool_build_count: Option<u64>,
185
186    /// Monotonic wall time of the
187    /// [`process_cpu_ms_turn`](Self::process_cpu_ms_turn) stretch, in ms
188    /// (CLI 2.1.300+).
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub process_wall_ms_turn: Option<u64>,
191
192    /// CPU time (user plus system), in ms, of the programs in the CLI's shell
193    /// CPU accounting group over the
194    /// [`process_cpu_ms_turn`](Self::process_cpu_ms_turn) stretch: the Bash
195    /// and Monitor tool shells and everything they start. Not part of
196    /// [`process_cpu_ms_turn`](Self::process_cpu_ms_turn), which leaves out
197    /// child processes. Hooks, MCP/LSP servers and plugin monitors are not
198    /// counted, and a command whose shell could not join the group (e.g.
199    /// under the command sandbox) makes it read lower rather than absent.
200    /// Present where `process_cpu_ms_turn` is, only on Linux with a cgroup v1
201    /// `cpuacct` hierarchy; absent, never 0, otherwise (CLI 2.1.312+).
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub shell_cpu_ms_turn: Option<u64>,
204
205    /// Shell CPU time, counted as in
206    /// [`shell_cpu_ms_turn`](Self::shell_cpu_ms_turn), from just before the
207    /// previous result that carried `shell_cpu_ms_turn` was built until this
208    /// turn's start, so summed over a process's results the two never count a
209    /// millisecond twice. Always comes with
210    /// [`shell_wall_ms_since_last_result`](Self::shell_wall_ms_since_last_result).
211    /// Absent on the process's first such result and after a gap in the
212    /// figures (CLI 2.1.312+).
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub shell_cpu_ms_since_last_result: Option<u64>,
215
216    /// Monotonic wall time of the
217    /// [`shell_cpu_ms_since_last_result`](Self::shell_cpu_ms_since_last_result)
218    /// stretch, in ms, by the same clock as
219    /// [`process_wall_ms_turn`](Self::process_wall_ms_turn) (CLI 2.1.312+).
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub shell_wall_ms_since_last_result: Option<u64>,
222
223    /// How long, within the
224    /// [`process_cpu_ms_turn`](Self::process_cpu_ms_turn) stretch and by its
225    /// clock, a response stream of a main-conversation model request was
226    /// open, in ms: from the stream's first event until the CLI stopped
227    /// reading it. Summed over the turn's requests; overlapping streams count
228    /// once. Subagent, side-request and compaction streams are not in it
229    /// (CLI 2.1.311+).
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub process_wall_ms_streaming: Option<u64>,
232
233    /// CPU time, counted as in
234    /// [`process_cpu_ms_between_turns`](Self::process_cpu_ms_between_turns),
235    /// used inside
236    /// [`process_wall_ms_streaming`](Self::process_wall_ms_streaming); part
237    /// of [`process_cpu_ms_turn`](Self::process_cpu_ms_turn). Always comes
238    /// with `process_wall_ms_streaming`, `main_stream_count` and
239    /// `main_stream_event_count` (CLI 2.1.311+).
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub process_cpu_ms_streaming: Option<u64>,
242
243    /// How many streams
244    /// [`process_wall_ms_streaming`](Self::process_wall_ms_streaming)
245    /// covers: one per attempt of a main-conversation model request that
246    /// brought a first event, so a retry counts again (CLI 2.1.311+).
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub main_stream_count: Option<u64>,
249
250    /// How many stream events the API sent on those streams, leaving out
251    /// keepalive pings the CLI makes up itself (CLI 2.1.311+).
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub main_stream_event_count: Option<u64>,
254
255    /// In how many chunks the runtime's fetch handed over the bodies of
256    /// those streams. Absent, never 0, when chunks were not counted (Bedrock,
257    /// Vertex, Foundry, Mantle, byte watchdog off, or a stream still open)
258    /// (CLI 2.1.311+).
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub main_stream_chunk_count: Option<u64>,
261
262    /// Bit set of the runtime tuning variables the CLI process was started
263    /// with (set and not empty): 1 `BUN_GC_TIMER_INTERVAL`, 2
264    /// `MIMALLOC_PURGE_HOLES_MIN_INTERVAL`, 4 `BUN_IDLE_GC_SECONDS`, 8
265    /// `MIMALLOC_PURGE_DELAY`, 16 `BUN_JSC_useFTLJIT`, 32
266    /// `BUN_JSC_percentCPUPerMBForEdenTimer`, 64
267    /// `BUN_JSC_percentCPUPerMBForFullTimer`. Names only, never values. Sent
268    /// only by the workers named under
269    /// [`process_rss_mib_turn_start`](Self::process_rss_mib_turn_start), on
270    /// results of any subtype (CLI 2.1.311+).
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub process_start_tuning: Option<u64>,
273
274    /// `MIMALLOC_PURGE_DELAY` in ms as the CLI process was started with it.
275    /// Sent only beside
276    /// [`process_start_tuning`](Self::process_start_tuning), and absent when
277    /// the variable was unset, empty or not one to seven digits
278    /// (CLI 2.1.311+).
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub process_start_purge_delay_ms: Option<u64>,
281
282    /// Garbage collections on the CLI process's main thread, counted from
283    /// early start-up. Only on the process's first numbered result within an
284    /// hour of its start, when started with
285    /// `CLAUDE_CODE_COUNT_GARBAGE_COLLECTIONS=1`, and only from
286    /// Anthropic-hosted cloud workers with telemetry on (CLI 2.1.311+).
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub process_garbage_collections: Option<ProcessGarbageCollections>,
289
290    /// What the `input_attachments` phase of `time_to_request_phases_ms` did
291    /// on the turn, to tell a wait inside one attachment producer from a
292    /// process that was busy. Present on turns where the phase started,
293    /// unless nothing of it is known (CLI 2.1.288+).
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub input_attachments_detail: Option<InputAttachmentsDetail>,
296
297    /// How the `resume_store_confirm` phase of `time_to_request_phases_ms`
298    /// was spent. Present where that phase is, in a session whose transcript
299    /// goes to a hosted store: on the result of a handed-off turn whose lines
300    /// were confirmed and that went on to a model request (CLI 2.1.290+).
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub resume_store_confirm_detail: Option<ResumeStoreConfirmDetail>,
303
304    /// How the `tool_schema_build` phase of `time_to_request_phases_ms` was
305    /// spent, over the main-conversation requests built before the turn's
306    /// first dispatch (normally one). Present where that phase is
307    /// (CLI 2.1.317+).
308    #[serde(default, skip_serializing_if = "Option::is_none")]
309    pub tool_schema_build_detail: Option<ToolSchemaBuildDetail>,
310
311    /// How the `message_normalization` phase of `time_to_request_phases_ms`
312    /// was spent: building, from the conversation, the messages the API
313    /// takes. Present where that phase is (CLI 2.1.317+).
314    #[serde(default, skip_serializing_if = "Option::is_none")]
315    pub message_normalization_detail: Option<MessageNormalizationDetail>,
316
317    /// How the `request_build` phase of `time_to_request_phases_ms` was
318    /// spent: assembling the request from the normalized messages up to its
319    /// dispatch. Present where that phase is (CLI 2.1.317+).
320    #[serde(default, skip_serializing_if = "Option::is_none")]
321    pub request_build_detail: Option<RequestBuildDetail>,
322
323    /// `process_cpu_ms_turn` and `process_wall_ms_turn` split among states
324    /// that exclude each other: the states' `cpu_ms` add up to the first
325    /// exactly and their `wall_ms` to the second, and the field is sent only
326    /// where they do (CLI 2.1.317+).
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub process_turn_states: Option<ProcessTurnStates>,
329
330    /// The turn's CPU time by thread group, in whole ms, keyed by a
331    /// `process_turn_states` state (or `between_turns` for the
332    /// `process_cpu_ms_between_turns` stretch). Each inner map holds
333    /// `one_in` (the N of the one-in-N sampling draw that picked this
334    /// process), `<group>_cpu_ms` and `<group>_system_ms` for the groups
335    /// `main`, `gc`, `jit`, `warm_up`, `allocator`, `http`, `pool` and
336    /// `other` (each left out where it rounds to 0), `process_cpu_ms`,
337    /// `process_system_ms`, `run_wait_ms` and `guest_steal_ms`. Sent by
338    /// about one worker process in 4,096 (`CLAUDE_CODE_THREAD_CPU_ONE_IN`
339    /// changes the rate), and left out where a reading failed or looked
340    /// unreliable. Marked `@internal` upstream (CLI 2.1.319+).
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub process_turn_threads:
343        Option<std::collections::BTreeMap<String, std::collections::BTreeMap<String, u64>>>,
344
345    /// Time from spawning a worker/spare until the first request was issued, in milliseconds.
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub time_to_request_from_spawn_ms: Option<u64>,
348
349    /// Whether a warm spare process was claimed for this request.
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub warm_spare_claimed: Option<bool>,
352
353    /// Whether this process started its start-up feature-flag fetch before
354    /// the wait for it, or why not: the same value as `system/init`'s
355    /// `startup_timing.flag_fetch_kick`. An open string set
356    /// (`started_in_init_*`, `started_after_init_*`, `not_asked_warm`,
357    /// `settings_pending_*`, `launch_excluded`, `not_eligible`; an `_empty`
358    /// or `_warm` ending is the flag cache at that moment). Present only
359    /// where `time_to_request_from_spawn_ms` is, and only when start-up
360    /// judged it (CLI 2.1.288+).
361    #[serde(default, skip_serializing_if = "Option::is_none")]
362    pub flag_fetch_kick: Option<String>,
363
364    /// Epoch-ish timestamp origin used by CLI timing instrumentation.
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub time_origin_ms: Option<u64>,
367
368    /// Wall-clock epoch milliseconds when the API request was sent
369    /// (fractional; from CLI timing instrumentation).
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub request_sent_wall_ms: Option<f64>,
372
373    /// Input tokens of the model call whose send time is
374    /// [`request_sent_wall_ms`](Self::request_sent_wall_ms): `input_tokens +
375    /// cache_read_input_tokens + cache_creation_input_tokens` from that call's
376    /// `message_start`, so a consumer can group turn latency by prompt size
377    /// (CLI 2.1.286+). Present only with `request_sent_wall_ms`, and only
378    /// when the sum is above zero.
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub first_request_input_tokens: Option<u64>,
381
382    /// The `cache_read_input_tokens` part of
383    /// [`first_request_input_tokens`](Self::first_request_input_tokens),
384    /// `0` included, so a consumer can tell a cold prompt cache from a large
385    /// prompt. Present exactly when `first_request_input_tokens` is
386    /// (CLI 2.1.321+).
387    #[serde(default, skip_serializing_if = "Option::is_none")]
388    pub first_request_cache_read_input_tokens: Option<u64>,
389
390    /// Time until the first content frame of the stream arrived, in
391    /// milliseconds (CLI 2.1.260+ timing instrumentation).
392    #[serde(default, skip_serializing_if = "Option::is_none")]
393    pub first_content_frame_ms: Option<u64>,
394
395    /// Time until the first stream POST was issued, in milliseconds
396    /// (CLI 2.1.260+).
397    #[serde(default, skip_serializing_if = "Option::is_none")]
398    pub first_stream_post_ms: Option<u64>,
399
400    /// Time until the first stream POST was acknowledged, in milliseconds
401    /// (CLI 2.1.260+).
402    #[serde(default, skip_serializing_if = "Option::is_none")]
403    pub first_stream_post_ack_ms: Option<u64>,
404
405    /// Time the first stream POST spent waiting in the outbound queue, in
406    /// milliseconds (CLI 2.1.278+).
407    #[serde(default, skip_serializing_if = "Option::is_none")]
408    pub first_stream_post_queue_wait_ms: Option<u64>,
409
410    /// What the first stream POST was queued behind (CLI 2.1.278+).
411    #[serde(default, skip_serializing_if = "Option::is_none")]
412    pub first_stream_post_queued_behind: Option<StreamPostQueuedBehind>,
413
414    /// Wall-clock epoch milliseconds when the first stream POST was issued
415    /// (fractional; CLI 2.1.260+).
416    #[serde(default, skip_serializing_if = "Option::is_none")]
417    pub first_stream_post_wall_ms: Option<f64>,
418
419    /// Wall-clock epoch milliseconds when the triggering send's frame arrived
420    /// on a session reading its input from the session server's SSE stream
421    /// (`--sdk-url`), before the input loop read it (CLI 2.1.278+). With
422    /// [`frame_enqueued_wall_ms`](Self::frame_enqueued_wall_ms) and
423    /// [`turn_started_wall_ms`](Self::turn_started_wall_ms) it splits the
424    /// stretch between the server's persist and `request_sent_wall_ms` into
425    /// transit, the input loop's handling of the frame, its wait on the
426    /// command queue, and the turn's own work. Present only together with
427    /// `request_sent_wall_ms`, and only when the transport recorded the
428    /// receipt; absent on a local stdin host.
429    #[serde(default, skip_serializing_if = "Option::is_none")]
430    pub frame_received_wall_ms: Option<f64>,
431
432    /// Wall-clock epoch milliseconds when the input loop put the triggering
433    /// send's frame on the command queue (CLI 2.1.278+). Present exactly
434    /// when [`frame_received_wall_ms`](Self::frame_received_wall_ms) is.
435    #[serde(default, skip_serializing_if = "Option::is_none")]
436    pub frame_enqueued_wall_ms: Option<f64>,
437
438    /// How the triggering send's frame spent the time between
439    /// [`frame_received_wall_ms`](Self::frame_received_wall_ms) and
440    /// [`frame_enqueued_wall_ms`](Self::frame_enqueued_wall_ms), in integer
441    /// milliseconds per intake step (CLI 2.1.282+): `before_read` (until the
442    /// input loop read the frame, including any wait behind earlier frames
443    /// and a first message's wait for the worker to finish starting),
444    /// `dedup`, `flag_settle`, `receive_hook`, `attachments`,
445    /// `admission_wait` (held behind a pending model switch, a pending MCP
446    /// server-set change, start-up work the worker still owed its first
447    /// turn, or an earlier held frame), `admit_check` (the host's decision
448    /// whether to queue the frame, when it had to be awaited) and `other`.
449    /// Steps that did not run are absent; `before_read` and `other` are
450    /// always present. The values sum to the enqueued-minus-received gap
451    /// unless a step came out negative and was clamped to 0. Open set —
452    /// keep unrecognized step names. Present exactly when
453    /// `frame_received_wall_ms` is, except from older producers.
454    #[serde(default, skip_serializing_if = "Option::is_none")]
455    pub frame_intake_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
456
457    /// Wall-clock epoch milliseconds when the turn's clock started: the
458    /// anchor `duration_ms`, `ttft_ms`, `ttft_stream_ms`,
459    /// `time_to_request_ms`, `first_content_frame_ms`, `first_stream_post_ms`
460    /// and `first_stream_post_ack_ms` are measured from (CLI 2.1.278+).
461    /// Present exactly when
462    /// [`frame_received_wall_ms`](Self::frame_received_wall_ms) is.
463    #[serde(default, skip_serializing_if = "Option::is_none")]
464    pub turn_started_wall_ms: Option<f64>,
465
466    /// How the time from
467    /// [`frame_enqueued_wall_ms`](Self::frame_enqueued_wall_ms) to
468    /// [`turn_started_wall_ms`](Self::turn_started_wall_ms) was spent, in
469    /// integer milliseconds per step of the host's work through its message
470    /// queue (CLI 2.1.285+): `idle`, `earlier_turn`, `after_turn`,
471    /// `background_wait`, `entry_joins`, `sdk_mcp_update`, `loop_top`,
472    /// `session_start_wait`, `first_command`, `mcp_prewait`, `repo_wait`,
473    /// `turn_joins`, `tool_pool`, `commit`, `after_arm`, `turn_gate`,
474    /// `turn_open`, `engine_setup`, `tool_reread`, `cwd` and `other`. Each
475    /// step is where the host was at the time, which for part of the stretch
476    /// may be an earlier message's turn. A step that rounds to 0 is absent;
477    /// `other` is always present. The values sum to the started-minus-enqueued
478    /// gap, except that they can exceed it (usually by 1 ms from rounding).
479    /// Open set — keep unrecognized step names. Present only when
480    /// `frame_received_wall_ms` is, and absent from older producers.
481    #[serde(default, skip_serializing_if = "Option::is_none")]
482    pub turn_start_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
483
484    /// Of the time from `frame_enqueued_wall_ms` to `turn_started_wall_ms`,
485    /// the integer milliseconds in which the `claude -p` host's message
486    /// reader was held by control requests (such as `mcp_set_servers` or
487    /// `set_permission_mode`) instead of reading the next message (CLI
488    /// 2.1.285+). It overlaps
489    /// [`turn_start_phases_ms`](Self::turn_start_phases_ms) and is not part
490    /// of its sum. Absent when 0.
491    #[serde(default, skip_serializing_if = "Option::is_none")]
492    pub turn_start_control_requests_ms: Option<u64>,
493
494    /// Time until the first text POST was issued, in milliseconds
495    /// (CLI 2.1.278+).
496    #[serde(default, skip_serializing_if = "Option::is_none")]
497    pub first_text_post_ms: Option<u64>,
498
499    /// Time the first text POST spent waiting in the outbound queue, in
500    /// milliseconds (CLI 2.1.287+).
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    pub first_text_post_queue_wait_ms: Option<u64>,
503
504    /// What the first text POST was queued behind (CLI 2.1.287+); the same
505    /// value set as
506    /// [`first_stream_post_queued_behind`](Self::first_stream_post_queued_behind).
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    pub first_text_post_queued_behind: Option<StreamPostQueuedBehind>,
509
510    /// Wall-clock epoch milliseconds when the first text POST was issued
511    /// (fractional; CLI 2.1.278+).
512    #[serde(default, skip_serializing_if = "Option::is_none")]
513    pub first_text_post_wall_ms: Option<f64>,
514
515    /// How many uploads carrying new visible reply text (as
516    /// [`first_text_post_ms`](Self::first_text_post_ms) defines it) the
517    /// remote transport sent during the turn, from the upload
518    /// `first_text_post_ms` measures until this result was built. Uploads
519    /// still queued then are not counted, and a retry counts only if it
520    /// carries text no earlier upload did. Present only when
521    /// `first_text_post_ms` is, and never in a claude-code-server tenant
522    /// (CLI 2.1.316+).
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    pub text_post_count: Option<u64>,
525
526    /// Of the uploads in [`text_post_count`](Self::text_post_count), how
527    /// many were dispatched more than 500 ms after the one before. Tool
528    /// calls, thinking and model pauses make such gaps too, so compare
529    /// groups of turns with each other, not with 0. Present where
530    /// `text_post_count` is (CLI 2.1.316+).
531    #[serde(default, skip_serializing_if = "Option::is_none")]
532    pub text_post_gap_over_500ms_count: Option<u64>,
533
534    /// The largest delay, in whole ms, between the remote transport's
535    /// stream-event flush timer falling due and its buffer being emptied,
536    /// from the turn's first frame of visible reply text until this result
537    /// was built — a lower bound on how late that timer fired. Read on the
538    /// wall clock and capped at 60000. Absent when
539    /// [`text_post_count`](Self::text_post_count) is, or when the buffer was
540    /// never emptied at or after a due time (CLI 2.1.316+).
541    #[serde(default, skip_serializing_if = "Option::is_none")]
542    pub text_post_flush_timer_max_late_ms: Option<u64>,
543
544    /// Wire uuid of the user message this result answers.
545    #[serde(skip_serializing_if = "Option::is_none")]
546    pub user_message_uuid: Option<String>,
547
548    /// The [`server_received_wall_ms`](crate::io::UserMessage::server_received_wall_ms)
549    /// of the user message [`user_message_uuid`](Self::user_message_uuid)
550    /// names, copied unchanged, so the session server can measure from its
551    /// own receipt to `request_sent_wall_ms` (CLI 2.1.286+). Present only with
552    /// `request_sent_wall_ms`, and only when that message carried a positive
553    /// finite number there.
554    #[serde(default, skip_serializing_if = "Option::is_none")]
555    pub user_message_server_received_wall_ms: Option<f64>,
556
557    /// Client uuids of every user message whose prompt this turn consumed, in
558    /// consumption order — all members of a prompt batch the host merged into
559    /// this one turn, plus any queued user message folded into the running
560    /// turn between tool rounds. Always contains `user_message_uuid`; at most
561    /// 64 entries. Absent on delivery-failure/zeroed results and from CLIs
562    /// before 2.1.259 (fall back to `user_message_uuid`).
563    #[serde(default, skip_serializing_if = "Vec::is_empty")]
564    pub user_message_uuids: Vec<String>,
565
566    /// Why this turn was the automatic re-run of a turn a worker restart
567    /// interrupted (`CLAUDE_CODE_RESUME_INTERRUPTED_TURN`): the host's
568    /// `CLAUDE_CODE_RESUME_REASON` when it set one (`host_draining`,
569    /// `checkpoint_restore`, `container_recreated`, ...), else
570    /// `interrupted_turn`. Present on a headless re-run's result, success or
571    /// error; absent on every other turn, on the Remote Control bridge's
572    /// per-turn synthetic results, and from CLIs before 2.1.268.
573    #[serde(default, skip_serializing_if = "Option::is_none")]
574    pub resume_reason: Option<String>,
575
576    /// The local slash command that produced this result when the query loop
577    /// was bypassed (success results only). Absent when the turn went to the
578    /// model and from CLIs before 2.1.268.
579    #[serde(default, skip_serializing_if = "Option::is_none")]
580    pub local_command: Option<String>,
581
582    /// User-initiated sends still waiting in the command queue when this
583    /// result was produced. Greater than 0 means at least one more user turn
584    /// follows without further input, barring cancellation. Absent on fatal
585    /// startup results, on surfaces without a command queue, and from CLIs
586    /// before 2.1.259.
587    #[serde(default, skip_serializing_if = "Option::is_none")]
588    pub queued_turn_count: Option<u64>,
589
590    pub num_turns: i32,
591
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub result: Option<String>,
594
595    #[serde(alias = "sessionId")]
596    pub session_id: String,
597    pub total_cost_usd: f64,
598
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub usage: Option<UsageInfo>,
601
602    /// Tools that were blocked due to permission denials during the session
603    #[serde(default)]
604    pub permission_denials: Vec<PermissionDenial>,
605
606    /// Error messages when `is_error` is true.
607    ///
608    /// Contains human-readable error strings (e.g., "No conversation found with session ID: ...").
609    /// This allows typed access to error conditions without needing to serialize to JSON and search.
610    #[serde(default)]
611    pub errors: Vec<String>,
612
613    #[serde(skip_serializing_if = "Option::is_none")]
614    pub uuid: Option<String>,
615
616    /// HTTP status code when the result is an API error (e.g., 429, 500, 529)
617    #[serde(skip_serializing_if = "Option::is_none")]
618    pub api_error_status: Option<u16>,
619
620    /// The `api_error_code` of the API error that ended the turn (see
621    /// `AssistantMessage::api_error_code`): the server's
622    /// `error.details.error_code` when it is an identifier. Absent when the
623    /// turn did not end on an API error, when the response carried no code,
624    /// and from CLIs before 2.1.274.
625    #[serde(default, skip_serializing_if = "Option::is_none")]
626    pub api_error_code: Option<String>,
627
628    /// The `api_error_origin` of the API error that ended the turn (see
629    /// `AssistantMessage::api_error_origin`): `first_party` when Anthropic's
630    /// own API sent the response. Absent when the turn did not end on an API
631    /// error or the origin is anything else; treat an unknown value as
632    /// absent (CLI 2.1.300+).
633    #[serde(default, skip_serializing_if = "Option::is_none")]
634    pub api_error_origin: Option<String>,
635
636    /// The typed kind of the API error that ended the turn (see
637    /// `AssistantMessage::api_error`). Absent when the turn did not end on
638    /// an API error, when the error has no typed kind, and from CLIs before
639    /// 2.1.285; treat an unknown value as absent.
640    #[serde(default, skip_serializing_if = "Option::is_none")]
641    pub api_error: Option<String>,
642
643    /// Why generation stopped (e.g., end_turn, max_tokens)
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub stop_reason: Option<String>,
646
647    /// Why the session ended (e.g., "completed")
648    #[serde(skip_serializing_if = "Option::is_none")]
649    pub terminal_reason: Option<String>,
650
651    /// Present, and true, when the host of a multi-tenant worker shut the
652    /// worker down while this turn ran, the shutdown was the first thing to
653    /// abort the turn, and the turn is left for the session's next worker.
654    /// The other frames of the ending, and `terminal_reason`, look the same
655    /// as a client's Stop; this tells the two apart. A success result can
656    /// carry it (a shutdown during a Stop hook), and a result without it can
657    /// still be a shutdown's (CLI 2.1.300+).
658    #[serde(default, skip_serializing_if = "Option::is_none")]
659    pub interrupted_by_shutdown: Option<bool>,
660
661    /// Present, and true, when a permission check ended this turn by
662    /// itself and nobody asked for the stop. Today only auto mode does so,
663    /// after a run of responses with no usable safety verdict. The turn ends
664    /// by an abort, so `terminal_reason` usually matches a client's Stop;
665    /// this tells the two apart. A result reporting a budget or turn limit
666    /// can carry it too, and so can a success result. Marked `@internal`
667    /// upstream (CLI 2.1.319+).
668    #[serde(default, skip_serializing_if = "Option::is_none")]
669    pub stopped_by_permission_check: Option<bool>,
670
671    /// Delivery sequence of this result within the run: how many results the
672    /// run numbered before this one, starting at 0, in the order the process
673    /// writes them. A result whose write fails still consumes its number, so
674    /// a gap means a result was lost. Distinct from `num_turns`. Numbered by
675    /// the process hosting the run (`claude -p`); a local client relaying a
676    /// cloud session passes the cloud numbering through and its own locally
677    /// built error results carry none. Absent from CLIs before 2.1.268.
678    #[serde(default, skip_serializing_if = "Option::is_none")]
679    pub result_index: Option<u64>,
680
681    /// When the claude.ai credential this process's requests send ends for
682    /// good, as an ISO 8601 instant, read when the result is written. For a
683    /// stored claude.ai login this is the end of the login's refresh chain
684    /// as stored at sign-in; renewing the access token does not move it. The
685    /// date is the server's when it sent one and otherwise a 30-day
686    /// estimate, and the real end can be up to 3 days earlier. The server is
687    /// not asked, so a login revoked early still shows its date, and a past
688    /// date means the login has probably ended, not that it certainly has.
689    /// Absent when no date is known, for every other credential (an API
690    /// key, a token from the environment, another provider), on results this
691    /// process did not number, and from CLIs before 2.1.318.
692    #[serde(default, skip_serializing_if = "Option::is_none")]
693    pub credential_expires_at: Option<String>,
694
695    /// Fast mode toggle state (e.g., "off")
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub fast_mode_state: Option<String>,
698
699    /// Why fast mode can't serve right now. Absent when nothing blocks it.
700    /// A paused-after-rate-limit run is not reported here; it rides
701    /// `fast_mode_state` as `"cooldown"`.
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
704
705    /// Per-model cost breakdown, keyed by model name (e.g. `"claude-opus-4-8"`).
706    #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
707    pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
708
709    /// Per-model usage and cost of the calls this process counted since it
710    /// wrote its previous result (since it started, for its first), keyed and
711    /// shaped like `modelUsage` and covering the same calls. Sum it over a
712    /// conversation's results instead of subtracting `modelUsage` totals: each
713    /// call is stated on at most one result, whatever a resume or a kill does
714    /// to `modelUsage`. Calls that finish after a result (a background
715    /// subagent, a late title call) are stated on the next result. Nothing is
716    /// carried across processes, so calls counted after a process's last
717    /// result (including a turn in flight when it is killed) are stated
718    /// nowhere. A model with nothing counted is left out; `{}` means nothing
719    /// was counted, and absent is not `{}`. Set by stream-json and `--sdk-url`
720    /// runs only; not set on hand-numbered error results (start-up failures,
721    /// errors that escape the run). Absent from CLIs before 2.1.309. Marked
722    /// `@internal` upstream.
723    #[serde(default, skip_serializing_if = "Option::is_none")]
724    pub model_usage_since_last_result: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
725
726    /// Subagents started through the Agent tool in this session, as running
727    /// totals. Cumulative like `modelUsage`: read the latest result rather
728    /// than summing across results. Absent from CLIs before 2.1.239.
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub subagent_stats: Option<SubagentStats>,
731
732    /// How many times a safety system has stopped one of this session's
733    /// model calls (main loop, subagents, forked skills, compaction and other
734    /// internal calls), as a running total since the session started in this
735    /// process: a `refusal` stop reason, a refusal answered on a fallback
736    /// model, output the API's content filter stopped, and (where the build
737    /// can tell) a server-side safety monitor block. Never sum across
738    /// results. Not saved, so a resumed session or a mid-session `/clear`
739    /// starts again at 0: over a stored log any result above 0 means a stop
740    /// happened, and a later 0 does not cancel it. Absent from CLIs before
741    /// 2.1.292. Marked `@internal` upstream.
742    #[serde(default, skip_serializing_if = "Option::is_none")]
743    pub safety_stops: Option<u64>,
744
745    /// Structured-output payload returned by the model, when enabled.
746    #[serde(skip_serializing_if = "Option::is_none")]
747    pub structured_output: Option<Value>,
748
749    /// Deferred tool-use termination payload.
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub deferred_tool_use: Option<DeferredToolUse>,
752
753    /// Provenance of the message/run.
754    #[serde(skip_serializing_if = "Option::is_none")]
755    pub origin: Option<super::message_types::MessageOrigin>,
756
757    /// Set by the self-hosted runner on the result it synthesizes when the
758    /// session process fails to start or exits abnormally. Absent on normal
759    /// results and on CLIs before 2.1.266.
760    #[serde(default, skip_serializing_if = "Option::is_none")]
761    pub runner_exit: Option<RunnerExit>,
762
763    /// Why Claude Code refused to start. Set on the zeroed
764    /// `error_during_execution` result a stream-json run writes before
765    /// exiting on a known startup failure; `errors` carries the same text as
766    /// stderr. Failures that used to end with stderr alone write that result
767    /// only when the host sets `CLAUDE_CODE_STARTUP_FAILURE_RESULTS`, except
768    /// `org_config_required_unavailable`, `org_config_refused` and
769    /// `config_file_invalid`, which every stream-json run writes. Absent on
770    /// every other result, on startup failures without a known cause, and
771    /// from CLIs before 2.1.274.
772    #[serde(default, skip_serializing_if = "Option::is_none")]
773    pub startup_failure_reason: Option<StartupFailureReason>,
774}
775
776/// [`ResultMessage::input_attachments_detail`] — what the
777/// `input_attachments` phase did on the turn (CLI 2.1.288+).
778#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
779pub struct InputAttachmentsDetail {
780    /// Label of the attachment producer that took longest from its start to
781    /// its result, among those that began within the phase.
782    #[serde(default, skip_serializing_if = "Option::is_none")]
783    pub slowest_producer: Option<String>,
784    /// How long [`Self::slowest_producer`] took. Well above
785    /// [`Self::cpu_ms`], that producer most likely waited (on a file, the
786    /// network or a child process).
787    #[serde(default, skip_serializing_if = "Option::is_none")]
788    pub slowest_producer_ms: Option<u64>,
789    /// `time_to_request_cpu_ms`'s counter over this phase alone. Absent
790    /// wherever `time_to_request_cpu_ms` is absent, and when the turn's
791    /// first model request went out before the phase ended.
792    #[serde(default, skip_serializing_if = "Option::is_none")]
793    pub cpu_ms: Option<u64>,
794    /// `time_to_request_major_faults`'s counter over this phase alone.
795    /// Absent where [`Self::cpu_ms`] is.
796    #[serde(default, skip_serializing_if = "Option::is_none")]
797    pub major_faults: Option<u64>,
798    /// How many files the `changed_files` producer set out to read again in
799    /// full because their modification time was later than the one recorded
800    /// when the conversation last wrote or edited them. Absent when the
801    /// conversation's record of files was empty or the producer did not run.
802    #[serde(default, skip_serializing_if = "Option::is_none")]
803    pub changed_files_reread: Option<u64>,
804    /// How the `changed_files` producer's compares ended in the main
805    /// thread's pass (a subagent's are not counted). Absent when nothing was
806    /// counted (CLI 2.1.300+).
807    #[serde(default, skip_serializing_if = "Option::is_none")]
808    pub touch_compare: Option<TouchCompareCounts>,
809
810    // The members below add up what the main thread's attachment passes
811    // measured of themselves (CLI 2.1.316+). Only a pass that began and
812    // ended within one run of the phase, before the turn's first model
813    // request, counts: not a subagent's or a background agent's, and not a
814    // pass with attachments switched off. The five `_ms` splits run in order,
815    // each rounded down, so their sum is at most the phase's time.
816    /// Work before any producer starts, such as working out which tools the
817    /// tool announcements list (CLI 2.1.316+).
818    #[serde(default, skip_serializing_if = "Option::is_none")]
819    pub setup_ms: Option<u64>,
820    /// Starting the producers that read the prompt's own text (at-mentioned
821    /// files, MCP resources, mentions of agents, skills and peers) and
822    /// waiting for them (CLI 2.1.316+).
823    #[serde(default, skip_serializing_if = "Option::is_none")]
824    pub prompt_producers_ms: Option<u64>,
825    /// Starting every other producer, each one's work up to its first wait
826    /// included, so the thread ran code throughout (CLI 2.1.316+).
827    #[serde(default, skip_serializing_if = "Option::is_none")]
828    pub ambient_start_ms: Option<u64>,
829    /// Waiting for the producers [`Self::ambient_start_ms`] started
830    /// (CLI 2.1.316+).
831    #[serde(default, skip_serializing_if = "Option::is_none")]
832    pub ambient_wait_ms: Option<u64>,
833    /// The rest: merging the producers' results, logging the attachments
834    /// with their token estimates and making each into a message
835    /// (CLI 2.1.316+).
836    #[serde(default, skip_serializing_if = "Option::is_none")]
837    pub render_ms: Option<u64>,
838    /// How many producers the passes started. Present, 0 included, whenever
839    /// a pass was kept (CLI 2.1.316+).
840    #[serde(default, skip_serializing_if = "Option::is_none")]
841    pub producers: Option<u64>,
842    /// How many times the passes asked the API to count a file's tokens
843    /// (only for text estimated above a quarter of the Read tool's token
844    /// limit). Present, 0 included, whenever a pass was kept
845    /// (CLI 2.1.316+).
846    #[serde(default, skip_serializing_if = "Option::is_none")]
847    pub token_counts: Option<u64>,
848    /// Time at least one token count was under way, overlaps counted once.
849    /// Absent when [`Self::token_counts`] is 0 (CLI 2.1.316+).
850    #[serde(default, skip_serializing_if = "Option::is_none")]
851    pub token_count_ms: Option<u64>,
852    /// Time at least one of the `changed_files` producer's Read calls was
853    /// under way, their token counts included. Absent when it made none
854    /// (CLI 2.1.316+).
855    #[serde(default, skip_serializing_if = "Option::is_none")]
856    pub changed_files_read_ms: Option<u64>,
857    /// The files counted under [`TouchCompareCounts::differs`], sorted by
858    /// size on disk, counting only the passes kept here (so the bands can
859    /// add up to less than `differs`). Absent when no compare in a kept pass
860    /// ended in `differs` (CLI 2.1.316+).
861    #[serde(default, skip_serializing_if = "Option::is_none")]
862    pub differs_files: Option<DiffersFilesCounts>,
863}
864
865/// [`InputAttachmentsDetail::differs_files`] — the `changed_files`
866/// compares that ended in `differs`, banded by file size on disk (1 KB is
867/// 1,024 bytes; a size on a boundary is in the band above it, except exactly
868/// 256 KB). A zero count is left out (CLI 2.1.316+).
869#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
870pub struct DiffersFilesCounts {
871    /// Under 25 KB.
872    #[serde(default, skip_serializing_if = "Option::is_none")]
873    pub size_under_25_kb: Option<u64>,
874    /// 25 KB up to 50 KB.
875    #[serde(default, skip_serializing_if = "Option::is_none")]
876    pub size_25_to_50_kb: Option<u64>,
877    /// 50 KB up to and including 256 KB.
878    #[serde(default, skip_serializing_if = "Option::is_none")]
879    pub size_50_to_256_kb: Option<u64>,
880    /// Over 256 KB (only where the Read size limit has been raised).
881    #[serde(default, skip_serializing_if = "Option::is_none")]
882    pub size_over_256_kb: Option<u64>,
883    /// Those files for which the conversation kept only a hash of the
884    /// earlier text, so the model, if told, gets the file's name without a
885    /// diff.
886    #[serde(default, skip_serializing_if = "Option::is_none")]
887    pub hash_only: Option<u64>,
888}
889
890/// [`InputAttachmentsDetail::touch_compare`] — how the `changed_files`
891/// producer's compares of touched files (size, then bytes, against what the
892/// conversation last saw) ended. Whole numbers; a zero count is left out
893/// (CLI 2.1.300+).
894#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
895pub struct TouchCompareCounts {
896    /// The text had not changed, so it was not read again.
897    #[serde(default, skip_serializing_if = "Option::is_none")]
898    pub same: Option<u64>,
899    /// The text had changed, so it was read again.
900    #[serde(default, skip_serializing_if = "Option::is_none")]
901    pub differs: Option<u64>,
902    /// Those of `same` and `differs` reached through the hosted container's
903    /// outputs link (`/mnt/user-data/outputs` → `/mnt/attach/outputs`).
904    #[serde(default, skip_serializing_if = "Option::is_none")]
905    pub via_outputs_link: Option<u64>,
906    /// Not compared, because the conversation has seen only part of the
907    /// file; read again.
908    #[serde(default, skip_serializing_if = "Option::is_none")]
909    pub partial_view: Option<u64>,
910    /// Not compared, because it was compared once already since the
911    /// conversation last read or wrote it; read again.
912    #[serde(default, skip_serializing_if = "Option::is_none")]
913    pub already_compared: Option<u64>,
914    /// The file could not be shown to be the one its path names (a symbolic
915    /// link on the way, an unjudged outputs-link landing, or the file swapped
916    /// or removed meanwhile); read again.
917    #[serde(default, skip_serializing_if = "Option::is_none")]
918    pub link_on_path: Option<u64>,
919    /// The open met a symbolic link; read again.
920    #[serde(default, skip_serializing_if = "Option::is_none")]
921    pub leaf_link: Option<u64>,
922    /// The file has a second hard link; read again.
923    #[serde(default, skip_serializing_if = "Option::is_none")]
924    pub nlink: Option<u64>,
925    /// Not a regular file; read again.
926    #[serde(default, skip_serializing_if = "Option::is_none")]
927    pub not_regular: Option<u64>,
928    /// Too large to compare; read again.
929    #[serde(default, skip_serializing_if = "Option::is_none")]
930    pub oversize: Option<u64>,
931    /// The file was gone.
932    #[serde(default, skip_serializing_if = "Option::is_none")]
933    pub enoent: Option<u64>,
934    /// Anything else, errors included; read again.
935    #[serde(default, skip_serializing_if = "Option::is_none")]
936    pub other: Option<u64>,
937}
938
939/// [`ResultMessage::resume_store_confirm_detail`] — how the
940/// `resume_store_confirm` phase was spent (CLI 2.1.290+). The four `_ms`
941/// values sum to that phase exactly.
942#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
943pub struct ResumeStoreConfirmDetail {
944    /// The transcript write, until the carried lines are in the queue of the
945    /// lane that uploads transcript rows.
946    pub write_ms: u64,
947    /// A POST was out that held only rows queued before the hand-off's write
948    /// began.
949    pub ahead_ms: u64,
950    /// Any other POST was out, each retry included.
951    pub post_ms: u64,
952    /// No POST was out (backoff, batching, a busy event loop), plus rounding
953    /// and any POST still out when the wait was cut short.
954    pub other_ms: u64,
955    /// POST attempts that ended between the start of the write and the end
956    /// of the wait.
957    pub posts: u64,
958    /// Those of [`Self::posts`] that did not go through.
959    pub failed_posts: u64,
960    /// Rows the lane had accepted and the store had not yet confirmed when
961    /// the write began.
962    pub rows_ahead: u64,
963    /// Rows queued since then that a POST delivered, the carried lines among
964    /// them.
965    pub rows: u64,
966}
967
968/// [`ResultMessage::tool_schema_build_detail`] — how the `tool_schema_build`
969/// phase was spent, in whole ms, in the order the code runs (CLI 2.1.317+).
970/// The six `_ms` values add up to the phase exactly; each is rounded so a
971/// fraction of a ms carries into the next.
972#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
973pub struct ToolSchemaBuildDetail {
974    /// Choosing the beta headers, reading what the model supports and the
975    /// thinking configuration.
976    pub betas_ms: u64,
977    /// Waiting for the account's Artifact capabilities, early schema builds
978    /// for plugin-movable tools, and deciding whether tool search is on.
979    pub waits_ms: u64,
980    /// Deciding which tools the request declares in full, deferred or by
981    /// reference, with the waits that takes.
982    pub placement_ms: u64,
983    /// Making the API schema of every declared tool.
984    pub render_ms: u64,
985    /// The rest of the phase, such as in-message tool schemas and applying
986    /// the plan for deferred tools.
987    pub after_ms: u64,
988    /// Time no part covers plus what rounding leaves: 0 or 1 unless some
989    /// stretch of the phase has no mark.
990    pub other_ms: u64,
991    /// How many tools the render made schemas for (the last build's).
992    #[serde(default, skip_serializing_if = "Option::is_none")]
993    pub tools: Option<u64>,
994    /// How much the process's tool-schema cache grew over the phase: about
995    /// how many schemas were built from scratch (the last build's).
996    #[serde(default, skip_serializing_if = "Option::is_none")]
997    pub new_schemas: Option<u64>,
998}
999
1000/// [`ResultMessage::message_normalization_detail`] — how the
1001/// `message_normalization` phase was spent (CLI 2.1.317+). Read it as
1002/// [`ToolSchemaBuildDetail`]: six `_ms` values in the order the code runs,
1003/// which add up to the phase exactly.
1004#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1005pub struct MessageNormalizationDetail {
1006    /// The passes ahead of the main one and the main pass's options.
1007    pub before_ms: u64,
1008    /// The main pass alone (`normalizeMessagesForAPI`).
1009    pub normalize_ms: u64,
1010    /// Checking every image against the model's size limit.
1011    pub images_ms: u64,
1012    /// The passes over the normalized list.
1013    pub post_process_ms: u64,
1014    /// The rest: per-turn notes, retry message lists, account-memory notes.
1015    pub after_ms: u64,
1016    /// As in [`ToolSchemaBuildDetail::other_ms`].
1017    pub other_ms: u64,
1018    /// How many messages the request was built from (the conversation since
1019    /// its last compaction, after any trimming); the last build's.
1020    #[serde(default, skip_serializing_if = "Option::is_none")]
1021    pub messages: Option<u64>,
1022    /// How many API messages this build made of them; the last build's.
1023    #[serde(default, skip_serializing_if = "Option::is_none")]
1024    pub wire_messages: Option<u64>,
1025}
1026
1027/// [`ResultMessage::request_build_detail`] — how the `request_build` phase
1028/// was spent (CLI 2.1.317+). The nine values from `records_ms` to
1029/// `other_ms` add up to the phase exactly; the three `sent_build_*` members
1030/// lie within [`sent_build_ms`](Self::sent_build_ms) and are not part of
1031/// the sum.
1032#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1033pub struct RequestBuildDetail {
1034    /// Recording what this request sends and setting up the shared retry
1035    /// handlers.
1036    pub records_ms: u64,
1037    /// The fallback-block check, attribution header, cache lifetime and the
1038    /// system prompt's blocks.
1039    pub system_ms: u64,
1040    /// Tool/system-prompt change detection, effort, fast mode, permission
1041    /// context waits and beta headers.
1042    pub headers_ms: u64,
1043    /// Recording the prompt's state so a later prompt-cache break can be
1044    /// explained. In a cloud runner's session the on-disk write is queued
1045    /// after the response instead, outside this phase, unless the model is
1046    /// one that cache-break detection skips (CLI 2.1.318+).
1047    pub cache_break_record_ms: u64,
1048    /// Defining the first retry handlers, planning tool-result clearing and
1049    /// opening the request's tracing span.
1050    pub plans_ms: u64,
1051    /// Defining the rest of the retry and fallback handlers.
1052    pub retry_setup_ms: u64,
1053    /// Building the parameters that are sent, for every attempt before the
1054    /// first dispatch.
1055    pub sent_build_ms: u64,
1056    /// From there to the moment the request is recorded as sent.
1057    pub record_attempt_ms: u64,
1058    /// As in [`ToolSchemaBuildDetail::other_ms`].
1059    pub other_ms: u64,
1060    /// Assembling the context for the server-side safety check.
1061    #[serde(default, skip_serializing_if = "Option::is_none")]
1062    pub sent_build_safety_context_ms: Option<u64>,
1063    /// The last clean-up of the messages before sending, placing the
1064    /// prompt-cache breakpoints.
1065    #[serde(default, skip_serializing_if = "Option::is_none")]
1066    pub sent_build_cache_breakpoints_ms: Option<u64>,
1067    /// Scanning the body for lone UTF-16 surrogates, and the scrub where one
1068    /// is found.
1069    #[serde(default, skip_serializing_if = "Option::is_none")]
1070    pub sent_build_surrogate_scan_ms: Option<u64>,
1071}
1072
1073/// [`ResultMessage::process_turn_states`] — the turn's process CPU and wall
1074/// time split among mutually exclusive states (CLI 2.1.317+). A state says
1075/// what was open, not what the time was spent on. Where several were open,
1076/// the first of `main_stream`, `side_stream`, `tool`, `agent_tool`,
1077/// `first_event_wait` names the state; with nothing open, the state is
1078/// named after the turn's main-conversation model calls.
1079#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1080pub struct ProcessTurnStates {
1081    /// A response stream of a main-conversation request was open.
1082    #[serde(default, skip_serializing_if = "Option::is_none")]
1083    pub main_stream: Option<ProcessStateTime>,
1084    /// Any other model request's stream was open (a subagent's, a
1085    /// compaction's).
1086    #[serde(default, skip_serializing_if = "Option::is_none")]
1087    pub side_stream: Option<ProcessStateTime>,
1088    /// A call of any tool but Agent was open, permission prompt included.
1089    #[serde(default, skip_serializing_if = "Option::is_none")]
1090    pub tool: Option<ProcessStateTime>,
1091    /// A call of the Agent tool was open.
1092    #[serde(default, skip_serializing_if = "Option::is_none")]
1093    pub agent_tool: Option<ProcessStateTime>,
1094    /// A main-conversation request was sent and no stream of it was open
1095    /// yet.
1096    #[serde(default, skip_serializing_if = "Option::is_none")]
1097    pub first_event_wait: Option<ProcessStateTime>,
1098    /// Nothing open, and no main-conversation model call sent yet.
1099    #[serde(default, skip_serializing_if = "Option::is_none")]
1100    pub before_first_request: Option<ProcessStateTime>,
1101    /// Nothing open, between two main-conversation model calls.
1102    #[serde(default, skip_serializing_if = "Option::is_none")]
1103    pub between_requests: Option<ProcessStateTime>,
1104    /// Nothing open, after the last main-conversation model call.
1105    #[serde(default, skip_serializing_if = "Option::is_none")]
1106    pub after_last_request: Option<ProcessStateTime>,
1107}
1108
1109/// Time in one [`ProcessTurnStates`] state (CLI 2.1.317+).
1110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1111pub struct ProcessStateTime {
1112    /// Process CPU time, counted as in `process_cpu_ms_between_turns`
1113    /// (child processes left out).
1114    pub cpu_ms: u64,
1115    /// Wall-clock time.
1116    pub wall_ms: u64,
1117    /// How many times the state was entered.
1118    pub entries: u64,
1119}
1120
1121/// [`ResultMessage::process_garbage_collections`] — garbage collections on
1122/// the CLI process's main thread (CLI 2.1.311+). A worker claimed from a
1123/// warm spare counts from the spare's own start.
1124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1125pub struct ProcessGarbageCollections {
1126    /// Up to the start of the process's first turn. Absent if no turn began
1127    /// first or its count failed.
1128    #[serde(default, skip_serializing_if = "Option::is_none")]
1129    pub to_first_message: Option<GarbageCollectionCounts>,
1130    /// Up to this result, including
1131    /// [`to_first_message`](Self::to_first_message)'s.
1132    pub to_first_result: GarbageCollectionCounts,
1133}
1134
1135/// One window of [`ProcessGarbageCollections`] (CLI 2.1.311+). A collection
1136/// still running at the window's end is not counted.
1137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1138pub struct GarbageCollectionCounts {
1139    /// Collections of young objects only (under Bun the process's first also
1140    /// marks the whole heap).
1141    pub eden: u64,
1142    /// Collections of the whole heap.
1143    pub full: u64,
1144    /// Their summed wall-clock time in ms, not CPU time.
1145    pub wall_ms: u64,
1146    /// Heap in use after the last one, in MiB. Absent if none ran.
1147    #[serde(default, skip_serializing_if = "Option::is_none")]
1148    pub heap_after_mib: Option<u64>,
1149}
1150
1151/// Why Claude Code refused to start, carried as
1152/// [`ResultMessage::startup_failure_reason`] so a host can offer the fix
1153/// instead of a retry (CLI 2.1.274+).
1154#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1155pub enum StartupFailureReason {
1156    /// Managed settings pin a first-party or Cloud gateway sign-in, and an
1157    /// Anthropic API key or auth token is configured instead.
1158    OrgPinApiKeyConflict,
1159    /// Managed settings list the API providers this machine may use
1160    /// (`allowedProviders`), and the session is set up for one that is not
1161    /// listed (CLI 2.1.285+).
1162    ProviderNotAllowed,
1163    /// The sign-in's organization could not be verified against the pin
1164    /// (network, or a revoked token).
1165    OrgVerifyFailed,
1166    /// The sign-in belongs to an organization the pin does not allow.
1167    OrgPinMismatch,
1168    /// Managed policy settings could not be read, the pin names no
1169    /// organization, or managed model settings (`deniedModels`, or an
1170    /// `availableModels` list matched exactly) block the default model and
1171    /// leave no allowed model to use instead.
1172    ManagedSettingsInvalid,
1173    /// Managed settings the organization requires could not be loaded.
1174    RemoteSettingsRequiredUnavailable,
1175    /// The Cloud gateway ended this sign-in.
1176    GatewaySigninRequired,
1177    /// The Cloud gateway refused managed settings for this account.
1178    GatewayAccessDenied,
1179    /// A proxy setting is not a complete URL.
1180    ProxyInvalid,
1181    /// The per-user temp directory is unsafe or could not be created.
1182    TempDirUnusable,
1183    /// The working directory was deleted, moved or cannot be read.
1184    CwdUnavailable,
1185    /// Windows has no shell tool: Git Bash is missing, and PowerShell is
1186    /// missing or turned off by `CLAUDE_CODE_USE_POWERSHELL_TOOL`.
1187    ShellToolMissing,
1188    /// The conversation to resume or continue is running as a background
1189    /// session.
1190    SessionHeldByBackground,
1191    /// The resume was refused because the session's worktree failed its
1192    /// safety checks or the resume was launched from inside it; `errors`
1193    /// says whether a re-run continues without the worktree.
1194    WorktreeResumeRefused,
1195    /// The session's worktree could not be verified right now; retrying may
1196    /// succeed.
1197    WorktreeUnverified,
1198    /// This Claude Code version is below the minimum Anthropic requires.
1199    CliVersionTooOld,
1200    /// Bypass permissions mode was requested while running as root.
1201    BypassRoot,
1202    /// The organization requires its policy limits and managed settings
1203    /// before a session starts, and they could not be loaded (network,
1204    /// proxy, or an Anthropic error); retrying may succeed (CLI 2.1.293+).
1205    OrgConfigRequiredUnavailable,
1206    /// The organization requires its policy limits and managed settings, and
1207    /// Anthropic refused them for this sign-in (expired or revoked session,
1208    /// or the organization does not allow Claude Code for this account);
1209    /// signing in again or an admin's change is the fix, not a retry
1210    /// (CLI 2.1.293+).
1211    OrgConfigRefused,
1212    /// A working sandbox is required and it cannot run in this session, for
1213    /// example because bubblewrap is missing or the platform is not
1214    /// supported; make the sandbox available or have the requirement
1215    /// changed (CLI 2.1.314+).
1216    SandboxRequiredUnavailable,
1217    /// `initialize_not_first` — Claude Code was started with
1218    /// `--await-initialize`, and its first stdin line was not the
1219    /// `initialize` control request (another message, or not valid JSON).
1220    /// The fix is in the host or a wrapper script between the host and
1221    /// Claude Code, not a retry (CLI 2.1.317+).
1222    InitializeNotFirst,
1223    /// `config_file_invalid` — a configuration file read at start-up is not
1224    /// valid JSON; `errors` names the file and, for the global configuration
1225    /// file, how to restore or repair it. The file is left unchanged, and
1226    /// every start without a terminal is refused until it is repaired, so a
1227    /// retry alone does not help (CLI 2.1.318+).
1228    ConfigFileInvalid,
1229    /// A cause not yet known to this version of the crate.
1230    Unknown(String),
1231}
1232
1233impl StartupFailureReason {
1234    pub fn as_str(&self) -> &str {
1235        match self {
1236            Self::OrgPinApiKeyConflict => "org_pin_api_key_conflict",
1237            Self::ProviderNotAllowed => "provider_not_allowed",
1238            Self::OrgVerifyFailed => "org_verify_failed",
1239            Self::OrgPinMismatch => "org_pin_mismatch",
1240            Self::ManagedSettingsInvalid => "managed_settings_invalid",
1241            Self::RemoteSettingsRequiredUnavailable => "remote_settings_required_unavailable",
1242            Self::GatewaySigninRequired => "gateway_signin_required",
1243            Self::GatewayAccessDenied => "gateway_access_denied",
1244            Self::ProxyInvalid => "proxy_invalid",
1245            Self::TempDirUnusable => "temp_dir_unusable",
1246            Self::CwdUnavailable => "cwd_unavailable",
1247            Self::ShellToolMissing => "shell_tool_missing",
1248            Self::SessionHeldByBackground => "session_held_by_background",
1249            Self::WorktreeResumeRefused => "worktree_resume_refused",
1250            Self::WorktreeUnverified => "worktree_unverified",
1251            Self::CliVersionTooOld => "cli_version_too_old",
1252            Self::BypassRoot => "bypass_root",
1253            Self::OrgConfigRequiredUnavailable => "org_config_required_unavailable",
1254            Self::OrgConfigRefused => "org_config_refused",
1255            Self::SandboxRequiredUnavailable => "sandbox_required_unavailable",
1256            Self::InitializeNotFirst => "initialize_not_first",
1257            Self::ConfigFileInvalid => "config_file_invalid",
1258            Self::Unknown(s) => s.as_str(),
1259        }
1260    }
1261}
1262
1263impl fmt::Display for StartupFailureReason {
1264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1265        f.write_str(self.as_str())
1266    }
1267}
1268
1269impl From<&str> for StartupFailureReason {
1270    fn from(s: &str) -> Self {
1271        match s {
1272            "org_pin_api_key_conflict" => Self::OrgPinApiKeyConflict,
1273            "provider_not_allowed" => Self::ProviderNotAllowed,
1274            "org_verify_failed" => Self::OrgVerifyFailed,
1275            "org_pin_mismatch" => Self::OrgPinMismatch,
1276            "managed_settings_invalid" => Self::ManagedSettingsInvalid,
1277            "remote_settings_required_unavailable" => Self::RemoteSettingsRequiredUnavailable,
1278            "gateway_signin_required" => Self::GatewaySigninRequired,
1279            "gateway_access_denied" => Self::GatewayAccessDenied,
1280            "proxy_invalid" => Self::ProxyInvalid,
1281            "temp_dir_unusable" => Self::TempDirUnusable,
1282            "cwd_unavailable" => Self::CwdUnavailable,
1283            "shell_tool_missing" => Self::ShellToolMissing,
1284            "session_held_by_background" => Self::SessionHeldByBackground,
1285            "worktree_resume_refused" => Self::WorktreeResumeRefused,
1286            "worktree_unverified" => Self::WorktreeUnverified,
1287            "cli_version_too_old" => Self::CliVersionTooOld,
1288            "bypass_root" => Self::BypassRoot,
1289            "org_config_required_unavailable" => Self::OrgConfigRequiredUnavailable,
1290            "org_config_refused" => Self::OrgConfigRefused,
1291            "sandbox_required_unavailable" => Self::SandboxRequiredUnavailable,
1292            "initialize_not_first" => Self::InitializeNotFirst,
1293            "config_file_invalid" => Self::ConfigFileInvalid,
1294            other => Self::Unknown(other.to_string()),
1295        }
1296    }
1297}
1298
1299impl Serialize for StartupFailureReason {
1300    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1301        serializer.serialize_str(self.as_str())
1302    }
1303}
1304
1305impl<'de> Deserialize<'de> for StartupFailureReason {
1306    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1307        let s = String::deserialize(deserializer)?;
1308        Ok(Self::from(s.as_str()))
1309    }
1310}
1311
1312/// How the self-hosted runner's session process terminated, carried on a
1313/// synthesized failure [`ResultMessage`] as [`ResultMessage::runner_exit`]
1314/// (CLI 2.1.266+).
1315#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1316pub struct RunnerExit {
1317    /// Which phase the process was in when it exited.
1318    pub phase: RunnerExitPhase,
1319    /// Process exit code. Absent (or wire `null`) when it exited on a signal
1320    /// instead, or when the runner did not capture one.
1321    #[serde(default, skip_serializing_if = "Option::is_none")]
1322    pub exit_code: Option<i64>,
1323    /// Terminating signal name. Absent (or wire `null`) when it exited with a
1324    /// code instead, or when the runner did not capture one.
1325    #[serde(default, skip_serializing_if = "Option::is_none")]
1326    pub signal: Option<String>,
1327}
1328
1329/// Which phase the runner's session process was in when it exited, in
1330/// [`RunnerExit::phase`]. Open set — unrecognized values deserialize to
1331/// [`RunnerExitPhase::Unknown`].
1332#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1333pub enum RunnerExitPhase {
1334    /// Failed while starting up.
1335    Setup,
1336    /// Failed once running.
1337    Run,
1338    /// A phase not yet known to this version of the crate.
1339    Unknown(String),
1340}
1341
1342impl RunnerExitPhase {
1343    pub fn as_str(&self) -> &str {
1344        match self {
1345            Self::Setup => "setup",
1346            Self::Run => "run",
1347            Self::Unknown(s) => s.as_str(),
1348        }
1349    }
1350}
1351
1352impl std::fmt::Display for RunnerExitPhase {
1353    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1354        f.write_str(self.as_str())
1355    }
1356}
1357
1358impl From<&str> for RunnerExitPhase {
1359    fn from(s: &str) -> Self {
1360        match s {
1361            "setup" => Self::Setup,
1362            "run" => Self::Run,
1363            other => Self::Unknown(other.to_string()),
1364        }
1365    }
1366}
1367
1368impl Serialize for RunnerExitPhase {
1369    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1370        serializer.serialize_str(self.as_str())
1371    }
1372}
1373
1374impl<'de> Deserialize<'de> for RunnerExitPhase {
1375    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1376        let s = String::deserialize(deserializer)?;
1377        Ok(Self::from(s.as_str()))
1378    }
1379}
1380
1381/// Usage and cost for a single model within a session, as found in
1382/// [`ResultMessage::model_usage`] and
1383/// [`ResultMessage::model_usage_since_last_result`].
1384///
1385/// The `extra` field captures any keys the CLI adds that aren't modeled here,
1386/// so new wire fields deserialize without error.
1387#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1388#[serde(rename_all = "camelCase")]
1389pub struct ModelUsageEntry {
1390    #[serde(default)]
1391    pub input_tokens: u64,
1392    #[serde(default)]
1393    pub output_tokens: u64,
1394    #[serde(default)]
1395    pub cache_read_input_tokens: u64,
1396    #[serde(default)]
1397    pub cache_creation_input_tokens: u64,
1398    #[serde(default, rename = "costUSD")]
1399    pub cost_usd: f64,
1400    #[serde(default)]
1401    pub web_search_requests: u32,
1402    #[serde(default)]
1403    pub context_window: u64,
1404    #[serde(default)]
1405    pub max_output_tokens: u64,
1406    #[serde(flatten)]
1407    pub extra: serde_json::Map<String, serde_json::Value>,
1408}
1409
1410/// Tool use deferred by a terminal result.
1411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1412pub struct DeferredToolUse {
1413    pub id: String,
1414    pub name: String,
1415    pub input: Value,
1416}
1417
1418/// Running totals for subagents started through the Agent tool, carried on
1419/// `result` frames (CLI 2.1.239+) as [`ResultMessage::subagent_stats`].
1420///
1421/// Cumulative for the session: a resumed session starts fresh, and a
1422/// mid-session `/clear` zeroes it — though a background subagent that outlives
1423/// the `/clear` still records its outcome, so `completed`, `failed`, and
1424/// `killed` can then exceed `spawned`. Forked skills, workflows, teammates,
1425/// and other internal agents are not counted.
1426#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1427pub struct SubagentStats {
1428    /// Subagents actually started; a refused or failed launch is not counted.
1429    pub spawned: u64,
1430    /// Spawns by the `run_in_background` value the model passed; all count as
1431    /// `unset` while the parameter is not offered (background tasks disabled,
1432    /// or subagents forced into the background: in a terminal, the desktop
1433    /// app or the VS Code extension, or with `CLAUDE_CODE_FORK_SUBAGENT=1`).
1434    pub requested: SubagentSpawnRequests,
1435    /// Spawns that started in the background after defaults and session
1436    /// settings; the rest blocked the spawning tool call.
1437    pub started_in_background: u64,
1438    /// Spawns by agent type.
1439    #[serde(default)]
1440    pub by_type: std::collections::BTreeMap<String, u64>,
1441    /// Deepest spawn: 1 = started by the main thread, 2 = by a depth-1
1442    /// subagent.
1443    pub max_depth: u64,
1444    /// Spawns made from inside another subagent (depth > 1).
1445    pub spawned_by_subagents: u64,
1446    pub completed: u64,
1447    pub failed: u64,
1448    /// Subagents stopped before finishing, by who stopped them.
1449    pub killed: SubagentKillCounts,
1450    /// Agent tool calls turned down because a limit was reached (other
1451    /// denials, such as an unknown agent type, are not counted).
1452    pub refused: SubagentRefusalCounts,
1453}
1454
1455/// Spawn counts by the `run_in_background` value the model passed, carried in
1456/// [`SubagentStats::requested`].
1457#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1458pub struct SubagentSpawnRequests {
1459    pub background: u64,
1460    pub foreground: u64,
1461    pub unset: u64,
1462}
1463
1464/// Subagents stopped before finishing, carried in [`SubagentStats::killed`].
1465/// `parent` = by another agent through TaskStop; `system` = by Claude Code
1466/// itself; `user` = any other stop.
1467#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1468pub struct SubagentKillCounts {
1469    pub parent: u64,
1470    pub user: u64,
1471    pub system: u64,
1472}
1473
1474/// Agent tool calls refused at a limit, carried in [`SubagentStats::refused`].
1475#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1476pub struct SubagentRefusalCounts {
1477    pub depth_limit: u64,
1478    pub concurrency_limit: u64,
1479    pub budget: u64,
1480}
1481
1482/// A record of a tool permission that was denied during the session.
1483///
1484/// This is included in `ResultMessage.permission_denials` to provide a summary
1485/// of all permission denials that occurred.
1486#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1487pub struct PermissionDenial {
1488    /// The name of the tool that was blocked (e.g., "Bash", "Write")
1489    pub tool_name: String,
1490
1491    /// The input that was passed to the tool
1492    pub tool_input: Value,
1493
1494    /// The unique identifier for this tool use request
1495    pub tool_use_id: String,
1496}
1497
1498/// What the first stream POST of a turn was queued behind, carried as
1499/// [`ResultMessage::first_stream_post_queued_behind`] (CLI 2.1.278+).
1500/// Internal timing instrumentation; the CLI does not describe the values.
1501#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1502pub enum StreamPostQueuedBehind {
1503    /// `durable_post`
1504    DurablePost,
1505    /// `ephemeral_post`
1506    EphemeralPost,
1507    /// `retry_backoff`
1508    RetryBackoff,
1509    /// `hold`
1510    Hold,
1511    /// `none`
1512    None,
1513    /// A value not yet known to this version of the crate.
1514    Unknown(String),
1515}
1516
1517impl StreamPostQueuedBehind {
1518    pub fn as_str(&self) -> &str {
1519        match self {
1520            Self::DurablePost => "durable_post",
1521            Self::EphemeralPost => "ephemeral_post",
1522            Self::RetryBackoff => "retry_backoff",
1523            Self::Hold => "hold",
1524            Self::None => "none",
1525            Self::Unknown(s) => s.as_str(),
1526        }
1527    }
1528}
1529
1530impl fmt::Display for StreamPostQueuedBehind {
1531    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1532        f.write_str(self.as_str())
1533    }
1534}
1535
1536impl From<&str> for StreamPostQueuedBehind {
1537    fn from(s: &str) -> Self {
1538        match s {
1539            "durable_post" => Self::DurablePost,
1540            "ephemeral_post" => Self::EphemeralPost,
1541            "retry_backoff" => Self::RetryBackoff,
1542            "hold" => Self::Hold,
1543            "none" => Self::None,
1544            other => Self::Unknown(other.to_string()),
1545        }
1546    }
1547}
1548
1549impl Serialize for StreamPostQueuedBehind {
1550    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1551        serializer.serialize_str(self.as_str())
1552    }
1553}
1554
1555impl<'de> Deserialize<'de> for StreamPostQueuedBehind {
1556    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1557        let s = String::deserialize(deserializer)?;
1558        Ok(Self::from(s.as_str()))
1559    }
1560}
1561
1562/// What a turn was given to settle before its own prompt, carried as
1563/// [`ResultMessage::turn_start_resume_kind`] (CLI 2.1.287+). A turn given
1564/// more than one reports the first in this order.
1565#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1566pub enum TurnStartResumeKind {
1567    /// `orphaned_permission` — a permission answer whose pending request was
1568    /// lost (as when a worker restarts), so the tool call is settled with
1569    /// that answer.
1570    OrphanedPermission,
1571    /// `turn_handoff` — a turn in progress that the session's client handed
1572    /// over.
1573    TurnHandoff,
1574    /// `deferred_tool_use` — a call a `PreToolUse` hook deferred.
1575    DeferredToolUse,
1576    /// `none` — nothing to settle (`resume_other` is still reported, at
1577    /// about 0).
1578    None,
1579    /// A value not yet known to this version of the crate.
1580    Unknown(String),
1581}
1582
1583impl TurnStartResumeKind {
1584    pub fn as_str(&self) -> &str {
1585        match self {
1586            Self::OrphanedPermission => "orphaned_permission",
1587            Self::TurnHandoff => "turn_handoff",
1588            Self::DeferredToolUse => "deferred_tool_use",
1589            Self::None => "none",
1590            Self::Unknown(s) => s.as_str(),
1591        }
1592    }
1593}
1594
1595impl fmt::Display for TurnStartResumeKind {
1596    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1597        f.write_str(self.as_str())
1598    }
1599}
1600
1601impl From<&str> for TurnStartResumeKind {
1602    fn from(s: &str) -> Self {
1603        match s {
1604            "orphaned_permission" => Self::OrphanedPermission,
1605            "turn_handoff" => Self::TurnHandoff,
1606            "deferred_tool_use" => Self::DeferredToolUse,
1607            "none" => Self::None,
1608            other => Self::Unknown(other.to_string()),
1609        }
1610    }
1611}
1612
1613impl Serialize for TurnStartResumeKind {
1614    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1615        serializer.serialize_str(self.as_str())
1616    }
1617}
1618
1619impl<'de> Deserialize<'de> for TurnStartResumeKind {
1620    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1621        let s = String::deserialize(deserializer)?;
1622        Ok(Self::from(s.as_str()))
1623    }
1624}
1625
1626/// Why fast mode can't serve right now, carried on `result` frames and
1627/// `system/init` (CLI 2.1.219+). Absent when nothing blocks fast mode.
1628#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1629pub enum FastModeDisabledReason {
1630    /// Free-tier account.
1631    Free,
1632    /// Disabled by user preference.
1633    Preference,
1634    /// Extra-usage purchases are disabled for the account.
1635    ExtraUsageDisabled,
1636    /// A network error prevented fast-mode eligibility from resolving.
1637    NetworkError,
1638    /// The CLI could not determine the reason.
1639    UnknownReason,
1640    /// Not a first-party API session (e.g. Bedrock/Vertex).
1641    NotFirstParty,
1642    /// Disabled via environment variable.
1643    DisabledByEnv,
1644    /// The active model does not support fast mode.
1645    ModelNotAllowed,
1646    /// SDK sessions must opt in to fast mode.
1647    SdkOptInRequired,
1648    /// Eligibility is still being determined.
1649    Pending,
1650    /// A reason not yet known to this version of the crate.
1651    Unknown(String),
1652}
1653
1654impl FastModeDisabledReason {
1655    pub fn as_str(&self) -> &str {
1656        match self {
1657            Self::Free => "free",
1658            Self::Preference => "preference",
1659            Self::ExtraUsageDisabled => "extra_usage_disabled",
1660            Self::NetworkError => "network_error",
1661            Self::UnknownReason => "unknown",
1662            Self::NotFirstParty => "not_first_party",
1663            Self::DisabledByEnv => "disabled_by_env",
1664            Self::ModelNotAllowed => "model_not_allowed",
1665            Self::SdkOptInRequired => "sdk_opt_in_required",
1666            Self::Pending => "pending",
1667            Self::Unknown(s) => s.as_str(),
1668        }
1669    }
1670}
1671
1672impl fmt::Display for FastModeDisabledReason {
1673    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1674        f.write_str(self.as_str())
1675    }
1676}
1677
1678impl From<&str> for FastModeDisabledReason {
1679    fn from(s: &str) -> Self {
1680        match s {
1681            "free" => Self::Free,
1682            "preference" => Self::Preference,
1683            "extra_usage_disabled" => Self::ExtraUsageDisabled,
1684            "network_error" => Self::NetworkError,
1685            "unknown" => Self::UnknownReason,
1686            "not_first_party" => Self::NotFirstParty,
1687            "disabled_by_env" => Self::DisabledByEnv,
1688            "model_not_allowed" => Self::ModelNotAllowed,
1689            "sdk_opt_in_required" => Self::SdkOptInRequired,
1690            "pending" => Self::Pending,
1691            other => Self::Unknown(other.to_string()),
1692        }
1693    }
1694}
1695
1696impl Serialize for FastModeDisabledReason {
1697    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1698        serializer.serialize_str(self.as_str())
1699    }
1700}
1701
1702impl<'de> Deserialize<'de> for FastModeDisabledReason {
1703    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1704        let s = String::deserialize(deserializer)?;
1705        Ok(Self::from(s.as_str()))
1706    }
1707}
1708
1709/// Result subtypes
1710#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1711pub enum ResultSubtype {
1712    Success,
1713    ErrorMaxTurns,
1714    ErrorDuringExecution,
1715    ErrorMaxBudgetUsd,
1716    ErrorMaxStructuredOutputRetries,
1717    Unknown(String),
1718}
1719
1720impl ResultSubtype {
1721    pub fn as_str(&self) -> &str {
1722        match self {
1723            Self::Success => "success",
1724            Self::ErrorMaxTurns => "error_max_turns",
1725            Self::ErrorDuringExecution => "error_during_execution",
1726            Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
1727            Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
1728            Self::Unknown(s) => s.as_str(),
1729        }
1730    }
1731}
1732
1733impl fmt::Display for ResultSubtype {
1734    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1735        f.write_str(self.as_str())
1736    }
1737}
1738
1739impl From<&str> for ResultSubtype {
1740    fn from(s: &str) -> Self {
1741        match s {
1742            "success" => Self::Success,
1743            "error_max_turns" => Self::ErrorMaxTurns,
1744            "error_during_execution" => Self::ErrorDuringExecution,
1745            "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
1746            "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
1747            other => Self::Unknown(other.to_string()),
1748        }
1749    }
1750}
1751
1752impl Serialize for ResultSubtype {
1753    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1754        serializer.serialize_str(self.as_str())
1755    }
1756}
1757
1758impl<'de> Deserialize<'de> for ResultSubtype {
1759    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1760        let s = String::deserialize(deserializer)?;
1761        Ok(Self::from(s.as_str()))
1762    }
1763}
1764
1765/// Usage information for the request.
1766///
1767/// **These counters are accumulated roll-ups, not snapshots.** The CLI sums
1768/// usage field-by-field across every API call ("iteration") in the turn's
1769/// tool-use loop, so on a turn with N iterations each counter is the sum of
1770/// all N. This is the right number for **cost**, and the wrong number for
1771/// **context occupancy**: `cache_read_input_tokens` in particular re-counts
1772/// the cached context on every iteration and can exceed the model's context
1773/// window several times over on tool-heavy turns. To estimate context
1774/// occupancy, use the *last* entry of [`iterations`](Self::iterations)
1775/// instead (the CLI itself does exactly this).
1776///
1777/// Note: the `result` frame's usage covers the **main agent only** — the
1778/// subagent (`Task` / sidechain) token rollup the CLI renders as
1779/// `<subagent_tokens>` / `<agent_count>` is not carried here or anywhere
1780/// else on the wire. Accumulate it from `Task` tool results with
1781/// [`SubagentUsageRollup`](crate::SubagentUsageRollup).
1782#[derive(Debug, Clone, Serialize, Deserialize)]
1783pub struct UsageInfo {
1784    /// Sum of fresh (uncached) input tokens across all iterations of the turn.
1785    #[serde(default)]
1786    pub input_tokens: u32,
1787    /// Sum of cache-write input tokens across all iterations of the turn.
1788    #[serde(default)]
1789    pub cache_creation_input_tokens: u32,
1790    /// Sum of cache-read input tokens across all iterations of the turn.
1791    /// Re-counts the cached context every iteration — **not** a measure of
1792    /// context occupancy; see the type-level docs.
1793    #[serde(default)]
1794    pub cache_read_input_tokens: u32,
1795    /// Sum of output tokens across all iterations of the turn.
1796    #[serde(default)]
1797    pub output_tokens: u32,
1798    #[serde(default)]
1799    pub server_tool_use: ServerToolUse,
1800    #[serde(default)]
1801    pub service_tier: String,
1802
1803    /// Cache creation breakdown
1804    #[serde(skip_serializing_if = "Option::is_none")]
1805    pub cache_creation: Option<super::message_types::CacheCreationDetails>,
1806
1807    /// Inference geography (e.g., "not_available")
1808    #[serde(skip_serializing_if = "Option::is_none")]
1809    pub inference_geo: Option<String>,
1810
1811    /// Per-iteration usage breakdown **within** this turn — one entry per
1812    /// API call in the tool-use loop, in order. The last entry reflects the
1813    /// final API call and is what the CLI reads for context-size estimates.
1814    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1815    pub iterations: Vec<UsageIteration>,
1816
1817    /// Speed tier (e.g., "standard")
1818    #[serde(skip_serializing_if = "Option::is_none")]
1819    pub speed: Option<String>,
1820
1821    /// Output-token breakdown (CLI 2.1.232+): currently the thinking-token
1822    /// share of `output_tokens`.
1823    #[serde(skip_serializing_if = "Option::is_none")]
1824    pub output_tokens_details: Option<OutputTokensDetails>,
1825
1826    /// Outcome of a fallback-credit token the request presented; `null` on
1827    /// the wire when it presented none (CLI 2.1.285+).
1828    #[serde(default, skip_serializing_if = "Option::is_none")]
1829    pub fallback_credit: Option<Value>,
1830}
1831
1832/// Breakdown of a turn's output tokens, carried in
1833/// [`UsageInfo::output_tokens_details`].
1834#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1835pub struct OutputTokensDetails {
1836    /// Output tokens spent on extended thinking.
1837    #[serde(skip_serializing_if = "Option::is_none")]
1838    pub thinking_tokens: Option<u64>,
1839}
1840
1841/// Usage for a single API call ("iteration") within a turn's tool-use loop.
1842///
1843/// Carried in [`UsageInfo::iterations`]. The cache fields are optional on
1844/// the wire: some frames carry only `input_tokens` + `output_tokens`
1845/// (observed with `type: "turn"`), while others carry the full cache
1846/// breakdown (observed with `type: "message"` in captured subagent
1847/// sessions). The CLI computes its context estimate from
1848/// `input_tokens + output_tokens` of the **last** iteration.
1849#[derive(Debug, Clone, Serialize, Deserialize)]
1850pub struct UsageIteration {
1851    #[serde(default)]
1852    pub input_tokens: u32,
1853    #[serde(default)]
1854    pub output_tokens: u32,
1855    /// Cache-read input tokens for this iteration, when carried.
1856    #[serde(default, skip_serializing_if = "Option::is_none")]
1857    pub cache_read_input_tokens: Option<u32>,
1858    /// Cache-write input tokens for this iteration, when carried.
1859    #[serde(default, skip_serializing_if = "Option::is_none")]
1860    pub cache_creation_input_tokens: Option<u32>,
1861    /// Cache-write breakdown by TTL, when carried.
1862    #[serde(default, skip_serializing_if = "Option::is_none")]
1863    pub cache_creation: Option<super::message_types::CacheCreationDetails>,
1864    /// Iteration kind; `"turn"` and `"message"` observed on the wire.
1865    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
1866    pub kind: Option<String>,
1867}
1868
1869/// Server tool usage information
1870#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1871pub struct ServerToolUse {
1872    #[serde(default)]
1873    pub web_search_requests: u32,
1874    /// Number of web fetch requests made
1875    #[serde(default)]
1876    pub web_fetch_requests: u32,
1877}
1878
1879#[cfg(test)]
1880mod tests {
1881    use super::*;
1882    use crate::io::ClaudeOutput;
1883
1884    #[test]
1885    fn test_deserialize_result_message() {
1886        let json = r#"{
1887            "type": "result",
1888            "subtype": "success",
1889            "is_error": false,
1890            "duration_ms": 100,
1891            "duration_api_ms": 200,
1892            "num_turns": 1,
1893            "result": "Done",
1894            "session_id": "123",
1895            "total_cost_usd": 0.01,
1896            "permission_denials": []
1897        }"#;
1898
1899        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1900        assert!(!output.is_error());
1901    }
1902
1903    #[test]
1904    fn test_result_subtype_new_and_unknown_values_do_not_fail() {
1905        let json = r#"{
1906            "type": "result",
1907            "subtype": "error_max_budget_usd",
1908            "is_error": true,
1909            "duration_ms": 100,
1910            "duration_api_ms": 200,
1911            "num_turns": 1,
1912            "session_id": "123",
1913            "total_cost_usd": 0.01
1914        }"#;
1915
1916        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1917        let ClaudeOutput::Result(result) = output else {
1918            panic!("Expected Result");
1919        };
1920        assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
1921
1922        let json = r#"{
1923            "type": "result",
1924            "subtype": "future_result_subtype",
1925            "is_error": true,
1926            "duration_ms": 100,
1927            "duration_api_ms": 200,
1928            "num_turns": 1,
1929            "session_id": "123",
1930            "total_cost_usd": 0.01
1931        }"#;
1932
1933        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1934        let ClaudeOutput::Result(result) = output else {
1935            panic!("Expected Result");
1936        };
1937        assert_eq!(
1938            result.subtype,
1939            ResultSubtype::Unknown("future_result_subtype".to_string())
1940        );
1941    }
1942
1943    #[test]
1944    fn test_deserialize_result_with_permission_denials() {
1945        let json = r#"{
1946            "type": "result",
1947            "subtype": "success",
1948            "is_error": false,
1949            "duration_ms": 100,
1950            "duration_api_ms": 200,
1951            "num_turns": 2,
1952            "result": "Done",
1953            "session_id": "123",
1954            "total_cost_usd": 0.01,
1955            "permission_denials": [
1956                {
1957                    "tool_name": "Bash",
1958                    "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1959                    "tool_use_id": "toolu_123"
1960                }
1961            ]
1962        }"#;
1963
1964        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1965        if let ClaudeOutput::Result(result) = output {
1966            assert_eq!(result.permission_denials.len(), 1);
1967            assert_eq!(result.permission_denials[0].tool_name, "Bash");
1968            assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1969            assert_eq!(
1970                result.permission_denials[0]
1971                    .tool_input
1972                    .get("command")
1973                    .unwrap(),
1974                "rm -rf /"
1975            );
1976        } else {
1977            panic!("Expected Result");
1978        }
1979    }
1980
1981    #[test]
1982    fn test_permission_denial_roundtrip() {
1983        let denial = PermissionDenial {
1984            tool_name: "Write".to_string(),
1985            tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1986            tool_use_id: "toolu_456".to_string(),
1987        };
1988
1989        let json = serde_json::to_string(&denial).unwrap();
1990        assert!(json.contains("\"tool_name\":\"Write\""));
1991        assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1992        assert!(json.contains("/etc/passwd"));
1993
1994        let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1995        assert_eq!(parsed, denial);
1996    }
1997
1998    #[test]
1999    fn test_deserialize_result_message_with_errors() {
2000        let json = r#"{
2001            "type": "result",
2002            "subtype": "error_during_execution",
2003            "duration_ms": 0,
2004            "duration_api_ms": 0,
2005            "is_error": true,
2006            "num_turns": 0,
2007            "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
2008            "total_cost_usd": 0,
2009            "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
2010            "permission_denials": []
2011        }"#;
2012
2013        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2014        assert!(output.is_error());
2015
2016        if let ClaudeOutput::Result(res) = output {
2017            assert!(res.is_error);
2018            assert_eq!(res.errors.len(), 1);
2019            assert!(res.errors[0].contains("No conversation found"));
2020        } else {
2021            panic!("Expected Result message");
2022        }
2023    }
2024
2025    #[test]
2026    fn test_deserialize_result_message_errors_defaults_empty() {
2027        let json = r#"{
2028            "type": "result",
2029            "subtype": "success",
2030            "is_error": false,
2031            "duration_ms": 100,
2032            "duration_api_ms": 200,
2033            "num_turns": 1,
2034            "session_id": "123",
2035            "total_cost_usd": 0.01
2036        }"#;
2037
2038        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2039        if let ClaudeOutput::Result(res) = output {
2040            assert!(res.errors.is_empty());
2041        } else {
2042            panic!("Expected Result message");
2043        }
2044    }
2045
2046    #[test]
2047    fn test_result_message_errors_roundtrip() {
2048        let json = r#"{
2049            "type": "result",
2050            "subtype": "error_during_execution",
2051            "is_error": true,
2052            "duration_ms": 0,
2053            "duration_api_ms": 0,
2054            "num_turns": 0,
2055            "session_id": "test-session",
2056            "total_cost_usd": 0.0,
2057            "errors": ["Error 1", "Error 2"]
2058        }"#;
2059
2060        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2061        let reserialized = serde_json::to_string(&output).unwrap();
2062
2063        assert!(reserialized.contains("Error 1"));
2064        assert!(reserialized.contains("Error 2"));
2065    }
2066
2067    #[test]
2068    fn test_result_with_new_fields() {
2069        let json = r#"{
2070            "type": "result",
2071            "subtype": "success",
2072            "is_error": false,
2073            "duration_ms": 5000,
2074            "duration_api_ms": 4500,
2075            "num_turns": 1,
2076            "result": "Done",
2077            "session_id": "abc",
2078            "total_cost_usd": 0.06,
2079            "api_error_status": null,
2080            "stop_reason": "end_turn",
2081            "terminal_reason": "completed",
2082            "fast_mode_state": "off",
2083            "modelUsage": {
2084                "claude-opus-4-7[1m]": {
2085                    "inputTokens": 3817,
2086                    "outputTokens": 14,
2087                    "costUSD": 0.06
2088                }
2089            },
2090            "usage": {
2091                "input_tokens": 3817,
2092                "output_tokens": 14,
2093                "cache_creation_input_tokens": 3540,
2094                "cache_read_input_tokens": 0,
2095                "server_tool_use": {
2096                    "web_search_requests": 0,
2097                    "web_fetch_requests": 2
2098                },
2099                "service_tier": "standard",
2100                "inference_geo": "not_available",
2101                "speed": "standard",
2102                "iterations": [
2103                    {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
2104                ]
2105            }
2106        }"#;
2107
2108        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2109        if let ClaudeOutput::Result(res) = output {
2110            assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
2111            assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
2112            assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
2113            let model_usage = res.model_usage.as_ref().unwrap();
2114            let entry = model_usage
2115                .get("claude-opus-4-7[1m]")
2116                .expect("per-model entry present");
2117            assert_eq!(entry.input_tokens, 3817);
2118            assert_eq!(entry.output_tokens, 14);
2119            assert_eq!(entry.cost_usd, 0.06);
2120            assert!(res.api_error_status.is_none());
2121
2122            let usage = res.usage.unwrap();
2123            assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
2124            assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
2125            assert_eq!(usage.speed.as_deref(), Some("standard"));
2126            assert_eq!(usage.iterations.len(), 1);
2127            assert_eq!(usage.iterations[0].input_tokens, 3817);
2128            assert_eq!(usage.iterations[0].output_tokens, 14);
2129            assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
2130        } else {
2131            panic!("Expected Result");
2132        }
2133    }
2134
2135    #[test]
2136    fn test_result_backwards_compatible_without_new_fields() {
2137        // Verify old-format messages still parse fine
2138        let json = r#"{
2139            "type": "result",
2140            "subtype": "success",
2141            "is_error": false,
2142            "duration_ms": 100,
2143            "duration_api_ms": 200,
2144            "num_turns": 1,
2145            "session_id": "abc",
2146            "total_cost_usd": 0.01
2147        }"#;
2148
2149        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2150        if let ClaudeOutput::Result(res) = output {
2151            assert!(res.api_error_status.is_none());
2152            assert!(res.stop_reason.is_none());
2153            assert!(res.terminal_reason.is_none());
2154            assert!(res.fast_mode_state.is_none());
2155            assert!(res.model_usage.is_none());
2156        } else {
2157            panic!("Expected Result");
2158        }
2159    }
2160
2161    #[test]
2162    fn test_result_fast_mode_disabled_reason() {
2163        let json = r#"{
2164            "type":"result","subtype":"success","is_error":false,
2165            "duration_ms":100,"duration_api_ms":80,"num_turns":1,
2166            "session_id":"s1","total_cost_usd":0.01,
2167            "fast_mode_state":"off",
2168            "fast_mode_disabled_reason":"sdk_opt_in_required"
2169        }"#;
2170        let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
2171        let crate::ClaudeOutput::Result(res) = &output else {
2172            panic!("expected Result");
2173        };
2174        assert_eq!(
2175            res.fast_mode_disabled_reason,
2176            Some(FastModeDisabledReason::SdkOptInRequired)
2177        );
2178        assert!(serde_json::to_string(&output)
2179            .unwrap()
2180            .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
2181
2182        // Unknown reasons survive decode and round-trip verbatim; the wire
2183        // literal "unknown" maps to the typed UnknownReason, not the fallback.
2184        assert_eq!(
2185            FastModeDisabledReason::from("unknown"),
2186            FastModeDisabledReason::UnknownReason
2187        );
2188        let novel = FastModeDisabledReason::from("solar_flare");
2189        assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
2190        assert_eq!(novel.as_str(), "solar_flare");
2191    }
2192
2193    #[test]
2194    fn test_result_timing_and_user_message_uuid_fields() {
2195        let json = r#"{
2196            "type":"result","subtype":"success","is_error":false,
2197            "duration_ms":100,"duration_api_ms":80,"num_turns":1,
2198            "session_id":"s1","total_cost_usd":0.01,
2199            "request_sent_wall_ms":1753212345678.25,
2200            "user_message_uuid":"um-1"
2201        }"#;
2202        let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
2203        let crate::ClaudeOutput::Result(res) = &output else {
2204            panic!("expected Result");
2205        };
2206        assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
2207        assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
2208
2209        let reserialized = serde_json::to_string(&output).unwrap();
2210        assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
2211    }
2212
2213    #[test]
2214    fn test_result_queued_turn_count_and_user_message_uuids() {
2215        // CLI 2.1.259 additive fields on the result frame.
2216        let json = r#"{
2217            "type":"result","subtype":"success","is_error":false,
2218            "duration_ms":100,"duration_api_ms":80,"num_turns":1,
2219            "session_id":"s1","total_cost_usd":0.01,
2220            "user_message_uuid":"um-2",
2221            "user_message_uuids":["um-1","um-2"],
2222            "queued_turn_count":3
2223        }"#;
2224        let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
2225        let crate::ClaudeOutput::Result(res) = &output else {
2226            panic!("expected Result");
2227        };
2228        assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
2229        assert_eq!(res.queued_turn_count, Some(3));
2230
2231        let reserialized = serde_json::to_string(&output).unwrap();
2232        assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
2233        assert!(reserialized.contains("\"queued_turn_count\":3"));
2234
2235        // Both fields are absent from older producers and must default cleanly.
2236        let old = r#"{
2237            "type":"result","subtype":"success","is_error":false,
2238            "duration_ms":100,"duration_api_ms":80,"num_turns":1,
2239            "session_id":"s1","total_cost_usd":0.01
2240        }"#;
2241        let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
2242        let crate::ClaudeOutput::Result(res) = &output else {
2243            panic!("expected Result");
2244        };
2245        assert!(res.user_message_uuids.is_empty());
2246        assert_eq!(res.queued_turn_count, None);
2247        let reserialized = serde_json::to_string(&output).unwrap();
2248        assert!(!reserialized.contains("user_message_uuids"));
2249        assert!(!reserialized.contains("queued_turn_count"));
2250    }
2251}