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