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    /// A subtype not yet known to this version of the crate.
48    Unknown(String),
49}
50
51impl SystemSubtype {
52    pub fn as_str(&self) -> &str {
53        match self {
54            Self::Init => "init",
55            Self::Status => "status",
56            Self::CompactBoundary => "compact_boundary",
57            Self::ThinkingTokens => "thinking_tokens",
58            Self::TaskStarted => "task_started",
59            Self::TaskProgress => "task_progress",
60            Self::TaskUpdated => "task_updated",
61            Self::TaskNotification => "task_notification",
62            Self::ApiRetry => "api_retry",
63            Self::ControlRequestProgress => "control_request_progress",
64            Self::ModelRefusalFallback => "model_refusal_fallback",
65            Self::ModelRefusalNoFallback => "model_refusal_no_fallback",
66            Self::LocalCommandOutput => "local_command_output",
67            Self::HookStarted => "hook_started",
68            Self::HookProgress => "hook_progress",
69            Self::HookResponse => "hook_response",
70            Self::PluginInstall => "plugin_install",
71            Self::BackgroundTasksChanged => "background_tasks_changed",
72            Self::SessionStateChanged => "session_state_changed",
73            Self::WorkerShuttingDown => "worker_shutting_down",
74            Self::CommandsChanged => "commands_changed",
75            Self::Notification => "notification",
76            Self::FilesPersisted => "files_persisted",
77            Self::MemoryRecall => "memory_recall",
78            Self::ElicitationComplete => "elicitation_complete",
79            Self::PermissionDenied => "permission_denied",
80            Self::MirrorError => "mirror_error",
81            Self::Informational => "informational",
82            Self::CodeChangePublished => "code_change_published",
83            Self::VcsStateChanged => "vcs_state_changed",
84            Self::FeedbackDraftQueued => "feedback_draft_queued",
85            Self::Unknown(s) => s.as_str(),
86        }
87    }
88}
89
90impl fmt::Display for SystemSubtype {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.write_str(self.as_str())
93    }
94}
95
96impl From<&str> for SystemSubtype {
97    fn from(s: &str) -> Self {
98        match s {
99            "init" => Self::Init,
100            "status" => Self::Status,
101            "compact_boundary" => Self::CompactBoundary,
102            "thinking_tokens" => Self::ThinkingTokens,
103            "task_started" => Self::TaskStarted,
104            "task_progress" => Self::TaskProgress,
105            "task_updated" => Self::TaskUpdated,
106            "task_notification" => Self::TaskNotification,
107            "api_retry" => Self::ApiRetry,
108            "control_request_progress" => Self::ControlRequestProgress,
109            "model_refusal_fallback" => Self::ModelRefusalFallback,
110            "model_refusal_no_fallback" => Self::ModelRefusalNoFallback,
111            "local_command_output" => Self::LocalCommandOutput,
112            "hook_started" => Self::HookStarted,
113            "hook_progress" => Self::HookProgress,
114            "hook_response" => Self::HookResponse,
115            "plugin_install" => Self::PluginInstall,
116            "background_tasks_changed" => Self::BackgroundTasksChanged,
117            "session_state_changed" => Self::SessionStateChanged,
118            "worker_shutting_down" => Self::WorkerShuttingDown,
119            "commands_changed" => Self::CommandsChanged,
120            "notification" => Self::Notification,
121            "files_persisted" => Self::FilesPersisted,
122            "memory_recall" => Self::MemoryRecall,
123            "elicitation_complete" => Self::ElicitationComplete,
124            "permission_denied" => Self::PermissionDenied,
125            "mirror_error" => Self::MirrorError,
126            "informational" => Self::Informational,
127            "code_change_published" => Self::CodeChangePublished,
128            "vcs_state_changed" => Self::VcsStateChanged,
129            "feedback_draft_queued" => Self::FeedbackDraftQueued,
130            other => Self::Unknown(other.to_string()),
131        }
132    }
133}
134
135impl Serialize for SystemSubtype {
136    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
137        serializer.serialize_str(self.as_str())
138    }
139}
140
141impl<'de> Deserialize<'de> for SystemSubtype {
142    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
143        let s = String::deserialize(deserializer)?;
144        Ok(Self::from(s.as_str()))
145    }
146}
147
148/// Known message roles.
149///
150/// Used in `MessageContent` and `AssistantMessageContent` to indicate the
151/// speaker of a message.
152#[derive(Debug, Clone, PartialEq, Eq, Hash)]
153pub enum MessageRole {
154    User,
155    Assistant,
156    /// A role not yet known to this version of the crate.
157    Unknown(String),
158}
159
160impl MessageRole {
161    pub fn as_str(&self) -> &str {
162        match self {
163            Self::User => "user",
164            Self::Assistant => "assistant",
165            Self::Unknown(s) => s.as_str(),
166        }
167    }
168}
169
170impl fmt::Display for MessageRole {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        f.write_str(self.as_str())
173    }
174}
175
176impl From<&str> for MessageRole {
177    fn from(s: &str) -> Self {
178        match s {
179            "user" => Self::User,
180            "assistant" => Self::Assistant,
181            other => Self::Unknown(other.to_string()),
182        }
183    }
184}
185
186impl Serialize for MessageRole {
187    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
188        serializer.serialize_str(self.as_str())
189    }
190}
191
192impl<'de> Deserialize<'de> for MessageRole {
193    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
194        let s = String::deserialize(deserializer)?;
195        Ok(Self::from(s.as_str()))
196    }
197}
198
199/// What triggered a context compaction.
200#[derive(Debug, Clone, PartialEq, Eq, Hash)]
201pub enum CompactionTrigger {
202    /// Automatic compaction triggered by token limit.
203    Auto,
204    /// User-initiated compaction (e.g., /compact command).
205    Manual,
206    /// A trigger not yet known to this version of the crate.
207    Unknown(String),
208}
209
210impl CompactionTrigger {
211    pub fn as_str(&self) -> &str {
212        match self {
213            Self::Auto => "auto",
214            Self::Manual => "manual",
215            Self::Unknown(s) => s.as_str(),
216        }
217    }
218}
219
220impl fmt::Display for CompactionTrigger {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        f.write_str(self.as_str())
223    }
224}
225
226impl From<&str> for CompactionTrigger {
227    fn from(s: &str) -> Self {
228        match s {
229            "auto" => Self::Auto,
230            "manual" => Self::Manual,
231            other => Self::Unknown(other.to_string()),
232        }
233    }
234}
235
236impl Serialize for CompactionTrigger {
237    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
238        serializer.serialize_str(self.as_str())
239    }
240}
241
242impl<'de> Deserialize<'de> for CompactionTrigger {
243    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
244        let s = String::deserialize(deserializer)?;
245        Ok(Self::from(s.as_str()))
246    }
247}
248
249/// Reason why the assistant stopped generating.
250#[derive(Debug, Clone, PartialEq, Eq, Hash)]
251pub enum StopReason {
252    /// The assistant reached a natural end of its turn.
253    EndTurn,
254    /// The response hit the maximum token limit.
255    MaxTokens,
256    /// The assistant wants to use a tool.
257    ToolUse,
258    /// A stop reason not yet known to this version of the crate.
259    Unknown(String),
260}
261
262impl StopReason {
263    pub fn as_str(&self) -> &str {
264        match self {
265            Self::EndTurn => "end_turn",
266            Self::MaxTokens => "max_tokens",
267            Self::ToolUse => "tool_use",
268            Self::Unknown(s) => s.as_str(),
269        }
270    }
271}
272
273impl fmt::Display for StopReason {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        f.write_str(self.as_str())
276    }
277}
278
279impl From<&str> for StopReason {
280    fn from(s: &str) -> Self {
281        match s {
282            "end_turn" => Self::EndTurn,
283            "max_tokens" => Self::MaxTokens,
284            "tool_use" => Self::ToolUse,
285            other => Self::Unknown(other.to_string()),
286        }
287    }
288}
289
290impl Serialize for StopReason {
291    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
292        serializer.serialize_str(self.as_str())
293    }
294}
295
296impl<'de> Deserialize<'de> for StopReason {
297    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
298        let s = String::deserialize(deserializer)?;
299        Ok(Self::from(s.as_str()))
300    }
301}
302
303/// How the API key was sourced for the session.
304#[derive(Debug, Clone, PartialEq, Eq, Hash)]
305pub enum ApiKeySource {
306    /// No API key provided.
307    None,
308    User,
309    Project,
310    Org,
311    Temporary,
312    Oauth,
313    /// A source not yet known to this version of the crate.
314    Unknown(String),
315}
316
317impl ApiKeySource {
318    pub fn as_str(&self) -> &str {
319        match self {
320            Self::None => "none",
321            Self::User => "user",
322            Self::Project => "project",
323            Self::Org => "org",
324            Self::Temporary => "temporary",
325            Self::Oauth => "oauth",
326            Self::Unknown(s) => s.as_str(),
327        }
328    }
329}
330
331impl fmt::Display for ApiKeySource {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        f.write_str(self.as_str())
334    }
335}
336
337impl From<&str> for ApiKeySource {
338    fn from(s: &str) -> Self {
339        match s {
340            "none" => Self::None,
341            "user" => Self::User,
342            "project" => Self::Project,
343            "org" => Self::Org,
344            "temporary" => Self::Temporary,
345            "oauth" => Self::Oauth,
346            other => Self::Unknown(other.to_string()),
347        }
348    }
349}
350
351impl Serialize for ApiKeySource {
352    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
353        serializer.serialize_str(self.as_str())
354    }
355}
356
357impl<'de> Deserialize<'de> for ApiKeySource {
358    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
359        let s = String::deserialize(deserializer)?;
360        Ok(Self::from(s.as_str()))
361    }
362}
363
364/// Output formatting style for the session.
365#[derive(Debug, Clone, PartialEq, Eq, Hash)]
366pub enum OutputStyle {
367    /// Default output style.
368    Default,
369    /// A style not yet known to this version of the crate.
370    Unknown(String),
371}
372
373impl OutputStyle {
374    pub fn as_str(&self) -> &str {
375        match self {
376            Self::Default => "default",
377            Self::Unknown(s) => s.as_str(),
378        }
379    }
380}
381
382impl fmt::Display for OutputStyle {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        f.write_str(self.as_str())
385    }
386}
387
388impl From<&str> for OutputStyle {
389    fn from(s: &str) -> Self {
390        match s {
391            "default" => Self::Default,
392            other => Self::Unknown(other.to_string()),
393        }
394    }
395}
396
397impl Serialize for OutputStyle {
398    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
399        serializer.serialize_str(self.as_str())
400    }
401}
402
403impl<'de> Deserialize<'de> for OutputStyle {
404    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
405        let s = String::deserialize(deserializer)?;
406        Ok(Self::from(s.as_str()))
407    }
408}
409
410/// Permission mode reported in init messages.
411#[derive(Debug, Clone, PartialEq, Eq, Hash)]
412pub enum InitPermissionMode {
413    /// Default permission mode.
414    Default,
415    AcceptEdits,
416    BypassPermissions,
417    Plan,
418    DontAsk,
419    Auto,
420    /// A mode not yet known to this version of the crate.
421    Unknown(String),
422}
423
424impl InitPermissionMode {
425    pub fn as_str(&self) -> &str {
426        match self {
427            Self::Default => "default",
428            Self::AcceptEdits => "acceptEdits",
429            Self::BypassPermissions => "bypassPermissions",
430            Self::Plan => "plan",
431            Self::DontAsk => "dontAsk",
432            Self::Auto => "auto",
433            Self::Unknown(s) => s.as_str(),
434        }
435    }
436}
437
438impl fmt::Display for InitPermissionMode {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        f.write_str(self.as_str())
441    }
442}
443
444impl From<&str> for InitPermissionMode {
445    fn from(s: &str) -> Self {
446        match s {
447            "default" => Self::Default,
448            "acceptEdits" => Self::AcceptEdits,
449            "bypassPermissions" => Self::BypassPermissions,
450            "plan" => Self::Plan,
451            "dontAsk" => Self::DontAsk,
452            "auto" => Self::Auto,
453            other => Self::Unknown(other.to_string()),
454        }
455    }
456}
457
458impl Serialize for InitPermissionMode {
459    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
460        serializer.serialize_str(self.as_str())
461    }
462}
463
464impl<'de> Deserialize<'de> for InitPermissionMode {
465    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
466        let s = String::deserialize(deserializer)?;
467        Ok(Self::from(s.as_str()))
468    }
469}
470
471/// Status of an ongoing operation (e.g., context compaction).
472#[derive(Debug, Clone, PartialEq, Eq, Hash)]
473pub enum StatusMessageStatus {
474    /// Context compaction is in progress.
475    Compacting,
476    /// The CLI is issuing a request.
477    Requesting,
478    /// A status not yet known to this version of the crate.
479    Unknown(String),
480}
481
482impl StatusMessageStatus {
483    pub fn as_str(&self) -> &str {
484        match self {
485            Self::Compacting => "compacting",
486            Self::Requesting => "requesting",
487            Self::Unknown(s) => s.as_str(),
488        }
489    }
490}
491
492impl fmt::Display for StatusMessageStatus {
493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494        f.write_str(self.as_str())
495    }
496}
497
498impl From<&str> for StatusMessageStatus {
499    fn from(s: &str) -> Self {
500        match s {
501            "compacting" => Self::Compacting,
502            "requesting" => Self::Requesting,
503            other => Self::Unknown(other.to_string()),
504        }
505    }
506}
507
508impl Serialize for StatusMessageStatus {
509    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
510        serializer.serialize_str(self.as_str())
511    }
512}
513
514impl<'de> Deserialize<'de> for StatusMessageStatus {
515    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
516        let s = String::deserialize(deserializer)?;
517        Ok(Self::from(s.as_str()))
518    }
519}
520
521/// Serialize an optional UUID as a string
522pub(crate) fn serialize_optional_uuid<S>(
523    uuid: &Option<Uuid>,
524    serializer: S,
525) -> Result<S::Ok, S::Error>
526where
527    S: Serializer,
528{
529    match uuid {
530        Some(id) => serializer.serialize_str(&id.to_string()),
531        None => serializer.serialize_none(),
532    }
533}
534
535/// Deserialize an optional UUID from a string
536pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
537where
538    D: Deserializer<'de>,
539{
540    let opt_str: Option<String> = Option::deserialize(deserializer)?;
541    match opt_str {
542        Some(s) => Uuid::parse_str(&s)
543            .map(Some)
544            .map_err(serde::de::Error::custom),
545        None => Ok(None),
546    }
547}
548
549/// Message provenance. The `kind` field is the stable discriminator; variant
550/// specific fields are preserved in `extra` for forward-compatible access.
551#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
552pub struct MessageOrigin {
553    pub kind: String,
554    #[serde(flatten)]
555    pub extra: serde_json::Map<String, Value>,
556}
557
558/// Metadata attached when user-visible transcript content summarizes prior messages.
559#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
560pub struct SummarizeMetadata {
561    pub messages_summarized: u64,
562    #[serde(default, skip_serializing_if = "Option::is_none")]
563    pub user_context: Option<String>,
564    #[serde(default, skip_serializing_if = "Option::is_none")]
565    pub direction: Option<String>,
566}
567
568/// MCP metadata passed through on user-message wrappers.
569#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
570pub struct McpMeta {
571    #[serde(default, skip_serializing_if = "Option::is_none", rename = "_meta")]
572    pub meta: Option<Value>,
573    #[serde(default, skip_serializing_if = "Option::is_none")]
574    pub structured_content: Option<Value>,
575}
576
577/// Display metadata for a `tool_result` block carried on the user wrapper.
578#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
579pub struct ToolResultMeta {
580    /// The `tool_use_id` of the matching `tool_result` block.
581    pub id: String,
582    /// Harness-stamped reason an `is_error: true` result did not carry the
583    /// tool's own execution output (`user-rejected`, `permission-rule`,
584    /// `automode-*`, `interrupted`, `cancelled`). Open set — treat
585    /// unrecognized values as valid reasons; absent means the tool ran to
586    /// completion.
587    pub non_execution_kind: String,
588    /// The deny comment a human typed at a permission prompt, when present.
589    #[serde(default, skip_serializing_if = "Option::is_none")]
590    pub user_feedback: Option<String>,
591}
592
593/// User message
594#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct UserMessage {
596    pub message: MessageContent,
597    #[serde(skip_serializing_if = "Option::is_none", alias = "sessionId")]
598    #[serde(
599        serialize_with = "serialize_optional_uuid",
600        deserialize_with = "deserialize_optional_uuid"
601    )]
602    pub session_id: Option<Uuid>,
603    /// Parent tool use ID for nested agent messages
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub parent_tool_use_id: Option<String>,
606    /// Message-level unique identifier
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub uuid: Option<String>,
609    /// CLI-emitted ISO-8601 timestamp for the message (present on echoed tool results).
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub timestamp: Option<String>,
612    /// Structured tool result data echoed by the CLI alongside the `tool_result`
613    /// content block. The shape depends on which tool produced it (e.g. for
614    /// `AskUserQuestion` it is `{ questions, answers }`; for `Bash` it is
615    /// `{ stdout, stderr, exit_code, ... }`). Stored as raw JSON to preserve
616    /// wire fidelity; use [`UserMessage::tool_use_result_as`] to parse into a
617    /// typed shape when you know which tool was invoked.
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub tool_use_result: Option<serde_json::Value>,
620    /// Subagent type, when this user message is the prompt echoed into a
621    /// `local_agent` subagent (e.g. `general-purpose`).
622    #[serde(skip_serializing_if = "Option::is_none")]
623    pub subagent_type: Option<String>,
624    /// Short description of the subagent task, present alongside `subagent_type`.
625    #[serde(skip_serializing_if = "Option::is_none")]
626    pub task_description: Option<String>,
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub origin: Option<MessageOrigin>,
629    #[serde(skip_serializing_if = "Option::is_none")]
630    pub priority: Option<String>,
631    #[serde(skip_serializing_if = "Option::is_none", rename = "isSynthetic")]
632    pub is_synthetic: Option<bool>,
633    #[serde(skip_serializing_if = "Option::is_none", rename = "shouldQuery")]
634    pub should_query: Option<bool>,
635    #[serde(default, skip_serializing_if = "Option::is_none")]
636    pub is_meta: Option<bool>,
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub is_visible_in_transcript_only: Option<bool>,
639    #[serde(default, skip_serializing_if = "Option::is_none")]
640    pub is_virtual: Option<bool>,
641    #[serde(default, skip_serializing_if = "Option::is_none")]
642    pub is_compact_summary: Option<bool>,
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub summarize_metadata: Option<SummarizeMetadata>,
645    #[serde(skip_serializing_if = "Option::is_none")]
646    pub mcp_meta: Option<McpMeta>,
647    /// Display metadata for this message's `tool_result` blocks, keyed by
648    /// `tool_use_id`.
649    #[serde(skip_serializing_if = "Option::is_none")]
650    pub tool_result_meta: Option<Vec<ToolResultMeta>>,
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub source_tool_use_id: Option<String>,
653    #[serde(skip_serializing_if = "Option::is_none")]
654    pub source_tool_assistant_uuid: Option<String>,
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub image_paste_ids: Option<Vec<u64>>,
657    #[serde(skip_serializing_if = "Option::is_none")]
658    pub client_platform: Option<String>,
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub inbound_origin: Option<String>,
661    #[serde(skip_serializing_if = "Option::is_none", rename = "isReplay")]
662    pub is_replay: Option<bool>,
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub file_attachments: Option<Vec<Value>>,
665}
666
667impl UserMessage {
668    /// Parse the `tool_use_result` field into a caller-specified type.
669    ///
670    /// Returns `None` if `tool_use_result` is absent, otherwise returns the
671    /// deserialization result. The caller must know which tool produced the
672    /// result and supply a matching type — e.g. for `AskUserQuestion` use
673    /// [`AskUserQuestionInput`](crate::AskUserQuestionInput), whose
674    /// `questions` + `answers` fields match the wire result shape.
675    pub fn tool_use_result_as<T: serde::de::DeserializeOwned>(
676        &self,
677    ) -> Option<Result<T, serde_json::Error>> {
678        self.tool_use_result
679            .as_ref()
680            .map(|v| serde_json::from_value(v.clone()))
681    }
682
683    /// Parse the `tool_use_result` as a subagent (`Task`) run result.
684    ///
685    /// When this user message echoes the result of a `Task` tool call, the CLI
686    /// attaches a structured `tool_use_result` carrying the subagent's token,
687    /// timing, and tool-use accounting. Returns `None` when the field is absent
688    /// or does not parse as a [`SubagentResult`].
689    ///
690    /// Summing [`SubagentResult::total_tokens`] across every `Task` result in a
691    /// session yields the subagent token rollup the CLI renders as
692    /// `subagent_tokens` in its terminal `<usage>` block.
693    pub fn subagent_result(&self) -> Option<SubagentResult> {
694        self.tool_use_result
695            .as_ref()
696            .and_then(|v| serde_json::from_value(v.clone()).ok())
697    }
698}
699
700/// Token, timing, and tool-use accounting for a completed subagent (`Task`) run.
701///
702/// The Claude CLI echoes this object in the `tool_use_result` of a `Task` tool's
703/// result message. It is the typed source of truth for subagent token
704/// attribution: the per-run [`total_tokens`](Self::total_tokens),
705/// [`total_duration_ms`](Self::total_duration_ms), and
706/// [`total_tool_use_count`](Self::total_tool_use_count) correspond to the
707/// `subagent_tokens` / `duration_ms` / `tool_uses` line items the CLI renders in
708/// its human-readable `<usage>` block, and [`usage`](Self::usage) carries the
709/// full per-model token breakdown for the run.
710#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct SubagentResult {
712    /// Completion status of the subagent run (e.g. `"completed"`).
713    #[serde(skip_serializing_if = "Option::is_none")]
714    pub status: Option<String>,
715    /// The prompt the subagent was launched with.
716    #[serde(skip_serializing_if = "Option::is_none")]
717    pub prompt: Option<String>,
718    /// Stable identifier of the spawned subagent.
719    #[serde(rename = "agentId", skip_serializing_if = "Option::is_none")]
720    pub agent_id: Option<String>,
721    /// Subagent type that ran (e.g. `general-purpose`, `Explore`).
722    #[serde(rename = "agentType", skip_serializing_if = "Option::is_none")]
723    pub agent_type: Option<String>,
724    /// Final content blocks the subagent returned.
725    #[serde(
726        default,
727        deserialize_with = "deserialize_content_blocks",
728        skip_serializing_if = "Vec::is_empty"
729    )]
730    pub content: Vec<ContentBlock>,
731    /// Model the subagent actually resolved to (e.g. `claude-sonnet-4-6`).
732    #[serde(rename = "resolvedModel", skip_serializing_if = "Option::is_none")]
733    pub resolved_model: Option<String>,
734    /// Wall-clock duration of the subagent run, in milliseconds.
735    #[serde(rename = "totalDurationMs", skip_serializing_if = "Option::is_none")]
736    pub total_duration_ms: Option<u64>,
737    /// Total tokens consumed by the subagent — the `subagent_tokens` rollup line.
738    #[serde(rename = "totalTokens", skip_serializing_if = "Option::is_none")]
739    pub total_tokens: Option<u64>,
740    /// Number of tool invocations the subagent made.
741    #[serde(rename = "totalToolUseCount", skip_serializing_if = "Option::is_none")]
742    pub total_tool_use_count: Option<u64>,
743    /// Detailed token / cache usage for the subagent run.
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub usage: Option<super::result::UsageInfo>,
746    /// Per-category tool-use counts, present for some agent types (e.g. `Explore`).
747    #[serde(rename = "toolStats", skip_serializing_if = "Option::is_none")]
748    pub tool_stats: Option<SubagentToolStats>,
749}
750
751/// Per-category tool-use counts for a subagent run, from `tool_use_result.toolStats`.
752///
753/// The `extra` field captures any counters the CLI adds that aren't modeled here,
754/// so new wire fields deserialize without error.
755#[derive(Debug, Clone, Default, Serialize, Deserialize)]
756#[serde(rename_all = "camelCase")]
757pub struct SubagentToolStats {
758    #[serde(default)]
759    pub read_count: u64,
760    #[serde(default)]
761    pub search_count: u64,
762    #[serde(default)]
763    pub bash_count: u64,
764    #[serde(default)]
765    pub edit_file_count: u64,
766    #[serde(default)]
767    pub lines_added: u64,
768    #[serde(default)]
769    pub lines_removed: u64,
770    #[serde(default)]
771    pub other_tool_count: u64,
772    #[serde(flatten)]
773    pub extra: serde_json::Map<String, Value>,
774}
775
776/// Session-level subagent token rollup — the `<subagent_tokens>` /
777/// `<agent_count>` line items the Claude CLI renders in its terminal
778/// `<usage>` block.
779///
780/// The `stream-json` protocol does **not** carry this rollup on the `result`
781/// frame's `usage` (confirmed against the CLI binary — the terminal renderer
782/// computes it from `Task` tool results). Consumers that need it must
783/// accumulate it the same way: feed every session message through
784/// [`observe`](Self::observe) and read the totals at any point.
785///
786/// A `Task` result observed twice under the same `agentId` (e.g. a replayed
787/// frame on resume) is counted once. Results with no `agentId` are counted
788/// every time they are observed.
789///
790/// # Example
791///
792/// ```
793/// use claude_codes::{ClaudeOutput, SubagentUsageRollup};
794///
795/// let mut rollup = SubagentUsageRollup::default();
796/// 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}}"#;
797/// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
798/// rollup.observe(&output);
799/// assert_eq!(rollup.subagent_tokens, 10201);
800/// assert_eq!(rollup.agent_count, 1);
801/// ```
802#[derive(Debug, Clone, Default, PartialEq, Eq)]
803pub struct SubagentUsageRollup {
804    /// Total tokens consumed by subagents — sum of
805    /// [`SubagentResult::total_tokens`] over every observed `Task` result.
806    pub subagent_tokens: u64,
807    /// Number of subagent runs observed (`<agent_count>`).
808    pub agent_count: u32,
809    /// Total subagent tool invocations — sum of `total_tool_use_count`.
810    pub tool_uses: u64,
811    /// Total subagent wall-clock milliseconds — sum of `total_duration_ms`.
812    pub duration_ms: u64,
813    seen_agent_ids: std::collections::BTreeSet<String>,
814}
815
816impl SubagentUsageRollup {
817    /// Accumulate `output` into the rollup if it is a `Task` tool result.
818    ///
819    /// Returns `true` when the message contributed to the totals. Non-user
820    /// messages, user messages without a `tool_use_result`, results from
821    /// other tools, and duplicate `agentId`s are all ignored.
822    pub fn observe(&mut self, output: &ClaudeOutput) -> bool {
823        match output {
824            ClaudeOutput::User(user) => self.observe_user(user),
825            _ => false,
826        }
827    }
828
829    /// Accumulate a user message's `Task` tool result, if it carries one.
830    ///
831    /// Every [`SubagentResult`] field is optional, so any JSON object in
832    /// `tool_use_result` parses as one (e.g. a `Bash` or `ToolSearch`
833    /// result). Only results carrying an `agentId` or a `totalTokens`
834    /// line item are treated as genuine `Task` results.
835    pub fn observe_user(&mut self, user: &UserMessage) -> bool {
836        let Some(result) = user.subagent_result() else {
837            return false;
838        };
839        if result.total_tokens.is_none() && result.agent_id.is_none() {
840            return false;
841        }
842        if let Some(agent_id) = &result.agent_id {
843            if !self.seen_agent_ids.insert(agent_id.clone()) {
844                return false;
845            }
846        }
847        self.agent_count += 1;
848        self.subagent_tokens += result.total_tokens.unwrap_or(0);
849        self.tool_uses += result.total_tool_use_count.unwrap_or(0);
850        self.duration_ms += result.total_duration_ms.unwrap_or(0);
851        true
852    }
853}
854
855/// Message content with role
856#[derive(Debug, Clone, Serialize, Deserialize)]
857pub struct MessageContent {
858    pub role: MessageRole,
859    #[serde(deserialize_with = "deserialize_content_blocks")]
860    pub content: Vec<ContentBlock>,
861}
862
863/// System message with metadata
864#[derive(Debug, Clone, Serialize, Deserialize)]
865pub struct SystemMessage {
866    pub subtype: SystemSubtype,
867    #[serde(flatten)]
868    pub data: Value, // Captures all other fields
869}
870
871impl SystemMessage {
872    /// Check if this is an init message
873    pub fn is_init(&self) -> bool {
874        self.subtype == SystemSubtype::Init
875    }
876
877    /// Check if this is a status message
878    pub fn is_status(&self) -> bool {
879        self.subtype == SystemSubtype::Status
880    }
881
882    /// Check if this is a compact_boundary message
883    pub fn is_compact_boundary(&self) -> bool {
884        self.subtype == SystemSubtype::CompactBoundary
885    }
886
887    /// Try to parse as an init message
888    pub fn as_init(&self) -> Option<InitMessage> {
889        if self.subtype != SystemSubtype::Init {
890            return None;
891        }
892        serde_json::from_value(self.data.clone()).ok()
893    }
894
895    /// Try to parse as a status message
896    pub fn as_status(&self) -> Option<StatusMessage> {
897        if self.subtype != SystemSubtype::Status {
898            return None;
899        }
900        serde_json::from_value(self.data.clone()).ok()
901    }
902
903    /// Try to parse as a compact_boundary message
904    pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
905        if self.subtype != SystemSubtype::CompactBoundary {
906            return None;
907        }
908        serde_json::from_value(self.data.clone()).ok()
909    }
910
911    /// Check if this is a task_started message
912    pub fn is_task_started(&self) -> bool {
913        self.subtype == SystemSubtype::TaskStarted
914    }
915
916    /// Check if this is a task_progress message
917    pub fn is_task_progress(&self) -> bool {
918        self.subtype == SystemSubtype::TaskProgress
919    }
920
921    /// Check if this is a task_notification message
922    pub fn is_task_notification(&self) -> bool {
923        self.subtype == SystemSubtype::TaskNotification
924    }
925
926    /// Try to parse as a task_started message
927    pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
928        if self.subtype != SystemSubtype::TaskStarted {
929            return None;
930        }
931        serde_json::from_value(self.data.clone()).ok()
932    }
933
934    /// Try to parse as a task_progress message
935    pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
936        if self.subtype != SystemSubtype::TaskProgress {
937            return None;
938        }
939        serde_json::from_value(self.data.clone()).ok()
940    }
941
942    /// Try to parse as a task_notification message
943    pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
944        if self.subtype != SystemSubtype::TaskNotification {
945            return None;
946        }
947        serde_json::from_value(self.data.clone()).ok()
948    }
949
950    /// Check if this is a task_updated message
951    pub fn is_task_updated(&self) -> bool {
952        self.subtype == SystemSubtype::TaskUpdated
953    }
954
955    /// Try to parse as a task_updated message
956    pub fn as_task_updated(&self) -> Option<TaskUpdatedMessage> {
957        if self.subtype != SystemSubtype::TaskUpdated {
958            return None;
959        }
960        serde_json::from_value(self.data.clone()).ok()
961    }
962
963    /// Check if this is a thinking_tokens message
964    pub fn is_thinking_tokens(&self) -> bool {
965        self.subtype == SystemSubtype::ThinkingTokens
966    }
967
968    /// Try to parse as a thinking_tokens message
969    pub fn as_thinking_tokens(&self) -> Option<ThinkingTokensMessage> {
970        if self.subtype != SystemSubtype::ThinkingTokens {
971            return None;
972        }
973        serde_json::from_value(self.data.clone()).ok()
974    }
975
976    /// Check if this is a code_change_published message
977    pub fn is_code_change_published(&self) -> bool {
978        self.subtype == SystemSubtype::CodeChangePublished
979    }
980
981    /// Try to parse as a code_change_published message
982    pub fn as_code_change_published(&self) -> Option<CodeChangePublishedMessage> {
983        if self.subtype != SystemSubtype::CodeChangePublished {
984            return None;
985        }
986        serde_json::from_value(self.data.clone()).ok()
987    }
988
989    /// Check if this is a vcs_state_changed message
990    pub fn is_vcs_state_changed(&self) -> bool {
991        self.subtype == SystemSubtype::VcsStateChanged
992    }
993
994    /// Try to parse as a vcs_state_changed message
995    pub fn as_vcs_state_changed(&self) -> Option<VcsStateChangedMessage> {
996        if self.subtype != SystemSubtype::VcsStateChanged {
997            return None;
998        }
999        serde_json::from_value(self.data.clone()).ok()
1000    }
1001
1002    /// Check if this is a feedback_draft_queued message.
1003    pub fn is_feedback_draft_queued(&self) -> bool {
1004        self.subtype == SystemSubtype::FeedbackDraftQueued
1005    }
1006
1007    /// Try to parse as a feedback_draft_queued message.
1008    pub fn as_feedback_draft_queued(&self) -> Option<FeedbackDraftQueuedMessage> {
1009        if self.subtype != SystemSubtype::FeedbackDraftQueued {
1010            return None;
1011        }
1012        serde_json::from_value(self.data.clone()).ok()
1013    }
1014
1015    /// Parse any typed system subtype known to this crate version.
1016    pub fn as_known_system_event(&self) -> Option<KnownSystemEvent> {
1017        macro_rules! parse {
1018            ($variant:ident, $ty:ty) => {
1019                serde_json::from_value::<$ty>(self.data.clone())
1020                    .ok()
1021                    .map(KnownSystemEvent::$variant)
1022            };
1023        }
1024
1025        match self.subtype {
1026            SystemSubtype::Init => parse!(Init, InitMessage),
1027            SystemSubtype::Status => parse!(Status, StatusMessage),
1028            SystemSubtype::CompactBoundary => parse!(CompactBoundary, CompactBoundaryMessage),
1029            SystemSubtype::ThinkingTokens => parse!(ThinkingTokens, ThinkingTokensMessage),
1030            SystemSubtype::TaskStarted => parse!(TaskStarted, TaskStartedMessage),
1031            SystemSubtype::TaskProgress => parse!(TaskProgress, TaskProgressMessage),
1032            SystemSubtype::TaskUpdated => parse!(TaskUpdated, TaskUpdatedMessage),
1033            SystemSubtype::TaskNotification => parse!(TaskNotification, TaskNotificationMessage),
1034            SystemSubtype::ApiRetry => parse!(ApiRetry, ApiRetryMessage),
1035            SystemSubtype::ControlRequestProgress => {
1036                parse!(ControlRequestProgress, ControlRequestProgressMessage)
1037            }
1038            SystemSubtype::ModelRefusalFallback => {
1039                parse!(ModelRefusalFallback, ModelRefusalFallbackMessage)
1040            }
1041            SystemSubtype::ModelRefusalNoFallback => {
1042                parse!(ModelRefusalNoFallback, ModelRefusalNoFallbackMessage)
1043            }
1044            SystemSubtype::LocalCommandOutput => {
1045                parse!(LocalCommandOutput, LocalCommandOutputMessage)
1046            }
1047            SystemSubtype::HookStarted => parse!(HookStarted, HookStartedMessage),
1048            SystemSubtype::HookProgress => parse!(HookProgress, HookProgressMessage),
1049            SystemSubtype::HookResponse => parse!(HookResponse, HookResponseMessage),
1050            SystemSubtype::PluginInstall => parse!(PluginInstall, PluginInstallMessage),
1051            SystemSubtype::BackgroundTasksChanged => {
1052                parse!(BackgroundTasksChanged, BackgroundTasksChangedMessage)
1053            }
1054            SystemSubtype::SessionStateChanged => {
1055                parse!(SessionStateChanged, SessionStateChangedMessage)
1056            }
1057            SystemSubtype::WorkerShuttingDown => {
1058                parse!(WorkerShuttingDown, WorkerShuttingDownMessage)
1059            }
1060            SystemSubtype::CommandsChanged => parse!(CommandsChanged, CommandsChangedMessage),
1061            SystemSubtype::Notification => parse!(Notification, NotificationMessage),
1062            SystemSubtype::FilesPersisted => parse!(FilesPersisted, FilesPersistedMessage),
1063            SystemSubtype::MemoryRecall => parse!(MemoryRecall, MemoryRecallMessage),
1064            SystemSubtype::ElicitationComplete => {
1065                parse!(ElicitationComplete, ElicitationCompleteMessage)
1066            }
1067            SystemSubtype::PermissionDenied => parse!(PermissionDenied, PermissionDeniedMessage),
1068            SystemSubtype::MirrorError => parse!(MirrorError, MirrorErrorMessage),
1069            SystemSubtype::Informational => parse!(Informational, InformationalMessage),
1070            SystemSubtype::CodeChangePublished => {
1071                parse!(CodeChangePublished, CodeChangePublishedMessage)
1072            }
1073            SystemSubtype::VcsStateChanged => parse!(VcsStateChanged, VcsStateChangedMessage),
1074            SystemSubtype::FeedbackDraftQueued => {
1075                parse!(FeedbackDraftQueued, FeedbackDraftQueuedMessage)
1076            }
1077            SystemSubtype::Unknown(_) => None,
1078        }
1079    }
1080
1081    /// Re-serialize this system message's payload through the typed view that
1082    /// matches its `subtype`, returning the result as JSON.
1083    ///
1084    /// Used by the wrapping audit ([`crate::io::audit_frame`]) to verify that a
1085    /// subtype's dedicated struct captures every wire field: the audit compares
1086    /// this against the raw [`SystemMessage::data`]. Returns `None` for subtypes
1087    /// this crate version has no dedicated struct for (including
1088    /// [`SystemSubtype::Unknown`]) — those are reported as not fully wrapped.
1089    pub fn typed_value(&self) -> Option<Value> {
1090        fn reserialize<T: Serialize>(parsed: Option<T>) -> Option<Value> {
1091            parsed.and_then(|v| serde_json::to_value(v).ok())
1092        }
1093        match self.subtype {
1094            SystemSubtype::Init => reserialize(self.as_init()),
1095            SystemSubtype::Status => reserialize(self.as_status()),
1096            SystemSubtype::CompactBoundary => reserialize(self.as_compact_boundary()),
1097            SystemSubtype::ThinkingTokens => reserialize(self.as_thinking_tokens()),
1098            SystemSubtype::TaskStarted => reserialize(self.as_task_started()),
1099            SystemSubtype::TaskProgress => reserialize(self.as_task_progress()),
1100            SystemSubtype::TaskUpdated => reserialize(self.as_task_updated()),
1101            SystemSubtype::TaskNotification => reserialize(self.as_task_notification()),
1102            SystemSubtype::ApiRetry => reserialize(parse_system::<ApiRetryMessage>(self)),
1103            SystemSubtype::ControlRequestProgress => {
1104                reserialize(parse_system::<ControlRequestProgressMessage>(self))
1105            }
1106            SystemSubtype::ModelRefusalFallback => {
1107                reserialize(parse_system::<ModelRefusalFallbackMessage>(self))
1108            }
1109            SystemSubtype::ModelRefusalNoFallback => {
1110                reserialize(parse_system::<ModelRefusalNoFallbackMessage>(self))
1111            }
1112            SystemSubtype::LocalCommandOutput => {
1113                reserialize(parse_system::<LocalCommandOutputMessage>(self))
1114            }
1115            SystemSubtype::HookStarted => reserialize(parse_system::<HookStartedMessage>(self)),
1116            SystemSubtype::HookProgress => reserialize(parse_system::<HookProgressMessage>(self)),
1117            SystemSubtype::HookResponse => reserialize(parse_system::<HookResponseMessage>(self)),
1118            SystemSubtype::PluginInstall => reserialize(parse_system::<PluginInstallMessage>(self)),
1119            SystemSubtype::BackgroundTasksChanged => {
1120                reserialize(parse_system::<BackgroundTasksChangedMessage>(self))
1121            }
1122            SystemSubtype::SessionStateChanged => {
1123                reserialize(parse_system::<SessionStateChangedMessage>(self))
1124            }
1125            SystemSubtype::WorkerShuttingDown => {
1126                reserialize(parse_system::<WorkerShuttingDownMessage>(self))
1127            }
1128            SystemSubtype::CommandsChanged => {
1129                reserialize(parse_system::<CommandsChangedMessage>(self))
1130            }
1131            SystemSubtype::Notification => reserialize(parse_system::<NotificationMessage>(self)),
1132            SystemSubtype::FilesPersisted => {
1133                reserialize(parse_system::<FilesPersistedMessage>(self))
1134            }
1135            SystemSubtype::MemoryRecall => reserialize(parse_system::<MemoryRecallMessage>(self)),
1136            SystemSubtype::ElicitationComplete => {
1137                reserialize(parse_system::<ElicitationCompleteMessage>(self))
1138            }
1139            SystemSubtype::PermissionDenied => {
1140                reserialize(parse_system::<PermissionDeniedMessage>(self))
1141            }
1142            SystemSubtype::MirrorError => reserialize(parse_system::<MirrorErrorMessage>(self)),
1143            SystemSubtype::Informational => reserialize(parse_system::<InformationalMessage>(self)),
1144            SystemSubtype::CodeChangePublished => {
1145                reserialize(parse_system::<CodeChangePublishedMessage>(self))
1146            }
1147            SystemSubtype::VcsStateChanged => {
1148                reserialize(parse_system::<VcsStateChangedMessage>(self))
1149            }
1150            SystemSubtype::FeedbackDraftQueued => {
1151                reserialize(parse_system::<FeedbackDraftQueuedMessage>(self))
1152            }
1153            SystemSubtype::Unknown(_) => None,
1154        }
1155    }
1156}
1157
1158fn parse_system<T: serde::de::DeserializeOwned>(message: &SystemMessage) -> Option<T> {
1159    serde_json::from_value(message.data.clone()).ok()
1160}
1161
1162/// Owned typed view over any known system message subtype.
1163// `InitMessage` outgrew clippy's variant-size threshold when CLI 2.1.232
1164// added fields. This enum is a transient per-parse classification (never
1165// stored in bulk), so boxing would break every match site for no retained-
1166// memory win.
1167#[allow(clippy::large_enum_variant)]
1168#[derive(Debug, Clone, Serialize, Deserialize)]
1169pub enum KnownSystemEvent {
1170    Init(InitMessage),
1171    Status(StatusMessage),
1172    CompactBoundary(CompactBoundaryMessage),
1173    ThinkingTokens(ThinkingTokensMessage),
1174    TaskStarted(TaskStartedMessage),
1175    TaskProgress(TaskProgressMessage),
1176    TaskUpdated(TaskUpdatedMessage),
1177    TaskNotification(TaskNotificationMessage),
1178    ApiRetry(ApiRetryMessage),
1179    ControlRequestProgress(ControlRequestProgressMessage),
1180    ModelRefusalFallback(ModelRefusalFallbackMessage),
1181    ModelRefusalNoFallback(ModelRefusalNoFallbackMessage),
1182    LocalCommandOutput(LocalCommandOutputMessage),
1183    HookStarted(HookStartedMessage),
1184    HookProgress(HookProgressMessage),
1185    HookResponse(HookResponseMessage),
1186    PluginInstall(PluginInstallMessage),
1187    BackgroundTasksChanged(BackgroundTasksChangedMessage),
1188    SessionStateChanged(SessionStateChangedMessage),
1189    WorkerShuttingDown(WorkerShuttingDownMessage),
1190    CommandsChanged(CommandsChangedMessage),
1191    Notification(NotificationMessage),
1192    FilesPersisted(FilesPersistedMessage),
1193    MemoryRecall(MemoryRecallMessage),
1194    ElicitationComplete(ElicitationCompleteMessage),
1195    PermissionDenied(PermissionDeniedMessage),
1196    MirrorError(MirrorErrorMessage),
1197    Informational(InformationalMessage),
1198    CodeChangePublished(CodeChangePublishedMessage),
1199    VcsStateChanged(VcsStateChangedMessage),
1200    FeedbackDraftQueued(FeedbackDraftQueuedMessage),
1201}
1202
1203#[derive(Debug, Clone, Serialize, Deserialize)]
1204pub struct ApiRetryMessage {
1205    pub attempt: u64,
1206    pub max_retries: u64,
1207    pub retry_delay_ms: u64,
1208    pub error_status: Option<u16>,
1209    pub error: String,
1210    #[serde(default, skip_serializing_if = "Option::is_none")]
1211    pub uuid: Option<String>,
1212    #[serde(default, skip_serializing_if = "Option::is_none")]
1213    pub session_id: Option<String>,
1214}
1215
1216#[derive(Debug, Clone, Serialize, Deserialize)]
1217pub struct ControlRequestProgressMessage {
1218    pub request_id: String,
1219    pub status: String,
1220    #[serde(default, skip_serializing_if = "Option::is_none")]
1221    pub attempt: Option<u64>,
1222    #[serde(default, skip_serializing_if = "Option::is_none")]
1223    pub max_retries: Option<u64>,
1224    #[serde(default, skip_serializing_if = "Option::is_none")]
1225    pub retry_delay_ms: Option<u64>,
1226    #[serde(default, skip_serializing_if = "Option::is_none")]
1227    pub error_status: Option<u16>,
1228    #[serde(default, skip_serializing_if = "Option::is_none")]
1229    pub error: Option<String>,
1230    #[serde(default, skip_serializing_if = "Option::is_none")]
1231    pub uuid: Option<String>,
1232    #[serde(default, skip_serializing_if = "Option::is_none")]
1233    pub session_id: Option<String>,
1234}
1235
1236#[derive(Debug, Clone, Serialize, Deserialize)]
1237pub struct ModelRefusalFallbackMessage {
1238    pub trigger: String,
1239    pub direction: String,
1240    /// `"session"`: the main thread fell back and the session model is
1241    /// swapped. `"local"`: a subagent / side-question (`/btw`) / background
1242    /// fork fell back — only that response came from the fallback model and
1243    /// the session model is unchanged. Absent from CLIs before 2.1.222
1244    /// (treat as `"session"`).
1245    #[serde(default, skip_serializing_if = "Option::is_none")]
1246    pub scope: Option<RefusalFallbackScope>,
1247    pub original_model: String,
1248    pub fallback_model: String,
1249    pub request_id: Option<String>,
1250    #[serde(default, skip_serializing_if = "Option::is_none")]
1251    pub api_refusal_category: Option<String>,
1252    #[serde(default, skip_serializing_if = "Option::is_none")]
1253    pub api_refusal_explanation: Option<String>,
1254    #[serde(default, skip_serializing_if = "Option::is_none")]
1255    pub retracted_message_uuids: Option<Vec<String>>,
1256    #[serde(default, skip_serializing_if = "Option::is_none")]
1257    pub refused_user_message_uuid: Option<String>,
1258    pub content: Value,
1259    #[serde(default, skip_serializing_if = "Option::is_none")]
1260    pub uuid: Option<String>,
1261    #[serde(default, skip_serializing_if = "Option::is_none")]
1262    pub session_id: Option<String>,
1263}
1264
1265/// Scope of a refusal-fallback model swap, carried by
1266/// [`ModelRefusalFallbackMessage::scope`]. Open — new scopes may ship on the
1267/// wire ahead of schema updates.
1268#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1269pub enum RefusalFallbackScope {
1270    /// The main thread fell back; the session model is swapped.
1271    Session,
1272    /// A subagent / side-question / background fork fell back; only that
1273    /// response used the fallback model, the session model is unchanged.
1274    Local,
1275    /// A scope not yet known to this version of the crate.
1276    Unknown(String),
1277}
1278
1279impl RefusalFallbackScope {
1280    pub fn as_str(&self) -> &str {
1281        match self {
1282            Self::Session => "session",
1283            Self::Local => "local",
1284            Self::Unknown(s) => s.as_str(),
1285        }
1286    }
1287}
1288
1289impl fmt::Display for RefusalFallbackScope {
1290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1291        f.write_str(self.as_str())
1292    }
1293}
1294
1295impl From<&str> for RefusalFallbackScope {
1296    fn from(s: &str) -> Self {
1297        match s {
1298            "session" => Self::Session,
1299            "local" => Self::Local,
1300            other => Self::Unknown(other.to_string()),
1301        }
1302    }
1303}
1304
1305impl Serialize for RefusalFallbackScope {
1306    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1307        serializer.serialize_str(self.as_str())
1308    }
1309}
1310
1311impl<'de> Deserialize<'de> for RefusalFallbackScope {
1312    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1313        let s = String::deserialize(deserializer)?;
1314        Ok(Self::from(s.as_str()))
1315    }
1316}
1317
1318#[derive(Debug, Clone, Serialize, Deserialize)]
1319pub struct ModelRefusalNoFallbackMessage {
1320    pub original_model: String,
1321    pub request_id: Option<String>,
1322    #[serde(default, skip_serializing_if = "Option::is_none")]
1323    pub api_refusal_category: Option<String>,
1324    #[serde(default, skip_serializing_if = "Option::is_none")]
1325    pub api_refusal_explanation: Option<String>,
1326    pub content: Value,
1327    #[serde(default, skip_serializing_if = "Option::is_none")]
1328    pub uuid: Option<String>,
1329    #[serde(default, skip_serializing_if = "Option::is_none")]
1330    pub session_id: Option<String>,
1331}
1332
1333#[derive(Debug, Clone, Serialize, Deserialize)]
1334pub struct LocalCommandOutputMessage {
1335    pub content: String,
1336    #[serde(default, skip_serializing_if = "Option::is_none")]
1337    pub uuid: Option<String>,
1338    #[serde(default, skip_serializing_if = "Option::is_none")]
1339    pub session_id: Option<String>,
1340}
1341
1342#[derive(Debug, Clone, Serialize, Deserialize)]
1343pub struct HookStartedMessage {
1344    pub hook_id: String,
1345    pub hook_name: String,
1346    pub hook_event: String,
1347    #[serde(default, skip_serializing_if = "Option::is_none")]
1348    pub uuid: Option<String>,
1349    #[serde(default, skip_serializing_if = "Option::is_none")]
1350    pub session_id: Option<String>,
1351}
1352
1353#[derive(Debug, Clone, Serialize, Deserialize)]
1354pub struct HookProgressMessage {
1355    pub hook_id: String,
1356    pub hook_name: String,
1357    pub hook_event: String,
1358    #[serde(default, skip_serializing_if = "Option::is_none")]
1359    pub stdout: Option<String>,
1360    #[serde(default, skip_serializing_if = "Option::is_none")]
1361    pub stderr: Option<String>,
1362    #[serde(default, skip_serializing_if = "Option::is_none")]
1363    pub output: Option<String>,
1364    #[serde(default, skip_serializing_if = "Option::is_none")]
1365    pub uuid: Option<String>,
1366    #[serde(default, skip_serializing_if = "Option::is_none")]
1367    pub session_id: Option<String>,
1368}
1369
1370#[derive(Debug, Clone, Serialize, Deserialize)]
1371pub struct HookResponseMessage {
1372    pub hook_id: String,
1373    pub hook_name: String,
1374    pub hook_event: String,
1375    #[serde(default, skip_serializing_if = "Option::is_none")]
1376    pub stdout: Option<String>,
1377    #[serde(default, skip_serializing_if = "Option::is_none")]
1378    pub stderr: Option<String>,
1379    #[serde(default, skip_serializing_if = "Option::is_none")]
1380    pub output: Option<String>,
1381    #[serde(default, skip_serializing_if = "Option::is_none")]
1382    pub exit_code: Option<i32>,
1383    pub outcome: String,
1384    #[serde(default, skip_serializing_if = "Option::is_none")]
1385    pub uuid: Option<String>,
1386    #[serde(default, skip_serializing_if = "Option::is_none")]
1387    pub session_id: Option<String>,
1388}
1389
1390#[derive(Debug, Clone, Serialize, Deserialize)]
1391pub struct PluginInstallMessage {
1392    pub status: String,
1393    #[serde(default, skip_serializing_if = "Option::is_none")]
1394    pub name: Option<String>,
1395    #[serde(default, skip_serializing_if = "Option::is_none")]
1396    pub error: Option<String>,
1397    #[serde(default, skip_serializing_if = "Option::is_none")]
1398    pub uuid: Option<String>,
1399    #[serde(default, skip_serializing_if = "Option::is_none")]
1400    pub session_id: Option<String>,
1401}
1402
1403#[derive(Debug, Clone, Serialize, Deserialize)]
1404pub struct BackgroundTasksChangedMessage {
1405    pub tasks: Vec<BackgroundTaskInfo>,
1406    #[serde(default, skip_serializing_if = "Option::is_none")]
1407    pub uuid: Option<String>,
1408    #[serde(default, skip_serializing_if = "Option::is_none")]
1409    pub session_id: Option<String>,
1410}
1411
1412#[derive(Debug, Clone, Serialize, Deserialize)]
1413pub struct BackgroundTaskInfo {
1414    pub task_id: String,
1415    pub task_type: String,
1416    pub description: String,
1417}
1418
1419#[derive(Debug, Clone, Serialize, Deserialize)]
1420pub struct SessionStateChangedMessage {
1421    pub state: String,
1422    #[serde(default, skip_serializing_if = "Option::is_none")]
1423    pub uuid: Option<String>,
1424    #[serde(default, skip_serializing_if = "Option::is_none")]
1425    pub session_id: Option<String>,
1426}
1427
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1429pub struct WorkerShuttingDownMessage {
1430    pub reason: String,
1431    #[serde(default, skip_serializing_if = "Option::is_none")]
1432    pub uuid: Option<String>,
1433    #[serde(default, skip_serializing_if = "Option::is_none")]
1434    pub session_id: Option<String>,
1435}
1436
1437#[derive(Debug, Clone, Serialize, Deserialize)]
1438pub struct CommandsChangedMessage {
1439    pub commands: Vec<CommandInfo>,
1440    #[serde(default, skip_serializing_if = "Option::is_none")]
1441    pub uuid: Option<String>,
1442    #[serde(default, skip_serializing_if = "Option::is_none")]
1443    pub session_id: Option<String>,
1444}
1445
1446#[derive(Debug, Clone, Serialize, Deserialize)]
1447pub struct CommandInfo {
1448    pub name: String,
1449    pub description: String,
1450    #[serde(rename = "argumentHint")]
1451    pub argument_hint: String,
1452    #[serde(default, skip_serializing_if = "Option::is_none")]
1453    pub aliases: Option<Vec<String>>,
1454}
1455
1456#[derive(Debug, Clone, Serialize, Deserialize)]
1457pub struct NotificationMessage {
1458    pub key: String,
1459    pub text: String,
1460    pub priority: String,
1461    #[serde(default, skip_serializing_if = "Option::is_none")]
1462    pub color: Option<String>,
1463    #[serde(default, skip_serializing_if = "Option::is_none")]
1464    pub timeout_ms: Option<u64>,
1465    #[serde(default, skip_serializing_if = "Option::is_none")]
1466    pub uuid: Option<String>,
1467    #[serde(default, skip_serializing_if = "Option::is_none")]
1468    pub session_id: Option<String>,
1469}
1470
1471#[derive(Debug, Clone, Serialize, Deserialize)]
1472pub struct FilesPersistedMessage {
1473    pub files: Vec<PersistedFile>,
1474    pub failed: Vec<FailedPersistedFile>,
1475    pub processed_at: String,
1476    #[serde(default, skip_serializing_if = "Option::is_none")]
1477    pub uuid: Option<String>,
1478    #[serde(default, skip_serializing_if = "Option::is_none")]
1479    pub session_id: Option<String>,
1480}
1481
1482#[derive(Debug, Clone, Serialize, Deserialize)]
1483pub struct PersistedFile {
1484    pub filename: String,
1485    pub file_id: String,
1486}
1487
1488#[derive(Debug, Clone, Serialize, Deserialize)]
1489pub struct FailedPersistedFile {
1490    pub filename: String,
1491    pub error: String,
1492}
1493
1494#[derive(Debug, Clone, Serialize, Deserialize)]
1495pub struct MemoryRecallMessage {
1496    pub mode: String,
1497    pub memories: Vec<MemoryRecallItem>,
1498    #[serde(default, skip_serializing_if = "Option::is_none")]
1499    pub uuid: Option<String>,
1500    #[serde(default, skip_serializing_if = "Option::is_none")]
1501    pub session_id: Option<String>,
1502}
1503
1504#[derive(Debug, Clone, Serialize, Deserialize)]
1505pub struct MemoryRecallItem {
1506    pub path: String,
1507    pub scope: String,
1508    #[serde(default, skip_serializing_if = "Option::is_none")]
1509    pub content: Option<String>,
1510}
1511
1512#[derive(Debug, Clone, Serialize, Deserialize)]
1513pub struct ElicitationCompleteMessage {
1514    pub mcp_server_name: String,
1515    pub elicitation_id: String,
1516    #[serde(default, skip_serializing_if = "Option::is_none")]
1517    pub uuid: Option<String>,
1518    #[serde(default, skip_serializing_if = "Option::is_none")]
1519    pub session_id: Option<String>,
1520}
1521
1522#[derive(Debug, Clone, Serialize, Deserialize)]
1523pub struct PermissionDeniedMessage {
1524    pub tool_name: String,
1525    pub tool_use_id: String,
1526    #[serde(default, skip_serializing_if = "Option::is_none")]
1527    pub agent_id: Option<String>,
1528    #[serde(default, skip_serializing_if = "Option::is_none")]
1529    pub decision_reason_type: Option<String>,
1530    #[serde(default, skip_serializing_if = "Option::is_none")]
1531    pub decision_reason: Option<String>,
1532    pub message: String,
1533    #[serde(default, skip_serializing_if = "Option::is_none")]
1534    pub uuid: Option<String>,
1535    #[serde(default, skip_serializing_if = "Option::is_none")]
1536    pub session_id: Option<String>,
1537}
1538
1539#[derive(Debug, Clone, Serialize, Deserialize)]
1540pub struct MirrorErrorMessage {
1541    pub error: String,
1542    pub key: MirrorErrorKey,
1543    #[serde(default, skip_serializing_if = "Option::is_none")]
1544    pub uuid: Option<String>,
1545    #[serde(default, skip_serializing_if = "Option::is_none")]
1546    pub session_id: Option<String>,
1547}
1548
1549#[derive(Debug, Clone, Serialize, Deserialize)]
1550pub struct MirrorErrorKey {
1551    #[serde(rename = "projectKey")]
1552    pub project_key: String,
1553    #[serde(rename = "sessionId")]
1554    pub session_id: String,
1555    #[serde(default, skip_serializing_if = "Option::is_none")]
1556    pub subpath: Option<String>,
1557}
1558
1559#[derive(Debug, Clone, Serialize, Deserialize)]
1560pub struct InformationalMessage {
1561    pub content: String,
1562    pub level: String,
1563    #[serde(default, skip_serializing_if = "Option::is_none")]
1564    pub tool_use_id: Option<String>,
1565    #[serde(default, skip_serializing_if = "Option::is_none")]
1566    pub prevent_continuation: Option<bool>,
1567    #[serde(default, skip_serializing_if = "Option::is_none")]
1568    pub uuid: Option<String>,
1569    #[serde(default, skip_serializing_if = "Option::is_none")]
1570    pub session_id: Option<String>,
1571}
1572
1573/// Plugin info from the init message
1574#[derive(Debug, Clone, Serialize, Deserialize)]
1575pub struct PluginInfo {
1576    /// Plugin name
1577    pub name: String,
1578    /// Path to the plugin on disk
1579    pub path: String,
1580    /// Plugin registry source (e.g., "rust-analyzer-lsp@claude-plugins-official")
1581    #[serde(skip_serializing_if = "Option::is_none")]
1582    pub source: Option<String>,
1583    /// Installed plugin version (e.g., "1.0.0"). Added in CLI 2.1.219.
1584    #[serde(default, skip_serializing_if = "Option::is_none")]
1585    pub version: Option<String>,
1586}
1587
1588/// Plugin load diagnostic reported by system init.
1589#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1590pub struct PluginDiagnostic {
1591    pub plugin: String,
1592    #[serde(rename = "type")]
1593    pub diagnostic_type: String,
1594    pub message: String,
1595}
1596
1597/// Memory paths reported by system init.
1598#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1599pub struct MemoryPaths {
1600    #[serde(default, skip_serializing_if = "Option::is_none")]
1601    pub auto: Option<String>,
1602    #[serde(default, skip_serializing_if = "Option::is_none")]
1603    pub team: Option<String>,
1604    #[serde(flatten)]
1605    pub extra: serde_json::Map<String, Value>,
1606}
1607
1608/// An MCP server config entry that failed validation, reported by system
1609/// init (e.g. a `url` entry with no `type`). The affected server is skipped
1610/// and absent from `InitMessage::mcp_servers`.
1611#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1612pub struct McpServerError {
1613    pub name: String,
1614    /// Stable error category.
1615    #[serde(rename = "type")]
1616    pub error_type: String,
1617    pub message: String,
1618}
1619
1620/// Init system message data - sent at session start
1621#[derive(Debug, Clone, Serialize, Deserialize)]
1622pub struct InitMessage {
1623    /// Session identifier
1624    pub session_id: String,
1625    /// Current working directory
1626    #[serde(skip_serializing_if = "Option::is_none")]
1627    pub cwd: Option<String>,
1628    /// Model being used
1629    #[serde(skip_serializing_if = "Option::is_none")]
1630    pub model: Option<String>,
1631    /// List of available tools
1632    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1633    pub tools: Vec<String>,
1634    /// MCP servers configured
1635    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1636    pub mcp_servers: Vec<Value>,
1637    /// Available slash commands (e.g., "compact", "cost", "review")
1638    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1639    pub slash_commands: Vec<String>,
1640    /// Slash commands only meaningful in a terminal context (CLI 2.1.232+,
1641    /// e.g. "doctor", "color")
1642    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1643    pub terminal_slash_commands: Vec<String>,
1644    /// Available agent types (e.g., "Bash", "Explore", "Plan")
1645    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1646    pub agents: Vec<String>,
1647    /// Installed plugins
1648    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1649    pub plugins: Vec<PluginInfo>,
1650    /// Installed skills
1651    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1652    pub skills: Vec<Value>,
1653    /// Claude Code CLI version
1654    #[serde(skip_serializing_if = "Option::is_none")]
1655    pub claude_code_version: Option<String>,
1656    /// Unix socket path for the harness's inter-session messaging bridge
1657    /// (new in CLI 2.1.232; absent on older CLIs and non-bridged runs)
1658    #[serde(skip_serializing_if = "Option::is_none")]
1659    pub messaging_socket_path: Option<String>,
1660    /// How the API key was sourced
1661    #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
1662    pub api_key_source: Option<ApiKeySource>,
1663    /// Output style
1664    #[serde(skip_serializing_if = "Option::is_none")]
1665    pub output_style: Option<OutputStyle>,
1666    /// Permission mode
1667    #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1668    pub permission_mode: Option<InitPermissionMode>,
1669
1670    /// Message-level unique identifier
1671    #[serde(skip_serializing_if = "Option::is_none")]
1672    pub uuid: Option<String>,
1673
1674    /// Memory storage paths (e.g., {"auto": "/path/to/memory/"})
1675    #[serde(skip_serializing_if = "Option::is_none")]
1676    pub memory_paths: Option<MemoryPaths>,
1677
1678    /// Fast mode toggle state (e.g., "off")
1679    #[serde(skip_serializing_if = "Option::is_none")]
1680    pub fast_mode_state: Option<String>,
1681
1682    /// Why fast mode can't serve right now. Absent when nothing blocks it.
1683    #[serde(default, skip_serializing_if = "Option::is_none")]
1684    pub fast_mode_disabled_reason: Option<super::result::FastModeDisabledReason>,
1685
1686    /// MCP server config entries (from `--mcp-config`) that failed validation
1687    /// and were skipped. Affected servers are absent from `mcp_servers`.
1688    #[serde(default, skip_serializing_if = "Option::is_none")]
1689    pub mcp_server_errors: Option<Vec<McpServerError>>,
1690
1691    /// Whether analytics collection is disabled for this session.
1692    #[serde(default, skip_serializing_if = "Option::is_none")]
1693    pub analytics_disabled: Option<bool>,
1694
1695    /// Whether product-feedback prompts are disabled for this session.
1696    #[serde(default, skip_serializing_if = "Option::is_none")]
1697    pub product_feedback_disabled: Option<bool>,
1698
1699    /// API beta flags active for the session.
1700    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1701    pub betas: Vec<String>,
1702
1703    /// Open-set protocol capability names supported by this CLI.
1704    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1705    pub capabilities: Vec<String>,
1706
1707    /// Plugin load errors.
1708    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1709    pub plugin_errors: Vec<PluginDiagnostic>,
1710
1711    /// Plugin load warnings.
1712    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1713    pub plugin_warnings: Vec<PluginDiagnostic>,
1714}
1715
1716/// Status system message - sent during operations like context compaction
1717#[derive(Debug, Clone, Serialize, Deserialize)]
1718pub struct StatusMessage {
1719    /// Session identifier
1720    pub session_id: String,
1721    /// Current status (e.g., compacting) or null when complete
1722    pub status: Option<StatusMessageStatus>,
1723    /// Unique identifier for this message
1724    #[serde(skip_serializing_if = "Option::is_none")]
1725    pub uuid: Option<String>,
1726    /// Current permission mode when changed mid-session.
1727    #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
1728    pub permission_mode: Option<InitPermissionMode>,
1729    #[serde(skip_serializing_if = "Option::is_none")]
1730    pub compact_result: Option<String>,
1731    #[serde(skip_serializing_if = "Option::is_none")]
1732    pub compact_error: Option<String>,
1733}
1734
1735/// Compact boundary message - marks where context compaction occurred
1736#[derive(Debug, Clone, Serialize, Deserialize)]
1737pub struct CompactBoundaryMessage {
1738    /// Session identifier
1739    pub session_id: String,
1740    /// Metadata about the compaction
1741    pub compact_metadata: CompactMetadata,
1742    /// Human-readable summary of what was compacted, when the CLI emits one.
1743    ///
1744    /// Also accepted under the `content` / `text` wire keys.
1745    #[serde(
1746        default,
1747        skip_serializing_if = "Option::is_none",
1748        alias = "content",
1749        alias = "text"
1750    )]
1751    pub summary: Option<String>,
1752    /// Number of messages summarized in this compaction pass, when present.
1753    ///
1754    /// Also accepted under the `message_count` wire key.
1755    #[serde(
1756        default,
1757        skip_serializing_if = "Option::is_none",
1758        alias = "message_count"
1759    )]
1760    pub leaf_message_count: Option<u32>,
1761    /// Wall-clock duration of the compaction pass in milliseconds, when present.
1762    #[serde(default, skip_serializing_if = "Option::is_none")]
1763    pub duration_ms: Option<u64>,
1764    /// Unique identifier for this message
1765    #[serde(skip_serializing_if = "Option::is_none")]
1766    pub uuid: Option<String>,
1767    /// Logical parent across the compaction boundary.
1768    #[serde(skip_serializing_if = "Option::is_none")]
1769    pub logical_parent_uuid: Option<Option<String>>,
1770}
1771
1772/// Metadata about context compaction
1773#[derive(Debug, Clone, Serialize, Deserialize)]
1774pub struct CompactMetadata {
1775    /// Number of tokens before compaction
1776    pub pre_tokens: u64,
1777    /// What triggered the compaction
1778    pub trigger: CompactionTrigger,
1779    #[serde(default, skip_serializing_if = "Option::is_none")]
1780    pub post_tokens: Option<u64>,
1781    #[serde(default, skip_serializing_if = "Option::is_none")]
1782    pub cumulative_dropped_tokens: Option<u64>,
1783    #[serde(default, skip_serializing_if = "Option::is_none")]
1784    pub duration_ms: Option<u64>,
1785    #[serde(default, skip_serializing_if = "Option::is_none")]
1786    pub user_context: Option<String>,
1787    #[serde(default, skip_serializing_if = "Option::is_none")]
1788    pub messages_summarized: Option<u64>,
1789    #[serde(default, skip_serializing_if = "Option::is_none")]
1790    pub precomputed: Option<bool>,
1791    #[serde(default, skip_serializing_if = "Option::is_none")]
1792    pub pre_compact_discovered_tools: Option<Vec<String>>,
1793    #[serde(default, skip_serializing_if = "Option::is_none")]
1794    pub preserved_segment: Option<PreservedSegment>,
1795    #[serde(default, skip_serializing_if = "Option::is_none")]
1796    pub preserved_messages: Option<PreservedMessages>,
1797}
1798
1799#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1800pub struct PreservedSegment {
1801    pub head_uuid: String,
1802    pub anchor_uuid: String,
1803    pub tail_uuid: String,
1804}
1805
1806#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1807pub struct PreservedMessages {
1808    pub anchor_uuid: String,
1809    pub uuids: Vec<String>,
1810    #[serde(default, skip_serializing_if = "Option::is_none")]
1811    pub all_uuids: Option<Vec<String>>,
1812}
1813
1814// ---------------------------------------------------------------------------
1815// Task system message types (task_started, task_progress, task_notification)
1816// ---------------------------------------------------------------------------
1817
1818/// Cumulative usage statistics for a background task.
1819#[derive(Debug, Clone, Serialize, Deserialize)]
1820pub struct TaskUsage {
1821    /// Wall-clock milliseconds since the task started.
1822    pub duration_ms: u64,
1823    /// Total number of tool calls made so far.
1824    pub tool_uses: u64,
1825    /// Total tokens consumed so far.
1826    pub total_tokens: u64,
1827}
1828
1829/// The kind of background task.
1830#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1831pub enum TaskType {
1832    /// A sub-agent task (e.g., Explore, Plan).
1833    LocalAgent,
1834    /// A background bash command.
1835    LocalBash,
1836    /// A local workflow task.
1837    LocalWorkflow,
1838    /// A task type not yet known to this version of the crate.
1839    Unknown(String),
1840}
1841
1842impl TaskType {
1843    pub fn as_str(&self) -> &str {
1844        match self {
1845            Self::LocalAgent => "local_agent",
1846            Self::LocalBash => "local_bash",
1847            Self::LocalWorkflow => "local_workflow",
1848            Self::Unknown(s) => s.as_str(),
1849        }
1850    }
1851}
1852
1853impl fmt::Display for TaskType {
1854    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1855        f.write_str(self.as_str())
1856    }
1857}
1858
1859impl From<&str> for TaskType {
1860    fn from(s: &str) -> Self {
1861        match s {
1862            "local_agent" => Self::LocalAgent,
1863            "local_bash" => Self::LocalBash,
1864            "local_workflow" => Self::LocalWorkflow,
1865            other => Self::Unknown(other.to_string()),
1866        }
1867    }
1868}
1869
1870impl Serialize for TaskType {
1871    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1872        serializer.serialize_str(self.as_str())
1873    }
1874}
1875
1876impl<'de> Deserialize<'de> for TaskType {
1877    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1878        let s = String::deserialize(deserializer)?;
1879        Ok(Self::from(s.as_str()))
1880    }
1881}
1882
1883/// Completion status of a background task.
1884#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1885pub enum TaskStatus {
1886    Pending,
1887    Running,
1888    Completed,
1889    Failed,
1890    Killed,
1891    Paused,
1892    Stopped,
1893    Unknown(String),
1894}
1895
1896impl TaskStatus {
1897    pub fn as_str(&self) -> &str {
1898        match self {
1899            Self::Pending => "pending",
1900            Self::Running => "running",
1901            Self::Completed => "completed",
1902            Self::Failed => "failed",
1903            Self::Killed => "killed",
1904            Self::Paused => "paused",
1905            Self::Stopped => "stopped",
1906            Self::Unknown(s) => s.as_str(),
1907        }
1908    }
1909}
1910
1911impl fmt::Display for TaskStatus {
1912    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1913        f.write_str(self.as_str())
1914    }
1915}
1916
1917impl From<&str> for TaskStatus {
1918    fn from(s: &str) -> Self {
1919        match s {
1920            "pending" => Self::Pending,
1921            "running" => Self::Running,
1922            "completed" => Self::Completed,
1923            "failed" => Self::Failed,
1924            "killed" => Self::Killed,
1925            "paused" => Self::Paused,
1926            "stopped" => Self::Stopped,
1927            other => Self::Unknown(other.to_string()),
1928        }
1929    }
1930}
1931
1932impl Serialize for TaskStatus {
1933    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1934        serializer.serialize_str(self.as_str())
1935    }
1936}
1937
1938impl<'de> Deserialize<'de> for TaskStatus {
1939    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1940        let s = String::deserialize(deserializer)?;
1941        Ok(Self::from(s.as_str()))
1942    }
1943}
1944
1945/// `task_started` system message — emitted once when a background task begins.
1946#[derive(Debug, Clone, Serialize, Deserialize)]
1947pub struct TaskStartedMessage {
1948    pub session_id: String,
1949    pub task_id: String,
1950    #[serde(default, skip_serializing_if = "Option::is_none")]
1951    pub task_type: Option<TaskType>,
1952    #[serde(default, skip_serializing_if = "Option::is_none")]
1953    pub tool_use_id: Option<String>,
1954    pub description: String,
1955    /// The subagent type for `local_agent` tasks (e.g. `general-purpose`,
1956    /// `Explore`). Absent for `local_bash` tasks.
1957    #[serde(default, skip_serializing_if = "Option::is_none")]
1958    pub subagent_type: Option<String>,
1959    /// The prompt handed to the subagent. Present for `local_agent` tasks.
1960    #[serde(default, skip_serializing_if = "Option::is_none")]
1961    pub prompt: Option<String>,
1962    #[serde(default, skip_serializing_if = "Option::is_none")]
1963    pub workflow_name: Option<String>,
1964    #[serde(default, skip_serializing_if = "Option::is_none")]
1965    pub skip_transcript: Option<bool>,
1966    pub uuid: String,
1967}
1968
1969/// `task_updated` system message — emitted when a background task's state
1970/// changes (e.g. transitions to `completed`). Carries a partial `patch` of the
1971/// fields that changed rather than the full task record.
1972#[derive(Debug, Clone, Serialize, Deserialize)]
1973pub struct TaskUpdatedMessage {
1974    pub session_id: String,
1975    pub task_id: String,
1976    pub patch: TaskPatch,
1977    pub uuid: String,
1978}
1979
1980/// The partial update carried by a [`TaskUpdatedMessage`]. Every field is
1981/// optional because the CLI only sends the keys that changed.
1982#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1983pub struct TaskPatch {
1984    #[serde(default, skip_serializing_if = "Option::is_none")]
1985    pub status: Option<TaskStatus>,
1986    /// Wall-clock epoch milliseconds when the task finished, when the patch
1987    /// reports completion.
1988    #[serde(default, skip_serializing_if = "Option::is_none")]
1989    pub end_time: Option<u64>,
1990    #[serde(default, skip_serializing_if = "Option::is_none")]
1991    pub description: Option<String>,
1992    #[serde(default, skip_serializing_if = "Option::is_none")]
1993    pub total_paused_ms: Option<u64>,
1994    #[serde(default, skip_serializing_if = "Option::is_none")]
1995    pub error: Option<String>,
1996    #[serde(default, skip_serializing_if = "Option::is_none")]
1997    pub is_backgrounded: Option<bool>,
1998}
1999
2000/// `thinking_tokens` system message — emitted as the model streams extended
2001/// thinking, reporting the running estimate of thinking tokens consumed.
2002#[derive(Debug, Clone, Serialize, Deserialize)]
2003pub struct ThinkingTokensMessage {
2004    pub session_id: String,
2005    /// Running estimate of total thinking tokens for the current turn.
2006    pub estimated_tokens: u64,
2007    /// Increase in the estimate since the previous `thinking_tokens` event.
2008    pub estimated_tokens_delta: u64,
2009    pub uuid: String,
2010}
2011
2012/// `task_progress` system message — emitted periodically as a background
2013/// agent task executes tools. Not emitted for `local_bash` tasks.
2014#[derive(Debug, Clone, Serialize, Deserialize)]
2015pub struct TaskProgressMessage {
2016    pub session_id: String,
2017    pub task_id: String,
2018    #[serde(default, skip_serializing_if = "Option::is_none")]
2019    pub tool_use_id: Option<String>,
2020    pub description: String,
2021    #[serde(default, skip_serializing_if = "Option::is_none")]
2022    pub last_tool_name: Option<String>,
2023    pub usage: TaskUsage,
2024    /// Subagent type for `local_agent` tasks (e.g. `Explore`).
2025    #[serde(default, skip_serializing_if = "Option::is_none")]
2026    pub subagent_type: Option<String>,
2027    #[serde(default, skip_serializing_if = "Option::is_none")]
2028    pub summary: Option<String>,
2029    pub uuid: String,
2030}
2031
2032/// `task_notification` system message — emitted once when a background
2033/// task completes or fails.
2034#[derive(Debug, Clone, Serialize, Deserialize)]
2035pub struct TaskNotificationMessage {
2036    pub session_id: String,
2037    pub task_id: String,
2038    pub status: TaskStatus,
2039    pub summary: String,
2040    pub output_file: Option<String>,
2041    #[serde(skip_serializing_if = "Option::is_none")]
2042    pub tool_use_id: Option<String>,
2043    #[serde(skip_serializing_if = "Option::is_none")]
2044    pub usage: Option<TaskUsage>,
2045    #[serde(default, skip_serializing_if = "Option::is_none")]
2046    pub skip_transcript: Option<bool>,
2047    #[serde(skip_serializing_if = "Option::is_none")]
2048    pub uuid: Option<String>,
2049}
2050
2051/// API error category attached to assistant wrapper frames.
2052#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2053pub enum AssistantErrorKind {
2054    AuthenticationFailed,
2055    OauthOrgNotAllowed,
2056    BillingError,
2057    RateLimit,
2058    Overloaded,
2059    InvalidRequest,
2060    ModelNotFound,
2061    ServerError,
2062    UnknownError,
2063    MaxOutputTokens,
2064    Unknown(String),
2065}
2066
2067impl AssistantErrorKind {
2068    pub fn as_str(&self) -> &str {
2069        match self {
2070            Self::AuthenticationFailed => "authentication_failed",
2071            Self::OauthOrgNotAllowed => "oauth_org_not_allowed",
2072            Self::BillingError => "billing_error",
2073            Self::RateLimit => "rate_limit",
2074            Self::Overloaded => "overloaded",
2075            Self::InvalidRequest => "invalid_request",
2076            Self::ModelNotFound => "model_not_found",
2077            Self::ServerError => "server_error",
2078            Self::UnknownError => "unknown",
2079            Self::MaxOutputTokens => "max_output_tokens",
2080            Self::Unknown(s) => s.as_str(),
2081        }
2082    }
2083}
2084
2085impl fmt::Display for AssistantErrorKind {
2086    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2087        f.write_str(self.as_str())
2088    }
2089}
2090
2091impl From<&str> for AssistantErrorKind {
2092    fn from(s: &str) -> Self {
2093        match s {
2094            "authentication_failed" => Self::AuthenticationFailed,
2095            "oauth_org_not_allowed" => Self::OauthOrgNotAllowed,
2096            "billing_error" => Self::BillingError,
2097            "rate_limit" => Self::RateLimit,
2098            "overloaded" => Self::Overloaded,
2099            "invalid_request" => Self::InvalidRequest,
2100            "model_not_found" => Self::ModelNotFound,
2101            "server_error" => Self::ServerError,
2102            "unknown" => Self::UnknownError,
2103            "max_output_tokens" => Self::MaxOutputTokens,
2104            other => Self::Unknown(other.to_string()),
2105        }
2106    }
2107}
2108
2109impl Serialize for AssistantErrorKind {
2110    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2111        serializer.serialize_str(self.as_str())
2112    }
2113}
2114
2115impl<'de> Deserialize<'de> for AssistantErrorKind {
2116    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2117        let s = String::deserialize(deserializer)?;
2118        Ok(Self::from(s.as_str()))
2119    }
2120}
2121
2122/// `code_change_published` system message — the session is now associated
2123/// with a published code change (a pull/merge request). Fires on creation and
2124/// whenever the session contributes to an existing one, so bind on every
2125/// event; re-emission for the same URL is possible and idempotent. Values are
2126/// scraped from captured command output — treat them as a binding hint and
2127/// verify against the forge before routing authenticated requests.
2128#[derive(Debug, Clone, Serialize, Deserialize)]
2129pub struct CodeChangePublishedMessage {
2130    /// Forge classification derived from the URL's shape (`github`,
2131    /// `github-enterprise`, `gitlab`, `bitbucket` today). Open set — treat an
2132    /// unknown value as a valid provider, never as an error.
2133    pub provider: String,
2134    /// Web URL of the pull/merge request. Unverified.
2135    pub url: String,
2136    /// Repository path from the URL (`owner/name` on GitHub; may carry more
2137    /// segments on GitLab).
2138    pub repo: String,
2139    /// Provider-native change identifier — the PR/MR number as a string.
2140    pub identifier: String,
2141    pub uuid: String,
2142    pub session_id: String,
2143}
2144
2145/// `vcs_state_changed` system message — a harness-observed shell command
2146/// mutated repository state. A cache-invalidation signal, deliberately
2147/// payload-free beyond classification: consumers re-read state (branch, head,
2148/// PR status) instead of decoding the event.
2149#[derive(Debug, Clone, Serialize, Deserialize)]
2150pub struct VcsStateChangedMessage {
2151    /// What class of mutation was observed. New kinds may be added — treat an
2152    /// unrecognized kind exactly like a recognized one (something changed).
2153    pub kind: VcsMutationKind,
2154    /// The session's working directory — a hint, not necessarily the mutated
2155    /// repo's path (`git -C` or an inner `cd` mutates elsewhere).
2156    pub cwd: String,
2157    pub uuid: String,
2158    pub session_id: String,
2159}
2160
2161/// Mutation class carried by a [`VcsStateChangedMessage`].
2162#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2163pub enum VcsMutationKind {
2164    Commit,
2165    Push,
2166    Merge,
2167    Rebase,
2168    /// A kind not yet known to this version of the crate.
2169    Unknown(String),
2170}
2171
2172impl VcsMutationKind {
2173    pub fn as_str(&self) -> &str {
2174        match self {
2175            Self::Commit => "commit",
2176            Self::Push => "push",
2177            Self::Merge => "merge",
2178            Self::Rebase => "rebase",
2179            Self::Unknown(s) => s.as_str(),
2180        }
2181    }
2182}
2183
2184impl fmt::Display for VcsMutationKind {
2185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2186        f.write_str(self.as_str())
2187    }
2188}
2189
2190impl From<&str> for VcsMutationKind {
2191    fn from(s: &str) -> Self {
2192        match s {
2193            "commit" => Self::Commit,
2194            "push" => Self::Push,
2195            "merge" => Self::Merge,
2196            "rebase" => Self::Rebase,
2197            other => Self::Unknown(other.to_string()),
2198        }
2199    }
2200}
2201
2202impl Serialize for VcsMutationKind {
2203    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2204        serializer.serialize_str(self.as_str())
2205    }
2206}
2207
2208impl<'de> Deserialize<'de> for VcsMutationKind {
2209    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2210        let s = String::deserialize(deserializer)?;
2211        Ok(Self::from(s.as_str()))
2212    }
2213}
2214
2215/// `system/feedback_draft_queued` — a feedback draft was queued for submission.
2216#[derive(Debug, Clone, Serialize, Deserialize)]
2217pub struct FeedbackDraftQueuedMessage {
2218    pub draft_id: String,
2219    pub draft_type: String,
2220    pub title: String,
2221    pub details_preview: String,
2222    #[serde(default, skip_serializing_if = "Option::is_none")]
2223    pub uuid: Option<String>,
2224    #[serde(default, skip_serializing_if = "Option::is_none")]
2225    pub session_id: Option<String>,
2226    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
2227    pub extra: serde_json::Map<String, Value>,
2228}
2229
2230/// Display metadata for a tool-use block carried on the assistant wrapper.
2231#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2232pub struct ToolUseMeta {
2233    pub id: String,
2234    pub display_name: String,
2235    #[serde(default, skip_serializing_if = "Option::is_none")]
2236    pub server_display_name: Option<String>,
2237    #[serde(default, skip_serializing_if = "Option::is_none")]
2238    pub icon_url: Option<String>,
2239}
2240
2241/// Assistant message
2242#[derive(Debug, Clone, Serialize, Deserialize)]
2243pub struct AssistantMessage {
2244    pub message: AssistantMessageContent,
2245    #[serde(alias = "sessionId")]
2246    pub session_id: String,
2247    #[serde(skip_serializing_if = "Option::is_none")]
2248    pub uuid: Option<String>,
2249    #[serde(skip_serializing_if = "Option::is_none")]
2250    pub parent_tool_use_id: Option<String>,
2251    /// Anthropic API request id that produced this message (e.g. `req_...`).
2252    #[serde(skip_serializing_if = "Option::is_none")]
2253    pub request_id: Option<String>,
2254    /// Subagent type, when this assistant message was produced inside a
2255    /// `local_agent` subagent (e.g. `general-purpose`, `Explore`).
2256    #[serde(skip_serializing_if = "Option::is_none")]
2257    pub subagent_type: Option<String>,
2258    /// Short description of the subagent task, present alongside `subagent_type`.
2259    #[serde(skip_serializing_if = "Option::is_none")]
2260    pub task_description: Option<String>,
2261    #[serde(skip_serializing_if = "Option::is_none")]
2262    pub error: Option<AssistantErrorKind>,
2263    /// True when this message was truncated by an interrupt/abort before the
2264    /// stream completed — `stop_reason` was never received and the content
2265    /// may end mid-word. Absent on normally completed messages.
2266    #[serde(default, skip_serializing_if = "Option::is_none")]
2267    pub aborted: Option<bool>,
2268    /// True when this turn continued the preceding truncated assistant turn
2269    /// inside its trailing signed thinking block (max-output-tokens
2270    /// recovery). Histories replayed through the bridge must carry the flag
2271    /// back so the normalizer keeps the run's prefix on the wire.
2272    #[serde(default, skip_serializing_if = "Option::is_none")]
2273    pub resumed_from_incomplete_thinking: Option<bool>,
2274    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2275    pub supersedes: Vec<String>,
2276    #[serde(skip_serializing_if = "Option::is_none")]
2277    pub timestamp: Option<String>,
2278    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2279    pub tool_use_meta: Vec<ToolUseMeta>,
2280    #[serde(default, skip_serializing_if = "Option::is_none")]
2281    pub is_meta: Option<bool>,
2282    #[serde(default, skip_serializing_if = "Option::is_none")]
2283    pub is_virtual: Option<bool>,
2284    #[serde(default, skip_serializing_if = "Option::is_none")]
2285    pub is_api_error_message: Option<bool>,
2286    #[serde(skip_serializing_if = "Option::is_none")]
2287    pub api_error_status: Option<u16>,
2288    #[serde(skip_serializing_if = "Option::is_none")]
2289    pub api_error: Option<String>,
2290    #[serde(skip_serializing_if = "Option::is_none")]
2291    pub error_details: Option<String>,
2292    #[serde(skip_serializing_if = "Option::is_none")]
2293    pub advisor_model: Option<String>,
2294    #[serde(skip_serializing_if = "Option::is_none")]
2295    pub attribution_agent: Option<String>,
2296    #[serde(skip_serializing_if = "Option::is_none")]
2297    pub attribution_skill: Option<String>,
2298    #[serde(skip_serializing_if = "Option::is_none")]
2299    pub attribution_plugin: Option<String>,
2300    #[serde(skip_serializing_if = "Option::is_none")]
2301    pub attribution_mcp_server: Option<String>,
2302    #[serde(skip_serializing_if = "Option::is_none")]
2303    pub attribution_mcp_tool: Option<String>,
2304}
2305
2306/// Nested message content for assistant messages
2307#[derive(Debug, Clone, Serialize, Deserialize)]
2308pub struct AssistantMessageContent {
2309    pub id: String,
2310    /// The Anthropic API message type — always `"message"`.
2311    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
2312    pub message_type: Option<String>,
2313    pub role: MessageRole,
2314    pub model: String,
2315    pub content: Vec<ContentBlock>,
2316    #[serde(skip_serializing_if = "Option::is_none")]
2317    pub stop_reason: Option<StopReason>,
2318    #[serde(skip_serializing_if = "Option::is_none")]
2319    pub stop_sequence: Option<String>,
2320    #[serde(skip_serializing_if = "Option::is_none")]
2321    pub usage: Option<AssistantUsage>,
2322    /// Details about why generation stopped
2323    #[serde(skip_serializing_if = "Option::is_none")]
2324    pub stop_details: Option<Value>,
2325    /// Context management metadata
2326    #[serde(skip_serializing_if = "Option::is_none")]
2327    pub context_management: Option<Value>,
2328}
2329
2330/// Usage information for assistant messages
2331#[derive(Debug, Clone, Serialize, Deserialize)]
2332pub struct AssistantUsage {
2333    /// Number of input tokens
2334    #[serde(default)]
2335    pub input_tokens: u32,
2336
2337    /// Number of output tokens
2338    #[serde(default)]
2339    pub output_tokens: u32,
2340
2341    /// Tokens used to create cache
2342    #[serde(default)]
2343    pub cache_creation_input_tokens: u32,
2344
2345    /// Tokens read from cache
2346    #[serde(default)]
2347    pub cache_read_input_tokens: u32,
2348
2349    /// Service tier used (e.g., "standard")
2350    #[serde(skip_serializing_if = "Option::is_none")]
2351    pub service_tier: Option<String>,
2352
2353    /// Detailed cache creation breakdown
2354    #[serde(skip_serializing_if = "Option::is_none")]
2355    pub cache_creation: Option<CacheCreationDetails>,
2356
2357    /// Inference geography (e.g., "not_available")
2358    #[serde(skip_serializing_if = "Option::is_none")]
2359    pub inference_geo: Option<String>,
2360}
2361
2362/// Detailed cache creation information
2363#[derive(Debug, Clone, Serialize, Deserialize)]
2364pub struct CacheCreationDetails {
2365    /// Ephemeral 1-hour input tokens
2366    #[serde(default)]
2367    pub ephemeral_1h_input_tokens: u32,
2368
2369    /// Ephemeral 5-minute input tokens
2370    #[serde(default)]
2371    pub ephemeral_5m_input_tokens: u32,
2372}
2373
2374#[cfg(test)]
2375mod tests {
2376    use crate::io::ClaudeOutput;
2377
2378    #[test]
2379    fn test_subagent_usage_rollup_accumulates_task_results() {
2380        use super::SubagentUsageRollup;
2381
2382        let mut rollup = SubagentUsageRollup::default();
2383
2384        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}}"#;
2385        let output: ClaudeOutput = serde_json::from_str(task_result).unwrap();
2386        assert!(rollup.observe(&output));
2387        assert_eq!(rollup.subagent_tokens, 10201);
2388        assert_eq!(rollup.agent_count, 1);
2389        assert_eq!(rollup.tool_uses, 3);
2390        assert_eq!(rollup.duration_ms, 1853);
2391
2392        // Replayed frame with the same agentId is counted once.
2393        assert!(!rollup.observe(&output));
2394        assert_eq!(rollup.agent_count, 1);
2395        assert_eq!(rollup.subagent_tokens, 10201);
2396
2397        // A second agent accumulates.
2398        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}}"#;
2399        let output: ClaudeOutput = serde_json::from_str(second).unwrap();
2400        assert!(rollup.observe(&output));
2401        assert_eq!(rollup.agent_count, 2);
2402        assert_eq!(rollup.subagent_tokens, 10701);
2403    }
2404
2405    #[test]
2406    fn test_subagent_usage_rollup_ignores_non_task_results() {
2407        use super::SubagentUsageRollup;
2408
2409        let mut rollup = SubagentUsageRollup::default();
2410
2411        // A ToolSearch tool_use_result parses as an all-None SubagentResult;
2412        // it must not count as a subagent.
2413        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}}"#;
2414        let output: ClaudeOutput = serde_json::from_str(tool_search).unwrap();
2415        assert!(!rollup.observe(&output));
2416
2417        // Plain user message without tool_use_result.
2418        let plain = r#"{"type":"user","message":{"role":"user","content":[]},"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2419        let output: ClaudeOutput = serde_json::from_str(plain).unwrap();
2420        assert!(!rollup.observe(&output));
2421
2422        // Non-user frames are ignored.
2423        let system = r#"{"type":"system","subtype":"status","status":null,"session_id":"7fbc568e-2bd6-45aa-b217-a1cf80004ba1"}"#;
2424        let output: ClaudeOutput = serde_json::from_str(system).unwrap();
2425        assert!(!rollup.observe(&output));
2426
2427        assert_eq!(rollup, SubagentUsageRollup::default());
2428    }
2429
2430    #[test]
2431    fn test_subagent_usage_rollup_over_captured_session() {
2432        use super::SubagentUsageRollup;
2433
2434        let mut rollup = SubagentUsageRollup::default();
2435        let fixture =
2436            include_str!("../../test_cases/subagent_sessions/general_purpose_compute.jsonl");
2437        for line in fixture.lines().filter(|l| !l.trim().is_empty()) {
2438            if let Ok(output) = serde_json::from_str::<ClaudeOutput>(line) {
2439                rollup.observe(&output);
2440            }
2441        }
2442        assert_eq!(rollup.agent_count, 1);
2443        assert_eq!(rollup.subagent_tokens, 10201);
2444    }
2445
2446    #[test]
2447    fn test_system_message_init() {
2448        let json = r#"{
2449            "type": "system",
2450            "subtype": "init",
2451            "session_id": "test-session-123",
2452            "cwd": "/home/user/project",
2453            "model": "claude-sonnet-4",
2454            "tools": ["Bash", "Read", "Write"],
2455            "mcp_servers": [],
2456            "slash_commands": ["compact", "cost", "review"],
2457            "agents": ["Bash", "Explore", "Plan"],
2458            "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
2459            "skills": [],
2460            "claude_code_version": "2.1.15",
2461            "apiKeySource": "none",
2462            "output_style": "default",
2463            "permissionMode": "default"
2464        }"#;
2465
2466        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2467        if let ClaudeOutput::System(sys) = output {
2468            assert!(sys.is_init());
2469            assert!(!sys.is_status());
2470            assert!(!sys.is_compact_boundary());
2471
2472            let init = sys.as_init().expect("Should parse as init");
2473            assert_eq!(init.session_id, "test-session-123");
2474            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2475            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2476            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2477            assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
2478            assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
2479            assert_eq!(init.plugins.len(), 1);
2480            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2481            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2482            assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
2483            assert_eq!(init.output_style, Some(super::OutputStyle::Default));
2484            assert_eq!(
2485                init.permission_mode,
2486                Some(super::InitPermissionMode::Default)
2487            );
2488        } else {
2489            panic!("Expected System message");
2490        }
2491    }
2492
2493    #[test]
2494    fn test_system_message_init_from_real_capture() {
2495        let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
2496        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2497        if let ClaudeOutput::System(sys) = output {
2498            let init = sys.as_init().expect("Should parse real init capture");
2499            assert_eq!(init.slash_commands.len(), 8);
2500            assert!(init.slash_commands.contains(&"compact".to_string()));
2501            assert!(init.slash_commands.contains(&"review".to_string()));
2502            assert_eq!(init.agents.len(), 5);
2503            assert!(init.agents.contains(&"Bash".to_string()));
2504            assert!(init.agents.contains(&"Explore".to_string()));
2505            assert_eq!(init.plugins.len(), 1);
2506            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
2507            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
2508        } else {
2509            panic!("Expected System message");
2510        }
2511    }
2512
2513    #[test]
2514    fn test_system_message_status() {
2515        let json = r#"{
2516            "type": "system",
2517            "subtype": "status",
2518            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2519            "status": "compacting",
2520            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
2521        }"#;
2522
2523        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2524        if let ClaudeOutput::System(sys) = output {
2525            assert!(sys.is_status());
2526            assert!(!sys.is_init());
2527
2528            let status = sys.as_status().expect("Should parse as status");
2529            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2530            assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
2531            assert_eq!(
2532                status.uuid,
2533                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
2534            );
2535        } else {
2536            panic!("Expected System message");
2537        }
2538    }
2539
2540    #[test]
2541    fn test_system_message_status_null() {
2542        let json = r#"{
2543            "type": "system",
2544            "subtype": "status",
2545            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2546            "status": null,
2547            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
2548        }"#;
2549
2550        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2551        if let ClaudeOutput::System(sys) = output {
2552            let status = sys.as_status().expect("Should parse as status");
2553            assert_eq!(status.status, None);
2554        } else {
2555            panic!("Expected System message");
2556        }
2557    }
2558
2559    #[test]
2560    fn test_system_message_task_started() {
2561        let json = r#"{
2562            "type": "system",
2563            "subtype": "task_started",
2564            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
2565            "task_id": "b6daf3f",
2566            "task_type": "local_bash",
2567            "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
2568            "description": "Wait for CI on PR #12",
2569            "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
2570        }"#;
2571
2572        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2573        if let ClaudeOutput::System(sys) = output {
2574            assert!(sys.is_task_started());
2575            assert!(!sys.is_task_progress());
2576            assert!(!sys.is_task_notification());
2577
2578            let task = sys.as_task_started().expect("Should parse as task_started");
2579            assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
2580            assert_eq!(task.task_id, "b6daf3f");
2581            assert_eq!(task.task_type, Some(super::TaskType::LocalBash));
2582            assert_eq!(
2583                task.tool_use_id.as_deref(),
2584                Some("toolu_011rfSTFumpJZdCCfzeD7jaS")
2585            );
2586            assert_eq!(task.description, "Wait for CI on PR #12");
2587        } else {
2588            panic!("Expected System message");
2589        }
2590    }
2591
2592    #[test]
2593    fn test_system_message_task_started_agent() {
2594        let json = r#"{
2595            "type": "system",
2596            "subtype": "task_started",
2597            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2598            "task_id": "a4a7e0906e5fc64cc",
2599            "task_type": "local_agent",
2600            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2601            "description": "Explore Scene/ArrayScene duplication",
2602            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2603        }"#;
2604
2605        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2606        if let ClaudeOutput::System(sys) = output {
2607            let task = sys.as_task_started().expect("Should parse as task_started");
2608            assert_eq!(task.task_type, Some(super::TaskType::LocalAgent));
2609            assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
2610        } else {
2611            panic!("Expected System message");
2612        }
2613    }
2614
2615    #[test]
2616    fn test_system_message_task_progress() {
2617        let json = r#"{
2618            "type": "system",
2619            "subtype": "task_progress",
2620            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2621            "task_id": "a4a7e0906e5fc64cc",
2622            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
2623            "description": "Reading src/jplephem/chebyshev.rs",
2624            "last_tool_name": "Read",
2625            "usage": {
2626                "duration_ms": 13996,
2627                "tool_uses": 9,
2628                "total_tokens": 38779
2629            },
2630            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
2631        }"#;
2632
2633        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2634        if let ClaudeOutput::System(sys) = output {
2635            assert!(sys.is_task_progress());
2636            assert!(!sys.is_task_started());
2637
2638            let progress = sys
2639                .as_task_progress()
2640                .expect("Should parse as task_progress");
2641            assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
2642            assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
2643            assert_eq!(progress.last_tool_name.as_deref(), Some("Read"));
2644            assert_eq!(progress.usage.duration_ms, 13996);
2645            assert_eq!(progress.usage.tool_uses, 9);
2646            assert_eq!(progress.usage.total_tokens, 38779);
2647        } else {
2648            panic!("Expected System message");
2649        }
2650    }
2651
2652    #[test]
2653    fn test_system_message_task_notification_completed() {
2654        let json = r#"{
2655            "type": "system",
2656            "subtype": "task_notification",
2657            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
2658            "task_id": "a0ba761e9dc9c316f",
2659            "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
2660            "status": "completed",
2661            "summary": "Agent \"Write Hipparcos data source doc\" completed",
2662            "output_file": "",
2663            "usage": {
2664                "duration_ms": 172300,
2665                "tool_uses": 11,
2666                "total_tokens": 42005
2667            },
2668            "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
2669        }"#;
2670
2671        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2672        if let ClaudeOutput::System(sys) = output {
2673            assert!(sys.is_task_notification());
2674
2675            let notif = sys
2676                .as_task_notification()
2677                .expect("Should parse as task_notification");
2678            assert_eq!(notif.status, super::TaskStatus::Completed);
2679            assert_eq!(
2680                notif.summary,
2681                "Agent \"Write Hipparcos data source doc\" completed"
2682            );
2683            assert_eq!(notif.output_file, Some("".to_string()));
2684            assert_eq!(
2685                notif.tool_use_id,
2686                Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
2687            );
2688            let usage = notif.usage.expect("Should have usage");
2689            assert_eq!(usage.duration_ms, 172300);
2690            assert_eq!(usage.tool_uses, 11);
2691            assert_eq!(usage.total_tokens, 42005);
2692        } else {
2693            panic!("Expected System message");
2694        }
2695    }
2696
2697    #[test]
2698    fn test_system_message_task_notification_failed_no_usage() {
2699        let json = r#"{
2700            "type": "system",
2701            "subtype": "task_notification",
2702            "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
2703            "task_id": "b98f6a3",
2704            "status": "failed",
2705            "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
2706            "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
2707        }"#;
2708
2709        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2710        if let ClaudeOutput::System(sys) = output {
2711            let notif = sys
2712                .as_task_notification()
2713                .expect("Should parse as task_notification");
2714            assert_eq!(notif.status, super::TaskStatus::Failed);
2715            assert!(notif.tool_use_id.is_none());
2716            assert!(notif.usage.is_none());
2717            assert_eq!(
2718                notif.output_file,
2719                Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
2720            );
2721        } else {
2722            panic!("Expected System message");
2723        }
2724    }
2725
2726    /// Task system messages survive a `to_value` → `from_value` round-trip
2727    /// with their typed accessors still resolving. Mirrors the proxy/relay
2728    /// path where output is reparsed from a `serde_json::Value` rather than
2729    /// straight from the CLI's stdout, so a silently dropped or renamed field
2730    /// surfaces here instead of as a `None` downstream.
2731    #[test]
2732    fn test_task_messages_roundtrip_through_value() {
2733        let cases = [
2734            r#"{"type":"system","subtype":"task_started","session_id":"s1",
2735                "task_id":"t1","task_type":"local_bash","tool_use_id":"tu1",
2736                "description":"Sleep 3s","uuid":"u1"}"#,
2737            r#"{"type":"system","subtype":"task_progress","session_id":"s1",
2738                "task_id":"t1","tool_use_id":"tu1","description":"Running ls",
2739                "last_tool_name":"Bash",
2740                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
2741                "uuid":"u2"}"#,
2742            r#"{"type":"system","subtype":"task_notification","session_id":"s1",
2743                "task_id":"t1","tool_use_id":"tu1","status":"completed",
2744                "summary":"done","output_file":"",
2745                "usage":{"duration_ms":100,"tool_uses":1,"total_tokens":500},
2746                "uuid":"u3"}"#,
2747        ];
2748
2749        for json in cases {
2750            let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2751            let value = serde_json::to_value(&output).unwrap();
2752            let reparsed: ClaudeOutput = serde_json::from_value(value).unwrap();
2753
2754            let ClaudeOutput::System(sys) = reparsed else {
2755                panic!("Expected System variant after round-trip");
2756            };
2757
2758            match sys.subtype {
2759                super::SystemSubtype::TaskStarted => {
2760                    assert!(
2761                        sys.as_task_started().is_some(),
2762                        "as_task_started failed after round-trip"
2763                    );
2764                }
2765                super::SystemSubtype::TaskProgress => {
2766                    assert!(
2767                        sys.as_task_progress().is_some(),
2768                        "as_task_progress failed after round-trip"
2769                    );
2770                }
2771                super::SystemSubtype::TaskNotification => {
2772                    assert!(
2773                        sys.as_task_notification().is_some(),
2774                        "as_task_notification failed after round-trip"
2775                    );
2776                }
2777                other => panic!("unexpected subtype after round-trip: {other:?}"),
2778            }
2779        }
2780    }
2781
2782    #[test]
2783    fn test_system_message_compact_boundary() {
2784        let json = r#"{
2785            "type": "system",
2786            "subtype": "compact_boundary",
2787            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2788            "compact_metadata": {
2789                "pre_tokens": 155285,
2790                "trigger": "auto"
2791            },
2792            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
2793        }"#;
2794
2795        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2796        if let ClaudeOutput::System(sys) = output {
2797            assert!(sys.is_compact_boundary());
2798            assert!(!sys.is_init());
2799            assert!(!sys.is_status());
2800
2801            let compact = sys
2802                .as_compact_boundary()
2803                .expect("Should parse as compact_boundary");
2804            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2805            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
2806            assert_eq!(
2807                compact.compact_metadata.trigger,
2808                super::CompactionTrigger::Auto
2809            );
2810            // Per-compaction stats are optional and absent here.
2811            assert!(compact.summary.is_none());
2812            assert!(compact.leaf_message_count.is_none());
2813            assert!(compact.duration_ms.is_none());
2814        } else {
2815            panic!("Expected System message");
2816        }
2817    }
2818
2819    #[test]
2820    fn test_compact_boundary_with_summary_stats() {
2821        // Canonical keys.
2822        let json = r#"{
2823            "type": "system",
2824            "subtype": "compact_boundary",
2825            "session_id": "s1",
2826            "compact_metadata": { "pre_tokens": 1000, "trigger": "manual" },
2827            "summary": "Summarized the earlier exploration.",
2828            "leaf_message_count": 42,
2829            "duration_ms": 1234,
2830            "uuid": "u1"
2831        }"#;
2832        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2833        let ClaudeOutput::System(sys) = output else {
2834            panic!("Expected System message");
2835        };
2836        let compact = sys.as_compact_boundary().expect("compact_boundary");
2837        assert_eq!(
2838            compact.summary.as_deref(),
2839            Some("Summarized the earlier exploration.")
2840        );
2841        assert_eq!(compact.leaf_message_count, Some(42));
2842        assert_eq!(compact.duration_ms, Some(1234));
2843
2844        // Alternate wire keys (`content` for summary, `message_count` for count)
2845        // deserialize into the same fields.
2846        let json_alt = r#"{
2847            "type": "system",
2848            "subtype": "compact_boundary",
2849            "session_id": "s2",
2850            "compact_metadata": { "pre_tokens": 2000, "trigger": "auto" },
2851            "content": "alt-key summary",
2852            "message_count": 7
2853        }"#;
2854        let output: ClaudeOutput = serde_json::from_str(json_alt).unwrap();
2855        let ClaudeOutput::System(sys) = output else {
2856            panic!("Expected System message");
2857        };
2858        let compact = sys.as_compact_boundary().expect("compact_boundary");
2859        assert_eq!(compact.summary.as_deref(), Some("alt-key summary"));
2860        assert_eq!(compact.leaf_message_count, Some(7));
2861    }
2862
2863    #[test]
2864    fn test_init_message_with_new_fields() {
2865        let json = r#"{
2866            "type": "system",
2867            "subtype": "init",
2868            "session_id": "test-session",
2869            "cwd": "/home/user",
2870            "model": "claude-opus-4-7",
2871            "tools": ["Bash"],
2872            "mcp_servers": [],
2873            "permissionMode": "default",
2874            "apiKeySource": "none",
2875            "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
2876            "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
2877            "fast_mode_state": "off",
2878            "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
2879            "claude_code_version": "2.1.117"
2880        }"#;
2881
2882        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2883        if let ClaudeOutput::System(sys) = output {
2884            let init = sys.as_init().expect("Should parse as init");
2885            assert_eq!(
2886                init.uuid.as_deref(),
2887                Some("44841a0d-182d-493a-86b5-79800d3d9665")
2888            );
2889            assert!(init.memory_paths.is_some());
2890            assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
2891            assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
2892            assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
2893        } else {
2894            panic!("Expected System message");
2895        }
2896    }
2897
2898    #[test]
2899    fn test_assistant_message_with_new_fields() {
2900        let json = r#"{
2901            "type": "assistant",
2902            "message": {
2903                "id": "msg_1",
2904                "type": "message",
2905                "role": "assistant",
2906                "model": "claude-opus-4-7",
2907                "content": [{"type": "text", "text": "Hello"}],
2908                "stop_reason": "end_turn",
2909                "stop_details": null,
2910                "context_management": null,
2911                "usage": {
2912                    "input_tokens": 100,
2913                    "output_tokens": 10,
2914                    "cache_creation_input_tokens": 50,
2915                    "cache_read_input_tokens": 0,
2916                    "service_tier": "standard",
2917                    "inference_geo": "not_available"
2918                }
2919            },
2920            "session_id": "abc",
2921            "uuid": "msg-uuid-123"
2922        }"#;
2923
2924        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2925        if let ClaudeOutput::Assistant(asst) = output {
2926            assert_eq!(asst.message.stop_details, None);
2927            assert_eq!(asst.message.context_management, None);
2928            let usage = asst.message.usage.unwrap();
2929            assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
2930        } else {
2931            panic!("Expected Assistant message");
2932        }
2933    }
2934
2935    #[test]
2936    fn test_user_message_with_new_fields() {
2937        let json = r#"{
2938            "type": "user",
2939            "message": {
2940                "role": "user",
2941                "content": [{"type": "text", "text": "Hello"}]
2942            },
2943            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
2944            "parent_tool_use_id": "toolu_123",
2945            "uuid": "user-msg-456"
2946        }"#;
2947
2948        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2949        if let ClaudeOutput::User(user) = output {
2950            assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
2951            assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
2952        } else {
2953            panic!("Expected User message");
2954        }
2955    }
2956
2957    /// Real wire payload captured from the CLI after answering an
2958    /// AskUserQuestion via the permission control protocol. The top-level
2959    /// `tool_use_result` and `timestamp` fields must round-trip without loss —
2960    /// proxies using this crate to relay messages to a viewer rely on those
2961    /// fields being preserved (the viewer reads `tool_use_result.answers`).
2962    #[test]
2963    fn test_user_message_preserves_tool_use_result_and_timestamp() {
2964        let json = r#"{
2965            "type":"user",
2966            "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"}]},
2967            "parent_tool_use_id":null,
2968            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
2969            "uuid":"8ef6e997-a849-4d15-bed3-2837c3d3f4cd",
2970            "timestamp":"2026-05-12T23:12:04.121Z",
2971            "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"}}
2972        }"#;
2973
2974        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2975        let user = match output {
2976            ClaudeOutput::User(u) => u,
2977            other => panic!("Expected User message, got {:?}", other.message_type()),
2978        };
2979
2980        assert_eq!(user.timestamp.as_deref(), Some("2026-05-12T23:12:04.121Z"));
2981        let raw = user
2982            .tool_use_result
2983            .as_ref()
2984            .expect("tool_use_result must be captured");
2985        assert_eq!(raw["answers"]["Color"], "Blue");
2986        assert_eq!(raw["questions"][0]["header"], "Color");
2987
2988        // Round-trip: re-serialize and confirm tool_use_result + timestamp
2989        // survive — the bug we're guarding against is that the proxy silently
2990        // drops these fields when relaying user messages.
2991        let reser: serde_json::Value = serde_json::to_value(&user).unwrap();
2992        assert_eq!(reser["timestamp"], "2026-05-12T23:12:04.121Z");
2993        assert_eq!(reser["tool_use_result"]["answers"]["Color"], "Blue");
2994        assert_eq!(
2995            reser["tool_use_result"]["questions"][0]["question"],
2996            "Which color do you prefer?"
2997        );
2998
2999        // Typed accessor: AskUserQuestionInput has the same shape as the
3000        // AskUserQuestion tool_use_result.
3001        let typed: crate::AskUserQuestionInput = user
3002            .tool_use_result_as::<crate::AskUserQuestionInput>()
3003            .expect("tool_use_result present")
3004            .expect("AskUserQuestionInput parses");
3005        assert_eq!(typed.questions.len(), 1);
3006        assert_eq!(typed.questions[0].header, "Color");
3007        let answers = typed.answers.expect("answers populated");
3008        assert_eq!(answers.get("Color").map(String::as_str), Some("Blue"));
3009    }
3010
3011    /// User messages without `tool_use_result` / `timestamp` must still
3012    /// deserialize fine and serialize back without spuriously emitting nulls.
3013    #[test]
3014    fn test_user_message_without_tool_use_result_omits_field() {
3015        let json = r#"{
3016            "type":"user",
3017            "message":{"role":"user","content":[{"type":"text","text":"hello"}]},
3018            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d"
3019        }"#;
3020
3021        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3022        let user = match output {
3023            ClaudeOutput::User(u) => u,
3024            _ => panic!("Expected User message"),
3025        };
3026        assert!(user.tool_use_result.is_none());
3027        assert!(user.timestamp.is_none());
3028
3029        let reser = serde_json::to_value(&user).unwrap();
3030        assert!(reser.get("tool_use_result").is_none());
3031        assert!(reser.get("timestamp").is_none());
3032    }
3033
3034    /// A `Task` tool result must expose subagent token / timing / tool-use
3035    /// accounting through the typed [`UserMessage::subagent_result`] accessor,
3036    /// including the nested per-model `usage` breakdown and `toolStats`.
3037    #[test]
3038    fn test_subagent_result_exposes_token_accounting() {
3039        let json = r#"{
3040            "type":"user",
3041            "message":{"role":"user","content":[{"tool_use_id":"toolu_01","type":"tool_result","content":[{"type":"text","text":"21"}]}]},
3042            "session_id":"d3fc5942-75e5-4aa1-a87d-b9484a176541",
3043            "tool_use_result":{
3044                "status":"completed",
3045                "prompt":"Count the .rs files.",
3046                "agentId":"ac4f0276e9d4b6232",
3047                "agentType":"Explore",
3048                "content":[{"type":"text","text":"21"}],
3049                "resolvedModel":"claude-haiku-4-5-20251001",
3050                "totalDurationMs":6869,
3051                "totalTokens":7834,
3052                "totalToolUseCount":1,
3053                "usage":{"input_tokens":6,"cache_creation_input_tokens":125,"cache_read_input_tokens":7699,"output_tokens":4,"service_tier":"standard"},
3054                "toolStats":{"readCount":0,"searchCount":0,"bashCount":1,"editFileCount":0,"linesAdded":0,"linesRemoved":0,"otherToolCount":0}
3055            }
3056        }"#;
3057
3058        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3059        let user = match output {
3060            ClaudeOutput::User(u) => u,
3061            _ => panic!("Expected User message"),
3062        };
3063
3064        let result = user.subagent_result().expect("subagent result parses");
3065        assert_eq!(result.agent_type.as_deref(), Some("Explore"));
3066        assert_eq!(
3067            result.resolved_model.as_deref(),
3068            Some("claude-haiku-4-5-20251001")
3069        );
3070        assert_eq!(result.total_tokens, Some(7834));
3071        assert_eq!(result.total_duration_ms, Some(6869));
3072        assert_eq!(result.total_tool_use_count, Some(1));
3073
3074        let usage = result.usage.expect("nested usage present");
3075        assert_eq!(usage.input_tokens, 6);
3076        assert_eq!(usage.cache_read_input_tokens, 7699);
3077
3078        let stats = result.tool_stats.expect("toolStats present");
3079        assert_eq!(stats.bash_count, 1);
3080    }
3081
3082    /// `tool_use_result` shapes that aren't subagent runs (e.g. AskUserQuestion)
3083    /// parse leniently into the all-`Option` [`SubagentResult`] with empty
3084    /// accounting rather than failing, so callers can probe without panicking.
3085    #[test]
3086    fn test_subagent_result_absent_for_non_task_result() {
3087        let json = r#"{
3088            "type":"user",
3089            "message":{"role":"user","content":[{"type":"text","text":"hi"}]},
3090            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3091            "tool_use_result":{"questions":[],"answers":{"Color":"Blue"}}
3092        }"#;
3093
3094        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3095        let user = match output {
3096            ClaudeOutput::User(u) => u,
3097            _ => panic!("Expected User message"),
3098        };
3099
3100        let result = user.subagent_result().expect("lenient parse");
3101        assert_eq!(result.total_tokens, None);
3102        assert_eq!(result.agent_type, None);
3103    }
3104
3105    #[test]
3106    fn test_init_fast_mode_reason_and_mcp_server_errors_fully_wrapped() {
3107        use serde_json::Value;
3108
3109        let raw: Value = serde_json::from_str(
3110            r#"{
3111            "type":"system","subtype":"init","session_id":"s1","uuid":"u1",
3112            "fast_mode_state":"off",
3113            "fast_mode_disabled_reason":"not_first_party",
3114            "mcp_server_errors":[{"name":"broken","type":"invalid_config","message":"url entry with no type"}]
3115        }"#,
3116        )
3117        .unwrap();
3118        crate::io::assert_fully_wrapped(&raw);
3119
3120        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3121        let ClaudeOutput::System(sys) = output else {
3122            panic!("expected System");
3123        };
3124        let init = sys.as_init().expect("parses as init");
3125        assert_eq!(
3126            init.fast_mode_disabled_reason,
3127            Some(crate::FastModeDisabledReason::NotFirstParty)
3128        );
3129        let errs = init.mcp_server_errors.unwrap();
3130        assert_eq!(errs.len(), 1);
3131        assert_eq!(errs[0].name, "broken");
3132        assert_eq!(errs[0].error_type, "invalid_config");
3133    }
3134
3135    #[test]
3136    fn test_code_change_published_fully_wrapped() {
3137        use super::{KnownSystemEvent, SystemSubtype};
3138        use serde_json::Value;
3139
3140        let raw: Value = serde_json::from_str(
3141            r#"{
3142            "type":"system","subtype":"code_change_published",
3143            "provider":"github","url":"https://github.com/owner/repo/pull/42",
3144            "repo":"owner/repo","identifier":"42",
3145            "uuid":"u1","session_id":"s1"
3146        }"#,
3147        )
3148        .unwrap();
3149        crate::io::assert_fully_wrapped(&raw);
3150
3151        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3152        let ClaudeOutput::System(sys) = output else {
3153            panic!("expected System");
3154        };
3155        assert_eq!(sys.subtype, SystemSubtype::CodeChangePublished);
3156        let Some(KnownSystemEvent::CodeChangePublished(msg)) = sys.as_known_system_event() else {
3157            panic!("expected CodeChangePublished event");
3158        };
3159        assert_eq!(msg.provider, "github");
3160        assert_eq!(msg.repo, "owner/repo");
3161        assert_eq!(msg.identifier, "42");
3162
3163        assert!(sys.is_code_change_published());
3164        assert!(!sys.is_vcs_state_changed());
3165        let direct = sys.as_code_change_published().expect("direct accessor");
3166        assert_eq!(direct.url, "https://github.com/owner/repo/pull/42");
3167        assert!(sys.as_vcs_state_changed().is_none());
3168    }
3169
3170    #[test]
3171    fn test_feedback_draft_queued_fully_wrapped() {
3172        use super::{KnownSystemEvent, SystemSubtype};
3173        use serde_json::Value;
3174
3175        let raw: Value = serde_json::from_str(
3176            r#"{
3177            "type":"system","subtype":"feedback_draft_queued",
3178            "draft_id":"draft-1","draft_type":"bug_report",
3179            "title":"Tool output was truncated",
3180            "details_preview":"The last command omitted its final lines",
3181            "uuid":"u1","session_id":"s1","future_field":"preserved"
3182        }"#,
3183        )
3184        .unwrap();
3185        crate::io::assert_fully_wrapped(&raw);
3186
3187        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3188        let ClaudeOutput::System(sys) = output else {
3189            panic!("expected System");
3190        };
3191        assert_eq!(sys.subtype, SystemSubtype::FeedbackDraftQueued);
3192        assert!(sys.is_feedback_draft_queued());
3193        assert!(!sys.is_vcs_state_changed());
3194
3195        let direct = sys
3196            .as_feedback_draft_queued()
3197            .expect("direct typed accessor");
3198        assert_eq!(direct.draft_id, "draft-1");
3199        assert_eq!(direct.draft_type, "bug_report");
3200        assert_eq!(direct.extra["future_field"], "preserved");
3201
3202        let Some(KnownSystemEvent::FeedbackDraftQueued(known)) = sys.as_known_system_event() else {
3203            panic!("expected FeedbackDraftQueued event");
3204        };
3205        assert_eq!(known.title, "Tool output was truncated");
3206        assert_eq!(
3207            sys.typed_value().expect("typed value")["future_field"],
3208            "preserved"
3209        );
3210    }
3211
3212    #[test]
3213    fn test_vcs_state_changed_fully_wrapped() {
3214        use super::{KnownSystemEvent, VcsMutationKind};
3215        use serde_json::Value;
3216
3217        for kind in ["commit", "push", "merge", "rebase"] {
3218            let raw: Value = serde_json::from_str(&format!(
3219                r#"{{"type":"system","subtype":"vcs_state_changed","kind":"{}","cwd":"/repo","uuid":"u1","session_id":"s1"}}"#,
3220                kind
3221            ))
3222            .unwrap();
3223            crate::io::assert_fully_wrapped(&raw);
3224
3225            let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3226            let ClaudeOutput::System(sys) = output else {
3227                panic!("expected System");
3228            };
3229            let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3230                panic!("expected VcsStateChanged event");
3231            };
3232            assert_eq!(msg.kind.as_str(), kind);
3233            assert!(!matches!(msg.kind, VcsMutationKind::Unknown(_)));
3234        }
3235
3236        // Unknown kinds are valid per the wire contract.
3237        let raw: Value = serde_json::from_str(
3238            r#"{"type":"system","subtype":"vcs_state_changed","kind":"tag","cwd":"/repo","uuid":"u2","session_id":"s2"}"#,
3239        )
3240        .unwrap();
3241        crate::io::assert_fully_wrapped(&raw);
3242        let output: ClaudeOutput = serde_json::from_value(raw).unwrap();
3243        let ClaudeOutput::System(sys) = output else {
3244            panic!("expected System");
3245        };
3246        let Some(KnownSystemEvent::VcsStateChanged(msg)) = sys.as_known_system_event() else {
3247            panic!("expected VcsStateChanged event");
3248        };
3249        assert_eq!(msg.kind, VcsMutationKind::Unknown("tag".to_string()));
3250
3251        assert!(sys.is_vcs_state_changed());
3252        let direct = sys.as_vcs_state_changed().expect("direct accessor");
3253        assert_eq!(direct.cwd, "/repo");
3254        assert!(sys.as_code_change_published().is_none());
3255    }
3256
3257    #[test]
3258    fn test_assistant_aborted_and_resume_flags_roundtrip() {
3259        let json = r#"{
3260            "type":"assistant",
3261            "message":{"id":"msg_1","role":"assistant","model":"claude-3","content":[{"type":"text","text":"partial"}]},
3262            "session_id":"s1",
3263            "aborted":true,
3264            "resumed_from_incomplete_thinking":true
3265        }"#;
3266        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3267        let ClaudeOutput::Assistant(msg) = &output else {
3268            panic!("expected Assistant");
3269        };
3270        assert_eq!(msg.aborted, Some(true));
3271        assert_eq!(msg.resumed_from_incomplete_thinking, Some(true));
3272        let reserialized = serde_json::to_string(&output).unwrap();
3273        assert!(reserialized.contains("\"aborted\":true"));
3274        assert!(reserialized.contains("\"resumed_from_incomplete_thinking\":true"));
3275
3276        // Absent flags stay absent on the wire.
3277        let json = r#"{
3278            "type":"assistant",
3279            "message":{"id":"msg_2","role":"assistant","model":"claude-3","content":[]},
3280            "session_id":"s2"
3281        }"#;
3282        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3283        let reserialized = serde_json::to_string(&output).unwrap();
3284        assert!(!reserialized.contains("aborted"));
3285        assert!(!reserialized.contains("resumed_from_incomplete_thinking"));
3286    }
3287
3288    #[test]
3289    fn test_user_tool_result_meta_roundtrip() {
3290        let json = r#"{
3291            "type":"user",
3292            "message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"denied"}]},
3293            "session_id":"622ae0c3-3d50-4fa7-9ee0-69d691238c6d",
3294            "tool_result_meta":[
3295                {"id":"toolu_1","non_execution_kind":"user-rejected","user_feedback":"use the staging db"},
3296                {"id":"toolu_2","non_execution_kind":"permission-rule"}
3297            ]
3298        }"#;
3299        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
3300        let ClaudeOutput::User(user) = &output else {
3301            panic!("expected User");
3302        };
3303        let meta = user.tool_result_meta.as_ref().unwrap();
3304        assert_eq!(meta.len(), 2);
3305        assert_eq!(meta[0].non_execution_kind, "user-rejected");
3306        assert_eq!(meta[0].user_feedback.as_deref(), Some("use the staging db"));
3307        assert_eq!(meta[1].user_feedback, None);
3308
3309        let reserialized = serde_json::to_string(&output).unwrap();
3310        assert!(reserialized.contains("\"non_execution_kind\":\"user-rejected\""));
3311        assert!(!reserialized.contains("\"user_feedback\":null"));
3312    }
3313
3314    /// CLI 2.1.222 added `scope` to `system/model_refusal_fallback`:
3315    /// "session" (main-thread swap, also the meaning when absent on older
3316    /// CLIs) vs "local" (subagent/side-question fallback only).
3317    #[test]
3318    fn model_refusal_fallback_scope_roundtrips_and_defaults() {
3319        use super::{ModelRefusalFallbackMessage, RefusalFallbackScope};
3320        let with_scope = serde_json::json!({
3321            "trigger": "refusal",
3322            "direction": "retry",
3323            "scope": "local",
3324            "original_model": "claude-fable-5",
3325            "fallback_model": "claude-opus-5",
3326            "request_id": null,
3327            "content": "Refused; retried on fallback model.",
3328            "uuid": "u1",
3329            "session_id": "s1"
3330        });
3331        let msg: ModelRefusalFallbackMessage = serde_json::from_value(with_scope.clone()).unwrap();
3332        assert_eq!(msg.scope, Some(RefusalFallbackScope::Local));
3333        assert_eq!(serde_json::to_value(&msg).unwrap(), with_scope);
3334
3335        // Older CLIs omit scope — absent, not null, and treated as session
3336        // by consumers per the wire docs.
3337        let mut without = with_scope.clone();
3338        without.as_object_mut().unwrap().remove("scope");
3339        let msg: ModelRefusalFallbackMessage = serde_json::from_value(without.clone()).unwrap();
3340        assert_eq!(msg.scope, None);
3341        assert_eq!(serde_json::to_value(&msg).unwrap(), without);
3342
3343        // Open enum: unknown scopes pass through verbatim.
3344        assert_eq!(
3345            RefusalFallbackScope::from("workspace").as_str(),
3346            "workspace"
3347        );
3348    }
3349}