Skip to main content

claude_codes/io/
result.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// Result message for completed queries
5#[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    /// Tools that were blocked due to permission denials during the session
23    #[serde(default)]
24    pub permission_denials: Vec<PermissionDenial>,
25
26    /// Error messages when `is_error` is true.
27    ///
28    /// Contains human-readable error strings (e.g., "No conversation found with session ID: ...").
29    /// This allows typed access to error conditions without needing to serialize to JSON and search.
30    #[serde(default)]
31    pub errors: Vec<String>,
32
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub uuid: Option<String>,
35}
36
37/// A record of a tool permission that was denied during the session.
38///
39/// This is included in `ResultMessage.permission_denials` to provide a summary
40/// of all permission denials that occurred.
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct PermissionDenial {
43    /// The name of the tool that was blocked (e.g., "Bash", "Write")
44    pub tool_name: String,
45
46    /// The input that was passed to the tool
47    pub tool_input: Value,
48
49    /// The unique identifier for this tool use request
50    pub tool_use_id: String,
51}
52
53/// Result subtypes
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum ResultSubtype {
57    Success,
58    ErrorMaxTurns,
59    ErrorDuringExecution,
60}
61
62/// Usage information for the request
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct UsageInfo {
65    pub input_tokens: u32,
66    pub cache_creation_input_tokens: u32,
67    pub cache_read_input_tokens: u32,
68    pub output_tokens: u32,
69    pub server_tool_use: ServerToolUse,
70    pub service_tier: String,
71}
72
73/// Server tool usage information
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ServerToolUse {
76    pub web_search_requests: u32,
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::io::ClaudeOutput;
83
84    #[test]
85    fn test_deserialize_result_message() {
86        let json = r#"{
87            "type": "result",
88            "subtype": "success",
89            "is_error": false,
90            "duration_ms": 100,
91            "duration_api_ms": 200,
92            "num_turns": 1,
93            "result": "Done",
94            "session_id": "123",
95            "total_cost_usd": 0.01,
96            "permission_denials": []
97        }"#;
98
99        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
100        assert!(!output.is_error());
101    }
102
103    #[test]
104    fn test_deserialize_result_with_permission_denials() {
105        let json = r#"{
106            "type": "result",
107            "subtype": "success",
108            "is_error": false,
109            "duration_ms": 100,
110            "duration_api_ms": 200,
111            "num_turns": 2,
112            "result": "Done",
113            "session_id": "123",
114            "total_cost_usd": 0.01,
115            "permission_denials": [
116                {
117                    "tool_name": "Bash",
118                    "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
119                    "tool_use_id": "toolu_123"
120                }
121            ]
122        }"#;
123
124        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
125        if let ClaudeOutput::Result(result) = output {
126            assert_eq!(result.permission_denials.len(), 1);
127            assert_eq!(result.permission_denials[0].tool_name, "Bash");
128            assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
129            assert_eq!(
130                result.permission_denials[0]
131                    .tool_input
132                    .get("command")
133                    .unwrap(),
134                "rm -rf /"
135            );
136        } else {
137            panic!("Expected Result");
138        }
139    }
140
141    #[test]
142    fn test_permission_denial_roundtrip() {
143        let denial = PermissionDenial {
144            tool_name: "Write".to_string(),
145            tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
146            tool_use_id: "toolu_456".to_string(),
147        };
148
149        let json = serde_json::to_string(&denial).unwrap();
150        assert!(json.contains("\"tool_name\":\"Write\""));
151        assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
152        assert!(json.contains("/etc/passwd"));
153
154        let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
155        assert_eq!(parsed, denial);
156    }
157
158    #[test]
159    fn test_deserialize_result_message_with_errors() {
160        let json = r#"{
161            "type": "result",
162            "subtype": "error_during_execution",
163            "duration_ms": 0,
164            "duration_api_ms": 0,
165            "is_error": true,
166            "num_turns": 0,
167            "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
168            "total_cost_usd": 0,
169            "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
170            "permission_denials": []
171        }"#;
172
173        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
174        assert!(output.is_error());
175
176        if let ClaudeOutput::Result(res) = output {
177            assert!(res.is_error);
178            assert_eq!(res.errors.len(), 1);
179            assert!(res.errors[0].contains("No conversation found"));
180        } else {
181            panic!("Expected Result message");
182        }
183    }
184
185    #[test]
186    fn test_deserialize_result_message_errors_defaults_empty() {
187        let json = r#"{
188            "type": "result",
189            "subtype": "success",
190            "is_error": false,
191            "duration_ms": 100,
192            "duration_api_ms": 200,
193            "num_turns": 1,
194            "session_id": "123",
195            "total_cost_usd": 0.01
196        }"#;
197
198        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
199        if let ClaudeOutput::Result(res) = output {
200            assert!(res.errors.is_empty());
201        } else {
202            panic!("Expected Result message");
203        }
204    }
205
206    #[test]
207    fn test_result_message_errors_roundtrip() {
208        let json = r#"{
209            "type": "result",
210            "subtype": "error_during_execution",
211            "is_error": true,
212            "duration_ms": 0,
213            "duration_api_ms": 0,
214            "num_turns": 0,
215            "session_id": "test-session",
216            "total_cost_usd": 0.0,
217            "errors": ["Error 1", "Error 2"]
218        }"#;
219
220        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
221        let reserialized = serde_json::to_string(&output).unwrap();
222
223        assert!(reserialized.contains("Error 1"));
224        assert!(reserialized.contains("Error 2"));
225    }
226}