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