1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use uuid::Uuid;
4
5use super::content_blocks::{deserialize_content_blocks, ContentBlock};
6
7pub(crate) fn serialize_optional_uuid<S>(
9 uuid: &Option<Uuid>,
10 serializer: S,
11) -> Result<S::Ok, S::Error>
12where
13 S: Serializer,
14{
15 match uuid {
16 Some(id) => serializer.serialize_str(&id.to_string()),
17 None => serializer.serialize_none(),
18 }
19}
20
21pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
23where
24 D: Deserializer<'de>,
25{
26 let opt_str: Option<String> = Option::deserialize(deserializer)?;
27 match opt_str {
28 Some(s) => Uuid::parse_str(&s)
29 .map(Some)
30 .map_err(serde::de::Error::custom),
31 None => Ok(None),
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct UserMessage {
38 pub message: MessageContent,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 #[serde(
41 serialize_with = "serialize_optional_uuid",
42 deserialize_with = "deserialize_optional_uuid"
43 )]
44 pub session_id: Option<Uuid>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct MessageContent {
50 pub role: String,
51 #[serde(deserialize_with = "deserialize_content_blocks")]
52 pub content: Vec<ContentBlock>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct SystemMessage {
58 pub subtype: String,
59 #[serde(flatten)]
60 pub data: Value, }
62
63impl SystemMessage {
64 pub fn is_init(&self) -> bool {
66 self.subtype == "init"
67 }
68
69 pub fn is_status(&self) -> bool {
71 self.subtype == "status"
72 }
73
74 pub fn is_compact_boundary(&self) -> bool {
76 self.subtype == "compact_boundary"
77 }
78
79 pub fn as_init(&self) -> Option<InitMessage> {
81 if self.subtype != "init" {
82 return None;
83 }
84 serde_json::from_value(self.data.clone()).ok()
85 }
86
87 pub fn as_status(&self) -> Option<StatusMessage> {
89 if self.subtype != "status" {
90 return None;
91 }
92 serde_json::from_value(self.data.clone()).ok()
93 }
94
95 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
97 if self.subtype != "compact_boundary" {
98 return None;
99 }
100 serde_json::from_value(self.data.clone()).ok()
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct InitMessage {
107 pub session_id: String,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub cwd: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub model: Option<String>,
115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
117 pub tools: Vec<String>,
118 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub mcp_servers: Vec<Value>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct StatusMessage {
126 pub session_id: String,
128 pub status: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub uuid: Option<String>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct CompactBoundaryMessage {
138 pub session_id: String,
140 pub compact_metadata: CompactMetadata,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub uuid: Option<String>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct CompactMetadata {
150 pub pre_tokens: u64,
152 pub trigger: String,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct AssistantMessage {
159 pub message: AssistantMessageContent,
160 pub session_id: String,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub uuid: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub parent_tool_use_id: Option<String>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct AssistantMessageContent {
170 pub id: String,
171 pub role: String,
172 pub model: String,
173 pub content: Vec<ContentBlock>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub stop_reason: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub stop_sequence: Option<String>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub usage: Option<AssistantUsage>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct AssistantUsage {
185 #[serde(default)]
187 pub input_tokens: u32,
188
189 #[serde(default)]
191 pub output_tokens: u32,
192
193 #[serde(default)]
195 pub cache_creation_input_tokens: u32,
196
197 #[serde(default)]
199 pub cache_read_input_tokens: u32,
200
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub service_tier: Option<String>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub cache_creation: Option<CacheCreationDetails>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct CacheCreationDetails {
213 #[serde(default)]
215 pub ephemeral_1h_input_tokens: u32,
216
217 #[serde(default)]
219 pub ephemeral_5m_input_tokens: u32,
220}
221
222#[cfg(test)]
223mod tests {
224 use crate::io::ClaudeOutput;
225
226 #[test]
227 fn test_system_message_init() {
228 let json = r#"{
229 "type": "system",
230 "subtype": "init",
231 "session_id": "test-session-123",
232 "cwd": "/home/user/project",
233 "model": "claude-sonnet-4",
234 "tools": ["Bash", "Read", "Write"],
235 "mcp_servers": []
236 }"#;
237
238 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
239 if let ClaudeOutput::System(sys) = output {
240 assert!(sys.is_init());
241 assert!(!sys.is_status());
242 assert!(!sys.is_compact_boundary());
243
244 let init = sys.as_init().expect("Should parse as init");
245 assert_eq!(init.session_id, "test-session-123");
246 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
247 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
248 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
249 } else {
250 panic!("Expected System message");
251 }
252 }
253
254 #[test]
255 fn test_system_message_status() {
256 let json = r#"{
257 "type": "system",
258 "subtype": "status",
259 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
260 "status": "compacting",
261 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
262 }"#;
263
264 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
265 if let ClaudeOutput::System(sys) = output {
266 assert!(sys.is_status());
267 assert!(!sys.is_init());
268
269 let status = sys.as_status().expect("Should parse as status");
270 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
271 assert_eq!(status.status, Some("compacting".to_string()));
272 assert_eq!(
273 status.uuid,
274 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
275 );
276 } else {
277 panic!("Expected System message");
278 }
279 }
280
281 #[test]
282 fn test_system_message_status_null() {
283 let json = r#"{
284 "type": "system",
285 "subtype": "status",
286 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
287 "status": null,
288 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
289 }"#;
290
291 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
292 if let ClaudeOutput::System(sys) = output {
293 let status = sys.as_status().expect("Should parse as status");
294 assert_eq!(status.status, None);
295 } else {
296 panic!("Expected System message");
297 }
298 }
299
300 #[test]
301 fn test_system_message_compact_boundary() {
302 let json = r#"{
303 "type": "system",
304 "subtype": "compact_boundary",
305 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
306 "compact_metadata": {
307 "pre_tokens": 155285,
308 "trigger": "auto"
309 },
310 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
311 }"#;
312
313 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
314 if let ClaudeOutput::System(sys) = output {
315 assert!(sys.is_compact_boundary());
316 assert!(!sys.is_init());
317 assert!(!sys.is_status());
318
319 let compact = sys
320 .as_compact_boundary()
321 .expect("Should parse as compact_boundary");
322 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
323 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
324 assert_eq!(compact.compact_metadata.trigger, "auto");
325 } else {
326 panic!("Expected System message");
327 }
328 }
329}