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 top-level assistant message so a consumer can bind
2677    /// the reply to the send it answers without waiting for the result. With
2678    /// `--include-partial-messages` the first non-ping stream event is
2679    /// stamped too, independently (CLI 2.1.269+), so the same uuid may
2680    /// appear on both frames. Absent on every later assistant message of the
2681    /// turn, on subagent frames, on synthetic/scheduled (meta) turns, and
2682    /// from CLIs before 2.1.259.
2683    #[serde(default, skip_serializing_if = "Option::is_none")]
2684    pub user_message_uuid: Option<String>,
2685    /// Client uuids of every user message whose prompt this turn has consumed
2686    /// so far, in consumption order — all members of a prompt batch the host
2687    /// merged into this one turn. Always contains `user_message_uuid`; at
2688    /// most 64 entries. Present exactly when `user_message_uuid` is; absent
2689    /// from CLIs before 2.1.259 (fall back to `user_message_uuid`).
2690    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2691    pub user_message_uuids: Vec<String>,
2692    /// Subagent type, when this assistant message was produced inside a
2693    /// `local_agent` subagent (e.g. `general-purpose`, `Explore`).
2694    #[serde(skip_serializing_if = "Option::is_none")]
2695    pub subagent_type: Option<String>,
2696    /// Short description of the subagent task, present alongside `subagent_type`.
2697    #[serde(skip_serializing_if = "Option::is_none")]
2698    pub task_description: Option<String>,
2699    #[serde(skip_serializing_if = "Option::is_none")]
2700    pub error: Option<AssistantErrorKind>,
2701    /// True when this message was truncated by an interrupt/abort before the
2702    /// stream completed — `stop_reason` was never received and the content
2703    /// may end mid-word. Absent on normally completed messages.
2704    #[serde(default, skip_serializing_if = "Option::is_none")]
2705    pub aborted: Option<bool>,
2706    /// True when this turn continued the preceding truncated assistant turn
2707    /// inside its trailing signed thinking block (max-output-tokens
2708    /// recovery). Histories replayed through the bridge must carry the flag
2709    /// back so the normalizer keeps the run's prefix on the wire.
2710    #[serde(default, skip_serializing_if = "Option::is_none")]
2711    pub resumed_from_incomplete_thinking: Option<bool>,
2712    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2713    pub supersedes: Vec<String>,
2714    #[serde(skip_serializing_if = "Option::is_none")]
2715    pub timestamp: Option<String>,
2716    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2717    pub tool_use_meta: Vec<ToolUseMeta>,
2718    /// `{id, name}` of the original `Batch*` tool_use block(s) for a message
2719    /// whose content was decomposed into synthetic v1 tool_use blocks.
2720    /// Round-tripped so a replayed history reassembles the batch block on the
2721    /// wire. Wrapper-level sibling — never inside `message.content`.
2722    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2723    pub batch_tool_uses: Vec<BatchToolUse>,
2724    /// `tool_use.input` exactly as the API produced it, keyed by `tool_use`
2725    /// id, for a message whose `message.content` carries the
2726    /// client-normalized input. Round-tripped so a replayed history echoes
2727    /// each earlier tool call back to the API as the API emitted it.
2728    /// Wrapper-level sibling — never inside `message.content` (CLI 2.1.259+).
2729    #[serde(default, skip_serializing_if = "Option::is_none")]
2730    pub wire_tool_inputs: Option<serde_json::Map<String, Value>>,
2731    /// What the client-side input normalization read from process state for
2732    /// the inputs in [`wire_tool_inputs`](Self::wire_tool_inputs), keyed by
2733    /// the same `tool_use` ids. Round-tripped so a replayed history can verify
2734    /// each recorded input against the normalized one. Wrapper-level sibling —
2735    /// never inside `message.content` (CLI 2.1.266+).
2736    #[serde(default, skip_serializing_if = "Option::is_none")]
2737    pub wire_ingest_context: Option<serde_json::Map<String, Value>>,
2738    /// Replayed history rather than a live message, stamped by the Remote
2739    /// Control bridge when it flushes history to the session server, which
2740    /// also stamps it on deliveries it replays (CLI 2.1.266+).
2741    #[serde(default, skip_serializing_if = "Option::is_none")]
2742    pub historical: Option<bool>,
2743    /// Why this frame's turn is the automatic re-run of a turn a worker
2744    /// restart interrupted: the host's `CLAUDE_CODE_RESUME_REASON` when it set
2745    /// one (`host_draining`, `checkpoint_restore`, `container_recreated`,
2746    /// ...), else `interrupted_turn`. Stamped on the same reply frames as
2747    /// `user_message_uuid`, which on such a re-run names the interrupted
2748    /// turn's own last user prompt. Absent on every other turn and from CLIs
2749    /// before 2.1.268.
2750    #[serde(default, skip_serializing_if = "Option::is_none")]
2751    pub resume_reason: Option<String>,
2752    /// The originating `system/local_command` row's wire-form content,
2753    /// carried on the loop-synthesized local-command twin so a bridge/SDK
2754    /// history replay rebuilds the internal system row instead of dropping
2755    /// the output. Wrapper-level sibling — never inside `message.content`
2756    /// (CLI 2.1.259+).
2757    #[serde(default, skip_serializing_if = "Option::is_none")]
2758    pub local_command_source: Option<String>,
2759    /// Ascending zero-based indexes into `message.content` of the thinking
2760    /// blocks whose signature the server tagged as narration (server
2761    /// summaries of the prose between tool calls, not the model's own
2762    /// reasoning), so a renderer can label them as summaries without decoding
2763    /// the signature envelope. Fail-closed: an unparseable or legacy
2764    /// signature is not listed. Omitted when the frame has no such block and
2765    /// by CLIs before 2.1.260; treat unlisted thinking blocks as ordinary
2766    /// thinking. Wrapper-level sibling — never inside `message.content`.
2767    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2768    pub narration_block_indexes: Vec<usize>,
2769    /// Structured twin of the `/context` report, carried on the synthetic
2770    /// assistant message that delivers the markdown table. Present only on
2771    /// `/context` results from CLIs new enough to attach it (2.1.239+).
2772    #[serde(default, skip_serializing_if = "Option::is_none")]
2773    pub context_usage: Option<ContextUsage>,
2774    #[serde(default, skip_serializing_if = "Option::is_none")]
2775    pub is_meta: Option<bool>,
2776    #[serde(default, skip_serializing_if = "Option::is_none")]
2777    pub is_virtual: Option<bool>,
2778    #[serde(default, skip_serializing_if = "Option::is_none")]
2779    pub is_api_error_message: Option<bool>,
2780    #[serde(skip_serializing_if = "Option::is_none")]
2781    pub api_error_status: Option<u16>,
2782    #[serde(skip_serializing_if = "Option::is_none")]
2783    pub api_error: Option<String>,
2784    #[serde(skip_serializing_if = "Option::is_none")]
2785    pub error_details: Option<String>,
2786    #[serde(skip_serializing_if = "Option::is_none")]
2787    pub advisor_model: Option<String>,
2788    #[serde(skip_serializing_if = "Option::is_none")]
2789    pub attribution_agent: Option<String>,
2790    #[serde(skip_serializing_if = "Option::is_none")]
2791    pub attribution_skill: Option<String>,
2792    #[serde(skip_serializing_if = "Option::is_none")]
2793    pub attribution_plugin: Option<String>,
2794    #[serde(skip_serializing_if = "Option::is_none")]
2795    pub attribution_mcp_server: Option<String>,
2796    #[serde(skip_serializing_if = "Option::is_none")]
2797    pub attribution_mcp_tool: Option<String>,
2798}
2799
2800/// Nested message content for assistant messages
2801#[derive(Debug, Clone, Serialize, Deserialize)]
2802pub struct AssistantMessageContent {
2803    pub id: String,
2804    /// The Anthropic API message type — always `"message"`.
2805    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
2806    pub message_type: Option<String>,
2807    pub role: MessageRole,
2808    pub model: String,
2809    pub content: Vec<ContentBlock>,
2810    #[serde(skip_serializing_if = "Option::is_none")]
2811    pub stop_reason: Option<StopReason>,
2812    #[serde(skip_serializing_if = "Option::is_none")]
2813    pub stop_sequence: Option<String>,
2814    #[serde(skip_serializing_if = "Option::is_none")]
2815    pub usage: Option<AssistantUsage>,
2816    /// Details about why generation stopped
2817    #[serde(skip_serializing_if = "Option::is_none")]
2818    pub stop_details: Option<Value>,
2819    /// Context management metadata
2820    #[serde(skip_serializing_if = "Option::is_none")]
2821    pub context_management: Option<Value>,
2822}
2823
2824/// Usage information for assistant messages
2825#[derive(Debug, Clone, Serialize, Deserialize)]
2826pub struct AssistantUsage {
2827    /// Number of input tokens
2828    #[serde(default)]
2829    pub input_tokens: u32,
2830
2831    /// Number of output tokens
2832    #[serde(default)]
2833    pub output_tokens: u32,
2834
2835    /// Tokens used to create cache
2836    #[serde(default)]
2837    pub cache_creation_input_tokens: u32,
2838
2839    /// Tokens read from cache
2840    #[serde(default)]
2841    pub cache_read_input_tokens: u32,
2842
2843    /// Service tier used (e.g., "standard")
2844    #[serde(skip_serializing_if = "Option::is_none")]
2845    pub service_tier: Option<String>,
2846
2847    /// Detailed cache creation breakdown
2848    #[serde(skip_serializing_if = "Option::is_none")]
2849    pub cache_creation: Option<CacheCreationDetails>,
2850
2851    /// Inference geography (e.g., "not_available")
2852    #[serde(skip_serializing_if = "Option::is_none")]
2853    pub inference_geo: Option<String>,
2854}
2855
2856/// Detailed cache creation information
2857#[derive(Debug, Clone, Serialize, Deserialize)]
2858pub struct CacheCreationDetails {
2859    /// Ephemeral 1-hour input tokens
2860    #[serde(default)]
2861    pub ephemeral_1h_input_tokens: u32,
2862
2863    /// Ephemeral 5-minute input tokens
2864    #[serde(default)]
2865    pub ephemeral_5m_input_tokens: u32,
2866}
2867
2868#[cfg(test)]
2869mod tests {
2870    use crate::io::ClaudeOutput;
2871
2872    #[test]
2873    fn test_subagent_usage_rollup_accumulates_task_results() {
2874        use super::SubagentUsageRollup;
2875
2876        let mut rollup = SubagentUsageRollup::default();
2877
2878        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}}"#;
2879        let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
2880        assert!(rollup.observe(&output));
2881        assert_eq!(rollup.subagent_tokens, 10201);
2882        assert_eq!(rollup.agent_count, 1);
2883        assert_eq!(rollup.tool_uses, 3);
2884        assert_eq!(rollup.duration_ms, 1853);
2885
2886        // Replayed frame with the same agentId is counted once.
2887        assert!(!rollup.observe(&output));
2888        assert_eq!(rollup.agent_count, 1);
2889        assert_eq!(rollup.subagent_tokens, 10201);
2890
2891        // A second agent accumulates.
2892        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}}"#;
2893        let output: ClaudeOutput = serde_json::from_str(second).unwrap();
2894        assert!(rollup.observe(&output));
2895        assert_eq!(rollup.agent_count, 2);
2896        assert_eq!(rollup.subagent_tokens, 10701);
2897    }
2898
2899    #[test]
2900    fn test_subagent_usage_rollup_ignores_non_task_results() {
2901        use super::SubagentUsageRollup;
2902
2903        let mut rollup = SubagentUsageRollup::default();
2904
2905        // A ToolSearch tool_use_result parses as an all-None SubagentResult;
2906        // it must not count as a subagent.
2907        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}}"#;
2908        let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
2909        assert!(!rollup.observe(&output));
2910
2911        // Plain user message without tool_use_result.
2912        let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2913        let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
2914        assert!(!rollup.observe(&output));
2915
2916        // Non-user frames are ignored.
2917        let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2918        let output: ClaudeOutput = serde_json::from_str(system).unwrap();
2919        assert!(!rollup.observe(&output));
2920
2921        assert_eq!(rollup, SubagentUsageRollup::default());
2922    }
2923
2924    #[test]
2925    fn test_subagent_usage_rollup_over_captured_session() {
2926        use super::SubagentUsageRollup;
2927
2928        let mut rollup = SubagentUsageRollup::default();
2929        let fixture =
2930            include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
2931        for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
2932            if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
2933                rollup.observe(&output);
2934            }
2935        }
2936        assert_eq!(rollup.agent_count, 1);
2937        assert_eq!(rollup.subagent_tokens, 10201);
2938    }
2939
2940    #[test]
2941    fn test_system_message_init() {
2942        let json = r#"{
2943            "type": "system",
2944            "subtype": "init",
2945            "session_id": "test-session-123",
2946            "cwd": "/home/user/project",
2947            "model": "claude-sonnet-4",
2948            "tools": ["Bash", "Read", "Write"],
2949            "mcp_servers": [],
2950            "slash_commands": ["compact", "cost", "review"],
2951            "agents": ["Bash", "Explore", "Plan"],
2952            "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
2953            "skills": [],
2954            "claude_code_version": "2.1.15",
2955            "apiKeySource": "none",
2956            "output_style": "default",
2957            "permissionMode": "default"
2958        }"#;
2959
2960        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2961        if let ClaudeOutput::System(sys) = output {
2962            assert!(sys.is_init());
2963            assert!(!sys.is_status());
2964            assert!(!sys.is_compact_boundary());
2965
2966            let init = sys.as_init().expect("Should parse as init");
2967            assert_eq!(init.session_id, "test-session-123");
2968            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2969            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2970            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2971            assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
2972            assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
2973            assert_eq!(init.plugins.len(), 1);
2974            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2975            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2976            assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
2977            assert_eq!(init.output_style, Some(super::OutputStyle::Default));
2978            assert_eq!(
2979                init.permission_mode,
2980                Some(super::InitPermissionMode::Default)
2981            );
2982        } else {
2983            panic!("Expected System message");
2984        }
2985    }
2986
2987    #[test]
2988    fn test_system_message_init_from_real_capture() {
2989        let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
2990        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2991        if let ClaudeOutput::System(sys) = output {
2992            let init = sys.as_init().expect("Should parse real init capture");
2993            assert_eq!(init.slash_commands.len(), 8);
2994            assert!(init.slash_commands.contains(&"compact".to_string()));
2995            assert!(init.slash_commands.contains(&"review".to_string()));
2996            assert_eq!(init.agents.len(), 5);
2997            assert!(init.agents.contains(&"Bash".to_string()));
2998            assert!(init.agents.contains(&"Explore".to_string()));
2999            assert_eq!(init.plugins.len(), 1);
3000            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
3001            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
3002        } else {
3003            panic!("Expected System message");
3004        }
3005    }
3006
3007    #[test]
3008    fn test_system_message_status() {
3009        let json = r#"{
3010            "type": "system",
3011            "subtype": "status",
3012            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3013            "status": "compacting",
3014            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
3015        }"#;
3016
3017        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3018        if let ClaudeOutput::System(sys) = output {
3019            assert!(sys.is_status());
3020            assert!(!sys.is_init());
3021
3022            let status = sys.as_status().expect("Should parse as status");
3023            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
3024            assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
3025            assert_eq!(
3026                status.uuid,
3027                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
3028            );
3029        } else {
3030            panic!("Expected System message");
3031        }
3032    }
3033
3034    #[test]
3035    fn test_system_message_status_null() {
3036        let json = r#"{
3037            "type": "system",
3038            "subtype": "status",
3039            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3040            "status": null,
3041            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
3042        }"#;
3043
3044        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3045        if let ClaudeOutput::System(sys) = output {
3046            let status = sys.as_status().expect("Should parse as status");
3047            assert_eq!(status.status, None);
3048        } else {
3049            panic!("Expected System message");
3050        }
3051    }
3052
3053    #[test]
3054    fn test_system_message_task_started() {
3055        let json = r#"{
3056            "type": "system",
3057            "subtype": "task_started",
3058            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
3059            "task_id": "b6daf3f",
3060            "task_type": "local_bash",
3061            "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
3062            "description": "Wait for CI on PR #12",
3063            "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
3064        }"#;
3065
3066        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3067        if let ClaudeOutput::System(sys) = output {
3068            assert!(sys.is_task_started());
3069            assert!(!sys.is_task_progress());
3070            assert!(!sys.is_task_notification());
3071
3072            let task = sys.as_task_started().expect("Should parse as task_started");
3073            assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
3074            assert_eq!(task.task_id, "b6daf3f");
3075            assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
3076            assert_eq!(
3077                task.tool_use_id.as_deref(),
3078                Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
3079            );
3080            assert_eq!(task.description, "Wait for CI on PR #12");
3081        } else {
3082            panic!("Expected System message");
3083        }
3084    }
3085
3086    #[test]
3087    fn test_system_message_task_started_agent() {
3088        let json = r#"{
3089            "type": "system",
3090            "subtype": "task_started",
3091            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3092            "task_id": "a4a7e0906e5fc64cc",
3093            "task_type": "local_agent",
3094            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
3095            "description": "Explore Scene/ArrayScene duplication",
3096            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
3097        }"#;
3098
3099        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3100        if let ClaudeOutput::System(sys) = output {
3101            let task = sys.as_task_started().expect("Should parse as task_started");
3102            assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
3103            assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
3104        } else {
3105            panic!("Expected System message");
3106        }
3107    }
3108
3109    #[test]
3110    fn test_system_message_task_progress() {
3111        let json = r#"{
3112            "type": "system",
3113            "subtype": "task_progress",
3114            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3115            "task_id": "a4a7e0906e5fc64cc",
3116            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
3117            "description": "Reading src/jplephem/chebyshev.rs",
3118            "last_tool_name": "Read",
3119            "usage": {
3120                "duration_ms": 13996,
3121                "tool_uses": 9,
3122                "total_tokens": 38779
3123            },
3124            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
3125        }"#;
3126
3127        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3128        if let ClaudeOutput::System(sys) = output {
3129            assert!(sys.is_task_progress());
3130            assert!(!sys.is_task_started());
3131
3132            let progress = sys
3133                .as_task_progress()
3134                .expect("Should parse as task_progress");
3135            assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
3136            assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
3137            assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
3138            assert_eq!(progress.usage.duration_ms, 13996);
3139            assert_eq!(progress.usage.tool_uses, 9);
3140            assert_eq!(progress.usage.total_tokens, 38779);
3141        } else {
3142            panic!("Expected System message");
3143        }
3144    }
3145
3146    #[test]
3147    fn test_system_message_task_notification_completed() {
3148        let json = r#"{
3149            "type": "system",
3150            "subtype": "task_notification",
3151            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
3152            "task_id": "a0ba761e9dc9c316f",
3153            "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
3154            "status": "completed",
3155            "summary": "Agent \"Write Hipparcos data source doc\" completed",
3156            "output_file": "",
3157            "usage": {
3158                "duration_ms": 172300,
3159                "tool_uses": 11,
3160                "total_tokens": 42005
3161            },
3162            "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
3163        }"#;
3164
3165        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3166        if let ClaudeOutput::System(sys) = output {
3167            assert!(sys.is_task_notification());
3168
3169            let notif = sys
3170                .as_task_notification()
3171                .expect("Should parse as task_notification");
3172            assert_eq!(notif.status, super::TaskStatus::Completed);
3173            assert_eq!(
3174                notif.summary,
3175                "Agent \"Write Hipparcos data source doc\" completed"
3176            );
3177            assert_eq!(notif.output_file, Some("".to_string()));
3178            assert_eq!(
3179                notif.tool_use_id,
3180                Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
3181            );
3182            let usage = notif.usage.expect("Should have usage");
3183            assert_eq!(usage.duration_ms, 172300);
3184            assert_eq!(usage.tool_uses, 11);
3185            assert_eq!(usage.total_tokens, 42005);
3186        } else {
3187            panic!("Expected System message");
3188        }
3189    }
3190
3191    #[test]
3192    fn test_system_message_task_notification_failed_no_usage() {
3193        let json = r#"{
3194            "type": "system",
3195            "subtype": "task_notification",
3196            "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
3197            "task_id": "b98f6a3",
3198            "status": "failed",
3199            "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
3200            "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
3201        }"#;
3202
3203        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3204        if let ClaudeOutput::System(sys) = output {
3205            let notif = sys
3206                .as_task_notification()
3207                .expect("Should parse as task_notification");
3208            assert_eq!(notif.status, super::TaskStatus::Failed);
3209            assert!(notif.tool_use_id.is_none());
3210            assert!(notif.usage.is_none());
3211            assert_eq!(
3212                notif.output_file,
3213                Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
3214            );
3215        } else {
3216            panic!("Expected System message");
3217        }
3218    }
3219
3220    /// Task system messages survive a `to_value` → `from_value` round-trip
3221    /// with their typed accessors still resolving. Mirrors the proxy/relay
3222    /// path where output is reparsed from a `serde_json::Value` rather than
3223    /// straight from the CLI's stdout, so a silently dropped or renamed field
3224    /// surfaces here instead of as a `None` downstream.
3225    #[test]
3226    fn test_task_messages_roundtrip_through_value() {
3227        let cases = [
3228            r#"{"type":"system","subtype":"task_started","session_id":"s1",
3229                "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
3230                "description":"Sleep 3s","uuid":"u1"}"#,
3231            r#"{"type":"system","subtype":"task_progress","session_id":"s1",
3232                "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
3233                "last_tool_name":"Bash",
3234                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
3235                "uuid":"u2"}"#,
3236            r#"{"type":"system","subtype":"task_notification","session_id":"s1",
3237                "task_id":"t1","tool_use_id":"tu1","status":"completed",
3238                "summary":"done","output_file":"",
3239                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
3240                "uuid":"u3"}"#,
3241        ];
3242
3243        for json in cases {
3244            let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3245            let value = serde_json::to_value(&output).unwrap();
3246            let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
3247
3248            let ClaudeOutput::System(sys) = reparsed else {
3249                panic!("Expected System variant after round-trip");
3250            };
3251
3252            match sys.subtype {
3253                super::SystemSubtype::TaskStarted => {
3254                    assert!(
3255                        sys.as_task_started().is_some(),
3256                        "as_task_started failed after round-trip"
3257                    );
3258                }
3259                super::SystemSubtype::TaskProgress => {
3260                    assert!(
3261                        sys.as_task_progress().is_some(),
3262                        "as_task_progress failed after round-trip"
3263                    );
3264                }
3265                super::SystemSubtype::TaskNotification => {
3266                    assert!(
3267                        sys.as_task_notification().is_some(),
3268                        "as_task_notification failed after round-trip"
3269                    );
3270                }
3271                other => panic!("unexpected subtype after round-trip: {other:?}"),
3272            }
3273        }
3274    }
3275
3276    #[test]
3277    fn test_system_message_compact_boundary() {
3278        let json = r#"{
3279            "type": "system",
3280            "subtype": "compact_boundary",
3281            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
3282            "compact_metadata": {
3283                "pre_tokens": 155285,
3284                "trigger": "auto"
3285            },
3286            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
3287        }"#;
3288
3289        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3290        if let ClaudeOutput::System(sys) = output {
3291            assert!(sys.is_compact_boundary());
3292            assert!(!sys.is_init());
3293            assert!(!sys.is_status());
3294
3295            let compact = sys
3296                .as_compact_boundary()
3297                .expect("Should parse as compact_boundary");
3298            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
3299            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
3300            assert_eq!(
3301                compact.compact_metadata.trigger,
3302                super::CompactionTrigger::Auto
3303            );
3304            // Per-compaction stats are optional and absent here.
3305            assert!(compact.summary.is_none());
3306            assert!(compact.leaf_message_count.is_none());
3307            assert!(compact.duration_ms.is_none());
3308        } else {
3309            panic!("Expected System message");
3310        }
3311    }
3312
3313    #[test]
3314    fn test_compact_boundary_with_summary_stats() {
3315        // Canonical keys.
3316        let json = r#"{
3317            "type": "system",
3318            "subtype": "compact_boundary",
3319            "session_id": "s1",
3320            "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
3321            "summary": "Summarized the earlier exploration.",
3322            "leaf_message_count": 42,
3323            "duration_ms": 1234,
3324            "uuid": "u1"
3325        }"#;
3326        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3327        let ClaudeOutput::System(sys) = output else {
3328            panic!("Expected System message");
3329        };
3330        let compact = sys.as_compact_boundary().expect("compact_boundary");
3331        assert_eq!(
3332            compact.summary.as_deref(),
3333            Some("Summarized the earlier exploration.")
3334        );
3335        assert_eq!(compact.leaf_message_count, Some(42));
3336        assert_eq!(compact.duration_ms, Some(1234));
3337
3338        // Alternate wire keys (`content` for summary, `message_count` for count)
3339        // deserialize into the same fields.
3340        let json_alt = r#"{
3341            "type": "system",
3342            "subtype": "compact_boundary",
3343            "session_id": "s2",
3344            "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
3345            "content": "alt-key summary",
3346            "message_count": 7
3347        }"#;
3348        let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
3349        let ClaudeOutput::System(sys) = output else {
3350            panic!("Expected System message");
3351        };
3352        let compact = sys.as_compact_boundary().expect("compact_boundary");
3353        assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
3354        assert_eq!(compact.leaf_message_count, Some(7));
3355    }
3356
3357    #[test]
3358    fn test_init_message_with_new_fields() {
3359        let json = r#"{
3360            "type": "system",
3361            "subtype": "init",
3362            "session_id": "test-session",
3363            "cwd": "/home/user",
3364            "model": "claude-opus-4-7",
3365            "tools": ["Bash"],
3366            "mcp_servers": [],
3367            "permissionMode": "default",
3368            "apiKeySource": "none",
3369            "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
3370            "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
3371            "fast_mode_state": "off",
3372            "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
3373            "claude_code_version": "2.1.117"
3374        }"#;
3375
3376        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3377        if let ClaudeOutput::System(sys) = output {
3378            let init = sys.as_init().expect("Should parse as init");
3379            assert_eq!(
3380                init.uuid.as_deref(),
3381                Some("44841a0d-182d-493a-86b5-79800d3d9665")
3382            );
3383            assert!(init.memory_paths.is_some());
3384            assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
3385            assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
3386            assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
3387        } else {
3388            panic!("Expected System message");
3389        }
3390    }
3391
3392    #[test]
3393    fn test_assistant_message_with_new_fields() {
3394        let json = r#"{
3395            "type": "assistant",
3396            "message": {
3397                "id": "msg_1",
3398                "type": "message",
3399                "role": "assistant",
3400                "model": "claude-opus-4-7",
3401                "content": [{"type": "text", "text": "Hello"}],
3402                "stop_reason": "end_turn",
3403                "stop_details": null,
3404                "context_management": null,
3405                "usage": {
3406                    "input_tokens": 100,
3407                    "output_tokens": 10,
3408                    "cache_creation_input_tokens": 50,
3409                    "cache_read_input_tokens": 0,
3410                    "service_tier": "standard",
3411                    "inference_geo": "not_available"
3412                }
3413            },
3414            "session_id": "abc",
3415            "uuid": "msg-uuid-123"
3416        }"#;
3417
3418        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3419        if let ClaudeOutput::Assistant(asst) = output {
3420            assert_eq!(asst.message.stop_details, None);
3421            assert_eq!(asst.message.context_management, None);
3422            let usage = asst.message.usage.unwrap();
3423            assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
3424        } else {
3425            panic!("Expected Assistant message");
3426        }
3427    }
3428
3429    #[test]
3430    fn test_user_message_with_new_fields() {
3431        let json = r#"{
3432            "type": "user",
3433            "message": {
3434                "role": "user",
3435                "content": [{"type": "text", "text": "Hello"}]
3436            },
3437            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
3438            "parent_tool_use_id": "toolu_123",
3439            "uuid": "user-msg-456"
3440        }"#;
3441
3442        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3443        if let ClaudeOutput::User(user) = output {
3444            assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
3445            assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
3446        } else {
3447            panic!("Expected User message");
3448        }
3449    }
3450
3451    /// Real wire payload captured from the CLI after answering an
3452    /// AskUserQuestion via the permission control protocol. The top-level
3453    /// `tool_use_result` and `timestamp` fields must round-trip without loss —
3454    /// proxies using this crate to relay messages to a viewer rely on those
3455    /// fields being preserved (the viewer reads `tool_use_result.answers`).
3456    #[test]
3457    fn test_user_message_preserves_tool_use_result_and_timestamp() {
3458        let json = r#"{
3459            "type":"user",
3460            "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"}]},
3461            "parent_tool_use_id":null,
3462            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3463            "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
3464            "timestamp":"2026-05-12T23:12:04.121Z",
3465            "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"}}
3466        }"#;
3467
3468        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3469        let user = match output {
3470            ClaudeOutput::User(u) => u,
3471            other => panic!("Expected User message, got {:?}", other.message_type()),
3472        };
3473
3474        assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
3475        let raw = user
3476            .tool_use_result
3477            .as_ref()
3478            .expect("tool_use_result must be captured");
3479        assert_eq!(raw["answers"]["Color"], "Blue");
3480        assert_eq!(raw["questions"][0]["header"], "Color");
3481
3482        // Round-trip: re-serialize and confirm tool_use_result + timestamp
3483        // survive — the bug we're guarding against is that the proxy silently
3484        // drops these fields when relaying user messages.
3485        let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
3486        assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
3487        assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
3488        assert_eq!(
3489            reser["tool_use_result"]["questions"][0]["question"],
3490            "Which color do you prefer?"
3491        );
3492
3493        // Typed accessor: AskUserQuestionInput has the same shape as the
3494        // AskUserQuestion tool_use_result.
3495        let typed: crate::AskUserQuestionInput = user
3496            .tool_use_result_as::<crate::AskUserQuestionInput>()
3497            .expect("tool_use_result present")
3498            .expect("AskUserQuestionInput parses");
3499        assert_eq!(typed.questions.len(), 1);
3500        assert_eq!(typed.questions[0].header, "Color");
3501        let answers = typed.answers.expect("answers populated");
3502        assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
3503    }
3504
3505    /// User messages without `tool_use_result` / `timestamp` must still
3506    /// deserialize fine and serialize back without spuriously emitting nulls.
3507    #[test]
3508    fn test_user_message_without_tool_use_result_omits_field() {
3509        let json = r#"{
3510            "type":"user",
3511            "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
3512            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
3513        }"#;
3514
3515        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3516        let user = match output {
3517            ClaudeOutput::User(u) => u,
3518            _ => panic!("Expected User message"),
3519        };
3520        assert!(user.tool_use_result.is_none());
3521        assert!(user.timestamp.is_none());
3522
3523        let reser = serde_json::to_value(&user).unwrap();
3524        assert!(reser.get("tool_use_result").is_none());
3525        assert!(reser.get("timestamp").is_none());
3526    }
3527
3528    /// A `Task` tool result must expose subagent token / timing / tool-use
3529    /// accounting through the typed [`UserMessage::subagent_result`] accessor,
3530    /// including the nested per-model `usage` breakdown and `toolStats`.
3531    #[test]
3532    fn test_subagent_result_exposes_token_accounting() {
3533        let json = r#"{
3534            "type":"user",
3535            "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
3536            "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
3537            "tool_use_result":{
3538                "status":"completed",
3539                "prompt":"Count the .rs files.",
3540                "agentId":"ac4f0276e9d4b6232",
3541                "agentType":"Explore",
3542                "content":[{"type":"text","text":"21"}],
3543                "resolvedModel":"claude-haiku-4-5-20251001",
3544                "totalDurationMs":6869,
3545                "totalTokens":7834,
3546                "totalToolUseCount":1,
3547                "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
3548                "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
3549            }
3550        }"#;
3551
3552        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3553        let user = match output {
3554            ClaudeOutput::User(u) => u,
3555            _ => panic!("Expected User message"),
3556        };
3557
3558        let result = user.subagent_result().expect("subagent result parses");
3559        assert_eq!(result.agent_type.as_deref(), Some("Explore"));
3560        assert_eq!(
3561            result.resolved_model.as_deref(),
3562            Some("claude-haiku-4-5-20251001")
3563        );
3564        assert_eq!(result.total_tokens, Some(7834));
3565        assert_eq!(result.total_duration_ms, Some(6869));
3566        assert_eq!(result.total_tool_use_count, Some(1));
3567
3568        let usage = result.usage.expect("nested usage present");
3569        assert_eq!(usage.input_tokens, 6);
3570        assert_eq!(usage.cache_read_input_tokens, 7699);
3571
3572        let stats = result.tool_stats.expect("toolStats present");
3573        assert_eq!(stats.bash_count, 1);
3574    }
3575
3576    /// `tool_use_result` shapes that aren't subagent runs (e.g. AskUserQuestion)
3577    /// parse leniently into the all-`Option` [`SubagentResult`] with empty
3578    /// accounting rather than failing, so callers can probe without panicking.
3579    #[test]
3580    fn test_subagent_result_absent_for_non_task_result() {
3581        let json = r#"{
3582            "type":"user",
3583            "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
3584            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3585            "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
3586        }"#;
3587
3588        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3589        let user = match output {
3590            ClaudeOutput::User(u) => u,
3591            _ => panic!("Expected User message"),
3592        };
3593
3594        let result = user.subagent_result().expect("lenient parse");
3595        assert_eq!(result.total_tokens, None);
3596        assert_eq!(result.agent_type, None);
3597    }
3598
3599    #[test]
3600    fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
3601        use serde_json::Value;
3602
3603        let raw: Value = serde_json::from_str(
3604            r#"{
3605            "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
3606            "fast_mode_state":"off",
3607            "fast_mode_disabled_reason":"not_first_party",
3608            "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
3609        }"#,
3610        )
3611        .unwrap();
3612        crate::io::assert_fully_wrapped(&raw);
3613
3614        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3615        let ClaudeOutput::System(sys) = output else {
3616            panic!("expected System");
3617        };
3618        let init = sys.as_init().expect("parses as init");
3619        assert_eq!(
3620            init.fast_mode_disabled_reason,
3621            Some(crate::FastModeDisabledReason::NotFirstParty)
3622        );
3623        let errs = init.mcp_server_errors.unwrap();
3624        assert_eq!(errs.len(), 1);
3625        assert_eq!(errs[0].name, "broken");
3626        assert_eq!(errs[0].error_type, "invalid_config");
3627    }
3628
3629    #[test]
3630    fn test_code_change_published_fully_wrapped() {
3631        use super::{KnownSystemEvent, SystemSubtype};
3632        use serde_json::Value;
3633
3634        let raw: Value = serde_json::from_str(
3635            r#"{
3636            "type":"system","subtype":"code_change_published",
3637            "provider":"github","url":"https://github.com/owner/repo/pull/42",
3638            "repo":"owner/repo","identifier":"42",
3639            "uuid":"u1","session_id":"s1"
3640        }"#,
3641        )
3642        .unwrap();
3643        crate::io::assert_fully_wrapped(&raw);
3644
3645        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3646        let ClaudeOutput::System(sys) = output else {
3647            panic!("expected System");
3648        };
3649        assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
3650        let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
3651            panic!("expected CodeChangePublished event");
3652        };
3653        assert_eq!(msg.provider, "github");
3654        assert_eq!(msg.repo, "owner/repo");
3655        assert_eq!(msg.identifier, "42");
3656
3657        assert!(sys.is_code_change_published());
3658        assert!(!sys.is_vcs_state_changed());
3659        let direct = sys.as_code_change_published().expect("direct accessor");
3660        assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
3661        assert!(sys.as_vcs_state_changed().is_none());
3662    }
3663
3664    #[test]
3665    fn test_feedback_draft_queued_fully_wrapped() {
3666        use super::{KnownSystemEvent, SystemSubtype};
3667        use serde_json::Value;
3668
3669        let raw: Value = serde_json::from_str(
3670            r#"{
3671            "type":"system","subtype":"feedback_draft_queued",
3672            "draft_id":"draft-1","draft_type":"bug_report",
3673            "title":"Tool output was truncated",
3674            "details_preview":"The last command omitted its final lines",
3675            "uuid":"u1","session_id":"s1","future_field":"preserved"
3676        }"#,
3677        )
3678        .unwrap();
3679        crate::io::assert_fully_wrapped(&raw);
3680
3681        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3682        let ClaudeOutput::System(sys) = output else {
3683            panic!("expected System");
3684        };
3685        assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
3686        assert!(sys.is_feedback_draft_queued());
3687        assert!(!sys.is_vcs_state_changed());
3688
3689        let direct = sys
3690            .as_feedback_draft_queued()
3691            .expect("direct typed accessor");
3692        assert_eq!(direct.draft_id, "draft-1");
3693        assert_eq!(direct.draft_type, "bug_report");
3694        assert_eq!(direct.extra["future_field"], "preserved");
3695
3696        let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
3697            panic!("expected FeedbackDraftQueued event");
3698        };
3699        assert_eq!(known.title, "Tool output was truncated");
3700        assert_eq!(
3701            sys.typed_value().expect("typed value")["future_field"],
3702            "preserved"
3703        );
3704    }
3705
3706    #[test]
3707    fn test_cloud_session_delta_fully_wrapped() {
3708        use super::{KnownSystemEvent, SystemSubtype};
3709        use serde_json::Value;
3710
3711        let raw: Value = serde_json::from_str(
3712            r#"{
3713            "type":"system","subtype":"cloud_session_delta",
3714            "seq":3,"changed":["serving","connection"],
3715            "cloud_session":{"id":"session_abc","view_url":"https://example.invalid/s/abc",
3716                "serving":{"state":"on"},"connection":{"state":"live"}},
3717            "uuid":"u1","session_id":"s1","future_field":"preserved"
3718        }"#,
3719        )
3720        .unwrap();
3721        crate::io::assert_fully_wrapped(&raw);
3722
3723        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3724        let ClaudeOutput::System(sys) = output else {
3725            panic!("expected System");
3726        };
3727        assert_eq!(sys.subtype, SystemSubtype::CloudSessionDelta);
3728        assert!(sys.is_cloud_session_delta());
3729        assert!(!sys.is_feedback_draft_queued());
3730
3731        let direct = sys.as_cloud_session_delta().expect("direct typed accessor");
3732        assert_eq!(direct.seq, 3);
3733        assert_eq!(direct.changed, vec!["serving", "connection"]);
3734        assert_eq!(direct.cloud_session["id"], "session_abc");
3735        assert_eq!(direct.extra["future_field"], "preserved");
3736
3737        let Some(KnownSystemEvent::CloudSessionDelta(known)) = sys.as_known_system_event() else {
3738            panic!("expected CloudSessionDelta event");
3739        };
3740        assert_eq!(known.session_id, "s1");
3741        assert_eq!(
3742            sys.typed_value().expect("typed value")["future_field"],
3743            "preserved"
3744        );
3745    }
3746
3747    #[test]
3748    fn test_vcs_state_changed_fully_wrapped() {
3749        use super::{KnownSystemEvent, VcsMutationKind};
3750        use serde_json::Value;
3751
3752        for kind in ["commit", "push", "merge", "rebase"] {
3753            let raw: Value = serde_json::from_str(&format!(
3754                r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
3755                kind
3756            ))
3757            .unwrap();
3758            crate::io::assert_fully_wrapped(&raw);
3759
3760            let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3761            let ClaudeOutput::System(sys) = output else {
3762                panic!("expected System");
3763            };
3764            let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3765                panic!("expected VcsStateChanged event");
3766            };
3767            assert_eq!(msg.kind.as_str(), kind);
3768            assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
3769        }
3770
3771        // Unknown kinds are valid per the wire contract.
3772        let raw: Value = serde_json::from_str(
3773            r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
3774        )
3775        .unwrap();
3776        crate::io::assert_fully_wrapped(&raw);
3777        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3778        let ClaudeOutput::System(sys) = output else {
3779            panic!("expected System");
3780        };
3781        let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3782            panic!("expected VcsStateChanged event");
3783        };
3784        assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
3785
3786        assert!(sys.is_vcs_state_changed());
3787        let direct = sys.as_vcs_state_changed().expect("direct accessor");
3788        assert_eq!(direct.cwd, "/repo");
3789        assert!(sys.as_code_change_published().is_none());
3790    }
3791
3792    #[test]
3793    fn test_assistant_aborted_and_resume_flags_roundtrip() {
3794        let json = r#"{
3795            "type":"assistant",
3796            "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
3797            "session_id":"s1",
3798            "aborted":true,
3799            "resumed_from_incomplete_thinking":true
3800        }"#;
3801        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3802        let ClaudeOutput::Assistant(msg) = &output else {
3803            panic!("expected Assistant");
3804        };
3805        assert_eq!(msg.aborted, Some(true));
3806        assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
3807        let reserialized = serde_json::to_string(&output).unwrap();
3808        assert!(reserialized.contains("\"aborted\":true"));
3809        assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
3810
3811        // Absent flags stay absent on the wire.
3812        let json = r#"{
3813            "type":"assistant",
3814            "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
3815            "session_id":"s2"
3816        }"#;
3817        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3818        let reserialized = serde_json::to_string(&output).unwrap();
3819        assert!(!reserialized.contains("aborted"));
3820        assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
3821    }
3822
3823    #[test]
3824    fn test_user_tool_result_meta_roundtrip() {
3825        let json = r#"{
3826            "type":"user",
3827            "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
3828            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3829            "tool_result_meta":[
3830                {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
3831                {"id":"toolu_2","non_execution_kind":"permission-rule"}
3832            ]
3833        }"#;
3834        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3835        let ClaudeOutput::User(user) = &output else {
3836            panic!("expected User");
3837        };
3838        let meta = user.tool_result_meta.as_ref().unwrap();
3839        assert_eq!(meta.len(), 2);
3840        assert_eq!(meta[0].non_execution_kind, "user-rejected");
3841        assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
3842        assert_eq!(meta[1].user_feedback, None);
3843
3844        let reserialized = serde_json::to_string(&output).unwrap();
3845        assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
3846        assert!(!reserialized.contains("\"user_feedback\":null"));
3847    }
3848
3849    /// CLI 2.1.222 added `scope` to `system/model_refusal_fallback`:
3850    /// "session" (main-thread swap, also the meaning when absent on older
3851    /// CLIs) vs "local" (subagent/side-question fallback only).
3852    #[test]
3853    fn model_refusal_fallback_scope_roundtrips_and_defaults() {
3854        use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
3855        let with_scope = serde_json::json!({
3856            "trigger": "refusal",
3857            "direction": "retry",
3858            "scope": "local",
3859            "original_model": "claude-fable-5",
3860            "fallback_model": "claude-opus-5",
3861            "request_id": null,
3862            "content": "Refused; retried on fallback model.",
3863            "uuid": "u1",
3864            "session_id": "s1"
3865        });
3866        let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
3867        assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
3868        assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
3869
3870        // Older CLIs omit scope — absent, not null, and treated as session
3871        // by consumers per the wire docs.
3872        let mut without = with_scope.clone();
3873        without.as_object_mut().unwrap().remove("scope");
3874        let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
3875        assert_eq!(msg.scope, None);
3876        assert_eq!(serde_json::to_value(&msg).unwrap(), without);
3877
3878        // Open enum: unknown scopes pass through verbatim.
3879        assert_eq!(
3880            RefusalFallbackScope::from("workspace").as_str(),
3881            "workspace"
3882        );
3883    }
3884}