Skip to main content

claude_codes/io/
message_types.rs

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