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