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