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