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