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