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::content_blocks::{deserialize_content_blocks, ContentBlock};
7
8/// Known system message subtypes.
9///
10/// The Claude CLI emits system messages with a `subtype` field indicating what
11/// kind of system event occurred. This enum captures the known subtypes while
12/// preserving unknown values via the `Unknown` variant for forward compatibility.
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum SystemSubtype {
15    Init,
16    Status,
17    CompactBoundary,
18    TaskStarted,
19    TaskProgress,
20    TaskNotification,
21    /// A subtype not yet known to this version of the crate.
22    Unknown(String),
23}
24
25impl SystemSubtype {
26    pub fn as_str(&self) -> &str {
27        match self {
28            Self::Init => "init",
29            Self::Status => "status",
30            Self::CompactBoundary => "compact_boundary",
31            Self::TaskStarted => "task_started",
32            Self::TaskProgress => "task_progress",
33            Self::TaskNotification => "task_notification",
34            Self::Unknown(s) => s.as_str(),
35        }
36    }
37}
38
39impl fmt::Display for SystemSubtype {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        f.write_str(self.as_str())
42    }
43}
44
45impl From<&str> for SystemSubtype {
46    fn from(s: &str) -> Self {
47        match s {
48            "init" => Self::Init,
49            "status" => Self::Status,
50            "compact_boundary" => Self::CompactBoundary,
51            "task_started" => Self::TaskStarted,
52            "task_progress" => Self::TaskProgress,
53            "task_notification" => Self::TaskNotification,
54            other => Self::Unknown(other.to_string()),
55        }
56    }
57}
58
59impl Serialize for SystemSubtype {
60    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
61        serializer.serialize_str(self.as_str())
62    }
63}
64
65impl<'de> Deserialize<'de> for SystemSubtype {
66    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
67        let s = String::deserialize(deserializer)?;
68        Ok(Self::from(s.as_str()))
69    }
70}
71
72/// Known message roles.
73///
74/// Used in `MessageContent` and `AssistantMessageContent` to indicate the
75/// speaker of a message.
76#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77pub enum MessageRole {
78    User,
79    Assistant,
80    /// A role not yet known to this version of the crate.
81    Unknown(String),
82}
83
84impl MessageRole {
85    pub fn as_str(&self) -> &str {
86        match self {
87            Self::User => "user",
88            Self::Assistant => "assistant",
89            Self::Unknown(s) => s.as_str(),
90        }
91    }
92}
93
94impl fmt::Display for MessageRole {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.write_str(self.as_str())
97    }
98}
99
100impl From<&str> for MessageRole {
101    fn from(s: &str) -> Self {
102        match s {
103            "user" => Self::User,
104            "assistant" => Self::Assistant,
105            other => Self::Unknown(other.to_string()),
106        }
107    }
108}
109
110impl Serialize for MessageRole {
111    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
112        serializer.serialize_str(self.as_str())
113    }
114}
115
116impl<'de> Deserialize<'de> for MessageRole {
117    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
118        let s = String::deserialize(deserializer)?;
119        Ok(Self::from(s.as_str()))
120    }
121}
122
123/// What triggered a context compaction.
124#[derive(Debug, Clone, PartialEq, Eq, Hash)]
125pub enum CompactionTrigger {
126    /// Automatic compaction triggered by token limit.
127    Auto,
128    /// User-initiated compaction (e.g., /compact command).
129    Manual,
130    /// A trigger not yet known to this version of the crate.
131    Unknown(String),
132}
133
134impl CompactionTrigger {
135    pub fn as_str(&self) -> &str {
136        match self {
137            Self::Auto => "auto",
138            Self::Manual => "manual",
139            Self::Unknown(s) => s.as_str(),
140        }
141    }
142}
143
144impl fmt::Display for CompactionTrigger {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        f.write_str(self.as_str())
147    }
148}
149
150impl From<&str> for CompactionTrigger {
151    fn from(s: &str) -> Self {
152        match s {
153            "auto" => Self::Auto,
154            "manual" => Self::Manual,
155            other => Self::Unknown(other.to_string()),
156        }
157    }
158}
159
160impl Serialize for CompactionTrigger {
161    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
162        serializer.serialize_str(self.as_str())
163    }
164}
165
166impl<'de> Deserialize<'de> for CompactionTrigger {
167    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
168        let s = String::deserialize(deserializer)?;
169        Ok(Self::from(s.as_str()))
170    }
171}
172
173/// Reason why the assistant stopped generating.
174#[derive(Debug, Clone, PartialEq, Eq, Hash)]
175pub enum StopReason {
176    /// The assistant reached a natural end of its turn.
177    EndTurn,
178    /// The response hit the maximum token limit.
179    MaxTokens,
180    /// The assistant wants to use a tool.
181    ToolUse,
182    /// A stop reason not yet known to this version of the crate.
183    Unknown(String),
184}
185
186impl StopReason {
187    pub fn as_str(&self) -> &str {
188        match self {
189            Self::EndTurn => "end_turn",
190            Self::MaxTokens => "max_tokens",
191            Self::ToolUse => "tool_use",
192            Self::Unknown(s) => s.as_str(),
193        }
194    }
195}
196
197impl fmt::Display for StopReason {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        f.write_str(self.as_str())
200    }
201}
202
203impl From<&str> for StopReason {
204    fn from(s: &str) -> Self {
205        match s {
206            "end_turn" => Self::EndTurn,
207            "max_tokens" => Self::MaxTokens,
208            "tool_use" => Self::ToolUse,
209            other => Self::Unknown(other.to_string()),
210        }
211    }
212}
213
214impl Serialize for StopReason {
215    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
216        serializer.serialize_str(self.as_str())
217    }
218}
219
220impl<'de> Deserialize<'de> for StopReason {
221    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
222        let s = String::deserialize(deserializer)?;
223        Ok(Self::from(s.as_str()))
224    }
225}
226
227/// How the API key was sourced for the session.
228#[derive(Debug, Clone, PartialEq, Eq, Hash)]
229pub enum ApiKeySource {
230    /// No API key provided.
231    None,
232    /// A source not yet known to this version of the crate.
233    Unknown(String),
234}
235
236impl ApiKeySource {
237    pub fn as_str(&self) -> &str {
238        match self {
239            Self::None => "none",
240            Self::Unknown(s) => s.as_str(),
241        }
242    }
243}
244
245impl fmt::Display for ApiKeySource {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        f.write_str(self.as_str())
248    }
249}
250
251impl From<&str> for ApiKeySource {
252    fn from(s: &str) -> Self {
253        match s {
254            "none" => Self::None,
255            other => Self::Unknown(other.to_string()),
256        }
257    }
258}
259
260impl Serialize for ApiKeySource {
261    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
262        serializer.serialize_str(self.as_str())
263    }
264}
265
266impl<'de> Deserialize<'de> for ApiKeySource {
267    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
268        let s = String::deserialize(deserializer)?;
269        Ok(Self::from(s.as_str()))
270    }
271}
272
273/// Output formatting style for the session.
274#[derive(Debug, Clone, PartialEq, Eq, Hash)]
275pub enum OutputStyle {
276    /// Default output style.
277    Default,
278    /// A style not yet known to this version of the crate.
279    Unknown(String),
280}
281
282impl OutputStyle {
283    pub fn as_str(&self) -> &str {
284        match self {
285            Self::Default => "default",
286            Self::Unknown(s) => s.as_str(),
287        }
288    }
289}
290
291impl fmt::Display for OutputStyle {
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        f.write_str(self.as_str())
294    }
295}
296
297impl From<&str> for OutputStyle {
298    fn from(s: &str) -> Self {
299        match s {
300            "default" => Self::Default,
301            other => Self::Unknown(other.to_string()),
302        }
303    }
304}
305
306impl Serialize for OutputStyle {
307    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
308        serializer.serialize_str(self.as_str())
309    }
310}
311
312impl<'de> Deserialize<'de> for OutputStyle {
313    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
314        let s = String::deserialize(deserializer)?;
315        Ok(Self::from(s.as_str()))
316    }
317}
318
319/// Permission mode reported in init messages.
320#[derive(Debug, Clone, PartialEq, Eq, Hash)]
321pub enum InitPermissionMode {
322    /// Default permission mode.
323    Default,
324    /// A mode not yet known to this version of the crate.
325    Unknown(String),
326}
327
328impl InitPermissionMode {
329    pub fn as_str(&self) -> &str {
330        match self {
331            Self::Default => "default",
332            Self::Unknown(s) => s.as_str(),
333        }
334    }
335}
336
337impl fmt::Display for InitPermissionMode {
338    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339        f.write_str(self.as_str())
340    }
341}
342
343impl From<&str> for InitPermissionMode {
344    fn from(s: &str) -> Self {
345        match s {
346            "default" => Self::Default,
347            other => Self::Unknown(other.to_string()),
348        }
349    }
350}
351
352impl Serialize for InitPermissionMode {
353    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
354        serializer.serialize_str(self.as_str())
355    }
356}
357
358impl<'de> Deserialize<'de> for InitPermissionMode {
359    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
360        let s = String::deserialize(deserializer)?;
361        Ok(Self::from(s.as_str()))
362    }
363}
364
365/// Status of an ongoing operation (e.g., context compaction).
366#[derive(Debug, Clone, PartialEq, Eq, Hash)]
367pub enum StatusMessageStatus {
368    /// Context compaction is in progress.
369    Compacting,
370    /// A status not yet known to this version of the crate.
371    Unknown(String),
372}
373
374impl StatusMessageStatus {
375    pub fn as_str(&self) -> &str {
376        match self {
377            Self::Compacting => "compacting",
378            Self::Unknown(s) => s.as_str(),
379        }
380    }
381}
382
383impl fmt::Display for StatusMessageStatus {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        f.write_str(self.as_str())
386    }
387}
388
389impl From<&str> for StatusMessageStatus {
390    fn from(s: &str) -> Self {
391        match s {
392            "compacting" => Self::Compacting,
393            other => Self::Unknown(other.to_string()),
394        }
395    }
396}
397
398impl Serialize for StatusMessageStatus {
399    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
400        serializer.serialize_str(self.as_str())
401    }
402}
403
404impl<'de> Deserialize<'de> for StatusMessageStatus {
405    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
406        let s = String::deserialize(deserializer)?;
407        Ok(Self::from(s.as_str()))
408    }
409}
410
411/// Serialize an optional UUID as a string
412pub(crate) fn serialize_optional_uuid<S>(
413    uuid: &Option<Uuid>,
414    serializer: S,
415) -> Result<S::Ok, S::Error>
416where
417    S: Serializer,
418{
419    match uuid {
420        Some(id) => serializer.serialize_str(&id.to_string()),
421        None => serializer.serialize_none(),
422    }
423}
424
425/// Deserialize an optional UUID from a string
426pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
427where
428    D: Deserializer<'de>,
429{
430    let opt_str: Option<String> = Option::deserialize(deserializer)?;
431    match opt_str {
432        Some(s) => Uuid::parse_str(&s)
433            .map(Some)
434            .map_err(serde::de::Error::custom),
435        None => Ok(None),
436    }
437}
438
439/// User message
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct UserMessage {
442    pub message: MessageContent,
443    #[serde(skip_serializing_if = "Option::is_none")]
444    #[serde(
445        serialize_with = "serialize_optional_uuid",
446        deserialize_with = "deserialize_optional_uuid"
447    )]
448    pub session_id: Option<Uuid>,
449    /// Parent tool use ID for nested agent messages
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub parent_tool_use_id: Option<String>,
452    /// Message-level unique identifier
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub uuid: Option<String>,
455}
456
457/// Message content with role
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct MessageContent {
460    pub role: MessageRole,
461    #[serde(deserialize_with = "deserialize_content_blocks")]
462    pub content: Vec<ContentBlock>,
463}
464
465/// System message with metadata
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct SystemMessage {
468    pub subtype: SystemSubtype,
469    #[serde(flatten)]
470    pub data: Value, // Captures all other fields
471}
472
473impl SystemMessage {
474    /// Check if this is an init message
475    pub fn is_init(&self) -> bool {
476        self.subtype == SystemSubtype::Init
477    }
478
479    /// Check if this is a status message
480    pub fn is_status(&self) -> bool {
481        self.subtype == SystemSubtype::Status
482    }
483
484    /// Check if this is a compact_boundary message
485    pub fn is_compact_boundary(&self) -> bool {
486        self.subtype == SystemSubtype::CompactBoundary
487    }
488
489    /// Try to parse as an init message
490    pub fn as_init(&self) -> Option<InitMessage> {
491        if self.subtype != SystemSubtype::Init {
492            return None;
493        }
494        serde_json::from_value(self.data.clone()).ok()
495    }
496
497    /// Try to parse as a status message
498    pub fn as_status(&self) -> Option<StatusMessage> {
499        if self.subtype != SystemSubtype::Status {
500            return None;
501        }
502        serde_json::from_value(self.data.clone()).ok()
503    }
504
505    /// Try to parse as a compact_boundary message
506    pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
507        if self.subtype != SystemSubtype::CompactBoundary {
508            return None;
509        }
510        serde_json::from_value(self.data.clone()).ok()
511    }
512
513    /// Check if this is a task_started message
514    pub fn is_task_started(&self) -> bool {
515        self.subtype == SystemSubtype::TaskStarted
516    }
517
518    /// Check if this is a task_progress message
519    pub fn is_task_progress(&self) -> bool {
520        self.subtype == SystemSubtype::TaskProgress
521    }
522
523    /// Check if this is a task_notification message
524    pub fn is_task_notification(&self) -> bool {
525        self.subtype == SystemSubtype::TaskNotification
526    }
527
528    /// Try to parse as a task_started message
529    pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
530        if self.subtype != SystemSubtype::TaskStarted {
531            return None;
532        }
533        serde_json::from_value(self.data.clone()).ok()
534    }
535
536    /// Try to parse as a task_progress message
537    pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
538        if self.subtype != SystemSubtype::TaskProgress {
539            return None;
540        }
541        serde_json::from_value(self.data.clone()).ok()
542    }
543
544    /// Try to parse as a task_notification message
545    pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
546        if self.subtype != SystemSubtype::TaskNotification {
547            return None;
548        }
549        serde_json::from_value(self.data.clone()).ok()
550    }
551}
552
553/// Plugin info from the init message
554#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct PluginInfo {
556    /// Plugin name
557    pub name: String,
558    /// Path to the plugin on disk
559    pub path: String,
560    /// Plugin registry source (e.g., "rust-analyzer-lsp@claude-plugins-official")
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub source: Option<String>,
563}
564
565/// Init system message data - sent at session start
566#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct InitMessage {
568    /// Session identifier
569    pub session_id: String,
570    /// Current working directory
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub cwd: Option<String>,
573    /// Model being used
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub model: Option<String>,
576    /// List of available tools
577    #[serde(default, skip_serializing_if = "Vec::is_empty")]
578    pub tools: Vec<String>,
579    /// MCP servers configured
580    #[serde(default, skip_serializing_if = "Vec::is_empty")]
581    pub mcp_servers: Vec<Value>,
582    /// Available slash commands (e.g., "compact", "cost", "review")
583    #[serde(default, skip_serializing_if = "Vec::is_empty")]
584    pub slash_commands: Vec<String>,
585    /// Available agent types (e.g., "Bash", "Explore", "Plan")
586    #[serde(default, skip_serializing_if = "Vec::is_empty")]
587    pub agents: Vec<String>,
588    /// Installed plugins
589    #[serde(default, skip_serializing_if = "Vec::is_empty")]
590    pub plugins: Vec<PluginInfo>,
591    /// Installed skills
592    #[serde(default, skip_serializing_if = "Vec::is_empty")]
593    pub skills: Vec<Value>,
594    /// Claude Code CLI version
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub claude_code_version: Option<String>,
597    /// How the API key was sourced
598    #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
599    pub api_key_source: Option<ApiKeySource>,
600    /// Output style
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub output_style: Option<OutputStyle>,
603    /// Permission mode
604    #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
605    pub permission_mode: Option<InitPermissionMode>,
606
607    /// Message-level unique identifier
608    #[serde(skip_serializing_if = "Option::is_none")]
609    pub uuid: Option<String>,
610
611    /// Memory storage paths (e.g., {"auto": "/path/to/memory/"})
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub memory_paths: Option<Value>,
614
615    /// Fast mode toggle state (e.g., "off")
616    #[serde(skip_serializing_if = "Option::is_none")]
617    pub fast_mode_state: Option<String>,
618}
619
620/// Status system message - sent during operations like context compaction
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct StatusMessage {
623    /// Session identifier
624    pub session_id: String,
625    /// Current status (e.g., compacting) or null when complete
626    pub status: Option<StatusMessageStatus>,
627    /// Unique identifier for this message
628    #[serde(skip_serializing_if = "Option::is_none")]
629    pub uuid: Option<String>,
630}
631
632/// Compact boundary message - marks where context compaction occurred
633#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct CompactBoundaryMessage {
635    /// Session identifier
636    pub session_id: String,
637    /// Metadata about the compaction
638    pub compact_metadata: CompactMetadata,
639    /// Unique identifier for this message
640    #[serde(skip_serializing_if = "Option::is_none")]
641    pub uuid: Option<String>,
642}
643
644/// Metadata about context compaction
645#[derive(Debug, Clone, Serialize, Deserialize)]
646pub struct CompactMetadata {
647    /// Number of tokens before compaction
648    pub pre_tokens: u64,
649    /// What triggered the compaction
650    pub trigger: CompactionTrigger,
651}
652
653// ---------------------------------------------------------------------------
654// Task system message types (task_started, task_progress, task_notification)
655// ---------------------------------------------------------------------------
656
657/// Cumulative usage statistics for a background task.
658#[derive(Debug, Clone, Serialize, Deserialize)]
659pub struct TaskUsage {
660    /// Wall-clock milliseconds since the task started.
661    pub duration_ms: u64,
662    /// Total number of tool calls made so far.
663    pub tool_uses: u64,
664    /// Total tokens consumed so far.
665    pub total_tokens: u64,
666}
667
668/// The kind of background task.
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
670#[serde(rename_all = "snake_case")]
671pub enum TaskType {
672    /// A sub-agent task (e.g., Explore, Plan).
673    LocalAgent,
674    /// A background bash command.
675    LocalBash,
676}
677
678/// Completion status of a background task.
679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
680#[serde(rename_all = "snake_case")]
681pub enum TaskStatus {
682    Completed,
683    Failed,
684}
685
686/// `task_started` system message — emitted once when a background task begins.
687#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct TaskStartedMessage {
689    pub session_id: String,
690    pub task_id: String,
691    pub task_type: TaskType,
692    pub tool_use_id: String,
693    pub description: String,
694    pub uuid: String,
695}
696
697/// `task_progress` system message — emitted periodically as a background
698/// agent task executes tools. Not emitted for `local_bash` tasks.
699#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct TaskProgressMessage {
701    pub session_id: String,
702    pub task_id: String,
703    pub tool_use_id: String,
704    pub description: String,
705    pub last_tool_name: String,
706    pub usage: TaskUsage,
707    pub uuid: String,
708}
709
710/// `task_notification` system message — emitted once when a background
711/// task completes or fails.
712#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct TaskNotificationMessage {
714    pub session_id: String,
715    pub task_id: String,
716    pub status: TaskStatus,
717    pub summary: String,
718    pub output_file: Option<String>,
719    #[serde(skip_serializing_if = "Option::is_none")]
720    pub tool_use_id: Option<String>,
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub usage: Option<TaskUsage>,
723    #[serde(skip_serializing_if = "Option::is_none")]
724    pub uuid: Option<String>,
725}
726
727/// Assistant message
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct AssistantMessage {
730    pub message: AssistantMessageContent,
731    pub session_id: String,
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub uuid: Option<String>,
734    #[serde(skip_serializing_if = "Option::is_none")]
735    pub parent_tool_use_id: Option<String>,
736}
737
738/// Nested message content for assistant messages
739#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct AssistantMessageContent {
741    pub id: String,
742    pub role: MessageRole,
743    pub model: String,
744    pub content: Vec<ContentBlock>,
745    #[serde(skip_serializing_if = "Option::is_none")]
746    pub stop_reason: Option<StopReason>,
747    #[serde(skip_serializing_if = "Option::is_none")]
748    pub stop_sequence: Option<String>,
749    #[serde(skip_serializing_if = "Option::is_none")]
750    pub usage: Option<AssistantUsage>,
751    /// Details about why generation stopped
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub stop_details: Option<Value>,
754    /// Context management metadata
755    #[serde(skip_serializing_if = "Option::is_none")]
756    pub context_management: Option<Value>,
757}
758
759/// Usage information for assistant messages
760#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct AssistantUsage {
762    /// Number of input tokens
763    #[serde(default)]
764    pub input_tokens: u32,
765
766    /// Number of output tokens
767    #[serde(default)]
768    pub output_tokens: u32,
769
770    /// Tokens used to create cache
771    #[serde(default)]
772    pub cache_creation_input_tokens: u32,
773
774    /// Tokens read from cache
775    #[serde(default)]
776    pub cache_read_input_tokens: u32,
777
778    /// Service tier used (e.g., "standard")
779    #[serde(skip_serializing_if = "Option::is_none")]
780    pub service_tier: Option<String>,
781
782    /// Detailed cache creation breakdown
783    #[serde(skip_serializing_if = "Option::is_none")]
784    pub cache_creation: Option<CacheCreationDetails>,
785
786    /// Inference geography (e.g., "not_available")
787    #[serde(skip_serializing_if = "Option::is_none")]
788    pub inference_geo: Option<String>,
789}
790
791/// Detailed cache creation information
792#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct CacheCreationDetails {
794    /// Ephemeral 1-hour input tokens
795    #[serde(default)]
796    pub ephemeral_1h_input_tokens: u32,
797
798    /// Ephemeral 5-minute input tokens
799    #[serde(default)]
800    pub ephemeral_5m_input_tokens: u32,
801}
802
803#[cfg(test)]
804mod tests {
805    use crate::io::ClaudeOutput;
806
807    #[test]
808    fn test_system_message_init() {
809        let json = r#"{
810            "type": "system",
811            "subtype": "init",
812            "session_id": "test-session-123",
813            "cwd": "/home/user/project",
814            "model": "claude-sonnet-4",
815            "tools": ["Bash", "Read", "Write"],
816            "mcp_servers": [],
817            "slash_commands": ["compact", "cost", "review"],
818            "agents": ["Bash", "Explore", "Plan"],
819            "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
820            "skills": [],
821            "claude_code_version": "2.1.15",
822            "apiKeySource": "none",
823            "output_style": "default",
824            "permissionMode": "default"
825        }"#;
826
827        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
828        if let ClaudeOutput::System(sys) = output {
829            assert!(sys.is_init());
830            assert!(!sys.is_status());
831            assert!(!sys.is_compact_boundary());
832
833            let init = sys.as_init().expect("Should parse as init");
834            assert_eq!(init.session_id, "test-session-123");
835            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
836            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
837            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
838            assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
839            assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
840            assert_eq!(init.plugins.len(), 1);
841            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
842            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
843            assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
844            assert_eq!(init.output_style, Some(super::OutputStyle::Default));
845            assert_eq!(
846                init.permission_mode,
847                Some(super::InitPermissionMode::Default)
848            );
849        } else {
850            panic!("Expected System message");
851        }
852    }
853
854    #[test]
855    fn test_system_message_init_from_real_capture() {
856        let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
857        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
858        if let ClaudeOutput::System(sys) = output {
859            let init = sys.as_init().expect("Should parse real init capture");
860            assert_eq!(init.slash_commands.len(), 8);
861            assert!(init.slash_commands.contains(&"compact".to_string()));
862            assert!(init.slash_commands.contains(&"review".to_string()));
863            assert_eq!(init.agents.len(), 5);
864            assert!(init.agents.contains(&"Bash".to_string()));
865            assert!(init.agents.contains(&"Explore".to_string()));
866            assert_eq!(init.plugins.len(), 1);
867            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
868            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
869        } else {
870            panic!("Expected System message");
871        }
872    }
873
874    #[test]
875    fn test_system_message_status() {
876        let json = r#"{
877            "type": "system",
878            "subtype": "status",
879            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
880            "status": "compacting",
881            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
882        }"#;
883
884        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
885        if let ClaudeOutput::System(sys) = output {
886            assert!(sys.is_status());
887            assert!(!sys.is_init());
888
889            let status = sys.as_status().expect("Should parse as status");
890            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
891            assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
892            assert_eq!(
893                status.uuid,
894                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
895            );
896        } else {
897            panic!("Expected System message");
898        }
899    }
900
901    #[test]
902    fn test_system_message_status_null() {
903        let json = r#"{
904            "type": "system",
905            "subtype": "status",
906            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
907            "status": null,
908            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
909        }"#;
910
911        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
912        if let ClaudeOutput::System(sys) = output {
913            let status = sys.as_status().expect("Should parse as status");
914            assert_eq!(status.status, None);
915        } else {
916            panic!("Expected System message");
917        }
918    }
919
920    #[test]
921    fn test_system_message_task_started() {
922        let json = r#"{
923            "type": "system",
924            "subtype": "task_started",
925            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
926            "task_id": "b6daf3f",
927            "task_type": "local_bash",
928            "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
929            "description": "Wait for CI on PR #12",
930            "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
931        }"#;
932
933        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
934        if let ClaudeOutput::System(sys) = output {
935            assert!(sys.is_task_started());
936            assert!(!sys.is_task_progress());
937            assert!(!sys.is_task_notification());
938
939            let task = sys.as_task_started().expect("Should parse as task_started");
940            assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
941            assert_eq!(task.task_id, "b6daf3f");
942            assert_eq!(task.task_type, super::TaskType::LocalBash);
943            assert_eq!(task.tool_use_id, "toolu_011rfSTFumpJZdCCfzeD7jaS");
944            assert_eq!(task.description, "Wait for CI on PR #12");
945        } else {
946            panic!("Expected System message");
947        }
948    }
949
950    #[test]
951    fn test_system_message_task_started_agent() {
952        let json = r#"{
953            "type": "system",
954            "subtype": "task_started",
955            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
956            "task_id": "a4a7e0906e5fc64cc",
957            "task_type": "local_agent",
958            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
959            "description": "Explore Scene/ArrayScene duplication",
960            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
961        }"#;
962
963        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
964        if let ClaudeOutput::System(sys) = output {
965            let task = sys.as_task_started().expect("Should parse as task_started");
966            assert_eq!(task.task_type, super::TaskType::LocalAgent);
967            assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
968        } else {
969            panic!("Expected System message");
970        }
971    }
972
973    #[test]
974    fn test_system_message_task_progress() {
975        let json = r#"{
976            "type": "system",
977            "subtype": "task_progress",
978            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
979            "task_id": "a4a7e0906e5fc64cc",
980            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
981            "description": "Reading src/jplephem/chebyshev.rs",
982            "last_tool_name": "Read",
983            "usage": {
984                "duration_ms": 13996,
985                "tool_uses": 9,
986                "total_tokens": 38779
987            },
988            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
989        }"#;
990
991        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
992        if let ClaudeOutput::System(sys) = output {
993            assert!(sys.is_task_progress());
994            assert!(!sys.is_task_started());
995
996            let progress = sys
997                .as_task_progress()
998                .expect("Should parse as task_progress");
999            assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
1000            assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
1001            assert_eq!(progress.last_tool_name, "Read");
1002            assert_eq!(progress.usage.duration_ms, 13996);
1003            assert_eq!(progress.usage.tool_uses, 9);
1004            assert_eq!(progress.usage.total_tokens, 38779);
1005        } else {
1006            panic!("Expected System message");
1007        }
1008    }
1009
1010    #[test]
1011    fn test_system_message_task_notification_completed() {
1012        let json = r#"{
1013            "type": "system",
1014            "subtype": "task_notification",
1015            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
1016            "task_id": "a0ba761e9dc9c316f",
1017            "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
1018            "status": "completed",
1019            "summary": "Agent \"Write Hipparcos data source doc\" completed",
1020            "output_file": "",
1021            "usage": {
1022                "duration_ms": 172300,
1023                "tool_uses": 11,
1024                "total_tokens": 42005
1025            },
1026            "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
1027        }"#;
1028
1029        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1030        if let ClaudeOutput::System(sys) = output {
1031            assert!(sys.is_task_notification());
1032
1033            let notif = sys
1034                .as_task_notification()
1035                .expect("Should parse as task_notification");
1036            assert_eq!(notif.status, super::TaskStatus::Completed);
1037            assert_eq!(
1038                notif.summary,
1039                "Agent \"Write Hipparcos data source doc\" completed"
1040            );
1041            assert_eq!(notif.output_file, Some("".to_string()));
1042            assert_eq!(
1043                notif.tool_use_id,
1044                Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
1045            );
1046            let usage = notif.usage.expect("Should have usage");
1047            assert_eq!(usage.duration_ms, 172300);
1048            assert_eq!(usage.tool_uses, 11);
1049            assert_eq!(usage.total_tokens, 42005);
1050        } else {
1051            panic!("Expected System message");
1052        }
1053    }
1054
1055    #[test]
1056    fn test_system_message_task_notification_failed_no_usage() {
1057        let json = r#"{
1058            "type": "system",
1059            "subtype": "task_notification",
1060            "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
1061            "task_id": "b98f6a3",
1062            "status": "failed",
1063            "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
1064            "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
1065        }"#;
1066
1067        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1068        if let ClaudeOutput::System(sys) = output {
1069            let notif = sys
1070                .as_task_notification()
1071                .expect("Should parse as task_notification");
1072            assert_eq!(notif.status, super::TaskStatus::Failed);
1073            assert!(notif.tool_use_id.is_none());
1074            assert!(notif.usage.is_none());
1075            assert_eq!(
1076                notif.output_file,
1077                Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
1078            );
1079        } else {
1080            panic!("Expected System message");
1081        }
1082    }
1083
1084    #[test]
1085    fn test_system_message_compact_boundary() {
1086        let json = r#"{
1087            "type": "system",
1088            "subtype": "compact_boundary",
1089            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1090            "compact_metadata": {
1091                "pre_tokens": 155285,
1092                "trigger": "auto"
1093            },
1094            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1095        }"#;
1096
1097        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1098        if let ClaudeOutput::System(sys) = output {
1099            assert!(sys.is_compact_boundary());
1100            assert!(!sys.is_init());
1101            assert!(!sys.is_status());
1102
1103            let compact = sys
1104                .as_compact_boundary()
1105                .expect("Should parse as compact_boundary");
1106            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1107            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1108            assert_eq!(
1109                compact.compact_metadata.trigger,
1110                super::CompactionTrigger::Auto
1111            );
1112        } else {
1113            panic!("Expected System message");
1114        }
1115    }
1116
1117    #[test]
1118    fn test_init_message_with_new_fields() {
1119        let json = r#"{
1120            "type": "system",
1121            "subtype": "init",
1122            "session_id": "test-session",
1123            "cwd": "/home/user",
1124            "model": "claude-opus-4-7",
1125            "tools": ["Bash"],
1126            "mcp_servers": [],
1127            "permissionMode": "default",
1128            "apiKeySource": "none",
1129            "uuid": "44841a0d-182d-493a-86b5-79800d3d9665",
1130            "memory_paths": {"auto": "/home/user/.claude/projects/memory/"},
1131            "fast_mode_state": "off",
1132            "plugins": [{"name": "lsp", "path": "/plugins/lsp", "source": "lsp@official"}],
1133            "claude_code_version": "2.1.117"
1134        }"#;
1135
1136        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1137        if let ClaudeOutput::System(sys) = output {
1138            let init = sys.as_init().expect("Should parse as init");
1139            assert_eq!(
1140                init.uuid.as_deref(),
1141                Some("44841a0d-182d-493a-86b5-79800d3d9665")
1142            );
1143            assert!(init.memory_paths.is_some());
1144            assert_eq!(init.fast_mode_state.as_deref(), Some("off"));
1145            assert_eq!(init.plugins[0].source.as_deref(), Some("lsp@official"));
1146            assert_eq!(init.claude_code_version.as_deref(), Some("2.1.117"));
1147        } else {
1148            panic!("Expected System message");
1149        }
1150    }
1151
1152    #[test]
1153    fn test_assistant_message_with_new_fields() {
1154        let json = r#"{
1155            "type": "assistant",
1156            "message": {
1157                "id": "msg_1",
1158                "type": "message",
1159                "role": "assistant",
1160                "model": "claude-opus-4-7",
1161                "content": [{"type": "text", "text": "Hello"}],
1162                "stop_reason": "end_turn",
1163                "stop_details": null,
1164                "context_management": null,
1165                "usage": {
1166                    "input_tokens": 100,
1167                    "output_tokens": 10,
1168                    "cache_creation_input_tokens": 50,
1169                    "cache_read_input_tokens": 0,
1170                    "service_tier": "standard",
1171                    "inference_geo": "not_available"
1172                }
1173            },
1174            "session_id": "abc",
1175            "uuid": "msg-uuid-123"
1176        }"#;
1177
1178        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1179        if let ClaudeOutput::Assistant(asst) = output {
1180            assert_eq!(asst.message.stop_details, None);
1181            assert_eq!(asst.message.context_management, None);
1182            let usage = asst.message.usage.unwrap();
1183            assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1184        } else {
1185            panic!("Expected Assistant message");
1186        }
1187    }
1188
1189    #[test]
1190    fn test_user_message_with_new_fields() {
1191        let json = r#"{
1192            "type": "user",
1193            "message": {
1194                "role": "user",
1195                "content": [{"type": "text", "text": "Hello"}]
1196            },
1197            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
1198            "parent_tool_use_id": "toolu_123",
1199            "uuid": "user-msg-456"
1200        }"#;
1201
1202        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1203        if let ClaudeOutput::User(user) = output {
1204            assert_eq!(user.parent_tool_use_id.as_deref(), Some("toolu_123"));
1205            assert_eq!(user.uuid.as_deref(), Some("user-msg-456"));
1206        } else {
1207            panic!("Expected User message");
1208        }
1209    }
1210}