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