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