Skip to main content

claude_codes/io/
message_types.rs

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