Skip to main content

claude_codes/io/
message_types.rs

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