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