Skip to main content

zag_agent/providers/claude/
models.rs

1/// Claude-specific JSON output models.
2///
3/// These structures directly map to the JSON output format produced by the
4/// Claude CLI when running with `--output json` (verbose mode). They can be
5/// deserialized from JSON and then converted to the unified `AgentOutput` format.
6///
7/// See README.md in this directory for detailed documentation on the output format.
8use 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
16/// The root structure: an array of events.
17pub type ClaudeOutput = Vec<ClaudeEvent>;
18
19/// A single event in Claude's output stream.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(tag = "type", rename_all = "snake_case")]
22pub enum ClaudeEvent {
23    /// System initialization event
24    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 message event
48    Assistant {
49        message: Message,
50        parent_tool_use_id: Option<String>,
51        session_id: String,
52        uuid: String,
53    },
54
55    /// User message event (tool results)
56    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    /// Final result event
65    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    /// Unknown/unhandled event type (e.g., rate_limit_event) — silently ignored
83    #[serde(other)]
84    Other,
85}
86
87/// An assistant message from Claude.
88#[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/// A user message containing tool results and other content.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct UserMessage {
105    pub role: String,
106    pub content: Vec<UserContentBlock>,
107}
108
109/// A content block in an assistant message.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(tag = "type", rename_all = "snake_case")]
112pub enum ContentBlock {
113    /// Text content
114    Text { text: String },
115
116    /// Tool invocation
117    ToolUse {
118        id: String,
119        name: String,
120        input: serde_json::Value,
121    },
122
123    /// Thinking content (extended thinking)
124    Thinking {
125        #[serde(default)]
126        thinking: String,
127        #[serde(flatten)]
128        extra: HashMap<String, serde_json::Value>,
129    },
130}
131
132/// A content block in a user message (tool results, text, or other types).
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(tag = "type", rename_all = "snake_case")]
135pub enum UserContentBlock {
136    /// Tool result
137    ToolResult {
138        tool_use_id: String,
139        content: String,
140        #[serde(default)]
141        is_error: bool,
142    },
143
144    /// Text content
145    Text { text: String },
146
147    /// Any other content type
148    #[serde(other)]
149    Other,
150}
151
152/// Usage statistics for a message or session.
153#[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/// Cache creation details.
170#[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/// Server-side tool usage.
179#[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/// Per-model usage statistics.
188#[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/// Information about a denied permission request.
209#[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/// Plugin information.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct Plugin {
219    pub name: String,
220    pub path: String,
221}
222
223/// Convert Claude output to unified agent output.
224pub 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                // Include all extra fields as metadata
245                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                // Convert content blocks (skip thinking blocks)
267                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                // Convert usage
280                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                // Convert tool results to tool execution events (skip non-tool-result blocks)
312                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                // Convert usage
369                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                // Add permission denial events
379                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                // Add final result event
391                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
412/// Find the tool name for a given tool_use_id by searching previous events.
413fn 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;