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