1use std::process::ExitStatus;
18
19use serde::{Deserialize, Serialize};
20
21use crate::anthropic::{Content, Message, Role, ServerToolUsage, StreamEvent, StreamingMessage};
22
23#[derive(Debug, thiserror::Error)]
25pub enum RunError {
26 #[error("process exited with {status}")]
28 ProcessFailed {
29 status: ExitStatus,
31 stderr: String,
33 },
34 #[error("no output messages")]
36 NoMessages,
37 #[error("final message was not a result")]
39 MissingResult,
40 #[error("claude error: {}", .messages.last().and_then(|m| match m {
42 OutputMessage::Result(r) => r.result.as_deref(),
43 _ => None,
44 }).unwrap_or("unknown"))]
45 ResultError {
46 messages: Vec<OutputMessage>,
48 },
49 #[error("failed to parse message")]
51 Parse(#[from] serde_json::Error),
52}
53
54pub fn parse_line(line: &str) -> Option<Result<OutputMessage, serde_json::Error>> {
59 if line.is_empty() {
60 return None;
61 }
62 Some(serde_json::from_str(line))
63}
64
65pub fn parse_output(output: &std::process::Output) -> Result<Vec<OutputMessage>, RunError> {
100 if !output.status.success() {
101 return Err(RunError::ProcessFailed {
102 status: output.status,
103 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
104 });
105 }
106
107 let stdout = String::from_utf8_lossy(&output.stdout);
108 let messages: Vec<OutputMessage> = stdout
109 .lines()
110 .filter(|line| !line.is_empty())
111 .map(serde_json::from_str)
112 .collect::<Result<_, _>>()?;
113
114 match messages.last() {
115 None => Err(RunError::NoMessages),
116 Some(OutputMessage::Result(r)) if r.is_error => Err(RunError::ResultError { messages }),
117 Some(OutputMessage::Result(_)) => Ok(messages),
118 Some(_) => Err(RunError::MissingResult),
119 }
120}
121
122#[derive(Clone, Debug, Deserialize)]
126pub struct Envelope {
127 pub session_id: String,
129 #[serde(default)]
131 pub parent_tool_use_id: Option<String>,
132 pub uuid: String,
134}
135
136#[derive(Debug, Deserialize)]
141#[serde(tag = "type", rename_all = "snake_case")]
142pub enum OutputMessage {
143 System(SystemMessage),
145 StreamEvent(StreamEventMessage),
147 Assistant(AssistantMessage),
149 User(UserMessage),
151 Result(ResultMessage),
153}
154
155#[derive(Clone, Debug, Deserialize)]
159pub struct SystemMessage {
160 pub subtype: String,
162 pub cwd: String,
164 pub tools: Vec<String>,
166 #[serde(default)]
168 pub mcp_servers: Vec<McpServerStatus>,
169 pub model: String,
171 #[serde(rename = "permissionMode")]
173 pub permission_mode: String,
174 #[serde(default)]
176 pub slash_commands: Vec<String>,
177 #[serde(rename = "apiKeySource")]
179 pub api_key_source: String,
180 pub claude_code_version: String,
182 pub output_style: String,
184 #[serde(default)]
186 pub agents: Vec<String>,
187 #[serde(default)]
189 pub skills: Vec<String>,
190 #[serde(default)]
192 pub plugins: Vec<String>,
193 #[serde(flatten)]
195 pub envelope: Envelope,
196}
197
198#[derive(Clone, Debug, Deserialize)]
200pub struct McpServerStatus {
201 pub name: String,
203 pub status: String,
205}
206
207#[derive(Debug, Deserialize)]
211pub struct StreamEventMessage {
212 pub event: StreamEvent,
214 #[serde(flatten)]
216 pub envelope: Envelope,
217}
218
219#[derive(Debug, Deserialize)]
224pub struct AssistantMessage {
225 pub message: StreamingMessage,
227 #[serde(flatten)]
229 pub envelope: Envelope,
230}
231
232#[derive(Clone, Debug, Deserialize)]
234pub struct UserMessage {
235 pub message: Message,
237 #[serde(flatten)]
239 pub envelope: Envelope,
240 #[serde(default)]
242 pub tool_use_result: Option<serde_json::Value>,
243}
244
245#[derive(Clone, Debug, Deserialize)]
247pub struct ResultMessage {
248 pub subtype: String,
250 pub is_error: bool,
252 pub duration_ms: u64,
254 #[serde(default)]
256 pub duration_api_ms: u64,
257 #[serde(default)]
259 pub num_turns: u32,
260 pub result: Option<String>,
262 #[serde(default)]
264 pub total_cost_usd: f64,
265 pub usage: ResultUsage,
267 #[serde(default, rename = "modelUsage")]
269 pub model_usage: serde_json::Map<String, serde_json::Value>,
270 #[serde(default)]
272 pub permission_denials: Vec<serde_json::Value>,
273 #[serde(flatten)]
275 pub envelope: Envelope,
276}
277
278#[derive(Clone, Debug, Default, Deserialize)]
280pub struct ResultUsage {
281 #[serde(default)]
283 pub input_tokens: u32,
284 #[serde(default)]
286 pub cache_creation_input_tokens: u32,
287 #[serde(default)]
289 pub cache_read_input_tokens: u32,
290 #[serde(default)]
292 pub output_tokens: u32,
293 #[serde(default)]
295 pub server_tool_use: ServerToolUsage,
296 #[serde(default)]
298 pub service_tier: String,
299 #[serde(default)]
301 pub cache_creation: CacheCreation,
302}
303
304#[derive(Clone, Debug, Default, Deserialize)]
306pub struct CacheCreation {
307 #[serde(default)]
309 pub ephemeral_1h_input_tokens: u32,
310 #[serde(default)]
312 pub ephemeral_5m_input_tokens: u32,
313}
314
315#[derive(Clone, Debug, Serialize)]
321pub struct InputMessage {
322 #[serde(rename = "type")]
324 message_type: &'static str,
325 pub message: Message,
327}
328
329impl InputMessage {
330 pub fn text(text: impl Into<String>) -> Self {
332 Self {
333 message_type: "user",
334 message: Message::from_text(Role::User, text),
335 }
336 }
337
338 pub fn with_content(content: Vec<Content>) -> Self {
340 Self {
341 message_type: "user",
342 message: Message {
343 role: Role::User,
344 content,
345 },
346 }
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn parse_system_init() {
356 let json = r#"{
357 "type": "system",
358 "subtype": "init",
359 "cwd": "/home/user/project",
360 "session_id": "6484002d-24fe-4f95-ad4b-6bf7130f1fcb",
361 "tools": ["Bash", "Read", "Write"],
362 "mcp_servers": [],
363 "model": "claude-opus-4-5-20251101",
364 "permissionMode": "default",
365 "slash_commands": ["commit"],
366 "apiKeySource": "none",
367 "claude_code_version": "2.1.17",
368 "output_style": "default",
369 "agents": [],
370 "skills": [],
371 "plugins": [],
372 "uuid": "f34a0e91-06ae-426c-9e5c-317a7572ff29"
373 }"#;
374
375 let msg: OutputMessage = serde_json::from_str(json).expect("parse");
376 match msg {
377 OutputMessage::System(sys) => {
378 assert_eq!(sys.subtype, "init");
379 assert_eq!(sys.cwd, "/home/user/project");
380 assert_eq!(sys.tools, vec!["Bash", "Read", "Write"]);
381 assert_eq!(sys.model, "claude-opus-4-5-20251101");
382 assert_eq!(sys.permission_mode, "default");
383 }
384 _ => panic!("expected System variant"),
385 }
386 }
387
388 #[test]
389 fn parse_result_success() {
390 let json = r#"{
391 "type": "result",
392 "subtype": "success",
393 "is_error": false,
394 "duration_ms": 2633,
395 "duration_api_ms": 2600,
396 "num_turns": 1,
397 "result": "hello",
398 "session_id": "6484002d-24fe-4f95-ad4b-6bf7130f1fcb",
399 "total_cost_usd": 0.12779625,
400 "usage": {
401 "input_tokens": 3,
402 "cache_creation_input_tokens": 20429,
403 "cache_read_input_tokens": 0,
404 "output_tokens": 4,
405 "server_tool_use": {"web_search_requests": 0, "web_fetch_requests": 0},
406 "service_tier": "standard",
407 "cache_creation": {"ephemeral_1h_input_tokens": 0, "ephemeral_5m_input_tokens": 20429}
408 },
409 "modelUsage": {},
410 "permission_denials": [],
411 "uuid": "4e5d6b84-6129-47b3-bba6-fdb375aa7b3d"
412 }"#;
413
414 let msg: OutputMessage = serde_json::from_str(json).expect("parse");
415 match msg {
416 OutputMessage::Result(res) => {
417 assert_eq!(res.subtype, "success");
418 assert!(!res.is_error);
419 assert_eq!(res.duration_ms, 2633);
420 assert_eq!(res.result, Some("hello".to_string()));
421 assert_eq!(res.usage.input_tokens, 3);
422 assert_eq!(res.usage.cache_creation_input_tokens, 20429);
423 }
424 _ => panic!("expected Result variant"),
425 }
426 }
427
428 #[test]
429 fn parse_assistant() {
430 let json = r#"{
431 "type": "assistant",
432 "message": {
433 "model": "claude-opus-4-5-20251101",
434 "id": "msg_016erzjGS5oTB6Q8uohJEpAs",
435 "type": "message",
436 "role": "assistant",
437 "content": [{"type": "text", "text": "hello"}],
438 "stop_reason": null,
439 "stop_sequence": null,
440 "usage": {
441 "input_tokens": 3,
442 "output_tokens": 1
443 }
444 },
445 "parent_tool_use_id": null,
446 "session_id": "6484002d-24fe-4f95-ad4b-6bf7130f1fcb",
447 "uuid": "9e40ea6e-9f3e-43e1-a6c0-59e9de6c347f"
448 }"#;
449
450 let msg: OutputMessage = serde_json::from_str(json).expect("parse");
451 match msg {
452 OutputMessage::Assistant(asst) => {
453 assert_eq!(asst.message.id, "msg_016erzjGS5oTB6Q8uohJEpAs");
454 assert_eq!(asst.message.content.len(), 1);
455 assert!(asst.envelope.parent_tool_use_id.is_none());
456 }
457 _ => panic!("expected Assistant variant"),
458 }
459 }
460
461 #[test]
462 fn serialize_input_text() {
463 let input = InputMessage::text("hello world");
464 let json = serde_json::to_value(&input).expect("serialize");
465
466 assert_eq!(json["type"], "user");
467 assert_eq!(json["message"]["role"], "user");
468 assert_eq!(json["message"]["content"][0]["type"], "text");
469 assert_eq!(json["message"]["content"][0]["text"], "hello world");
470 }
471
472 #[test]
473 fn parse_user_tool_result() {
474 use crate::anthropic::Role;
475
476 let json = r#"{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01TV2WdLXaSZwBGgKGPvLEmy","type":"tool_result","content":"hello"}]},"parent_tool_use_id":null,"session_id":"bf7004a5-4781-4c4e-bd35-6f4516db86fd","uuid":"dfc99bb7-55dc-4829-87a8-e9fd6333f970","tool_use_result":{"type":"text","file":{"filePath":"/tmp/hello.txt"}}}"#;
477
478 let msg: OutputMessage = serde_json::from_str(json).expect("parse");
479 match msg {
480 OutputMessage::User(user) => {
481 assert_eq!(user.message.role, Role::User);
482 assert_eq!(user.message.content.len(), 1);
483 assert!(user.tool_use_result.is_some());
484 }
485 _ => panic!("expected User variant"),
486 }
487 }
488
489 #[test]
491 fn parse_real_assistant_with_tool_use() {
492 let json = r#"{"type":"assistant","message":{"model":"claude-opus-4-5-20251101","id":"msg_01UQFX7fDMP5CKAWQWTgtodQ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01TV2WdLXaSZwBGgKGPvLEmy","name":"Read","input":{"file_path":"/tmp/hello.txt"},"caller":{"type":"direct"}}],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":22175,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":22175,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"},"context_management":null},"parent_tool_use_id":null,"session_id":"bf7004a5-4781-4c4e-bd35-6f4516db86fd","uuid":"a3c66f24-58f3-4727-b052-961d2205958f"}"#;
493
494 let msg: OutputMessage = serde_json::from_str(json).expect("parse");
495 match msg {
496 OutputMessage::Assistant(asst) => {
497 assert_eq!(asst.message.model, "claude-opus-4-5-20251101");
498 assert_eq!(asst.message.content.len(), 1);
499 }
500 _ => panic!("expected Assistant variant"),
501 }
502 }
503}