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                parent_tool_use_id,
263                ..
264            } => {
265                session_id = sid;
266
267                // Convert content blocks (skip thinking blocks)
268                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                // Convert usage
281                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                // Convert tool results to tool execution events (skip non-tool-result blocks)
315                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                // Convert usage
373                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                // Add permission denial events
383                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                // Add final result event
395                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
418/// Find the tool name for a given tool_use_id by searching previous events.
419fn 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;