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