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