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 for event in claude_output {
233 match event {
234 ClaudeEvent::System {
235 session_id: sid,
236 model,
237 tools,
238 cwd,
239 mut extra,
240 ..
241 } => {
242 session_id = sid;
243
244 if let Some(cwd) = cwd {
246 extra.insert("cwd".to_string(), serde_json::json!(cwd));
247 }
248
249 events.push(UnifiedEvent::Init {
250 model,
251 tools,
252 working_directory: extra
253 .get("cwd")
254 .and_then(|v| v.as_str().map(|s| s.to_string())),
255 metadata: extra,
256 });
257 }
258
259 ClaudeEvent::Assistant {
260 message,
261 session_id: sid,
262 parent_tool_use_id,
263 ..
264 } => {
265 session_id = sid;
266
267 let content: Vec<UnifiedContentBlock> = message
269 .content
270 .into_iter()
271 .filter_map(|block| match block {
272 ContentBlock::Text { text } => Some(UnifiedContentBlock::Text { text }),
273 ContentBlock::ToolUse { id, name, input } => {
274 Some(UnifiedContentBlock::ToolUse { id, name, input })
275 }
276 ContentBlock::Thinking { .. } => None,
277 })
278 .collect();
279
280 let msg_usage = Some(UnifiedUsage {
282 input_tokens: message.usage.input_tokens,
283 output_tokens: message.usage.output_tokens,
284 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
285 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
286 web_search_requests: message
287 .usage
288 .server_tool_use
289 .as_ref()
290 .map(|s| s.web_search_requests),
291 web_fetch_requests: message
292 .usage
293 .server_tool_use
294 .as_ref()
295 .map(|s| s.web_fetch_requests),
296 });
297
298 events.push(UnifiedEvent::AssistantMessage {
299 content,
300 usage: msg_usage,
301 parent_tool_use_id,
302 });
303 }
304
305 ClaudeEvent::User {
306 message,
307 tool_use_result,
308 session_id: sid,
309 parent_tool_use_id,
310 ..
311 } => {
312 session_id = sid;
313
314 for block in message.content {
316 if let UserContentBlock::ToolResult {
317 tool_use_id,
318 content,
319 is_error,
320 } = block
321 {
322 let tool_name = find_tool_name(&events, &tool_use_id)
323 .unwrap_or_else(|| "unknown".to_string());
324
325 let tool_result = ToolResult {
326 success: !is_error,
327 output: if !is_error {
328 Some(content.clone())
329 } else {
330 None
331 },
332 error: if is_error {
333 Some(content.clone())
334 } else {
335 None
336 },
337 data: tool_use_result.clone(),
338 };
339
340 events.push(UnifiedEvent::ToolExecution {
341 tool_name,
342 tool_id: tool_use_id,
343 input: serde_json::Value::Null,
344 result: tool_result,
345 parent_tool_use_id: parent_tool_use_id.clone(),
346 });
347 }
348 }
349 }
350
351 ClaudeEvent::Other => {
352 log::debug!("Skipping unknown Claude event type during output conversion");
353 }
354
355 ClaudeEvent::Result {
356 is_error: err,
357 result: res,
358 total_cost_usd: cost,
359 usage: u,
360 duration_ms,
361 num_turns,
362 permission_denials,
363 session_id: sid,
364 subtype: _,
365 ..
366 } => {
367 session_id = sid;
368 is_error = err;
369 result = Some(res.clone());
370 total_cost_usd = Some(cost);
371
372 usage = Some(UnifiedUsage {
374 input_tokens: u.input_tokens,
375 output_tokens: u.output_tokens,
376 cache_read_tokens: Some(u.cache_read_input_tokens),
377 cache_creation_tokens: Some(u.cache_creation_input_tokens),
378 web_search_requests: u.server_tool_use.as_ref().map(|s| s.web_search_requests),
379 web_fetch_requests: u.server_tool_use.as_ref().map(|s| s.web_fetch_requests),
380 });
381
382 for denial in permission_denials {
384 events.push(UnifiedEvent::PermissionRequest {
385 tool_name: denial.tool_name,
386 description: format!(
387 "Permission denied for tool input: {}",
388 serde_json::to_string(&denial.tool_input).unwrap_or_default()
389 ),
390 granted: false,
391 });
392 }
393
394 events.push(UnifiedEvent::Result {
396 success: !err,
397 message: Some(res),
398 duration_ms: Some(duration_ms),
399 num_turns: Some(num_turns),
400 });
401 }
402 }
403 }
404
405 AgentOutput {
406 agent: "claude".to_string(),
407 session_id,
408 events,
409 result,
410 is_error,
411 exit_code: None,
412 error_message: None,
413 total_cost_usd,
414 usage,
415 }
416}
417
418fn find_tool_name(events: &[UnifiedEvent], tool_use_id: &str) -> Option<String> {
420 for event in events.iter().rev() {
421 if let UnifiedEvent::AssistantMessage { content, .. } = event {
422 for block in content {
423 if let UnifiedContentBlock::ToolUse { id, name, .. } = block
424 && id == tool_use_id
425 {
426 return Some(name.clone());
427 }
428 }
429 }
430 }
431 None
432}
433
434#[cfg(test)]
435#[path = "models_tests.rs"]
436mod tests;