Skip to main content

claude_codes/io/
message_types.rs

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