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