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 as the plugin gave it, at most 4096 characters. Plain text:
3987    /// draw it as a text node, never as markup.
3988    pub text: String,
3989    pub uuid: String,
3990    pub session_id: String,
3991}
3992
3993/// `system/ui_toast` — one `$.ui.toast(text)` from a plugin in a headless
3994/// session: what the REPL shows on the notification bar under the prompt
3995/// for a few seconds, under the plugin's name (CLI 2.1.285+).
3996#[derive(Debug, Clone, Serialize, Deserialize)]
3997pub struct UiToastMessage {
3998    /// The calling plugin's registry name, as the hooks engine knows it.
3999    pub plugin: String,
4000    /// The line as the plugin gave it, at most 4096 characters. Plain text:
4001    /// draw it as a text node, never as markup.
4002    pub text: String,
4003    /// How long the REPL would keep it on the notification bar: the plugin's
4004    /// `timeoutMs`, or the engine's default (4000).
4005    pub timeout_ms: u64,
4006    pub uuid: String,
4007    pub session_id: String,
4008}
4009
4010/// `system/ui_status` — one `$.ui.status(text)` from a plugin in a headless
4011/// session: the line the REPL pins under the prompt until the plugin's next
4012/// call replaces or clears it. One status line per plugin (CLI 2.1.285+).
4013#[derive(Debug, Clone, Serialize, Deserialize)]
4014pub struct UiStatusMessage {
4015    /// The calling plugin's registry name, as the hooks engine knows it.
4016    pub plugin: String,
4017    /// The plugin's pinned line, at most 4096 characters, replacing its
4018    /// previous one; `None` (wire `null`) removes it. Plain text: draw it as
4019    /// a text node, never as markup.
4020    pub text: Option<String>,
4021    pub uuid: String,
4022    pub session_id: String,
4023}
4024
4025/// `system/ui_invalidate` — a plugin called `$.ui.invalidate("ui.render")`,
4026/// or a plugin loaded or reloaded: every tree a remote surface drew through
4027/// `ui_render` may be stale, and the surface re-asks each mounted instance.
4028/// Bumps from one plugin fold to at most one per 100 ms (CLI 2.1.285+).
4029#[derive(Debug, Clone, Serialize, Deserialize)]
4030pub struct UiInvalidateMessage {
4031    /// The render event whose version moved — `"ui.render"`; the engine
4032    /// versions it as one event, so no component or instance is named.
4033    pub event: String,
4034    /// Present when a plugin state write made only these instances stale,
4035    /// so the surface may re-ask them alone. Absent, every mounted instance
4036    /// may be stale.
4037    #[serde(default, skip_serializing_if = "Option::is_none")]
4038    pub instances: Option<Vec<UiInvalidatedInstance>>,
4039    pub uuid: String,
4040    pub session_id: String,
4041}
4042
4043/// One stale instance named by [`UiInvalidateMessage::instances`].
4044#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4045pub struct UiInvalidatedInstance {
4046    /// The surface that drew the instance.
4047    pub surface: String,
4048    /// The component of the instance.
4049    pub component: String,
4050    /// The engine's requestId of the instance, as the `ui_render` that drew
4051    /// it named it.
4052    pub instance_id: String,
4053}
4054
4055/// `system/ui_panes` — the pane roster a remote surface draws changed: a
4056/// plugin opened, retitled or closed a pane, the person showed, focused or
4057/// closed one, or a placing client attached. Deduplicated: an unchanged
4058/// roster is not pushed. A pane opened before the stream armed produces
4059/// none, so read the roster with a `ui_panes` request on mount
4060/// (CLI 2.1.285+).
4061#[derive(Debug, Clone, Serialize, Deserialize)]
4062pub struct UiPanesMessage {
4063    /// The placed open panes in open order (an open appends, a close
4064    /// removes); a pane waiting unplaced is not listed.
4065    pub panes: Vec<UiPane>,
4066    /// The pane the surface shows: the last opened or focused, never
4067    /// displacing one the person holds; `None` with none open.
4068    pub shown_id: Option<String>,
4069    /// The pane the person gave the keyboard; `None` while the composer has
4070    /// it.
4071    pub focused_id: Option<String>,
4072    /// A standing `$.ui.open({ focus: true })` the surface has yet to judge:
4073    /// honour it only while the composer is empty and idle and no dialog is
4074    /// up, else refuse it.
4075    pub focus_requested_id: Option<String>,
4076    pub uuid: String,
4077    pub session_id: String,
4078}
4079
4080/// One placed pane in a [`UiPanesMessage`] roster.
4081#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4082pub struct UiPane {
4083    /// The pane's id as the plugin opened it: 1 to 64 of letters, digits,
4084    /// `_`, `-`.
4085    pub id: String,
4086    /// The pane's tab label: the `title` its latest open gave, else its id.
4087    /// Plugin-authored text: draw it as a text node, never as markup.
4088    pub title: String,
4089    /// The registry name of the plugin that opened it.
4090    pub plugin: String,
4091    /// `true`: Escape while the pane holds the keyboard, or at an idle empty
4092    /// composer, closes it. Absent, Escape only returns the keyboard.
4093    #[serde(default, skip_serializing_if = "Option::is_none")]
4094    pub close_on_escape: Option<bool>,
4095    /// `true`: the surface holds its transient toasts while this pane is
4096    /// the one shown, and shows them once it closes.
4097    #[serde(default, skip_serializing_if = "Option::is_none")]
4098    pub hold_toasts: Option<bool>,
4099    /// The body rows its content asked for while seated inline; a request
4100    /// the person's own sizing overrides.
4101    #[serde(default, skip_serializing_if = "Option::is_none")]
4102    pub rows: Option<u64>,
4103    /// The body columns its content asked for while docked; a request the
4104    /// person's own sizing overrides.
4105    #[serde(default, skip_serializing_if = "Option::is_none")]
4106    pub columns: Option<u64>,
4107}
4108
4109/// `system/ui_scroll` — a plugin's `$.ui.scroll` moved the window of a site
4110/// a remote surface draws: the surface scrolls the site's body there. The
4111/// person's own scrolls are never pushed back (CLI 2.1.285+).
4112#[derive(Debug, Clone, Serialize, Deserialize)]
4113pub struct UiScrollMessage {
4114    /// The client drawing the site, as it attached.
4115    pub client_id: String,
4116    /// Which site: a pane's body or the band above the prompt.
4117    pub component: UiSiteComponent,
4118    /// The pane's id, or `above-prompt` for the band.
4119    pub instance_id: String,
4120    /// The first content row the window is to show, 0 at the top, clamped
4121    /// to the tree as the surface last reported it.
4122    pub offset: u64,
4123    /// `true`: keep the window on the tree's last rows through the draws to
4124    /// come, until the person or a plugin next moves it.
4125    #[serde(default, skip_serializing_if = "Option::is_none")]
4126    pub follow_end: Option<bool>,
4127    pub uuid: String,
4128    pub session_id: String,
4129}
4130
4131/// `system/ui_focus` — a plugin's `$.ui.focus` moved the focus ring of a
4132/// site a remote surface draws onto one of its Buttons, Inputs or Selects:
4133/// the surface focuses that element if it still draws it. The person's own
4134/// moves are never pushed back (CLI 2.1.285+).
4135#[derive(Debug, Clone, Serialize, Deserialize)]
4136pub struct UiFocusMessage {
4137    /// The client drawing the site, as it attached.
4138    pub client_id: String,
4139    /// Which site: a pane's body or the band above the prompt.
4140    pub component: UiSiteComponent,
4141    /// The pane's id, or `above-prompt` for the band.
4142    pub instance_id: String,
4143    /// Whose element takes the ring: its `press.plugin`.
4144    pub plugin: String,
4145    /// The element's `props.key`, as `ui.press` names it.
4146    pub key: String,
4147    pub uuid: String,
4148    pub session_id: String,
4149}
4150
4151/// The site a [`UiScrollMessage`] or [`UiFocusMessage`] addresses.
4152#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4153pub enum UiSiteComponent {
4154    /// A pane's body.
4155    Pane,
4156    /// The band above the prompt.
4157    AbovePrompt,
4158    /// A site not yet known to this version of the crate.
4159    Unknown(String),
4160}
4161
4162impl UiSiteComponent {
4163    pub fn as_str(&self) -> &str {
4164        match self {
4165            Self::Pane => "Pane",
4166            Self::AbovePrompt => "AbovePrompt",
4167            Self::Unknown(s) => s.as_str(),
4168        }
4169    }
4170}
4171
4172impl fmt::Display for UiSiteComponent {
4173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4174        f.write_str(self.as_str())
4175    }
4176}
4177
4178impl From<&str> for UiSiteComponent {
4179    fn from(s: &str) -> Self {
4180        match s {
4181            "Pane" => Self::Pane,
4182            "AbovePrompt" => Self::AbovePrompt,
4183            other => Self::Unknown(other.to_string()),
4184        }
4185    }
4186}
4187
4188impl Serialize for UiSiteComponent {
4189    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4190        serializer.serialize_str(self.as_str())
4191    }
4192}
4193
4194impl<'de> Deserialize<'de> for UiSiteComponent {
4195    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4196        let s = String::deserialize(deserializer)?;
4197        Ok(Self::from(s.as_str()))
4198    }
4199}
4200
4201/// The run that printed a local-command row, carried as
4202/// [`AssistantMessage::local_command_run`].
4203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4204pub struct LocalCommandRun {
4205    /// The command's name as `command.run` carried it (no slash).
4206    pub command: String,
4207    /// Its arguments as the echo shows them (`***` when the command marks
4208    /// them sensitive).
4209    pub args: String,
4210}
4211
4212/// What a local-command row reports, carried as
4213/// [`AssistantMessage::local_command_outcome`] so a host can offer a fix
4214/// instead of relaying the text (CLI 2.1.281+).
4215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4216pub struct LocalCommandOutcome {
4217    pub kind: LocalCommandOutcomeKind,
4218    /// Kind `unknown` only: the registered command closest to the typed
4219    /// name, no slash.
4220    #[serde(default, skip_serializing_if = "Option::is_none")]
4221    pub suggestion: Option<String>,
4222}
4223
4224/// The outcome a local-command row reports ([`LocalCommandOutcome::kind`]).
4225/// New kinds are additive; the CLI itself drops the whole outcome when it
4226/// meets a kind it does not know, so treat [`Unknown`](Self::Unknown) as
4227/// absent.
4228#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4229pub enum LocalCommandOutcomeKind {
4230    /// An interactive-panel command this (headless) session cannot open.
4231    UnavailableHeadless,
4232    /// No command has the typed name; see
4233    /// [`LocalCommandOutcome::suggestion`].
4234    Unknown,
4235    /// The command ran and failed.
4236    Failed,
4237    /// The change applies after Claude Code restarts.
4238    RestartRequired,
4239    /// A kind not yet known to this version of the crate.
4240    Other(String),
4241}
4242
4243impl LocalCommandOutcomeKind {
4244    pub fn as_str(&self) -> &str {
4245        match self {
4246            Self::UnavailableHeadless => "unavailable_headless",
4247            Self::Unknown => "unknown",
4248            Self::Failed => "failed",
4249            Self::RestartRequired => "restart_required",
4250            Self::Other(s) => s.as_str(),
4251        }
4252    }
4253}
4254
4255impl fmt::Display for LocalCommandOutcomeKind {
4256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4257        f.write_str(self.as_str())
4258    }
4259}
4260
4261impl From<&str> for LocalCommandOutcomeKind {
4262    fn from(s: &str) -> Self {
4263        match s {
4264            "unavailable_headless" => Self::UnavailableHeadless,
4265            "unknown" => Self::Unknown,
4266            "failed" => Self::Failed,
4267            "restart_required" => Self::RestartRequired,
4268            other => Self::Other(other.to_string()),
4269        }
4270    }
4271}
4272
4273impl Serialize for LocalCommandOutcomeKind {
4274    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4275        serializer.serialize_str(self.as_str())
4276    }
4277}
4278
4279impl<'de> Deserialize<'de> for LocalCommandOutcomeKind {
4280    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4281        let s = String::deserialize(deserializer)?;
4282        Ok(Self::from(s.as_str()))
4283    }
4284}
4285
4286/// Structured twin of a `/usage` result, carried as
4287/// [`AssistantMessage::usage_report`]: the session's totals, the plan's
4288/// usage rows as the server sent them and the extra-usage spend, and nothing
4289/// else from the usage body (the `get_usage` control reply carries the
4290/// rest). Experimental — the shape may change.
4291#[derive(Debug, Clone, Serialize, Deserialize)]
4292pub struct UsageReport {
4293    /// Cost and usage accumulated by the current session.
4294    pub session: super::control::UsageSession,
4295    /// The plan's usage rows and extra-usage spend from the claude.ai usage
4296    /// endpoint; `None` when the CLI could not fetch them (no plan on this
4297    /// lane, or a token without the profile scope).
4298    pub rate_limits: Option<UsageReportRateLimits>,
4299    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4300    pub extra: serde_json::Map<String, Value>,
4301}
4302
4303/// The plan's usage rows and extra-usage spend in a [`UsageReport`].
4304#[derive(Debug, Clone, Serialize, Deserialize)]
4305pub struct UsageReportRateLimits {
4306    /// The server's usage rows (the usage endpoint's `limits[]`), as sent:
4307    /// which meters apply, their scope, labels, severity and order are the
4308    /// server's, so a client renders them verbatim. Empty when the server
4309    /// reported no meters; `None` when the body carried no rows at all (a
4310    /// server that predates them). When the usage fetch failed and the CLI
4311    /// fell back to rate-limit response headers, this holds at most the one
4312    /// row it synthesizes from them.
4313    pub limits: Option<Vec<UsageReportLimit>>,
4314    /// Extra-usage (overage) spend for the billing period, when the plan
4315    /// has it.
4316    #[serde(default, skip_serializing_if = "Option::is_none")]
4317    pub extra_usage: Option<UsageReportExtraUsage>,
4318    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4319    pub extra: serde_json::Map<String, Value>,
4320}
4321
4322/// One server usage row in [`UsageReportRateLimits::limits`].
4323#[derive(Debug, Clone, Serialize, Deserialize)]
4324pub struct UsageReportLimit {
4325    /// The server's meter kind, e.g. `session`, `weekly_all` or
4326    /// `weekly_scoped`. Classify a row on this, never on a label.
4327    pub kind: String,
4328    /// The server's row group, e.g. `session` or `weekly`; rows render
4329    /// grouped under it, in the server's order.
4330    pub group: String,
4331    /// Share of the window used, 0–100.
4332    pub percent: f64,
4333    /// ISO 8601 timestamp when the window resets.
4334    pub resets_at: Option<String>,
4335    /// What a scoped row is for, a model or a surface, with the server's
4336    /// display label.
4337    #[serde(default, skip_serializing_if = "Option::is_none")]
4338    pub scope: Option<UsageReportScope>,
4339    /// The server's reading of the row for a meter's colour, e.g. `normal`,
4340    /// `warning` or `critical`; a client falls back to its own thresholds
4341    /// without it.
4342    #[serde(default, skip_serializing_if = "Option::is_none")]
4343    pub severity: Option<String>,
4344    /// The server's headline pick: the row a single-value indicator shows.
4345    #[serde(default, skip_serializing_if = "Option::is_none")]
4346    pub is_active: Option<bool>,
4347    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4348    pub extra: serde_json::Map<String, Value>,
4349}
4350
4351/// What a scoped [`UsageReportLimit`] row is for.
4352#[derive(Debug, Clone, Serialize, Deserialize)]
4353pub struct UsageReportScope {
4354    #[serde(default, skip_serializing_if = "Option::is_none")]
4355    pub model: Option<UsageReportScopeLabel>,
4356    #[serde(default, skip_serializing_if = "Option::is_none")]
4357    pub surface: Option<UsageReportScopeLabel>,
4358    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4359    pub extra: serde_json::Map<String, Value>,
4360}
4361
4362/// The server's display label for a [`UsageReportScope`] member.
4363#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4364pub struct UsageReportScopeLabel {
4365    pub display_name: String,
4366}
4367
4368/// Extra-usage (overage) spend for the billing period in a
4369/// [`UsageReportRateLimits`]. Amounts are in minor units of `currency`
4370/// (cents for USD).
4371#[derive(Debug, Clone, Serialize, Deserialize)]
4372pub struct UsageReportExtraUsage {
4373    /// `false` while extra usage cannot cover sends.
4374    pub is_enabled: bool,
4375    pub monthly_limit: Option<f64>,
4376    pub used_credits: Option<f64>,
4377    pub utilization: Option<f64>,
4378    #[serde(default, skip_serializing_if = "Option::is_none")]
4379    pub currency: Option<String>,
4380    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
4381    pub extra: serde_json::Map<String, Value>,
4382}
4383
4384/// `{id, name}` of an original `Batch*` tool_use block, carried in
4385/// [`AssistantMessage::batch_tool_uses`].
4386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4387pub struct BatchToolUse {
4388    pub id: String,
4389    pub name: String,
4390}
4391
4392/// Structured twin of the `/context` report, carried as
4393/// [`AssistantMessage::context_usage`] — the data a client needs to render
4394/// the context-usage card without parsing the markdown table. Evolves
4395/// additively; a breaking reshape would ship as a sibling field.
4396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4397pub struct ContextUsage {
4398    /// Main-loop model the usage was computed for.
4399    pub model: String,
4400    /// Estimated tokens in use, unclamped — may exceed `raw_max_tokens` when
4401    /// over limit.
4402    pub total_tokens: u64,
4403    /// The window usage is measured against: the resolved autocompact window —
4404    /// the model's believed limit, or a smaller compaction-policy window.
4405    pub raw_max_tokens: u64,
4406    /// Rounded `total_tokens / raw_max_tokens`, 0–100+.
4407    pub percentage: u64,
4408    /// Present when `total_tokens` exceeds `raw_max_tokens`.
4409    #[serde(default, skip_serializing_if = "Option::is_none")]
4410    pub over_limit: Option<ContextOverLimit>,
4411    /// Usage-by-category rows (`Messages`, `System prompt`, …).
4412    #[serde(default)]
4413    pub categories: Vec<ContextCategory>,
4414    /// Per-tool token contributions of MCP tools.
4415    #[serde(default)]
4416    pub mcp_tools: Vec<ContextMcpTool>,
4417    /// Per-file token contributions of memory files.
4418    #[serde(default)]
4419    pub memory_files: Vec<ContextMemoryFile>,
4420    /// Per-agent token contributions of agent definitions.
4421    #[serde(default)]
4422    pub agents: Vec<ContextAgent>,
4423    /// Per-skill token contributions. Omitted when no skills contribute.
4424    #[serde(default, skip_serializing_if = "Option::is_none")]
4425    pub skills: Option<Vec<ContextSkill>>,
4426}
4427
4428/// Why and by how much a [`ContextUsage`] exceeds its window.
4429#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4430pub struct ContextOverLimit {
4431    pub tokens_over: u64,
4432    /// How the window was resolved: `"hard_limit"` (the model's believed
4433    /// limit) or `"compaction_window"` (a compaction-policy window).
4434    pub kind: String,
4435}
4436
4437/// One row of the `/context` usage-by-category breakdown.
4438#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4439pub struct ContextCategory {
4440    /// Display name of the row as the CLI renders it, e.g. `"Messages"`.
4441    /// Use `kind` (not this name) to classify the row.
4442    pub name: String,
4443    pub tokens: u64,
4444    /// What the row is: `"used"` content occupies the window; `"free"` is the
4445    /// remaining window; `"buffer"` is the compaction reserve; `"deferred"`
4446    /// rows are out-of-window tool schemas, excluded from usage math.
4447    pub kind: String,
4448}
4449
4450/// An MCP tool's token contribution, in [`ContextUsage::mcp_tools`].
4451#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4452pub struct ContextMcpTool {
4453    /// Wire name, e.g. `"mcp__linear__create_issue"`.
4454    pub name: String,
4455    pub server_name: String,
4456    pub tokens: u64,
4457}
4458
4459/// A memory file's token contribution, in [`ContextUsage::memory_files`].
4460#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4461pub struct ContextMemoryFile {
4462    pub path: String,
4463    /// Display label of the memory-file source, e.g. `"Project"` or `"User"`.
4464    #[serde(rename = "type")]
4465    pub file_type: String,
4466    pub tokens: u64,
4467}
4468
4469/// An agent definition's token contribution, in [`ContextUsage::agents`].
4470#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4471pub struct ContextAgent {
4472    pub agent_type: String,
4473    /// Raw source identifier, e.g. `"projectSettings"`, `"plugin"`.
4474    pub source: String,
4475    pub tokens: u64,
4476}
4477
4478/// A skill's token contribution, in [`ContextUsage::skills`].
4479#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4480pub struct ContextSkill {
4481    pub name: String,
4482    /// Raw source identifier, e.g. `"userSettings"`, `"plugin"`.
4483    pub source: String,
4484    #[serde(default, skip_serializing_if = "Option::is_none")]
4485    pub plugin_name: Option<String>,
4486    pub tokens: u64,
4487}
4488
4489/// Display metadata for a tool-use block carried on the assistant wrapper.
4490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4491pub struct ToolUseMeta {
4492    pub id: String,
4493    pub display_name: String,
4494    #[serde(default, skip_serializing_if = "Option::is_none")]
4495    pub server_display_name: Option<String>,
4496    #[serde(default, skip_serializing_if = "Option::is_none")]
4497    pub icon_url: Option<String>,
4498}
4499
4500/// Assistant message
4501#[derive(Debug, Clone, Serialize, Deserialize)]
4502pub struct AssistantMessage {
4503    pub message: AssistantMessageContent,
4504    #[serde(alias = "sessionId")]
4505    pub session_id: String,
4506    #[serde(skip_serializing_if = "Option::is_none")]
4507    pub uuid: Option<String>,
4508    #[serde(skip_serializing_if = "Option::is_none")]
4509    pub parent_tool_use_id: Option<String>,
4510    /// Anthropic API request id that produced this message (e.g. `req_...`).
4511    #[serde(skip_serializing_if = "Option::is_none")]
4512    pub request_id: Option<String>,
4513    /// Client uuid of the user message that triggered this turn, stamped on
4514    /// the turn's first top-level assistant message so a consumer can bind
4515    /// the reply to the send it answers without waiting for the result. With
4516    /// `--include-partial-messages` the first non-ping stream event is
4517    /// stamped too, independently (CLI 2.1.269+), so the same uuid may
4518    /// appear on both frames. Absent on every later assistant message of the
4519    /// turn, on subagent frames, on synthetic/scheduled (meta) turns, and
4520    /// from CLIs before 2.1.259.
4521    #[serde(default, skip_serializing_if = "Option::is_none")]
4522    pub user_message_uuid: Option<String>,
4523    /// Client uuids of every user message whose prompt this turn has consumed
4524    /// so far, in consumption order — all members of a prompt batch the host
4525    /// merged into this one turn. Always contains `user_message_uuid`; at
4526    /// most 64 entries. Present exactly when `user_message_uuid` is; absent
4527    /// from CLIs before 2.1.259 (fall back to `user_message_uuid`).
4528    #[serde(default, skip_serializing_if = "Vec::is_empty")]
4529    pub user_message_uuids: Vec<String>,
4530    /// Subagent type, when this assistant message was produced inside a
4531    /// `local_agent` subagent (e.g. `general-purpose`, `Explore`).
4532    #[serde(skip_serializing_if = "Option::is_none")]
4533    pub subagent_type: Option<String>,
4534    /// Short description of the subagent task, present alongside `subagent_type`.
4535    #[serde(skip_serializing_if = "Option::is_none")]
4536    pub task_description: Option<String>,
4537    #[serde(skip_serializing_if = "Option::is_none")]
4538    pub error: Option<AssistantErrorKind>,
4539    /// True when this message was truncated by an interrupt/abort before the
4540    /// stream completed — `stop_reason` was never received and the content
4541    /// may end mid-word. Absent on normally completed messages.
4542    #[serde(default, skip_serializing_if = "Option::is_none")]
4543    pub aborted: Option<bool>,
4544    /// True when this turn continued the preceding truncated assistant turn
4545    /// inside its trailing signed thinking block (max-output-tokens
4546    /// recovery). Histories replayed through the bridge must carry the flag
4547    /// back so the normalizer keeps the run's prefix on the wire.
4548    #[serde(default, skip_serializing_if = "Option::is_none")]
4549    pub resumed_from_incomplete_thinking: Option<bool>,
4550    #[serde(default, skip_serializing_if = "Vec::is_empty")]
4551    pub supersedes: Vec<String>,
4552    #[serde(skip_serializing_if = "Option::is_none")]
4553    pub timestamp: Option<String>,
4554    #[serde(default, skip_serializing_if = "Vec::is_empty")]
4555    pub tool_use_meta: Vec<ToolUseMeta>,
4556    /// `{id, name}` of the original `Batch*` tool_use block(s) for a message
4557    /// whose content was decomposed into synthetic v1 tool_use blocks.
4558    /// Round-tripped so a replayed history reassembles the batch block on the
4559    /// wire. Wrapper-level sibling — never inside `message.content`.
4560    #[serde(default, skip_serializing_if = "Vec::is_empty")]
4561    pub batch_tool_uses: Vec<BatchToolUse>,
4562    /// `tool_use.input` exactly as the API produced it, keyed by `tool_use`
4563    /// id, for a message whose `message.content` carries the
4564    /// client-normalized input. Round-tripped so a replayed history echoes
4565    /// each earlier tool call back to the API as the API emitted it.
4566    /// Wrapper-level sibling — never inside `message.content` (CLI 2.1.259+).
4567    #[serde(default, skip_serializing_if = "Option::is_none")]
4568    pub wire_tool_inputs: Option<serde_json::Map<String, Value>>,
4569    /// What the client-side input normalization read from process state for
4570    /// the inputs in [`wire_tool_inputs`](Self::wire_tool_inputs), keyed by
4571    /// the same `tool_use` ids. Round-tripped so a replayed history can verify
4572    /// each recorded input against the normalized one. Wrapper-level sibling —
4573    /// never inside `message.content` (CLI 2.1.266+).
4574    #[serde(default, skip_serializing_if = "Option::is_none")]
4575    pub wire_ingest_context: Option<serde_json::Map<String, Value>>,
4576    /// Replayed history rather than a live message, stamped by the Remote
4577    /// Control bridge when it flushes history to the session server, which
4578    /// also stamps it on deliveries it replays (CLI 2.1.266+).
4579    #[serde(default, skip_serializing_if = "Option::is_none")]
4580    pub historical: Option<bool>,
4581    /// Why this frame's turn is the automatic re-run of a turn a worker
4582    /// restart interrupted: the host's `CLAUDE_CODE_RESUME_REASON` when it set
4583    /// one (`host_draining`, `checkpoint_restore`, `container_recreated`,
4584    /// ...), else `interrupted_turn`. Stamped on the same reply frames as
4585    /// `user_message_uuid`, which on such a re-run names the interrupted
4586    /// turn's own last user prompt. Absent on every other turn and from CLIs
4587    /// before 2.1.268.
4588    #[serde(default, skip_serializing_if = "Option::is_none")]
4589    pub resume_reason: Option<String>,
4590    /// The originating `system/local_command` row's wire-form content,
4591    /// carried on the loop-synthesized local-command twin so a bridge/SDK
4592    /// history replay rebuilds the internal system row instead of dropping
4593    /// the output. Wrapper-level sibling — never inside `message.content`
4594    /// (CLI 2.1.259+).
4595    #[serde(default, skip_serializing_if = "Option::is_none")]
4596    pub local_command_source: Option<String>,
4597    /// Ascending zero-based indexes into `message.content` of the thinking
4598    /// blocks whose signature the server tagged as narration (server
4599    /// summaries of the prose between tool calls, not the model's own
4600    /// reasoning), so a renderer can label them as summaries without decoding
4601    /// the signature envelope. Fail-closed: an unparseable or legacy
4602    /// signature is not listed. Omitted when the frame has no such block and
4603    /// by CLIs before 2.1.260; treat unlisted thinking blocks as ordinary
4604    /// thinking. Wrapper-level sibling — never inside `message.content`.
4605    #[serde(default, skip_serializing_if = "Vec::is_empty")]
4606    pub narration_block_indexes: Vec<usize>,
4607    /// How a renderer should treat this frame's narration blocks:
4608    /// `"hidden"` or `"faint"` on CLI 2.1.280+ (kept open for future
4609    /// values). Omitted when the CLI expresses no preference.
4610    #[serde(default, skip_serializing_if = "Option::is_none")]
4611    pub narration_hint: Option<String>,
4612    /// How long the thinking block in this frame took to stream, in whole
4613    /// milliseconds (at least 1): from its `content_block_start` to its
4614    /// `content_block_stop`, so the wait for the response's first token is
4615    /// not counted. For display only. Sent only on a frame whose one content
4616    /// block is a thinking block that was streamed; absent from CLIs before
4617    /// 2.1.287 and from a CLI replaying history it rebuilt from SDK frames,
4618    /// so be ready to show the block without a duration. Wrapper-level
4619    /// sibling — never inside `message.content`.
4620    #[serde(default, skip_serializing_if = "Option::is_none")]
4621    pub thinking_duration_ms: Option<u64>,
4622    /// Structured twin of the `/context` report, carried on the synthetic
4623    /// assistant message that delivers the markdown table. Present only on
4624    /// `/context` results from CLIs new enough to attach it (2.1.239+).
4625    #[serde(default, skip_serializing_if = "Option::is_none")]
4626    pub context_usage: Option<ContextUsage>,
4627    /// Structured twin of the `/usage` report, carried on the synthetic
4628    /// assistant message that delivers its text: the session totals, the
4629    /// plan's usage rows and extra-usage spend, for remote clients that
4630    /// render a card from data. Present only on `/usage` results from CLIs
4631    /// new enough to attach it (2.1.273+) and from claude.ai-subscriber
4632    /// sessions; the text in `message.content` remains the canonical
4633    /// fallback. Wrapper-level sibling — never inside `message.content`.
4634    #[serde(default, skip_serializing_if = "Option::is_none")]
4635    pub usage_report: Option<Box<UsageReport>>,
4636    /// On the local-command twin, the run that printed the row: the
4637    /// command's name (no slash) and its arguments as the echo shows them
4638    /// (`***` when the command marks them sensitive). Present when a `local`
4639    /// command's dispatch printed the row or a `command.run` hook answered
4640    /// it, absent on a never-ran notice. Wrapper-level sibling — never inside
4641    /// `message.content` (CLI 2.1.273+).
4642    #[serde(default, skip_serializing_if = "Option::is_none")]
4643    pub local_command_run: Option<LocalCommandRun>,
4644    /// On the local-command twin, what the row reports (`unavailable_headless`,
4645    /// `unknown`, `failed`, `restart_required`), so a host can offer a fix
4646    /// instead of relaying the text. Absent when the row reports none of
4647    /// these; new kinds are additive, so treat an unknown one as absent.
4648    /// Wrapper-level sibling — never inside `message.content` (CLI 2.1.281+).
4649    #[serde(default, skip_serializing_if = "Option::is_none")]
4650    pub local_command_outcome: Option<LocalCommandOutcome>,
4651    #[serde(default, skip_serializing_if = "Option::is_none")]
4652    pub is_meta: Option<bool>,
4653    #[serde(default, skip_serializing_if = "Option::is_none")]
4654    pub is_virtual: Option<bool>,
4655    #[serde(default, skip_serializing_if = "Option::is_none")]
4656    pub is_api_error_message: Option<bool>,
4657    #[serde(skip_serializing_if = "Option::is_none")]
4658    pub api_error_status: Option<u16>,
4659    /// Typed kind of the API error when `is_api_error_message` is true. An
4660    /// open set the CLI grows release to release: 2.1.274 extends the
4661    /// original `max_output_tokens` / `dlp_request_denied` /
4662    /// `claude_code_version_too_old` with `effort_requires_thinking`,
4663    /// `advisor_incompatible`, `tool_history_mismatch`,
4664    /// `autocompact_thrashing`, `pdf_too_large`, `pdf_password_protected`,
4665    /// `media_removed` (CLI 2.1.288+: the API refused an image or a
4666    /// document, so Claude Code leaves it out of the requests that follow),
4667    /// `no_response`, `tls_untrusted_ca`, `gateway_content_type`,
4668    /// `provider_credentials`, `gateway_signin_required`,
4669    /// `gateway_session_expired`, `api_key_auth_disabled`,
4670    /// `org_disabled_credential`, `invalid_credential_header`,
4671    /// `model_requires_usage_credits`, `long_context_credits_required`,
4672    /// `consent_unanswered`, `no_allowed_fallback`,
4673    /// `model_substitution_disabled` and `field_not_granted`. Kinds with
4674    /// parameters carry them in [`Self::api_error_params`].
4675    #[serde(skip_serializing_if = "Option::is_none")]
4676    pub api_error: Option<String>,
4677    /// The server's `error.details.error_code` for this API error, copied
4678    /// through when it is an identifier (`^[a-z][a-z0-9_]{0,63}$`) and
4679    /// dropped otherwise. Carries server gate codes this CLI build has no
4680    /// `api_error` value for, so a host can key on a new gate without a
4681    /// Claude Code release. Absent when the response carried no code and
4682    /// from CLIs before 2.1.274.
4683    #[serde(default, skip_serializing_if = "Option::is_none")]
4684    pub api_error_code: Option<String>,
4685    /// Parameters of [`Self::api_error`], present only for the kinds that
4686    /// have any (CLI 2.1.274+).
4687    #[serde(default, skip_serializing_if = "Option::is_none")]
4688    pub api_error_params: Option<ApiErrorParams>,
4689    #[serde(skip_serializing_if = "Option::is_none")]
4690    pub error_details: Option<String>,
4691    #[serde(skip_serializing_if = "Option::is_none")]
4692    pub advisor_model: Option<String>,
4693    #[serde(skip_serializing_if = "Option::is_none")]
4694    pub attribution_agent: Option<String>,
4695    #[serde(skip_serializing_if = "Option::is_none")]
4696    pub attribution_skill: Option<String>,
4697    #[serde(skip_serializing_if = "Option::is_none")]
4698    pub attribution_plugin: Option<String>,
4699    #[serde(skip_serializing_if = "Option::is_none")]
4700    pub attribution_mcp_server: Option<String>,
4701    #[serde(skip_serializing_if = "Option::is_none")]
4702    pub attribution_mcp_tool: Option<String>,
4703}
4704
4705/// Parameters of an assistant frame's `api_error`, carried as
4706/// [`AssistantMessage::api_error_params`] only for the error kinds that have
4707/// any (CLI 2.1.274+).
4708#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
4709pub struct ApiErrorParams {
4710    /// The effort level the API refused (`effort_requires_thinking`).
4711    #[serde(default, skip_serializing_if = "Option::is_none")]
4712    pub effort: Option<String>,
4713    /// The API provider whose credentials failed (`provider_credentials`,
4714    /// `gateway_session_expired`).
4715    #[serde(default, skip_serializing_if = "Option::is_none")]
4716    pub provider: Option<ApiErrorProvider>,
4717    /// How to repair the failed credentials (`provider_credentials`,
4718    /// `gateway_session_expired`).
4719    #[serde(default, skip_serializing_if = "Option::is_none")]
4720    pub remedy: Option<ApiErrorRemedy>,
4721    /// The kind of block the API refused (`media_removed`); absent for
4722    /// [`ApiErrorMediaReason::MediaBudget`] (CLI 2.1.288+).
4723    #[serde(default, skip_serializing_if = "Option::is_none")]
4724    pub media: Option<ApiErrorMedia>,
4725    /// Why the API refused the block (`media_removed`, CLI 2.1.288+).
4726    #[serde(default, skip_serializing_if = "Option::is_none")]
4727    pub media_reason: Option<ApiErrorMediaReason>,
4728}
4729
4730/// The kind of block the API refused ([`ApiErrorParams::media`]).
4731#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4732pub enum ApiErrorMedia {
4733    Image,
4734    Document,
4735    /// A kind not yet known to this version of the crate.
4736    Unknown(String),
4737}
4738
4739impl ApiErrorMedia {
4740    pub fn as_str(&self) -> &str {
4741        match self {
4742            Self::Image => "image",
4743            Self::Document => "document",
4744            Self::Unknown(s) => s.as_str(),
4745        }
4746    }
4747}
4748
4749impl fmt::Display for ApiErrorMedia {
4750    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4751        f.write_str(self.as_str())
4752    }
4753}
4754
4755impl From<&str> for ApiErrorMedia {
4756    fn from(s: &str) -> Self {
4757        match s {
4758            "image" => Self::Image,
4759            "document" => Self::Document,
4760            other => Self::Unknown(other.to_string()),
4761        }
4762    }
4763}
4764
4765impl Serialize for ApiErrorMedia {
4766    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4767        serializer.serialize_str(self.as_str())
4768    }
4769}
4770
4771impl<'de> Deserialize<'de> for ApiErrorMedia {
4772    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4773        let s = String::deserialize(deserializer)?;
4774        Ok(Self::from(s.as_str()))
4775    }
4776}
4777
4778/// Why the API refused an image or document
4779/// ([`ApiErrorParams::media_reason`]).
4780#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4781pub enum ApiErrorMediaReason {
4782    /// Every other refusal, a gateway's report of a model's refusal among
4783    /// them.
4784    Unprocessable,
4785    /// The API said that the model does not accept PDF documents.
4786    UnsupportedByModel,
4787    /// The request's images and documents together exceed the API's limit,
4788    /// and the requests that follow may leave out blocks of either kind.
4789    MediaBudget,
4790    /// A reason not yet known to this version of the crate.
4791    Unknown(String),
4792}
4793
4794impl ApiErrorMediaReason {
4795    pub fn as_str(&self) -> &str {
4796        match self {
4797            Self::Unprocessable => "unprocessable",
4798            Self::UnsupportedByModel => "unsupported_by_model",
4799            Self::MediaBudget => "media_budget",
4800            Self::Unknown(s) => s.as_str(),
4801        }
4802    }
4803}
4804
4805impl fmt::Display for ApiErrorMediaReason {
4806    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4807        f.write_str(self.as_str())
4808    }
4809}
4810
4811impl From<&str> for ApiErrorMediaReason {
4812    fn from(s: &str) -> Self {
4813        match s {
4814            "unprocessable" => Self::Unprocessable,
4815            "unsupported_by_model" => Self::UnsupportedByModel,
4816            "media_budget" => Self::MediaBudget,
4817            other => Self::Unknown(other.to_string()),
4818        }
4819    }
4820}
4821
4822impl Serialize for ApiErrorMediaReason {
4823    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4824        serializer.serialize_str(self.as_str())
4825    }
4826}
4827
4828impl<'de> Deserialize<'de> for ApiErrorMediaReason {
4829    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4830        let s = String::deserialize(deserializer)?;
4831        Ok(Self::from(s.as_str()))
4832    }
4833}
4834
4835/// The API provider whose credentials failed ([`ApiErrorParams::provider`]).
4836#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4837pub enum ApiErrorProvider {
4838    Bedrock,
4839    AnthropicAws,
4840    Mantle,
4841    AnthropicGoogleCloud,
4842    Vertex,
4843    Foundry,
4844    Gateway,
4845    /// A provider not yet known to this version of the crate.
4846    Unknown(String),
4847}
4848
4849impl ApiErrorProvider {
4850    pub fn as_str(&self) -> &str {
4851        match self {
4852            Self::Bedrock => "bedrock",
4853            Self::AnthropicAws => "anthropicAws",
4854            Self::Mantle => "mantle",
4855            Self::AnthropicGoogleCloud => "anthropicGoogleCloud",
4856            Self::Vertex => "vertex",
4857            Self::Foundry => "foundry",
4858            Self::Gateway => "gateway",
4859            Self::Unknown(s) => s.as_str(),
4860        }
4861    }
4862}
4863
4864impl fmt::Display for ApiErrorProvider {
4865    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4866        f.write_str(self.as_str())
4867    }
4868}
4869
4870impl From<&str> for ApiErrorProvider {
4871    fn from(s: &str) -> Self {
4872        match s {
4873            "bedrock" => Self::Bedrock,
4874            "anthropicAws" => Self::AnthropicAws,
4875            "mantle" => Self::Mantle,
4876            "anthropicGoogleCloud" => Self::AnthropicGoogleCloud,
4877            "vertex" => Self::Vertex,
4878            "foundry" => Self::Foundry,
4879            "gateway" => Self::Gateway,
4880            other => Self::Unknown(other.to_string()),
4881        }
4882    }
4883}
4884
4885impl Serialize for ApiErrorProvider {
4886    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4887        serializer.serialize_str(self.as_str())
4888    }
4889}
4890
4891impl<'de> Deserialize<'de> for ApiErrorProvider {
4892    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4893        let s = String::deserialize(deserializer)?;
4894        Ok(Self::from(s.as_str()))
4895    }
4896}
4897
4898/// How to repair failed provider credentials ([`ApiErrorParams::remedy`]).
4899#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4900pub enum ApiErrorRemedy {
4901    /// A configured refresh command (`awsAuthRefresh` / `gcpAuthRefresh`)
4902    /// refreshes the credentials.
4903    RefreshCommand,
4904    /// Refresh the provider's credentials by hand; no command is configured.
4905    RefreshCredentials,
4906    /// Refresh the Google application default credentials or the
4907    /// `GOOGLE_APPLICATION_CREDENTIALS` key file.
4908    Adc,
4909    /// Replace the gateway token supplied through `ANTHROPIC_AUTH_TOKEN` or
4910    /// `ANTHROPIC_CUSTOM_HEADERS`.
4911    GatewayToken,
4912    /// The host application owns these credentials.
4913    HostManaged,
4914    /// The credentials work but the model is not enabled for this account
4915    /// and region (Amazon Bedrock).
4916    ModelAccess,
4917    /// A remedy not yet known to this version of the crate.
4918    Unknown(String),
4919}
4920
4921impl ApiErrorRemedy {
4922    pub fn as_str(&self) -> &str {
4923        match self {
4924            Self::RefreshCommand => "refresh_command",
4925            Self::RefreshCredentials => "refresh_credentials",
4926            Self::Adc => "adc",
4927            Self::GatewayToken => "gateway_token",
4928            Self::HostManaged => "host_managed",
4929            Self::ModelAccess => "model_access",
4930            Self::Unknown(s) => s.as_str(),
4931        }
4932    }
4933}
4934
4935impl fmt::Display for ApiErrorRemedy {
4936    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4937        f.write_str(self.as_str())
4938    }
4939}
4940
4941impl From<&str> for ApiErrorRemedy {
4942    fn from(s: &str) -> Self {
4943        match s {
4944            "refresh_command" => Self::RefreshCommand,
4945            "refresh_credentials" => Self::RefreshCredentials,
4946            "adc" => Self::Adc,
4947            "gateway_token" => Self::GatewayToken,
4948            "host_managed" => Self::HostManaged,
4949            "model_access" => Self::ModelAccess,
4950            other => Self::Unknown(other.to_string()),
4951        }
4952    }
4953}
4954
4955impl Serialize for ApiErrorRemedy {
4956    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
4957        serializer.serialize_str(self.as_str())
4958    }
4959}
4960
4961impl<'de> Deserialize<'de> for ApiErrorRemedy {
4962    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
4963        let s = String::deserialize(deserializer)?;
4964        Ok(Self::from(s.as_str()))
4965    }
4966}
4967
4968/// Nested message content for assistant messages
4969#[derive(Debug, Clone, Serialize, Deserialize)]
4970pub struct AssistantMessageContent {
4971    pub id: String,
4972    /// The Anthropic API message type — always `"message"`.
4973    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
4974    pub message_type: Option<String>,
4975    pub role: MessageRole,
4976    pub model: String,
4977    pub content: Vec<ContentBlock>,
4978    #[serde(skip_serializing_if = "Option::is_none")]
4979    pub stop_reason: Option<StopReason>,
4980    #[serde(skip_serializing_if = "Option::is_none")]
4981    pub stop_sequence: Option<String>,
4982    #[serde(skip_serializing_if = "Option::is_none")]
4983    pub usage: Option<AssistantUsage>,
4984    /// Details about why generation stopped
4985    #[serde(skip_serializing_if = "Option::is_none")]
4986    pub stop_details: Option<Value>,
4987    /// Context management metadata
4988    #[serde(skip_serializing_if = "Option::is_none")]
4989    pub context_management: Option<Value>,
4990}
4991
4992/// Usage information for assistant messages
4993#[derive(Debug, Clone, Serialize, Deserialize)]
4994pub struct AssistantUsage {
4995    /// Number of input tokens
4996    #[serde(default)]
4997    pub input_tokens: u32,
4998
4999    /// Number of output tokens
5000    #[serde(default)]
5001    pub output_tokens: u32,
5002
5003    /// Tokens used to create cache
5004    #[serde(default)]
5005    pub cache_creation_input_tokens: u32,
5006
5007    /// Tokens read from cache
5008    #[serde(default)]
5009    pub cache_read_input_tokens: u32,
5010
5011    /// Service tier used (e.g., "standard")
5012    #[serde(skip_serializing_if = "Option::is_none")]
5013    pub service_tier: Option<String>,
5014
5015    /// Detailed cache creation breakdown
5016    #[serde(skip_serializing_if = "Option::is_none")]
5017    pub cache_creation: Option<CacheCreationDetails>,
5018
5019    /// Inference geography (e.g., "not_available")
5020    #[serde(skip_serializing_if = "Option::is_none")]
5021    pub inference_geo: Option<String>,
5022
5023    /// Outcome of a fallback-credit token the request presented; `null` on
5024    /// the wire when it presented none (CLI 2.1.285+).
5025    #[serde(default, skip_serializing_if = "Option::is_none")]
5026    pub fallback_credit: Option<Value>,
5027}
5028
5029/// Detailed cache creation information
5030#[derive(Debug, Clone, Serialize, Deserialize)]
5031pub struct CacheCreationDetails {
5032    /// Ephemeral 1-hour input tokens
5033    #[serde(default)]
5034    pub ephemeral_1h_input_tokens: u32,
5035
5036    /// Ephemeral 5-minute input tokens
5037    #[serde(default)]
5038    pub ephemeral_5m_input_tokens: u32,
5039}
5040
5041#[cfg(test)]
5042mod tests {
5043    use crate::io::ClaudeOutput;
5044
5045    #[test]
5046    fn test_subagent_usage_rollup_accumulates_task_results() {
5047        use super::SubagentUsageRollup;
5048
5049        let mut rollup = SubagentUsageRollup::default();
5050
5051        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}}"#;
5052        let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
5053        assert!(rollup.observe(&output));
5054        assert_eq!(rollup.subagent_tokens, 10201);
5055        assert_eq!(rollup.agent_count, 1);
5056        assert_eq!(rollup.tool_uses, 3);
5057        assert_eq!(rollup.duration_ms, 1853);
5058
5059        // Replayed frame with the same agentId is counted once.
5060        assert!(!rollup.observe(&output));
5061        assert_eq!(rollup.agent_count, 1);
5062        assert_eq!(rollup.subagent_tokens, 10201);
5063
5064        // A second agent accumulates.
5065        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}}"#;
5066        let output: ClaudeOutput = serde_json::from_str(second).unwrap();
5067        assert!(rollup.observe(&output));
5068        assert_eq!(rollup.agent_count, 2);
5069        assert_eq!(rollup.subagent_tokens, 10701);
5070    }
5071
5072    #[test]
5073    fn test_subagent_usage_rollup_ignores_non_task_results() {
5074        use super::SubagentUsageRollup;
5075
5076        let mut rollup = SubagentUsageRollup::default();
5077
5078        // A ToolSearch tool_use_result parses as an all-None SubagentResult;
5079        // it must not count as a subagent.
5080        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}}"#;
5081        let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
5082        assert!(!rollup.observe(&output));
5083
5084        // Plain user message without tool_use_result.
5085        let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
5086        let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
5087        assert!(!rollup.observe(&output));
5088
5089        // Non-user frames are ignored.
5090        let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
5091        let output: ClaudeOutput = serde_json::from_str(system).unwrap();
5092        assert!(!rollup.observe(&output));
5093
5094        assert_eq!(rollup, SubagentUsageRollup::default());
5095    }
5096
5097    #[test]
5098    fn test_subagent_usage_rollup_over_captured_session() {
5099        use super::SubagentUsageRollup;
5100
5101        let mut rollup = SubagentUsageRollup::default();
5102        let fixture =
5103            include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
5104        for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
5105            if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
5106                rollup.observe(&output);
5107            }
5108        }
5109        assert_eq!(rollup.agent_count, 1);
5110        assert_eq!(rollup.subagent_tokens, 10201);
5111    }
5112
5113    #[test]
5114    fn test_system_message_init() {
5115        let json = r#"{
5116            "type": "system",
5117            "subtype": "init",
5118            "session_id": "test-session-123",
5119            "cwd": "/home/user/project",
5120            "model": "claude-sonnet-4",
5121            "tools": ["Bash", "Read", "Write"],
5122            "mcp_servers": [],
5123            "slash_commands": ["compact", "cost", "review"],
5124            "agents": ["Bash", "Explore", "Plan"],
5125            "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
5126            "skills": [],
5127            "claude_code_version": "2.1.15",
5128            "apiKeySource": "none",
5129            "output_style": "default",
5130            "permissionMode": "default"
5131        }"#;
5132
5133        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5134        if let ClaudeOutput::System(sys) = output {
5135            assert!(sys.is_init());
5136            assert!(!sys.is_status());
5137            assert!(!sys.is_compact_boundary());
5138
5139            let init = sys.as_init().expect("Should parse as init");
5140            assert_eq!(init.session_id, "test-session-123");
5141            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
5142            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
5143            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
5144            assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
5145            assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
5146            assert_eq!(init.plugins.len(), 1);
5147            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
5148            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
5149            assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
5150            assert_eq!(init.output_style, Some(super::OutputStyle::Default));
5151            assert_eq!(
5152                init.permission_mode,
5153                Some(super::InitPermissionMode::Default)
5154            );
5155        } else {
5156            panic!("Expected System message");
5157        }
5158    }
5159
5160    #[test]
5161    fn test_system_message_init_from_real_capture() {
5162        let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
5163        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5164        if let ClaudeOutput::System(sys) = output {
5165            let init = sys.as_init().expect("Should parse real init capture");
5166            assert_eq!(init.slash_commands.len(), 8);
5167            assert!(init.slash_commands.contains(&"compact".to_string()));
5168            assert!(init.slash_commands.contains(&"review".to_string()));
5169            assert_eq!(init.agents.len(), 5);
5170            assert!(init.agents.contains(&"Bash".to_string()));
5171            assert!(init.agents.contains(&"Explore".to_string()));
5172            assert_eq!(init.plugins.len(), 1);
5173            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
5174            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
5175        } else {
5176            panic!("Expected System message");
5177        }
5178    }
5179
5180    #[test]
5181    fn test_system_message_status() {
5182        let json = r#"{
5183            "type": "system",
5184            "subtype": "status",
5185            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
5186            "status": "compacting",
5187            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
5188        }"#;
5189
5190        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5191        if let ClaudeOutput::System(sys) = output {
5192            assert!(sys.is_status());
5193            assert!(!sys.is_init());
5194
5195            let status = sys.as_status().expect("Should parse as status");
5196            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
5197            assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
5198            assert_eq!(
5199                status.uuid,
5200                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
5201            );
5202        } else {
5203            panic!("Expected System message");
5204        }
5205    }
5206
5207    #[test]
5208    fn test_system_message_status_null() {
5209        let json = r#"{
5210            "type": "system",
5211            "subtype": "status",
5212            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
5213            "status": null,
5214            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
5215        }"#;
5216
5217        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5218        if let ClaudeOutput::System(sys) = output {
5219            let status = sys.as_status().expect("Should parse as status");
5220            assert_eq!(status.status, None);
5221        } else {
5222            panic!("Expected System message");
5223        }
5224    }
5225
5226    #[test]
5227    fn test_system_message_task_started() {
5228        let json = r#"{
5229            "type": "system",
5230            "subtype": "task_started",
5231            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
5232            "task_id": "b6daf3f",
5233            "task_type": "local_bash",
5234            "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
5235            "description": "Wait for CI on PR #12",
5236            "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
5237        }"#;
5238
5239        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5240        if let ClaudeOutput::System(sys) = output {
5241            assert!(sys.is_task_started());
5242            assert!(!sys.is_task_progress());
5243            assert!(!sys.is_task_notification());
5244
5245            let task = sys.as_task_started().expect("Should parse as task_started");
5246            assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
5247            assert_eq!(task.task_id, "b6daf3f");
5248            assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
5249            assert_eq!(
5250                task.tool_use_id.as_deref(),
5251                Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
5252            );
5253            assert_eq!(task.description, "Wait for CI on PR #12");
5254        } else {
5255            panic!("Expected System message");
5256        }
5257    }
5258
5259    #[test]
5260    fn test_system_message_task_started_agent() {
5261        let json = r#"{
5262            "type": "system",
5263            "subtype": "task_started",
5264            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
5265            "task_id": "a4a7e0906e5fc64cc",
5266            "task_type": "local_agent",
5267            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
5268            "description": "Explore Scene/ArrayScene duplication",
5269            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
5270        }"#;
5271
5272        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5273        if let ClaudeOutput::System(sys) = output {
5274            let task = sys.as_task_started().expect("Should parse as task_started");
5275            assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
5276            assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
5277        } else {
5278            panic!("Expected System message");
5279        }
5280    }
5281
5282    #[test]
5283    fn test_system_message_task_progress() {
5284        let json = r#"{
5285            "type": "system",
5286            "subtype": "task_progress",
5287            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
5288            "task_id": "a4a7e0906e5fc64cc",
5289            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
5290            "description": "Reading src/jplephem/chebyshev.rs",
5291            "last_tool_name": "Read",
5292            "usage": {
5293                "duration_ms": 13996,
5294                "tool_uses": 9,
5295                "total_tokens": 38779
5296            },
5297            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
5298        }"#;
5299
5300        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5301        if let ClaudeOutput::System(sys) = output {
5302            assert!(sys.is_task_progress());
5303            assert!(!sys.is_task_started());
5304
5305            let progress = sys
5306                .as_task_progress()
5307                .expect("Should parse as task_progress");
5308            assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
5309            assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
5310            assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
5311            assert_eq!(progress.usage.duration_ms, 13996);
5312            assert_eq!(progress.usage.tool_uses, 9);
5313            assert_eq!(progress.usage.total_tokens, 38779);
5314        } else {
5315            panic!("Expected System message");
5316        }
5317    }
5318
5319    #[test]
5320    fn test_system_message_task_notification_completed() {
5321        let json = r#"{
5322            "type": "system",
5323            "subtype": "task_notification",
5324            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
5325            "task_id": "a0ba761e9dc9c316f",
5326            "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
5327            "status": "completed",
5328            "summary": "Agent \"Write Hipparcos data source doc\" completed",
5329            "output_file": "",
5330            "usage": {
5331                "duration_ms": 172300,
5332                "tool_uses": 11,
5333                "total_tokens": 42005
5334            },
5335            "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
5336        }"#;
5337
5338        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5339        if let ClaudeOutput::System(sys) = output {
5340            assert!(sys.is_task_notification());
5341
5342            let notif = sys
5343                .as_task_notification()
5344                .expect("Should parse as task_notification");
5345            assert_eq!(notif.status, super::TaskStatus::Completed);
5346            assert_eq!(
5347                notif.summary,
5348                "Agent \"Write Hipparcos data source doc\" completed"
5349            );
5350            assert_eq!(notif.output_file, Some("".to_string()));
5351            assert_eq!(
5352                notif.tool_use_id,
5353                Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
5354            );
5355            let usage = notif.usage.expect("Should have usage");
5356            assert_eq!(usage.duration_ms, 172300);
5357            assert_eq!(usage.tool_uses, 11);
5358            assert_eq!(usage.total_tokens, 42005);
5359        } else {
5360            panic!("Expected System message");
5361        }
5362    }
5363
5364    #[test]
5365    fn test_system_message_task_notification_failed_no_usage() {
5366        let json = r#"{
5367            "type": "system",
5368            "subtype": "task_notification",
5369            "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
5370            "task_id": "b98f6a3",
5371            "status": "failed",
5372            "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
5373            "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
5374        }"#;
5375
5376        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5377        if let ClaudeOutput::System(sys) = output {
5378            let notif = sys
5379                .as_task_notification()
5380                .expect("Should parse as task_notification");
5381            assert_eq!(notif.status, super::TaskStatus::Failed);
5382            assert!(notif.tool_use_id.is_none());
5383            assert!(notif.usage.is_none());
5384            assert_eq!(
5385                notif.output_file,
5386                Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
5387            );
5388        } else {
5389            panic!("Expected System message");
5390        }
5391    }
5392
5393    /// Task system messages survive a `to_value` → `from_value` round-trip
5394    /// with their typed accessors still resolving. Mirrors the proxy/relay
5395    /// path where output is reparsed from a `serde_json::Value` rather than
5396    /// straight from the CLI's stdout, so a silently dropped or renamed field
5397    /// surfaces here instead of as a `None` downstream.
5398    #[test]
5399    fn test_task_messages_roundtrip_through_value() {
5400        let cases = [
5401            r#"{"type":"system","subtype":"task_started","session_id":"s1",
5402                "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
5403                "description":"Sleep 3s","uuid":"u1"}"#,
5404            r#"{"type":"system","subtype":"task_progress","session_id":"s1",
5405                "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
5406                "last_tool_name":"Bash",
5407                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
5408                "uuid":"u2"}"#,
5409            r#"{"type":"system","subtype":"task_notification","session_id":"s1",
5410                "task_id":"t1","tool_use_id":"tu1","status":"completed",
5411                "summary":"done","output_file":"",
5412                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
5413                "uuid":"u3"}"#,
5414        ];
5415
5416        for json in cases {
5417            let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5418            let value = serde_json::to_value(&output).unwrap();
5419            let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
5420
5421            let ClaudeOutput::System(sys) = reparsed else {
5422                panic!("Expected System variant after round-trip");
5423            };
5424
5425            match sys.subtype {
5426                super::SystemSubtype::TaskStarted => {
5427                    assert!(
5428                        sys.as_task_started().is_some(),
5429                        "as_task_started failed after round-trip"
5430                    );
5431                }
5432                super::SystemSubtype::TaskProgress => {
5433                    assert!(
5434                        sys.as_task_progress().is_some(),
5435                        "as_task_progress failed after round-trip"
5436                    );
5437                }
5438                super::SystemSubtype::TaskNotification => {
5439                    assert!(
5440                        sys.as_task_notification().is_some(),
5441                        "as_task_notification failed after round-trip"
5442                    );
5443                }
5444                other => panic!("unexpected subtype after round-trip: {other:?}"),
5445            }
5446        }
5447    }
5448
5449    #[test]
5450    fn test_system_message_compact_boundary() {
5451        let json = r#"{
5452            "type": "system",
5453            "subtype": "compact_boundary",
5454            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
5455            "compact_metadata": {
5456                "pre_tokens": 155285,
5457                "trigger": "auto"
5458            },
5459            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
5460        }"#;
5461
5462        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5463        if let ClaudeOutput::System(sys) = output {
5464            assert!(sys.is_compact_boundary());
5465            assert!(!sys.is_init());
5466            assert!(!sys.is_status());
5467
5468            let compact = sys
5469                .as_compact_boundary()
5470                .expect("Should parse as compact_boundary");
5471            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
5472            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
5473            assert_eq!(
5474                compact.compact_metadata.trigger,
5475                super::CompactionTrigger::Auto
5476            );
5477            // Per-compaction stats are optional and absent here.
5478            assert!(compact.summary.is_none());
5479            assert!(compact.leaf_message_count.is_none());
5480            assert!(compact.duration_ms.is_none());
5481        } else {
5482            panic!("Expected System message");
5483        }
5484    }
5485
5486    #[test]
5487    fn test_compact_boundary_with_summary_stats() {
5488        // Canonical keys.
5489        let json = r#"{
5490            "type": "system",
5491            "subtype": "compact_boundary",
5492            "session_id": "s1",
5493            "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
5494            "summary": "Summarized the earlier exploration.",
5495            "leaf_message_count": 42,
5496            "duration_ms": 1234,
5497            "uuid": "u1"
5498        }"#;
5499        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5500        let ClaudeOutput::System(sys) = output else {
5501            panic!("Expected System message");
5502        };
5503        let compact = sys.as_compact_boundary().expect("compact_boundary");
5504        assert_eq!(
5505            compact.summary.as_deref(),
5506            Some("Summarized the earlier exploration.")
5507        );
5508        assert_eq!(compact.leaf_message_count, Some(42));
5509        assert_eq!(compact.duration_ms, Some(1234));
5510
5511        // Alternate wire keys (`content` for summary, `message_count` for count)
5512        // deserialize into the same fields.
5513        let json_alt = r#"{
5514            "type": "system",
5515            "subtype": "compact_boundary",
5516            "session_id": "s2",
5517            "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
5518            "content": "alt-key summary",
5519            "message_count": 7
5520        }"#;
5521        let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
5522        let ClaudeOutput::System(sys) = output else {
5523            panic!("Expected System message");
5524        };
5525        let compact = sys.as_compact_boundary().expect("compact_boundary");
5526        assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
5527        assert_eq!(compact.leaf_message_count, Some(7));
5528    }
5529
5530    #[test]
5531    fn test_init_message_with_new_fields() {
5532        let json = r#"{
5533            "type": "system",
5534            "subtype": "init",
5535            "session_id": "test-session",
5536            "cwd": "/home/user",
5537            "model": "claude-opus-4-7",
5538            "tools": ["Bash"],
5539            "mcp_servers": [],
5540            "permissionMode": "default",
5541            "apiKeySource": "none",
5542            "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
5543            "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
5544            "fast_mode_state": "off",
5545            "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
5546            "claude_code_version": "2.1.117"
5547        }"#;
5548
5549        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5550        if let ClaudeOutput::System(sys) = output {
5551            let init = sys.as_init().expect("Should parse as init");
5552            assert_eq!(
5553                init.uuid.as_deref(),
5554                Some("44841a0d-182d-493a-86b5-79800d3d9665")
5555            );
5556            assert!(init.memory_paths.is_some());
5557            assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
5558            assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
5559            assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
5560        } else {
5561            panic!("Expected System message");
5562        }
5563    }
5564
5565    #[test]
5566    fn test_assistant_message_with_new_fields() {
5567        let json = r#"{
5568            "type": "assistant",
5569            "message": {
5570                "id": "msg_1",
5571                "type": "message",
5572                "role": "assistant",
5573                "model": "claude-opus-4-7",
5574                "content": [{"type": "text", "text": "Hello"}],
5575                "stop_reason": "end_turn",
5576                "stop_details": null,
5577                "context_management": null,
5578                "usage": {
5579                    "input_tokens": 100,
5580                    "output_tokens": 10,
5581                    "cache_creation_input_tokens": 50,
5582                    "cache_read_input_tokens": 0,
5583                    "service_tier": "standard",
5584                    "inference_geo": "not_available"
5585                }
5586            },
5587            "session_id": "abc",
5588            "uuid": "msg-uuid-123"
5589        }"#;
5590
5591        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5592        if let ClaudeOutput::Assistant(asst) = output {
5593            assert_eq!(asst.message.stop_details, None);
5594            assert_eq!(asst.message.context_management, None);
5595            let usage = asst.message.usage.unwrap();
5596            assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
5597        } else {
5598            panic!("Expected Assistant message");
5599        }
5600    }
5601
5602    #[test]
5603    fn test_user_message_with_new_fields() {
5604        let json = r#"{
5605            "type": "user",
5606            "message": {
5607                "role": "user",
5608                "content": [{"type": "text", "text": "Hello"}]
5609            },
5610            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
5611            "parent_tool_use_id": "toolu_123",
5612            "uuid": "user-msg-456"
5613        }"#;
5614
5615        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5616        if let ClaudeOutput::User(user) = output {
5617            assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
5618            assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
5619        } else {
5620            panic!("Expected User message");
5621        }
5622    }
5623
5624    /// Real wire payload captured from the CLI after answering an
5625    /// AskUserQuestion via the permission control protocol. The top-level
5626    /// `tool_use_result` and `timestamp` fields must round-trip without loss —
5627    /// proxies using this crate to relay messages to a viewer rely on those
5628    /// fields being preserved (the viewer reads `tool_use_result.answers`).
5629    #[test]
5630    fn test_user_message_preserves_tool_use_result_and_timestamp() {
5631        let json = r#"{
5632            "type":"user",
5633            "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"}]},
5634            "parent_tool_use_id":null,
5635            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
5636            "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
5637            "timestamp":"2026-05-12T23:12:04.121Z",
5638            "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"}}
5639        }"#;
5640
5641        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5642        let user = match output {
5643            ClaudeOutput::User(u) => u,
5644            other => panic!("Expected User message, got {:?}", other.message_type()),
5645        };
5646
5647        assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
5648        let raw = user
5649            .tool_use_result
5650            .as_ref()
5651            .expect("tool_use_result must be captured");
5652        assert_eq!(raw["answers"]["Color"], "Blue");
5653        assert_eq!(raw["questions"][0]["header"], "Color");
5654
5655        // Round-trip: re-serialize and confirm tool_use_result + timestamp
5656        // survive — the bug we're guarding against is that the proxy silently
5657        // drops these fields when relaying user messages.
5658        let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
5659        assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
5660        assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
5661        assert_eq!(
5662            reser["tool_use_result"]["questions"][0]["question"],
5663            "Which color do you prefer?"
5664        );
5665
5666        // Typed accessor: AskUserQuestionInput has the same shape as the
5667        // AskUserQuestion tool_use_result.
5668        let typed: crate::AskUserQuestionInput = user
5669            .tool_use_result_as::<crate::AskUserQuestionInput>()
5670            .expect("tool_use_result present")
5671            .expect("AskUserQuestionInput parses");
5672        assert_eq!(typed.questions.len(), 1);
5673        assert_eq!(typed.questions[0].header, "Color");
5674        let answers = typed.answers.expect("answers populated");
5675        assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
5676    }
5677
5678    /// User messages without `tool_use_result` / `timestamp` must still
5679    /// deserialize fine and serialize back without spuriously emitting nulls.
5680    #[test]
5681    fn test_user_message_without_tool_use_result_omits_field() {
5682        let json = r#"{
5683            "type":"user",
5684            "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
5685            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
5686        }"#;
5687
5688        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5689        let user = match output {
5690            ClaudeOutput::User(u) => u,
5691            _ => panic!("Expected User message"),
5692        };
5693        assert!(user.tool_use_result.is_none());
5694        assert!(user.timestamp.is_none());
5695
5696        let reser = serde_json::to_value(&user).unwrap();
5697        assert!(reser.get("tool_use_result").is_none());
5698        assert!(reser.get("timestamp").is_none());
5699    }
5700
5701    /// A `Task` tool result must expose subagent token / timing / tool-use
5702    /// accounting through the typed [`UserMessage::subagent_result`] accessor,
5703    /// including the nested per-model `usage` breakdown and `toolStats`.
5704    #[test]
5705    fn test_subagent_result_exposes_token_accounting() {
5706        let json = r#"{
5707            "type":"user",
5708            "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
5709            "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
5710            "tool_use_result":{
5711                "status":"completed",
5712                "prompt":"Count the .rs files.",
5713                "agentId":"ac4f0276e9d4b6232",
5714                "agentType":"Explore",
5715                "content":[{"type":"text","text":"21"}],
5716                "resolvedModel":"claude-haiku-4-5-20251001",
5717                "totalDurationMs":6869,
5718                "totalTokens":7834,
5719                "totalToolUseCount":1,
5720                "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
5721                "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
5722            }
5723        }"#;
5724
5725        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5726        let user = match output {
5727            ClaudeOutput::User(u) => u,
5728            _ => panic!("Expected User message"),
5729        };
5730
5731        let result = user.subagent_result().expect("subagent result parses");
5732        assert_eq!(result.agent_type.as_deref(), Some("Explore"));
5733        assert_eq!(
5734            result.resolved_model.as_deref(),
5735            Some("claude-haiku-4-5-20251001")
5736        );
5737        assert_eq!(result.total_tokens, Some(7834));
5738        assert_eq!(result.total_duration_ms, Some(6869));
5739        assert_eq!(result.total_tool_use_count, Some(1));
5740
5741        let usage = result.usage.expect("nested usage present");
5742        assert_eq!(usage.input_tokens, 6);
5743        assert_eq!(usage.cache_read_input_tokens, 7699);
5744
5745        let stats = result.tool_stats.expect("toolStats present");
5746        assert_eq!(stats.bash_count, 1);
5747    }
5748
5749    /// `tool_use_result` shapes that aren't subagent runs (e.g. AskUserQuestion)
5750    /// parse leniently into the all-`Option` [`SubagentResult`] with empty
5751    /// accounting rather than failing, so callers can probe without panicking.
5752    #[test]
5753    fn test_subagent_result_absent_for_non_task_result() {
5754        let json = r#"{
5755            "type":"user",
5756            "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
5757            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
5758            "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
5759        }"#;
5760
5761        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5762        let user = match output {
5763            ClaudeOutput::User(u) => u,
5764            _ => panic!("Expected User message"),
5765        };
5766
5767        let result = user.subagent_result().expect("lenient parse");
5768        assert_eq!(result.total_tokens, None);
5769        assert_eq!(result.agent_type, None);
5770    }
5771
5772    #[test]
5773    fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
5774        use serde_json::Value;
5775
5776        let raw: Value = serde_json::from_str(
5777            r#"{
5778            "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
5779            "fast_mode_state":"off",
5780            "fast_mode_disabled_reason":"not_first_party",
5781            "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
5782        }"#,
5783        )
5784        .unwrap();
5785        crate::io::assert_fully_wrapped(&raw);
5786
5787        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5788        let ClaudeOutput::System(sys) = output else {
5789            panic!("expected System");
5790        };
5791        let init = sys.as_init().expect("parses as init");
5792        assert_eq!(
5793            init.fast_mode_disabled_reason,
5794            Some(crate::FastModeDisabledReason::NotFirstParty)
5795        );
5796        let errs = init.mcp_server_errors.unwrap();
5797        assert_eq!(errs.len(), 1);
5798        assert_eq!(errs[0].name, "broken");
5799        assert_eq!(errs[0].error_type, "invalid_config");
5800    }
5801
5802    #[test]
5803    fn test_code_change_published_fully_wrapped() {
5804        use super::{KnownSystemEvent, SystemSubtype};
5805        use serde_json::Value;
5806
5807        let raw: Value = serde_json::from_str(
5808            r#"{
5809            "type":"system","subtype":"code_change_published",
5810            "provider":"github","url":"https://github.com/owner/repo/pull/42",
5811            "repo":"owner/repo","identifier":"42",
5812            "uuid":"u1","session_id":"s1"
5813        }"#,
5814        )
5815        .unwrap();
5816        crate::io::assert_fully_wrapped(&raw);
5817
5818        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5819        let ClaudeOutput::System(sys) = output else {
5820            panic!("expected System");
5821        };
5822        assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
5823        let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
5824            panic!("expected CodeChangePublished event");
5825        };
5826        assert_eq!(msg.provider, "github");
5827        assert_eq!(msg.repo, "owner/repo");
5828        assert_eq!(msg.identifier, "42");
5829
5830        assert!(sys.is_code_change_published());
5831        assert!(!sys.is_vcs_state_changed());
5832        let direct = sys.as_code_change_published().expect("direct accessor");
5833        assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
5834        assert!(sys.as_vcs_state_changed().is_none());
5835    }
5836
5837    #[test]
5838    fn test_feedback_draft_queued_fully_wrapped() {
5839        use super::{KnownSystemEvent, SystemSubtype};
5840        use serde_json::Value;
5841
5842        let raw: Value = serde_json::from_str(
5843            r#"{
5844            "type":"system","subtype":"feedback_draft_queued",
5845            "draft_id":"draft-1","draft_type":"bug_report",
5846            "title":"Tool output was truncated",
5847            "details_preview":"The last command omitted its final lines",
5848            "uuid":"u1","session_id":"s1","future_field":"preserved"
5849        }"#,
5850        )
5851        .unwrap();
5852        crate::io::assert_fully_wrapped(&raw);
5853
5854        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5855        let ClaudeOutput::System(sys) = output else {
5856            panic!("expected System");
5857        };
5858        assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
5859        assert!(sys.is_feedback_draft_queued());
5860        assert!(!sys.is_vcs_state_changed());
5861
5862        let direct = sys
5863            .as_feedback_draft_queued()
5864            .expect("direct typed accessor");
5865        assert_eq!(direct.draft_id, "draft-1");
5866        assert_eq!(direct.draft_type, "bug_report");
5867        assert_eq!(direct.extra["future_field"], "preserved");
5868
5869        let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
5870            panic!("expected FeedbackDraftQueued event");
5871        };
5872        assert_eq!(known.title, "Tool output was truncated");
5873        assert_eq!(
5874            sys.typed_value().expect("typed value")["future_field"],
5875            "preserved"
5876        );
5877    }
5878
5879    #[test]
5880    fn test_cloud_session_delta_fully_wrapped() {
5881        use super::{KnownSystemEvent, SystemSubtype};
5882        use serde_json::Value;
5883
5884        let raw: Value = serde_json::from_str(
5885            r#"{
5886            "type":"system","subtype":"cloud_session_delta",
5887            "seq":3,"changed":["serving","connection"],
5888            "cloud_session":{"id":"session_abc","view_url":"https://example.invalid/s/abc",
5889                "serving":{"state":"on"},"connection":{"state":"live"}},
5890            "uuid":"u1","session_id":"s1","future_field":"preserved"
5891        }"#,
5892        )
5893        .unwrap();
5894        crate::io::assert_fully_wrapped(&raw);
5895
5896        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5897        let ClaudeOutput::System(sys) = output else {
5898            panic!("expected System");
5899        };
5900        assert_eq!(sys.subtype, SystemSubtype::CloudSessionDelta);
5901        assert!(sys.is_cloud_session_delta());
5902        assert!(!sys.is_feedback_draft_queued());
5903
5904        let direct = sys.as_cloud_session_delta().expect("direct typed accessor");
5905        assert_eq!(direct.seq, 3);
5906        assert_eq!(direct.changed, vec!["serving", "connection"]);
5907        assert_eq!(direct.cloud_session["id"], "session_abc");
5908        assert_eq!(direct.extra["future_field"], "preserved");
5909
5910        let Some(KnownSystemEvent::CloudSessionDelta(known)) = sys.as_known_system_event() else {
5911            panic!("expected CloudSessionDelta event");
5912        };
5913        assert_eq!(known.session_id, "s1");
5914        assert_eq!(
5915            sys.typed_value().expect("typed value")["future_field"],
5916            "preserved"
5917        );
5918    }
5919
5920    #[test]
5921    fn test_vcs_state_changed_fully_wrapped() {
5922        use super::{KnownSystemEvent, VcsMutationKind};
5923        use serde_json::Value;
5924
5925        for kind in ["commit", "push", "merge", "rebase"] {
5926            let raw: Value = serde_json::from_str(&format!(
5927                r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
5928                kind
5929            ))
5930            .unwrap();
5931            crate::io::assert_fully_wrapped(&raw);
5932
5933            let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5934            let ClaudeOutput::System(sys) = output else {
5935                panic!("expected System");
5936            };
5937            let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
5938                panic!("expected VcsStateChanged event");
5939            };
5940            assert_eq!(msg.kind.as_str(), kind);
5941            assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
5942        }
5943
5944        // Unknown kinds are valid per the wire contract.
5945        let raw: Value = serde_json::from_str(
5946            r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
5947        )
5948        .unwrap();
5949        crate::io::assert_fully_wrapped(&raw);
5950        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
5951        let ClaudeOutput::System(sys) = output else {
5952            panic!("expected System");
5953        };
5954        let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
5955            panic!("expected VcsStateChanged event");
5956        };
5957        assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
5958
5959        assert!(sys.is_vcs_state_changed());
5960        let direct = sys.as_vcs_state_changed().expect("direct accessor");
5961        assert_eq!(direct.cwd, "/repo");
5962        assert!(sys.as_code_change_published().is_none());
5963    }
5964
5965    #[test]
5966    fn test_assistant_aborted_and_resume_flags_roundtrip() {
5967        let json = r#"{
5968            "type":"assistant",
5969            "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
5970            "session_id":"s1",
5971            "aborted":true,
5972            "resumed_from_incomplete_thinking":true
5973        }"#;
5974        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5975        let ClaudeOutput::Assistant(msg) = &output else {
5976            panic!("expected Assistant");
5977        };
5978        assert_eq!(msg.aborted, Some(true));
5979        assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
5980        let reserialized = serde_json::to_string(&output).unwrap();
5981        assert!(reserialized.contains("\"aborted\":true"));
5982        assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
5983
5984        // Absent flags stay absent on the wire.
5985        let json = r#"{
5986            "type":"assistant",
5987            "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
5988            "session_id":"s2"
5989        }"#;
5990        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
5991        let reserialized = serde_json::to_string(&output).unwrap();
5992        assert!(!reserialized.contains("aborted"));
5993        assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
5994    }
5995
5996    #[test]
5997    fn test_user_tool_result_meta_roundtrip() {
5998        let json = r#"{
5999            "type":"user",
6000            "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
6001            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
6002            "tool_result_meta":[
6003                {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
6004                {"id":"toolu_2","non_execution_kind":"permission-rule"}
6005            ]
6006        }"#;
6007        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
6008        let ClaudeOutput::User(user) = &output else {
6009            panic!("expected User");
6010        };
6011        let meta = user.tool_result_meta.as_ref().unwrap();
6012        assert_eq!(meta.len(), 2);
6013        assert_eq!(meta[0].non_execution_kind.as_deref(), Some("user-rejected"));
6014        assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
6015        assert_eq!(meta[1].user_feedback, None);
6016
6017        let reserialized = serde_json::to_string(&output).unwrap();
6018        assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
6019        assert!(!reserialized.contains("\"user_feedback\":null"));
6020    }
6021
6022    /// CLI 2.1.222 added `scope` to `system/model_refusal_fallback`:
6023    /// "session" (main-thread swap, also the meaning when absent on older
6024    /// CLIs) vs "local" (subagent/side-question fallback only).
6025    #[test]
6026    fn model_refusal_fallback_scope_roundtrips_and_defaults() {
6027        use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
6028        let with_scope = serde_json::json!({
6029            "trigger": "refusal",
6030            "direction": "retry",
6031            "scope": "local",
6032            "original_model": "claude-fable-5",
6033            "fallback_model": "claude-opus-5",
6034            "request_id": null,
6035            "content": "Refused; retried on fallback model.",
6036            "uuid": "u1",
6037            "session_id": "s1"
6038        });
6039        let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
6040        assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
6041        assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
6042
6043        // Older CLIs omit scope — absent, not null, and treated as session
6044        // by consumers per the wire docs.
6045        let mut without = with_scope.clone();
6046        without.as_object_mut().unwrap().remove("scope");
6047        let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
6048        assert_eq!(msg.scope, None);
6049        assert_eq!(serde_json::to_value(&msg).unwrap(), without);
6050
6051        // Open enum: unknown scopes pass through verbatim.
6052        assert_eq!(
6053            RefusalFallbackScope::from("workspace").as_str(),
6054            "workspace"
6055        );
6056    }
6057}