1use serde::de::{self, Deserializer};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use crate::output::{
13 AgentOutput, ContentBlock as UnifiedContentBlock, Event as UnifiedEvent, ToolResult,
14 Usage as UnifiedUsage,
15};
16
17pub type ClaudeOutput = Vec<ClaudeEvent>;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(tag = "type", rename_all = "snake_case")]
23pub enum ClaudeEvent {
24 System {
26 subtype: String,
27 session_id: String,
28 cwd: Option<String>,
29 model: String,
30 tools: Vec<String>,
31 #[serde(default)]
32 mcp_servers: Vec<serde_json::Value>,
33 #[serde(rename = "permissionMode")]
34 permission_mode: Option<String>,
35 #[serde(default)]
36 slash_commands: Vec<String>,
37 #[serde(default)]
38 agents: Vec<String>,
39 #[serde(default)]
40 skills: Vec<serde_json::Value>,
41 #[serde(default)]
42 plugins: Vec<Plugin>,
43 uuid: String,
44 #[serde(flatten)]
45 extra: HashMap<String, serde_json::Value>,
46 },
47
48 Assistant {
50 message: Message,
51 parent_tool_use_id: Option<String>,
52 session_id: String,
53 uuid: String,
54 },
55
56 User {
58 message: UserMessage,
59 parent_tool_use_id: Option<String>,
60 session_id: String,
61 uuid: String,
62 tool_use_result: Option<serde_json::Value>,
63 },
64
65 Result {
67 subtype: String,
68 is_error: bool,
69 duration_ms: u64,
70 duration_api_ms: u64,
71 num_turns: u32,
72 result: String,
73 session_id: String,
74 total_cost_usd: f64,
75 usage: Usage,
76 #[serde(default, rename = "modelUsage")]
77 model_usage: HashMap<String, ModelUsage>,
78 #[serde(default)]
79 permission_denials: Vec<PermissionDenial>,
80 #[serde(default)]
84 structured_output: Option<serde_json::Value>,
85 uuid: String,
86 },
87
88 #[serde(other)]
90 Other,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct Message {
96 pub model: String,
97 pub id: String,
98 #[serde(rename = "type")]
99 pub message_type: String,
100 pub role: String,
101 pub content: Vec<ContentBlock>,
102 pub stop_reason: Option<String>,
103 pub stop_sequence: Option<String>,
104 pub usage: Usage,
105 pub context_management: Option<serde_json::Value>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct UserMessage {
111 pub role: String,
112 pub content: Vec<UserContentBlock>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(tag = "type", rename_all = "snake_case")]
118pub enum ContentBlock {
119 Text { text: String },
121
122 ToolUse {
124 id: String,
125 name: String,
126 input: serde_json::Value,
127 },
128
129 Thinking {
131 #[serde(default)]
132 thinking: String,
133 #[serde(flatten)]
134 extra: HashMap<String, serde_json::Value>,
135 },
136
137 #[serde(other)]
139 Other,
140}
141
142fn deserialize_content_string_or_array<'de, D>(deserializer: D) -> Result<String, D::Error>
150where
151 D: Deserializer<'de>,
152{
153 let value: serde_json::Value = Deserialize::deserialize(deserializer)?;
154 match value {
155 serde_json::Value::String(s) => Ok(s),
156 serde_json::Value::Array(arr) => {
157 let texts: Vec<String> = arr
158 .into_iter()
159 .filter_map(|block| {
160 block
161 .get("text")
162 .and_then(|t| t.as_str())
163 .map(|s| s.to_string())
164 })
165 .collect();
166 Ok(texts.join("\n"))
167 }
168 serde_json::Value::Null => Ok(String::new()),
169 other => Err(de::Error::custom(format!(
170 "expected string or array for content, got {other}"
171 ))),
172 }
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177#[serde(tag = "type", rename_all = "snake_case")]
178pub enum UserContentBlock {
179 ToolResult {
181 tool_use_id: String,
182 #[serde(deserialize_with = "deserialize_content_string_or_array")]
183 content: String,
184 #[serde(default)]
185 is_error: bool,
186 },
187
188 Text { text: String },
190
191 #[serde(other)]
193 Other,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct Usage {
199 pub input_tokens: u64,
200 #[serde(default)]
201 pub cache_creation_input_tokens: u64,
202 #[serde(default)]
203 pub cache_read_input_tokens: u64,
204 pub output_tokens: u64,
205 #[serde(default)]
206 pub cache_creation: Option<CacheCreation>,
207 #[serde(default)]
208 pub server_tool_use: Option<ServerToolUse>,
209 #[serde(default)]
210 pub service_tier: Option<String>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct CacheCreation {
216 #[serde(default)]
217 pub ephemeral_5m_input_tokens: u64,
218 #[serde(default)]
219 pub ephemeral_1h_input_tokens: u64,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct ServerToolUse {
225 #[serde(default)]
226 pub web_search_requests: u32,
227 #[serde(default)]
228 pub web_fetch_requests: u32,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct ModelUsage {
234 #[serde(rename = "inputTokens")]
235 pub input_tokens: u64,
236 #[serde(rename = "outputTokens")]
237 pub output_tokens: u64,
238 #[serde(default, rename = "cacheReadInputTokens")]
239 pub cache_read_input_tokens: u64,
240 #[serde(default, rename = "cacheCreationInputTokens")]
241 pub cache_creation_input_tokens: u64,
242 #[serde(default, rename = "webSearchRequests")]
243 pub web_search_requests: u32,
244 #[serde(rename = "costUSD")]
245 pub cost_usd: f64,
246 #[serde(default, rename = "contextWindow")]
247 pub context_window: u64,
248 #[serde(default, rename = "maxOutputTokens")]
249 pub max_output_tokens: u64,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct PermissionDenial {
255 pub tool_name: String,
256 pub tool_use_id: String,
257 pub tool_input: serde_json::Value,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct Plugin {
263 pub name: String,
264 pub path: String,
265}
266
267pub fn claude_output_to_agent_output(claude_output: ClaudeOutput) -> AgentOutput {
269 let mut session_id = String::from("unknown");
270 let mut result = None;
271 let mut is_error = false;
272 let mut total_cost_usd = None;
273 let mut usage = None;
274 let mut events = Vec::new();
275 let mut model_name: Option<String> = None;
276
277 let mut pending_stop_reason: Option<String> = None;
283 let mut pending_turn_usage: Option<UnifiedUsage> = None;
284 let mut next_turn_index: u32 = 0;
285
286 let mut last_assistant_text: Option<String> = None;
291
292 for event in claude_output {
293 match event {
294 ClaudeEvent::System {
295 session_id: sid,
296 model,
297 tools,
298 cwd,
299 mut extra,
300 ..
301 } => {
302 session_id = sid;
303 model_name = Some(model.clone());
304
305 if let Some(cwd) = cwd {
307 extra.insert("cwd".to_string(), serde_json::json!(cwd));
308 }
309
310 events.push(UnifiedEvent::Init {
311 model,
312 tools,
313 working_directory: extra
314 .get("cwd")
315 .and_then(|v| v.as_str().map(|s| s.to_string())),
316 metadata: extra,
317 });
318 }
319
320 ClaudeEvent::Assistant {
321 message,
322 session_id: sid,
323 parent_tool_use_id,
324 ..
325 } => {
326 session_id = sid;
327
328 if let Some(reason) = &message.stop_reason {
332 pending_stop_reason = Some(reason.clone());
333 }
334
335 let content: Vec<UnifiedContentBlock> = message
337 .content
338 .into_iter()
339 .filter_map(|block| match block {
340 ContentBlock::Text { text } => Some(UnifiedContentBlock::Text { text }),
341 ContentBlock::ToolUse { id, name, input } => {
342 Some(UnifiedContentBlock::ToolUse { id, name, input })
343 }
344 ContentBlock::Thinking { .. } | ContentBlock::Other => None,
345 })
346 .collect();
347
348 let text_parts: Vec<&str> = content
350 .iter()
351 .filter_map(|b| match b {
352 UnifiedContentBlock::Text { text } => Some(text.as_str()),
353 _ => None,
354 })
355 .collect();
356 if !text_parts.is_empty() {
357 last_assistant_text = Some(text_parts.join("\n"));
358 }
359
360 let msg_usage = Some(UnifiedUsage {
362 input_tokens: message.usage.input_tokens,
363 output_tokens: message.usage.output_tokens,
364 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
365 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
366 web_search_requests: message
367 .usage
368 .server_tool_use
369 .as_ref()
370 .map(|s| s.web_search_requests),
371 web_fetch_requests: message
372 .usage
373 .server_tool_use
374 .as_ref()
375 .map(|s| s.web_fetch_requests),
376 });
377 pending_turn_usage = msg_usage.clone();
378
379 events.push(UnifiedEvent::AssistantMessage {
380 content,
381 usage: msg_usage,
382 parent_tool_use_id,
383 });
384 }
385
386 ClaudeEvent::User {
387 message,
388 tool_use_result,
389 session_id: sid,
390 parent_tool_use_id,
391 ..
392 } => {
393 session_id = sid;
394
395 for block in message.content {
397 if let UserContentBlock::ToolResult {
398 tool_use_id,
399 content,
400 is_error,
401 } = block
402 {
403 let tool_name = find_tool_name(&events, &tool_use_id)
404 .unwrap_or_else(|| "unknown".to_string());
405
406 let tool_result = ToolResult {
407 success: !is_error,
408 output: if !is_error {
409 Some(content.clone())
410 } else {
411 None
412 },
413 error: if is_error {
414 Some(content.clone())
415 } else {
416 None
417 },
418 data: tool_use_result.clone(),
419 };
420
421 events.push(UnifiedEvent::ToolExecution {
422 tool_name,
423 tool_id: tool_use_id,
424 input: serde_json::Value::Null,
425 result: tool_result,
426 parent_tool_use_id: parent_tool_use_id.clone(),
427 });
428 }
429 }
430 }
431
432 ClaudeEvent::Other => {
433 log::debug!("Skipping unknown Claude event type during output conversion");
434 }
435
436 ClaudeEvent::Result {
437 is_error: err,
438 result: res,
439 total_cost_usd: cost,
440 usage: u,
441 duration_ms,
442 num_turns,
443 permission_denials,
444 session_id: sid,
445 structured_output,
446 subtype: _,
447 ..
448 } => {
449 session_id = sid;
450 is_error = err;
451
452 let effective_result = if res.is_empty() {
456 if let Some(ref so) = structured_output {
457 let json = serde_json::to_string(so).unwrap_or_default();
458 log::debug!(
459 "Result.result is empty; using structured_output ({} bytes)",
460 json.len()
461 );
462 json
463 } else if let Some(ref fallback) = last_assistant_text {
464 log::debug!(
465 "Result.result is empty; using last assistant text ({} bytes)",
466 fallback.len()
467 );
468 fallback.clone()
469 } else {
470 res.clone()
471 }
472 } else {
473 res.clone()
474 };
475
476 result = Some(effective_result.clone());
477 total_cost_usd = Some(cost);
478
479 usage = Some(UnifiedUsage {
481 input_tokens: u.input_tokens,
482 output_tokens: u.output_tokens,
483 cache_read_tokens: Some(u.cache_read_input_tokens),
484 cache_creation_tokens: Some(u.cache_creation_input_tokens),
485 web_search_requests: u.server_tool_use.as_ref().map(|s| s.web_search_requests),
486 web_fetch_requests: u.server_tool_use.as_ref().map(|s| s.web_fetch_requests),
487 });
488
489 for denial in permission_denials {
491 events.push(UnifiedEvent::PermissionRequest {
492 tool_name: denial.tool_name,
493 description: format!(
494 "Permission denied for tool input: {}",
495 serde_json::to_string(&denial.tool_input).unwrap_or_default()
496 ),
497 granted: false,
498 });
499 }
500
501 events.push(UnifiedEvent::TurnComplete {
503 stop_reason: pending_stop_reason.take(),
504 turn_index: next_turn_index,
505 usage: pending_turn_usage.take(),
506 });
507 next_turn_index = next_turn_index.saturating_add(1);
508
509 events.push(UnifiedEvent::Result {
511 success: !err,
512 message: Some(effective_result),
513 duration_ms: Some(duration_ms),
514 num_turns: Some(num_turns),
515 });
516 }
517 }
518 }
519
520 AgentOutput {
521 agent: "claude".to_string(),
522 session_id,
523 events,
524 result,
525 is_error,
526 exit_code: None,
527 error_message: None,
528 total_cost_usd,
529 usage,
530 model: model_name,
531 provider: Some("claude".to_string()),
532 log_path: None,
533 }
534}
535
536fn find_tool_name(events: &[UnifiedEvent], tool_use_id: &str) -> Option<String> {
538 for event in events.iter().rev() {
539 if let UnifiedEvent::AssistantMessage { content, .. } = event {
540 for block in content {
541 if let UnifiedContentBlock::ToolUse { id, name, .. } = block
542 && id == tool_use_id
543 {
544 return Some(name.clone());
545 }
546 }
547 }
548 }
549 None
550}
551
552#[cfg(test)]
553#[path = "models_tests.rs"]
554mod tests;