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