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` and `other` (the remainder no phase covers). A phase
44    /// that did not run is absent; `other` is always present. Open set —
45    /// keep unrecognized phase names. Present where `time_to_request_ms` is,
46    /// on `CLAUDE_CODE_REMOTE` sessions and hosts that set
47    /// `CLAUDE_CODE_EMIT_STARTUP_TIMING`; absent elsewhere.
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub time_to_request_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
50
51    /// What the turn was given to settle before its own prompt; the
52    /// `resume_*` phases and `turn_start_tool_run` in
53    /// [`time_to_request_phases_ms`](Self::time_to_request_phases_ms) say how
54    /// long that took (CLI 2.1.287+). A turn given more than one kind reports
55    /// the first in [`TurnStartResumeKind`] order. Present where
56    /// `time_to_request_phases_ms` is.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub turn_start_resume_kind: Option<TurnStartResumeKind>,
59
60    /// Number of earlier turns of this CLI process that sent a
61    /// main-conversation model request: 0 is the process's first turn to
62    /// reach the model, not the conversation's first. Turns stopped or
63    /// failed after sending count but carry no value, so values can skip.
64    /// Where one process hosts several sessions, each has its own count.
65    /// Present where
66    /// [`time_to_request_phases_ms`](Self::time_to_request_phases_ms) is
67    /// (CLI 2.1.285+).
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub process_turn_index: Option<u64>,
70
71    /// CPU time (user plus system) the CLI process used from turn start
72    /// until it sent the turn's first main-conversation model request, in
73    /// milliseconds (CLI 2.1.285+). Near `time_to_request_ms`, the window
74    /// was mostly computing; near 0, mostly waiting. It adds up all threads,
75    /// so it can exceed the window, and leaves out child processes. Present
76    /// where `time_to_request_phases_ms` is, unless the runtime reports no
77    /// resource usage or the process hosts other sessions too.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub time_to_request_cpu_ms: Option<u64>,
80
81    /// The longest single stall of the session's thread between turn start
82    /// and the turn's first main-conversation model request, in
83    /// milliseconds, measured as how late a 50 ms timer fired (CLI
84    /// 2.1.285+): the real stall can be up to 50 ms longer, and a few ms is
85    /// timer noise. Only the first two minutes are measured. Present where
86    /// `time_to_request_phases_ms` is.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub time_to_request_loop_lag_ms: Option<u64>,
89
90    /// Major page faults (waits for memory to be read from disk) by the
91    /// process in the
92    /// [`time_to_request_cpu_ms`](Self::time_to_request_cpu_ms) window;
93    /// Windows also counts faults needing no disk read. Present where
94    /// `time_to_request_cpu_ms` is (CLI 2.1.285+).
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub time_to_request_major_faults: Option<u64>,
97
98    /// What the `input_attachments` phase of `time_to_request_phases_ms` did
99    /// on the turn, to tell a wait inside one attachment producer from a
100    /// process that was busy. Present on turns where the phase started,
101    /// unless nothing of it is known (CLI 2.1.288+).
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub input_attachments_detail: Option<InputAttachmentsDetail>,
104
105    /// How the `resume_store_confirm` phase of `time_to_request_phases_ms`
106    /// was spent. Present where that phase is, in a session whose transcript
107    /// goes to a hosted store: on the result of a handed-off turn whose lines
108    /// were confirmed and that went on to a model request (CLI 2.1.290+).
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub resume_store_confirm_detail: Option<ResumeStoreConfirmDetail>,
111
112    /// Time from spawning a worker/spare until the first request was issued, in milliseconds.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub time_to_request_from_spawn_ms: Option<u64>,
115
116    /// Whether a warm spare process was claimed for this request.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub warm_spare_claimed: Option<bool>,
119
120    /// Whether this process started its start-up feature-flag fetch before
121    /// the wait for it, or why not: the same value as `system/init`'s
122    /// `startup_timing.flag_fetch_kick`. An open string set
123    /// (`started_in_init_*`, `started_after_init_*`, `not_asked_warm`,
124    /// `settings_pending_*`, `launch_excluded`, `not_eligible`; an `_empty`
125    /// or `_warm` ending is the flag cache at that moment). Present only
126    /// where `time_to_request_from_spawn_ms` is, and only when start-up
127    /// judged it (CLI 2.1.288+).
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub flag_fetch_kick: Option<String>,
130
131    /// Epoch-ish timestamp origin used by CLI timing instrumentation.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub time_origin_ms: Option<u64>,
134
135    /// Wall-clock epoch milliseconds when the API request was sent
136    /// (fractional; from CLI timing instrumentation).
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub request_sent_wall_ms: Option<f64>,
139
140    /// Input tokens of the model call whose send time is
141    /// [`request_sent_wall_ms`](Self::request_sent_wall_ms): `input_tokens +
142    /// cache_read_input_tokens + cache_creation_input_tokens` from that call's
143    /// `message_start`, so a consumer can group turn latency by prompt size
144    /// (CLI 2.1.286+). Present only with `request_sent_wall_ms`, and only
145    /// when the sum is above zero.
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub first_request_input_tokens: Option<u64>,
148
149    /// Time until the first content frame of the stream arrived, in
150    /// milliseconds (CLI 2.1.260+ timing instrumentation).
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub first_content_frame_ms: Option<u64>,
153
154    /// Time until the first stream POST was issued, in milliseconds
155    /// (CLI 2.1.260+).
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub first_stream_post_ms: Option<u64>,
158
159    /// Time until the first stream POST was acknowledged, in milliseconds
160    /// (CLI 2.1.260+).
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub first_stream_post_ack_ms: Option<u64>,
163
164    /// Time the first stream POST spent waiting in the outbound queue, in
165    /// milliseconds (CLI 2.1.278+).
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub first_stream_post_queue_wait_ms: Option<u64>,
168
169    /// What the first stream POST was queued behind (CLI 2.1.278+).
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub first_stream_post_queued_behind: Option<StreamPostQueuedBehind>,
172
173    /// Wall-clock epoch milliseconds when the first stream POST was issued
174    /// (fractional; CLI 2.1.260+).
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub first_stream_post_wall_ms: Option<f64>,
177
178    /// Wall-clock epoch milliseconds when the triggering send's frame arrived
179    /// on a session reading its input from the session server's SSE stream
180    /// (`--sdk-url`), before the input loop read it (CLI 2.1.278+). With
181    /// [`frame_enqueued_wall_ms`](Self::frame_enqueued_wall_ms) and
182    /// [`turn_started_wall_ms`](Self::turn_started_wall_ms) it splits the
183    /// stretch between the server's persist and `request_sent_wall_ms` into
184    /// transit, the input loop's handling of the frame, its wait on the
185    /// command queue, and the turn's own work. Present only together with
186    /// `request_sent_wall_ms`, and only when the transport recorded the
187    /// receipt; absent on a local stdin host.
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub frame_received_wall_ms: Option<f64>,
190
191    /// Wall-clock epoch milliseconds when the input loop put the triggering
192    /// send's frame on the command queue (CLI 2.1.278+). Present exactly
193    /// when [`frame_received_wall_ms`](Self::frame_received_wall_ms) is.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub frame_enqueued_wall_ms: Option<f64>,
196
197    /// How the triggering send's frame spent the time between
198    /// [`frame_received_wall_ms`](Self::frame_received_wall_ms) and
199    /// [`frame_enqueued_wall_ms`](Self::frame_enqueued_wall_ms), in integer
200    /// milliseconds per intake step (CLI 2.1.282+): `before_read` (until the
201    /// input loop read the frame, including any wait behind earlier frames
202    /// and a first message's wait for the worker to finish starting),
203    /// `dedup`, `flag_settle`, `receive_hook`, `attachments`,
204    /// `admission_wait` (held behind a pending model switch, a pending MCP
205    /// server-set change, start-up work the worker still owed its first
206    /// turn, or an earlier held frame), `admit_check` (the host's decision
207    /// whether to queue the frame, when it had to be awaited) and `other`.
208    /// Steps that did not run are absent; `before_read` and `other` are
209    /// always present. The values sum to the enqueued-minus-received gap
210    /// unless a step came out negative and was clamped to 0. Open set —
211    /// keep unrecognized step names. Present exactly when
212    /// `frame_received_wall_ms` is, except from older producers.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub frame_intake_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
215
216    /// Wall-clock epoch milliseconds when the turn's clock started: the
217    /// anchor `duration_ms`, `ttft_ms`, `ttft_stream_ms`,
218    /// `time_to_request_ms`, `first_content_frame_ms`, `first_stream_post_ms`
219    /// and `first_stream_post_ack_ms` are measured from (CLI 2.1.278+).
220    /// Present exactly when
221    /// [`frame_received_wall_ms`](Self::frame_received_wall_ms) is.
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    pub turn_started_wall_ms: Option<f64>,
224
225    /// How the time from
226    /// [`frame_enqueued_wall_ms`](Self::frame_enqueued_wall_ms) to
227    /// [`turn_started_wall_ms`](Self::turn_started_wall_ms) was spent, in
228    /// integer milliseconds per step of the host's work through its message
229    /// queue (CLI 2.1.285+): `idle`, `earlier_turn`, `after_turn`,
230    /// `background_wait`, `entry_joins`, `sdk_mcp_update`, `loop_top`,
231    /// `session_start_wait`, `first_command`, `mcp_prewait`, `repo_wait`,
232    /// `turn_joins`, `tool_pool`, `commit`, `after_arm`, `turn_gate`,
233    /// `turn_open`, `engine_setup`, `tool_reread`, `cwd` and `other`. Each
234    /// step is where the host was at the time, which for part of the stretch
235    /// may be an earlier message's turn. A step that rounds to 0 is absent;
236    /// `other` is always present. The values sum to the started-minus-enqueued
237    /// gap, except that they can exceed it (usually by 1 ms from rounding).
238    /// Open set — keep unrecognized step names. Present only when
239    /// `frame_received_wall_ms` is, and absent from older producers.
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub turn_start_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
242
243    /// Of the time from `frame_enqueued_wall_ms` to `turn_started_wall_ms`,
244    /// the integer milliseconds in which the `claude -p` host's message
245    /// reader was held by control requests (such as `mcp_set_servers` or
246    /// `set_permission_mode`) instead of reading the next message (CLI
247    /// 2.1.285+). It overlaps
248    /// [`turn_start_phases_ms`](Self::turn_start_phases_ms) and is not part
249    /// of its sum. Absent when 0.
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub turn_start_control_requests_ms: Option<u64>,
252
253    /// Time until the first text POST was issued, in milliseconds
254    /// (CLI 2.1.278+).
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub first_text_post_ms: Option<u64>,
257
258    /// Time the first text POST spent waiting in the outbound queue, in
259    /// milliseconds (CLI 2.1.287+).
260    #[serde(default, skip_serializing_if = "Option::is_none")]
261    pub first_text_post_queue_wait_ms: Option<u64>,
262
263    /// What the first text POST was queued behind (CLI 2.1.287+); the same
264    /// value set as
265    /// [`first_stream_post_queued_behind`](Self::first_stream_post_queued_behind).
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub first_text_post_queued_behind: Option<StreamPostQueuedBehind>,
268
269    /// Wall-clock epoch milliseconds when the first text POST was issued
270    /// (fractional; CLI 2.1.278+).
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub first_text_post_wall_ms: Option<f64>,
273
274    /// Wire uuid of the user message this result answers.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub user_message_uuid: Option<String>,
277
278    /// The [`server_received_wall_ms`](crate::io::UserMessage::server_received_wall_ms)
279    /// of the user message [`user_message_uuid`](Self::user_message_uuid)
280    /// names, copied unchanged, so the session server can measure from its
281    /// own receipt to `request_sent_wall_ms` (CLI 2.1.286+). Present only with
282    /// `request_sent_wall_ms`, and only when that message carried a positive
283    /// finite number there.
284    #[serde(default, skip_serializing_if = "Option::is_none")]
285    pub user_message_server_received_wall_ms: Option<f64>,
286
287    /// Client uuids of every user message whose prompt this turn consumed, in
288    /// consumption order — all members of a prompt batch the host merged into
289    /// this one turn, plus any queued user message folded into the running
290    /// turn between tool rounds. Always contains `user_message_uuid`; at most
291    /// 64 entries. Absent on delivery-failure/zeroed results and from CLIs
292    /// before 2.1.259 (fall back to `user_message_uuid`).
293    #[serde(default, skip_serializing_if = "Vec::is_empty")]
294    pub user_message_uuids: Vec<String>,
295
296    /// Why this turn was the automatic re-run of a turn a worker restart
297    /// interrupted (`CLAUDE_CODE_RESUME_INTERRUPTED_TURN`): the host's
298    /// `CLAUDE_CODE_RESUME_REASON` when it set one (`host_draining`,
299    /// `checkpoint_restore`, `container_recreated`, ...), else
300    /// `interrupted_turn`. Present on a headless re-run's result, success or
301    /// error; absent on every other turn, on the Remote Control bridge's
302    /// per-turn synthetic results, and from CLIs before 2.1.268.
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub resume_reason: Option<String>,
305
306    /// The local slash command that produced this result when the query loop
307    /// was bypassed (success results only). Absent when the turn went to the
308    /// model and from CLIs before 2.1.268.
309    #[serde(default, skip_serializing_if = "Option::is_none")]
310    pub local_command: Option<String>,
311
312    /// User-initiated sends still waiting in the command queue when this
313    /// result was produced. Greater than 0 means at least one more user turn
314    /// follows without further input, barring cancellation. Absent on fatal
315    /// startup results, on surfaces without a command queue, and from CLIs
316    /// before 2.1.259.
317    #[serde(default, skip_serializing_if = "Option::is_none")]
318    pub queued_turn_count: Option<u64>,
319
320    pub num_turns: i32,
321
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub result: Option<String>,
324
325    #[serde(alias = "sessionId")]
326    pub session_id: String,
327    pub total_cost_usd: f64,
328
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub usage: Option<UsageInfo>,
331
332    /// Tools that were blocked due to permission denials during the session
333    #[serde(default)]
334    pub permission_denials: Vec<PermissionDenial>,
335
336    /// Error messages when `is_error` is true.
337    ///
338    /// Contains human-readable error strings (e.g., "No conversation found with session ID: ...").
339    /// This allows typed access to error conditions without needing to serialize to JSON and search.
340    #[serde(default)]
341    pub errors: Vec<String>,
342
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub uuid: Option<String>,
345
346    /// HTTP status code when the result is an API error (e.g., 429, 500, 529)
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub api_error_status: Option<u16>,
349
350    /// The `api_error_code` of the API error that ended the turn (see
351    /// `AssistantMessage::api_error_code`): the server's
352    /// `error.details.error_code` when it is an identifier. Absent when the
353    /// turn did not end on an API error, when the response carried no code,
354    /// and from CLIs before 2.1.274.
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub api_error_code: Option<String>,
357
358    /// The typed kind of the API error that ended the turn (see
359    /// `AssistantMessage::api_error`). Absent when the turn did not end on
360    /// an API error, when the error has no typed kind, and from CLIs before
361    /// 2.1.285; treat an unknown value as absent.
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub api_error: Option<String>,
364
365    /// Why generation stopped (e.g., end_turn, max_tokens)
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub stop_reason: Option<String>,
368
369    /// Why the session ended (e.g., "completed")
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub terminal_reason: Option<String>,
372
373    /// Delivery sequence of this result within the run: how many results the
374    /// run numbered before this one, starting at 0, in the order the process
375    /// writes them. A result whose write fails still consumes its number, so
376    /// a gap means a result was lost. Distinct from `num_turns`. Numbered by
377    /// the process hosting the run (`claude -p`); a local client relaying a
378    /// cloud session passes the cloud numbering through and its own locally
379    /// built error results carry none. Absent from CLIs before 2.1.268.
380    #[serde(default, skip_serializing_if = "Option::is_none")]
381    pub result_index: Option<u64>,
382
383    /// Fast mode toggle state (e.g., "off")
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub fast_mode_state: Option<String>,
386
387    /// Why fast mode can't serve right now. Absent when nothing blocks it.
388    /// A paused-after-rate-limit run is not reported here; it rides
389    /// `fast_mode_state` as `"cooldown"`.
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
392
393    /// Per-model cost breakdown, keyed by model name (e.g. `"claude-opus-4-8"`).
394    #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
395    pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
396
397    /// Subagents started through the Agent tool in this session, as running
398    /// totals. Cumulative like `modelUsage`: read the latest result rather
399    /// than summing across results. Absent from CLIs before 2.1.239.
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub subagent_stats: Option<SubagentStats>,
402
403    /// How many times a safety system has stopped one of this session's
404    /// model calls (main loop, subagents, forked skills, compaction and other
405    /// internal calls), as a running total since the session started in this
406    /// process: a `refusal` stop reason, a refusal answered on a fallback
407    /// model, output the API's content filter stopped, and (where the build
408    /// can tell) a server-side safety monitor block. Never sum across
409    /// results. Not saved, so a resumed session or a mid-session `/clear`
410    /// starts again at 0: over a stored log any result above 0 means a stop
411    /// happened, and a later 0 does not cancel it. Absent from CLIs before
412    /// 2.1.292. Marked `@internal` upstream.
413    #[serde(default, skip_serializing_if = "Option::is_none")]
414    pub safety_stops: Option<u64>,
415
416    /// Structured-output payload returned by the model, when enabled.
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub structured_output: Option<Value>,
419
420    /// Deferred tool-use termination payload.
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub deferred_tool_use: Option<DeferredToolUse>,
423
424    /// Provenance of the message/run.
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub origin: Option<super::message_types::MessageOrigin>,
427
428    /// Set by the self-hosted runner on the result it synthesizes when the
429    /// session process fails to start or exits abnormally. Absent on normal
430    /// results and on CLIs before 2.1.266.
431    #[serde(default, skip_serializing_if = "Option::is_none")]
432    pub runner_exit: Option<RunnerExit>,
433
434    /// Why Claude Code refused to start. Set on the zeroed
435    /// `error_during_execution` result a stream-json run writes before
436    /// exiting on a known startup failure; `errors` carries the same text as
437    /// stderr. Failures that used to end with stderr alone write that result
438    /// only when the host sets `CLAUDE_CODE_STARTUP_FAILURE_RESULTS`. Absent
439    /// on every other result, on startup failures without a known cause, and
440    /// from CLIs before 2.1.274.
441    #[serde(default, skip_serializing_if = "Option::is_none")]
442    pub startup_failure_reason: Option<StartupFailureReason>,
443}
444
445/// [`ResultMessage::input_attachments_detail`] — what the
446/// `input_attachments` phase did on the turn (CLI 2.1.288+).
447#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
448pub struct InputAttachmentsDetail {
449    /// Label of the attachment producer that took longest from its start to
450    /// its result, among those that began within the phase.
451    #[serde(default, skip_serializing_if = "Option::is_none")]
452    pub slowest_producer: Option<String>,
453    /// How long [`Self::slowest_producer`] took. Well above
454    /// [`Self::cpu_ms`], that producer most likely waited (on a file, the
455    /// network or a child process).
456    #[serde(default, skip_serializing_if = "Option::is_none")]
457    pub slowest_producer_ms: Option<u64>,
458    /// `time_to_request_cpu_ms`'s counter over this phase alone. Absent
459    /// wherever `time_to_request_cpu_ms` is absent, and when the turn's
460    /// first model request went out before the phase ended.
461    #[serde(default, skip_serializing_if = "Option::is_none")]
462    pub cpu_ms: Option<u64>,
463    /// `time_to_request_major_faults`'s counter over this phase alone.
464    /// Absent where [`Self::cpu_ms`] is.
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub major_faults: Option<u64>,
467    /// How many files the `changed_files` producer set out to read again in
468    /// full because their modification time was later than the one recorded
469    /// when the conversation last wrote or edited them. Absent when the
470    /// conversation's record of files was empty or the producer did not run.
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub changed_files_reread: Option<u64>,
473}
474
475/// [`ResultMessage::resume_store_confirm_detail`] — how the
476/// `resume_store_confirm` phase was spent (CLI 2.1.290+). The four `_ms`
477/// values sum to that phase exactly.
478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
479pub struct ResumeStoreConfirmDetail {
480    /// The transcript write, until the carried lines are in the queue of the
481    /// lane that uploads transcript rows.
482    pub write_ms: u64,
483    /// A POST was out that held only rows queued before the hand-off's write
484    /// began.
485    pub ahead_ms: u64,
486    /// Any other POST was out, each retry included.
487    pub post_ms: u64,
488    /// No POST was out (backoff, batching, a busy event loop), plus rounding
489    /// and any POST still out when the wait was cut short.
490    pub other_ms: u64,
491    /// POST attempts that ended between the start of the write and the end
492    /// of the wait.
493    pub posts: u64,
494    /// Those of [`Self::posts`] that did not go through.
495    pub failed_posts: u64,
496    /// Rows the lane had accepted and the store had not yet confirmed when
497    /// the write began.
498    pub rows_ahead: u64,
499    /// Rows queued since then that a POST delivered, the carried lines among
500    /// them.
501    pub rows: u64,
502}
503
504/// Why Claude Code refused to start, carried as
505/// [`ResultMessage::startup_failure_reason`] so a host can offer the fix
506/// instead of a retry (CLI 2.1.274+).
507#[derive(Debug, Clone, PartialEq, Eq, Hash)]
508pub enum StartupFailureReason {
509    /// Managed settings pin a first-party or Cloud gateway sign-in, and an
510    /// Anthropic API key or auth token is configured instead.
511    OrgPinApiKeyConflict,
512    /// Managed settings list the API providers this machine may use
513    /// (`allowedProviders`), and the session is set up for one that is not
514    /// listed (CLI 2.1.285+).
515    ProviderNotAllowed,
516    /// The sign-in's organization could not be verified against the pin
517    /// (network, or a revoked token).
518    OrgVerifyFailed,
519    /// The sign-in belongs to an organization the pin does not allow.
520    OrgPinMismatch,
521    /// Managed policy settings could not be read, the pin names no
522    /// organization, or managed model settings (`deniedModels`, or an
523    /// `availableModels` list matched exactly) block the default model and
524    /// leave no allowed model to use instead.
525    ManagedSettingsInvalid,
526    /// Managed settings the organization requires could not be loaded.
527    RemoteSettingsRequiredUnavailable,
528    /// The Cloud gateway ended this sign-in.
529    GatewaySigninRequired,
530    /// The Cloud gateway refused managed settings for this account.
531    GatewayAccessDenied,
532    /// A proxy setting is not a complete URL.
533    ProxyInvalid,
534    /// The per-user temp directory is unsafe or could not be created.
535    TempDirUnusable,
536    /// The working directory was deleted, moved or cannot be read.
537    CwdUnavailable,
538    /// Windows has no shell tool: Git Bash is missing, and PowerShell is
539    /// missing or turned off by `CLAUDE_CODE_USE_POWERSHELL_TOOL`.
540    ShellToolMissing,
541    /// The conversation to resume or continue is running as a background
542    /// session.
543    SessionHeldByBackground,
544    /// The resume was refused because the session's worktree failed its
545    /// safety checks or the resume was launched from inside it; `errors`
546    /// says whether a re-run continues without the worktree.
547    WorktreeResumeRefused,
548    /// The session's worktree could not be verified right now; retrying may
549    /// succeed.
550    WorktreeUnverified,
551    /// This Claude Code version is below the minimum Anthropic requires.
552    CliVersionTooOld,
553    /// Bypass permissions mode was requested while running as root.
554    BypassRoot,
555    /// The organization requires its policy limits and managed settings
556    /// before a session starts, and they could not be loaded (network,
557    /// proxy, or an Anthropic error); retrying may succeed (CLI 2.1.293+).
558    OrgConfigRequiredUnavailable,
559    /// The organization requires its policy limits and managed settings, and
560    /// Anthropic refused them for this sign-in (expired or revoked session,
561    /// or the organization does not allow Claude Code for this account);
562    /// signing in again or an admin's change is the fix, not a retry
563    /// (CLI 2.1.293+).
564    OrgConfigRefused,
565    /// A cause not yet known to this version of the crate.
566    Unknown(String),
567}
568
569impl StartupFailureReason {
570    pub fn as_str(&self) -> &str {
571        match self {
572            Self::OrgPinApiKeyConflict => "org_pin_api_key_conflict",
573            Self::ProviderNotAllowed => "provider_not_allowed",
574            Self::OrgVerifyFailed => "org_verify_failed",
575            Self::OrgPinMismatch => "org_pin_mismatch",
576            Self::ManagedSettingsInvalid => "managed_settings_invalid",
577            Self::RemoteSettingsRequiredUnavailable => "remote_settings_required_unavailable",
578            Self::GatewaySigninRequired => "gateway_signin_required",
579            Self::GatewayAccessDenied => "gateway_access_denied",
580            Self::ProxyInvalid => "proxy_invalid",
581            Self::TempDirUnusable => "temp_dir_unusable",
582            Self::CwdUnavailable => "cwd_unavailable",
583            Self::ShellToolMissing => "shell_tool_missing",
584            Self::SessionHeldByBackground => "session_held_by_background",
585            Self::WorktreeResumeRefused => "worktree_resume_refused",
586            Self::WorktreeUnverified => "worktree_unverified",
587            Self::CliVersionTooOld => "cli_version_too_old",
588            Self::BypassRoot => "bypass_root",
589            Self::OrgConfigRequiredUnavailable => "org_config_required_unavailable",
590            Self::OrgConfigRefused => "org_config_refused",
591            Self::Unknown(s) => s.as_str(),
592        }
593    }
594}
595
596impl fmt::Display for StartupFailureReason {
597    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598        f.write_str(self.as_str())
599    }
600}
601
602impl From<&str> for StartupFailureReason {
603    fn from(s: &str) -> Self {
604        match s {
605            "org_pin_api_key_conflict" => Self::OrgPinApiKeyConflict,
606            "provider_not_allowed" => Self::ProviderNotAllowed,
607            "org_verify_failed" => Self::OrgVerifyFailed,
608            "org_pin_mismatch" => Self::OrgPinMismatch,
609            "managed_settings_invalid" => Self::ManagedSettingsInvalid,
610            "remote_settings_required_unavailable" => Self::RemoteSettingsRequiredUnavailable,
611            "gateway_signin_required" => Self::GatewaySigninRequired,
612            "gateway_access_denied" => Self::GatewayAccessDenied,
613            "proxy_invalid" => Self::ProxyInvalid,
614            "temp_dir_unusable" => Self::TempDirUnusable,
615            "cwd_unavailable" => Self::CwdUnavailable,
616            "shell_tool_missing" => Self::ShellToolMissing,
617            "session_held_by_background" => Self::SessionHeldByBackground,
618            "worktree_resume_refused" => Self::WorktreeResumeRefused,
619            "worktree_unverified" => Self::WorktreeUnverified,
620            "cli_version_too_old" => Self::CliVersionTooOld,
621            "bypass_root" => Self::BypassRoot,
622            "org_config_required_unavailable" => Self::OrgConfigRequiredUnavailable,
623            "org_config_refused" => Self::OrgConfigRefused,
624            other => Self::Unknown(other.to_string()),
625        }
626    }
627}
628
629impl Serialize for StartupFailureReason {
630    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
631        serializer.serialize_str(self.as_str())
632    }
633}
634
635impl<'de> Deserialize<'de> for StartupFailureReason {
636    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
637        let s = String::deserialize(deserializer)?;
638        Ok(Self::from(s.as_str()))
639    }
640}
641
642/// How the self-hosted runner's session process terminated, carried on a
643/// synthesized failure [`ResultMessage`] as [`ResultMessage::runner_exit`]
644/// (CLI 2.1.266+).
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
646pub struct RunnerExit {
647    /// Which phase the process was in when it exited.
648    pub phase: RunnerExitPhase,
649    /// Process exit code. Absent (or wire `null`) when it exited on a signal
650    /// instead, or when the runner did not capture one.
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub exit_code: Option<i64>,
653    /// Terminating signal name. Absent (or wire `null`) when it exited with a
654    /// code instead, or when the runner did not capture one.
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    pub signal: Option<String>,
657}
658
659/// Which phase the runner's session process was in when it exited, in
660/// [`RunnerExit::phase`]. Open set — unrecognized values deserialize to
661/// [`RunnerExitPhase::Unknown`].
662#[derive(Debug, Clone, PartialEq, Eq, Hash)]
663pub enum RunnerExitPhase {
664    /// Failed while starting up.
665    Setup,
666    /// Failed once running.
667    Run,
668    /// A phase not yet known to this version of the crate.
669    Unknown(String),
670}
671
672impl RunnerExitPhase {
673    pub fn as_str(&self) -> &str {
674        match self {
675            Self::Setup => "setup",
676            Self::Run => "run",
677            Self::Unknown(s) => s.as_str(),
678        }
679    }
680}
681
682impl std::fmt::Display for RunnerExitPhase {
683    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
684        f.write_str(self.as_str())
685    }
686}
687
688impl From<&str> for RunnerExitPhase {
689    fn from(s: &str) -> Self {
690        match s {
691            "setup" => Self::Setup,
692            "run" => Self::Run,
693            other => Self::Unknown(other.to_string()),
694        }
695    }
696}
697
698impl Serialize for RunnerExitPhase {
699    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
700        serializer.serialize_str(self.as_str())
701    }
702}
703
704impl<'de> Deserialize<'de> for RunnerExitPhase {
705    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
706        let s = String::deserialize(deserializer)?;
707        Ok(Self::from(s.as_str()))
708    }
709}
710
711/// Usage and cost for a single model within a session, as found in
712/// [`ResultMessage::model_usage`].
713///
714/// The `extra` field captures any keys the CLI adds that aren't modeled here,
715/// so new wire fields deserialize without error.
716#[derive(Debug, Clone, Default, Serialize, Deserialize)]
717#[serde(rename_all = "camelCase")]
718pub struct ModelUsageEntry {
719    #[serde(default)]
720    pub input_tokens: u64,
721    #[serde(default)]
722    pub output_tokens: u64,
723    #[serde(default)]
724    pub cache_read_input_tokens: u64,
725    #[serde(default)]
726    pub cache_creation_input_tokens: u64,
727    #[serde(default, rename = "costUSD")]
728    pub cost_usd: f64,
729    #[serde(default)]
730    pub web_search_requests: u32,
731    #[serde(default)]
732    pub context_window: u64,
733    #[serde(default)]
734    pub max_output_tokens: u64,
735    #[serde(flatten)]
736    pub extra: serde_json::Map<String, serde_json::Value>,
737}
738
739/// Tool use deferred by a terminal result.
740#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
741pub struct DeferredToolUse {
742    pub id: String,
743    pub name: String,
744    pub input: Value,
745}
746
747/// Running totals for subagents started through the Agent tool, carried on
748/// `result` frames (CLI 2.1.239+) as [`ResultMessage::subagent_stats`].
749///
750/// Cumulative for the session: a resumed session starts fresh, and a
751/// mid-session `/clear` zeroes it — though a background subagent that outlives
752/// the `/clear` still records its outcome, so `completed`, `failed`, and
753/// `killed` can then exceed `spawned`. Forked skills, workflows, teammates,
754/// and other internal agents are not counted.
755#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
756pub struct SubagentStats {
757    /// Subagents actually started; a refused or failed launch is not counted.
758    pub spawned: u64,
759    /// Spawns by the `run_in_background` value the model passed; all count as
760    /// `unset` while the parameter is not offered.
761    pub requested: SubagentSpawnRequests,
762    /// Spawns that started in the background after defaults and session
763    /// settings; the rest blocked the spawning tool call.
764    pub started_in_background: u64,
765    /// Spawns by agent type.
766    #[serde(default)]
767    pub by_type: std::collections::BTreeMap<String, u64>,
768    /// Deepest spawn: 1 = started by the main thread, 2 = by a depth-1
769    /// subagent.
770    pub max_depth: u64,
771    /// Spawns made from inside another subagent (depth > 1).
772    pub spawned_by_subagents: u64,
773    pub completed: u64,
774    pub failed: u64,
775    /// Subagents stopped before finishing, by who stopped them.
776    pub killed: SubagentKillCounts,
777    /// Agent tool calls turned down because a limit was reached (other
778    /// denials, such as an unknown agent type, are not counted).
779    pub refused: SubagentRefusalCounts,
780}
781
782/// Spawn counts by the `run_in_background` value the model passed, carried in
783/// [`SubagentStats::requested`].
784#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
785pub struct SubagentSpawnRequests {
786    pub background: u64,
787    pub foreground: u64,
788    pub unset: u64,
789}
790
791/// Subagents stopped before finishing, carried in [`SubagentStats::killed`].
792/// `parent` = by another agent through TaskStop; `system` = by Claude Code
793/// itself; `user` = any other stop.
794#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
795pub struct SubagentKillCounts {
796    pub parent: u64,
797    pub user: u64,
798    pub system: u64,
799}
800
801/// Agent tool calls refused at a limit, carried in [`SubagentStats::refused`].
802#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
803pub struct SubagentRefusalCounts {
804    pub depth_limit: u64,
805    pub concurrency_limit: u64,
806    pub budget: u64,
807}
808
809/// A record of a tool permission that was denied during the session.
810///
811/// This is included in `ResultMessage.permission_denials` to provide a summary
812/// of all permission denials that occurred.
813#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
814pub struct PermissionDenial {
815    /// The name of the tool that was blocked (e.g., "Bash", "Write")
816    pub tool_name: String,
817
818    /// The input that was passed to the tool
819    pub tool_input: Value,
820
821    /// The unique identifier for this tool use request
822    pub tool_use_id: String,
823}
824
825/// What the first stream POST of a turn was queued behind, carried as
826/// [`ResultMessage::first_stream_post_queued_behind`] (CLI 2.1.278+).
827/// Internal timing instrumentation; the CLI does not describe the values.
828#[derive(Debug, Clone, PartialEq, Eq, Hash)]
829pub enum StreamPostQueuedBehind {
830    /// `durable_post`
831    DurablePost,
832    /// `ephemeral_post`
833    EphemeralPost,
834    /// `retry_backoff`
835    RetryBackoff,
836    /// `hold`
837    Hold,
838    /// `none`
839    None,
840    /// A value not yet known to this version of the crate.
841    Unknown(String),
842}
843
844impl StreamPostQueuedBehind {
845    pub fn as_str(&self) -> &str {
846        match self {
847            Self::DurablePost => "durable_post",
848            Self::EphemeralPost => "ephemeral_post",
849            Self::RetryBackoff => "retry_backoff",
850            Self::Hold => "hold",
851            Self::None => "none",
852            Self::Unknown(s) => s.as_str(),
853        }
854    }
855}
856
857impl fmt::Display for StreamPostQueuedBehind {
858    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
859        f.write_str(self.as_str())
860    }
861}
862
863impl From<&str> for StreamPostQueuedBehind {
864    fn from(s: &str) -> Self {
865        match s {
866            "durable_post" => Self::DurablePost,
867            "ephemeral_post" => Self::EphemeralPost,
868            "retry_backoff" => Self::RetryBackoff,
869            "hold" => Self::Hold,
870            "none" => Self::None,
871            other => Self::Unknown(other.to_string()),
872        }
873    }
874}
875
876impl Serialize for StreamPostQueuedBehind {
877    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
878        serializer.serialize_str(self.as_str())
879    }
880}
881
882impl<'de> Deserialize<'de> for StreamPostQueuedBehind {
883    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
884        let s = String::deserialize(deserializer)?;
885        Ok(Self::from(s.as_str()))
886    }
887}
888
889/// What a turn was given to settle before its own prompt, carried as
890/// [`ResultMessage::turn_start_resume_kind`] (CLI 2.1.287+). A turn given
891/// more than one reports the first in this order.
892#[derive(Debug, Clone, PartialEq, Eq, Hash)]
893pub enum TurnStartResumeKind {
894    /// `orphaned_permission` — a permission answer whose pending request was
895    /// lost (as when a worker restarts), so the tool call is settled with
896    /// that answer.
897    OrphanedPermission,
898    /// `turn_handoff` — a turn in progress that the session's client handed
899    /// over.
900    TurnHandoff,
901    /// `deferred_tool_use` — a call a `PreToolUse` hook deferred.
902    DeferredToolUse,
903    /// `none` — nothing to settle (`resume_other` is still reported, at
904    /// about 0).
905    None,
906    /// A value not yet known to this version of the crate.
907    Unknown(String),
908}
909
910impl TurnStartResumeKind {
911    pub fn as_str(&self) -> &str {
912        match self {
913            Self::OrphanedPermission => "orphaned_permission",
914            Self::TurnHandoff => "turn_handoff",
915            Self::DeferredToolUse => "deferred_tool_use",
916            Self::None => "none",
917            Self::Unknown(s) => s.as_str(),
918        }
919    }
920}
921
922impl fmt::Display for TurnStartResumeKind {
923    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
924        f.write_str(self.as_str())
925    }
926}
927
928impl From<&str> for TurnStartResumeKind {
929    fn from(s: &str) -> Self {
930        match s {
931            "orphaned_permission" => Self::OrphanedPermission,
932            "turn_handoff" => Self::TurnHandoff,
933            "deferred_tool_use" => Self::DeferredToolUse,
934            "none" => Self::None,
935            other => Self::Unknown(other.to_string()),
936        }
937    }
938}
939
940impl Serialize for TurnStartResumeKind {
941    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
942        serializer.serialize_str(self.as_str())
943    }
944}
945
946impl<'de> Deserialize<'de> for TurnStartResumeKind {
947    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
948        let s = String::deserialize(deserializer)?;
949        Ok(Self::from(s.as_str()))
950    }
951}
952
953/// Why fast mode can't serve right now, carried on `result` frames and
954/// `system/init` (CLI 2.1.219+). Absent when nothing blocks fast mode.
955#[derive(Debug, Clone, PartialEq, Eq, Hash)]
956pub enum FastModeDisabledReason {
957    /// Free-tier account.
958    Free,
959    /// Disabled by user preference.
960    Preference,
961    /// Extra-usage purchases are disabled for the account.
962    ExtraUsageDisabled,
963    /// A network error prevented fast-mode eligibility from resolving.
964    NetworkError,
965    /// The CLI could not determine the reason.
966    UnknownReason,
967    /// Not a first-party API session (e.g. Bedrock/Vertex).
968    NotFirstParty,
969    /// Disabled via environment variable.
970    DisabledByEnv,
971    /// The active model does not support fast mode.
972    ModelNotAllowed,
973    /// SDK sessions must opt in to fast mode.
974    SdkOptInRequired,
975    /// Eligibility is still being determined.
976    Pending,
977    /// A reason not yet known to this version of the crate.
978    Unknown(String),
979}
980
981impl FastModeDisabledReason {
982    pub fn as_str(&self) -> &str {
983        match self {
984            Self::Free => "free",
985            Self::Preference => "preference",
986            Self::ExtraUsageDisabled => "extra_usage_disabled",
987            Self::NetworkError => "network_error",
988            Self::UnknownReason => "unknown",
989            Self::NotFirstParty => "not_first_party",
990            Self::DisabledByEnv => "disabled_by_env",
991            Self::ModelNotAllowed => "model_not_allowed",
992            Self::SdkOptInRequired => "sdk_opt_in_required",
993            Self::Pending => "pending",
994            Self::Unknown(s) => s.as_str(),
995        }
996    }
997}
998
999impl fmt::Display for FastModeDisabledReason {
1000    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1001        f.write_str(self.as_str())
1002    }
1003}
1004
1005impl From<&str> for FastModeDisabledReason {
1006    fn from(s: &str) -> Self {
1007        match s {
1008            "free" => Self::Free,
1009            "preference" => Self::Preference,
1010            "extra_usage_disabled" => Self::ExtraUsageDisabled,
1011            "network_error" => Self::NetworkError,
1012            "unknown" => Self::UnknownReason,
1013            "not_first_party" => Self::NotFirstParty,
1014            "disabled_by_env" => Self::DisabledByEnv,
1015            "model_not_allowed" => Self::ModelNotAllowed,
1016            "sdk_opt_in_required" => Self::SdkOptInRequired,
1017            "pending" => Self::Pending,
1018            other => Self::Unknown(other.to_string()),
1019        }
1020    }
1021}
1022
1023impl Serialize for FastModeDisabledReason {
1024    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1025        serializer.serialize_str(self.as_str())
1026    }
1027}
1028
1029impl<'de> Deserialize<'de> for FastModeDisabledReason {
1030    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1031        let s = String::deserialize(deserializer)?;
1032        Ok(Self::from(s.as_str()))
1033    }
1034}
1035
1036/// Result subtypes
1037#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1038pub enum ResultSubtype {
1039    Success,
1040    ErrorMaxTurns,
1041    ErrorDuringExecution,
1042    ErrorMaxBudgetUsd,
1043    ErrorMaxStructuredOutputRetries,
1044    Unknown(String),
1045}
1046
1047impl ResultSubtype {
1048    pub fn as_str(&self) -> &str {
1049        match self {
1050            Self::Success => "success",
1051            Self::ErrorMaxTurns => "error_max_turns",
1052            Self::ErrorDuringExecution => "error_during_execution",
1053            Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
1054            Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
1055            Self::Unknown(s) => s.as_str(),
1056        }
1057    }
1058}
1059
1060impl fmt::Display for ResultSubtype {
1061    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1062        f.write_str(self.as_str())
1063    }
1064}
1065
1066impl From<&str> for ResultSubtype {
1067    fn from(s: &str) -> Self {
1068        match s {
1069            "success" => Self::Success,
1070            "error_max_turns" => Self::ErrorMaxTurns,
1071            "error_during_execution" => Self::ErrorDuringExecution,
1072            "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
1073            "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
1074            other => Self::Unknown(other.to_string()),
1075        }
1076    }
1077}
1078
1079impl Serialize for ResultSubtype {
1080    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1081        serializer.serialize_str(self.as_str())
1082    }
1083}
1084
1085impl<'de> Deserialize<'de> for ResultSubtype {
1086    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1087        let s = String::deserialize(deserializer)?;
1088        Ok(Self::from(s.as_str()))
1089    }
1090}
1091
1092/// Usage information for the request.
1093///
1094/// **These counters are accumulated roll-ups, not snapshots.** The CLI sums
1095/// usage field-by-field across every API call ("iteration") in the turn's
1096/// tool-use loop, so on a turn with N iterations each counter is the sum of
1097/// all N. This is the right number for **cost**, and the wrong number for
1098/// **context occupancy**: `cache_read_input_tokens` in particular re-counts
1099/// the cached context on every iteration and can exceed the model's context
1100/// window several times over on tool-heavy turns. To estimate context
1101/// occupancy, use the *last* entry of [`iterations`](Self::iterations)
1102/// instead (the CLI itself does exactly this).
1103///
1104/// Note: the `result` frame's usage covers the **main agent only** — the
1105/// subagent (`Task` / sidechain) token rollup the CLI renders as
1106/// `<subagent_tokens>` / `<agent_count>` is not carried here or anywhere
1107/// else on the wire. Accumulate it from `Task` tool results with
1108/// [`SubagentUsageRollup`](crate::SubagentUsageRollup).
1109#[derive(Debug, Clone, Serialize, Deserialize)]
1110pub struct UsageInfo {
1111    /// Sum of fresh (uncached) input tokens across all iterations of the turn.
1112    #[serde(default)]
1113    pub input_tokens: u32,
1114    /// Sum of cache-write input tokens across all iterations of the turn.
1115    #[serde(default)]
1116    pub cache_creation_input_tokens: u32,
1117    /// Sum of cache-read input tokens across all iterations of the turn.
1118    /// Re-counts the cached context every iteration — **not** a measure of
1119    /// context occupancy; see the type-level docs.
1120    #[serde(default)]
1121    pub cache_read_input_tokens: u32,
1122    /// Sum of output tokens across all iterations of the turn.
1123    #[serde(default)]
1124    pub output_tokens: u32,
1125    #[serde(default)]
1126    pub server_tool_use: ServerToolUse,
1127    #[serde(default)]
1128    pub service_tier: String,
1129
1130    /// Cache creation breakdown
1131    #[serde(skip_serializing_if = "Option::is_none")]
1132    pub cache_creation: Option<super::message_types::CacheCreationDetails>,
1133
1134    /// Inference geography (e.g., "not_available")
1135    #[serde(skip_serializing_if = "Option::is_none")]
1136    pub inference_geo: Option<String>,
1137
1138    /// Per-iteration usage breakdown **within** this turn — one entry per
1139    /// API call in the tool-use loop, in order. The last entry reflects the
1140    /// final API call and is what the CLI reads for context-size estimates.
1141    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1142    pub iterations: Vec<UsageIteration>,
1143
1144    /// Speed tier (e.g., "standard")
1145    #[serde(skip_serializing_if = "Option::is_none")]
1146    pub speed: Option<String>,
1147
1148    /// Output-token breakdown (CLI 2.1.232+): currently the thinking-token
1149    /// share of `output_tokens`.
1150    #[serde(skip_serializing_if = "Option::is_none")]
1151    pub output_tokens_details: Option<OutputTokensDetails>,
1152
1153    /// Outcome of a fallback-credit token the request presented; `null` on
1154    /// the wire when it presented none (CLI 2.1.285+).
1155    #[serde(default, skip_serializing_if = "Option::is_none")]
1156    pub fallback_credit: Option<Value>,
1157}
1158
1159/// Breakdown of a turn's output tokens, carried in
1160/// [`UsageInfo::output_tokens_details`].
1161#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1162pub struct OutputTokensDetails {
1163    /// Output tokens spent on extended thinking.
1164    #[serde(skip_serializing_if = "Option::is_none")]
1165    pub thinking_tokens: Option<u64>,
1166}
1167
1168/// Usage for a single API call ("iteration") within a turn's tool-use loop.
1169///
1170/// Carried in [`UsageInfo::iterations`]. The cache fields are optional on
1171/// the wire: some frames carry only `input_tokens` + `output_tokens`
1172/// (observed with `type: "turn"`), while others carry the full cache
1173/// breakdown (observed with `type: "message"` in captured subagent
1174/// sessions). The CLI computes its context estimate from
1175/// `input_tokens + output_tokens` of the **last** iteration.
1176#[derive(Debug, Clone, Serialize, Deserialize)]
1177pub struct UsageIteration {
1178    #[serde(default)]
1179    pub input_tokens: u32,
1180    #[serde(default)]
1181    pub output_tokens: u32,
1182    /// Cache-read input tokens for this iteration, when carried.
1183    #[serde(default, skip_serializing_if = "Option::is_none")]
1184    pub cache_read_input_tokens: Option<u32>,
1185    /// Cache-write input tokens for this iteration, when carried.
1186    #[serde(default, skip_serializing_if = "Option::is_none")]
1187    pub cache_creation_input_tokens: Option<u32>,
1188    /// Cache-write breakdown by TTL, when carried.
1189    #[serde(default, skip_serializing_if = "Option::is_none")]
1190    pub cache_creation: Option<super::message_types::CacheCreationDetails>,
1191    /// Iteration kind; `"turn"` and `"message"` observed on the wire.
1192    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
1193    pub kind: Option<String>,
1194}
1195
1196/// Server tool usage information
1197#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1198pub struct ServerToolUse {
1199    #[serde(default)]
1200    pub web_search_requests: u32,
1201    /// Number of web fetch requests made
1202    #[serde(default)]
1203    pub web_fetch_requests: u32,
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208    use super::*;
1209    use crate::io::ClaudeOutput;
1210
1211    #[test]
1212    fn test_deserialize_result_message() {
1213        let json = r#"{
1214            "type": "result",
1215            "subtype": "success",
1216            "is_error": false,
1217            "duration_ms": 100,
1218            "duration_api_ms": 200,
1219            "num_turns": 1,
1220            "result": "Done",
1221            "session_id": "123",
1222            "total_cost_usd": 0.01,
1223            "permission_denials": []
1224        }"#;
1225
1226        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1227        assert!(!output.is_error());
1228    }
1229
1230    #[test]
1231    fn test_result_subtype_new_and_unknown_values_do_not_fail() {
1232        let json = r#"{
1233            "type": "result",
1234            "subtype": "error_max_budget_usd",
1235            "is_error": true,
1236            "duration_ms": 100,
1237            "duration_api_ms": 200,
1238            "num_turns": 1,
1239            "session_id": "123",
1240            "total_cost_usd": 0.01
1241        }"#;
1242
1243        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1244        let ClaudeOutput::Result(result) = output else {
1245            panic!("Expected Result");
1246        };
1247        assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
1248
1249        let json = r#"{
1250            "type": "result",
1251            "subtype": "future_result_subtype",
1252            "is_error": true,
1253            "duration_ms": 100,
1254            "duration_api_ms": 200,
1255            "num_turns": 1,
1256            "session_id": "123",
1257            "total_cost_usd": 0.01
1258        }"#;
1259
1260        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1261        let ClaudeOutput::Result(result) = output else {
1262            panic!("Expected Result");
1263        };
1264        assert_eq!(
1265            result.subtype,
1266            ResultSubtype::Unknown("future_result_subtype".to_string())
1267        );
1268    }
1269
1270    #[test]
1271    fn test_deserialize_result_with_permission_denials() {
1272        let json = r#"{
1273            "type": "result",
1274            "subtype": "success",
1275            "is_error": false,
1276            "duration_ms": 100,
1277            "duration_api_ms": 200,
1278            "num_turns": 2,
1279            "result": "Done",
1280            "session_id": "123",
1281            "total_cost_usd": 0.01,
1282            "permission_denials": [
1283                {
1284                    "tool_name": "Bash",
1285                    "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1286                    "tool_use_id": "toolu_123"
1287                }
1288            ]
1289        }"#;
1290
1291        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1292        if let ClaudeOutput::Result(result) = output {
1293            assert_eq!(result.permission_denials.len(), 1);
1294            assert_eq!(result.permission_denials[0].tool_name, "Bash");
1295            assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1296            assert_eq!(
1297                result.permission_denials[0]
1298                    .tool_input
1299                    .get("command")
1300                    .unwrap(),
1301                "rm -rf /"
1302            );
1303        } else {
1304            panic!("Expected Result");
1305        }
1306    }
1307
1308    #[test]
1309    fn test_permission_denial_roundtrip() {
1310        let denial = PermissionDenial {
1311            tool_name: "Write".to_string(),
1312            tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1313            tool_use_id: "toolu_456".to_string(),
1314        };
1315
1316        let json = serde_json::to_string(&denial).unwrap();
1317        assert!(json.contains("\"tool_name\":\"Write\""));
1318        assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1319        assert!(json.contains("/etc/passwd"));
1320
1321        let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1322        assert_eq!(parsed, denial);
1323    }
1324
1325    #[test]
1326    fn test_deserialize_result_message_with_errors() {
1327        let json = r#"{
1328            "type": "result",
1329            "subtype": "error_during_execution",
1330            "duration_ms": 0,
1331            "duration_api_ms": 0,
1332            "is_error": true,
1333            "num_turns": 0,
1334            "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
1335            "total_cost_usd": 0,
1336            "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
1337            "permission_denials": []
1338        }"#;
1339
1340        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1341        assert!(output.is_error());
1342
1343        if let ClaudeOutput::Result(res) = output {
1344            assert!(res.is_error);
1345            assert_eq!(res.errors.len(), 1);
1346            assert!(res.errors[0].contains("No conversation found"));
1347        } else {
1348            panic!("Expected Result message");
1349        }
1350    }
1351
1352    #[test]
1353    fn test_deserialize_result_message_errors_defaults_empty() {
1354        let json = r#"{
1355            "type": "result",
1356            "subtype": "success",
1357            "is_error": false,
1358            "duration_ms": 100,
1359            "duration_api_ms": 200,
1360            "num_turns": 1,
1361            "session_id": "123",
1362            "total_cost_usd": 0.01
1363        }"#;
1364
1365        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1366        if let ClaudeOutput::Result(res) = output {
1367            assert!(res.errors.is_empty());
1368        } else {
1369            panic!("Expected Result message");
1370        }
1371    }
1372
1373    #[test]
1374    fn test_result_message_errors_roundtrip() {
1375        let json = r#"{
1376            "type": "result",
1377            "subtype": "error_during_execution",
1378            "is_error": true,
1379            "duration_ms": 0,
1380            "duration_api_ms": 0,
1381            "num_turns": 0,
1382            "session_id": "test-session",
1383            "total_cost_usd": 0.0,
1384            "errors": ["Error 1", "Error 2"]
1385        }"#;
1386
1387        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1388        let reserialized = serde_json::to_string(&output).unwrap();
1389
1390        assert!(reserialized.contains("Error 1"));
1391        assert!(reserialized.contains("Error 2"));
1392    }
1393
1394    #[test]
1395    fn test_result_with_new_fields() {
1396        let json = r#"{
1397            "type": "result",
1398            "subtype": "success",
1399            "is_error": false,
1400            "duration_ms": 5000,
1401            "duration_api_ms": 4500,
1402            "num_turns": 1,
1403            "result": "Done",
1404            "session_id": "abc",
1405            "total_cost_usd": 0.06,
1406            "api_error_status": null,
1407            "stop_reason": "end_turn",
1408            "terminal_reason": "completed",
1409            "fast_mode_state": "off",
1410            "modelUsage": {
1411                "claude-opus-4-7[1m]": {
1412                    "inputTokens": 3817,
1413                    "outputTokens": 14,
1414                    "costUSD": 0.06
1415                }
1416            },
1417            "usage": {
1418                "input_tokens": 3817,
1419                "output_tokens": 14,
1420                "cache_creation_input_tokens": 3540,
1421                "cache_read_input_tokens": 0,
1422                "server_tool_use": {
1423                    "web_search_requests": 0,
1424                    "web_fetch_requests": 2
1425                },
1426                "service_tier": "standard",
1427                "inference_geo": "not_available",
1428                "speed": "standard",
1429                "iterations": [
1430                    {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
1431                ]
1432            }
1433        }"#;
1434
1435        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1436        if let ClaudeOutput::Result(res) = output {
1437            assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
1438            assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
1439            assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
1440            let model_usage = res.model_usage.as_ref().unwrap();
1441            let entry = model_usage
1442                .get("claude-opus-4-7[1m]")
1443                .expect("per-model entry present");
1444            assert_eq!(entry.input_tokens, 3817);
1445            assert_eq!(entry.output_tokens, 14);
1446            assert_eq!(entry.cost_usd, 0.06);
1447            assert!(res.api_error_status.is_none());
1448
1449            let usage = res.usage.unwrap();
1450            assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
1451            assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1452            assert_eq!(usage.speed.as_deref(), Some("standard"));
1453            assert_eq!(usage.iterations.len(), 1);
1454            assert_eq!(usage.iterations[0].input_tokens, 3817);
1455            assert_eq!(usage.iterations[0].output_tokens, 14);
1456            assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
1457        } else {
1458            panic!("Expected Result");
1459        }
1460    }
1461
1462    #[test]
1463    fn test_result_backwards_compatible_without_new_fields() {
1464        // Verify old-format messages still parse fine
1465        let json = r#"{
1466            "type": "result",
1467            "subtype": "success",
1468            "is_error": false,
1469            "duration_ms": 100,
1470            "duration_api_ms": 200,
1471            "num_turns": 1,
1472            "session_id": "abc",
1473            "total_cost_usd": 0.01
1474        }"#;
1475
1476        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1477        if let ClaudeOutput::Result(res) = output {
1478            assert!(res.api_error_status.is_none());
1479            assert!(res.stop_reason.is_none());
1480            assert!(res.terminal_reason.is_none());
1481            assert!(res.fast_mode_state.is_none());
1482            assert!(res.model_usage.is_none());
1483        } else {
1484            panic!("Expected Result");
1485        }
1486    }
1487
1488    #[test]
1489    fn test_result_fast_mode_disabled_reason() {
1490        let json = r#"{
1491            "type":"result","subtype":"success","is_error":false,
1492            "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1493            "session_id":"s1","total_cost_usd":0.01,
1494            "fast_mode_state":"off",
1495            "fast_mode_disabled_reason":"sdk_opt_in_required"
1496        }"#;
1497        let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1498        let crate::ClaudeOutput::Result(res) = &output else {
1499            panic!("expected Result");
1500        };
1501        assert_eq!(
1502            res.fast_mode_disabled_reason,
1503            Some(FastModeDisabledReason::SdkOptInRequired)
1504        );
1505        assert!(serde_json::to_string(&output)
1506            .unwrap()
1507            .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
1508
1509        // Unknown reasons survive decode and round-trip verbatim; the wire
1510        // literal "unknown" maps to the typed UnknownReason, not the fallback.
1511        assert_eq!(
1512            FastModeDisabledReason::from("unknown"),
1513            FastModeDisabledReason::UnknownReason
1514        );
1515        let novel = FastModeDisabledReason::from("solar_flare");
1516        assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
1517        assert_eq!(novel.as_str(), "solar_flare");
1518    }
1519
1520    #[test]
1521    fn test_result_timing_and_user_message_uuid_fields() {
1522        let json = r#"{
1523            "type":"result","subtype":"success","is_error":false,
1524            "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1525            "session_id":"s1","total_cost_usd":0.01,
1526            "request_sent_wall_ms":1753212345678.25,
1527            "user_message_uuid":"um-1"
1528        }"#;
1529        let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1530        let crate::ClaudeOutput::Result(res) = &output else {
1531            panic!("expected Result");
1532        };
1533        assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
1534        assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
1535
1536        let reserialized = serde_json::to_string(&output).unwrap();
1537        assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
1538    }
1539
1540    #[test]
1541    fn test_result_queued_turn_count_and_user_message_uuids() {
1542        // CLI 2.1.259 additive fields on the result frame.
1543        let json = r#"{
1544            "type":"result","subtype":"success","is_error":false,
1545            "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1546            "session_id":"s1","total_cost_usd":0.01,
1547            "user_message_uuid":"um-2",
1548            "user_message_uuids":["um-1","um-2"],
1549            "queued_turn_count":3
1550        }"#;
1551        let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1552        let crate::ClaudeOutput::Result(res) = &output else {
1553            panic!("expected Result");
1554        };
1555        assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
1556        assert_eq!(res.queued_turn_count, Some(3));
1557
1558        let reserialized = serde_json::to_string(&output).unwrap();
1559        assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
1560        assert!(reserialized.contains("\"queued_turn_count\":3"));
1561
1562        // Both fields are absent from older producers and must default cleanly.
1563        let old = r#"{
1564            "type":"result","subtype":"success","is_error":false,
1565            "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1566            "session_id":"s1","total_cost_usd":0.01
1567        }"#;
1568        let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
1569        let crate::ClaudeOutput::Result(res) = &output else {
1570            panic!("expected Result");
1571        };
1572        assert!(res.user_message_uuids.is_empty());
1573        assert_eq!(res.queued_turn_count, None);
1574        let reserialized = serde_json::to_string(&output).unwrap();
1575        assert!(!reserialized.contains("user_message_uuids"));
1576        assert!(!reserialized.contains("queued_turn_count"));
1577    }
1578}