claude_codes/
io.rs

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