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 uuid: String,
80 },
81
82 #[serde(other)]
84 Other,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct Message {
90 pub model: String,
91 pub id: String,
92 #[serde(rename = "type")]
93 pub message_type: String,
94 pub role: String,
95 pub content: Vec<ContentBlock>,
96 pub stop_reason: Option<String>,
97 pub stop_sequence: Option<String>,
98 pub usage: Usage,
99 pub context_management: Option<serde_json::Value>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct UserMessage {
105 pub role: String,
106 pub content: Vec<UserContentBlock>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(tag = "type", rename_all = "snake_case")]
112pub enum ContentBlock {
113 Text { text: String },
115
116 ToolUse {
118 id: String,
119 name: String,
120 input: serde_json::Value,
121 },
122
123 Thinking {
125 #[serde(default)]
126 thinking: String,
127 #[serde(flatten)]
128 extra: HashMap<String, serde_json::Value>,
129 },
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(tag = "type", rename_all = "snake_case")]
135pub enum UserContentBlock {
136 ToolResult {
138 tool_use_id: String,
139 content: String,
140 #[serde(default)]
141 is_error: bool,
142 },
143
144 Text { text: String },
146
147 #[serde(other)]
149 Other,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct Usage {
155 pub input_tokens: u64,
156 #[serde(default)]
157 pub cache_creation_input_tokens: u64,
158 #[serde(default)]
159 pub cache_read_input_tokens: u64,
160 pub output_tokens: u64,
161 #[serde(default)]
162 pub cache_creation: Option<CacheCreation>,
163 #[serde(default)]
164 pub server_tool_use: Option<ServerToolUse>,
165 #[serde(default)]
166 pub service_tier: Option<String>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct CacheCreation {
172 #[serde(default)]
173 pub ephemeral_5m_input_tokens: u64,
174 #[serde(default)]
175 pub ephemeral_1h_input_tokens: u64,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ServerToolUse {
181 #[serde(default)]
182 pub web_search_requests: u32,
183 #[serde(default)]
184 pub web_fetch_requests: u32,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct ModelUsage {
190 #[serde(rename = "inputTokens")]
191 pub input_tokens: u64,
192 #[serde(rename = "outputTokens")]
193 pub output_tokens: u64,
194 #[serde(default, rename = "cacheReadInputTokens")]
195 pub cache_read_input_tokens: u64,
196 #[serde(default, rename = "cacheCreationInputTokens")]
197 pub cache_creation_input_tokens: u64,
198 #[serde(default, rename = "webSearchRequests")]
199 pub web_search_requests: u32,
200 #[serde(rename = "costUSD")]
201 pub cost_usd: f64,
202 #[serde(default, rename = "contextWindow")]
203 pub context_window: u64,
204 #[serde(default, rename = "maxOutputTokens")]
205 pub max_output_tokens: u64,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct PermissionDenial {
211 pub tool_name: String,
212 pub tool_use_id: String,
213 pub tool_input: serde_json::Value,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct Plugin {
219 pub name: String,
220 pub path: String,
221}
222
223pub fn claude_output_to_agent_output(claude_output: ClaudeOutput) -> AgentOutput {
225 let mut session_id = String::from("unknown");
226 let mut result = None;
227 let mut is_error = false;
228 let mut total_cost_usd = None;
229 let mut usage = None;
230 let mut events = Vec::new();
231
232 let mut pending_stop_reason: Option<String> = None;
238 let mut pending_turn_usage: Option<UnifiedUsage> = None;
239 let mut next_turn_index: u32 = 0;
240
241 let mut last_assistant_text: Option<String> = None;
246
247 for event in claude_output {
248 match event {
249 ClaudeEvent::System {
250 session_id: sid,
251 model,
252 tools,
253 cwd,
254 mut extra,
255 ..
256 } => {
257 session_id = sid;
258
259 if let Some(cwd) = cwd {
261 extra.insert("cwd".to_string(), serde_json::json!(cwd));
262 }
263
264 events.push(UnifiedEvent::Init {
265 model,
266 tools,
267 working_directory: extra
268 .get("cwd")
269 .and_then(|v| v.as_str().map(|s| s.to_string())),
270 metadata: extra,
271 });
272 }
273
274 ClaudeEvent::Assistant {
275 message,
276 session_id: sid,
277 parent_tool_use_id,
278 ..
279 } => {
280 session_id = sid;
281
282 if let Some(reason) = &message.stop_reason {
286 pending_stop_reason = Some(reason.clone());
287 }
288
289 let content: Vec<UnifiedContentBlock> = message
291 .content
292 .into_iter()
293 .filter_map(|block| match block {
294 ContentBlock::Text { text } => Some(UnifiedContentBlock::Text { text }),
295 ContentBlock::ToolUse { id, name, input } => {
296 Some(UnifiedContentBlock::ToolUse { id, name, input })
297 }
298 ContentBlock::Thinking { .. } => None,
299 })
300 .collect();
301
302 let text_parts: Vec<&str> = content
304 .iter()
305 .filter_map(|b| match b {
306 UnifiedContentBlock::Text { text } => Some(text.as_str()),
307 _ => None,
308 })
309 .collect();
310 if !text_parts.is_empty() {
311 last_assistant_text = Some(text_parts.join("\n"));
312 }
313
314 let msg_usage = Some(UnifiedUsage {
316 input_tokens: message.usage.input_tokens,
317 output_tokens: message.usage.output_tokens,
318 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
319 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
320 web_search_requests: message
321 .usage
322 .server_tool_use
323 .as_ref()
324 .map(|s| s.web_search_requests),
325 web_fetch_requests: message
326 .usage
327 .server_tool_use
328 .as_ref()
329 .map(|s| s.web_fetch_requests),
330 });
331 pending_turn_usage = msg_usage.clone();
332
333 events.push(UnifiedEvent::AssistantMessage {
334 content,
335 usage: msg_usage,
336 parent_tool_use_id,
337 });
338 }
339
340 ClaudeEvent::User {
341 message,
342 tool_use_result,
343 session_id: sid,
344 parent_tool_use_id,
345 ..
346 } => {
347 session_id = sid;
348
349 for block in message.content {
351 if let UserContentBlock::ToolResult {
352 tool_use_id,
353 content,
354 is_error,
355 } = block
356 {
357 let tool_name = find_tool_name(&events, &tool_use_id)
358 .unwrap_or_else(|| "unknown".to_string());
359
360 let tool_result = ToolResult {
361 success: !is_error,
362 output: if !is_error {
363 Some(content.clone())
364 } else {
365 None
366 },
367 error: if is_error {
368 Some(content.clone())
369 } else {
370 None
371 },
372 data: tool_use_result.clone(),
373 };
374
375 events.push(UnifiedEvent::ToolExecution {
376 tool_name,
377 tool_id: tool_use_id,
378 input: serde_json::Value::Null,
379 result: tool_result,
380 parent_tool_use_id: parent_tool_use_id.clone(),
381 });
382 }
383 }
384 }
385
386 ClaudeEvent::Other => {
387 log::debug!("Skipping unknown Claude event type during output conversion");
388 }
389
390 ClaudeEvent::Result {
391 is_error: err,
392 result: res,
393 total_cost_usd: cost,
394 usage: u,
395 duration_ms,
396 num_turns,
397 permission_denials,
398 session_id: sid,
399 subtype: _,
400 ..
401 } => {
402 session_id = sid;
403 is_error = err;
404
405 let effective_result = if res.is_empty() {
410 if let Some(ref fallback) = last_assistant_text {
411 log::debug!(
412 "Result.result is empty; using last assistant text ({} bytes)",
413 fallback.len()
414 );
415 fallback.clone()
416 } else {
417 res.clone()
418 }
419 } else {
420 res.clone()
421 };
422
423 result = Some(effective_result.clone());
424 total_cost_usd = Some(cost);
425
426 usage = Some(UnifiedUsage {
428 input_tokens: u.input_tokens,
429 output_tokens: u.output_tokens,
430 cache_read_tokens: Some(u.cache_read_input_tokens),
431 cache_creation_tokens: Some(u.cache_creation_input_tokens),
432 web_search_requests: u.server_tool_use.as_ref().map(|s| s.web_search_requests),
433 web_fetch_requests: u.server_tool_use.as_ref().map(|s| s.web_fetch_requests),
434 });
435
436 for denial in permission_denials {
438 events.push(UnifiedEvent::PermissionRequest {
439 tool_name: denial.tool_name,
440 description: format!(
441 "Permission denied for tool input: {}",
442 serde_json::to_string(&denial.tool_input).unwrap_or_default()
443 ),
444 granted: false,
445 });
446 }
447
448 events.push(UnifiedEvent::TurnComplete {
450 stop_reason: pending_stop_reason.take(),
451 turn_index: next_turn_index,
452 usage: pending_turn_usage.take(),
453 });
454 next_turn_index = next_turn_index.saturating_add(1);
455
456 events.push(UnifiedEvent::Result {
458 success: !err,
459 message: Some(effective_result),
460 duration_ms: Some(duration_ms),
461 num_turns: Some(num_turns),
462 });
463 }
464 }
465 }
466
467 AgentOutput {
468 agent: "claude".to_string(),
469 session_id,
470 events,
471 result,
472 is_error,
473 exit_code: None,
474 error_message: None,
475 total_cost_usd,
476 usage,
477 }
478}
479
480fn find_tool_name(events: &[UnifiedEvent], tool_use_id: &str) -> Option<String> {
482 for event in events.iter().rev() {
483 if let UnifiedEvent::AssistantMessage { content, .. } = event {
484 for block in content {
485 if let UnifiedContentBlock::ToolUse { id, name, .. } = block
486 && id == tool_use_id
487 {
488 return Some(name.clone());
489 }
490 }
491 }
492 }
493 None
494}
495
496#[cfg(test)]
497#[path = "models_tests.rs"]
498mod tests;