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