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