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