1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ResultMessage {
7 pub subtype: ResultSubtype,
8 pub is_error: bool,
9 pub duration_ms: u64,
10 pub duration_api_ms: u64,
11 pub num_turns: i32,
12
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub result: Option<String>,
15
16 pub session_id: String,
17 pub total_cost_usd: f64,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub usage: Option<UsageInfo>,
21
22 #[serde(default)]
24 pub permission_denials: Vec<PermissionDenial>,
25
26 #[serde(default)]
31 pub errors: Vec<String>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub uuid: Option<String>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub api_error_status: Option<u16>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub stop_reason: Option<String>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub terminal_reason: Option<String>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub fast_mode_state: Option<String>,
51
52 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
54 pub model_usage: Option<Value>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62pub struct PermissionDenial {
63 pub tool_name: String,
65
66 pub tool_input: Value,
68
69 pub tool_use_id: String,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum ResultSubtype {
77 Success,
78 ErrorMaxTurns,
79 ErrorDuringExecution,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct UsageInfo {
85 #[serde(default)]
86 pub input_tokens: u32,
87 #[serde(default)]
88 pub cache_creation_input_tokens: u32,
89 #[serde(default)]
90 pub cache_read_input_tokens: u32,
91 #[serde(default)]
92 pub output_tokens: u32,
93 #[serde(default)]
94 pub server_tool_use: ServerToolUse,
95 #[serde(default)]
96 pub service_tier: String,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub inference_geo: Option<String>,
105
106 #[serde(default, skip_serializing_if = "Vec::is_empty")]
108 pub iterations: Vec<Value>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub speed: Option<String>,
113}
114
115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
117pub struct ServerToolUse {
118 #[serde(default)]
119 pub web_search_requests: u32,
120 #[serde(default)]
122 pub web_fetch_requests: u32,
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::io::ClaudeOutput;
129
130 #[test]
131 fn test_deserialize_result_message() {
132 let json = r#"{
133 "type": "result",
134 "subtype": "success",
135 "is_error": false,
136 "duration_ms": 100,
137 "duration_api_ms": 200,
138 "num_turns": 1,
139 "result": "Done",
140 "session_id": "123",
141 "total_cost_usd": 0.01,
142 "permission_denials": []
143 }"#;
144
145 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
146 assert!(!output.is_error());
147 }
148
149 #[test]
150 fn test_deserialize_result_with_permission_denials() {
151 let json = r#"{
152 "type": "result",
153 "subtype": "success",
154 "is_error": false,
155 "duration_ms": 100,
156 "duration_api_ms": 200,
157 "num_turns": 2,
158 "result": "Done",
159 "session_id": "123",
160 "total_cost_usd": 0.01,
161 "permission_denials": [
162 {
163 "tool_name": "Bash",
164 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
165 "tool_use_id": "toolu_123"
166 }
167 ]
168 }"#;
169
170 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
171 if let ClaudeOutput::Result(result) = output {
172 assert_eq!(result.permission_denials.len(), 1);
173 assert_eq!(result.permission_denials[0].tool_name, "Bash");
174 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
175 assert_eq!(
176 result.permission_denials[0]
177 .tool_input
178 .get("command")
179 .unwrap(),
180 "rm -rf /"
181 );
182 } else {
183 panic!("Expected Result");
184 }
185 }
186
187 #[test]
188 fn test_permission_denial_roundtrip() {
189 let denial = PermissionDenial {
190 tool_name: "Write".to_string(),
191 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
192 tool_use_id: "toolu_456".to_string(),
193 };
194
195 let json = serde_json::to_string(&denial).unwrap();
196 assert!(json.contains("\"tool_name\":\"Write\""));
197 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
198 assert!(json.contains("/etc/passwd"));
199
200 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
201 assert_eq!(parsed, denial);
202 }
203
204 #[test]
205 fn test_deserialize_result_message_with_errors() {
206 let json = r#"{
207 "type": "result",
208 "subtype": "error_during_execution",
209 "duration_ms": 0,
210 "duration_api_ms": 0,
211 "is_error": true,
212 "num_turns": 0,
213 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
214 "total_cost_usd": 0,
215 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
216 "permission_denials": []
217 }"#;
218
219 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
220 assert!(output.is_error());
221
222 if let ClaudeOutput::Result(res) = output {
223 assert!(res.is_error);
224 assert_eq!(res.errors.len(), 1);
225 assert!(res.errors[0].contains("No conversation found"));
226 } else {
227 panic!("Expected Result message");
228 }
229 }
230
231 #[test]
232 fn test_deserialize_result_message_errors_defaults_empty() {
233 let json = r#"{
234 "type": "result",
235 "subtype": "success",
236 "is_error": false,
237 "duration_ms": 100,
238 "duration_api_ms": 200,
239 "num_turns": 1,
240 "session_id": "123",
241 "total_cost_usd": 0.01
242 }"#;
243
244 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
245 if let ClaudeOutput::Result(res) = output {
246 assert!(res.errors.is_empty());
247 } else {
248 panic!("Expected Result message");
249 }
250 }
251
252 #[test]
253 fn test_result_message_errors_roundtrip() {
254 let json = r#"{
255 "type": "result",
256 "subtype": "error_during_execution",
257 "is_error": true,
258 "duration_ms": 0,
259 "duration_api_ms": 0,
260 "num_turns": 0,
261 "session_id": "test-session",
262 "total_cost_usd": 0.0,
263 "errors": ["Error 1", "Error 2"]
264 }"#;
265
266 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
267 let reserialized = serde_json::to_string(&output).unwrap();
268
269 assert!(reserialized.contains("Error 1"));
270 assert!(reserialized.contains("Error 2"));
271 }
272
273 #[test]
274 fn test_result_with_new_fields() {
275 let json = r#"{
276 "type": "result",
277 "subtype": "success",
278 "is_error": false,
279 "duration_ms": 5000,
280 "duration_api_ms": 4500,
281 "num_turns": 1,
282 "result": "Done",
283 "session_id": "abc",
284 "total_cost_usd": 0.06,
285 "api_error_status": null,
286 "stop_reason": "end_turn",
287 "terminal_reason": "completed",
288 "fast_mode_state": "off",
289 "modelUsage": {
290 "claude-opus-4-7[1m]": {
291 "inputTokens": 3817,
292 "outputTokens": 14,
293 "costUSD": 0.06
294 }
295 },
296 "usage": {
297 "input_tokens": 3817,
298 "output_tokens": 14,
299 "cache_creation_input_tokens": 3540,
300 "cache_read_input_tokens": 0,
301 "server_tool_use": {
302 "web_search_requests": 0,
303 "web_fetch_requests": 2
304 },
305 "service_tier": "standard",
306 "inference_geo": "not_available",
307 "speed": "standard",
308 "iterations": [
309 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
310 ]
311 }
312 }"#;
313
314 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
315 if let ClaudeOutput::Result(res) = output {
316 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
317 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
318 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
319 assert!(res.model_usage.is_some());
320 assert!(res.api_error_status.is_none());
321
322 let usage = res.usage.unwrap();
323 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
324 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
325 assert_eq!(usage.speed.as_deref(), Some("standard"));
326 assert_eq!(usage.iterations.len(), 1);
327 } else {
328 panic!("Expected Result");
329 }
330 }
331
332 #[test]
333 fn test_result_backwards_compatible_without_new_fields() {
334 let json = r#"{
336 "type": "result",
337 "subtype": "success",
338 "is_error": false,
339 "duration_ms": 100,
340 "duration_api_ms": 200,
341 "num_turns": 1,
342 "session_id": "abc",
343 "total_cost_usd": 0.01
344 }"#;
345
346 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
347 if let ClaudeOutput::Result(res) = output {
348 assert!(res.api_error_status.is_none());
349 assert!(res.stop_reason.is_none());
350 assert!(res.terminal_reason.is_none());
351 assert!(res.fast_mode_state.is_none());
352 assert!(res.model_usage.is_none());
353 } else {
354 panic!("Expected Result");
355 }
356 }
357}