1use serde::{Deserialize, Deserializer, Serialize, Serializer};
33use serde_json::Value;
34use std::fmt;
35use tracing::debug;
36use uuid::Uuid;
37
38fn 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
49fn 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#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(tag = "type", rename_all = "snake_case")]
66pub enum ClaudeInput {
67 User(UserMessage),
69
70 #[serde(untagged)]
72 Raw(Value),
73}
74
75#[derive(Debug, Clone)]
77pub struct ParseError {
78 pub raw_json: Value,
80 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#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(tag = "type", rename_all = "snake_case")]
95pub enum ClaudeOutput {
96 System(SystemMessage),
98
99 User(UserMessage),
101
102 Assistant(AssistantMessage),
104
105 Result(ResultMessage),
107}
108
109#[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#[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
129fn 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#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct SystemMessage {
147 pub subtype: String,
148 #[serde(flatten)]
149 pub data: Value, }
151
152#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct TextBlock {
192 pub text: String,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ImageBlock {
198 pub source: ImageSource,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ImageSource {
204 #[serde(rename = "type")]
205 pub source_type: String, pub media_type: String, pub data: String, }
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct ThinkingBlock {
213 pub thinking: String,
214 pub signature: String,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct ToolUseBlock {
220 pub id: String,
221 pub name: String,
222 pub input: Value,
223}
224
225#[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#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(untagged)]
238pub enum ToolResultContent {
239 Text(String),
240 Structured(Vec<Value>),
241}
242
243#[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#[derive(Debug, Clone, Serialize, Deserialize)]
270#[serde(rename_all = "snake_case")]
271pub enum ResultSubtype {
272 Success,
273 ErrorMaxTurns,
274 ErrorDuringExecution,
275}
276
277#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase")]
315pub enum PermissionMode {
316 Default,
317 AcceptEdits,
318 BypassPermissions,
319 Plan,
320}
321
322#[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#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct ServerToolUse {
336 pub web_search_requests: u32,
337}
338
339impl ClaudeInput {
340 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 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 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 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 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 pub fn is_error(&self) -> bool {
409 matches!(self, ClaudeOutput::Result(r) if r.is_error)
410 }
411
412 pub fn is_assistant_message(&self) -> bool {
414 matches!(self, ClaudeOutput::Assistant(_))
415 }
416
417 pub fn is_system_message(&self) -> bool {
419 matches!(self, ClaudeOutput::System(_))
420 }
421
422 pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
427 match Self::parse_json(s) {
429 Ok(output) => Ok(output),
430 Err(first_error) => {
431 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 Err(first_error)
443 }
444 }
445 } else {
446 Err(first_error)
447 }
448 }
449 }
450 }
451
452 pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
454 debug!("[IO] Attempting to parse JSON: {}", s);
455
456 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 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}