Skip to main content

claude_code_rs/types/
messages.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use super::content::ContentBlock;
5
6/// A message from the Claude CLI streaming protocol.
7///
8/// The CLI emits newline-delimited JSON objects with a top-level `type` field.
9/// Each variant corresponds to one of these message types.
10#[derive(Debug, Clone)]
11pub enum Message {
12    /// System-level message (init acknowledgment, etc.)
13    System {
14        subtype: String,
15        data: Value,
16    },
17
18    /// Assistant (Claude) response message.
19    Assistant {
20        message: AssistantMessage,
21    },
22
23    /// User message echo.
24    User {
25        message: UserMessage,
26    },
27
28    /// Result/completion message - signals end of a turn.
29    Result {
30        result: ResultMessage,
31    },
32
33    /// An unknown message type we don't recognize but preserve.
34    Unknown {
35        message_type: String,
36        raw: Value,
37    },
38}
39
40/// An assistant response with content blocks.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct AssistantMessage {
43    #[serde(default)]
44    pub id: Option<String>,
45    #[serde(default)]
46    pub model: Option<String>,
47    #[serde(default)]
48    pub content: Vec<ContentBlock>,
49    #[serde(default)]
50    pub stop_reason: Option<String>,
51    #[serde(default)]
52    pub usage: Option<Usage>,
53    /// Raw extra fields we don't explicitly model.
54    #[serde(flatten)]
55    pub extra: Value,
56}
57
58/// A user message as echoed back by the CLI.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct UserMessage {
61    #[serde(default)]
62    pub id: Option<String>,
63    #[serde(default)]
64    pub content: UserContent,
65    #[serde(flatten)]
66    pub extra: Value,
67}
68
69/// User message content can be a string or structured blocks.
70#[derive(Debug, Clone, Serialize, Deserialize, Default)]
71#[serde(untagged)]
72pub enum UserContent {
73    Text(String),
74    Blocks(Vec<ContentBlock>),
75    #[default]
76    Empty,
77}
78
79/// Result message indicating the end of a query turn.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ResultMessage {
82    #[serde(default)]
83    pub subtype: Option<String>,
84    #[serde(default)]
85    pub is_error: bool,
86    #[serde(default)]
87    pub error: Option<String>,
88    #[serde(default)]
89    pub duration_ms: Option<f64>,
90    #[serde(default)]
91    pub duration_api_ms: Option<f64>,
92    #[serde(default)]
93    pub num_turns: Option<u32>,
94    #[serde(default)]
95    pub session_id: Option<String>,
96    #[serde(default)]
97    pub cost_usd: Option<f64>,
98    #[serde(default)]
99    pub total_cost_usd: Option<f64>,
100    #[serde(default)]
101    pub usage: Option<Usage>,
102    #[serde(flatten)]
103    pub extra: Value,
104}
105
106/// Token usage information.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Usage {
109    #[serde(default)]
110    pub input_tokens: Option<u64>,
111    #[serde(default)]
112    pub output_tokens: Option<u64>,
113    #[serde(default)]
114    pub cache_creation_input_tokens: Option<u64>,
115    #[serde(default)]
116    pub cache_read_input_tokens: Option<u64>,
117    #[serde(flatten)]
118    pub extra: Value,
119}
120
121impl Message {
122    /// Returns true if this is a Result message (end of turn).
123    pub fn is_result(&self) -> bool {
124        matches!(self, Message::Result { .. })
125    }
126
127    /// Returns true if this is a Result message with an error.
128    pub fn is_error(&self) -> bool {
129        matches!(self, Message::Result { result } if result.is_error)
130    }
131
132    /// Extract all text content from an Assistant message.
133    pub fn text(&self) -> Option<String> {
134        match self {
135            Message::Assistant { message } => {
136                let texts: Vec<&str> = message
137                    .content
138                    .iter()
139                    .filter_map(|b| b.as_text())
140                    .collect();
141                if texts.is_empty() {
142                    None
143                } else {
144                    Some(texts.join(""))
145                }
146            }
147            _ => None,
148        }
149    }
150
151    /// Get the session ID from a Result message.
152    pub fn session_id(&self) -> Option<&str> {
153        match self {
154            Message::Result { result } => result.session_id.as_deref(),
155            _ => None,
156        }
157    }
158}