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