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}
450
451/// Message content with role
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct MessageContent {
454    pub role: MessageRole,
455    #[serde(deserialize_with = "deserialize_content_blocks")]
456    pub content: Vec<ContentBlock>,
457}
458
459/// System message with metadata
460#[derive(Debug, Clone, Serialize, Deserialize)]
461pub struct SystemMessage {
462    pub subtype: SystemSubtype,
463    #[serde(flatten)]
464    pub data: Value, // Captures all other fields
465}
466
467impl SystemMessage {
468    /// Check if this is an init message
469    pub fn is_init(&self) -> bool {
470        self.subtype == SystemSubtype::Init
471    }
472
473    /// Check if this is a status message
474    pub fn is_status(&self) -> bool {
475        self.subtype == SystemSubtype::Status
476    }
477
478    /// Check if this is a compact_boundary message
479    pub fn is_compact_boundary(&self) -> bool {
480        self.subtype == SystemSubtype::CompactBoundary
481    }
482
483    /// Try to parse as an init message
484    pub fn as_init(&self) -> Option<InitMessage> {
485        if self.subtype != SystemSubtype::Init {
486            return None;
487        }
488        serde_json::from_value(self.data.clone()).ok()
489    }
490
491    /// Try to parse as a status message
492    pub fn as_status(&self) -> Option<StatusMessage> {
493        if self.subtype != SystemSubtype::Status {
494            return None;
495        }
496        serde_json::from_value(self.data.clone()).ok()
497    }
498
499    /// Try to parse as a compact_boundary message
500    pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
501        if self.subtype != SystemSubtype::CompactBoundary {
502            return None;
503        }
504        serde_json::from_value(self.data.clone()).ok()
505    }
506
507    /// Check if this is a task_started message
508    pub fn is_task_started(&self) -> bool {
509        self.subtype == SystemSubtype::TaskStarted
510    }
511
512    /// Check if this is a task_progress message
513    pub fn is_task_progress(&self) -> bool {
514        self.subtype == SystemSubtype::TaskProgress
515    }
516
517    /// Check if this is a task_notification message
518    pub fn is_task_notification(&self) -> bool {
519        self.subtype == SystemSubtype::TaskNotification
520    }
521
522    /// Try to parse as a task_started message
523    pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
524        if self.subtype != SystemSubtype::TaskStarted {
525            return None;
526        }
527        serde_json::from_value(self.data.clone()).ok()
528    }
529
530    /// Try to parse as a task_progress message
531    pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
532        if self.subtype != SystemSubtype::TaskProgress {
533            return None;
534        }
535        serde_json::from_value(self.data.clone()).ok()
536    }
537
538    /// Try to parse as a task_notification message
539    pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
540        if self.subtype != SystemSubtype::TaskNotification {
541            return None;
542        }
543        serde_json::from_value(self.data.clone()).ok()
544    }
545}
546
547/// Plugin info from the init message
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct PluginInfo {
550    /// Plugin name
551    pub name: String,
552    /// Path to the plugin on disk
553    pub path: String,
554}
555
556/// Init system message data - sent at session start
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct InitMessage {
559    /// Session identifier
560    pub session_id: String,
561    /// Current working directory
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub cwd: Option<String>,
564    /// Model being used
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub model: Option<String>,
567    /// List of available tools
568    #[serde(default, skip_serializing_if = "Vec::is_empty")]
569    pub tools: Vec<String>,
570    /// MCP servers configured
571    #[serde(default, skip_serializing_if = "Vec::is_empty")]
572    pub mcp_servers: Vec<Value>,
573    /// Available slash commands (e.g., "compact", "cost", "review")
574    #[serde(default, skip_serializing_if = "Vec::is_empty")]
575    pub slash_commands: Vec<String>,
576    /// Available agent types (e.g., "Bash", "Explore", "Plan")
577    #[serde(default, skip_serializing_if = "Vec::is_empty")]
578    pub agents: Vec<String>,
579    /// Installed plugins
580    #[serde(default, skip_serializing_if = "Vec::is_empty")]
581    pub plugins: Vec<PluginInfo>,
582    /// Installed skills
583    #[serde(default, skip_serializing_if = "Vec::is_empty")]
584    pub skills: Vec<Value>,
585    /// Claude Code CLI version
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub claude_code_version: Option<String>,
588    /// How the API key was sourced
589    #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
590    pub api_key_source: Option<ApiKeySource>,
591    /// Output style
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub output_style: Option<OutputStyle>,
594    /// Permission mode
595    #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
596    pub permission_mode: Option<InitPermissionMode>,
597}
598
599/// Status system message - sent during operations like context compaction
600#[derive(Debug, Clone, Serialize, Deserialize)]
601pub struct StatusMessage {
602    /// Session identifier
603    pub session_id: String,
604    /// Current status (e.g., compacting) or null when complete
605    pub status: Option<StatusMessageStatus>,
606    /// Unique identifier for this message
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub uuid: Option<String>,
609}
610
611/// Compact boundary message - marks where context compaction occurred
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct CompactBoundaryMessage {
614    /// Session identifier
615    pub session_id: String,
616    /// Metadata about the compaction
617    pub compact_metadata: CompactMetadata,
618    /// Unique identifier for this message
619    #[serde(skip_serializing_if = "Option::is_none")]
620    pub uuid: Option<String>,
621}
622
623/// Metadata about context compaction
624#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct CompactMetadata {
626    /// Number of tokens before compaction
627    pub pre_tokens: u64,
628    /// What triggered the compaction
629    pub trigger: CompactionTrigger,
630}
631
632// ---------------------------------------------------------------------------
633// Task system message types (task_started, task_progress, task_notification)
634// ---------------------------------------------------------------------------
635
636/// Cumulative usage statistics for a background task.
637#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct TaskUsage {
639    /// Wall-clock milliseconds since the task started.
640    pub duration_ms: u64,
641    /// Total number of tool calls made so far.
642    pub tool_uses: u64,
643    /// Total tokens consumed so far.
644    pub total_tokens: u64,
645}
646
647/// The kind of background task.
648#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
649#[serde(rename_all = "snake_case")]
650pub enum TaskType {
651    /// A sub-agent task (e.g., Explore, Plan).
652    LocalAgent,
653    /// A background bash command.
654    LocalBash,
655}
656
657/// Completion status of a background task.
658#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
659#[serde(rename_all = "snake_case")]
660pub enum TaskStatus {
661    Completed,
662    Failed,
663}
664
665/// `task_started` system message — emitted once when a background task begins.
666#[derive(Debug, Clone, Serialize, Deserialize)]
667pub struct TaskStartedMessage {
668    pub session_id: String,
669    pub task_id: String,
670    pub task_type: TaskType,
671    pub tool_use_id: String,
672    pub description: String,
673    pub uuid: String,
674}
675
676/// `task_progress` system message — emitted periodically as a background
677/// agent task executes tools. Not emitted for `local_bash` tasks.
678#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct TaskProgressMessage {
680    pub session_id: String,
681    pub task_id: String,
682    pub tool_use_id: String,
683    pub description: String,
684    pub last_tool_name: String,
685    pub usage: TaskUsage,
686    pub uuid: String,
687}
688
689/// `task_notification` system message — emitted once when a background
690/// task completes or fails.
691#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct TaskNotificationMessage {
693    pub session_id: String,
694    pub task_id: String,
695    pub status: TaskStatus,
696    pub summary: String,
697    pub output_file: Option<String>,
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub tool_use_id: Option<String>,
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub usage: Option<TaskUsage>,
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub uuid: Option<String>,
704}
705
706/// Assistant message
707#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct AssistantMessage {
709    pub message: AssistantMessageContent,
710    pub session_id: String,
711    #[serde(skip_serializing_if = "Option::is_none")]
712    pub uuid: Option<String>,
713    #[serde(skip_serializing_if = "Option::is_none")]
714    pub parent_tool_use_id: Option<String>,
715}
716
717/// Nested message content for assistant messages
718#[derive(Debug, Clone, Serialize, Deserialize)]
719pub struct AssistantMessageContent {
720    pub id: String,
721    pub role: MessageRole,
722    pub model: String,
723    pub content: Vec<ContentBlock>,
724    #[serde(skip_serializing_if = "Option::is_none")]
725    pub stop_reason: Option<StopReason>,
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub stop_sequence: Option<String>,
728    #[serde(skip_serializing_if = "Option::is_none")]
729    pub usage: Option<AssistantUsage>,
730}
731
732/// Usage information for assistant messages
733#[derive(Debug, Clone, Serialize, Deserialize)]
734pub struct AssistantUsage {
735    /// Number of input tokens
736    #[serde(default)]
737    pub input_tokens: u32,
738
739    /// Number of output tokens
740    #[serde(default)]
741    pub output_tokens: u32,
742
743    /// Tokens used to create cache
744    #[serde(default)]
745    pub cache_creation_input_tokens: u32,
746
747    /// Tokens read from cache
748    #[serde(default)]
749    pub cache_read_input_tokens: u32,
750
751    /// Service tier used (e.g., "standard")
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub service_tier: Option<String>,
754
755    /// Detailed cache creation breakdown
756    #[serde(skip_serializing_if = "Option::is_none")]
757    pub cache_creation: Option<CacheCreationDetails>,
758}
759
760/// Detailed cache creation information
761#[derive(Debug, Clone, Serialize, Deserialize)]
762pub struct CacheCreationDetails {
763    /// Ephemeral 1-hour input tokens
764    #[serde(default)]
765    pub ephemeral_1h_input_tokens: u32,
766
767    /// Ephemeral 5-minute input tokens
768    #[serde(default)]
769    pub ephemeral_5m_input_tokens: u32,
770}
771
772#[cfg(test)]
773mod tests {
774    use crate::io::ClaudeOutput;
775
776    #[test]
777    fn test_system_message_init() {
778        let json = r#"{
779            "type": "system",
780            "subtype": "init",
781            "session_id": "test-session-123",
782            "cwd": "/home/user/project",
783            "model": "claude-sonnet-4",
784            "tools": ["Bash", "Read", "Write"],
785            "mcp_servers": [],
786            "slash_commands": ["compact", "cost", "review"],
787            "agents": ["Bash", "Explore", "Plan"],
788            "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
789            "skills": [],
790            "claude_code_version": "2.1.15",
791            "apiKeySource": "none",
792            "output_style": "default",
793            "permissionMode": "default"
794        }"#;
795
796        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
797        if let ClaudeOutput::System(sys) = output {
798            assert!(sys.is_init());
799            assert!(!sys.is_status());
800            assert!(!sys.is_compact_boundary());
801
802            let init = sys.as_init().expect("Should parse as init");
803            assert_eq!(init.session_id, "test-session-123");
804            assert_eq!(init.cwd, Some("/home/user/project".to_string()));
805            assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
806            assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
807            assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
808            assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
809            assert_eq!(init.plugins.len(), 1);
810            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
811            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
812            assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
813            assert_eq!(init.output_style, Some(super::OutputStyle::Default));
814            assert_eq!(
815                init.permission_mode,
816                Some(super::InitPermissionMode::Default)
817            );
818        } else {
819            panic!("Expected System message");
820        }
821    }
822
823    #[test]
824    fn test_system_message_init_from_real_capture() {
825        let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
826        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
827        if let ClaudeOutput::System(sys) = output {
828            let init = sys.as_init().expect("Should parse real init capture");
829            assert_eq!(init.slash_commands.len(), 8);
830            assert!(init.slash_commands.contains(&"compact".to_string()));
831            assert!(init.slash_commands.contains(&"review".to_string()));
832            assert_eq!(init.agents.len(), 5);
833            assert!(init.agents.contains(&"Bash".to_string()));
834            assert!(init.agents.contains(&"Explore".to_string()));
835            assert_eq!(init.plugins.len(), 1);
836            assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
837            assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
838        } else {
839            panic!("Expected System message");
840        }
841    }
842
843    #[test]
844    fn test_system_message_status() {
845        let json = r#"{
846            "type": "system",
847            "subtype": "status",
848            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
849            "status": "compacting",
850            "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
851        }"#;
852
853        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
854        if let ClaudeOutput::System(sys) = output {
855            assert!(sys.is_status());
856            assert!(!sys.is_init());
857
858            let status = sys.as_status().expect("Should parse as status");
859            assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
860            assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
861            assert_eq!(
862                status.uuid,
863                Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
864            );
865        } else {
866            panic!("Expected System message");
867        }
868    }
869
870    #[test]
871    fn test_system_message_status_null() {
872        let json = r#"{
873            "type": "system",
874            "subtype": "status",
875            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
876            "status": null,
877            "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
878        }"#;
879
880        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
881        if let ClaudeOutput::System(sys) = output {
882            let status = sys.as_status().expect("Should parse as status");
883            assert_eq!(status.status, None);
884        } else {
885            panic!("Expected System message");
886        }
887    }
888
889    #[test]
890    fn test_system_message_task_started() {
891        let json = r#"{
892            "type": "system",
893            "subtype": "task_started",
894            "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
895            "task_id": "b6daf3f",
896            "task_type": "local_bash",
897            "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
898            "description": "Wait for CI on PR #12",
899            "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
900        }"#;
901
902        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
903        if let ClaudeOutput::System(sys) = output {
904            assert!(sys.is_task_started());
905            assert!(!sys.is_task_progress());
906            assert!(!sys.is_task_notification());
907
908            let task = sys.as_task_started().expect("Should parse as task_started");
909            assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
910            assert_eq!(task.task_id, "b6daf3f");
911            assert_eq!(task.task_type, super::TaskType::LocalBash);
912            assert_eq!(task.tool_use_id, "toolu_011rfSTFumpJZdCCfzeD7jaS");
913            assert_eq!(task.description, "Wait for CI on PR #12");
914        } else {
915            panic!("Expected System message");
916        }
917    }
918
919    #[test]
920    fn test_system_message_task_started_agent() {
921        let json = r#"{
922            "type": "system",
923            "subtype": "task_started",
924            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
925            "task_id": "a4a7e0906e5fc64cc",
926            "task_type": "local_agent",
927            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
928            "description": "Explore Scene/ArrayScene duplication",
929            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
930        }"#;
931
932        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
933        if let ClaudeOutput::System(sys) = output {
934            let task = sys.as_task_started().expect("Should parse as task_started");
935            assert_eq!(task.task_type, super::TaskType::LocalAgent);
936            assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
937        } else {
938            panic!("Expected System message");
939        }
940    }
941
942    #[test]
943    fn test_system_message_task_progress() {
944        let json = r#"{
945            "type": "system",
946            "subtype": "task_progress",
947            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
948            "task_id": "a4a7e0906e5fc64cc",
949            "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
950            "description": "Reading src/jplephem/chebyshev.rs",
951            "last_tool_name": "Read",
952            "usage": {
953                "duration_ms": 13996,
954                "tool_uses": 9,
955                "total_tokens": 38779
956            },
957            "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
958        }"#;
959
960        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
961        if let ClaudeOutput::System(sys) = output {
962            assert!(sys.is_task_progress());
963            assert!(!sys.is_task_started());
964
965            let progress = sys
966                .as_task_progress()
967                .expect("Should parse as task_progress");
968            assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
969            assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
970            assert_eq!(progress.last_tool_name, "Read");
971            assert_eq!(progress.usage.duration_ms, 13996);
972            assert_eq!(progress.usage.tool_uses, 9);
973            assert_eq!(progress.usage.total_tokens, 38779);
974        } else {
975            panic!("Expected System message");
976        }
977    }
978
979    #[test]
980    fn test_system_message_task_notification_completed() {
981        let json = r#"{
982            "type": "system",
983            "subtype": "task_notification",
984            "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
985            "task_id": "a0ba761e9dc9c316f",
986            "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
987            "status": "completed",
988            "summary": "Agent \"Write Hipparcos data source doc\" completed",
989            "output_file": "",
990            "usage": {
991                "duration_ms": 172300,
992                "tool_uses": 11,
993                "total_tokens": 42005
994            },
995            "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
996        }"#;
997
998        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
999        if let ClaudeOutput::System(sys) = output {
1000            assert!(sys.is_task_notification());
1001
1002            let notif = sys
1003                .as_task_notification()
1004                .expect("Should parse as task_notification");
1005            assert_eq!(notif.status, super::TaskStatus::Completed);
1006            assert_eq!(
1007                notif.summary,
1008                "Agent \"Write Hipparcos data source doc\" completed"
1009            );
1010            assert_eq!(notif.output_file, Some("".to_string()));
1011            assert_eq!(
1012                notif.tool_use_id,
1013                Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
1014            );
1015            let usage = notif.usage.expect("Should have usage");
1016            assert_eq!(usage.duration_ms, 172300);
1017            assert_eq!(usage.tool_uses, 11);
1018            assert_eq!(usage.total_tokens, 42005);
1019        } else {
1020            panic!("Expected System message");
1021        }
1022    }
1023
1024    #[test]
1025    fn test_system_message_task_notification_failed_no_usage() {
1026        let json = r#"{
1027            "type": "system",
1028            "subtype": "task_notification",
1029            "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
1030            "task_id": "b98f6a3",
1031            "status": "failed",
1032            "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
1033            "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
1034        }"#;
1035
1036        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1037        if let ClaudeOutput::System(sys) = output {
1038            let notif = sys
1039                .as_task_notification()
1040                .expect("Should parse as task_notification");
1041            assert_eq!(notif.status, super::TaskStatus::Failed);
1042            assert!(notif.tool_use_id.is_none());
1043            assert!(notif.usage.is_none());
1044            assert_eq!(
1045                notif.output_file,
1046                Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
1047            );
1048        } else {
1049            panic!("Expected System message");
1050        }
1051    }
1052
1053    #[test]
1054    fn test_system_message_compact_boundary() {
1055        let json = r#"{
1056            "type": "system",
1057            "subtype": "compact_boundary",
1058            "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1059            "compact_metadata": {
1060                "pre_tokens": 155285,
1061                "trigger": "auto"
1062            },
1063            "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1064        }"#;
1065
1066        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1067        if let ClaudeOutput::System(sys) = output {
1068            assert!(sys.is_compact_boundary());
1069            assert!(!sys.is_init());
1070            assert!(!sys.is_status());
1071
1072            let compact = sys
1073                .as_compact_boundary()
1074                .expect("Should parse as compact_boundary");
1075            assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1076            assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1077            assert_eq!(
1078                compact.compact_metadata.trigger,
1079                super::CompactionTrigger::Auto
1080            );
1081        } else {
1082            panic!("Expected System message");
1083        }
1084    }
1085}