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!", uuid::Uuid::new_v4());
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, Deserializer, Serialize, Serializer};
33use serde_json::Value;
34use std::fmt;
35use tracing::debug;
36use uuid::Uuid;
37
38/// Serialize an optional UUID as a string
39fn serialize_optional_uuid<S>(uuid: &Option<Uuid>, serializer: S) -> Result<S::Ok, S::Error>
40where
41    S: Serializer,
42{
43    match uuid {
44        Some(id) => serializer.serialize_str(&id.to_string()),
45        None => serializer.serialize_none(),
46    }
47}
48
49/// Deserialize an optional UUID from a string
50fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
51where
52    D: Deserializer<'de>,
53{
54    let opt_str: Option<String> = Option::deserialize(deserializer)?;
55    match opt_str {
56        Some(s) => Uuid::parse_str(&s)
57            .map(Some)
58            .map_err(serde::de::Error::custom),
59        None => Ok(None),
60    }
61}
62
63/// Top-level enum for all possible Claude input messages
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(tag = "type", rename_all = "snake_case")]
66pub enum ClaudeInput {
67    /// User message input
68    User(UserMessage),
69
70    /// Raw JSON for untyped messages
71    #[serde(untagged)]
72    Raw(Value),
73}
74
75/// Error type for parsing failures that preserves the raw JSON
76#[derive(Debug, Clone)]
77pub struct ParseError {
78    /// The raw JSON value that failed to parse
79    pub raw_json: Value,
80    /// The underlying serde error message
81    pub error_message: String,
82}
83
84impl fmt::Display for ParseError {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        write!(f, "Failed to parse ClaudeOutput: {}", self.error_message)
87    }
88}
89
90impl std::error::Error for ParseError {}
91
92/// Top-level enum for all possible Claude output messages
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(tag = "type", rename_all = "snake_case")]
95pub enum ClaudeOutput {
96    /// System initialization message
97    System(SystemMessage),
98
99    /// User message echoed back
100    User(UserMessage),
101
102    /// Assistant response
103    Assistant(AssistantMessage),
104
105    /// Result message (completion of a query)
106    Result(ResultMessage),
107}
108
109/// User message
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct UserMessage {
112    pub message: MessageContent,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    #[serde(
115        serialize_with = "serialize_optional_uuid",
116        deserialize_with = "deserialize_optional_uuid"
117    )]
118    pub session_id: Option<Uuid>,
119}
120
121/// Message content with role
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct MessageContent {
124    pub role: String,
125    pub content: Vec<ContentBlock>,
126}
127
128/// System message with metadata
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct SystemMessage {
131    pub subtype: String,
132    #[serde(flatten)]
133    pub data: Value, // Captures all other fields
134}
135
136/// Assistant message
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct AssistantMessage {
139    pub message: AssistantMessageContent,
140    pub session_id: String,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub uuid: Option<String>,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub parent_tool_use_id: Option<String>,
145}
146
147/// Nested message content for assistant messages
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct AssistantMessageContent {
150    pub id: String,
151    pub role: String,
152    pub model: String,
153    pub content: Vec<ContentBlock>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub stop_reason: Option<String>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub stop_sequence: Option<String>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub usage: Option<serde_json::Value>,
160}
161
162/// Content blocks for messages
163#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(tag = "type", rename_all = "snake_case")]
165pub enum ContentBlock {
166    Text(TextBlock),
167    Image(ImageBlock),
168    Thinking(ThinkingBlock),
169    ToolUse(ToolUseBlock),
170    ToolResult(ToolResultBlock),
171}
172
173/// Text content block
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct TextBlock {
176    pub text: String,
177}
178
179/// Image content block (follows Anthropic API structure)
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ImageBlock {
182    pub source: ImageSource,
183}
184
185/// Image source information
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct ImageSource {
188    #[serde(rename = "type")]
189    pub source_type: String, // "base64"
190    pub media_type: String, // e.g., "image/jpeg", "image/png"
191    pub data: String,       // Base64-encoded image data
192}
193
194/// Thinking content block
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ThinkingBlock {
197    pub thinking: String,
198    pub signature: String,
199}
200
201/// Tool use content block
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ToolUseBlock {
204    pub id: String,
205    pub name: String,
206    pub input: Value,
207}
208
209/// Tool result content block
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct ToolResultBlock {
212    pub tool_use_id: String,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub content: Option<ToolResultContent>,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub is_error: Option<bool>,
217}
218
219/// Tool result content type
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(untagged)]
222pub enum ToolResultContent {
223    Text(String),
224    Structured(Vec<Value>),
225}
226
227/// Result message for completed queries
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct ResultMessage {
230    pub subtype: ResultSubtype,
231    pub is_error: bool,
232    pub duration_ms: u64,
233    pub duration_api_ms: u64,
234    pub num_turns: i32,
235
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub result: Option<String>,
238
239    pub session_id: String,
240    pub total_cost_usd: f64,
241
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub usage: Option<UsageInfo>,
244
245    #[serde(default)]
246    pub permission_denials: Vec<Value>,
247
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub uuid: Option<String>,
250}
251
252/// Result subtypes
253#[derive(Debug, Clone, Serialize, Deserialize)]
254#[serde(rename_all = "snake_case")]
255pub enum ResultSubtype {
256    Success,
257    ErrorMaxTurns,
258    ErrorDuringExecution,
259}
260
261/// MCP Server configuration types
262#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(tag = "type", rename_all = "snake_case")]
264pub enum McpServerConfig {
265    Stdio(McpStdioServerConfig),
266    Sse(McpSseServerConfig),
267    Http(McpHttpServerConfig),
268}
269
270/// MCP stdio server configuration
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct McpStdioServerConfig {
273    pub command: String,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub args: Option<Vec<String>>,
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub env: Option<std::collections::HashMap<String, String>>,
278}
279
280/// MCP SSE server configuration
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct McpSseServerConfig {
283    pub url: String,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub headers: Option<std::collections::HashMap<String, String>>,
286}
287
288/// MCP HTTP server configuration
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct McpHttpServerConfig {
291    pub url: String,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub headers: Option<std::collections::HashMap<String, String>>,
294}
295
296/// Permission mode for Claude operations
297#[derive(Debug, Clone, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub enum PermissionMode {
300    Default,
301    AcceptEdits,
302    BypassPermissions,
303    Plan,
304}
305
306/// Usage information for the request
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct UsageInfo {
309    pub input_tokens: u32,
310    pub cache_creation_input_tokens: u32,
311    pub cache_read_input_tokens: u32,
312    pub output_tokens: u32,
313    pub server_tool_use: ServerToolUse,
314    pub service_tier: String,
315}
316
317/// Server tool usage information
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct ServerToolUse {
320    pub web_search_requests: u32,
321}
322
323impl ClaudeInput {
324    /// Create a simple text user message
325    pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
326        ClaudeInput::User(UserMessage {
327            message: MessageContent {
328                role: "user".to_string(),
329                content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
330            },
331            session_id: Some(session_id),
332        })
333    }
334
335    /// Create a user message with content blocks
336    pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
337        ClaudeInput::User(UserMessage {
338            message: MessageContent {
339                role: "user".to_string(),
340                content: blocks,
341            },
342            session_id: Some(session_id),
343        })
344    }
345
346    /// Create a user message with an image and optional text
347    /// Only supports JPEG, PNG, GIF, and WebP media types
348    pub fn user_message_with_image(
349        image_data: String,
350        media_type: String,
351        text: Option<String>,
352        session_id: Uuid,
353    ) -> Result<Self, String> {
354        // Validate media type
355        let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
356
357        if !valid_types.contains(&media_type.as_str()) {
358            return Err(format!(
359                "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
360                media_type
361            ));
362        }
363
364        let mut blocks = vec![ContentBlock::Image(ImageBlock {
365            source: ImageSource {
366                source_type: "base64".to_string(),
367                media_type,
368                data: image_data,
369            },
370        })];
371
372        if let Some(text_content) = text {
373            blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
374        }
375
376        Ok(Self::user_message_blocks(blocks, session_id))
377    }
378}
379
380impl ClaudeOutput {
381    /// Get the message type as a string
382    pub fn message_type(&self) -> String {
383        match self {
384            ClaudeOutput::System(_) => "system".to_string(),
385            ClaudeOutput::User(_) => "user".to_string(),
386            ClaudeOutput::Assistant(_) => "assistant".to_string(),
387            ClaudeOutput::Result(_) => "result".to_string(),
388        }
389    }
390
391    /// Check if this is a result with error
392    pub fn is_error(&self) -> bool {
393        matches!(self, ClaudeOutput::Result(r) if r.is_error)
394    }
395
396    /// Check if this is an assistant message
397    pub fn is_assistant_message(&self) -> bool {
398        matches!(self, ClaudeOutput::Assistant(_))
399    }
400
401    /// Check if this is a system message
402    pub fn is_system_message(&self) -> bool {
403        matches!(self, ClaudeOutput::System(_))
404    }
405
406    /// Parse a JSON string, handling potential ANSI escape codes and other prefixes
407    /// This method will:
408    /// 1. First try to parse as-is
409    /// 2. If that fails, trim until it finds a '{' and try again
410    pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
411        // First try to parse as-is
412        match Self::parse_json(s) {
413            Ok(output) => Ok(output),
414            Err(first_error) => {
415                // If that fails, look for the first '{' character
416                if let Some(json_start) = s.find('{') {
417                    let trimmed = &s[json_start..];
418                    debug!(
419                        "[IO] Retrying parse after trimming {} chars of prefix",
420                        json_start
421                    );
422                    match Self::parse_json(trimmed) {
423                        Ok(output) => Ok(output),
424                        Err(_) => {
425                            // Return the original error if both attempts fail
426                            Err(first_error)
427                        }
428                    }
429                } else {
430                    Err(first_error)
431                }
432            }
433        }
434    }
435
436    /// Parse a JSON string, returning ParseError with raw JSON if it doesn't match our types
437    pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
438        debug!("[IO] Attempting to parse JSON: {}", s);
439
440        // First try to parse as a Value
441        let value: Value = serde_json::from_str(s).map_err(|e| {
442            debug!("[IO] Failed to parse as JSON Value: {}", e);
443            ParseError {
444                raw_json: Value::String(s.to_string()),
445                error_message: format!("Invalid JSON: {}", e),
446            }
447        })?;
448
449        debug!("[IO] Successfully parsed as JSON Value, attempting to deserialize as ClaudeOutput");
450
451        // Then try to parse that Value as ClaudeOutput
452        serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| {
453            debug!("[IO] Failed to deserialize as ClaudeOutput: {}", e);
454            ParseError {
455                raw_json: value,
456                error_message: e.to_string(),
457            }
458        })
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_serialize_user_message() {
468        let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
469        let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
470        let json = serde_json::to_string(&input).unwrap();
471        assert!(json.contains("\"type\":\"user\""));
472        assert!(json.contains("\"role\":\"user\""));
473        assert!(json.contains("\"text\":\"Hello, Claude!\""));
474        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
475    }
476
477    #[test]
478    fn test_deserialize_assistant_message() {
479        let json = r#"{
480            "type": "assistant",
481            "message": {
482                "id": "msg_123",
483                "role": "assistant",
484                "model": "claude-3-sonnet",
485                "content": [{"type": "text", "text": "Hello! How can I help you?"}]
486            },
487            "session_id": "123"
488        }"#;
489
490        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
491        assert!(output.is_assistant_message());
492    }
493
494    #[test]
495    fn test_deserialize_result_message() {
496        let json = r#"{
497            "type": "result",
498            "subtype": "success",
499            "is_error": false,
500            "duration_ms": 100,
501            "duration_api_ms": 200,
502            "num_turns": 1,
503            "result": "Done",
504            "session_id": "123",
505            "total_cost_usd": 0.01,
506            "permission_denials": []
507        }"#;
508
509        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
510        assert!(!output.is_error());
511    }
512}