Skip to main content

claude_codes/io/
message_types.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4use uuid::Uuid;
5
6use super::claude_output::ClaudeOutput;
7use super::content_blocks::{deserialize_content_blocks, ContentBlock};
8
9/// Known system message subtypes.
10///
11/// The Claude CLI emits system messages with a `subtype` field indicating what
12/// kind of system event occurred. This enum captures the known subtypes while
13/// preserving unknown values via the `Unknown` variant for forward compatibility.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub enum SystemSubtype {
16    Init,
17    Status,
18    CompactBoundary,
19    ThinkingTokens,
20    TaskStarted,
21    TaskProgress,
22    TaskUpdated,
23    TaskNotification,
24    ApiRetry,
25    ControlRequestProgress,
26    ModelRefusalFallback,
27    ModelRefusalNoFallback,
28    LocalCommandOutput,
29    HookStarted,
30    HookProgress,
31    HookResponse,
32    PluginInstall,
33    BackgroundTasksChanged,
34    SessionStateChanged,
35    WorkerShuttingDown,
36    CommandsChanged,
37    Notification,
38    FilesPersisted,
39    MemoryRecall,
40    ElicitationComplete,
41    PermissionDenied,
42    MirrorError,
43    Informational,
44    CodeChangePublished,
45    VcsStateChanged,
46    FeedbackDraftQueued,
47    CloudSessionDelta,
48    DevIntent,
49    /// A subtype not yet known to this version of the crate.
50    Unknown(String),
51}
52
53impl SystemSubtype {
54    pub fn as_str(&self) -> &str {
55        match self {
56            Self::Init => "init",
57            Self::Status => "status",
58            Self::CompactBoundary => "compact_boundary",
59            Self::ThinkingTokens => "thinking_tokens",
60            Self::TaskStarted => "task_started",
61            Self::TaskProgress => "task_progress",
62            Self::TaskUpdated => "task_updated",
63            Self::TaskNotification => "task_notification",
64            Self::ApiRetry => "api_retry",
65            Self::ControlRequestProgress => "control_request_progress",
66            Self::ModelRefusalFallback => "model_refusal_fallback",
67            Self::ModelRefusalNoFallback => "model_refusal_no_fallback",
68            Self::LocalCommandOutput => "local_command_output",
69            Self::HookStarted => "hook_started",
70            Self::HookProgress => "hook_progress",
71            Self::HookResponse => "hook_response",
72            Self::PluginInstall => "plugin_install",
73            Self::BackgroundTasksChanged => "background_tasks_changed",
74            Self::SessionStateChanged => "session_state_changed",
75            Self::WorkerShuttingDown => "worker_shutting_down",
76            Self::CommandsChanged => "commands_changed",
77            Self::Notification => "notification",
78            Self::FilesPersisted => "files_persisted",
79            Self::MemoryRecall => "memory_recall",
80            Self::ElicitationComplete => "elicitation_complete",
81            Self::PermissionDenied => "permission_denied",
82            Self::MirrorError => "mirror_error",
83            Self::Informational => "informational",
84            Self::CodeChangePublished => "code_change_published",
85            Self::VcsStateChanged => "vcs_state_changed",
86            Self::FeedbackDraftQueued => "feedback_draft_queued",
87            Self::CloudSessionDelta => "cloud_session_delta",
88            Self::DevIntent => "dev_intent",
89            Self::Unknown(s) => s.as_str(),
90        }
91    }
92}
93
94impl fmt::Display for SystemSubtype {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.write_str(self.as_str())
97    }
98}
99
100impl From<&str> for SystemSubtype {
101    fn from(s: &str) -> Self {
102        match s {
103            "init" => Self::Init,
104            "status" => Self::Status,
105            "compact_boundary" => Self::CompactBoundary,
106            "thinking_tokens" => Self::ThinkingTokens,
107            "task_started" => Self::TaskStarted,
108            "task_progress" => Self::TaskProgress,
109            "task_updated" => Self::TaskUpdated,
110            "task_notification" => Self::TaskNotification,
111            "api_retry" => Self::ApiRetry,
112            "control_request_progress" => Self::ControlRequestProgress,
113            "model_refusal_fallback" => Self::ModelRefusalFallback,
114            "model_refusal_no_fallback" => Self::ModelRefusalNoFallback,
115            "local_command_output" => Self::LocalCommandOutput,
116            "hook_started" => Self::HookStarted,
117            "hook_progress" => Self::HookProgress,
118            "hook_response" => Self::HookResponse,
119            "plugin_install" => Self::PluginInstall,
120            "background_tasks_changed" => Self::BackgroundTasksChanged,
121            "session_state_changed" => Self::SessionStateChanged,
122            "worker_shutting_down" => Self::WorkerShuttingDown,
123            "commands_changed" => Self::CommandsChanged,
124            "notification" => Self::Notification,
125            "files_persisted" => Self::FilesPersisted,
126            "memory_recall" => Self::MemoryRecall,
127            "elicitation_complete" => Self::ElicitationComplete,
128            "permission_denied" => Self::PermissionDenied,
129            "mirror_error" => Self::MirrorError,
130            "informational" => Self::Informational,
131            "code_change_published" => Self::CodeChangePublished,
132            "vcs_state_changed" => Self::VcsStateChanged,
133            "feedback_draft_queued" => Self::FeedbackDraftQueued,
134            "cloud_session_delta" => Self::CloudSessionDelta,
135            "dev_intent" => Self::DevIntent,
136            other => Self::Unknown(other.to_string()),
137        }
138    }
139}
140
141impl Serialize for SystemSubtype {
142    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
143        serializer.serialize_str(self.as_str())
144    }
145}
146
147impl<'de> Deserialize<'de> for SystemSubtype {
148    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
149        let s = String::deserialize(deserializer)?;
150        Ok(Self::from(s.as_str()))
151    }
152}
153
154/// Known message roles.
155///
156/// Used in `MessageContent` and `AssistantMessageContent` to indicate the
157/// speaker of a message.
158#[derive(Debug, Clone, PartialEq, Eq, Hash)]
159pub enum MessageRole {
160    User,
161    Assistant,
162    /// A role not yet known to this version of the crate.
163    Unknown(String),
164}
165
166impl MessageRole {
167    pub fn as_str(&self) -> &str {
168        match self {
169            Self::User => "user",
170            Self::Assistant => "assistant",
171            Self::Unknown(s) => s.as_str(),
172        }
173    }
174}
175
176impl fmt::Display for MessageRole {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        f.write_str(self.as_str())
179    }
180}
181
182impl From<&str> for MessageRole {
183    fn from(s: &str) -> Self {
184        match s {
185            "user" => Self::User,
186            "assistant" => Self::Assistant,
187            other => Self::Unknown(other.to_string()),
188        }
189    }
190}
191
192impl Serialize for MessageRole {
193    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
194        serializer.serialize_str(self.as_str())
195    }
196}
197
198impl<'de> Deserialize<'de> for MessageRole {
199    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
200        let s = String::deserialize(deserializer)?;
201        Ok(Self::from(s.as_str()))
202    }
203}
204
205/// What triggered a context compaction.
206#[derive(Debug, Clone, PartialEq, Eq, Hash)]
207pub enum CompactionTrigger {
208    /// Automatic compaction triggered by token limit.
209    Auto,
210    /// User-initiated compaction (e.g., /compact command).
211    Manual,
212    /// A trigger not yet known to this version of the crate.
213    Unknown(String),
214}
215
216impl CompactionTrigger {
217    pub fn as_str(&self) -> &str {
218        match self {
219            Self::Auto => "auto",
220            Self::Manual => "manual",
221            Self::Unknown(s) => s.as_str(),
222        }
223    }
224}
225
226impl fmt::Display for CompactionTrigger {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        f.write_str(self.as_str())
229    }
230}
231
232impl From<&str> for CompactionTrigger {
233    fn from(s: &str) -> Self {
234        match s {
235            "auto" => Self::Auto,
236            "manual" => Self::Manual,
237            other => Self::Unknown(other.to_string()),
238        }
239    }
240}
241
242impl Serialize for CompactionTrigger {
243    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
244        serializer.serialize_str(self.as_str())
245    }
246}
247
248impl<'de> Deserialize<'de> for CompactionTrigger {
249    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
250        let s = String::deserialize(deserializer)?;
251        Ok(Self::from(s.as_str()))
252    }
253}
254
255/// Reason why the assistant stopped generating.
256#[derive(Debug, Clone, PartialEq, Eq, Hash)]
257pub enum StopReason {
258    /// The assistant reached a natural end of its turn.
259    EndTurn,
260    /// The response hit the maximum token limit.
261    MaxTokens,
262    /// The assistant wants to use a tool.
263    ToolUse,
264    /// A stop reason not yet known to this version of the crate.
265    Unknown(String),
266}
267
268impl StopReason {
269    pub fn as_str(&self) -> &str {
270        match self {
271            Self::EndTurn => "end_turn",
272            Self::MaxTokens => "max_tokens",
273            Self::ToolUse => "tool_use",
274            Self::Unknown(s) => s.as_str(),
275        }
276    }
277}
278
279impl fmt::Display for StopReason {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        f.write_str(self.as_str())
282    }
283}
284
285impl From<&str> for StopReason {
286    fn from(s: &str) -> Self {
287        match s {
288            "end_turn" => Self::EndTurn,
289            "max_tokens" => Self::MaxTokens,
290            "tool_use" => Self::ToolUse,
291            other => Self::Unknown(other.to_string()),
292        }
293    }
294}
295
296impl Serialize for StopReason {
297    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
298        serializer.serialize_str(self.as_str())
299    }
300}
301
302impl<'de> Deserialize<'de> for StopReason {
303    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
304        let s = String::deserialize(deserializer)?;
305        Ok(Self::from(s.as_str()))
306    }
307}
308
309/// How the API key was sourced for the session.
310#[derive(Debug, Clone, PartialEq, Eq, Hash)]
311pub enum ApiKeySource {
312    /// No API key provided.
313    None,
314    User,
315    Project,
316    Org,
317    Temporary,
318    Oauth,
319    /// A source not yet known to this version of the crate.
320    Unknown(String),
321}
322
323impl ApiKeySource {
324    pub fn as_str(&self) -> &str {
325        match self {
326            Self::None => "none",
327            Self::User => "user",
328            Self::Project => "project",
329            Self::Org => "org",
330            Self::Temporary => "temporary",
331            Self::Oauth => "oauth",
332            Self::Unknown(s) => s.as_str(),
333        }
334    }
335}
336
337impl fmt::Display for ApiKeySource {
338    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339        f.write_str(self.as_str())
340    }
341}
342
343impl From<&str> for ApiKeySource {
344    fn from(s: &str) -> Self {
345        match s {
346            "none" => Self::None,
347            "user" => Self::User,
348            "project" => Self::Project,
349            "org" => Self::Org,
350            "temporary" => Self::Temporary,
351            "oauth" => Self::Oauth,
352            other => Self::Unknown(other.to_string()),
353        }
354    }
355}
356
357impl Serialize for ApiKeySource {
358    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
359        serializer.serialize_str(self.as_str())
360    }
361}
362
363impl<'de> Deserialize<'de> for ApiKeySource {
364    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
365        let s = String::deserialize(deserializer)?;
366        Ok(Self::from(s.as_str()))
367    }
368}
369
370/// Output formatting style for the session.
371#[derive(Debug, Clone, PartialEq, Eq, Hash)]
372pub enum OutputStyle {
373    /// Default output style.
374    Default,
375    /// A style not yet known to this version of the crate.
376    Unknown(String),
377}
378
379impl OutputStyle {
380    pub fn as_str(&self) -> &str {
381        match self {
382            Self::Default => "default",
383            Self::Unknown(s) => s.as_str(),
384        }
385    }
386}
387
388impl fmt::Display for OutputStyle {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        f.write_str(self.as_str())
391    }
392}
393
394impl From<&str> for OutputStyle {
395    fn from(s: &str) -> Self {
396        match s {
397            "default" => Self::Default,
398            other => Self::Unknown(other.to_string()),
399        }
400    }
401}
402
403impl Serialize for OutputStyle {
404    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
405        serializer.serialize_str(self.as_str())
406    }
407}
408
409impl<'de> Deserialize<'de> for OutputStyle {
410    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
411        let s = String::deserialize(deserializer)?;
412        Ok(Self::from(s.as_str()))
413    }
414}
415
416/// Permission mode reported in init messages.
417#[derive(Debug, Clone, PartialEq, Eq, Hash)]
418pub enum InitPermissionMode {
419    /// Default permission mode.
420    Default,
421    AcceptEdits,
422    BypassPermissions,
423    Plan,
424    DontAsk,
425    Auto,
426    /// A mode not yet known to this version of the crate.
427    Unknown(String),
428}
429
430impl InitPermissionMode {
431    pub fn as_str(&self) -> &str {
432        match self {
433            Self::Default => "default",
434            Self::AcceptEdits => "acceptEdits",
435            Self::BypassPermissions => "bypassPermissions",
436            Self::Plan => "plan",
437            Self::DontAsk => "dontAsk",
438            Self::Auto => "auto",
439            Self::Unknown(s) => s.as_str(),
440        }
441    }
442}
443
444impl fmt::Display for InitPermissionMode {
445    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446        f.write_str(self.as_str())
447    }
448}
449
450impl From<&str> for InitPermissionMode {
451    fn from(s: &str) -> Self {
452        match s {
453            "default" => Self::Default,
454            "acceptEdits" => Self::AcceptEdits,
455            "bypassPermissions" => Self::BypassPermissions,
456            "plan" => Self::Plan,
457            "dontAsk" => Self::DontAsk,
458            "auto" => Self::Auto,
459            other => Self::Unknown(other.to_string()),
460        }
461    }
462}
463
464impl Serialize for InitPermissionMode {
465    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
466        serializer.serialize_str(self.as_str())
467    }
468}
469
470impl<'de> Deserialize<'de> for InitPermissionMode {
471    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
472        let s = String::deserialize(deserializer)?;
473        Ok(Self::from(s.as_str()))
474    }
475}
476
477/// Status of an ongoing operation (e.g., context compaction).
478#[derive(Debug, Clone, PartialEq, Eq, Hash)]
479pub enum StatusMessageStatus {
480    /// Context compaction is in progress.
481    Compacting,
482    /// The CLI is issuing a request.
483    Requesting,
484    /// A status not yet known to this version of the crate.
485    Unknown(String),
486}
487
488impl StatusMessageStatus {
489    pub fn as_str(&self) -> &str {
490        match self {
491            Self::Compacting => "compacting",
492            Self::Requesting => "requesting",
493            Self::Unknown(s) => s.as_str(),
494        }
495    }
496}
497
498impl fmt::Display for StatusMessageStatus {
499    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500        f.write_str(self.as_str())
501    }
502}
503
504impl From<&str> for StatusMessageStatus {
505    fn from(s: &str) -> Self {
506        match s {
507            "compacting" => Self::Compacting,
508            "requesting" => Self::Requesting,
509            other => Self::Unknown(other.to_string()),
510        }
511    }
512}
513
514impl Serialize for StatusMessageStatus {
515    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
516        serializer.serialize_str(self.as_str())
517    }
518}
519
520impl<'de> Deserialize<'de> for StatusMessageStatus {
521    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
522        let s = String::deserialize(deserializer)?;
523        Ok(Self::from(s.as_str()))
524    }
525}
526
527/// Serialize an optional UUID as a string
528pub(crate) fn serialize_optional_uuid<S>(
529    uuid: &Option<Uuid>,
530    serializer: S,
531) -> Result<S::Ok, S::Error>
532where
533    S: Serializer,
534{
535    match uuid {
536        Some(id) => serializer.serialize_str(&id.to_string()),
537        None => serializer.serialize_none(),
538    }
539}
540
541/// Deserialize an optional UUID from a string
542pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
543where
544    D: Deserializer<'de>,
545{
546    let opt_str: Option<String> = Option::deserialize(deserializer)?;
547    match opt_str {
548        Some(s) => Uuid::parse_str(&s)
549            .map(Some)
550            .map_err(serde::de::Error::custom),
551        None => Ok(None),
552    }
553}
554
555/// Message provenance. The `kind` field is the stable discriminator; variant
556/// specific fields are preserved in `extra` for forward-compatible access.
557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
558pub struct MessageOrigin {
559    pub kind: String,
560    #[serde(flatten)]
561    pub extra: serde_json::Map<String, Value>,
562}
563
564/// Metadata attached when user-visible transcript content summarizes prior messages.
565#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
566pub struct SummarizeMetadata {
567    pub messages_summarized: u64,
568    #[serde(default, skip_serializing_if = "Option::is_none")]
569    pub user_context: Option<String>,
570    #[serde(default, skip_serializing_if = "Option::is_none")]
571    pub direction: Option<String>,
572}
573
574/// MCP metadata passed through on user-message wrappers.
575#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
576pub struct McpMeta {
577    #[serde(default, skip_serializing_if = "Option::is_none", rename = "_meta")]
578    pub meta: Option<Value>,
579    #[serde(default, skip_serializing_if = "Option::is_none")]
580    pub structured_content: Option<Value>,
581    /// The `resource_link` content blocks the MCP tool returned, collected
582    /// from the raw result before the CLI rewrites each into the
583    /// `[Resource link: NAME] URI` text line the model reads. At most 50
584    /// links and 64 KiB serialized; absent when the result had none.
585    #[serde(default, skip_serializing_if = "Vec::is_empty")]
586    pub resource_links: Vec<ResourceLink>,
587}
588
589/// A file an MCP tool returned by reference — a `resource_link` content block
590/// as carried on [`McpMeta::resource_links`] and
591/// [`TaskNotificationMessage::resource_links`] (CLI 2.1.259+).
592#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
593pub struct ResourceLink {
594    pub uri: String,
595    pub name: String,
596    #[serde(default, skip_serializing_if = "Option::is_none")]
597    pub title: Option<String>,
598    #[serde(default, skip_serializing_if = "Option::is_none")]
599    pub description: Option<String>,
600    #[serde(default, skip_serializing_if = "Option::is_none", rename = "mimeType")]
601    pub mime_type: Option<String>,
602    #[serde(default, skip_serializing_if = "Option::is_none")]
603    pub size: Option<u64>,
604    #[serde(default, skip_serializing_if = "Option::is_none")]
605    pub annotations: Option<Value>,
606}
607
608/// Display metadata for a `tool_result` block carried on the user wrapper.
609#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
610pub struct ToolResultMeta {
611    /// The `tool_use_id` of the matching `tool_result` block.
612    pub id: String,
613    /// Harness-stamped reason an `is_error: true` result did not carry the
614    /// tool's own execution output (`user-rejected`, `permission-rule`,
615    /// `automode-*`, `interrupted`, `cancelled`). Open set — treat
616    /// unrecognized values as valid reasons; absent means the tool ran to
617    /// completion.
618    pub non_execution_kind: String,
619    /// The deny comment a human typed at a permission prompt, when present.
620    #[serde(default, skip_serializing_if = "Option::is_none")]
621    pub user_feedback: Option<String>,
622}
623
624/// User message
625#[derive(Debug, Clone, Serialize, Deserialize)]
626pub struct UserMessage {
627    pub message: MessageContent,
628    #[serde(skip_serializing_if = "Option::is_none", alias = "sessionId")]
629    #[serde(
630        serialize_with = "serialize_optional_uuid",
631        deserialize_with = "deserialize_optional_uuid"
632    )]
633    pub session_id: Option<Uuid>,
634    /// Parent tool use ID for nested agent messages
635    #[serde(skip_serializing_if = "Option::is_none")]
636    pub parent_tool_use_id: Option<String>,
637    /// Message-level unique identifier
638    #[serde(skip_serializing_if = "Option::is_none")]
639    pub uuid: Option<String>,
640    /// CLI-emitted ISO-8601 timestamp for the message (present on echoed tool results).
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub timestamp: Option<String>,
643    /// Structured tool result data echoed by the CLI alongside the `tool_result`
644    /// content block. The shape depends on which tool produced it (e.g. for
645    /// `AskUserQuestion` it is `{ questions, answers }`; for `Bash` it is
646    /// `{ stdout, stderr, exit_code, ... }`). Stored as raw JSON to preserve
647    /// wire fidelity; use [`UserMessage::tool_use_result_as`] to parse into a
648    /// typed shape when you know which tool was invoked.
649    #[serde(skip_serializing_if = "Option::is_none")]
650    pub tool_use_result: Option<serde_json::Value>,
651    /// Subagent type, when this user message is the prompt echoed into a
652    /// `local_agent` subagent (e.g. `general-purpose`).
653    #[serde(skip_serializing_if = "Option::is_none")]
654    pub subagent_type: Option<String>,
655    /// Short description of the subagent task, present alongside `subagent_type`.
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub task_description: Option<String>,
658    #[serde(skip_serializing_if = "Option::is_none")]
659    pub origin: Option<MessageOrigin>,
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub priority: Option<String>,
662    #[serde(skip_serializing_if = "Option::is_none", rename = "isSynthetic")]
663    pub is_synthetic: Option<bool>,
664    #[serde(skip_serializing_if = "Option::is_none", rename = "shouldQuery")]
665    pub should_query: Option<bool>,
666    #[serde(default, skip_serializing_if = "Option::is_none")]
667    pub is_meta: Option<bool>,
668    #[serde(default, skip_serializing_if = "Option::is_none")]
669    pub is_visible_in_transcript_only: Option<bool>,
670    #[serde(default, skip_serializing_if = "Option::is_none")]
671    pub is_virtual: Option<bool>,
672    #[serde(default, skip_serializing_if = "Option::is_none")]
673    pub is_compact_summary: Option<bool>,
674    #[serde(skip_serializing_if = "Option::is_none")]
675    pub summarize_metadata: Option<SummarizeMetadata>,
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub mcp_meta: Option<McpMeta>,
678    /// Display metadata for this message's `tool_result` blocks, keyed by
679    /// `tool_use_id`.
680    #[serde(skip_serializing_if = "Option::is_none")]
681    pub tool_result_meta: Option<Vec<ToolResultMeta>>,
682    #[serde(skip_serializing_if = "Option::is_none")]
683    pub source_tool_use_id: Option<String>,
684    #[serde(skip_serializing_if = "Option::is_none")]
685    pub source_tool_assistant_uuid: Option<String>,
686    #[serde(skip_serializing_if = "Option::is_none")]
687    pub image_paste_ids: Option<Vec<u64>>,
688    #[serde(skip_serializing_if = "Option::is_none")]
689    pub client_platform: Option<String>,
690    #[serde(skip_serializing_if = "Option::is_none")]
691    pub inbound_origin: Option<String>,
692    #[serde(skip_serializing_if = "Option::is_none", rename = "isReplay")]
693    pub is_replay: Option<bool>,
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub file_attachments: Option<Vec<Value>>,
696    /// Desktop host only: the host's own seeded summon (CLI 2.1.239+).
697    #[serde(default, skip_serializing_if = "Option::is_none")]
698    pub seeded_summon: Option<bool>,
699    /// True when the client composed this turn from content the user did not
700    /// type; its text is delivered as written (CLI 2.1.259+).
701    #[serde(default, skip_serializing_if = "Option::is_none")]
702    pub client_composed: Option<bool>,
703    /// Replayed history rather than a live message: the Remote Control
704    /// bridge stamps it on the messages it flushes to the session server,
705    /// which also stamps it on deliveries it replays (CLI 2.1.266+).
706    #[serde(default, skip_serializing_if = "Option::is_none")]
707    pub historical: Option<bool>,
708}
709
710impl UserMessage {
711    /// Parse the `tool_use_result` field into a caller-specified type.
712    ///
713    /// Returns `None` if `tool_use_result` is absent, otherwise returns the
714    /// deserialization result. The caller must know which tool produced the
715    /// result and supply a matching type — e.g. for `AskUserQuestion` use
716    /// [`AskUserQuestionInput`](crate::AskUserQuestionInput), whose
717    /// `questions` + `answers` fields match the wire result shape.
718    pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
719        &self,
720    ) -> Option<Result<T, serde_json::Error>> {
721        self.tool_use_result
722            .as_ref()
723            .map(|v| serde_json::from_value(v.clone()))
724    }
725
726    /// Parse the `tool_use_result` as a subagent (`Task`) run result.
727    ///
728    /// When this user message echoes the result of a `Task` tool call, the CLI
729    /// attaches a structured `tool_use_result` carrying the subagent's token,
730    /// timing, and tool-use accounting. Returns `None` when the field is absent
731    /// or does not parse as a [`SubagentResult`].
732    ///
733    /// Summing [`SubagentResult::total_tokens`] across every `Task` result in a
734    /// session yields the subagent token rollup the CLI renders as
735    /// `subagent_tokens` in its terminal `<usage>` block.
736    pub fn subagent_result(&self) -> Option<SubagentResult> {
737        self.tool_use_result
738            .as_ref()
739            .and_then(|v| serde_json::from_value(v.clone()).ok())
740    }
741}
742
743/// Token, timing, and tool-use accounting for a completed subagent (`Task`) run.
744///
745/// The Claude CLI echoes this object in the `tool_use_result` of a `Task` tool's
746/// result message. It is the typed source of truth for subagent token
747/// attribution: the per-run [`total_tokens`](Self::total_tokens),
748/// [`total_duration_ms`](Self::total_duration_ms), and
749/// [`total_tool_use_count`](Self::total_tool_use_count) correspond to the
750/// `subagent_tokens` / `duration_ms` / `tool_uses` line items the CLI renders in
751/// its human-readable `<usage>` block, and [`usage`](Self::usage) carries the
752/// full per-model token breakdown for the run.
753#[derive(Debug, Clone, Serialize, Deserialize)]
754pub struct SubagentResult {
755    /// Completion status of the subagent run (e.g. `"completed"`).
756    #[serde(skip_serializing_if = "Option::is_none")]
757    pub status: Option<String>,
758    /// The prompt the subagent was launched with.
759    #[serde(skip_serializing_if = "Option::is_none")]
760    pub prompt: Option<String>,
761    /// Stable identifier of the spawned subagent.
762    #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
763    pub agent_id: Option<String>,
764    /// Subagent type that ran (e.g. `general-purpose`, `Explore`).
765    #[serde(rename = "agentType", skip_serializing_if = "Option::is_none")]
766    pub agent_type: Option<String>,
767    /// Final content blocks the subagent returned.
768    #[serde(
769        default,
770        deserialize_with = "deserialize_content_blocks",
771        skip_serializing_if = "Vec::is_empty"
772    )]
773    pub content: Vec<ContentBlock>,
774    /// Model the subagent actually resolved to (e.g. `claude-sonnet-4-6`).
775    #[serde(rename = "resolvedModel", skip_serializing_if = "Option::is_none")]
776    pub resolved_model: Option<String>,
777    /// Wall-clock duration of the subagent run, in milliseconds.
778    #[serde(rename = "totalDurationMs", skip_serializing_if = "Option::is_none")]
779    pub total_duration_ms: Option<u64>,
780    /// Total tokens consumed by the subagent — the `subagent_tokens` rollup line.
781    #[serde(rename = "totalTokens", skip_serializing_if = "Option::is_none")]
782    pub total_tokens: Option<u64>,
783    /// Number of tool invocations the subagent made.
784    #[serde(rename = "totalToolUseCount", skip_serializing_if = "Option::is_none")]
785    pub total_tool_use_count: Option<u64>,
786    /// Detailed token / cache usage for the subagent run.
787    #[serde(skip_serializing_if = "Option::is_none")]
788    pub usage: Option<super::result::UsageInfo>,
789    /// Per-category tool-use counts, present for some agent types (e.g. `Explore`).
790    #[serde(rename = "toolStats", skip_serializing_if = "Option::is_none")]
791    pub tool_stats: Option<SubagentToolStats>,
792}
793
794/// Per-category tool-use counts for a subagent run, from `tool_use_result.toolStats`.
795///
796/// The `extra` field captures any counters the CLI adds that aren't modeled here,
797/// so new wire fields deserialize without error.
798#[derive(Debug, Clone, Default, Serialize, Deserialize)]
799#[serde(rename_all = "camelCase")]
800pub struct SubagentToolStats {
801    #[serde(default)]
802    pub read_count: u64,
803    #[serde(default)]
804    pub search_count: u64,
805    #[serde(default)]
806    pub bash_count: u64,
807    #[serde(default)]
808    pub edit_file_count: u64,
809    #[serde(default)]
810    pub lines_added: u64,
811    #[serde(default)]
812    pub lines_removed: u64,
813    #[serde(default)]
814    pub other_tool_count: u64,
815    #[serde(flatten)]
816    pub extra: serde_json::Map<String, Value>,
817}
818
819/// Session-level subagent token rollup — the `<subagent_tokens>` /
820/// `<agent_count>` line items the Claude CLI renders in its terminal
821/// `<usage>` block.
822///
823/// The `stream-json` protocol does **not** carry this rollup on the `result`
824/// frame's `usage` (confirmed against the CLI binary — the terminal renderer
825/// computes it from `Task` tool results). Consumers that need it must
826/// accumulate it the same way: feed every session message through
827/// [`observe`](Self::observe) and read the totals at any point.
828///
829/// A `Task` result observed twice under the same `agentId` (e.g. a replayed
830/// frame on resume) is counted once. Results with no `agentId` are counted
831/// every time they are observed.
832///
833/// # Example
834///
835/// ```
836/// use claude_codes::{ClaudeOutput, SubagentUsageRollup};
837///
838/// let mut rollup = SubagentUsageRollup::default();
839/// let json = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1","tool_use_result":{"status":"completed","agentId":"ab52f22445470d454","totalDurationMs":1853,"totalTokens":10201,"totalToolUseCount":0}}"#;
840/// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
841/// rollup.observe(&output);
842/// assert_eq!(rollup.subagent_tokens, 10201);
843/// assert_eq!(rollup.agent_count, 1);
844/// ```
845#[derive(Debug, Clone, Default, PartialEq, Eq)]
846pub struct SubagentUsageRollup {
847    /// Total tokens consumed by subagents — sum of
848    /// [`SubagentResult::total_tokens`] over every observed `Task` result.
849    pub subagent_tokens: u64,
850    /// Number of subagent runs observed (`<agent_count>`).
851    pub agent_count: u32,
852    /// Total subagent tool invocations — sum of `total_tool_use_count`.
853    pub tool_uses: u64,
854    /// Total subagent wall-clock milliseconds — sum of `total_duration_ms`.
855    pub duration_ms: u64,
856    seen_agent_ids: std::collections::BTreeSet<String>,
857}
858
859impl SubagentUsageRollup {
860    /// Accumulate `output` into the rollup if it is a `Task` tool result.
861    ///
862    /// Returns `true` when the message contributed to the totals. Non-user
863    /// messages, user messages without a `tool_use_result`, results from
864    /// other tools, and duplicate `agentId`s are all ignored.
865    pub fn observe(&mut self, output: &ClaudeOutput) -> bool {
866        match output {
867            ClaudeOutput::User(user) => self.observe_user(user),
868            _ => false,
869        }
870    }
871
872    /// Accumulate a user message's `Task` tool result, if it carries one.
873    ///
874    /// Every [`SubagentResult`] field is optional, so any JSON object in
875    /// `tool_use_result` parses as one (e.g. a `Bash` or `ToolSearch`
876    /// result). Only results carrying an `agentId` or a `totalTokens`
877    /// line item are treated as genuine `Task` results.
878    pub fn observe_user(&mut self, user: &UserMessage) -> bool {
879        let Some(result) = user.subagent_result() else {
880            return false;
881        };
882        if result.total_tokens.is_none() && result.agent_id.is_none() {
883            return false;
884        }
885        if let Some(agent_id) = &result.agent_id {
886            if !self.seen_agent_ids.insert(agent_id.clone()) {
887                return false;
888            }
889        }
890        self.agent_count += 1;
891        self.subagent_tokens += result.total_tokens.unwrap_or(0);
892        self.tool_uses += result.total_tool_use_count.unwrap_or(0);
893        self.duration_ms += result.total_duration_ms.unwrap_or(0);
894        true
895    }
896}
897
898/// Message content with role
899#[derive(Debug, Clone, Serialize, Deserialize)]
900pub struct MessageContent {
901    pub role: MessageRole,
902    #[serde(deserialize_with = "deserialize_content_blocks")]
903    pub content: Vec<ContentBlock>,
904}
905
906/// System message with metadata
907#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct SystemMessage {
909    pub subtype: SystemSubtype,
910    #[serde(flatten)]
911    pub data: Value, // Captures all other fields
912}
913
914impl SystemMessage {
915    /// Check if this is an init message
916    pub fn is_init(&self) -> bool {
917        self.subtype == SystemSubtype::Init
918    }
919
920    /// Check if this is a status message
921    pub fn is_status(&self) -> bool {
922        self.subtype == SystemSubtype::Status
923    }
924
925    /// Check if this is a compact_boundary message
926    pub fn is_compact_boundary(&self) -> bool {
927        self.subtype == SystemSubtype::CompactBoundary
928    }
929
930    /// Try to parse as an init message
931    pub fn as_init(&self) -> Option<InitMessage> {
932        if self.subtype != SystemSubtype::Init {
933            return None;
934        }
935        serde_json::from_value(self.data.clone()).ok()
936    }
937
938    /// Try to parse as a status message
939    pub fn as_status(&self) -> Option<StatusMessage> {
940        if self.subtype != SystemSubtype::Status {
941            return None;
942        }
943        serde_json::from_value(self.data.clone()).ok()
944    }
945
946    /// Try to parse as a compact_boundary message
947    pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
948        if self.subtype != SystemSubtype::CompactBoundary {
949            return None;
950        }
951        serde_json::from_value(self.data.clone()).ok()
952    }
953
954    /// Check if this is a task_started message
955    pub fn is_task_started(&self) -> bool {
956        self.subtype == SystemSubtype::TaskStarted
957    }
958
959    /// Check if this is a task_progress message
960    pub fn is_task_progress(&self) -> bool {
961        self.subtype == SystemSubtype::TaskProgress
962    }
963
964    /// Check if this is a task_notification message
965    pub fn is_task_notification(&self) -> bool {
966        self.subtype == SystemSubtype::TaskNotification
967    }
968
969    /// Try to parse as a task_started message
970    pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
971        if self.subtype != SystemSubtype::TaskStarted {
972            return None;
973        }
974        serde_json::from_value(self.data.clone()).ok()
975    }
976
977    /// Try to parse as a task_progress message
978    pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
979        if self.subtype != SystemSubtype::TaskProgress {
980            return None;
981        }
982        serde_json::from_value(self.data.clone()).ok()
983    }
984
985    /// Try to parse as a task_notification message
986    pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
987        if self.subtype != SystemSubtype::TaskNotification {
988            return None;
989        }
990        serde_json::from_value(self.data.clone()).ok()
991    }
992
993    /// Check if this is a task_updated message
994    pub fn is_task_updated(&self) -> bool {
995        self.subtype == SystemSubtype::TaskUpdated
996    }
997
998    /// Try to parse as a task_updated message
999    pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
1000        if self.subtype != SystemSubtype::TaskUpdated {
1001            return None;
1002        }
1003        serde_json::from_value(self.data.clone()).ok()
1004    }
1005
1006    /// Check if this is a thinking_tokens message
1007    pub fn is_thinking_tokens(&self) -> bool {
1008        self.subtype == SystemSubtype::ThinkingTokens
1009    }
1010
1011    /// Try to parse as a thinking_tokens message
1012    pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
1013        if self.subtype != SystemSubtype::ThinkingTokens {
1014            return None;
1015        }
1016        serde_json::from_value(self.data.clone()).ok()
1017    }
1018
1019    /// Check if this is a code_change_published message
1020    pub fn is_code_change_published(&self) -> bool {
1021        self.subtype == SystemSubtype::CodeChangePublished
1022    }
1023
1024    /// Try to parse as a code_change_published message
1025    pub fn as_code_change_published(&self) -> Option<CodeChangePublishedMessage> {
1026        if self.subtype != SystemSubtype::CodeChangePublished {
1027            return None;
1028        }
1029        serde_json::from_value(self.data.clone()).ok()
1030    }
1031
1032    /// Check if this is a vcs_state_changed message
1033    pub fn is_vcs_state_changed(&self) -> bool {
1034        self.subtype == SystemSubtype::VcsStateChanged
1035    }
1036
1037    /// Try to parse as a vcs_state_changed message
1038    pub fn as_vcs_state_changed(&self) -> Option<VcsStateChangedMessage> {
1039        if self.subtype != SystemSubtype::VcsStateChanged {
1040            return None;
1041        }
1042        serde_json::from_value(self.data.clone()).ok()
1043    }
1044
1045    /// Check if this is a feedback_draft_queued message.
1046    pub fn is_feedback_draft_queued(&self) -> bool {
1047        self.subtype == SystemSubtype::FeedbackDraftQueued
1048    }
1049
1050    /// Try to parse as a feedback_draft_queued message.
1051    pub fn as_feedback_draft_queued(&self) -> Option<FeedbackDraftQueuedMessage> {
1052        if self.subtype != SystemSubtype::FeedbackDraftQueued {
1053            return None;
1054        }
1055        serde_json::from_value(self.data.clone()).ok()
1056    }
1057
1058    /// Check if this is a cloud_session_delta message.
1059    pub fn is_cloud_session_delta(&self) -> bool {
1060        self.subtype == SystemSubtype::CloudSessionDelta
1061    }
1062
1063    /// Try to parse as a cloud_session_delta message.
1064    pub fn as_cloud_session_delta(&self) -> Option<CloudSessionDeltaMessage> {
1065        if self.subtype != SystemSubtype::CloudSessionDelta {
1066            return None;
1067        }
1068        serde_json::from_value(self.data.clone()).ok()
1069    }
1070
1071    /// Check if this is a dev_intent message.
1072    pub fn is_dev_intent(&self) -> bool {
1073        self.subtype == SystemSubtype::DevIntent
1074    }
1075
1076    /// Try to parse as a dev_intent message.
1077    pub fn as_dev_intent(&self) -> Option<DevIntentMessage> {
1078        if self.subtype != SystemSubtype::DevIntent {
1079            return None;
1080        }
1081        serde_json::from_value(self.data.clone()).ok()
1082    }
1083
1084    /// Parse any typed system subtype known to this crate version.
1085    pub fn as_known_system_event(&self) -> Option<KnownSystemEvent> {
1086        macro_rules! parse {
1087            ($variant:ident, $ty:ty) => {
1088                serde_json::from_value::<$ty>(self.data.clone())
1089                    .ok()
1090                    .map(KnownSystemEvent::$variant)
1091            };
1092        }
1093
1094        match self.subtype {
1095            SystemSubtype::Init => parse!(Init, InitMessage),
1096            SystemSubtype::Status => parse!(Status, StatusMessage),
1097            SystemSubtype::CompactBoundary => parse!(CompactBoundary, CompactBoundaryMessage),
1098            SystemSubtype::ThinkingTokens => parse!(ThinkingTokens, ThinkingTokensMessage),
1099            SystemSubtype::TaskStarted => parse!(TaskStarted, TaskStartedMessage),
1100            SystemSubtype::TaskProgress => parse!(TaskProgress, TaskProgressMessage),
1101            SystemSubtype::TaskUpdated => parse!(TaskUpdated, TaskUpdatedMessage),
1102            SystemSubtype::TaskNotification => parse!(TaskNotification, TaskNotificationMessage),
1103            SystemSubtype::ApiRetry => parse!(ApiRetry, ApiRetryMessage),
1104            SystemSubtype::ControlRequestProgress => {
1105                parse!(ControlRequestProgress, ControlRequestProgressMessage)
1106            }
1107            SystemSubtype::ModelRefusalFallback => {
1108                parse!(ModelRefusalFallback, ModelRefusalFallbackMessage)
1109            }
1110            SystemSubtype::ModelRefusalNoFallback => {
1111                parse!(ModelRefusalNoFallback, ModelRefusalNoFallbackMessage)
1112            }
1113            SystemSubtype::LocalCommandOutput => {
1114                parse!(LocalCommandOutput, LocalCommandOutputMessage)
1115            }
1116            SystemSubtype::HookStarted => parse!(HookStarted, HookStartedMessage),
1117            SystemSubtype::HookProgress => parse!(HookProgress, HookProgressMessage),
1118            SystemSubtype::HookResponse => parse!(HookResponse, HookResponseMessage),
1119            SystemSubtype::PluginInstall => parse!(PluginInstall, PluginInstallMessage),
1120            SystemSubtype::BackgroundTasksChanged => {
1121                parse!(BackgroundTasksChanged, BackgroundTasksChangedMessage)
1122            }
1123            SystemSubtype::SessionStateChanged => {
1124                parse!(SessionStateChanged, SessionStateChangedMessage)
1125            }
1126            SystemSubtype::WorkerShuttingDown => {
1127                parse!(WorkerShuttingDown, WorkerShuttingDownMessage)
1128            }
1129            SystemSubtype::CommandsChanged => parse!(CommandsChanged, CommandsChangedMessage),
1130            SystemSubtype::Notification => parse!(Notification, NotificationMessage),
1131            SystemSubtype::FilesPersisted => parse!(FilesPersisted, FilesPersistedMessage),
1132            SystemSubtype::MemoryRecall => parse!(MemoryRecall, MemoryRecallMessage),
1133            SystemSubtype::ElicitationComplete => {
1134                parse!(ElicitationComplete, ElicitationCompleteMessage)
1135            }
1136            SystemSubtype::PermissionDenied => parse!(PermissionDenied, PermissionDeniedMessage),
1137            SystemSubtype::MirrorError => parse!(MirrorError, MirrorErrorMessage),
1138            SystemSubtype::Informational => parse!(Informational, InformationalMessage),
1139            SystemSubtype::CodeChangePublished => {
1140                parse!(CodeChangePublished, CodeChangePublishedMessage)
1141            }
1142            SystemSubtype::VcsStateChanged => parse!(VcsStateChanged, VcsStateChangedMessage),
1143            SystemSubtype::FeedbackDraftQueued => {
1144                parse!(FeedbackDraftQueued, FeedbackDraftQueuedMessage)
1145            }
1146            SystemSubtype::CloudSessionDelta => {
1147                parse!(CloudSessionDelta, CloudSessionDeltaMessage)
1148            }
1149            SystemSubtype::DevIntent => parse!(DevIntent, DevIntentMessage),
1150            SystemSubtype::Unknown(_) => None,
1151        }
1152    }
1153
1154    /// Re-serialize this system message's payload through the typed view that
1155    /// matches its `subtype`, returning the result as JSON.
1156    ///
1157    /// Used by the wrapping audit ([`crate::io::audit_frame`]) to verify that a
1158    /// subtype's dedicated struct captures every wire field: the audit compares
1159    /// this against the raw [`SystemMessage::data`]. Returns `None` for subtypes
1160    /// this crate version has no dedicated struct for (including
1161    /// [`SystemSubtype::Unknown`]) — those are reported as not fully wrapped.
1162    pub fn typed_value(&self) -> Option<Value> {
1163        fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
1164            parsed.and_then(|v| serde_json::to_value(v).ok())
1165        }
1166        match self.subtype {
1167            SystemSubtype::Init => reserialize(self.as_init()),
1168            SystemSubtype::Status => reserialize(self.as_status()),
1169            SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
1170            SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
1171            SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
1172            SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
1173            SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
1174            SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
1175            SystemSubtype::ApiRetry => reserialize(parse_system::<ApiRetryMessage>(self)),
1176            SystemSubtype::ControlRequestProgress => {
1177                reserialize(parse_system::<ControlRequestProgressMessage>(self))
1178            }
1179            SystemSubtype::ModelRefusalFallback => {
1180                reserialize(parse_system::<ModelRefusalFallbackMessage>(self))
1181            }
1182            SystemSubtype::ModelRefusalNoFallback => {
1183                reserialize(parse_system::<ModelRefusalNoFallbackMessage>(self))
1184            }
1185            SystemSubtype::LocalCommandOutput => {
1186                reserialize(parse_system::<LocalCommandOutputMessage>(self))
1187            }
1188            SystemSubtype::HookStarted => reserialize(parse_system::<HookStartedMessage>(self)),
1189            SystemSubtype::HookProgress => reserialize(parse_system::<HookProgressMessage>(self)),
1190            SystemSubtype::HookResponse => reserialize(parse_system::<HookResponseMessage>(self)),
1191            SystemSubtype::PluginInstall => reserialize(parse_system::<PluginInstallMessage>(self)),
1192            SystemSubtype::BackgroundTasksChanged => {
1193                reserialize(parse_system::<BackgroundTasksChangedMessage>(self))
1194            }
1195            SystemSubtype::SessionStateChanged => {
1196                reserialize(parse_system::<SessionStateChangedMessage>(self))
1197            }
1198            SystemSubtype::WorkerShuttingDown => {
1199                reserialize(parse_system::<WorkerShuttingDownMessage>(self))
1200            }
1201            SystemSubtype::CommandsChanged => {
1202                reserialize(parse_system::<CommandsChangedMessage>(self))
1203            }
1204            SystemSubtype::Notification => reserialize(parse_system::<NotificationMessage>(self)),
1205            SystemSubtype::FilesPersisted => {
1206                reserialize(parse_system::<FilesPersistedMessage>(self))
1207            }
1208            SystemSubtype::MemoryRecall => reserialize(parse_system::<MemoryRecallMessage>(self)),
1209            SystemSubtype::ElicitationComplete => {
1210                reserialize(parse_system::<ElicitationCompleteMessage>(self))
1211            }
1212            SystemSubtype::PermissionDenied => {
1213                reserialize(parse_system::<PermissionDeniedMessage>(self))
1214            }
1215            SystemSubtype::MirrorError => reserialize(parse_system::<MirrorErrorMessage>(self)),
1216            SystemSubtype::Informational => reserialize(parse_system::<InformationalMessage>(self)),
1217            SystemSubtype::CodeChangePublished => {
1218                reserialize(parse_system::<CodeChangePublishedMessage>(self))
1219            }
1220            SystemSubtype::VcsStateChanged => {
1221                reserialize(parse_system::<VcsStateChangedMessage>(self))
1222            }
1223            SystemSubtype::FeedbackDraftQueued => {
1224                reserialize(parse_system::<FeedbackDraftQueuedMessage>(self))
1225            }
1226            SystemSubtype::CloudSessionDelta => {
1227                reserialize(parse_system::<CloudSessionDeltaMessage>(self))
1228            }
1229            SystemSubtype::DevIntent => reserialize(parse_system::<DevIntentMessage>(self)),
1230            SystemSubtype::Unknown(_) => None,
1231        }
1232    }
1233}
1234
1235fn parse_system<T: serde::de::DeserializeOwned>(message: &SystemMessage) -> Option<T> {
1236    serde_json::from_value(message.data.clone()).ok()
1237}
1238
1239/// Owned typed view over any known system message subtype.
1240// `InitMessage` outgrew clippy's variant-size threshold when CLI 2.1.232
1241// added fields. This enum is a transient per-parse classification (never
1242// stored in bulk), so boxing would break every match site for no retained-
1243// memory win.
1244#[allow(clippy::large_enum_variant)]
1245#[derive(Debug, Clone, Serialize, Deserialize)]
1246pub enum KnownSystemEvent {
1247    Init(InitMessage),
1248    Status(StatusMessage),
1249    CompactBoundary(CompactBoundaryMessage),
1250    ThinkingTokens(ThinkingTokensMessage),
1251    TaskStarted(TaskStartedMessage),
1252    TaskProgress(TaskProgressMessage),
1253    TaskUpdated(TaskUpdatedMessage),
1254    TaskNotification(TaskNotificationMessage),
1255    ApiRetry(ApiRetryMessage),
1256    ControlRequestProgress(ControlRequestProgressMessage),
1257    ModelRefusalFallback(ModelRefusalFallbackMessage),
1258    ModelRefusalNoFallback(ModelRefusalNoFallbackMessage),
1259    LocalCommandOutput(LocalCommandOutputMessage),
1260    HookStarted(HookStartedMessage),
1261    HookProgress(HookProgressMessage),
1262    HookResponse(HookResponseMessage),
1263    PluginInstall(PluginInstallMessage),
1264    BackgroundTasksChanged(BackgroundTasksChangedMessage),
1265    SessionStateChanged(SessionStateChangedMessage),
1266    WorkerShuttingDown(WorkerShuttingDownMessage),
1267    CommandsChanged(CommandsChangedMessage),
1268    Notification(NotificationMessage),
1269    FilesPersisted(FilesPersistedMessage),
1270    MemoryRecall(MemoryRecallMessage),
1271    ElicitationComplete(ElicitationCompleteMessage),
1272    PermissionDenied(PermissionDeniedMessage),
1273    MirrorError(MirrorErrorMessage),
1274    Informational(InformationalMessage),
1275    CodeChangePublished(CodeChangePublishedMessage),
1276    VcsStateChanged(VcsStateChangedMessage),
1277    FeedbackDraftQueued(FeedbackDraftQueuedMessage),
1278    CloudSessionDelta(CloudSessionDeltaMessage),
1279    DevIntent(DevIntentMessage),
1280}
1281
1282#[derive(Debug, Clone, Serialize, Deserialize)]
1283pub struct ApiRetryMessage {
1284    pub attempt: u64,
1285    pub max_retries: u64,
1286    pub retry_delay_ms: u64,
1287    pub error_status: Option<u16>,
1288    pub error: String,
1289    /// Present only when the API sent no response headers within the
1290    /// first-byte window (`CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS`). For this
1291    /// cause `max_retries` is its own cap (normally one retry), not the
1292    /// session budget (CLI 2.1.261+).
1293    #[serde(default, skip_serializing_if = "Option::is_none")]
1294    pub no_response: Option<ApiRetryNoResponse>,
1295    #[serde(default, skip_serializing_if = "Option::is_none")]
1296    pub uuid: Option<String>,
1297    #[serde(default, skip_serializing_if = "Option::is_none")]
1298    pub session_id: Option<String>,
1299}
1300
1301/// Timing of a first-byte-timeout retry, carried as
1302/// [`ApiRetryMessage::no_response`].
1303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1304pub struct ApiRetryNoResponse {
1305    /// How long the failed attempt waited for response headers.
1306    pub waited_ms: u64,
1307    /// How long the retry will wait for them.
1308    pub retry_wait_ms: u64,
1309}
1310
1311#[derive(Debug, Clone, Serialize, Deserialize)]
1312pub struct ControlRequestProgressMessage {
1313    pub request_id: String,
1314    pub status: String,
1315    #[serde(default, skip_serializing_if = "Option::is_none")]
1316    pub attempt: Option<u64>,
1317    #[serde(default, skip_serializing_if = "Option::is_none")]
1318    pub max_retries: Option<u64>,
1319    #[serde(default, skip_serializing_if = "Option::is_none")]
1320    pub retry_delay_ms: Option<u64>,
1321    #[serde(default, skip_serializing_if = "Option::is_none")]
1322    pub error_status: Option<u16>,
1323    #[serde(default, skip_serializing_if = "Option::is_none")]
1324    pub error: Option<String>,
1325    #[serde(default, skip_serializing_if = "Option::is_none")]
1326    pub uuid: Option<String>,
1327    #[serde(default, skip_serializing_if = "Option::is_none")]
1328    pub session_id: Option<String>,
1329}
1330
1331#[derive(Debug, Clone, Serialize, Deserialize)]
1332pub struct ModelRefusalFallbackMessage {
1333    pub trigger: String,
1334    pub direction: String,
1335    /// `"session"`: the main thread fell back and the session model is
1336    /// swapped. `"local"`: a subagent / side-question (`/btw`) / background
1337    /// fork fell back — only that response came from the fallback model and
1338    /// the session model is unchanged. Absent from CLIs before 2.1.222
1339    /// (treat as `"session"`).
1340    #[serde(default, skip_serializing_if = "Option::is_none")]
1341    pub scope: Option<RefusalFallbackScope>,
1342    pub original_model: String,
1343    pub fallback_model: String,
1344    pub request_id: Option<String>,
1345    #[serde(default, skip_serializing_if = "Option::is_none")]
1346    pub api_refusal_category: Option<String>,
1347    /// Present when any hop of this banner's multi-hop episode was a cyber
1348    /// refusal — not only the origin hop `api_refusal_category` describes.
1349    /// Re-arm evidence for the CLI's cyber-exclusion header on session
1350    /// restore; absent on cyber-free episodes and older CLIs (2.1.239+).
1351    #[serde(default, skip_serializing_if = "Option::is_none")]
1352    pub saw_cyber_refusal: Option<bool>,
1353    #[serde(default, skip_serializing_if = "Option::is_none")]
1354    pub api_refusal_explanation: Option<String>,
1355    #[serde(default, skip_serializing_if = "Option::is_none")]
1356    pub retracted_message_uuids: Option<Vec<String>>,
1357    #[serde(default, skip_serializing_if = "Option::is_none")]
1358    pub refused_user_message_uuid: Option<String>,
1359    pub content: Value,
1360    #[serde(default, skip_serializing_if = "Option::is_none")]
1361    pub uuid: Option<String>,
1362    #[serde(default, skip_serializing_if = "Option::is_none")]
1363    pub session_id: Option<String>,
1364}
1365
1366/// Scope of a refusal-fallback model swap, carried by
1367/// [`ModelRefusalFallbackMessage::scope`]. Open — new scopes may ship on the
1368/// wire ahead of schema updates.
1369#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1370pub enum RefusalFallbackScope {
1371    /// The main thread fell back; the session model is swapped.
1372    Session,
1373    /// A subagent / side-question / background fork fell back; only that
1374    /// response used the fallback model, the session model is unchanged.
1375    Local,
1376    /// A scope not yet known to this version of the crate.
1377    Unknown(String),
1378}
1379
1380impl RefusalFallbackScope {
1381    pub fn as_str(&self) -> &str {
1382        match self {
1383            Self::Session => "session",
1384            Self::Local => "local",
1385            Self::Unknown(s) => s.as_str(),
1386        }
1387    }
1388}
1389
1390impl fmt::Display for RefusalFallbackScope {
1391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1392        f.write_str(self.as_str())
1393    }
1394}
1395
1396impl From<&str> for RefusalFallbackScope {
1397    fn from(s: &str) -> Self {
1398        match s {
1399            "session" => Self::Session,
1400            "local" => Self::Local,
1401            other => Self::Unknown(other.to_string()),
1402        }
1403    }
1404}
1405
1406impl Serialize for RefusalFallbackScope {
1407    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1408        serializer.serialize_str(self.as_str())
1409    }
1410}
1411
1412impl<'de> Deserialize<'de> for RefusalFallbackScope {
1413    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1414        let s = String::deserialize(deserializer)?;
1415        Ok(Self::from(s.as_str()))
1416    }
1417}
1418
1419#[derive(Debug, Clone, Serialize, Deserialize)]
1420pub struct ModelRefusalNoFallbackMessage {
1421    pub original_model: String,
1422    pub request_id: Option<String>,
1423    #[serde(default, skip_serializing_if = "Option::is_none")]
1424    pub api_refusal_category: Option<String>,
1425    #[serde(default, skip_serializing_if = "Option::is_none")]
1426    pub api_refusal_explanation: Option<String>,
1427    pub content: Value,
1428    #[serde(default, skip_serializing_if = "Option::is_none")]
1429    pub uuid: Option<String>,
1430    #[serde(default, skip_serializing_if = "Option::is_none")]
1431    pub session_id: Option<String>,
1432}
1433
1434#[derive(Debug, Clone, Serialize, Deserialize)]
1435pub struct LocalCommandOutputMessage {
1436    pub content: String,
1437    #[serde(default, skip_serializing_if = "Option::is_none")]
1438    pub uuid: Option<String>,
1439    #[serde(default, skip_serializing_if = "Option::is_none")]
1440    pub session_id: Option<String>,
1441}
1442
1443#[derive(Debug, Clone, Serialize, Deserialize)]
1444pub struct HookStartedMessage {
1445    pub hook_id: String,
1446    pub hook_name: String,
1447    pub hook_event: String,
1448    #[serde(default, skip_serializing_if = "Option::is_none")]
1449    pub uuid: Option<String>,
1450    #[serde(default, skip_serializing_if = "Option::is_none")]
1451    pub session_id: Option<String>,
1452}
1453
1454#[derive(Debug, Clone, Serialize, Deserialize)]
1455pub struct HookProgressMessage {
1456    pub hook_id: String,
1457    pub hook_name: String,
1458    pub hook_event: String,
1459    #[serde(default, skip_serializing_if = "Option::is_none")]
1460    pub stdout: Option<String>,
1461    #[serde(default, skip_serializing_if = "Option::is_none")]
1462    pub stderr: Option<String>,
1463    #[serde(default, skip_serializing_if = "Option::is_none")]
1464    pub output: Option<String>,
1465    #[serde(default, skip_serializing_if = "Option::is_none")]
1466    pub uuid: Option<String>,
1467    #[serde(default, skip_serializing_if = "Option::is_none")]
1468    pub session_id: Option<String>,
1469}
1470
1471#[derive(Debug, Clone, Serialize, Deserialize)]
1472pub struct HookResponseMessage {
1473    pub hook_id: String,
1474    pub hook_name: String,
1475    pub hook_event: String,
1476    #[serde(default, skip_serializing_if = "Option::is_none")]
1477    pub stdout: Option<String>,
1478    #[serde(default, skip_serializing_if = "Option::is_none")]
1479    pub stderr: Option<String>,
1480    #[serde(default, skip_serializing_if = "Option::is_none")]
1481    pub output: Option<String>,
1482    #[serde(default, skip_serializing_if = "Option::is_none")]
1483    pub exit_code: Option<i32>,
1484    pub outcome: String,
1485    #[serde(default, skip_serializing_if = "Option::is_none")]
1486    pub uuid: Option<String>,
1487    #[serde(default, skip_serializing_if = "Option::is_none")]
1488    pub session_id: Option<String>,
1489}
1490
1491#[derive(Debug, Clone, Serialize, Deserialize)]
1492pub struct PluginInstallMessage {
1493    pub status: String,
1494    #[serde(default, skip_serializing_if = "Option::is_none")]
1495    pub name: Option<String>,
1496    #[serde(default, skip_serializing_if = "Option::is_none")]
1497    pub error: Option<String>,
1498    #[serde(default, skip_serializing_if = "Option::is_none")]
1499    pub uuid: Option<String>,
1500    #[serde(default, skip_serializing_if = "Option::is_none")]
1501    pub session_id: Option<String>,
1502}
1503
1504#[derive(Debug, Clone, Serialize, Deserialize)]
1505pub struct BackgroundTasksChangedMessage {
1506    pub tasks: Vec<BackgroundTaskInfo>,
1507    #[serde(default, skip_serializing_if = "Option::is_none")]
1508    pub uuid: Option<String>,
1509    #[serde(default, skip_serializing_if = "Option::is_none")]
1510    pub session_id: Option<String>,
1511}
1512
1513#[derive(Debug, Clone, Serialize, Deserialize)]
1514pub struct BackgroundTaskInfo {
1515    pub task_id: String,
1516    pub task_type: String,
1517    pub description: String,
1518    /// True for housekeeping tasks the CLI does not surface as user work;
1519    /// hosts should exclude them from activity indicators (CLI 2.1.259+).
1520    #[serde(default, skip_serializing_if = "Option::is_none")]
1521    pub ambient: Option<bool>,
1522}
1523
1524#[derive(Debug, Clone, Serialize, Deserialize)]
1525pub struct SessionStateChangedMessage {
1526    pub state: String,
1527    #[serde(default, skip_serializing_if = "Option::is_none")]
1528    pub uuid: Option<String>,
1529    #[serde(default, skip_serializing_if = "Option::is_none")]
1530    pub session_id: Option<String>,
1531}
1532
1533#[derive(Debug, Clone, Serialize, Deserialize)]
1534pub struct WorkerShuttingDownMessage {
1535    pub reason: String,
1536    #[serde(default, skip_serializing_if = "Option::is_none")]
1537    pub uuid: Option<String>,
1538    #[serde(default, skip_serializing_if = "Option::is_none")]
1539    pub session_id: Option<String>,
1540}
1541
1542#[derive(Debug, Clone, Serialize, Deserialize)]
1543pub struct CommandsChangedMessage {
1544    pub commands: Vec<CommandInfo>,
1545    #[serde(default, skip_serializing_if = "Option::is_none")]
1546    pub uuid: Option<String>,
1547    #[serde(default, skip_serializing_if = "Option::is_none")]
1548    pub session_id: Option<String>,
1549}
1550
1551#[derive(Debug, Clone, Serialize, Deserialize)]
1552pub struct CommandInfo {
1553    pub name: String,
1554    pub description: String,
1555    #[serde(rename = "argumentHint")]
1556    pub argument_hint: String,
1557    #[serde(default, skip_serializing_if = "Option::is_none")]
1558    pub aliases: Option<Vec<String>>,
1559}
1560
1561#[derive(Debug, Clone, Serialize, Deserialize)]
1562pub struct NotificationMessage {
1563    pub key: String,
1564    pub text: String,
1565    pub priority: String,
1566    #[serde(default, skip_serializing_if = "Option::is_none")]
1567    pub color: Option<String>,
1568    #[serde(default, skip_serializing_if = "Option::is_none")]
1569    pub timeout_ms: Option<u64>,
1570    #[serde(default, skip_serializing_if = "Option::is_none")]
1571    pub uuid: Option<String>,
1572    #[serde(default, skip_serializing_if = "Option::is_none")]
1573    pub session_id: Option<String>,
1574}
1575
1576#[derive(Debug, Clone, Serialize, Deserialize)]
1577pub struct FilesPersistedMessage {
1578    pub files: Vec<PersistedFile>,
1579    pub failed: Vec<FailedPersistedFile>,
1580    pub processed_at: String,
1581    #[serde(default, skip_serializing_if = "Option::is_none")]
1582    pub uuid: Option<String>,
1583    #[serde(default, skip_serializing_if = "Option::is_none")]
1584    pub session_id: Option<String>,
1585}
1586
1587#[derive(Debug, Clone, Serialize, Deserialize)]
1588pub struct PersistedFile {
1589    pub filename: String,
1590    pub file_id: String,
1591}
1592
1593#[derive(Debug, Clone, Serialize, Deserialize)]
1594pub struct FailedPersistedFile {
1595    pub filename: String,
1596    pub error: String,
1597}
1598
1599#[derive(Debug, Clone, Serialize, Deserialize)]
1600pub struct MemoryRecallMessage {
1601    pub mode: String,
1602    pub memories: Vec<MemoryRecallItem>,
1603    #[serde(default, skip_serializing_if = "Option::is_none")]
1604    pub uuid: Option<String>,
1605    #[serde(default, skip_serializing_if = "Option::is_none")]
1606    pub session_id: Option<String>,
1607}
1608
1609#[derive(Debug, Clone, Serialize, Deserialize)]
1610pub struct MemoryRecallItem {
1611    pub path: String,
1612    pub scope: String,
1613    #[serde(default, skip_serializing_if = "Option::is_none")]
1614    pub content: Option<String>,
1615}
1616
1617#[derive(Debug, Clone, Serialize, Deserialize)]
1618pub struct ElicitationCompleteMessage {
1619    pub mcp_server_name: String,
1620    pub elicitation_id: String,
1621    #[serde(default, skip_serializing_if = "Option::is_none")]
1622    pub uuid: Option<String>,
1623    #[serde(default, skip_serializing_if = "Option::is_none")]
1624    pub session_id: Option<String>,
1625}
1626
1627#[derive(Debug, Clone, Serialize, Deserialize)]
1628pub struct PermissionDeniedMessage {
1629    pub tool_name: String,
1630    pub tool_use_id: String,
1631    #[serde(default, skip_serializing_if = "Option::is_none")]
1632    pub agent_id: Option<String>,
1633    #[serde(default, skip_serializing_if = "Option::is_none")]
1634    pub decision_reason_type: Option<String>,
1635    #[serde(default, skip_serializing_if = "Option::is_none")]
1636    pub decision_reason: Option<String>,
1637    pub message: String,
1638    #[serde(default, skip_serializing_if = "Option::is_none")]
1639    pub uuid: Option<String>,
1640    #[serde(default, skip_serializing_if = "Option::is_none")]
1641    pub session_id: Option<String>,
1642}
1643
1644#[derive(Debug, Clone, Serialize, Deserialize)]
1645pub struct MirrorErrorMessage {
1646    pub error: String,
1647    pub key: MirrorErrorKey,
1648    #[serde(default, skip_serializing_if = "Option::is_none")]
1649    pub uuid: Option<String>,
1650    #[serde(default, skip_serializing_if = "Option::is_none")]
1651    pub session_id: Option<String>,
1652}
1653
1654#[derive(Debug, Clone, Serialize, Deserialize)]
1655pub struct MirrorErrorKey {
1656    #[serde(rename = "projectKey")]
1657    pub project_key: String,
1658    #[serde(rename = "sessionId")]
1659    pub session_id: String,
1660    #[serde(default, skip_serializing_if = "Option::is_none")]
1661    pub subpath: Option<String>,
1662}
1663
1664#[derive(Debug, Clone, Serialize, Deserialize)]
1665pub struct InformationalMessage {
1666    pub content: String,
1667    pub level: String,
1668    #[serde(default, skip_serializing_if = "Option::is_none")]
1669    pub tool_use_id: Option<String>,
1670    #[serde(default, skip_serializing_if = "Option::is_none")]
1671    pub prevent_continuation: Option<bool>,
1672    #[serde(default, skip_serializing_if = "Option::is_none")]
1673    pub uuid: Option<String>,
1674    #[serde(default, skip_serializing_if = "Option::is_none")]
1675    pub session_id: Option<String>,
1676}
1677
1678/// Plugin info from the init message
1679#[derive(Debug, Clone, Serialize, Deserialize)]
1680pub struct PluginInfo {
1681    /// Plugin name
1682    pub name: String,
1683    /// Path to the plugin on disk
1684    pub path: String,
1685    /// Plugin registry source (e.g., "rust-analyzer-lsp@claude-plugins-official")
1686    #[serde(skip_serializing_if = "Option::is_none")]
1687    pub source: Option<String>,
1688    /// Installed plugin version (e.g., "1.0.0"). Added in CLI 2.1.219.
1689    #[serde(default, skip_serializing_if = "Option::is_none")]
1690    pub version: Option<String>,
1691}
1692
1693/// Plugin load diagnostic reported by system init.
1694#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1695pub struct PluginDiagnostic {
1696    pub plugin: String,
1697    #[serde(rename = "type")]
1698    pub diagnostic_type: String,
1699    pub message: String,
1700}
1701
1702/// Memory paths reported by system init.
1703#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1704pub struct MemoryPaths {
1705    #[serde(default, skip_serializing_if = "Option::is_none")]
1706    pub auto: Option<String>,
1707    #[serde(default, skip_serializing_if = "Option::is_none")]
1708    pub team: Option<String>,
1709    #[serde(flatten)]
1710    pub extra: serde_json::Map<String, Value>,
1711}
1712
1713/// An MCP server config entry that failed validation, reported by system
1714/// init (e.g. a `url` entry with no `type`). The affected server is skipped
1715/// and absent from `InitMessage::mcp_servers`.
1716#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1717pub struct McpServerError {
1718    pub name: String,
1719    /// Stable error category.
1720    #[serde(rename = "type")]
1721    pub error_type: String,
1722    pub message: String,
1723}
1724
1725/// Init system message data - sent at session start
1726#[derive(Debug, Clone, Serialize, Deserialize)]
1727pub struct InitMessage {
1728    /// Session identifier
1729    pub session_id: String,
1730    /// Current working directory
1731    #[serde(skip_serializing_if = "Option::is_none")]
1732    pub cwd: Option<String>,
1733    /// Model being used
1734    #[serde(skip_serializing_if = "Option::is_none")]
1735    pub model: Option<String>,
1736    /// List of available tools
1737    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1738    pub tools: Vec<String>,
1739    /// MCP servers configured
1740    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1741    pub mcp_servers: Vec<Value>,
1742    /// Available slash commands (e.g., "compact", "cost", "review")
1743    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1744    pub slash_commands: Vec<String>,
1745    /// Slash commands only meaningful in a terminal context (CLI 2.1.232+,
1746    /// e.g. "doctor", "color")
1747    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1748    pub terminal_slash_commands: Vec<String>,
1749    /// Available agent types (e.g., "Bash", "Explore", "Plan")
1750    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1751    pub agents: Vec<String>,
1752    /// Installed plugins
1753    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1754    pub plugins: Vec<PluginInfo>,
1755    /// Installed skills
1756    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1757    pub skills: Vec<Value>,
1758    /// Claude Code CLI version
1759    #[serde(skip_serializing_if = "Option::is_none")]
1760    pub claude_code_version: Option<String>,
1761    /// Unix socket path for the harness's inter-session messaging bridge
1762    /// (new in CLI 2.1.232; absent on older CLIs and non-bridged runs)
1763    #[serde(skip_serializing_if = "Option::is_none")]
1764    pub messaging_socket_path: Option<String>,
1765    /// How the API key was sourced
1766    #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
1767    pub api_key_source: Option<ApiKeySource>,
1768    /// Output style
1769    #[serde(skip_serializing_if = "Option::is_none")]
1770    pub output_style: Option<OutputStyle>,
1771    /// Permission mode
1772    #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1773    pub permission_mode: Option<InitPermissionMode>,
1774
1775    /// Message-level unique identifier
1776    #[serde(skip_serializing_if = "Option::is_none")]
1777    pub uuid: Option<String>,
1778
1779    /// Memory storage paths (e.g., {"auto": "/path/to/memory/"})
1780    #[serde(skip_serializing_if = "Option::is_none")]
1781    pub memory_paths: Option<MemoryPaths>,
1782
1783    /// Fast mode toggle state (e.g., "off")
1784    #[serde(skip_serializing_if = "Option::is_none")]
1785    pub fast_mode_state: Option<String>,
1786
1787    /// Why fast mode can't serve right now. Absent when nothing blocks it.
1788    #[serde(default, skip_serializing_if = "Option::is_none")]
1789    pub fast_mode_disabled_reason: Option<super::result::FastModeDisabledReason>,
1790
1791    /// MCP server config entries (from `--mcp-config`) that failed validation
1792    /// and were skipped. Affected servers are absent from `mcp_servers`.
1793    #[serde(default, skip_serializing_if = "Option::is_none")]
1794    pub mcp_server_errors: Option<Vec<McpServerError>>,
1795
1796    /// Whether analytics collection is disabled for this session.
1797    #[serde(default, skip_serializing_if = "Option::is_none")]
1798    pub analytics_disabled: Option<bool>,
1799
1800    /// Whether product-feedback prompts are disabled for this session.
1801    #[serde(default, skip_serializing_if = "Option::is_none")]
1802    pub product_feedback_disabled: Option<bool>,
1803
1804    /// API beta flags active for the session.
1805    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1806    pub betas: Vec<String>,
1807
1808    /// Open-set protocol capability names supported by this CLI.
1809    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1810    pub capabilities: Vec<String>,
1811
1812    /// Plugin load errors.
1813    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1814    pub plugin_errors: Vec<PluginDiagnostic>,
1815
1816    /// Plugin load warnings.
1817    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1818    pub plugin_warnings: Vec<PluginDiagnostic>,
1819
1820    /// The effort level the session will send on its next request — after env
1821    /// overrides, session state, org caps, and model-support downgrades
1822    /// (`"low"` | `"medium"` | `"high"` | `"xhigh"` | `"max"`). `None` when no
1823    /// effort parameter will be sent, or on CLIs before 2.1.239.
1824    #[serde(default, skip_serializing_if = "Option::is_none")]
1825    pub effort: Option<String>,
1826
1827    /// Only on init frames written by the headless stream-json client of a
1828    /// cloud-hosted session: a per-frame snapshot of the cloud session's id,
1829    /// view URL, device binding, and directory-sync state. Absent in every
1830    /// other mode. Stored as raw JSON (the shape is internal and evolving).
1831    #[serde(default, skip_serializing_if = "Option::is_none")]
1832    pub cloud_session: Option<Value>,
1833
1834    /// The terminal's server-configured `◆ <text>` footer pill, carried so a
1835    /// host UI can render the same pill. Absent when nothing is configured
1836    /// (CLI 2.1.259+).
1837    #[serde(default, skip_serializing_if = "Option::is_none")]
1838    pub footer_indicator: Option<FooterIndicator>,
1839
1840    /// This cloud worker's life (`CLAUDE_CODE_WORKER_EPOCH`): a new number
1841    /// each time the session's worker is started. Absent outside cloud
1842    /// workers and on CLIs before 2.1.259.
1843    #[serde(default, skip_serializing_if = "Option::is_none")]
1844    pub worker_epoch: Option<u64>,
1845
1846    /// Windows only: the absolute path of the PowerShell binary this session
1847    /// runs PowerShell commands with, or `Some(None)` (wire `null`) when
1848    /// none was found. Absent on other platforms and on CLIs before 2.1.259.
1849    #[serde(default, skip_serializing_if = "Option::is_none")]
1850    pub powershell_path: Option<Option<String>>,
1851
1852    /// Cold-start telemetry for hosted (CCR) sessions: named startup phases
1853    /// and resume-hydration counters. Absent elsewhere. Stored as raw JSON
1854    /// (the shape is internal and evolving) (CLI 2.1.266+).
1855    #[serde(default, skip_serializing_if = "Option::is_none")]
1856    pub startup_timing: Option<serde_json::Map<String, Value>>,
1857}
1858
1859/// The server-configured session indicator that the terminal renders as a
1860/// `◆ <text>` pill in the prompt footer, carried on `system/init` and the
1861/// `initialize` response (CLI 2.1.259+).
1862#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1863pub struct FooterIndicator {
1864    /// The label to show — already sanitized to a single line of plain text,
1865    /// exactly as the terminal footer renders it after its `◆` glyph.
1866    pub text: String,
1867}
1868
1869/// Status system message - sent during operations like context compaction
1870#[derive(Debug, Clone, Serialize, Deserialize)]
1871pub struct StatusMessage {
1872    /// Session identifier
1873    pub session_id: String,
1874    /// Current status (e.g., compacting) or null when complete
1875    pub status: Option<StatusMessageStatus>,
1876    /// Unique identifier for this message
1877    #[serde(skip_serializing_if = "Option::is_none")]
1878    pub uuid: Option<String>,
1879    /// Current permission mode when changed mid-session.
1880    #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1881    pub permission_mode: Option<InitPermissionMode>,
1882    #[serde(skip_serializing_if = "Option::is_none")]
1883    pub compact_result: Option<String>,
1884    #[serde(skip_serializing_if = "Option::is_none")]
1885    pub compact_error: Option<String>,
1886}
1887
1888/// Compact boundary message - marks where context compaction occurred
1889#[derive(Debug, Clone, Serialize, Deserialize)]
1890pub struct CompactBoundaryMessage {
1891    /// Session identifier
1892    pub session_id: String,
1893    /// Metadata about the compaction
1894    pub compact_metadata: CompactMetadata,
1895    /// Human-readable summary of what was compacted, when the CLI emits one.
1896    ///
1897    /// Also accepted under the `content` / `text` wire keys.
1898    #[serde(
1899        default,
1900        skip_serializing_if = "Option::is_none",
1901        alias = "content",
1902        alias = "text"
1903    )]
1904    pub summary: Option<String>,
1905    /// Number of messages summarized in this compaction pass, when present.
1906    ///
1907    /// Also accepted under the `message_count` wire key.
1908    #[serde(
1909        default,
1910        skip_serializing_if = "Option::is_none",
1911        alias = "message_count"
1912    )]
1913    pub leaf_message_count: Option<u32>,
1914    /// Wall-clock duration of the compaction pass in milliseconds, when present.
1915    #[serde(default, skip_serializing_if = "Option::is_none")]
1916    pub duration_ms: Option<u64>,
1917    /// Unique identifier for this message
1918    #[serde(skip_serializing_if = "Option::is_none")]
1919    pub uuid: Option<String>,
1920    /// Logical parent across the compaction boundary.
1921    #[serde(skip_serializing_if = "Option::is_none")]
1922    pub logical_parent_uuid: Option<Option<String>>,
1923    /// Replayed history rather than a live message, stamped by the Remote
1924    /// Control bridge when it flushes history to the session server
1925    /// (CLI 2.1.266+).
1926    #[serde(default, skip_serializing_if = "Option::is_none")]
1927    pub historical: Option<bool>,
1928}
1929
1930/// Metadata about context compaction
1931#[derive(Debug, Clone, Serialize, Deserialize)]
1932pub struct CompactMetadata {
1933    /// Number of tokens before compaction
1934    pub pre_tokens: u64,
1935    /// What triggered the compaction
1936    pub trigger: CompactionTrigger,
1937    #[serde(default, skip_serializing_if = "Option::is_none")]
1938    pub post_tokens: Option<u64>,
1939    #[serde(default, skip_serializing_if = "Option::is_none")]
1940    pub cumulative_dropped_tokens: Option<u64>,
1941    #[serde(default, skip_serializing_if = "Option::is_none")]
1942    pub duration_ms: Option<u64>,
1943    #[serde(default, skip_serializing_if = "Option::is_none")]
1944    pub user_context: Option<String>,
1945    #[serde(default, skip_serializing_if = "Option::is_none")]
1946    pub messages_summarized: Option<u64>,
1947    #[serde(default, skip_serializing_if = "Option::is_none")]
1948    pub precomputed: Option<bool>,
1949    #[serde(default, skip_serializing_if = "Option::is_none")]
1950    pub pre_compact_discovered_tools: Option<Vec<String>>,
1951    #[serde(default, skip_serializing_if = "Option::is_none")]
1952    pub preserved_segment: Option<PreservedSegment>,
1953    #[serde(default, skip_serializing_if = "Option::is_none")]
1954    pub preserved_messages: Option<PreservedMessages>,
1955}
1956
1957#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1958pub struct PreservedSegment {
1959    pub head_uuid: String,
1960    pub anchor_uuid: String,
1961    pub tail_uuid: String,
1962}
1963
1964#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1965pub struct PreservedMessages {
1966    pub anchor_uuid: String,
1967    pub uuids: Vec<String>,
1968    #[serde(default, skip_serializing_if = "Option::is_none")]
1969    pub all_uuids: Option<Vec<String>>,
1970}
1971
1972// ---------------------------------------------------------------------------
1973// Task system message types (task_started, task_progress, task_notification)
1974// ---------------------------------------------------------------------------
1975
1976/// Cumulative usage statistics for a background task.
1977#[derive(Debug, Clone, Serialize, Deserialize)]
1978pub struct TaskUsage {
1979    /// Wall-clock milliseconds since the task started.
1980    pub duration_ms: u64,
1981    /// Total number of tool calls made so far.
1982    pub tool_uses: u64,
1983    /// Total tokens consumed so far.
1984    pub total_tokens: u64,
1985}
1986
1987/// The kind of background task.
1988#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1989pub enum TaskType {
1990    /// A sub-agent task (e.g., Explore, Plan).
1991    LocalAgent,
1992    /// A background bash command.
1993    LocalBash,
1994    /// A local workflow task.
1995    LocalWorkflow,
1996    /// A task type not yet known to this version of the crate.
1997    Unknown(String),
1998}
1999
2000impl TaskType {
2001    pub fn as_str(&self) -> &str {
2002        match self {
2003            Self::LocalAgent => "local_agent",
2004            Self::LocalBash => "local_bash",
2005            Self::LocalWorkflow => "local_workflow",
2006            Self::Unknown(s) => s.as_str(),
2007        }
2008    }
2009}
2010
2011impl fmt::Display for TaskType {
2012    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2013        f.write_str(self.as_str())
2014    }
2015}
2016
2017impl From<&str> for TaskType {
2018    fn from(s: &str) -> Self {
2019        match s {
2020            "local_agent" => Self::LocalAgent,
2021            "local_bash" => Self::LocalBash,
2022            "local_workflow" => Self::LocalWorkflow,
2023            other => Self::Unknown(other.to_string()),
2024        }
2025    }
2026}
2027
2028impl Serialize for TaskType {
2029    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2030        serializer.serialize_str(self.as_str())
2031    }
2032}
2033
2034impl<'de> Deserialize<'de> for TaskType {
2035    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2036        let s = String::deserialize(deserializer)?;
2037        Ok(Self::from(s.as_str()))
2038    }
2039}
2040
2041/// Completion status of a background task.
2042#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2043pub enum TaskStatus {
2044    Pending,
2045    Running,
2046    Completed,
2047    Failed,
2048    Killed,
2049    Paused,
2050    Stopped,
2051    Unknown(String),
2052}
2053
2054impl TaskStatus {
2055    pub fn as_str(&self) -> &str {
2056        match self {
2057            Self::Pending => "pending",
2058            Self::Running => "running",
2059            Self::Completed => "completed",
2060            Self::Failed => "failed",
2061            Self::Killed => "killed",
2062            Self::Paused => "paused",
2063            Self::Stopped => "stopped",
2064            Self::Unknown(s) => s.as_str(),
2065        }
2066    }
2067}
2068
2069impl fmt::Display for TaskStatus {
2070    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2071        f.write_str(self.as_str())
2072    }
2073}
2074
2075impl From<&str> for TaskStatus {
2076    fn from(s: &str) -> Self {
2077        match s {
2078            "pending" => Self::Pending,
2079            "running" => Self::Running,
2080            "completed" => Self::Completed,
2081            "failed" => Self::Failed,
2082            "killed" => Self::Killed,
2083            "paused" => Self::Paused,
2084            "stopped" => Self::Stopped,
2085            other => Self::Unknown(other.to_string()),
2086        }
2087    }
2088}
2089
2090impl Serialize for TaskStatus {
2091    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2092        serializer.serialize_str(self.as_str())
2093    }
2094}
2095
2096impl<'de> Deserialize<'de> for TaskStatus {
2097    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2098        let s = String::deserialize(deserializer)?;
2099        Ok(Self::from(s.as_str()))
2100    }
2101}
2102
2103/// `task_started` system message — emitted once when a background task begins.
2104#[derive(Debug, Clone, Serialize, Deserialize)]
2105pub struct TaskStartedMessage {
2106    pub session_id: String,
2107    pub task_id: String,
2108    #[serde(default, skip_serializing_if = "Option::is_none")]
2109    pub task_type: Option<TaskType>,
2110    #[serde(default, skip_serializing_if = "Option::is_none")]
2111    pub tool_use_id: Option<String>,
2112    pub description: String,
2113    /// The subagent type for `local_agent` tasks (e.g. `general-purpose`,
2114    /// `Explore`). Absent for `local_bash` tasks.
2115    #[serde(default, skip_serializing_if = "Option::is_none")]
2116    pub subagent_type: Option<String>,
2117    /// Whether the task was registered in the background (`true`) or in the
2118    /// foreground with the spawning tool call blocking on it (`false`). A
2119    /// later move to the background arrives as `task_updated`
2120    /// `patch.is_backgrounded`. Set for `local_agent` and `local_bash` tasks
2121    /// (CLI 2.1.239+).
2122    #[serde(default, skip_serializing_if = "Option::is_none")]
2123    pub is_backgrounded: Option<bool>,
2124    /// Nesting depth of a spawned subagent (`local_agent`) task: 1 for a
2125    /// top-level spawn, N+1 when spawned from inside a depth-N agent. Not set
2126    /// on other tasks (CLI 2.1.239+).
2127    #[serde(default, skip_serializing_if = "Option::is_none")]
2128    pub spawn_depth: Option<u32>,
2129    /// The prompt handed to the subagent. Present for `local_agent` tasks.
2130    #[serde(default, skip_serializing_if = "Option::is_none")]
2131    pub prompt: Option<String>,
2132    #[serde(default, skip_serializing_if = "Option::is_none")]
2133    pub workflow_name: Option<String>,
2134    #[serde(default, skip_serializing_if = "Option::is_none")]
2135    pub skip_transcript: Option<bool>,
2136    /// True for housekeeping tasks the CLI does not surface as user work
2137    /// (every `skip_transcript` task, plus auto-started live-update
2138    /// watchers); hosts should exclude them from activity indicators
2139    /// (CLI 2.1.259+).
2140    #[serde(default, skip_serializing_if = "Option::is_none")]
2141    pub ambient: Option<bool>,
2142    pub uuid: String,
2143}
2144
2145/// `task_updated` system message — emitted when a background task's state
2146/// changes (e.g. transitions to `completed`). Carries a partial `patch` of the
2147/// fields that changed rather than the full task record.
2148#[derive(Debug, Clone, Serialize, Deserialize)]
2149pub struct TaskUpdatedMessage {
2150    pub session_id: String,
2151    pub task_id: String,
2152    pub patch: TaskPatch,
2153    pub uuid: String,
2154}
2155
2156/// The partial update carried by a [`TaskUpdatedMessage`]. Every field is
2157/// optional because the CLI only sends the keys that changed.
2158#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2159pub struct TaskPatch {
2160    #[serde(default, skip_serializing_if = "Option::is_none")]
2161    pub status: Option<TaskStatus>,
2162    /// Wall-clock epoch milliseconds when the task finished, when the patch
2163    /// reports completion.
2164    #[serde(default, skip_serializing_if = "Option::is_none")]
2165    pub end_time: Option<u64>,
2166    #[serde(default, skip_serializing_if = "Option::is_none")]
2167    pub description: Option<String>,
2168    #[serde(default, skip_serializing_if = "Option::is_none")]
2169    pub total_paused_ms: Option<u64>,
2170    #[serde(default, skip_serializing_if = "Option::is_none")]
2171    pub error: Option<String>,
2172    #[serde(default, skip_serializing_if = "Option::is_none")]
2173    pub is_backgrounded: Option<bool>,
2174}
2175
2176/// `thinking_tokens` system message — emitted as the model streams extended
2177/// thinking, reporting the running estimate of thinking tokens consumed.
2178#[derive(Debug, Clone, Serialize, Deserialize)]
2179pub struct ThinkingTokensMessage {
2180    pub session_id: String,
2181    /// Running estimate of total thinking tokens for the current turn.
2182    pub estimated_tokens: u64,
2183    /// Increase in the estimate since the previous `thinking_tokens` event.
2184    pub estimated_tokens_delta: u64,
2185    /// Client uuid of the user message that triggered this turn, stamped on
2186    /// every `thinking_tokens` frame of a headless turn so a consumer can
2187    /// attribute thinking progress to the send it answers before any reply
2188    /// frame arrives. Absent on synthetic/scheduled (meta) turns, on turns
2189    /// without a client uuid, on Remote Control sessions, and from CLIs
2190    /// before 2.1.261.
2191    #[serde(default, skip_serializing_if = "Option::is_none")]
2192    pub user_message_uuid: Option<String>,
2193    pub uuid: String,
2194}
2195
2196/// `task_progress` system message — emitted periodically as a background
2197/// agent task executes tools. Not emitted for `local_bash` tasks.
2198#[derive(Debug, Clone, Serialize, Deserialize)]
2199pub struct TaskProgressMessage {
2200    pub session_id: String,
2201    pub task_id: String,
2202    #[serde(default, skip_serializing_if = "Option::is_none")]
2203    pub tool_use_id: Option<String>,
2204    pub description: String,
2205    #[serde(default, skip_serializing_if = "Option::is_none")]
2206    pub last_tool_name: Option<String>,
2207    pub usage: TaskUsage,
2208    /// Subagent type for `local_agent` tasks (e.g. `Explore`).
2209    #[serde(default, skip_serializing_if = "Option::is_none")]
2210    pub subagent_type: Option<String>,
2211    #[serde(default, skip_serializing_if = "Option::is_none")]
2212    pub summary: Option<String>,
2213    pub uuid: String,
2214}
2215
2216/// `task_notification` system message — emitted once when a background
2217/// task completes or fails.
2218#[derive(Debug, Clone, Serialize, Deserialize)]
2219pub struct TaskNotificationMessage {
2220    pub session_id: String,
2221    pub task_id: String,
2222    pub status: TaskStatus,
2223    pub summary: String,
2224    pub output_file: Option<String>,
2225    #[serde(skip_serializing_if = "Option::is_none")]
2226    pub tool_use_id: Option<String>,
2227    #[serde(skip_serializing_if = "Option::is_none")]
2228    pub usage: Option<TaskUsage>,
2229    /// For a backgrounded MCP task that completed, the `resource_link`
2230    /// content blocks of its final result — the files it returned by
2231    /// reference — collected from the raw result before the CLI renders it
2232    /// as text. Join to the originating call via `tool_use_id`. Absent when
2233    /// the result had none or the task is any other type (CLI 2.1.259+).
2234    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2235    pub resource_links: Vec<ResourceLink>,
2236    #[serde(default, skip_serializing_if = "Option::is_none")]
2237    pub skip_transcript: Option<bool>,
2238    /// True for housekeeping tasks the CLI does not surface as user work;
2239    /// hosts should exclude them from activity indicators (CLI 2.1.259+).
2240    #[serde(default, skip_serializing_if = "Option::is_none")]
2241    pub ambient: Option<bool>,
2242    #[serde(skip_serializing_if = "Option::is_none")]
2243    pub uuid: Option<String>,
2244}
2245
2246/// API error category attached to assistant wrapper frames.
2247#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2248pub enum AssistantErrorKind {
2249    AuthenticationFailed,
2250    OauthOrgNotAllowed,
2251    AccountOnHold,
2252    BillingError,
2253    RateLimit,
2254    Overloaded,
2255    InvalidRequest,
2256    ModelNotFound,
2257    ServerError,
2258    UnknownError,
2259    MaxOutputTokens,
2260    Unknown(String),
2261}
2262
2263impl AssistantErrorKind {
2264    pub fn as_str(&self) -> &str {
2265        match self {
2266            Self::AuthenticationFailed => "authentication_failed",
2267            Self::OauthOrgNotAllowed => "oauth_org_not_allowed",
2268            Self::AccountOnHold => "account_on_hold",
2269            Self::BillingError => "billing_error",
2270            Self::RateLimit => "rate_limit",
2271            Self::Overloaded => "overloaded",
2272            Self::InvalidRequest => "invalid_request",
2273            Self::ModelNotFound => "model_not_found",
2274            Self::ServerError => "server_error",
2275            Self::UnknownError => "unknown",
2276            Self::MaxOutputTokens => "max_output_tokens",
2277            Self::Unknown(s) => s.as_str(),
2278        }
2279    }
2280}
2281
2282impl fmt::Display for AssistantErrorKind {
2283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2284        f.write_str(self.as_str())
2285    }
2286}
2287
2288impl From<&str> for AssistantErrorKind {
2289    fn from(s: &str) -> Self {
2290        match s {
2291            "authentication_failed" => Self::AuthenticationFailed,
2292            "oauth_org_not_allowed" => Self::OauthOrgNotAllowed,
2293            "account_on_hold" => Self::AccountOnHold,
2294            "billing_error" => Self::BillingError,
2295            "rate_limit" => Self::RateLimit,
2296            "overloaded" => Self::Overloaded,
2297            "invalid_request" => Self::InvalidRequest,
2298            "model_not_found" => Self::ModelNotFound,
2299            "server_error" => Self::ServerError,
2300            "unknown" => Self::UnknownError,
2301            "max_output_tokens" => Self::MaxOutputTokens,
2302            other => Self::Unknown(other.to_string()),
2303        }
2304    }
2305}
2306
2307impl Serialize for AssistantErrorKind {
2308    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2309        serializer.serialize_str(self.as_str())
2310    }
2311}
2312
2313impl<'de> Deserialize<'de> for AssistantErrorKind {
2314    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2315        let s = String::deserialize(deserializer)?;
2316        Ok(Self::from(s.as_str()))
2317    }
2318}
2319
2320/// `code_change_published` system message — the session is now associated
2321/// with a published code change (a pull/merge request). Fires on creation and
2322/// whenever the session contributes to an existing one, so bind on every
2323/// event; re-emission for the same URL is possible and idempotent. Values are
2324/// scraped from captured command output — treat them as a binding hint and
2325/// verify against the forge before routing authenticated requests.
2326#[derive(Debug, Clone, Serialize, Deserialize)]
2327pub struct CodeChangePublishedMessage {
2328    /// Forge classification derived from the URL's shape (`github`,
2329    /// `github-enterprise`, `gitlab`, `bitbucket`, `gerrit` today). Open set
2330    /// — treat an unknown value as a valid provider, never as an error.
2331    pub provider: String,
2332    /// Web URL of the pull/merge request. Unverified.
2333    pub url: String,
2334    /// Repository path from the URL (`owner/name` on GitHub; may carry more
2335    /// segments on GitLab).
2336    pub repo: String,
2337    /// Provider-native change identifier — the PR/MR number as a string.
2338    pub identifier: String,
2339    /// What the session did that produced this announcement: the flag-aware
2340    /// `gh pr` verb it ran (`"created"`, `"edited"`, `"merged"`,
2341    /// `"commented"`, `"closed"`, `"reopened"`, `"ready"`, `"draft"`,
2342    /// `"auto-merge-enabled"`, `"auto-merge-disabled"`), `"pushed"` for a
2343    /// push to a branch that has a PR, `"checked-out"` for `gh pr checkout`,
2344    /// or `"started"` for the open change on the branch a Claude Desktop
2345    /// session began on. Always sent by current producers (CLI 2.1.239+),
2346    /// absent only from older ones. Open set — treat unknown values as valid.
2347    #[serde(default, skip_serializing_if = "Option::is_none")]
2348    pub action: Option<String>,
2349    /// The session's working branch when it produced the change. Sent only
2350    /// for providers whose changes have no head branch of their own
2351    /// (`gerrit`), so a host can place the change on that checkout; absent
2352    /// for every other provider (CLI 2.1.259+).
2353    #[serde(default, skip_serializing_if = "Option::is_none")]
2354    pub branch: Option<String>,
2355    pub uuid: String,
2356    pub session_id: String,
2357}
2358
2359/// `vcs_state_changed` system message — a harness-observed shell command
2360/// mutated repository state. A cache-invalidation signal, deliberately
2361/// payload-free beyond classification: consumers re-read state (branch, head,
2362/// PR status) instead of decoding the event.
2363#[derive(Debug, Clone, Serialize, Deserialize)]
2364pub struct VcsStateChangedMessage {
2365    /// What class of mutation was observed. New kinds may be added — treat an
2366    /// unrecognized kind exactly like a recognized one (something changed).
2367    pub kind: VcsMutationKind,
2368    /// The session's working directory — a hint, not necessarily the mutated
2369    /// repo's path (`git -C` or an inner `cd` mutates elsewhere).
2370    pub cwd: String,
2371    /// The branch a commit landed on or a push updated. Commit and push
2372    /// events carry it; a command that pushed several branches emits one push
2373    /// event per branch. A best-effort hint: absent whenever attribution is
2374    /// uncertain, and never a required key (CLI 2.1.239+).
2375    #[serde(default, skip_serializing_if = "Option::is_none")]
2376    pub branch: Option<String>,
2377    pub uuid: String,
2378    pub session_id: String,
2379}
2380
2381/// Mutation class carried by a [`VcsStateChangedMessage`].
2382#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2383pub enum VcsMutationKind {
2384    Commit,
2385    Push,
2386    Merge,
2387    Rebase,
2388    /// A kind not yet known to this version of the crate.
2389    Unknown(String),
2390}
2391
2392impl VcsMutationKind {
2393    pub fn as_str(&self) -> &str {
2394        match self {
2395            Self::Commit => "commit",
2396            Self::Push => "push",
2397            Self::Merge => "merge",
2398            Self::Rebase => "rebase",
2399            Self::Unknown(s) => s.as_str(),
2400        }
2401    }
2402}
2403
2404impl fmt::Display for VcsMutationKind {
2405    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2406        f.write_str(self.as_str())
2407    }
2408}
2409
2410impl From<&str> for VcsMutationKind {
2411    fn from(s: &str) -> Self {
2412        match s {
2413            "commit" => Self::Commit,
2414            "push" => Self::Push,
2415            "merge" => Self::Merge,
2416            "rebase" => Self::Rebase,
2417            other => Self::Unknown(other.to_string()),
2418        }
2419    }
2420}
2421
2422impl Serialize for VcsMutationKind {
2423    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2424        serializer.serialize_str(self.as_str())
2425    }
2426}
2427
2428impl<'de> Deserialize<'de> for VcsMutationKind {
2429    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2430        let s = String::deserialize(deserializer)?;
2431        Ok(Self::from(s.as_str()))
2432    }
2433}
2434
2435/// `system/feedback_draft_queued` — a feedback draft was queued for submission.
2436#[derive(Debug, Clone, Serialize, Deserialize)]
2437pub struct FeedbackDraftQueuedMessage {
2438    pub draft_id: String,
2439    pub draft_type: String,
2440    pub title: String,
2441    pub details_preview: String,
2442    #[serde(default, skip_serializing_if = "Option::is_none")]
2443    pub uuid: Option<String>,
2444    #[serde(default, skip_serializing_if = "Option::is_none")]
2445    pub session_id: Option<String>,
2446    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2447    pub extra: serde_json::Map<String, Value>,
2448}
2449
2450/// `system/cloud_session_delta` — written only by the headless stream-json
2451/// client of a cloud-hosted session (the same client that puts
2452/// `cloud_session` on its init frames): the session's status changed between
2453/// two inits. Only after the first init; at most a few per second; none when
2454/// nothing differs. An init is always a complete snapshot, so a host that
2455/// (re)attaches resynchronises from the first init it reads and applies
2456/// these on top. Display-only (CLI 2.1.260+).
2457#[derive(Debug, Clone, Serialize, Deserialize)]
2458pub struct CloudSessionDeltaMessage {
2459    /// Rises by one with each of these frames this client writes (1 for the
2460    /// first); never reset by an init. A reader keeps the highest it has
2461    /// applied and drops a lower one.
2462    pub seq: u64,
2463    /// The top-level keys of `cloud_session` whose value differs from the
2464    /// last block this client wrote, e.g. `["serving"]`; never empty. A hint
2465    /// for what to redraw — the block is complete either way.
2466    pub changed: Vec<String>,
2467    /// The whole block exactly as the next init would carry it (see
2468    /// [`InitMessage::cloud_session`]): replace the held copy, do not merge.
2469    /// Stored as raw JSON (the shape is internal and evolving).
2470    pub cloud_session: Value,
2471    pub uuid: String,
2472    pub session_id: String,
2473    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2474    pub extra: serde_json::Map<String, Value>,
2475}
2476
2477/// `system/dev_intent` — the conversation shows a known kind of development
2478/// work, for hosts that key tooling on it (Claude Code Desktop opens its iOS
2479/// Simulator entry point on `ios_app`). Sent with no other payload at most
2480/// once per kind per conversation per process: when the evidence for that
2481/// kind first completes, or at startup when a resumed conversation already
2482/// has it (possibly before `system/init`). A rewind or compaction never
2483/// retracts it and only a `conversation_reset` starts over, so treat each
2484/// kind as a sticky fact about the conversation and ignore repeats
2485/// (CLI 2.1.266+).
2486#[derive(Debug, Clone, Serialize, Deserialize)]
2487pub struct DevIntentMessage {
2488    /// What kind of development the conversation turned out to be doing.
2489    pub kind: DevIntentKind,
2490    pub uuid: String,
2491    pub session_id: String,
2492    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2493    pub extra: serde_json::Map<String, Value>,
2494}
2495
2496/// The kind of development a [`DevIntentMessage`] reports. Open set: the CLI
2497/// says "more kinds will be added; ignore a kind you do not recognize", so
2498/// unrecognized values deserialize to [`DevIntentKind::Unknown`].
2499#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2500pub enum DevIntentKind {
2501    /// Claude wrote or edited a `.swift` file and something it wrote, read, or
2502    /// ran is iPhone-specific (a macOS-only app or server-side Swift package
2503    /// never qualifies).
2504    IosApp,
2505    /// A kind not yet known to this version of the crate.
2506    Unknown(String),
2507}
2508
2509impl DevIntentKind {
2510    pub fn as_str(&self) -> &str {
2511        match self {
2512            Self::IosApp => "ios_app",
2513            Self::Unknown(s) => s.as_str(),
2514        }
2515    }
2516}
2517
2518impl fmt::Display for DevIntentKind {
2519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2520        f.write_str(self.as_str())
2521    }
2522}
2523
2524impl From<&str> for DevIntentKind {
2525    fn from(s: &str) -> Self {
2526        match s {
2527            "ios_app" => Self::IosApp,
2528            other => Self::Unknown(other.to_string()),
2529        }
2530    }
2531}
2532
2533impl Serialize for DevIntentKind {
2534    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2535        serializer.serialize_str(self.as_str())
2536    }
2537}
2538
2539impl<'de> Deserialize<'de> for DevIntentKind {
2540    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2541        let s = String::deserialize(deserializer)?;
2542        Ok(Self::from(s.as_str()))
2543    }
2544}
2545
2546/// `{id, name}` of an original `Batch*` tool_use block, carried in
2547/// [`AssistantMessage::batch_tool_uses`].
2548#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2549pub struct BatchToolUse {
2550    pub id: String,
2551    pub name: String,
2552}
2553
2554/// Structured twin of the `/context` report, carried as
2555/// [`AssistantMessage::context_usage`] — the data a client needs to render
2556/// the context-usage card without parsing the markdown table. Evolves
2557/// additively; a breaking reshape would ship as a sibling field.
2558#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2559pub struct ContextUsage {
2560    /// Main-loop model the usage was computed for.
2561    pub model: String,
2562    /// Estimated tokens in use, unclamped — may exceed `raw_max_tokens` when
2563    /// over limit.
2564    pub total_tokens: u64,
2565    /// The window usage is measured against: the resolved autocompact window —
2566    /// the model's believed limit, or a smaller compaction-policy window.
2567    pub raw_max_tokens: u64,
2568    /// Rounded `total_tokens / raw_max_tokens`, 0–100+.
2569    pub percentage: u64,
2570    /// Present when `total_tokens` exceeds `raw_max_tokens`.
2571    #[serde(default, skip_serializing_if = "Option::is_none")]
2572    pub over_limit: Option<ContextOverLimit>,
2573    /// Usage-by-category rows (`Messages`, `System prompt`, …).
2574    #[serde(default)]
2575    pub categories: Vec<ContextCategory>,
2576    /// Per-tool token contributions of MCP tools.
2577    #[serde(default)]
2578    pub mcp_tools: Vec<ContextMcpTool>,
2579    /// Per-file token contributions of memory files.
2580    #[serde(default)]
2581    pub memory_files: Vec<ContextMemoryFile>,
2582    /// Per-agent token contributions of agent definitions.
2583    #[serde(default)]
2584    pub agents: Vec<ContextAgent>,
2585    /// Per-skill token contributions. Omitted when no skills contribute.
2586    #[serde(default, skip_serializing_if = "Option::is_none")]
2587    pub skills: Option<Vec<ContextSkill>>,
2588}
2589
2590/// Why and by how much a [`ContextUsage`] exceeds its window.
2591#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2592pub struct ContextOverLimit {
2593    pub tokens_over: u64,
2594    /// How the window was resolved: `"hard_limit"` (the model's believed
2595    /// limit) or `"compaction_window"` (a compaction-policy window).
2596    pub kind: String,
2597}
2598
2599/// One row of the `/context` usage-by-category breakdown.
2600#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2601pub struct ContextCategory {
2602    /// Display name of the row as the CLI renders it, e.g. `"Messages"`.
2603    /// Use `kind` (not this name) to classify the row.
2604    pub name: String,
2605    pub tokens: u64,
2606    /// What the row is: `"used"` content occupies the window; `"free"` is the
2607    /// remaining window; `"buffer"` is the compaction reserve; `"deferred"`
2608    /// rows are out-of-window tool schemas, excluded from usage math.
2609    pub kind: String,
2610}
2611
2612/// An MCP tool's token contribution, in [`ContextUsage::mcp_tools`].
2613#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2614pub struct ContextMcpTool {
2615    /// Wire name, e.g. `"mcp__linear__create_issue"`.
2616    pub name: String,
2617    pub server_name: String,
2618    pub tokens: u64,
2619}
2620
2621/// A memory file's token contribution, in [`ContextUsage::memory_files`].
2622#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2623pub struct ContextMemoryFile {
2624    pub path: String,
2625    /// Display label of the memory-file source, e.g. `"Project"` or `"User"`.
2626    #[serde(rename = "type")]
2627    pub file_type: String,
2628    pub tokens: u64,
2629}
2630
2631/// An agent definition's token contribution, in [`ContextUsage::agents`].
2632#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2633pub struct ContextAgent {
2634    pub agent_type: String,
2635    /// Raw source identifier, e.g. `"projectSettings"`, `"plugin"`.
2636    pub source: String,
2637    pub tokens: u64,
2638}
2639
2640/// A skill's token contribution, in [`ContextUsage::skills`].
2641#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2642pub struct ContextSkill {
2643    pub name: String,
2644    /// Raw source identifier, e.g. `"userSettings"`, `"plugin"`.
2645    pub source: String,
2646    #[serde(default, skip_serializing_if = "Option::is_none")]
2647    pub plugin_name: Option<String>,
2648    pub tokens: u64,
2649}
2650
2651/// Display metadata for a tool-use block carried on the assistant wrapper.
2652#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2653pub struct ToolUseMeta {
2654    pub id: String,
2655    pub display_name: String,
2656    #[serde(default, skip_serializing_if = "Option::is_none")]
2657    pub server_display_name: Option<String>,
2658    #[serde(default, skip_serializing_if = "Option::is_none")]
2659    pub icon_url: Option<String>,
2660}
2661
2662/// Assistant message
2663#[derive(Debug, Clone, Serialize, Deserialize)]
2664pub struct AssistantMessage {
2665    pub message: AssistantMessageContent,
2666    #[serde(alias = "sessionId")]
2667    pub session_id: String,
2668    #[serde(skip_serializing_if = "Option::is_none")]
2669    pub uuid: Option<String>,
2670    #[serde(skip_serializing_if = "Option::is_none")]
2671    pub parent_tool_use_id: Option<String>,
2672    /// Anthropic API request id that produced this message (e.g. `req_...`).
2673    #[serde(skip_serializing_if = "Option::is_none")]
2674    pub request_id: Option<String>,
2675    /// Client uuid of the user message that triggered this turn, stamped on
2676    /// the turn's first reply frame only so a consumer can bind the reply to
2677    /// the send it answers without waiting for the result. Absent on every
2678    /// later frame of the turn, on subagent frames, on synthetic/scheduled
2679    /// (meta) turns, and from CLIs before 2.1.259.
2680    #[serde(default, skip_serializing_if = "Option::is_none")]
2681    pub user_message_uuid: Option<String>,
2682    /// Client uuids of every user message whose prompt this turn has consumed
2683    /// so far, in consumption order — all members of a prompt batch the host
2684    /// merged into this one turn. Always contains `user_message_uuid`; at
2685    /// most 64 entries. Present exactly when `user_message_uuid` is; absent
2686    /// from CLIs before 2.1.259 (fall back to `user_message_uuid`).
2687    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2688    pub user_message_uuids: Vec<String>,
2689    /// Subagent type, when this assistant message was produced inside a
2690    /// `local_agent` subagent (e.g. `general-purpose`, `Explore`).
2691    #[serde(skip_serializing_if = "Option::is_none")]
2692    pub subagent_type: Option<String>,
2693    /// Short description of the subagent task, present alongside `subagent_type`.
2694    #[serde(skip_serializing_if = "Option::is_none")]
2695    pub task_description: Option<String>,
2696    #[serde(skip_serializing_if = "Option::is_none")]
2697    pub error: Option<AssistantErrorKind>,
2698    /// True when this message was truncated by an interrupt/abort before the
2699    /// stream completed — `stop_reason` was never received and the content
2700    /// may end mid-word. Absent on normally completed messages.
2701    #[serde(default, skip_serializing_if = "Option::is_none")]
2702    pub aborted: Option<bool>,
2703    /// True when this turn continued the preceding truncated assistant turn
2704    /// inside its trailing signed thinking block (max-output-tokens
2705    /// recovery). Histories replayed through the bridge must carry the flag
2706    /// back so the normalizer keeps the run's prefix on the wire.
2707    #[serde(default, skip_serializing_if = "Option::is_none")]
2708    pub resumed_from_incomplete_thinking: Option<bool>,
2709    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2710    pub supersedes: Vec<String>,
2711    #[serde(skip_serializing_if = "Option::is_none")]
2712    pub timestamp: Option<String>,
2713    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2714    pub tool_use_meta: Vec<ToolUseMeta>,
2715    /// `{id, name}` of the original `Batch*` tool_use block(s) for a message
2716    /// whose content was decomposed into synthetic v1 tool_use blocks.
2717    /// Round-tripped so a replayed history reassembles the batch block on the
2718    /// wire. Wrapper-level sibling — never inside `message.content`.
2719    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2720    pub batch_tool_uses: Vec<BatchToolUse>,
2721    /// `tool_use.input` exactly as the API produced it, keyed by `tool_use`
2722    /// id, for a message whose `message.content` carries the
2723    /// client-normalized input. Round-tripped so a replayed history echoes
2724    /// each earlier tool call back to the API as the API emitted it.
2725    /// Wrapper-level sibling — never inside `message.content` (CLI 2.1.259+).
2726    #[serde(default, skip_serializing_if = "Option::is_none")]
2727    pub wire_tool_inputs: Option<serde_json::Map<String, Value>>,
2728    /// What the client-side input normalization read from process state for
2729    /// the inputs in [`wire_tool_inputs`](Self::wire_tool_inputs), keyed by
2730    /// the same `tool_use` ids. Round-tripped so a replayed history can verify
2731    /// each recorded input against the normalized one. Wrapper-level sibling —
2732    /// never inside `message.content` (CLI 2.1.266+).
2733    #[serde(default, skip_serializing_if = "Option::is_none")]
2734    pub wire_ingest_context: Option<serde_json::Map<String, Value>>,
2735    /// Replayed history rather than a live message, stamped by the Remote
2736    /// Control bridge when it flushes history to the session server, which
2737    /// also stamps it on deliveries it replays (CLI 2.1.266+).
2738    #[serde(default, skip_serializing_if = "Option::is_none")]
2739    pub historical: Option<bool>,
2740    /// The originating `system/local_command` row's wire-form content,
2741    /// carried on the loop-synthesized local-command twin so a bridge/SDK
2742    /// history replay rebuilds the internal system row instead of dropping
2743    /// the output. Wrapper-level sibling — never inside `message.content`
2744    /// (CLI 2.1.259+).
2745    #[serde(default, skip_serializing_if = "Option::is_none")]
2746    pub local_command_source: Option<String>,
2747    /// Ascending zero-based indexes into `message.content` of the thinking
2748    /// blocks whose signature the server tagged as narration (server
2749    /// summaries of the prose between tool calls, not the model's own
2750    /// reasoning), so a renderer can label them as summaries without decoding
2751    /// the signature envelope. Fail-closed: an unparseable or legacy
2752    /// signature is not listed. Omitted when the frame has no such block and
2753    /// by CLIs before 2.1.260; treat unlisted thinking blocks as ordinary
2754    /// thinking. Wrapper-level sibling — never inside `message.content`.
2755    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2756    pub narration_block_indexes: Vec<usize>,
2757    /// Structured twin of the `/context` report, carried on the synthetic
2758    /// assistant message that delivers the markdown table. Present only on
2759    /// `/context` results from CLIs new enough to attach it (2.1.239+).
2760    #[serde(default, skip_serializing_if = "Option::is_none")]
2761    pub context_usage: Option<ContextUsage>,
2762    #[serde(default, skip_serializing_if = "Option::is_none")]
2763    pub is_meta: Option<bool>,
2764    #[serde(default, skip_serializing_if = "Option::is_none")]
2765    pub is_virtual: Option<bool>,
2766    #[serde(default, skip_serializing_if = "Option::is_none")]
2767    pub is_api_error_message: Option<bool>,
2768    #[serde(skip_serializing_if = "Option::is_none")]
2769    pub api_error_status: Option<u16>,
2770    #[serde(skip_serializing_if = "Option::is_none")]
2771    pub api_error: Option<String>,
2772    #[serde(skip_serializing_if = "Option::is_none")]
2773    pub error_details: Option<String>,
2774    #[serde(skip_serializing_if = "Option::is_none")]
2775    pub advisor_model: Option<String>,
2776    #[serde(skip_serializing_if = "Option::is_none")]
2777    pub attribution_agent: Option<String>,
2778    #[serde(skip_serializing_if = "Option::is_none")]
2779    pub attribution_skill: Option<String>,
2780    #[serde(skip_serializing_if = "Option::is_none")]
2781    pub attribution_plugin: Option<String>,
2782    #[serde(skip_serializing_if = "Option::is_none")]
2783    pub attribution_mcp_server: Option<String>,
2784    #[serde(skip_serializing_if = "Option::is_none")]
2785    pub attribution_mcp_tool: Option<String>,
2786}
2787
2788/// Nested message content for assistant messages
2789#[derive(Debug, Clone, Serialize, Deserialize)]
2790pub struct AssistantMessageContent {
2791    pub id: String,
2792    /// The Anthropic API message type — always `"message"`.
2793    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
2794    pub message_type: Option<String>,
2795    pub role: MessageRole,
2796    pub model: String,
2797    pub content: Vec<ContentBlock>,
2798    #[serde(skip_serializing_if = "Option::is_none")]
2799    pub stop_reason: Option<StopReason>,
2800    #[serde(skip_serializing_if = "Option::is_none")]
2801    pub stop_sequence: Option<String>,
2802    #[serde(skip_serializing_if = "Option::is_none")]
2803    pub usage: Option<AssistantUsage>,
2804    /// Details about why generation stopped
2805    #[serde(skip_serializing_if = "Option::is_none")]
2806    pub stop_details: Option<Value>,
2807    /// Context management metadata
2808    #[serde(skip_serializing_if = "Option::is_none")]
2809    pub context_management: Option<Value>,
2810}
2811
2812/// Usage information for assistant messages
2813#[derive(Debug, Clone, Serialize, Deserialize)]
2814pub struct AssistantUsage {
2815    /// Number of input tokens
2816    #[serde(default)]
2817    pub input_tokens: u32,
2818
2819    /// Number of output tokens
2820    #[serde(default)]
2821    pub output_tokens: u32,
2822
2823    /// Tokens used to create cache
2824    #[serde(default)]
2825    pub cache_creation_input_tokens: u32,
2826
2827    /// Tokens read from cache
2828    #[serde(default)]
2829    pub cache_read_input_tokens: u32,
2830
2831    /// Service tier used (e.g., "standard")
2832    #[serde(skip_serializing_if = "Option::is_none")]
2833    pub service_tier: Option<String>,
2834
2835    /// Detailed cache creation breakdown
2836    #[serde(skip_serializing_if = "Option::is_none")]
2837    pub cache_creation: Option<CacheCreationDetails>,
2838
2839    /// Inference geography (e.g., "not_available")
2840    #[serde(skip_serializing_if = "Option::is_none")]
2841    pub inference_geo: Option<String>,
2842}
2843
2844/// Detailed cache creation information
2845#[derive(Debug, Clone, Serialize, Deserialize)]
2846pub struct CacheCreationDetails {
2847    /// Ephemeral 1-hour input tokens
2848    #[serde(default)]
2849    pub ephemeral_1h_input_tokens: u32,
2850
2851    /// Ephemeral 5-minute input tokens
2852    #[serde(default)]
2853    pub ephemeral_5m_input_tokens: u32,
2854}
2855
2856#[cfg(test)]
2857mod tests {
2858    use crate::io::ClaudeOutput;
2859
2860    #[test]
2861    fn test_subagent_usage_rollup_accumulates_task_results() {
2862        use super::SubagentUsageRollup;
2863
2864        let mut rollup = SubagentUsageRollup::default();
2865
2866        let task_result = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1","tool_use_result":{"status":"completed","prompt":"Compute 6 times 7.","agentId":"ab52f22445470d454","agentType":"general-purpose","resolvedModel":"claude-sonnet-4-6","totalDurationMs":1853,"totalTokens":10201,"totalToolUseCount":3}}"#;
2867        let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
2868        assert!(rollup.observe(&output));
2869        assert_eq!(rollup.subagent_tokens, 10201);
2870        assert_eq!(rollup.agent_count, 1);
2871        assert_eq!(rollup.tool_uses, 3);
2872        assert_eq!(rollup.duration_ms, 1853);
2873
2874        // Replayed frame with the same agentId is counted once.
2875        assert!(!rollup.observe(&output));
2876        assert_eq!(rollup.agent_count, 1);
2877        assert_eq!(rollup.subagent_tokens, 10201);
2878
2879        // A second agent accumulates.
2880        let second = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1","tool_use_result":{"status":"completed","agentId":"ffff00001111","totalDurationMs":100,"totalTokens":500,"totalToolUseCount":1}}"#;
2881        let output: ClaudeOutput = serde_json::from_str(second).unwrap();
2882        assert!(rollup.observe(&output));
2883        assert_eq!(rollup.agent_count, 2);
2884        assert_eq!(rollup.subagent_tokens, 10701);
2885    }
2886
2887    #[test]
2888    fn test_subagent_usage_rollup_ignores_non_task_results() {
2889        use super::SubagentUsageRollup;
2890
2891        let mut rollup = SubagentUsageRollup::default();
2892
2893        // A ToolSearch tool_use_result parses as an all-None SubagentResult;
2894        // it must not count as a subagent.
2895        let tool_search = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1","tool_use_result":{"matches":["TaskCreate"],"query":"select:TaskCreate","total_deferred_tools":27}}"#;
2896        let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
2897        assert!(!rollup.observe(&output));
2898
2899        // Plain user message without tool_use_result.
2900        let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2901        let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
2902        assert!(!rollup.observe(&output));
2903
2904        // Non-user frames are ignored.
2905        let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2906        let output: ClaudeOutput = serde_json::from_str(system).unwrap();
2907        assert!(!rollup.observe(&output));
2908
2909        assert_eq!(rollup, SubagentUsageRollup::default());
2910    }
2911
2912    #[test]
2913    fn test_subagent_usage_rollup_over_captured_session() {
2914        use super::SubagentUsageRollup;
2915
2916        let mut rollup = SubagentUsageRollup::default();
2917        let fixture =
2918            include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
2919        for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
2920            if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
2921                rollup.observe(&output);
2922            }
2923        }
2924        assert_eq!(rollup.agent_count, 1);
2925        assert_eq!(rollup.subagent_tokens, 10201);
2926    }
2927
2928    #[test]
2929    fn test_system_message_init() {
2930        let json = r#"{
2931            "type": "system",
2932            "subtype": "init",
2933            "session_id": "test-session-123",
2934            "cwd": "/home/user/project",
2935            "model": "claude-sonnet-4",
2936            "tools": ["Bash", "Read", "Write"],
2937            "mcp_servers": [],
2938            "slash_commands": ["compact", "cost", "review"],
2939            "agents": ["Bash", "Explore", "Plan"],
2940            "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
2941            "skills": [],
2942            "claude_code_version": "2.1.15",
2943            "apiKeySource": "none",
2944            "output_style": "default",
2945            "permissionMode": "default"
2946        }"#;
2947
2948        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2949        if let ClaudeOutput::System(sys) = output {
2950            assert!(sys.is_init());
2951            assert!(!sys.is_status());
2952            assert!(!sys.is_compact_boundary());
2953
2954            let init = sys.as_init().expect("Should parse as init");
2955            assert_eq!(init.session_id, "test-session-123");
2956            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2957            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2958            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2959            assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
2960            assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
2961            assert_eq!(init.plugins.len(), 1);
2962            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2963            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2964            assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
2965            assert_eq!(init.output_style, Some(super::OutputStyle::Default));
2966            assert_eq!(
2967                init.permission_mode,
2968                Some(super::InitPermissionMode::Default)
2969            );
2970        } else {
2971            panic!("Expected System message");
2972        }
2973    }
2974
2975    #[test]
2976    fn test_system_message_init_from_real_capture() {
2977        let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
2978        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2979        if let ClaudeOutput::System(sys) = output {
2980            let init = sys.as_init().expect("Should parse real init capture");
2981            assert_eq!(init.slash_commands.len(), 8);
2982            assert!(init.slash_commands.contains(&"compact".to_string()));
2983            assert!(init.slash_commands.contains(&"review".to_string()));
2984            assert_eq!(init.agents.len(), 5);
2985            assert!(init.agents.contains(&"Bash".to_string()));
2986            assert!(init.agents.contains(&"Explore".to_string()));
2987            assert_eq!(init.plugins.len(), 1);
2988            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2989            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2990        } else {
2991            panic!("Expected System message");
2992        }
2993    }
2994
2995    #[test]
2996    fn test_system_message_status() {
2997        let json = r#"{
2998            "type": "system",
2999            "subtype": "status",
3000            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3001            "status": "compacting",
3002            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
3003        }"#;
3004
3005        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3006        if let ClaudeOutput::System(sys) = output {
3007            assert!(sys.is_status());
3008            assert!(!sys.is_init());
3009
3010            let status = sys.as_status().expect("Should parse as status");
3011            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
3012            assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
3013            assert_eq!(
3014                status.uuid,
3015                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
3016            );
3017        } else {
3018            panic!("Expected System message");
3019        }
3020    }
3021
3022    #[test]
3023    fn test_system_message_status_null() {
3024        let json = r#"{
3025            "type": "system",
3026            "subtype": "status",
3027            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3028            "status": null,
3029            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
3030        }"#;
3031
3032        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3033        if let ClaudeOutput::System(sys) = output {
3034            let status = sys.as_status().expect("Should parse as status");
3035            assert_eq!(status.status, None);
3036        } else {
3037            panic!("Expected System message");
3038        }
3039    }
3040
3041    #[test]
3042    fn test_system_message_task_started() {
3043        let json = r#"{
3044            "type": "system",
3045            "subtype": "task_started",
3046            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
3047            "task_id": "b6daf3f",
3048            "task_type": "local_bash",
3049            "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
3050            "description": "Wait for CI on PR #12",
3051            "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
3052        }"#;
3053
3054        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3055        if let ClaudeOutput::System(sys) = output {
3056            assert!(sys.is_task_started());
3057            assert!(!sys.is_task_progress());
3058            assert!(!sys.is_task_notification());
3059
3060            let task = sys.as_task_started().expect("Should parse as task_started");
3061            assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
3062            assert_eq!(task.task_id, "b6daf3f");
3063            assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
3064            assert_eq!(
3065                task.tool_use_id.as_deref(),
3066                Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
3067            );
3068            assert_eq!(task.description, "Wait for CI on PR #12");
3069        } else {
3070            panic!("Expected System message");
3071        }
3072    }
3073
3074    #[test]
3075    fn test_system_message_task_started_agent() {
3076        let json = r#"{
3077            "type": "system",
3078            "subtype": "task_started",
3079            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3080            "task_id": "a4a7e0906e5fc64cc",
3081            "task_type": "local_agent",
3082            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
3083            "description": "Explore Scene/ArrayScene duplication",
3084            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
3085        }"#;
3086
3087        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3088        if let ClaudeOutput::System(sys) = output {
3089            let task = sys.as_task_started().expect("Should parse as task_started");
3090            assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
3091            assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
3092        } else {
3093            panic!("Expected System message");
3094        }
3095    }
3096
3097    #[test]
3098    fn test_system_message_task_progress() {
3099        let json = r#"{
3100            "type": "system",
3101            "subtype": "task_progress",
3102            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3103            "task_id": "a4a7e0906e5fc64cc",
3104            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
3105            "description": "Reading src/jplephem/chebyshev.rs",
3106            "last_tool_name": "Read",
3107            "usage": {
3108                "duration_ms": 13996,
3109                "tool_uses": 9,
3110                "total_tokens": 38779
3111            },
3112            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
3113        }"#;
3114
3115        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3116        if let ClaudeOutput::System(sys) = output {
3117            assert!(sys.is_task_progress());
3118            assert!(!sys.is_task_started());
3119
3120            let progress = sys
3121                .as_task_progress()
3122                .expect("Should parse as task_progress");
3123            assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
3124            assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
3125            assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
3126            assert_eq!(progress.usage.duration_ms, 13996);
3127            assert_eq!(progress.usage.tool_uses, 9);
3128            assert_eq!(progress.usage.total_tokens, 38779);
3129        } else {
3130            panic!("Expected System message");
3131        }
3132    }
3133
3134    #[test]
3135    fn test_system_message_task_notification_completed() {
3136        let json = r#"{
3137            "type": "system",
3138            "subtype": "task_notification",
3139            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3140            "task_id": "a0ba761e9dc9c316f",
3141            "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
3142            "status": "completed",
3143            "summary": "Agent \"Write Hipparcos data source doc\" completed",
3144            "output_file": "",
3145            "usage": {
3146                "duration_ms": 172300,
3147                "tool_uses": 11,
3148                "total_tokens": 42005
3149            },
3150            "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
3151        }"#;
3152
3153        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3154        if let ClaudeOutput::System(sys) = output {
3155            assert!(sys.is_task_notification());
3156
3157            let notif = sys
3158                .as_task_notification()
3159                .expect("Should parse as task_notification");
3160            assert_eq!(notif.status, super::TaskStatus::Completed);
3161            assert_eq!(
3162                notif.summary,
3163                "Agent \"Write Hipparcos data source doc\" completed"
3164            );
3165            assert_eq!(notif.output_file, Some("".to_string()));
3166            assert_eq!(
3167                notif.tool_use_id,
3168                Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
3169            );
3170            let usage = notif.usage.expect("Should have usage");
3171            assert_eq!(usage.duration_ms, 172300);
3172            assert_eq!(usage.tool_uses, 11);
3173            assert_eq!(usage.total_tokens, 42005);
3174        } else {
3175            panic!("Expected System message");
3176        }
3177    }
3178
3179    #[test]
3180    fn test_system_message_task_notification_failed_no_usage() {
3181        let json = r#"{
3182            "type": "system",
3183            "subtype": "task_notification",
3184            "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
3185            "task_id": "b98f6a3",
3186            "status": "failed",
3187            "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
3188            "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
3189        }"#;
3190
3191        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3192        if let ClaudeOutput::System(sys) = output {
3193            let notif = sys
3194                .as_task_notification()
3195                .expect("Should parse as task_notification");
3196            assert_eq!(notif.status, super::TaskStatus::Failed);
3197            assert!(notif.tool_use_id.is_none());
3198            assert!(notif.usage.is_none());
3199            assert_eq!(
3200                notif.output_file,
3201                Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
3202            );
3203        } else {
3204            panic!("Expected System message");
3205        }
3206    }
3207
3208    /// Task system messages survive a `to_value` → `from_value` round-trip
3209    /// with their typed accessors still resolving. Mirrors the proxy/relay
3210    /// path where output is reparsed from a `serde_json::Value` rather than
3211    /// straight from the CLI's stdout, so a silently dropped or renamed field
3212    /// surfaces here instead of as a `None` downstream.
3213    #[test]
3214    fn test_task_messages_roundtrip_through_value() {
3215        let cases = [
3216            r#"{"type":"system","subtype":"task_started","session_id":"s1",
3217                "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
3218                "description":"Sleep 3s","uuid":"u1"}"#,
3219            r#"{"type":"system","subtype":"task_progress","session_id":"s1",
3220                "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
3221                "last_tool_name":"Bash",
3222                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
3223                "uuid":"u2"}"#,
3224            r#"{"type":"system","subtype":"task_notification","session_id":"s1",
3225                "task_id":"t1","tool_use_id":"tu1","status":"completed",
3226                "summary":"done","output_file":"",
3227                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
3228                "uuid":"u3"}"#,
3229        ];
3230
3231        for json in cases {
3232            let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3233            let value = serde_json::to_value(&output).unwrap();
3234            let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
3235
3236            let ClaudeOutput::System(sys) = reparsed else {
3237                panic!("Expected System variant after round-trip");
3238            };
3239
3240            match sys.subtype {
3241                super::SystemSubtype::TaskStarted => {
3242                    assert!(
3243                        sys.as_task_started().is_some(),
3244                        "as_task_started failed after round-trip"
3245                    );
3246                }
3247                super::SystemSubtype::TaskProgress => {
3248                    assert!(
3249                        sys.as_task_progress().is_some(),
3250                        "as_task_progress failed after round-trip"
3251                    );
3252                }
3253                super::SystemSubtype::TaskNotification => {
3254                    assert!(
3255                        sys.as_task_notification().is_some(),
3256                        "as_task_notification failed after round-trip"
3257                    );
3258                }
3259                other => panic!("unexpected subtype after round-trip: {other:?}"),
3260            }
3261        }
3262    }
3263
3264    #[test]
3265    fn test_system_message_compact_boundary() {
3266        let json = r#"{
3267            "type": "system",
3268            "subtype": "compact_boundary",
3269            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3270            "compact_metadata": {
3271                "pre_tokens": 155285,
3272                "trigger": "auto"
3273            },
3274            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
3275        }"#;
3276
3277        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3278        if let ClaudeOutput::System(sys) = output {
3279            assert!(sys.is_compact_boundary());
3280            assert!(!sys.is_init());
3281            assert!(!sys.is_status());
3282
3283            let compact = sys
3284                .as_compact_boundary()
3285                .expect("Should parse as compact_boundary");
3286            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
3287            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
3288            assert_eq!(
3289                compact.compact_metadata.trigger,
3290                super::CompactionTrigger::Auto
3291            );
3292            // Per-compaction stats are optional and absent here.
3293            assert!(compact.summary.is_none());
3294            assert!(compact.leaf_message_count.is_none());
3295            assert!(compact.duration_ms.is_none());
3296        } else {
3297            panic!("Expected System message");
3298        }
3299    }
3300
3301    #[test]
3302    fn test_compact_boundary_with_summary_stats() {
3303        // Canonical keys.
3304        let json = r#"{
3305            "type": "system",
3306            "subtype": "compact_boundary",
3307            "session_id": "s1",
3308            "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
3309            "summary": "Summarized the earlier exploration.",
3310            "leaf_message_count": 42,
3311            "duration_ms": 1234,
3312            "uuid": "u1"
3313        }"#;
3314        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3315        let ClaudeOutput::System(sys) = output else {
3316            panic!("Expected System message");
3317        };
3318        let compact = sys.as_compact_boundary().expect("compact_boundary");
3319        assert_eq!(
3320            compact.summary.as_deref(),
3321            Some("Summarized the earlier exploration.")
3322        );
3323        assert_eq!(compact.leaf_message_count, Some(42));
3324        assert_eq!(compact.duration_ms, Some(1234));
3325
3326        // Alternate wire keys (`content` for summary, `message_count` for count)
3327        // deserialize into the same fields.
3328        let json_alt = r#"{
3329            "type": "system",
3330            "subtype": "compact_boundary",
3331            "session_id": "s2",
3332            "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
3333            "content": "alt-key summary",
3334            "message_count": 7
3335        }"#;
3336        let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
3337        let ClaudeOutput::System(sys) = output else {
3338            panic!("Expected System message");
3339        };
3340        let compact = sys.as_compact_boundary().expect("compact_boundary");
3341        assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
3342        assert_eq!(compact.leaf_message_count, Some(7));
3343    }
3344
3345    #[test]
3346    fn test_init_message_with_new_fields() {
3347        let json = r#"{
3348            "type": "system",
3349            "subtype": "init",
3350            "session_id": "test-session",
3351            "cwd": "/home/user",
3352            "model": "claude-opus-4-7",
3353            "tools": ["Bash"],
3354            "mcp_servers": [],
3355            "permissionMode": "default",
3356            "apiKeySource": "none",
3357            "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
3358            "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
3359            "fast_mode_state": "off",
3360            "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
3361            "claude_code_version": "2.1.117"
3362        }"#;
3363
3364        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3365        if let ClaudeOutput::System(sys) = output {
3366            let init = sys.as_init().expect("Should parse as init");
3367            assert_eq!(
3368                init.uuid.as_deref(),
3369                Some("44841a0d-182d-493a-86b5-79800d3d9665")
3370            );
3371            assert!(init.memory_paths.is_some());
3372            assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
3373            assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
3374            assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
3375        } else {
3376            panic!("Expected System message");
3377        }
3378    }
3379
3380    #[test]
3381    fn test_assistant_message_with_new_fields() {
3382        let json = r#"{
3383            "type": "assistant",
3384            "message": {
3385                "id": "msg_1",
3386                "type": "message",
3387                "role": "assistant",
3388                "model": "claude-opus-4-7",
3389                "content": [{"type": "text", "text": "Hello"}],
3390                "stop_reason": "end_turn",
3391                "stop_details": null,
3392                "context_management": null,
3393                "usage": {
3394                    "input_tokens": 100,
3395                    "output_tokens": 10,
3396                    "cache_creation_input_tokens": 50,
3397                    "cache_read_input_tokens": 0,
3398                    "service_tier": "standard",
3399                    "inference_geo": "not_available"
3400                }
3401            },
3402            "session_id": "abc",
3403            "uuid": "msg-uuid-123"
3404        }"#;
3405
3406        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3407        if let ClaudeOutput::Assistant(asst) = output {
3408            assert_eq!(asst.message.stop_details, None);
3409            assert_eq!(asst.message.context_management, None);
3410            let usage = asst.message.usage.unwrap();
3411            assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
3412        } else {
3413            panic!("Expected Assistant message");
3414        }
3415    }
3416
3417    #[test]
3418    fn test_user_message_with_new_fields() {
3419        let json = r#"{
3420            "type": "user",
3421            "message": {
3422                "role": "user",
3423                "content": [{"type": "text", "text": "Hello"}]
3424            },
3425            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
3426            "parent_tool_use_id": "toolu_123",
3427            "uuid": "user-msg-456"
3428        }"#;
3429
3430        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3431        if let ClaudeOutput::User(user) = output {
3432            assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
3433            assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
3434        } else {
3435            panic!("Expected User message");
3436        }
3437    }
3438
3439    /// Real wire payload captured from the CLI after answering an
3440    /// AskUserQuestion via the permission control protocol. The top-level
3441    /// `tool_use_result` and `timestamp` fields must round-trip without loss —
3442    /// proxies using this crate to relay messages to a viewer rely on those
3443    /// fields being preserved (the viewer reads `tool_use_result.answers`).
3444    #[test]
3445    fn test_user_message_preserves_tool_use_result_and_timestamp() {
3446        let json = r#"{
3447            "type":"user",
3448            "message":{"role":"user","content":[{"type":"tool_result","content":"User has answered your questions: . You can now continue with the user's answers in mind.","tool_use_id":"toolu_01331duMqP2PrRaqR2yWa8e4"}]},
3449            "parent_tool_use_id":null,
3450            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3451            "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
3452            "timestamp":"2026-05-12T23:12:04.121Z",
3453            "tool_use_result":{"questions":[{"question":"Which color do you prefer?","header":"Color","options":[{"label":"Red","description":"A warm color"},{"label":"Blue","description":"A cool color"}],"multiSelect":false}],"answers":{"Color":"Blue"}}
3454        }"#;
3455
3456        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3457        let user = match output {
3458            ClaudeOutput::User(u) => u,
3459            other => panic!("Expected User message, got {:?}", other.message_type()),
3460        };
3461
3462        assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
3463        let raw = user
3464            .tool_use_result
3465            .as_ref()
3466            .expect("tool_use_result must be captured");
3467        assert_eq!(raw["answers"]["Color"], "Blue");
3468        assert_eq!(raw["questions"][0]["header"], "Color");
3469
3470        // Round-trip: re-serialize and confirm tool_use_result + timestamp
3471        // survive — the bug we're guarding against is that the proxy silently
3472        // drops these fields when relaying user messages.
3473        let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
3474        assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
3475        assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
3476        assert_eq!(
3477            reser["tool_use_result"]["questions"][0]["question"],
3478            "Which color do you prefer?"
3479        );
3480
3481        // Typed accessor: AskUserQuestionInput has the same shape as the
3482        // AskUserQuestion tool_use_result.
3483        let typed: crate::AskUserQuestionInput = user
3484            .tool_use_result_as::<crate::AskUserQuestionInput>()
3485            .expect("tool_use_result present")
3486            .expect("AskUserQuestionInput parses");
3487        assert_eq!(typed.questions.len(), 1);
3488        assert_eq!(typed.questions[0].header, "Color");
3489        let answers = typed.answers.expect("answers populated");
3490        assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
3491    }
3492
3493    /// User messages without `tool_use_result` / `timestamp` must still
3494    /// deserialize fine and serialize back without spuriously emitting nulls.
3495    #[test]
3496    fn test_user_message_without_tool_use_result_omits_field() {
3497        let json = r#"{
3498            "type":"user",
3499            "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
3500            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
3501        }"#;
3502
3503        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3504        let user = match output {
3505            ClaudeOutput::User(u) => u,
3506            _ => panic!("Expected User message"),
3507        };
3508        assert!(user.tool_use_result.is_none());
3509        assert!(user.timestamp.is_none());
3510
3511        let reser = serde_json::to_value(&user).unwrap();
3512        assert!(reser.get("tool_use_result").is_none());
3513        assert!(reser.get("timestamp").is_none());
3514    }
3515
3516    /// A `Task` tool result must expose subagent token / timing / tool-use
3517    /// accounting through the typed [`UserMessage::subagent_result`] accessor,
3518    /// including the nested per-model `usage` breakdown and `toolStats`.
3519    #[test]
3520    fn test_subagent_result_exposes_token_accounting() {
3521        let json = r#"{
3522            "type":"user",
3523            "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
3524            "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
3525            "tool_use_result":{
3526                "status":"completed",
3527                "prompt":"Count the .rs files.",
3528                "agentId":"ac4f0276e9d4b6232",
3529                "agentType":"Explore",
3530                "content":[{"type":"text","text":"21"}],
3531                "resolvedModel":"claude-haiku-4-5-20251001",
3532                "totalDurationMs":6869,
3533                "totalTokens":7834,
3534                "totalToolUseCount":1,
3535                "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
3536                "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
3537            }
3538        }"#;
3539
3540        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3541        let user = match output {
3542            ClaudeOutput::User(u) => u,
3543            _ => panic!("Expected User message"),
3544        };
3545
3546        let result = user.subagent_result().expect("subagent result parses");
3547        assert_eq!(result.agent_type.as_deref(), Some("Explore"));
3548        assert_eq!(
3549            result.resolved_model.as_deref(),
3550            Some("claude-haiku-4-5-20251001")
3551        );
3552        assert_eq!(result.total_tokens, Some(7834));
3553        assert_eq!(result.total_duration_ms, Some(6869));
3554        assert_eq!(result.total_tool_use_count, Some(1));
3555
3556        let usage = result.usage.expect("nested usage present");
3557        assert_eq!(usage.input_tokens, 6);
3558        assert_eq!(usage.cache_read_input_tokens, 7699);
3559
3560        let stats = result.tool_stats.expect("toolStats present");
3561        assert_eq!(stats.bash_count, 1);
3562    }
3563
3564    /// `tool_use_result` shapes that aren't subagent runs (e.g. AskUserQuestion)
3565    /// parse leniently into the all-`Option` [`SubagentResult`] with empty
3566    /// accounting rather than failing, so callers can probe without panicking.
3567    #[test]
3568    fn test_subagent_result_absent_for_non_task_result() {
3569        let json = r#"{
3570            "type":"user",
3571            "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
3572            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3573            "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
3574        }"#;
3575
3576        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3577        let user = match output {
3578            ClaudeOutput::User(u) => u,
3579            _ => panic!("Expected User message"),
3580        };
3581
3582        let result = user.subagent_result().expect("lenient parse");
3583        assert_eq!(result.total_tokens, None);
3584        assert_eq!(result.agent_type, None);
3585    }
3586
3587    #[test]
3588    fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
3589        use serde_json::Value;
3590
3591        let raw: Value = serde_json::from_str(
3592            r#"{
3593            "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
3594            "fast_mode_state":"off",
3595            "fast_mode_disabled_reason":"not_first_party",
3596            "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
3597        }"#,
3598        )
3599        .unwrap();
3600        crate::io::assert_fully_wrapped(&raw);
3601
3602        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3603        let ClaudeOutput::System(sys) = output else {
3604            panic!("expected System");
3605        };
3606        let init = sys.as_init().expect("parses as init");
3607        assert_eq!(
3608            init.fast_mode_disabled_reason,
3609            Some(crate::FastModeDisabledReason::NotFirstParty)
3610        );
3611        let errs = init.mcp_server_errors.unwrap();
3612        assert_eq!(errs.len(), 1);
3613        assert_eq!(errs[0].name, "broken");
3614        assert_eq!(errs[0].error_type, "invalid_config");
3615    }
3616
3617    #[test]
3618    fn test_code_change_published_fully_wrapped() {
3619        use super::{KnownSystemEvent, SystemSubtype};
3620        use serde_json::Value;
3621
3622        let raw: Value = serde_json::from_str(
3623            r#"{
3624            "type":"system","subtype":"code_change_published",
3625            "provider":"github","url":"https://github.com/owner/repo/pull/42",
3626            "repo":"owner/repo","identifier":"42",
3627            "uuid":"u1","session_id":"s1"
3628        }"#,
3629        )
3630        .unwrap();
3631        crate::io::assert_fully_wrapped(&raw);
3632
3633        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3634        let ClaudeOutput::System(sys) = output else {
3635            panic!("expected System");
3636        };
3637        assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
3638        let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
3639            panic!("expected CodeChangePublished event");
3640        };
3641        assert_eq!(msg.provider, "github");
3642        assert_eq!(msg.repo, "owner/repo");
3643        assert_eq!(msg.identifier, "42");
3644
3645        assert!(sys.is_code_change_published());
3646        assert!(!sys.is_vcs_state_changed());
3647        let direct = sys.as_code_change_published().expect("direct accessor");
3648        assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
3649        assert!(sys.as_vcs_state_changed().is_none());
3650    }
3651
3652    #[test]
3653    fn test_feedback_draft_queued_fully_wrapped() {
3654        use super::{KnownSystemEvent, SystemSubtype};
3655        use serde_json::Value;
3656
3657        let raw: Value = serde_json::from_str(
3658            r#"{
3659            "type":"system","subtype":"feedback_draft_queued",
3660            "draft_id":"draft-1","draft_type":"bug_report",
3661            "title":"Tool output was truncated",
3662            "details_preview":"The last command omitted its final lines",
3663            "uuid":"u1","session_id":"s1","future_field":"preserved"
3664        }"#,
3665        )
3666        .unwrap();
3667        crate::io::assert_fully_wrapped(&raw);
3668
3669        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3670        let ClaudeOutput::System(sys) = output else {
3671            panic!("expected System");
3672        };
3673        assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
3674        assert!(sys.is_feedback_draft_queued());
3675        assert!(!sys.is_vcs_state_changed());
3676
3677        let direct = sys
3678            .as_feedback_draft_queued()
3679            .expect("direct typed accessor");
3680        assert_eq!(direct.draft_id, "draft-1");
3681        assert_eq!(direct.draft_type, "bug_report");
3682        assert_eq!(direct.extra["future_field"], "preserved");
3683
3684        let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
3685            panic!("expected FeedbackDraftQueued event");
3686        };
3687        assert_eq!(known.title, "Tool output was truncated");
3688        assert_eq!(
3689            sys.typed_value().expect("typed value")["future_field"],
3690            "preserved"
3691        );
3692    }
3693
3694    #[test]
3695    fn test_cloud_session_delta_fully_wrapped() {
3696        use super::{KnownSystemEvent, SystemSubtype};
3697        use serde_json::Value;
3698
3699        let raw: Value = serde_json::from_str(
3700            r#"{
3701            "type":"system","subtype":"cloud_session_delta",
3702            "seq":3,"changed":["serving","connection"],
3703            "cloud_session":{"id":"session_abc","view_url":"https://example.invalid/s/abc",
3704                "serving":{"state":"on"},"connection":{"state":"live"}},
3705            "uuid":"u1","session_id":"s1","future_field":"preserved"
3706        }"#,
3707        )
3708        .unwrap();
3709        crate::io::assert_fully_wrapped(&raw);
3710
3711        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3712        let ClaudeOutput::System(sys) = output else {
3713            panic!("expected System");
3714        };
3715        assert_eq!(sys.subtype, SystemSubtype::CloudSessionDelta);
3716        assert!(sys.is_cloud_session_delta());
3717        assert!(!sys.is_feedback_draft_queued());
3718
3719        let direct = sys.as_cloud_session_delta().expect("direct typed accessor");
3720        assert_eq!(direct.seq, 3);
3721        assert_eq!(direct.changed, vec!["serving", "connection"]);
3722        assert_eq!(direct.cloud_session["id"], "session_abc");
3723        assert_eq!(direct.extra["future_field"], "preserved");
3724
3725        let Some(KnownSystemEvent::CloudSessionDelta(known)) = sys.as_known_system_event() else {
3726            panic!("expected CloudSessionDelta event");
3727        };
3728        assert_eq!(known.session_id, "s1");
3729        assert_eq!(
3730            sys.typed_value().expect("typed value")["future_field"],
3731            "preserved"
3732        );
3733    }
3734
3735    #[test]
3736    fn test_vcs_state_changed_fully_wrapped() {
3737        use super::{KnownSystemEvent, VcsMutationKind};
3738        use serde_json::Value;
3739
3740        for kind in ["commit", "push", "merge", "rebase"] {
3741            let raw: Value = serde_json::from_str(&format!(
3742                r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
3743                kind
3744            ))
3745            .unwrap();
3746            crate::io::assert_fully_wrapped(&raw);
3747
3748            let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3749            let ClaudeOutput::System(sys) = output else {
3750                panic!("expected System");
3751            };
3752            let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3753                panic!("expected VcsStateChanged event");
3754            };
3755            assert_eq!(msg.kind.as_str(), kind);
3756            assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
3757        }
3758
3759        // Unknown kinds are valid per the wire contract.
3760        let raw: Value = serde_json::from_str(
3761            r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
3762        )
3763        .unwrap();
3764        crate::io::assert_fully_wrapped(&raw);
3765        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3766        let ClaudeOutput::System(sys) = output else {
3767            panic!("expected System");
3768        };
3769        let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3770            panic!("expected VcsStateChanged event");
3771        };
3772        assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
3773
3774        assert!(sys.is_vcs_state_changed());
3775        let direct = sys.as_vcs_state_changed().expect("direct accessor");
3776        assert_eq!(direct.cwd, "/repo");
3777        assert!(sys.as_code_change_published().is_none());
3778    }
3779
3780    #[test]
3781    fn test_assistant_aborted_and_resume_flags_roundtrip() {
3782        let json = r#"{
3783            "type":"assistant",
3784            "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
3785            "session_id":"s1",
3786            "aborted":true,
3787            "resumed_from_incomplete_thinking":true
3788        }"#;
3789        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3790        let ClaudeOutput::Assistant(msg) = &output else {
3791            panic!("expected Assistant");
3792        };
3793        assert_eq!(msg.aborted, Some(true));
3794        assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
3795        let reserialized = serde_json::to_string(&output).unwrap();
3796        assert!(reserialized.contains("\"aborted\":true"));
3797        assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
3798
3799        // Absent flags stay absent on the wire.
3800        let json = r#"{
3801            "type":"assistant",
3802            "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
3803            "session_id":"s2"
3804        }"#;
3805        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3806        let reserialized = serde_json::to_string(&output).unwrap();
3807        assert!(!reserialized.contains("aborted"));
3808        assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
3809    }
3810
3811    #[test]
3812    fn test_user_tool_result_meta_roundtrip() {
3813        let json = r#"{
3814            "type":"user",
3815            "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
3816            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3817            "tool_result_meta":[
3818                {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
3819                {"id":"toolu_2","non_execution_kind":"permission-rule"}
3820            ]
3821        }"#;
3822        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3823        let ClaudeOutput::User(user) = &output else {
3824            panic!("expected User");
3825        };
3826        let meta = user.tool_result_meta.as_ref().unwrap();
3827        assert_eq!(meta.len(), 2);
3828        assert_eq!(meta[0].non_execution_kind, "user-rejected");
3829        assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
3830        assert_eq!(meta[1].user_feedback, None);
3831
3832        let reserialized = serde_json::to_string(&output).unwrap();
3833        assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
3834        assert!(!reserialized.contains("\"user_feedback\":null"));
3835    }
3836
3837    /// CLI 2.1.222 added `scope` to `system/model_refusal_fallback`:
3838    /// "session" (main-thread swap, also the meaning when absent on older
3839    /// CLIs) vs "local" (subagent/side-question fallback only).
3840    #[test]
3841    fn model_refusal_fallback_scope_roundtrips_and_defaults() {
3842        use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
3843        let with_scope = serde_json::json!({
3844            "trigger": "refusal",
3845            "direction": "retry",
3846            "scope": "local",
3847            "original_model": "claude-fable-5",
3848            "fallback_model": "claude-opus-5",
3849            "request_id": null,
3850            "content": "Refused; retried on fallback model.",
3851            "uuid": "u1",
3852            "session_id": "s1"
3853        });
3854        let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
3855        assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
3856        assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
3857
3858        // Older CLIs omit scope — absent, not null, and treated as session
3859        // by consumers per the wire docs.
3860        let mut without = with_scope.clone();
3861        without.as_object_mut().unwrap().remove("scope");
3862        let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
3863        assert_eq!(msg.scope, None);
3864        assert_eq!(serde_json::to_value(&msg).unwrap(), without);
3865
3866        // Open enum: unknown scopes pass through verbatim.
3867        assert_eq!(
3868            RefusalFallbackScope::from("workspace").as_str(),
3869            "workspace"
3870        );
3871    }
3872}