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    /// Exception: a request with `filestore_path` set (a synced file) is not
3528    /// waited for, so a call that names that file may start before it is
3529    /// synced (CLI 2.1.287+).
3530    #[serde(default, skip_serializing_if = "Option::is_none")]
3531    pub staged_files: Option<bool>,
3532    /// True when user messages with `shouldQuery` false that reach this
3533    /// worker before a `turn_handoff` request are appended, and
3534    /// acknowledged, before the handed-off turn produces any output.
3535    /// Exception: a request that continues a turn whose leading lines an
3536    /// earlier request appended; that turn goes first. A worker that omits
3537    /// it may run the turn first (CLI 2.1.278+).
3538    #[serde(default, skip_serializing_if = "Option::is_none")]
3539    pub no_query_first: Option<bool>,
3540    /// Present, and true, only on a worker that takes a `stage_file` control
3541    /// request whose `mount_path` is `/uploads/.home/<path>` as a file for
3542    /// `/home/claude/<path>`: it writes the file there, answers ok with a
3543    /// noop (`already_present`, `path_occupied`, `refused_name`,
3544    /// `home_files_off`) and writes nothing, or answers an error. Any other
3545    /// worker treats that path as an ordinary upload (CLI 2.1.287+).
3546    #[serde(default, skip_serializing_if = "Option::is_none")]
3547    pub home_files: Option<bool>,
3548    /// Present, and true, only when this worker uses the `file_names` member
3549    /// of a `turn_handoff` request; a client sends that member to no other
3550    /// worker (CLI 2.1.287+).
3551    #[serde(default, skip_serializing_if = "Option::is_none")]
3552    pub file_names: Option<bool>,
3553    pub uuid: String,
3554    pub session_id: String,
3555    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
3556    pub extra: serde_json::Map<String, Value>,
3557}
3558
3559/// `system/turn_preempted` — the CLI itself stopped the running turn so a
3560/// user's rapid follow-up message is answered at once, exactly as a priority
3561/// `now` message would have (running shell commands are backgrounded, not
3562/// killed). Sent at the moment of the stop, so it precedes the stopped turn's
3563/// `result` frame (`terminal_reason` `aborted_streaming` or `aborted_tools`)
3564/// and its members' `cancelled` `command_lifecycle` frames: a host renders
3565/// that turn as superseded by the follow-up rather than as interrupted, does
3566/// not resend its messages, and treats only `preempted_by_uuid` as picked
3567/// up. At most one per burst. Emitted in `-p`/SDK sessions only, and only to
3568/// a consumer that declared `rapidFollowupPreempt` on its initialize request
3569/// while the feature's rollout flag is on (CLI 2.1.273+).
3570#[derive(Debug, Clone, Serialize, Deserialize)]
3571pub struct TurnPreemptedMessage {
3572    /// Why the turn was stopped. `rapid_followup`: the user's next message
3573    /// arrived before the running turn showed any output. More reasons may
3574    /// be added; treat an unknown one the same way.
3575    pub reason: String,
3576    /// The client-supplied uuid of the queued user message the turn was
3577    /// stopped for. It runs next, together with any messages queued behind
3578    /// it.
3579    pub preempted_by_uuid: String,
3580    /// The client-supplied uuids of the user messages the stopped turn was
3581    /// running (uuid-less members omitted; may be empty).
3582    #[serde(default)]
3583    pub preempted_message_uuids: Vec<String>,
3584    pub uuid: String,
3585    pub session_id: String,
3586    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
3587    pub extra: serde_json::Map<String, Value>,
3588}
3589
3590/// `system/peer_message_hold` — a cross-session (peer) message this
3591/// session's receive-side policy held rather than queued, and how that hold
3592/// resolved. Lets a host show the human that a message arrived but has not
3593/// reached the model (and may never) instead of nothing at all; the sending
3594/// session is told separately over its own transport where one exists.
3595/// Informational only — there is no host-side approval through this frame.
3596/// Emitted in `-p`/SDK sessions (CLI 2.1.273+).
3597#[derive(Debug, Clone, Serialize, Deserialize)]
3598pub struct PeerMessageHoldMessage {
3599    /// `held` once per message per hold cause (a re-announcement under a
3600    /// different cause emits again); then at most one of `released` (it
3601    /// enters the queue — its `command_lifecycle` `queued` and user replay
3602    /// echo follow under `message_uuid`) or `dropped` (it will never reach
3603    /// the model in this session; see `outcome`).
3604    pub state: PeerMessageHoldState,
3605    /// The parked command's uuid — the value its later `command_lifecycle`
3606    /// frames and user replay echo carry. For lane `bridge`/`stdin` it is
3607    /// the id the host supplied on the inbound message; for lane `socket` it
3608    /// was chosen by the sending session, so correlate it only within
3609    /// `peer_message_hold` / peer replay frames, never against the host's
3610    /// own prompts' lifecycle.
3611    #[serde(default, skip_serializing_if = "Option::is_none")]
3612    pub message_uuid: Option<String>,
3613    /// Which ingress delivered it.
3614    pub lane: PeerMessageLane,
3615    /// The sender's address as the envelope will show it — an address-shaped
3616    /// token with control and invisible code points scrubbed; empty when the
3617    /// sender supplied none or an unshaped one. Sender-asserted on the
3618    /// socket lane: a label, not an identity proof.
3619    pub from: String,
3620    /// The sender's display name when it supplied one, normalized and
3621    /// scrubbed the same way. A claim, like `from`.
3622    #[serde(default, skip_serializing_if = "Option::is_none")]
3623    pub from_name: Option<String>,
3624    /// State `held` only: why the policy parked it.
3625    #[serde(default, skip_serializing_if = "Option::is_none")]
3626    pub cause: Option<PeerMessageHoldCause>,
3627    /// State `dropped` only: how the hold ended.
3628    #[serde(default, skip_serializing_if = "Option::is_none")]
3629    pub outcome: Option<PeerMessageHoldOutcome>,
3630    pub uuid: String,
3631    pub session_id: String,
3632    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
3633    pub extra: serde_json::Map<String, Value>,
3634}
3635
3636/// Lifecycle state carried by a [`PeerMessageHoldMessage`].
3637#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3638pub enum PeerMessageHoldState {
3639    /// The receive-side policy parked the message instead of queueing it.
3640    Held,
3641    /// The policy now accepts it: it enters the queue.
3642    Released,
3643    /// It will never reach the model in this session.
3644    Dropped,
3645    /// A state not yet known to this version of the crate.
3646    Unknown(String),
3647}
3648
3649impl PeerMessageHoldState {
3650    pub fn as_str(&self) -> &str {
3651        match self {
3652            Self::Held => "held",
3653            Self::Released => "released",
3654            Self::Dropped => "dropped",
3655            Self::Unknown(s) => s.as_str(),
3656        }
3657    }
3658}
3659
3660impl fmt::Display for PeerMessageHoldState {
3661    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3662        f.write_str(self.as_str())
3663    }
3664}
3665
3666impl From<&str> for PeerMessageHoldState {
3667    fn from(s: &str) -> Self {
3668        match s {
3669            "held" => Self::Held,
3670            "released" => Self::Released,
3671            "dropped" => Self::Dropped,
3672            other => Self::Unknown(other.to_string()),
3673        }
3674    }
3675}
3676
3677impl Serialize for PeerMessageHoldState {
3678    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3679        serializer.serialize_str(self.as_str())
3680    }
3681}
3682
3683impl<'de> Deserialize<'de> for PeerMessageHoldState {
3684    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3685        let s = String::deserialize(deserializer)?;
3686        Ok(Self::from(s.as_str()))
3687    }
3688}
3689
3690/// Which ingress delivered the message a [`PeerMessageHoldMessage`] reports.
3691#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3692pub enum PeerMessageLane {
3693    /// The Remote Control bridge.
3694    Bridge,
3695    /// The host's own stdin stream.
3696    Stdin,
3697    /// The local cross-session socket.
3698    Socket,
3699    /// A lane not yet known to this version of the crate.
3700    Unknown(String),
3701}
3702
3703impl PeerMessageLane {
3704    pub fn as_str(&self) -> &str {
3705        match self {
3706            Self::Bridge => "bridge",
3707            Self::Stdin => "stdin",
3708            Self::Socket => "socket",
3709            Self::Unknown(s) => s.as_str(),
3710        }
3711    }
3712}
3713
3714impl fmt::Display for PeerMessageLane {
3715    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3716        f.write_str(self.as_str())
3717    }
3718}
3719
3720impl From<&str> for PeerMessageLane {
3721    fn from(s: &str) -> Self {
3722        match s {
3723            "bridge" => Self::Bridge,
3724            "stdin" => Self::Stdin,
3725            "socket" => Self::Socket,
3726            other => Self::Unknown(other.to_string()),
3727        }
3728    }
3729}
3730
3731impl Serialize for PeerMessageLane {
3732    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3733        serializer.serialize_str(self.as_str())
3734    }
3735}
3736
3737impl<'de> Deserialize<'de> for PeerMessageLane {
3738    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3739        let s = String::deserialize(deserializer)?;
3740        Ok(Self::from(s.as_str()))
3741    }
3742}
3743
3744/// Why the receive-side policy parked a peer message
3745/// ([`PeerMessageHoldMessage::cause`], state `held` only). The `*Setting`
3746/// causes are a standing `crossSessionInbound: "hold"`; `ModeMismatch` and
3747/// `NoModeAsserted` are the permission-mode parity holds (the sender runs in
3748/// a different permission class, or asserted none while this session
3749/// bypasses permissions).
3750#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3751pub enum PeerMessageHoldCause {
3752    ExplicitSetting,
3753    ManagedSetting,
3754    RepoSetting,
3755    InvalidSetting,
3756    BypassDefault,
3757    ModeUnknown,
3758    ModeMismatch,
3759    NoModeAsserted,
3760    /// A cause not yet known to this version of the crate.
3761    Unknown(String),
3762}
3763
3764impl PeerMessageHoldCause {
3765    pub fn as_str(&self) -> &str {
3766        match self {
3767            Self::ExplicitSetting => "explicit-setting",
3768            Self::ManagedSetting => "managed-setting",
3769            Self::RepoSetting => "repo-setting",
3770            Self::InvalidSetting => "invalid-setting",
3771            Self::BypassDefault => "bypass-default",
3772            Self::ModeUnknown => "mode-unknown",
3773            Self::ModeMismatch => "mode-mismatch",
3774            Self::NoModeAsserted => "no-mode-asserted",
3775            Self::Unknown(s) => s.as_str(),
3776        }
3777    }
3778}
3779
3780impl fmt::Display for PeerMessageHoldCause {
3781    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3782        f.write_str(self.as_str())
3783    }
3784}
3785
3786impl From<&str> for PeerMessageHoldCause {
3787    fn from(s: &str) -> Self {
3788        match s {
3789            "explicit-setting" => Self::ExplicitSetting,
3790            "managed-setting" => Self::ManagedSetting,
3791            "repo-setting" => Self::RepoSetting,
3792            "invalid-setting" => Self::InvalidSetting,
3793            "bypass-default" => Self::BypassDefault,
3794            "mode-unknown" => Self::ModeUnknown,
3795            "mode-mismatch" => Self::ModeMismatch,
3796            "no-mode-asserted" => Self::NoModeAsserted,
3797            other => Self::Unknown(other.to_string()),
3798        }
3799    }
3800}
3801
3802impl Serialize for PeerMessageHoldCause {
3803    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3804        serializer.serialize_str(self.as_str())
3805    }
3806}
3807
3808impl<'de> Deserialize<'de> for PeerMessageHoldCause {
3809    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3810        let s = String::deserialize(deserializer)?;
3811        Ok(Self::from(s.as_str()))
3812    }
3813}
3814
3815/// How a peer-message hold ended ([`PeerMessageHoldMessage::outcome`],
3816/// state `dropped` only).
3817#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3818pub enum PeerMessageHoldOutcome {
3819    /// No one approved it before the approval deadline (a headless host has
3820    /// no approval surface, so every parity hold ends this way unless the
3821    /// mode changes first).
3822    Expired,
3823    /// The policy turned to refuse while it waited.
3824    Refused,
3825    /// Released or approved, but the ingress guard (rate limit, duplicate,
3826    /// queue cap) discarded it.
3827    Dropped,
3828    /// The session ended with it still parked.
3829    Discarded,
3830    /// An outcome not yet known to this version of the crate.
3831    Unknown(String),
3832}
3833
3834impl PeerMessageHoldOutcome {
3835    pub fn as_str(&self) -> &str {
3836        match self {
3837            Self::Expired => "expired",
3838            Self::Refused => "refused",
3839            Self::Dropped => "dropped",
3840            Self::Discarded => "discarded",
3841            Self::Unknown(s) => s.as_str(),
3842        }
3843    }
3844}
3845
3846impl fmt::Display for PeerMessageHoldOutcome {
3847    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3848        f.write_str(self.as_str())
3849    }
3850}
3851
3852impl From<&str> for PeerMessageHoldOutcome {
3853    fn from(s: &str) -> Self {
3854        match s {
3855            "expired" => Self::Expired,
3856            "refused" => Self::Refused,
3857            "dropped" => Self::Dropped,
3858            "discarded" => Self::Discarded,
3859            other => Self::Unknown(other.to_string()),
3860        }
3861    }
3862}
3863
3864impl Serialize for PeerMessageHoldOutcome {
3865    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3866        serializer.serialize_str(self.as_str())
3867    }
3868}
3869
3870impl<'de> Deserialize<'de> for PeerMessageHoldOutcome {
3871    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3872        let s = String::deserialize(deserializer)?;
3873        Ok(Self::from(s.as_str()))
3874    }
3875}
3876
3877/// `system/per_turn_effort_changed` — the conversation stopped sending effort
3878/// per turn, because the server refused its per-turn effort message or a
3879/// `role:"system"` message. From the retried request on, an effort change
3880/// rewrites the cached prefix, for every model, until a later `system/init`
3881/// says otherwise. Sent once, when the conversation first turns per-turn
3882/// effort off, before the retried request's output; sent again only if the
3883/// server refuses again after the conversation's refusals are cleared (for
3884/// example by `/clear` or a compaction). It can repeat a `false` the host
3885/// already holds. Pairs with [`InitMessage::per_turn_effort_active`]
3886/// (CLI 2.1.281+).
3887#[derive(Debug, Clone, Serialize, Deserialize)]
3888pub struct PerTurnEffortChangedMessage {
3889    /// Always `false`: a change to `true` is reported only by `system/init`.
3890    pub per_turn_effort_active: bool,
3891    pub uuid: String,
3892    pub session_id: String,
3893}
3894
3895/// `system/session_title_changed` — the session's name, for a host that
3896/// shows it (an IDE editor tab, say). A headless session sends it at startup
3897/// when the session already has a name, and again after each change to the
3898/// name (not an AI-generated title), including a rename the host itself sent
3899/// with `rename_session`. A cleared name is not sent. The same name can
3900/// arrive more than once, so treat a repeated name as no change. The frame
3901/// can arrive before the `system/init` that first names its `session_id`
3902/// (at startup or after `/clear`); a host that learns the session id from
3903/// init can hold the name until then (CLI 2.1.285+).
3904#[derive(Debug, Clone, Serialize, Deserialize)]
3905pub struct SessionTitleChangedMessage {
3906    /// The session's current name: control, bidirectional and zero-width
3907    /// characters replaced with single spaces, ends trimmed, at most 200
3908    /// Unicode code points. A suffix can make it unique when another session
3909    /// already has the name.
3910    pub title: String,
3911    pub uuid: String,
3912    pub session_id: String,
3913}
3914
3915/// `system/ui_log` — one `$.ui.log(text)` line from a plugin in a headless
3916/// session: what the REPL appends to its transcript as a dim notice under
3917/// the plugin's name. Not sent to the model (CLI 2.1.285+).
3918#[derive(Debug, Clone, Serialize, Deserialize)]
3919pub struct UiLogMessage {
3920    /// The calling plugin's registry name, as the hooks engine knows it.
3921    pub plugin: String,
3922    /// The line as the plugin gave it, at most 4096 characters. Plain text:
3923    /// draw it as a text node, never as markup.
3924    pub text: String,
3925    pub uuid: String,
3926    pub session_id: String,
3927}
3928
3929/// `system/ui_toast` — one `$.ui.toast(text)` from a plugin in a headless
3930/// session: what the REPL shows on the notification bar under the prompt
3931/// for a few seconds, under the plugin's name (CLI 2.1.285+).
3932#[derive(Debug, Clone, Serialize, Deserialize)]
3933pub struct UiToastMessage {
3934    /// The calling plugin's registry name, as the hooks engine knows it.
3935    pub plugin: String,
3936    /// The line as the plugin gave it, at most 4096 characters. Plain text:
3937    /// draw it as a text node, never as markup.
3938    pub text: String,
3939    /// How long the REPL would keep it on the notification bar: the plugin's
3940    /// `timeoutMs`, or the engine's default (4000).
3941    pub timeout_ms: u64,
3942    pub uuid: String,
3943    pub session_id: String,
3944}
3945
3946/// `system/ui_status` — one `$.ui.status(text)` from a plugin in a headless
3947/// session: the line the REPL pins under the prompt until the plugin's next
3948/// call replaces or clears it. One status line per plugin (CLI 2.1.285+).
3949#[derive(Debug, Clone, Serialize, Deserialize)]
3950pub struct UiStatusMessage {
3951    /// The calling plugin's registry name, as the hooks engine knows it.
3952    pub plugin: String,
3953    /// The plugin's pinned line, at most 4096 characters, replacing its
3954    /// previous one; `None` (wire `null`) removes it. Plain text: draw it as
3955    /// a text node, never as markup.
3956    pub text: Option<String>,
3957    pub uuid: String,
3958    pub session_id: String,
3959}
3960
3961/// `system/ui_invalidate` — a plugin called `$.ui.invalidate("ui.render")`,
3962/// or a plugin loaded or reloaded: every tree a remote surface drew through
3963/// `ui_render` may be stale, and the surface re-asks each mounted instance.
3964/// Bumps from one plugin fold to at most one per 100 ms (CLI 2.1.285+).
3965#[derive(Debug, Clone, Serialize, Deserialize)]
3966pub struct UiInvalidateMessage {
3967    /// The render event whose version moved — `"ui.render"`; the engine
3968    /// versions it as one event, so no component or instance is named.
3969    pub event: String,
3970    /// Present when a plugin state write made only these instances stale,
3971    /// so the surface may re-ask them alone. Absent, every mounted instance
3972    /// may be stale.
3973    #[serde(default, skip_serializing_if = "Option::is_none")]
3974    pub instances: Option<Vec<UiInvalidatedInstance>>,
3975    pub uuid: String,
3976    pub session_id: String,
3977}
3978
3979/// One stale instance named by [`UiInvalidateMessage::instances`].
3980#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
3981pub struct UiInvalidatedInstance {
3982    /// The surface that drew the instance.
3983    pub surface: String,
3984    /// The component of the instance.
3985    pub component: String,
3986    /// The engine's requestId of the instance, as the `ui_render` that drew
3987    /// it named it.
3988    pub instance_id: String,
3989}
3990
3991/// `system/ui_panes` — the pane roster a remote surface draws changed: a
3992/// plugin opened, retitled or closed a pane, the person showed, focused or
3993/// closed one, or a placing client attached. Deduplicated: an unchanged
3994/// roster is not pushed. A pane opened before the stream armed produces
3995/// none, so read the roster with a `ui_panes` request on mount
3996/// (CLI 2.1.285+).
3997#[derive(Debug, Clone, Serialize, Deserialize)]
3998pub struct UiPanesMessage {
3999    /// The placed open panes in open order (an open appends, a close
4000    /// removes); a pane waiting unplaced is not listed.
4001    pub panes: Vec<UiPane>,
4002    /// The pane the surface shows: the last opened or focused, never
4003    /// displacing one the person holds; `None` with none open.
4004    pub shown_id: Option<String>,
4005    /// The pane the person gave the keyboard; `None` while the composer has
4006    /// it.
4007    pub focused_id: Option<String>,
4008    /// A standing `$.ui.open({ focus: true })` the surface has yet to judge:
4009    /// honour it only while the composer is empty and idle and no dialog is
4010    /// up, else refuse it.
4011    pub focus_requested_id: Option<String>,
4012    pub uuid: String,
4013    pub session_id: String,
4014}
4015
4016/// One placed pane in a [`UiPanesMessage`] roster.
4017#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4018pub struct UiPane {
4019    /// The pane's id as the plugin opened it: 1 to 64 of letters, digits,
4020    /// `_`, `-`.
4021    pub id: String,
4022    /// The pane's tab label: the `title` its latest open gave, else its id.
4023    /// Plugin-authored text: draw it as a text node, never as markup.
4024    pub title: String,
4025    /// The registry name of the plugin that opened it.
4026    pub plugin: String,
4027    /// `true`: Escape while the pane holds the keyboard, or at an idle empty
4028    /// composer, closes it. Absent, Escape only returns the keyboard.
4029    #[serde(default, skip_serializing_if = "Option::is_none")]
4030    pub close_on_escape: Option<bool>,
4031    /// `true`: the surface holds its transient toasts while this pane is
4032    /// the one shown, and shows them once it closes.
4033    #[serde(default, skip_serializing_if = "Option::is_none")]
4034    pub hold_toasts: Option<bool>,
4035    /// The body rows its content asked for while seated inline; a request
4036    /// the person's own sizing overrides.
4037    #[serde(default, skip_serializing_if = "Option::is_none")]
4038    pub rows: Option<u64>,
4039    /// The body columns its content asked for while docked; a request the
4040    /// person's own sizing overrides.
4041    #[serde(default, skip_serializing_if = "Option::is_none")]
4042    pub columns: Option<u64>,
4043}
4044
4045/// `system/ui_scroll` — a plugin's `$.ui.scroll` moved the window of a site
4046/// a remote surface draws: the surface scrolls the site's body there. The
4047/// person's own scrolls are never pushed back (CLI 2.1.285+).
4048#[derive(Debug, Clone, Serialize, Deserialize)]
4049pub struct UiScrollMessage {
4050    /// The client drawing the site, as it attached.
4051    pub client_id: String,
4052    /// Which site: a pane's body or the band above the prompt.
4053    pub component: UiSiteComponent,
4054    /// The pane's id, or `above-prompt` for the band.
4055    pub instance_id: String,
4056    /// The first content row the window is to show, 0 at the top, clamped
4057    /// to the tree as the surface last reported it.
4058    pub offset: u64,
4059    /// `true`: keep the window on the tree's last rows through the draws to
4060    /// come, until the person or a plugin next moves it.
4061    #[serde(default, skip_serializing_if = "Option::is_none")]
4062    pub follow_end: Option<bool>,
4063    pub uuid: String,
4064    pub session_id: String,
4065}
4066
4067/// `system/ui_focus` — a plugin's `$.ui.focus` moved the focus ring of a
4068/// site a remote surface draws onto one of its Buttons, Inputs or Selects:
4069/// the surface focuses that element if it still draws it. The person's own
4070/// moves are never pushed back (CLI 2.1.285+).
4071#[derive(Debug, Clone, Serialize, Deserialize)]
4072pub struct UiFocusMessage {
4073    /// The client drawing the site, as it attached.
4074    pub client_id: String,
4075    /// Which site: a pane's body or the band above the prompt.
4076    pub component: UiSiteComponent,
4077    /// The pane's id, or `above-prompt` for the band.
4078    pub instance_id: String,
4079    /// Whose element takes the ring: its `press.plugin`.
4080    pub plugin: String,
4081    /// The element's `props.key`, as `ui.press` names it.
4082    pub key: String,
4083    pub uuid: String,
4084    pub session_id: String,
4085}
4086
4087/// The site a [`UiScrollMessage`] or [`UiFocusMessage`] addresses.
4088#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4089pub enum UiSiteComponent {
4090    /// A pane's body.
4091    Pane,
4092    /// The band above the prompt.
4093    AbovePrompt,
4094    /// A site not yet known to this version of the crate.
4095    Unknown(String),
4096}
4097
4098impl UiSiteComponent {
4099    pub fn as_str(&self) -> &str {
4100        match self {
4101            Self::Pane => "Pane",
4102            Self::AbovePrompt => "AbovePrompt",
4103            Self::Unknown(s) => s.as_str(),
4104        }
4105    }
4106}
4107
4108impl fmt::Display for UiSiteComponent {
4109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4110        f.write_str(self.as_str())
4111    }
4112}
4113
4114impl From<&str> for UiSiteComponent {
4115    fn from(s: &str) -> Self {
4116        match s {
4117            "Pane" => Self::Pane,
4118            "AbovePrompt" => Self::AbovePrompt,
4119            other => Self::Unknown(other.to_string()),
4120        }
4121    }
4122}
4123
4124impl Serialize for UiSiteComponent {
4125    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4126        serializer.serialize_str(self.as_str())
4127    }
4128}
4129
4130impl<'de> Deserialize<'de> for UiSiteComponent {
4131    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4132        let s = String::deserialize(deserializer)?;
4133        Ok(Self::from(s.as_str()))
4134    }
4135}
4136
4137/// The run that printed a local-command row, carried as
4138/// [`AssistantMessage::local_command_run`].
4139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4140pub struct LocalCommandRun {
4141    /// The command's name as `command.run` carried it (no slash).
4142    pub command: String,
4143    /// Its arguments as the echo shows them (`***` when the command marks
4144    /// them sensitive).
4145    pub args: String,
4146}
4147
4148/// What a local-command row reports, carried as
4149/// [`AssistantMessage::local_command_outcome`] so a host can offer a fix
4150/// instead of relaying the text (CLI 2.1.281+).
4151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4152pub struct LocalCommandOutcome {
4153    pub kind: LocalCommandOutcomeKind,
4154    /// Kind `unknown` only: the registered command closest to the typed
4155    /// name, no slash.
4156    #[serde(default, skip_serializing_if = "Option::is_none")]
4157    pub suggestion: Option<String>,
4158}
4159
4160/// The outcome a local-command row reports ([`LocalCommandOutcome::kind`]).
4161/// New kinds are additive; the CLI itself drops the whole outcome when it
4162/// meets a kind it does not know, so treat [`Unknown`](Self::Unknown) as
4163/// absent.
4164#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4165pub enum LocalCommandOutcomeKind {
4166    /// An interactive-panel command this (headless) session cannot open.
4167    UnavailableHeadless,
4168    /// No command has the typed name; see
4169    /// [`LocalCommandOutcome::suggestion`].
4170    Unknown,
4171    /// The command ran and failed.
4172    Failed,
4173    /// The change applies after Claude Code restarts.
4174    RestartRequired,
4175    /// A kind not yet known to this version of the crate.
4176    Other(String),
4177}
4178
4179impl LocalCommandOutcomeKind {
4180    pub fn as_str(&self) -> &str {
4181        match self {
4182            Self::UnavailableHeadless => "unavailable_headless",
4183            Self::Unknown => "unknown",
4184            Self::Failed => "failed",
4185            Self::RestartRequired => "restart_required",
4186            Self::Other(s) => s.as_str(),
4187        }
4188    }
4189}
4190
4191impl fmt::Display for LocalCommandOutcomeKind {
4192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4193        f.write_str(self.as_str())
4194    }
4195}
4196
4197impl From<&str> for LocalCommandOutcomeKind {
4198    fn from(s: &str) -> Self {
4199        match s {
4200            "unavailable_headless" => Self::UnavailableHeadless,
4201            "unknown" => Self::Unknown,
4202            "failed" => Self::Failed,
4203            "restart_required" => Self::RestartRequired,
4204            other => Self::Other(other.to_string()),
4205        }
4206    }
4207}
4208
4209impl Serialize for LocalCommandOutcomeKind {
4210    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4211        serializer.serialize_str(self.as_str())
4212    }
4213}
4214
4215impl<'de> Deserialize<'de> for LocalCommandOutcomeKind {
4216    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4217        let s = String::deserialize(deserializer)?;
4218        Ok(Self::from(s.as_str()))
4219    }
4220}
4221
4222/// Structured twin of a `/usage` result, carried as
4223/// [`AssistantMessage::usage_report`]: the session's totals, the plan's
4224/// usage rows as the server sent them and the extra-usage spend, and nothing
4225/// else from the usage body (the `get_usage` control reply carries the
4226/// rest). Experimental — the shape may change.
4227#[derive(Debug, Clone, Serialize, Deserialize)]
4228pub struct UsageReport {
4229    /// Cost and usage accumulated by the current session.
4230    pub session: super::control::UsageSession,
4231    /// The plan's usage rows and extra-usage spend from the claude.ai usage
4232    /// endpoint; `None` when the CLI could not fetch them (no plan on this
4233    /// lane, or a token without the profile scope).
4234    pub rate_limits: Option<UsageReportRateLimits>,
4235    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4236    pub extra: serde_json::Map<String, Value>,
4237}
4238
4239/// The plan's usage rows and extra-usage spend in a [`UsageReport`].
4240#[derive(Debug, Clone, Serialize, Deserialize)]
4241pub struct UsageReportRateLimits {
4242    /// The server's usage rows (the usage endpoint's `limits[]`), as sent:
4243    /// which meters apply, their scope, labels, severity and order are the
4244    /// server's, so a client renders them verbatim. Empty when the server
4245    /// reported no meters; `None` when the body carried no rows at all (a
4246    /// server that predates them). When the usage fetch failed and the CLI
4247    /// fell back to rate-limit response headers, this holds at most the one
4248    /// row it synthesizes from them.
4249    pub limits: Option<Vec<UsageReportLimit>>,
4250    /// Extra-usage (overage) spend for the billing period, when the plan
4251    /// has it.
4252    #[serde(default, skip_serializing_if = "Option::is_none")]
4253    pub extra_usage: Option<UsageReportExtraUsage>,
4254    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4255    pub extra: serde_json::Map<String, Value>,
4256}
4257
4258/// One server usage row in [`UsageReportRateLimits::limits`].
4259#[derive(Debug, Clone, Serialize, Deserialize)]
4260pub struct UsageReportLimit {
4261    /// The server's meter kind, e.g. `session`, `weekly_all` or
4262    /// `weekly_scoped`. Classify a row on this, never on a label.
4263    pub kind: String,
4264    /// The server's row group, e.g. `session` or `weekly`; rows render
4265    /// grouped under it, in the server's order.
4266    pub group: String,
4267    /// Share of the window used, 0–100.
4268    pub percent: f64,
4269    /// ISO 8601 timestamp when the window resets.
4270    pub resets_at: Option<String>,
4271    /// What a scoped row is for, a model or a surface, with the server's
4272    /// display label.
4273    #[serde(default, skip_serializing_if = "Option::is_none")]
4274    pub scope: Option<UsageReportScope>,
4275    /// The server's reading of the row for a meter's colour, e.g. `normal`,
4276    /// `warning` or `critical`; a client falls back to its own thresholds
4277    /// without it.
4278    #[serde(default, skip_serializing_if = "Option::is_none")]
4279    pub severity: Option<String>,
4280    /// The server's headline pick: the row a single-value indicator shows.
4281    #[serde(default, skip_serializing_if = "Option::is_none")]
4282    pub is_active: Option<bool>,
4283    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4284    pub extra: serde_json::Map<String, Value>,
4285}
4286
4287/// What a scoped [`UsageReportLimit`] row is for.
4288#[derive(Debug, Clone, Serialize, Deserialize)]
4289pub struct UsageReportScope {
4290    #[serde(default, skip_serializing_if = "Option::is_none")]
4291    pub model: Option<UsageReportScopeLabel>,
4292    #[serde(default, skip_serializing_if = "Option::is_none")]
4293    pub surface: Option<UsageReportScopeLabel>,
4294    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4295    pub extra: serde_json::Map<String, Value>,
4296}
4297
4298/// The server's display label for a [`UsageReportScope`] member.
4299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4300pub struct UsageReportScopeLabel {
4301    pub display_name: String,
4302}
4303
4304/// Extra-usage (overage) spend for the billing period in a
4305/// [`UsageReportRateLimits`]. Amounts are in minor units of `currency`
4306/// (cents for USD).
4307#[derive(Debug, Clone, Serialize, Deserialize)]
4308pub struct UsageReportExtraUsage {
4309    /// `false` while extra usage cannot cover sends.
4310    pub is_enabled: bool,
4311    pub monthly_limit: Option<f64>,
4312    pub used_credits: Option<f64>,
4313    pub utilization: Option<f64>,
4314    #[serde(default, skip_serializing_if = "Option::is_none")]
4315    pub currency: Option<String>,
4316    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4317    pub extra: serde_json::Map<String, Value>,
4318}
4319
4320/// `{id, name}` of an original `Batch*` tool_use block, carried in
4321/// [`AssistantMessage::batch_tool_uses`].
4322#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4323pub struct BatchToolUse {
4324    pub id: String,
4325    pub name: String,
4326}
4327
4328/// Structured twin of the `/context` report, carried as
4329/// [`AssistantMessage::context_usage`] — the data a client needs to render
4330/// the context-usage card without parsing the markdown table. Evolves
4331/// additively; a breaking reshape would ship as a sibling field.
4332#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4333pub struct ContextUsage {
4334    /// Main-loop model the usage was computed for.
4335    pub model: String,
4336    /// Estimated tokens in use, unclamped — may exceed `raw_max_tokens` when
4337    /// over limit.
4338    pub total_tokens: u64,
4339    /// The window usage is measured against: the resolved autocompact window —
4340    /// the model's believed limit, or a smaller compaction-policy window.
4341    pub raw_max_tokens: u64,
4342    /// Rounded `total_tokens / raw_max_tokens`, 0–100+.
4343    pub percentage: u64,
4344    /// Present when `total_tokens` exceeds `raw_max_tokens`.
4345    #[serde(default, skip_serializing_if = "Option::is_none")]
4346    pub over_limit: Option<ContextOverLimit>,
4347    /// Usage-by-category rows (`Messages`, `System prompt`, …).
4348    #[serde(default)]
4349    pub categories: Vec<ContextCategory>,
4350    /// Per-tool token contributions of MCP tools.
4351    #[serde(default)]
4352    pub mcp_tools: Vec<ContextMcpTool>,
4353    /// Per-file token contributions of memory files.
4354    #[serde(default)]
4355    pub memory_files: Vec<ContextMemoryFile>,
4356    /// Per-agent token contributions of agent definitions.
4357    #[serde(default)]
4358    pub agents: Vec<ContextAgent>,
4359    /// Per-skill token contributions. Omitted when no skills contribute.
4360    #[serde(default, skip_serializing_if = "Option::is_none")]
4361    pub skills: Option<Vec<ContextSkill>>,
4362}
4363
4364/// Why and by how much a [`ContextUsage`] exceeds its window.
4365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4366pub struct ContextOverLimit {
4367    pub tokens_over: u64,
4368    /// How the window was resolved: `"hard_limit"` (the model's believed
4369    /// limit) or `"compaction_window"` (a compaction-policy window).
4370    pub kind: String,
4371}
4372
4373/// One row of the `/context` usage-by-category breakdown.
4374#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4375pub struct ContextCategory {
4376    /// Display name of the row as the CLI renders it, e.g. `"Messages"`.
4377    /// Use `kind` (not this name) to classify the row.
4378    pub name: String,
4379    pub tokens: u64,
4380    /// What the row is: `"used"` content occupies the window; `"free"` is the
4381    /// remaining window; `"buffer"` is the compaction reserve; `"deferred"`
4382    /// rows are out-of-window tool schemas, excluded from usage math.
4383    pub kind: String,
4384}
4385
4386/// An MCP tool's token contribution, in [`ContextUsage::mcp_tools`].
4387#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4388pub struct ContextMcpTool {
4389    /// Wire name, e.g. `"mcp__linear__create_issue"`.
4390    pub name: String,
4391    pub server_name: String,
4392    pub tokens: u64,
4393}
4394
4395/// A memory file's token contribution, in [`ContextUsage::memory_files`].
4396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4397pub struct ContextMemoryFile {
4398    pub path: String,
4399    /// Display label of the memory-file source, e.g. `"Project"` or `"User"`.
4400    #[serde(rename = "type")]
4401    pub file_type: String,
4402    pub tokens: u64,
4403}
4404
4405/// An agent definition's token contribution, in [`ContextUsage::agents`].
4406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4407pub struct ContextAgent {
4408    pub agent_type: String,
4409    /// Raw source identifier, e.g. `"projectSettings"`, `"plugin"`.
4410    pub source: String,
4411    pub tokens: u64,
4412}
4413
4414/// A skill's token contribution, in [`ContextUsage::skills`].
4415#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4416pub struct ContextSkill {
4417    pub name: String,
4418    /// Raw source identifier, e.g. `"userSettings"`, `"plugin"`.
4419    pub source: String,
4420    #[serde(default, skip_serializing_if = "Option::is_none")]
4421    pub plugin_name: Option<String>,
4422    pub tokens: u64,
4423}
4424
4425/// Display metadata for a tool-use block carried on the assistant wrapper.
4426#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4427pub struct ToolUseMeta {
4428    pub id: String,
4429    pub display_name: String,
4430    #[serde(default, skip_serializing_if = "Option::is_none")]
4431    pub server_display_name: Option<String>,
4432    #[serde(default, skip_serializing_if = "Option::is_none")]
4433    pub icon_url: Option<String>,
4434}
4435
4436/// Assistant message
4437#[derive(Debug, Clone, Serialize, Deserialize)]
4438pub struct AssistantMessage {
4439    pub message: AssistantMessageContent,
4440    #[serde(alias = "sessionId")]
4441    pub session_id: String,
4442    #[serde(skip_serializing_if = "Option::is_none")]
4443    pub uuid: Option<String>,
4444    #[serde(skip_serializing_if = "Option::is_none")]
4445    pub parent_tool_use_id: Option<String>,
4446    /// Anthropic API request id that produced this message (e.g. `req_...`).
4447    #[serde(skip_serializing_if = "Option::is_none")]
4448    pub request_id: Option<String>,
4449    /// Client uuid of the user message that triggered this turn, stamped on
4450    /// the turn's first top-level assistant message so a consumer can bind
4451    /// the reply to the send it answers without waiting for the result. With
4452    /// `--include-partial-messages` the first non-ping stream event is
4453    /// stamped too, independently (CLI 2.1.269+), so the same uuid may
4454    /// appear on both frames. Absent on every later assistant message of the
4455    /// turn, on subagent frames, on synthetic/scheduled (meta) turns, and
4456    /// from CLIs before 2.1.259.
4457    #[serde(default, skip_serializing_if = "Option::is_none")]
4458    pub user_message_uuid: Option<String>,
4459    /// Client uuids of every user message whose prompt this turn has consumed
4460    /// so far, in consumption order — all members of a prompt batch the host
4461    /// merged into this one turn. Always contains `user_message_uuid`; at
4462    /// most 64 entries. Present exactly when `user_message_uuid` is; absent
4463    /// from CLIs before 2.1.259 (fall back to `user_message_uuid`).
4464    #[serde(default, skip_serializing_if = "Vec::is_empty")]
4465    pub user_message_uuids: Vec<String>,
4466    /// Subagent type, when this assistant message was produced inside a
4467    /// `local_agent` subagent (e.g. `general-purpose`, `Explore`).
4468    #[serde(skip_serializing_if = "Option::is_none")]
4469    pub subagent_type: Option<String>,
4470    /// Short description of the subagent task, present alongside `subagent_type`.
4471    #[serde(skip_serializing_if = "Option::is_none")]
4472    pub task_description: Option<String>,
4473    #[serde(skip_serializing_if = "Option::is_none")]
4474    pub error: Option<AssistantErrorKind>,
4475    /// True when this message was truncated by an interrupt/abort before the
4476    /// stream completed — `stop_reason` was never received and the content
4477    /// may end mid-word. Absent on normally completed messages.
4478    #[serde(default, skip_serializing_if = "Option::is_none")]
4479    pub aborted: Option<bool>,
4480    /// True when this turn continued the preceding truncated assistant turn
4481    /// inside its trailing signed thinking block (max-output-tokens
4482    /// recovery). Histories replayed through the bridge must carry the flag
4483    /// back so the normalizer keeps the run's prefix on the wire.
4484    #[serde(default, skip_serializing_if = "Option::is_none")]
4485    pub resumed_from_incomplete_thinking: Option<bool>,
4486    #[serde(default, skip_serializing_if = "Vec::is_empty")]
4487    pub supersedes: Vec<String>,
4488    #[serde(skip_serializing_if = "Option::is_none")]
4489    pub timestamp: Option<String>,
4490    #[serde(default, skip_serializing_if = "Vec::is_empty")]
4491    pub tool_use_meta: Vec<ToolUseMeta>,
4492    /// `{id, name}` of the original `Batch*` tool_use block(s) for a message
4493    /// whose content was decomposed into synthetic v1 tool_use blocks.
4494    /// Round-tripped so a replayed history reassembles the batch block on the
4495    /// wire. Wrapper-level sibling — never inside `message.content`.
4496    #[serde(default, skip_serializing_if = "Vec::is_empty")]
4497    pub batch_tool_uses: Vec<BatchToolUse>,
4498    /// `tool_use.input` exactly as the API produced it, keyed by `tool_use`
4499    /// id, for a message whose `message.content` carries the
4500    /// client-normalized input. Round-tripped so a replayed history echoes
4501    /// each earlier tool call back to the API as the API emitted it.
4502    /// Wrapper-level sibling — never inside `message.content` (CLI 2.1.259+).
4503    #[serde(default, skip_serializing_if = "Option::is_none")]
4504    pub wire_tool_inputs: Option<serde_json::Map<String, Value>>,
4505    /// What the client-side input normalization read from process state for
4506    /// the inputs in [`wire_tool_inputs`](Self::wire_tool_inputs), keyed by
4507    /// the same `tool_use` ids. Round-tripped so a replayed history can verify
4508    /// each recorded input against the normalized one. Wrapper-level sibling —
4509    /// never inside `message.content` (CLI 2.1.266+).
4510    #[serde(default, skip_serializing_if = "Option::is_none")]
4511    pub wire_ingest_context: Option<serde_json::Map<String, Value>>,
4512    /// Replayed history rather than a live message, stamped by the Remote
4513    /// Control bridge when it flushes history to the session server, which
4514    /// also stamps it on deliveries it replays (CLI 2.1.266+).
4515    #[serde(default, skip_serializing_if = "Option::is_none")]
4516    pub historical: Option<bool>,
4517    /// Why this frame's turn is the automatic re-run of a turn a worker
4518    /// restart interrupted: the host's `CLAUDE_CODE_RESUME_REASON` when it set
4519    /// one (`host_draining`, `checkpoint_restore`, `container_recreated`,
4520    /// ...), else `interrupted_turn`. Stamped on the same reply frames as
4521    /// `user_message_uuid`, which on such a re-run names the interrupted
4522    /// turn's own last user prompt. Absent on every other turn and from CLIs
4523    /// before 2.1.268.
4524    #[serde(default, skip_serializing_if = "Option::is_none")]
4525    pub resume_reason: Option<String>,
4526    /// The originating `system/local_command` row's wire-form content,
4527    /// carried on the loop-synthesized local-command twin so a bridge/SDK
4528    /// history replay rebuilds the internal system row instead of dropping
4529    /// the output. Wrapper-level sibling — never inside `message.content`
4530    /// (CLI 2.1.259+).
4531    #[serde(default, skip_serializing_if = "Option::is_none")]
4532    pub local_command_source: Option<String>,
4533    /// Ascending zero-based indexes into `message.content` of the thinking
4534    /// blocks whose signature the server tagged as narration (server
4535    /// summaries of the prose between tool calls, not the model's own
4536    /// reasoning), so a renderer can label them as summaries without decoding
4537    /// the signature envelope. Fail-closed: an unparseable or legacy
4538    /// signature is not listed. Omitted when the frame has no such block and
4539    /// by CLIs before 2.1.260; treat unlisted thinking blocks as ordinary
4540    /// thinking. Wrapper-level sibling — never inside `message.content`.
4541    #[serde(default, skip_serializing_if = "Vec::is_empty")]
4542    pub narration_block_indexes: Vec<usize>,
4543    /// How a renderer should treat this frame's narration blocks:
4544    /// `"hidden"` or `"faint"` on CLI 2.1.280+ (kept open for future
4545    /// values). Omitted when the CLI expresses no preference.
4546    #[serde(default, skip_serializing_if = "Option::is_none")]
4547    pub narration_hint: Option<String>,
4548    /// How long the thinking block in this frame took to stream, in whole
4549    /// milliseconds (at least 1): from its `content_block_start` to its
4550    /// `content_block_stop`, so the wait for the response's first token is
4551    /// not counted. For display only. Sent only on a frame whose one content
4552    /// block is a thinking block that was streamed; absent from CLIs before
4553    /// 2.1.287 and from a CLI replaying history it rebuilt from SDK frames,
4554    /// so be ready to show the block without a duration. Wrapper-level
4555    /// sibling — never inside `message.content`.
4556    #[serde(default, skip_serializing_if = "Option::is_none")]
4557    pub thinking_duration_ms: Option<u64>,
4558    /// Structured twin of the `/context` report, carried on the synthetic
4559    /// assistant message that delivers the markdown table. Present only on
4560    /// `/context` results from CLIs new enough to attach it (2.1.239+).
4561    #[serde(default, skip_serializing_if = "Option::is_none")]
4562    pub context_usage: Option<ContextUsage>,
4563    /// Structured twin of the `/usage` report, carried on the synthetic
4564    /// assistant message that delivers its text: the session totals, the
4565    /// plan's usage rows and extra-usage spend, for remote clients that
4566    /// render a card from data. Present only on `/usage` results from CLIs
4567    /// new enough to attach it (2.1.273+) and from claude.ai-subscriber
4568    /// sessions; the text in `message.content` remains the canonical
4569    /// fallback. Wrapper-level sibling — never inside `message.content`.
4570    #[serde(default, skip_serializing_if = "Option::is_none")]
4571    pub usage_report: Option<Box<UsageReport>>,
4572    /// On the local-command twin, the run that printed the row: the
4573    /// command's name (no slash) and its arguments as the echo shows them
4574    /// (`***` when the command marks them sensitive). Present when a `local`
4575    /// command's dispatch printed the row or a `command.run` hook answered
4576    /// it, absent on a never-ran notice. Wrapper-level sibling — never inside
4577    /// `message.content` (CLI 2.1.273+).
4578    #[serde(default, skip_serializing_if = "Option::is_none")]
4579    pub local_command_run: Option<LocalCommandRun>,
4580    /// On the local-command twin, what the row reports (`unavailable_headless`,
4581    /// `unknown`, `failed`, `restart_required`), so a host can offer a fix
4582    /// instead of relaying the text. Absent when the row reports none of
4583    /// these; new kinds are additive, so treat an unknown one as absent.
4584    /// Wrapper-level sibling — never inside `message.content` (CLI 2.1.281+).
4585    #[serde(default, skip_serializing_if = "Option::is_none")]
4586    pub local_command_outcome: Option<LocalCommandOutcome>,
4587    #[serde(default, skip_serializing_if = "Option::is_none")]
4588    pub is_meta: Option<bool>,
4589    #[serde(default, skip_serializing_if = "Option::is_none")]
4590    pub is_virtual: Option<bool>,
4591    #[serde(default, skip_serializing_if = "Option::is_none")]
4592    pub is_api_error_message: Option<bool>,
4593    #[serde(skip_serializing_if = "Option::is_none")]
4594    pub api_error_status: Option<u16>,
4595    /// Typed kind of the API error when `is_api_error_message` is true. An
4596    /// open set the CLI grows release to release: 2.1.274 extends the
4597    /// original `max_output_tokens` / `dlp_request_denied` /
4598    /// `claude_code_version_too_old` with `effort_requires_thinking`,
4599    /// `advisor_incompatible`, `tool_history_mismatch`,
4600    /// `autocompact_thrashing`, `pdf_too_large`, `pdf_password_protected`,
4601    /// `no_response`, `tls_untrusted_ca`, `gateway_content_type`,
4602    /// `provider_credentials`, `gateway_signin_required`,
4603    /// `gateway_session_expired`, `api_key_auth_disabled`,
4604    /// `org_disabled_credential`, `invalid_credential_header`,
4605    /// `model_requires_usage_credits`, `long_context_credits_required`,
4606    /// `consent_unanswered`, `no_allowed_fallback`,
4607    /// `model_substitution_disabled` and `field_not_granted`. Kinds with
4608    /// parameters carry them in [`Self::api_error_params`].
4609    #[serde(skip_serializing_if = "Option::is_none")]
4610    pub api_error: Option<String>,
4611    /// The server's `error.details.error_code` for this API error, copied
4612    /// through when it is an identifier (`^[a-z][a-z0-9_]{0,63}$`) and
4613    /// dropped otherwise. Carries server gate codes this CLI build has no
4614    /// `api_error` value for, so a host can key on a new gate without a
4615    /// Claude Code release. Absent when the response carried no code and
4616    /// from CLIs before 2.1.274.
4617    #[serde(default, skip_serializing_if = "Option::is_none")]
4618    pub api_error_code: Option<String>,
4619    /// Parameters of [`Self::api_error`], present only for the kinds that
4620    /// have any (CLI 2.1.274+).
4621    #[serde(default, skip_serializing_if = "Option::is_none")]
4622    pub api_error_params: Option<ApiErrorParams>,
4623    #[serde(skip_serializing_if = "Option::is_none")]
4624    pub error_details: Option<String>,
4625    #[serde(skip_serializing_if = "Option::is_none")]
4626    pub advisor_model: Option<String>,
4627    #[serde(skip_serializing_if = "Option::is_none")]
4628    pub attribution_agent: Option<String>,
4629    #[serde(skip_serializing_if = "Option::is_none")]
4630    pub attribution_skill: Option<String>,
4631    #[serde(skip_serializing_if = "Option::is_none")]
4632    pub attribution_plugin: Option<String>,
4633    #[serde(skip_serializing_if = "Option::is_none")]
4634    pub attribution_mcp_server: Option<String>,
4635    #[serde(skip_serializing_if = "Option::is_none")]
4636    pub attribution_mcp_tool: Option<String>,
4637}
4638
4639/// Parameters of an assistant frame's `api_error`, carried as
4640/// [`AssistantMessage::api_error_params`] only for the error kinds that have
4641/// any (CLI 2.1.274+).
4642#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
4643pub struct ApiErrorParams {
4644    /// The effort level the API refused (`effort_requires_thinking`).
4645    #[serde(default, skip_serializing_if = "Option::is_none")]
4646    pub effort: Option<String>,
4647    /// The API provider whose credentials failed (`provider_credentials`,
4648    /// `gateway_session_expired`).
4649    #[serde(default, skip_serializing_if = "Option::is_none")]
4650    pub provider: Option<ApiErrorProvider>,
4651    /// How to repair the failed credentials (`provider_credentials`,
4652    /// `gateway_session_expired`).
4653    #[serde(default, skip_serializing_if = "Option::is_none")]
4654    pub remedy: Option<ApiErrorRemedy>,
4655}
4656
4657/// The API provider whose credentials failed ([`ApiErrorParams::provider`]).
4658#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4659pub enum ApiErrorProvider {
4660    Bedrock,
4661    AnthropicAws,
4662    Mantle,
4663    AnthropicGoogleCloud,
4664    Vertex,
4665    Foundry,
4666    Gateway,
4667    /// A provider not yet known to this version of the crate.
4668    Unknown(String),
4669}
4670
4671impl ApiErrorProvider {
4672    pub fn as_str(&self) -> &str {
4673        match self {
4674            Self::Bedrock => "bedrock",
4675            Self::AnthropicAws => "anthropicAws",
4676            Self::Mantle => "mantle",
4677            Self::AnthropicGoogleCloud => "anthropicGoogleCloud",
4678            Self::Vertex => "vertex",
4679            Self::Foundry => "foundry",
4680            Self::Gateway => "gateway",
4681            Self::Unknown(s) => s.as_str(),
4682        }
4683    }
4684}
4685
4686impl fmt::Display for ApiErrorProvider {
4687    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4688        f.write_str(self.as_str())
4689    }
4690}
4691
4692impl From<&str> for ApiErrorProvider {
4693    fn from(s: &str) -> Self {
4694        match s {
4695            "bedrock" => Self::Bedrock,
4696            "anthropicAws" => Self::AnthropicAws,
4697            "mantle" => Self::Mantle,
4698            "anthropicGoogleCloud" => Self::AnthropicGoogleCloud,
4699            "vertex" => Self::Vertex,
4700            "foundry" => Self::Foundry,
4701            "gateway" => Self::Gateway,
4702            other => Self::Unknown(other.to_string()),
4703        }
4704    }
4705}
4706
4707impl Serialize for ApiErrorProvider {
4708    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4709        serializer.serialize_str(self.as_str())
4710    }
4711}
4712
4713impl<'de> Deserialize<'de> for ApiErrorProvider {
4714    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4715        let s = String::deserialize(deserializer)?;
4716        Ok(Self::from(s.as_str()))
4717    }
4718}
4719
4720/// How to repair failed provider credentials ([`ApiErrorParams::remedy`]).
4721#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4722pub enum ApiErrorRemedy {
4723    /// A configured refresh command (`awsAuthRefresh` / `gcpAuthRefresh`)
4724    /// refreshes the credentials.
4725    RefreshCommand,
4726    /// Refresh the provider's credentials by hand; no command is configured.
4727    RefreshCredentials,
4728    /// Refresh the Google application default credentials or the
4729    /// `GOOGLE_APPLICATION_CREDENTIALS` key file.
4730    Adc,
4731    /// Replace the gateway token supplied through `ANTHROPIC_AUTH_TOKEN` or
4732    /// `ANTHROPIC_CUSTOM_HEADERS`.
4733    GatewayToken,
4734    /// The host application owns these credentials.
4735    HostManaged,
4736    /// The credentials work but the model is not enabled for this account
4737    /// and region (Amazon Bedrock).
4738    ModelAccess,
4739    /// A remedy not yet known to this version of the crate.
4740    Unknown(String),
4741}
4742
4743impl ApiErrorRemedy {
4744    pub fn as_str(&self) -> &str {
4745        match self {
4746            Self::RefreshCommand => "refresh_command",
4747            Self::RefreshCredentials => "refresh_credentials",
4748            Self::Adc => "adc",
4749            Self::GatewayToken => "gateway_token",
4750            Self::HostManaged => "host_managed",
4751            Self::ModelAccess => "model_access",
4752            Self::Unknown(s) => s.as_str(),
4753        }
4754    }
4755}
4756
4757impl fmt::Display for ApiErrorRemedy {
4758    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4759        f.write_str(self.as_str())
4760    }
4761}
4762
4763impl From<&str> for ApiErrorRemedy {
4764    fn from(s: &str) -> Self {
4765        match s {
4766            "refresh_command" => Self::RefreshCommand,
4767            "refresh_credentials" => Self::RefreshCredentials,
4768            "adc" => Self::Adc,
4769            "gateway_token" => Self::GatewayToken,
4770            "host_managed" => Self::HostManaged,
4771            "model_access" => Self::ModelAccess,
4772            other => Self::Unknown(other.to_string()),
4773        }
4774    }
4775}
4776
4777impl Serialize for ApiErrorRemedy {
4778    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4779        serializer.serialize_str(self.as_str())
4780    }
4781}
4782
4783impl<'de> Deserialize<'de> for ApiErrorRemedy {
4784    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4785        let s = String::deserialize(deserializer)?;
4786        Ok(Self::from(s.as_str()))
4787    }
4788}
4789
4790/// Nested message content for assistant messages
4791#[derive(Debug, Clone, Serialize, Deserialize)]
4792pub struct AssistantMessageContent {
4793    pub id: String,
4794    /// The Anthropic API message type — always `"message"`.
4795    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
4796    pub message_type: Option<String>,
4797    pub role: MessageRole,
4798    pub model: String,
4799    pub content: Vec<ContentBlock>,
4800    #[serde(skip_serializing_if = "Option::is_none")]
4801    pub stop_reason: Option<StopReason>,
4802    #[serde(skip_serializing_if = "Option::is_none")]
4803    pub stop_sequence: Option<String>,
4804    #[serde(skip_serializing_if = "Option::is_none")]
4805    pub usage: Option<AssistantUsage>,
4806    /// Details about why generation stopped
4807    #[serde(skip_serializing_if = "Option::is_none")]
4808    pub stop_details: Option<Value>,
4809    /// Context management metadata
4810    #[serde(skip_serializing_if = "Option::is_none")]
4811    pub context_management: Option<Value>,
4812}
4813
4814/// Usage information for assistant messages
4815#[derive(Debug, Clone, Serialize, Deserialize)]
4816pub struct AssistantUsage {
4817    /// Number of input tokens
4818    #[serde(default)]
4819    pub input_tokens: u32,
4820
4821    /// Number of output tokens
4822    #[serde(default)]
4823    pub output_tokens: u32,
4824
4825    /// Tokens used to create cache
4826    #[serde(default)]
4827    pub cache_creation_input_tokens: u32,
4828
4829    /// Tokens read from cache
4830    #[serde(default)]
4831    pub cache_read_input_tokens: u32,
4832
4833    /// Service tier used (e.g., "standard")
4834    #[serde(skip_serializing_if = "Option::is_none")]
4835    pub service_tier: Option<String>,
4836
4837    /// Detailed cache creation breakdown
4838    #[serde(skip_serializing_if = "Option::is_none")]
4839    pub cache_creation: Option<CacheCreationDetails>,
4840
4841    /// Inference geography (e.g., "not_available")
4842    #[serde(skip_serializing_if = "Option::is_none")]
4843    pub inference_geo: Option<String>,
4844
4845    /// Outcome of a fallback-credit token the request presented; `null` on
4846    /// the wire when it presented none (CLI 2.1.285+).
4847    #[serde(default, skip_serializing_if = "Option::is_none")]
4848    pub fallback_credit: Option<Value>,
4849}
4850
4851/// Detailed cache creation information
4852#[derive(Debug, Clone, Serialize, Deserialize)]
4853pub struct CacheCreationDetails {
4854    /// Ephemeral 1-hour input tokens
4855    #[serde(default)]
4856    pub ephemeral_1h_input_tokens: u32,
4857
4858    /// Ephemeral 5-minute input tokens
4859    #[serde(default)]
4860    pub ephemeral_5m_input_tokens: u32,
4861}
4862
4863#[cfg(test)]
4864mod tests {
4865    use crate::io::ClaudeOutput;
4866
4867    #[test]
4868    fn test_subagent_usage_rollup_accumulates_task_results() {
4869        use super::SubagentUsageRollup;
4870
4871        let mut rollup = SubagentUsageRollup::default();
4872
4873        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}}"#;
4874        let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
4875        assert!(rollup.observe(&output));
4876        assert_eq!(rollup.subagent_tokens, 10201);
4877        assert_eq!(rollup.agent_count, 1);
4878        assert_eq!(rollup.tool_uses, 3);
4879        assert_eq!(rollup.duration_ms, 1853);
4880
4881        // Replayed frame with the same agentId is counted once.
4882        assert!(!rollup.observe(&output));
4883        assert_eq!(rollup.agent_count, 1);
4884        assert_eq!(rollup.subagent_tokens, 10201);
4885
4886        // A second agent accumulates.
4887        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}}"#;
4888        let output: ClaudeOutput = serde_json::from_str(second).unwrap();
4889        assert!(rollup.observe(&output));
4890        assert_eq!(rollup.agent_count, 2);
4891        assert_eq!(rollup.subagent_tokens, 10701);
4892    }
4893
4894    #[test]
4895    fn test_subagent_usage_rollup_ignores_non_task_results() {
4896        use super::SubagentUsageRollup;
4897
4898        let mut rollup = SubagentUsageRollup::default();
4899
4900        // A ToolSearch tool_use_result parses as an all-None SubagentResult;
4901        // it must not count as a subagent.
4902        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}}"#;
4903        let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
4904        assert!(!rollup.observe(&output));
4905
4906        // Plain user message without tool_use_result.
4907        let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
4908        let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
4909        assert!(!rollup.observe(&output));
4910
4911        // Non-user frames are ignored.
4912        let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
4913        let output: ClaudeOutput = serde_json::from_str(system).unwrap();
4914        assert!(!rollup.observe(&output));
4915
4916        assert_eq!(rollup, SubagentUsageRollup::default());
4917    }
4918
4919    #[test]
4920    fn test_subagent_usage_rollup_over_captured_session() {
4921        use super::SubagentUsageRollup;
4922
4923        let mut rollup = SubagentUsageRollup::default();
4924        let fixture =
4925            include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
4926        for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
4927            if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
4928                rollup.observe(&output);
4929            }
4930        }
4931        assert_eq!(rollup.agent_count, 1);
4932        assert_eq!(rollup.subagent_tokens, 10201);
4933    }
4934
4935    #[test]
4936    fn test_system_message_init() {
4937        let json = r#"{
4938            "type": "system",
4939            "subtype": "init",
4940            "session_id": "test-session-123",
4941            "cwd": "/home/user/project",
4942            "model": "claude-sonnet-4",
4943            "tools": ["Bash", "Read", "Write"],
4944            "mcp_servers": [],
4945            "slash_commands": ["compact", "cost", "review"],
4946            "agents": ["Bash", "Explore", "Plan"],
4947            "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
4948            "skills": [],
4949            "claude_code_version": "2.1.15",
4950            "apiKeySource": "none",
4951            "output_style": "default",
4952            "permissionMode": "default"
4953        }"#;
4954
4955        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
4956        if let ClaudeOutput::System(sys) = output {
4957            assert!(sys.is_init());
4958            assert!(!sys.is_status());
4959            assert!(!sys.is_compact_boundary());
4960
4961            let init = sys.as_init().expect("Should parse as init");
4962            assert_eq!(init.session_id, "test-session-123");
4963            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
4964            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
4965            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
4966            assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
4967            assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
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            assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
4972            assert_eq!(init.output_style, Some(super::OutputStyle::Default));
4973            assert_eq!(
4974                init.permission_mode,
4975                Some(super::InitPermissionMode::Default)
4976            );
4977        } else {
4978            panic!("Expected System message");
4979        }
4980    }
4981
4982    #[test]
4983    fn test_system_message_init_from_real_capture() {
4984        let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
4985        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
4986        if let ClaudeOutput::System(sys) = output {
4987            let init = sys.as_init().expect("Should parse real init capture");
4988            assert_eq!(init.slash_commands.len(), 8);
4989            assert!(init.slash_commands.contains(&"compact".to_string()));
4990            assert!(init.slash_commands.contains(&"review".to_string()));
4991            assert_eq!(init.agents.len(), 5);
4992            assert!(init.agents.contains(&"Bash".to_string()));
4993            assert!(init.agents.contains(&"Explore".to_string()));
4994            assert_eq!(init.plugins.len(), 1);
4995            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
4996            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
4997        } else {
4998            panic!("Expected System message");
4999        }
5000    }
5001
5002    #[test]
5003    fn test_system_message_status() {
5004        let json = r#"{
5005            "type": "system",
5006            "subtype": "status",
5007            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
5008            "status": "compacting",
5009            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
5010        }"#;
5011
5012        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5013        if let ClaudeOutput::System(sys) = output {
5014            assert!(sys.is_status());
5015            assert!(!sys.is_init());
5016
5017            let status = sys.as_status().expect("Should parse as status");
5018            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
5019            assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
5020            assert_eq!(
5021                status.uuid,
5022                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
5023            );
5024        } else {
5025            panic!("Expected System message");
5026        }
5027    }
5028
5029    #[test]
5030    fn test_system_message_status_null() {
5031        let json = r#"{
5032            "type": "system",
5033            "subtype": "status",
5034            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
5035            "status": null,
5036            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
5037        }"#;
5038
5039        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5040        if let ClaudeOutput::System(sys) = output {
5041            let status = sys.as_status().expect("Should parse as status");
5042            assert_eq!(status.status, None);
5043        } else {
5044            panic!("Expected System message");
5045        }
5046    }
5047
5048    #[test]
5049    fn test_system_message_task_started() {
5050        let json = r#"{
5051            "type": "system",
5052            "subtype": "task_started",
5053            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
5054            "task_id": "b6daf3f",
5055            "task_type": "local_bash",
5056            "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
5057            "description": "Wait for CI on PR #12",
5058            "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
5059        }"#;
5060
5061        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5062        if let ClaudeOutput::System(sys) = output {
5063            assert!(sys.is_task_started());
5064            assert!(!sys.is_task_progress());
5065            assert!(!sys.is_task_notification());
5066
5067            let task = sys.as_task_started().expect("Should parse as task_started");
5068            assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
5069            assert_eq!(task.task_id, "b6daf3f");
5070            assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
5071            assert_eq!(
5072                task.tool_use_id.as_deref(),
5073                Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
5074            );
5075            assert_eq!(task.description, "Wait for CI on PR #12");
5076        } else {
5077            panic!("Expected System message");
5078        }
5079    }
5080
5081    #[test]
5082    fn test_system_message_task_started_agent() {
5083        let json = r#"{
5084            "type": "system",
5085            "subtype": "task_started",
5086            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
5087            "task_id": "a4a7e0906e5fc64cc",
5088            "task_type": "local_agent",
5089            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
5090            "description": "Explore Scene/ArrayScene duplication",
5091            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
5092        }"#;
5093
5094        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5095        if let ClaudeOutput::System(sys) = output {
5096            let task = sys.as_task_started().expect("Should parse as task_started");
5097            assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
5098            assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
5099        } else {
5100            panic!("Expected System message");
5101        }
5102    }
5103
5104    #[test]
5105    fn test_system_message_task_progress() {
5106        let json = r#"{
5107            "type": "system",
5108            "subtype": "task_progress",
5109            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
5110            "task_id": "a4a7e0906e5fc64cc",
5111            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
5112            "description": "Reading src/jplephem/chebyshev.rs",
5113            "last_tool_name": "Read",
5114            "usage": {
5115                "duration_ms": 13996,
5116                "tool_uses": 9,
5117                "total_tokens": 38779
5118            },
5119            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
5120        }"#;
5121
5122        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5123        if let ClaudeOutput::System(sys) = output {
5124            assert!(sys.is_task_progress());
5125            assert!(!sys.is_task_started());
5126
5127            let progress = sys
5128                .as_task_progress()
5129                .expect("Should parse as task_progress");
5130            assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
5131            assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
5132            assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
5133            assert_eq!(progress.usage.duration_ms, 13996);
5134            assert_eq!(progress.usage.tool_uses, 9);
5135            assert_eq!(progress.usage.total_tokens, 38779);
5136        } else {
5137            panic!("Expected System message");
5138        }
5139    }
5140
5141    #[test]
5142    fn test_system_message_task_notification_completed() {
5143        let json = r#"{
5144            "type": "system",
5145            "subtype": "task_notification",
5146            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
5147            "task_id": "a0ba761e9dc9c316f",
5148            "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
5149            "status": "completed",
5150            "summary": "Agent \"Write Hipparcos data source doc\" completed",
5151            "output_file": "",
5152            "usage": {
5153                "duration_ms": 172300,
5154                "tool_uses": 11,
5155                "total_tokens": 42005
5156            },
5157            "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
5158        }"#;
5159
5160        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5161        if let ClaudeOutput::System(sys) = output {
5162            assert!(sys.is_task_notification());
5163
5164            let notif = sys
5165                .as_task_notification()
5166                .expect("Should parse as task_notification");
5167            assert_eq!(notif.status, super::TaskStatus::Completed);
5168            assert_eq!(
5169                notif.summary,
5170                "Agent \"Write Hipparcos data source doc\" completed"
5171            );
5172            assert_eq!(notif.output_file, Some("".to_string()));
5173            assert_eq!(
5174                notif.tool_use_id,
5175                Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
5176            );
5177            let usage = notif.usage.expect("Should have usage");
5178            assert_eq!(usage.duration_ms, 172300);
5179            assert_eq!(usage.tool_uses, 11);
5180            assert_eq!(usage.total_tokens, 42005);
5181        } else {
5182            panic!("Expected System message");
5183        }
5184    }
5185
5186    #[test]
5187    fn test_system_message_task_notification_failed_no_usage() {
5188        let json = r#"{
5189            "type": "system",
5190            "subtype": "task_notification",
5191            "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
5192            "task_id": "b98f6a3",
5193            "status": "failed",
5194            "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
5195            "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
5196        }"#;
5197
5198        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5199        if let ClaudeOutput::System(sys) = output {
5200            let notif = sys
5201                .as_task_notification()
5202                .expect("Should parse as task_notification");
5203            assert_eq!(notif.status, super::TaskStatus::Failed);
5204            assert!(notif.tool_use_id.is_none());
5205            assert!(notif.usage.is_none());
5206            assert_eq!(
5207                notif.output_file,
5208                Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
5209            );
5210        } else {
5211            panic!("Expected System message");
5212        }
5213    }
5214
5215    /// Task system messages survive a `to_value` → `from_value` round-trip
5216    /// with their typed accessors still resolving. Mirrors the proxy/relay
5217    /// path where output is reparsed from a `serde_json::Value` rather than
5218    /// straight from the CLI's stdout, so a silently dropped or renamed field
5219    /// surfaces here instead of as a `None` downstream.
5220    #[test]
5221    fn test_task_messages_roundtrip_through_value() {
5222        let cases = [
5223            r#"{"type":"system","subtype":"task_started","session_id":"s1",
5224                "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
5225                "description":"Sleep 3s","uuid":"u1"}"#,
5226            r#"{"type":"system","subtype":"task_progress","session_id":"s1",
5227                "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
5228                "last_tool_name":"Bash",
5229                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
5230                "uuid":"u2"}"#,
5231            r#"{"type":"system","subtype":"task_notification","session_id":"s1",
5232                "task_id":"t1","tool_use_id":"tu1","status":"completed",
5233                "summary":"done","output_file":"",
5234                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
5235                "uuid":"u3"}"#,
5236        ];
5237
5238        for json in cases {
5239            let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5240            let value = serde_json::to_value(&output).unwrap();
5241            let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
5242
5243            let ClaudeOutput::System(sys) = reparsed else {
5244                panic!("Expected System variant after round-trip");
5245            };
5246
5247            match sys.subtype {
5248                super::SystemSubtype::TaskStarted => {
5249                    assert!(
5250                        sys.as_task_started().is_some(),
5251                        "as_task_started failed after round-trip"
5252                    );
5253                }
5254                super::SystemSubtype::TaskProgress => {
5255                    assert!(
5256                        sys.as_task_progress().is_some(),
5257                        "as_task_progress failed after round-trip"
5258                    );
5259                }
5260                super::SystemSubtype::TaskNotification => {
5261                    assert!(
5262                        sys.as_task_notification().is_some(),
5263                        "as_task_notification failed after round-trip"
5264                    );
5265                }
5266                other => panic!("unexpected subtype after round-trip: {other:?}"),
5267            }
5268        }
5269    }
5270
5271    #[test]
5272    fn test_system_message_compact_boundary() {
5273        let json = r#"{
5274            "type": "system",
5275            "subtype": "compact_boundary",
5276            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
5277            "compact_metadata": {
5278                "pre_tokens": 155285,
5279                "trigger": "auto"
5280            },
5281            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
5282        }"#;
5283
5284        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5285        if let ClaudeOutput::System(sys) = output {
5286            assert!(sys.is_compact_boundary());
5287            assert!(!sys.is_init());
5288            assert!(!sys.is_status());
5289
5290            let compact = sys
5291                .as_compact_boundary()
5292                .expect("Should parse as compact_boundary");
5293            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
5294            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
5295            assert_eq!(
5296                compact.compact_metadata.trigger,
5297                super::CompactionTrigger::Auto
5298            );
5299            // Per-compaction stats are optional and absent here.
5300            assert!(compact.summary.is_none());
5301            assert!(compact.leaf_message_count.is_none());
5302            assert!(compact.duration_ms.is_none());
5303        } else {
5304            panic!("Expected System message");
5305        }
5306    }
5307
5308    #[test]
5309    fn test_compact_boundary_with_summary_stats() {
5310        // Canonical keys.
5311        let json = r#"{
5312            "type": "system",
5313            "subtype": "compact_boundary",
5314            "session_id": "s1",
5315            "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
5316            "summary": "Summarized the earlier exploration.",
5317            "leaf_message_count": 42,
5318            "duration_ms": 1234,
5319            "uuid": "u1"
5320        }"#;
5321        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5322        let ClaudeOutput::System(sys) = output else {
5323            panic!("Expected System message");
5324        };
5325        let compact = sys.as_compact_boundary().expect("compact_boundary");
5326        assert_eq!(
5327            compact.summary.as_deref(),
5328            Some("Summarized the earlier exploration.")
5329        );
5330        assert_eq!(compact.leaf_message_count, Some(42));
5331        assert_eq!(compact.duration_ms, Some(1234));
5332
5333        // Alternate wire keys (`content` for summary, `message_count` for count)
5334        // deserialize into the same fields.
5335        let json_alt = r#"{
5336            "type": "system",
5337            "subtype": "compact_boundary",
5338            "session_id": "s2",
5339            "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
5340            "content": "alt-key summary",
5341            "message_count": 7
5342        }"#;
5343        let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
5344        let ClaudeOutput::System(sys) = output else {
5345            panic!("Expected System message");
5346        };
5347        let compact = sys.as_compact_boundary().expect("compact_boundary");
5348        assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
5349        assert_eq!(compact.leaf_message_count, Some(7));
5350    }
5351
5352    #[test]
5353    fn test_init_message_with_new_fields() {
5354        let json = r#"{
5355            "type": "system",
5356            "subtype": "init",
5357            "session_id": "test-session",
5358            "cwd": "/home/user",
5359            "model": "claude-opus-4-7",
5360            "tools": ["Bash"],
5361            "mcp_servers": [],
5362            "permissionMode": "default",
5363            "apiKeySource": "none",
5364            "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
5365            "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
5366            "fast_mode_state": "off",
5367            "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
5368            "claude_code_version": "2.1.117"
5369        }"#;
5370
5371        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5372        if let ClaudeOutput::System(sys) = output {
5373            let init = sys.as_init().expect("Should parse as init");
5374            assert_eq!(
5375                init.uuid.as_deref(),
5376                Some("44841a0d-182d-493a-86b5-79800d3d9665")
5377            );
5378            assert!(init.memory_paths.is_some());
5379            assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
5380            assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
5381            assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
5382        } else {
5383            panic!("Expected System message");
5384        }
5385    }
5386
5387    #[test]
5388    fn test_assistant_message_with_new_fields() {
5389        let json = r#"{
5390            "type": "assistant",
5391            "message": {
5392                "id": "msg_1",
5393                "type": "message",
5394                "role": "assistant",
5395                "model": "claude-opus-4-7",
5396                "content": [{"type": "text", "text": "Hello"}],
5397                "stop_reason": "end_turn",
5398                "stop_details": null,
5399                "context_management": null,
5400                "usage": {
5401                    "input_tokens": 100,
5402                    "output_tokens": 10,
5403                    "cache_creation_input_tokens": 50,
5404                    "cache_read_input_tokens": 0,
5405                    "service_tier": "standard",
5406                    "inference_geo": "not_available"
5407                }
5408            },
5409            "session_id": "abc",
5410            "uuid": "msg-uuid-123"
5411        }"#;
5412
5413        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5414        if let ClaudeOutput::Assistant(asst) = output {
5415            assert_eq!(asst.message.stop_details, None);
5416            assert_eq!(asst.message.context_management, None);
5417            let usage = asst.message.usage.unwrap();
5418            assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
5419        } else {
5420            panic!("Expected Assistant message");
5421        }
5422    }
5423
5424    #[test]
5425    fn test_user_message_with_new_fields() {
5426        let json = r#"{
5427            "type": "user",
5428            "message": {
5429                "role": "user",
5430                "content": [{"type": "text", "text": "Hello"}]
5431            },
5432            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
5433            "parent_tool_use_id": "toolu_123",
5434            "uuid": "user-msg-456"
5435        }"#;
5436
5437        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5438        if let ClaudeOutput::User(user) = output {
5439            assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
5440            assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
5441        } else {
5442            panic!("Expected User message");
5443        }
5444    }
5445
5446    /// Real wire payload captured from the CLI after answering an
5447    /// AskUserQuestion via the permission control protocol. The top-level
5448    /// `tool_use_result` and `timestamp` fields must round-trip without loss —
5449    /// proxies using this crate to relay messages to a viewer rely on those
5450    /// fields being preserved (the viewer reads `tool_use_result.answers`).
5451    #[test]
5452    fn test_user_message_preserves_tool_use_result_and_timestamp() {
5453        let json = r#"{
5454            "type":"user",
5455            "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"}]},
5456            "parent_tool_use_id":null,
5457            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
5458            "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
5459            "timestamp":"2026-05-12T23:12:04.121Z",
5460            "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"}}
5461        }"#;
5462
5463        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5464        let user = match output {
5465            ClaudeOutput::User(u) => u,
5466            other => panic!("Expected User message, got {:?}", other.message_type()),
5467        };
5468
5469        assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
5470        let raw = user
5471            .tool_use_result
5472            .as_ref()
5473            .expect("tool_use_result must be captured");
5474        assert_eq!(raw["answers"]["Color"], "Blue");
5475        assert_eq!(raw["questions"][0]["header"], "Color");
5476
5477        // Round-trip: re-serialize and confirm tool_use_result + timestamp
5478        // survive — the bug we're guarding against is that the proxy silently
5479        // drops these fields when relaying user messages.
5480        let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
5481        assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
5482        assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
5483        assert_eq!(
5484            reser["tool_use_result"]["questions"][0]["question"],
5485            "Which color do you prefer?"
5486        );
5487
5488        // Typed accessor: AskUserQuestionInput has the same shape as the
5489        // AskUserQuestion tool_use_result.
5490        let typed: crate::AskUserQuestionInput = user
5491            .tool_use_result_as::<crate::AskUserQuestionInput>()
5492            .expect("tool_use_result present")
5493            .expect("AskUserQuestionInput parses");
5494        assert_eq!(typed.questions.len(), 1);
5495        assert_eq!(typed.questions[0].header, "Color");
5496        let answers = typed.answers.expect("answers populated");
5497        assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
5498    }
5499
5500    /// User messages without `tool_use_result` / `timestamp` must still
5501    /// deserialize fine and serialize back without spuriously emitting nulls.
5502    #[test]
5503    fn test_user_message_without_tool_use_result_omits_field() {
5504        let json = r#"{
5505            "type":"user",
5506            "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
5507            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
5508        }"#;
5509
5510        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5511        let user = match output {
5512            ClaudeOutput::User(u) => u,
5513            _ => panic!("Expected User message"),
5514        };
5515        assert!(user.tool_use_result.is_none());
5516        assert!(user.timestamp.is_none());
5517
5518        let reser = serde_json::to_value(&user).unwrap();
5519        assert!(reser.get("tool_use_result").is_none());
5520        assert!(reser.get("timestamp").is_none());
5521    }
5522
5523    /// A `Task` tool result must expose subagent token / timing / tool-use
5524    /// accounting through the typed [`UserMessage::subagent_result`] accessor,
5525    /// including the nested per-model `usage` breakdown and `toolStats`.
5526    #[test]
5527    fn test_subagent_result_exposes_token_accounting() {
5528        let json = r#"{
5529            "type":"user",
5530            "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
5531            "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
5532            "tool_use_result":{
5533                "status":"completed",
5534                "prompt":"Count the .rs files.",
5535                "agentId":"ac4f0276e9d4b6232",
5536                "agentType":"Explore",
5537                "content":[{"type":"text","text":"21"}],
5538                "resolvedModel":"claude-haiku-4-5-20251001",
5539                "totalDurationMs":6869,
5540                "totalTokens":7834,
5541                "totalToolUseCount":1,
5542                "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
5543                "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
5544            }
5545        }"#;
5546
5547        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5548        let user = match output {
5549            ClaudeOutput::User(u) => u,
5550            _ => panic!("Expected User message"),
5551        };
5552
5553        let result = user.subagent_result().expect("subagent result parses");
5554        assert_eq!(result.agent_type.as_deref(), Some("Explore"));
5555        assert_eq!(
5556            result.resolved_model.as_deref(),
5557            Some("claude-haiku-4-5-20251001")
5558        );
5559        assert_eq!(result.total_tokens, Some(7834));
5560        assert_eq!(result.total_duration_ms, Some(6869));
5561        assert_eq!(result.total_tool_use_count, Some(1));
5562
5563        let usage = result.usage.expect("nested usage present");
5564        assert_eq!(usage.input_tokens, 6);
5565        assert_eq!(usage.cache_read_input_tokens, 7699);
5566
5567        let stats = result.tool_stats.expect("toolStats present");
5568        assert_eq!(stats.bash_count, 1);
5569    }
5570
5571    /// `tool_use_result` shapes that aren't subagent runs (e.g. AskUserQuestion)
5572    /// parse leniently into the all-`Option` [`SubagentResult`] with empty
5573    /// accounting rather than failing, so callers can probe without panicking.
5574    #[test]
5575    fn test_subagent_result_absent_for_non_task_result() {
5576        let json = r#"{
5577            "type":"user",
5578            "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
5579            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
5580            "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
5581        }"#;
5582
5583        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5584        let user = match output {
5585            ClaudeOutput::User(u) => u,
5586            _ => panic!("Expected User message"),
5587        };
5588
5589        let result = user.subagent_result().expect("lenient parse");
5590        assert_eq!(result.total_tokens, None);
5591        assert_eq!(result.agent_type, None);
5592    }
5593
5594    #[test]
5595    fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
5596        use serde_json::Value;
5597
5598        let raw: Value = serde_json::from_str(
5599            r#"{
5600            "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
5601            "fast_mode_state":"off",
5602            "fast_mode_disabled_reason":"not_first_party",
5603            "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
5604        }"#,
5605        )
5606        .unwrap();
5607        crate::io::assert_fully_wrapped(&raw);
5608
5609        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5610        let ClaudeOutput::System(sys) = output else {
5611            panic!("expected System");
5612        };
5613        let init = sys.as_init().expect("parses as init");
5614        assert_eq!(
5615            init.fast_mode_disabled_reason,
5616            Some(crate::FastModeDisabledReason::NotFirstParty)
5617        );
5618        let errs = init.mcp_server_errors.unwrap();
5619        assert_eq!(errs.len(), 1);
5620        assert_eq!(errs[0].name, "broken");
5621        assert_eq!(errs[0].error_type, "invalid_config");
5622    }
5623
5624    #[test]
5625    fn test_code_change_published_fully_wrapped() {
5626        use super::{KnownSystemEvent, SystemSubtype};
5627        use serde_json::Value;
5628
5629        let raw: Value = serde_json::from_str(
5630            r#"{
5631            "type":"system","subtype":"code_change_published",
5632            "provider":"github","url":"https://github.com/owner/repo/pull/42",
5633            "repo":"owner/repo","identifier":"42",
5634            "uuid":"u1","session_id":"s1"
5635        }"#,
5636        )
5637        .unwrap();
5638        crate::io::assert_fully_wrapped(&raw);
5639
5640        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5641        let ClaudeOutput::System(sys) = output else {
5642            panic!("expected System");
5643        };
5644        assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
5645        let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
5646            panic!("expected CodeChangePublished event");
5647        };
5648        assert_eq!(msg.provider, "github");
5649        assert_eq!(msg.repo, "owner/repo");
5650        assert_eq!(msg.identifier, "42");
5651
5652        assert!(sys.is_code_change_published());
5653        assert!(!sys.is_vcs_state_changed());
5654        let direct = sys.as_code_change_published().expect("direct accessor");
5655        assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
5656        assert!(sys.as_vcs_state_changed().is_none());
5657    }
5658
5659    #[test]
5660    fn test_feedback_draft_queued_fully_wrapped() {
5661        use super::{KnownSystemEvent, SystemSubtype};
5662        use serde_json::Value;
5663
5664        let raw: Value = serde_json::from_str(
5665            r#"{
5666            "type":"system","subtype":"feedback_draft_queued",
5667            "draft_id":"draft-1","draft_type":"bug_report",
5668            "title":"Tool output was truncated",
5669            "details_preview":"The last command omitted its final lines",
5670            "uuid":"u1","session_id":"s1","future_field":"preserved"
5671        }"#,
5672        )
5673        .unwrap();
5674        crate::io::assert_fully_wrapped(&raw);
5675
5676        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5677        let ClaudeOutput::System(sys) = output else {
5678            panic!("expected System");
5679        };
5680        assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
5681        assert!(sys.is_feedback_draft_queued());
5682        assert!(!sys.is_vcs_state_changed());
5683
5684        let direct = sys
5685            .as_feedback_draft_queued()
5686            .expect("direct typed accessor");
5687        assert_eq!(direct.draft_id, "draft-1");
5688        assert_eq!(direct.draft_type, "bug_report");
5689        assert_eq!(direct.extra["future_field"], "preserved");
5690
5691        let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
5692            panic!("expected FeedbackDraftQueued event");
5693        };
5694        assert_eq!(known.title, "Tool output was truncated");
5695        assert_eq!(
5696            sys.typed_value().expect("typed value")["future_field"],
5697            "preserved"
5698        );
5699    }
5700
5701    #[test]
5702    fn test_cloud_session_delta_fully_wrapped() {
5703        use super::{KnownSystemEvent, SystemSubtype};
5704        use serde_json::Value;
5705
5706        let raw: Value = serde_json::from_str(
5707            r#"{
5708            "type":"system","subtype":"cloud_session_delta",
5709            "seq":3,"changed":["serving","connection"],
5710            "cloud_session":{"id":"session_abc","view_url":"https://example.invalid/s/abc",
5711                "serving":{"state":"on"},"connection":{"state":"live"}},
5712            "uuid":"u1","session_id":"s1","future_field":"preserved"
5713        }"#,
5714        )
5715        .unwrap();
5716        crate::io::assert_fully_wrapped(&raw);
5717
5718        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5719        let ClaudeOutput::System(sys) = output else {
5720            panic!("expected System");
5721        };
5722        assert_eq!(sys.subtype, SystemSubtype::CloudSessionDelta);
5723        assert!(sys.is_cloud_session_delta());
5724        assert!(!sys.is_feedback_draft_queued());
5725
5726        let direct = sys.as_cloud_session_delta().expect("direct typed accessor");
5727        assert_eq!(direct.seq, 3);
5728        assert_eq!(direct.changed, vec!["serving", "connection"]);
5729        assert_eq!(direct.cloud_session["id"], "session_abc");
5730        assert_eq!(direct.extra["future_field"], "preserved");
5731
5732        let Some(KnownSystemEvent::CloudSessionDelta(known)) = sys.as_known_system_event() else {
5733            panic!("expected CloudSessionDelta event");
5734        };
5735        assert_eq!(known.session_id, "s1");
5736        assert_eq!(
5737            sys.typed_value().expect("typed value")["future_field"],
5738            "preserved"
5739        );
5740    }
5741
5742    #[test]
5743    fn test_vcs_state_changed_fully_wrapped() {
5744        use super::{KnownSystemEvent, VcsMutationKind};
5745        use serde_json::Value;
5746
5747        for kind in ["commit", "push", "merge", "rebase"] {
5748            let raw: Value = serde_json::from_str(&format!(
5749                r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
5750                kind
5751            ))
5752            .unwrap();
5753            crate::io::assert_fully_wrapped(&raw);
5754
5755            let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5756            let ClaudeOutput::System(sys) = output else {
5757                panic!("expected System");
5758            };
5759            let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
5760                panic!("expected VcsStateChanged event");
5761            };
5762            assert_eq!(msg.kind.as_str(), kind);
5763            assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
5764        }
5765
5766        // Unknown kinds are valid per the wire contract.
5767        let raw: Value = serde_json::from_str(
5768            r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
5769        )
5770        .unwrap();
5771        crate::io::assert_fully_wrapped(&raw);
5772        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5773        let ClaudeOutput::System(sys) = output else {
5774            panic!("expected System");
5775        };
5776        let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
5777            panic!("expected VcsStateChanged event");
5778        };
5779        assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
5780
5781        assert!(sys.is_vcs_state_changed());
5782        let direct = sys.as_vcs_state_changed().expect("direct accessor");
5783        assert_eq!(direct.cwd, "/repo");
5784        assert!(sys.as_code_change_published().is_none());
5785    }
5786
5787    #[test]
5788    fn test_assistant_aborted_and_resume_flags_roundtrip() {
5789        let json = r#"{
5790            "type":"assistant",
5791            "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
5792            "session_id":"s1",
5793            "aborted":true,
5794            "resumed_from_incomplete_thinking":true
5795        }"#;
5796        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5797        let ClaudeOutput::Assistant(msg) = &output else {
5798            panic!("expected Assistant");
5799        };
5800        assert_eq!(msg.aborted, Some(true));
5801        assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
5802        let reserialized = serde_json::to_string(&output).unwrap();
5803        assert!(reserialized.contains("\"aborted\":true"));
5804        assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
5805
5806        // Absent flags stay absent on the wire.
5807        let json = r#"{
5808            "type":"assistant",
5809            "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
5810            "session_id":"s2"
5811        }"#;
5812        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5813        let reserialized = serde_json::to_string(&output).unwrap();
5814        assert!(!reserialized.contains("aborted"));
5815        assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
5816    }
5817
5818    #[test]
5819    fn test_user_tool_result_meta_roundtrip() {
5820        let json = r#"{
5821            "type":"user",
5822            "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
5823            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
5824            "tool_result_meta":[
5825                {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
5826                {"id":"toolu_2","non_execution_kind":"permission-rule"}
5827            ]
5828        }"#;
5829        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5830        let ClaudeOutput::User(user) = &output else {
5831            panic!("expected User");
5832        };
5833        let meta = user.tool_result_meta.as_ref().unwrap();
5834        assert_eq!(meta.len(), 2);
5835        assert_eq!(meta[0].non_execution_kind.as_deref(), Some("user-rejected"));
5836        assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
5837        assert_eq!(meta[1].user_feedback, None);
5838
5839        let reserialized = serde_json::to_string(&output).unwrap();
5840        assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
5841        assert!(!reserialized.contains("\"user_feedback\":null"));
5842    }
5843
5844    /// CLI 2.1.222 added `scope` to `system/model_refusal_fallback`:
5845    /// "session" (main-thread swap, also the meaning when absent on older
5846    /// CLIs) vs "local" (subagent/side-question fallback only).
5847    #[test]
5848    fn model_refusal_fallback_scope_roundtrips_and_defaults() {
5849        use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
5850        let with_scope = serde_json::json!({
5851            "trigger": "refusal",
5852            "direction": "retry",
5853            "scope": "local",
5854            "original_model": "claude-fable-5",
5855            "fallback_model": "claude-opus-5",
5856            "request_id": null,
5857            "content": "Refused; retried on fallback model.",
5858            "uuid": "u1",
5859            "session_id": "s1"
5860        });
5861        let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
5862        assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
5863        assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
5864
5865        // Older CLIs omit scope — absent, not null, and treated as session
5866        // by consumers per the wire docs.
5867        let mut without = with_scope.clone();
5868        without.as_object_mut().unwrap().remove("scope");
5869        let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
5870        assert_eq!(msg.scope, None);
5871        assert_eq!(serde_json::to_value(&msg).unwrap(), without);
5872
5873        // Open enum: unknown scopes pass through verbatim.
5874        assert_eq!(
5875            RefusalFallbackScope::from("workspace").as_str(),
5876            "workspace"
5877        );
5878    }
5879}