Skip to main content

claude_codes/io/
message_types.rs

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