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