claude_codes/
io.rs

1//! Top-level I/O types for Claude communication
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::fmt;
6use tracing::debug;
7
8/// Top-level enum for all possible Claude input messages
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum ClaudeInput {
12    /// User message input
13    User(UserMessage),
14
15    /// Raw JSON for untyped messages
16    #[serde(untagged)]
17    Raw(Value),
18}
19
20/// Error type for parsing failures that preserves the raw JSON
21#[derive(Debug, Clone)]
22pub struct ParseError {
23    /// The raw JSON value that failed to parse
24    pub raw_json: Value,
25    /// The underlying serde error message
26    pub error_message: String,
27}
28
29impl fmt::Display for ParseError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        write!(f, "Failed to parse ClaudeOutput: {}", self.error_message)
32    }
33}
34
35impl std::error::Error for ParseError {}
36
37/// Top-level enum for all possible Claude output messages
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(tag = "type", rename_all = "snake_case")]
40pub enum ClaudeOutput {
41    /// System initialization message
42    System(SystemMessage),
43
44    /// User message echoed back
45    User(UserMessage),
46
47    /// Assistant response
48    Assistant(AssistantMessage),
49
50    /// Result message (completion of a query)
51    Result(ResultMessage),
52}
53
54/// User message
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct UserMessage {
57    pub message: MessageContent,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub session_id: Option<String>,
60}
61
62/// Message content with role
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct MessageContent {
65    pub role: String,
66    pub content: Vec<ContentBlock>,
67}
68
69/// System message with metadata
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct SystemMessage {
72    pub subtype: String,
73    #[serde(flatten)]
74    pub data: Value, // Captures all other fields
75}
76
77/// Assistant message
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct AssistantMessage {
80    pub message: AssistantMessageContent,
81    pub session_id: String,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub uuid: Option<String>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub parent_tool_use_id: Option<String>,
86}
87
88/// Nested message content for assistant messages
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct AssistantMessageContent {
91    pub id: String,
92    pub role: String,
93    pub model: String,
94    pub content: Vec<ContentBlock>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub stop_reason: Option<String>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub stop_sequence: Option<String>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub usage: Option<serde_json::Value>,
101}
102
103/// Content blocks for messages
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(tag = "type", rename_all = "snake_case")]
106pub enum ContentBlock {
107    Text(TextBlock),
108    Thinking(ThinkingBlock),
109    ToolUse(ToolUseBlock),
110    ToolResult(ToolResultBlock),
111}
112
113/// Text content block
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct TextBlock {
116    pub text: String,
117}
118
119/// Thinking content block
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ThinkingBlock {
122    pub thinking: String,
123    pub signature: String,
124}
125
126/// Tool use content block
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ToolUseBlock {
129    pub id: String,
130    pub name: String,
131    pub input: Value,
132}
133
134/// Tool result content block
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ToolResultBlock {
137    pub tool_use_id: String,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub content: Option<ToolResultContent>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub is_error: Option<bool>,
142}
143
144/// Tool result content type
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(untagged)]
147pub enum ToolResultContent {
148    Text(String),
149    Structured(Vec<Value>),
150}
151
152/// Result message for completed queries
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct ResultMessage {
155    pub subtype: ResultSubtype,
156    pub is_error: bool,
157    pub duration_ms: u64,
158    pub duration_api_ms: u64,
159    pub num_turns: u32,
160
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub result: Option<String>,
163
164    pub session_id: String,
165    pub total_cost_usd: f64,
166
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub usage: Option<UsageInfo>,
169
170    #[serde(default)]
171    pub permission_denials: Vec<Value>,
172
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub uuid: Option<String>,
175}
176
177/// Result subtypes
178#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(rename_all = "snake_case")]
180pub enum ResultSubtype {
181    Success,
182    ErrorMaxTurns,
183    ErrorDuringExecution,
184}
185
186/// MCP Server configuration types
187#[derive(Debug, Clone, Serialize, Deserialize)]
188#[serde(tag = "type", rename_all = "snake_case")]
189pub enum McpServerConfig {
190    Stdio(McpStdioServerConfig),
191    Sse(McpSseServerConfig),
192    Http(McpHttpServerConfig),
193}
194
195/// MCP stdio server configuration
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct McpStdioServerConfig {
198    pub command: String,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub args: Option<Vec<String>>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub env: Option<std::collections::HashMap<String, String>>,
203}
204
205/// MCP SSE server configuration
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct McpSseServerConfig {
208    pub url: String,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub headers: Option<std::collections::HashMap<String, String>>,
211}
212
213/// MCP HTTP server configuration
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct McpHttpServerConfig {
216    pub url: String,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub headers: Option<std::collections::HashMap<String, String>>,
219}
220
221/// Permission mode for Claude operations
222#[derive(Debug, Clone, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub enum PermissionMode {
225    Default,
226    AcceptEdits,
227    BypassPermissions,
228    Plan,
229}
230
231/// Usage information for the request
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct UsageInfo {
234    pub input_tokens: u32,
235    pub cache_creation_input_tokens: u32,
236    pub cache_read_input_tokens: u32,
237    pub output_tokens: u32,
238    pub server_tool_use: ServerToolUse,
239    pub service_tier: String,
240}
241
242/// Server tool usage information
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct ServerToolUse {
245    pub web_search_requests: u32,
246}
247
248impl ClaudeInput {
249    /// Create a simple text user message
250    pub fn user_message(text: impl Into<String>, session_id: impl Into<String>) -> Self {
251        ClaudeInput::User(UserMessage {
252            message: MessageContent {
253                role: "user".to_string(),
254                content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
255            },
256            session_id: Some(session_id.into()),
257        })
258    }
259
260    /// Create a user message with content blocks
261    pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: impl Into<String>) -> Self {
262        ClaudeInput::User(UserMessage {
263            message: MessageContent {
264                role: "user".to_string(),
265                content: blocks,
266            },
267            session_id: Some(session_id.into()),
268        })
269    }
270}
271
272impl ClaudeOutput {
273    /// Get the message type as a string
274    pub fn message_type(&self) -> String {
275        match self {
276            ClaudeOutput::System(_) => "system".to_string(),
277            ClaudeOutput::User(_) => "user".to_string(),
278            ClaudeOutput::Assistant(_) => "assistant".to_string(),
279            ClaudeOutput::Result(_) => "result".to_string(),
280        }
281    }
282
283    /// Check if this is a result with error
284    pub fn is_error(&self) -> bool {
285        matches!(self, ClaudeOutput::Result(r) if r.is_error)
286    }
287
288    /// Check if this is an assistant message
289    pub fn is_assistant_message(&self) -> bool {
290        matches!(self, ClaudeOutput::Assistant(_))
291    }
292
293    /// Check if this is a system message
294    pub fn is_system_message(&self) -> bool {
295        matches!(self, ClaudeOutput::System(_))
296    }
297
298    /// Parse a JSON string, returning ParseError with raw JSON if it doesn't match our types
299    pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
300        debug!("[IO] Attempting to parse JSON: {}", s);
301
302        // First try to parse as a Value
303        let value: Value = serde_json::from_str(s).map_err(|e| {
304            debug!("[IO] Failed to parse as JSON Value: {}", e);
305            ParseError {
306                raw_json: Value::String(s.to_string()),
307                error_message: format!("Invalid JSON: {}", e),
308            }
309        })?;
310
311        debug!("[IO] Successfully parsed as JSON Value, attempting to deserialize as ClaudeOutput");
312
313        // Then try to parse that Value as ClaudeOutput
314        serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| {
315            debug!("[IO] Failed to deserialize as ClaudeOutput: {}", e);
316            ParseError {
317                raw_json: value,
318                error_message: e.to_string(),
319            }
320        })
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_serialize_user_message() {
330        let input = ClaudeInput::user_message("Hello, Claude!", "session-123");
331        let json = serde_json::to_string(&input).unwrap();
332        assert!(json.contains("\"type\":\"user\""));
333        assert!(json.contains("\"role\":\"user\""));
334        assert!(json.contains("\"text\":\"Hello, Claude!\""));
335        assert!(json.contains("session-123"));
336    }
337
338    #[test]
339    fn test_deserialize_assistant_message() {
340        let json = r#"{
341            "type": "assistant",
342            "message": {
343                "id": "msg_123",
344                "role": "assistant",
345                "model": "claude-3-sonnet",
346                "content": [{"type": "text", "text": "Hello! How can I help you?"}]
347            },
348            "session_id": "123"
349        }"#;
350
351        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
352        assert!(output.is_assistant_message());
353    }
354
355    #[test]
356    fn test_deserialize_result_message() {
357        let json = r#"{
358            "type": "result",
359            "subtype": "success",
360            "is_error": false,
361            "duration_ms": 100,
362            "duration_api_ms": 200,
363            "num_turns": 1,
364            "result": "Done",
365            "session_id": "123",
366            "total_cost_usd": 0.01,
367            "permission_denials": []
368        }"#;
369
370        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
371        assert!(!output.is_error());
372    }
373}