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