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