1use serde::{Deserialize, Deserializer, Serialize};
2use serde_json::{Map, Value};
3
4use super::content_blocks::{ContentBlock, ToolUseBlock};
5use super::control::{ControlRequest, ControlResponse};
6use super::errors::{AnthropicError, ParseError};
7use super::message_types::{AssistantMessage, SystemMessage, UserMessage};
8use super::rate_limit::RateLimitEvent;
9use super::result::ResultMessage;
10
11#[derive(Debug, Clone, Serialize)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum ClaudeOutput {
15 System(SystemMessage),
17
18 User(UserMessage),
20
21 Assistant(AssistantMessage),
23
24 Result(ResultMessage),
26
27 #[serde(rename = "result")]
29 TranscriptResult(TranscriptMessage),
30
31 ControlRequest(ControlRequest),
33
34 ControlResponse(ControlResponse),
36
37 Error(AnthropicError),
39
40 RateLimitEvent(RateLimitEvent),
42
43 StreamEvent(StreamEventMessage),
45
46 ToolProgress(ToolProgressMessage),
48
49 AuthStatus(AuthStatusMessage),
51
52 ToolUseSummary(ToolUseSummaryMessage),
54
55 PromptSuggestion(PromptSuggestionMessage),
57
58 ConversationReset(ConversationResetMessage),
60
61 Progress(TranscriptMessage),
63
64 #[serde(rename = "queue-operation")]
66 QueueOperation(TranscriptMessage),
67
68 #[serde(rename = "pr-link")]
70 PrLink(TranscriptMessage),
71
72 #[serde(rename = "file-history-snapshot")]
74 FileHistorySnapshot(TranscriptMessage),
75
76 Summary(TranscriptMessage),
78
79 Mode(TranscriptMessage),
81
82 #[serde(rename = "permission-mode")]
84 PermissionMode(TranscriptMessage),
85
86 Attachment(TranscriptMessage),
88
89 #[serde(rename = "ai-title")]
91 AiTitle(TranscriptMessage),
92
93 #[serde(rename = "last-prompt")]
95 LastPrompt(TranscriptMessage),
96
97 Started(TranscriptMessage),
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct StreamEventMessage {
103 pub event: Value,
104 pub parent_tool_use_id: Option<String>,
105 pub uuid: String,
106 pub session_id: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub ttft_ms: Option<u64>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ToolProgressMessage {
113 pub tool_use_id: String,
114 pub tool_name: String,
115 pub parent_tool_use_id: Option<String>,
116 pub elapsed_time_seconds: f64,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub task_id: Option<String>,
119 pub uuid: String,
120 pub session_id: String,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct AuthStatusMessage {
125 #[serde(rename = "isAuthenticating")]
126 pub is_authenticating: bool,
127 pub output: Vec<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub error: Option<String>,
130 pub uuid: String,
131 pub session_id: String,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ToolUseSummaryMessage {
136 pub summary: String,
137 pub preceding_tool_use_ids: Vec<String>,
138 pub uuid: String,
139 pub session_id: String,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub timestamp: Option<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct PromptSuggestionMessage {
146 pub suggestion: String,
147 pub uuid: String,
148 pub session_id: String,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ConversationResetMessage {
153 pub new_conversation_id: String,
154 pub uuid: String,
155 pub session_id: String,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct TranscriptMessage {
166 #[serde(flatten)]
167 pub data: Map<String, Value>,
168}
169
170impl ClaudeOutput {
171 pub fn message_type(&self) -> String {
173 match self {
174 ClaudeOutput::System(_) => "system".to_string(),
175 ClaudeOutput::User(_) => "user".to_string(),
176 ClaudeOutput::Assistant(_) => "assistant".to_string(),
177 ClaudeOutput::Result(_) => "result".to_string(),
178 ClaudeOutput::TranscriptResult(_) => "result".to_string(),
179 ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
180 ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
181 ClaudeOutput::Error(_) => "error".to_string(),
182 ClaudeOutput::RateLimitEvent(_) => "rate_limit_event".to_string(),
183 ClaudeOutput::StreamEvent(_) => "stream_event".to_string(),
184 ClaudeOutput::ToolProgress(_) => "tool_progress".to_string(),
185 ClaudeOutput::AuthStatus(_) => "auth_status".to_string(),
186 ClaudeOutput::ToolUseSummary(_) => "tool_use_summary".to_string(),
187 ClaudeOutput::PromptSuggestion(_) => "prompt_suggestion".to_string(),
188 ClaudeOutput::ConversationReset(_) => "conversation_reset".to_string(),
189 ClaudeOutput::Progress(_) => "progress".to_string(),
190 ClaudeOutput::QueueOperation(_) => "queue-operation".to_string(),
191 ClaudeOutput::PrLink(_) => "pr-link".to_string(),
192 ClaudeOutput::FileHistorySnapshot(_) => "file-history-snapshot".to_string(),
193 ClaudeOutput::Summary(_) => "summary".to_string(),
194 ClaudeOutput::Mode(_) => "mode".to_string(),
195 ClaudeOutput::PermissionMode(_) => "permission-mode".to_string(),
196 ClaudeOutput::Attachment(_) => "attachment".to_string(),
197 ClaudeOutput::AiTitle(_) => "ai-title".to_string(),
198 ClaudeOutput::LastPrompt(_) => "last-prompt".to_string(),
199 ClaudeOutput::Started(_) => "started".to_string(),
200 }
201 }
202
203 pub fn is_control_request(&self) -> bool {
205 matches!(self, ClaudeOutput::ControlRequest(_))
206 }
207
208 pub fn is_control_response(&self) -> bool {
210 matches!(self, ClaudeOutput::ControlResponse(_))
211 }
212
213 pub fn is_api_error(&self) -> bool {
215 matches!(self, ClaudeOutput::Error(_))
216 }
217
218 pub fn as_control_request(&self) -> Option<&ControlRequest> {
220 match self {
221 ClaudeOutput::ControlRequest(req) => Some(req),
222 _ => None,
223 }
224 }
225
226 pub fn as_anthropic_error(&self) -> Option<&AnthropicError> {
242 match self {
243 ClaudeOutput::Error(err) => Some(err),
244 _ => None,
245 }
246 }
247
248 pub fn is_rate_limit_event(&self) -> bool {
250 matches!(self, ClaudeOutput::RateLimitEvent(_))
251 }
252
253 pub fn as_rate_limit_event(&self) -> Option<&RateLimitEvent> {
255 match self {
256 ClaudeOutput::RateLimitEvent(evt) => Some(evt),
257 _ => None,
258 }
259 }
260
261 pub fn is_error(&self) -> bool {
263 matches!(self, ClaudeOutput::Result(r) if r.is_error)
264 }
265
266 pub fn is_assistant_message(&self) -> bool {
268 matches!(self, ClaudeOutput::Assistant(_))
269 }
270
271 pub fn is_system_message(&self) -> bool {
273 matches!(self, ClaudeOutput::System(_))
274 }
275
276 pub fn is_system_init(&self) -> bool {
287 matches!(self, ClaudeOutput::System(sys) if sys.is_init())
288 }
289
290 pub fn session_id(&self) -> Option<&str> {
306 match self {
307 ClaudeOutput::System(sys) => sys
308 .data
309 .get("session_id")
310 .or_else(|| sys.data.get("sessionId"))
311 .and_then(|v| v.as_str()),
312 ClaudeOutput::Assistant(ass) => Some(&ass.session_id),
313 ClaudeOutput::Result(res) => Some(&res.session_id),
314 ClaudeOutput::TranscriptResult(msg) => msg
315 .data
316 .get("session_id")
317 .or_else(|| msg.data.get("sessionId"))
318 .and_then(|v| v.as_str()),
319 ClaudeOutput::User(_) => None,
320 ClaudeOutput::ControlRequest(_) => None,
321 ClaudeOutput::ControlResponse(_) => None,
322 ClaudeOutput::Error(_) => None,
323 ClaudeOutput::RateLimitEvent(evt) => Some(&evt.session_id),
324 ClaudeOutput::StreamEvent(msg) => Some(&msg.session_id),
325 ClaudeOutput::ToolProgress(msg) => Some(&msg.session_id),
326 ClaudeOutput::AuthStatus(msg) => Some(&msg.session_id),
327 ClaudeOutput::ToolUseSummary(msg) => Some(&msg.session_id),
328 ClaudeOutput::PromptSuggestion(msg) => Some(&msg.session_id),
329 ClaudeOutput::ConversationReset(msg) => Some(&msg.session_id),
330 ClaudeOutput::Progress(msg)
331 | ClaudeOutput::QueueOperation(msg)
332 | ClaudeOutput::PrLink(msg)
333 | ClaudeOutput::FileHistorySnapshot(msg)
334 | ClaudeOutput::Summary(msg)
335 | ClaudeOutput::Mode(msg)
336 | ClaudeOutput::PermissionMode(msg)
337 | ClaudeOutput::Attachment(msg)
338 | ClaudeOutput::AiTitle(msg)
339 | ClaudeOutput::LastPrompt(msg)
340 | ClaudeOutput::Started(msg) => msg
341 .data
342 .get("session_id")
343 .or_else(|| msg.data.get("sessionId"))
344 .and_then(|v| v.as_str()),
345 }
346 }
347
348 pub fn as_tool_use(&self, tool_name: &str) -> Option<&ToolUseBlock> {
367 match self {
368 ClaudeOutput::Assistant(ass) => {
369 ass.message.content.iter().find_map(|block| match block {
370 ContentBlock::ToolUse(tu) if tu.name == tool_name => Some(tu),
371 _ => None,
372 })
373 }
374 _ => None,
375 }
376 }
377
378 pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUseBlock> {
398 let content = match self {
399 ClaudeOutput::Assistant(ass) => Some(&ass.message.content),
400 _ => None,
401 };
402
403 content
404 .into_iter()
405 .flat_map(|c| c.iter())
406 .filter_map(|block| match block {
407 ContentBlock::ToolUse(tu) => Some(tu),
408 _ => None,
409 })
410 }
411
412 pub fn text_content(&self) -> Option<String> {
428 match self {
429 ClaudeOutput::Assistant(ass) => {
430 let texts: Vec<&str> = ass
431 .message
432 .content
433 .iter()
434 .filter_map(|block| match block {
435 ContentBlock::Text(t) => Some(t.text.as_str()),
436 _ => None,
437 })
438 .collect();
439
440 if texts.is_empty() {
441 None
442 } else {
443 Some(texts.join(""))
444 }
445 }
446 _ => None,
447 }
448 }
449
450 pub fn as_assistant(&self) -> Option<&AssistantMessage> {
465 match self {
466 ClaudeOutput::Assistant(ass) => Some(ass),
467 _ => None,
468 }
469 }
470
471 pub fn as_result(&self) -> Option<&ResultMessage> {
487 match self {
488 ClaudeOutput::Result(res) => Some(res),
489 _ => None,
490 }
491 }
492
493 pub fn as_system(&self) -> Option<&SystemMessage> {
495 match self {
496 ClaudeOutput::System(sys) => Some(sys),
497 _ => None,
498 }
499 }
500
501 pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
506 match Self::parse_json(s) {
508 Ok(output) => Ok(output),
509 Err(first_error) => {
510 if let Some(json_start) = s.find('{') {
512 let trimmed = &s[json_start..];
513 match Self::parse_json(trimmed) {
514 Ok(output) => Ok(output),
515 Err(_) => {
516 Err(first_error)
518 }
519 }
520 } else {
521 Err(first_error)
522 }
523 }
524 }
525 }
526
527 pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
529 let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
531 raw_line: s.to_string(),
532 raw_json: None,
533 error_message: format!("Invalid JSON: {}", e),
534 })?;
535
536 serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
538 raw_line: s.to_string(),
539 raw_json: Some(value),
540 error_message: e.to_string(),
541 })
542 }
543}
544
545impl<'de> Deserialize<'de> for ClaudeOutput {
546 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
547 let value = Value::deserialize(deserializer)?;
548 let message_type = value
549 .get("type")
550 .and_then(|v| v.as_str())
551 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
552 let mut payload = value.clone();
553 if let Some(obj) = payload.as_object_mut() {
554 obj.remove("type");
555 }
556
557 fn parse<T, E>(value: Value) -> Result<T, E>
558 where
559 T: serde::de::DeserializeOwned,
560 E: serde::de::Error,
561 {
562 serde_json::from_value(value).map_err(E::custom)
563 }
564
565 match message_type {
566 "system" => parse(payload).map(Self::System),
567 "user" => parse(payload).map(Self::User),
568 "assistant" => parse(payload).map(Self::Assistant),
569 "result" if value.get("subtype").is_some() => parse(payload).map(Self::Result),
570 "result" => parse(payload).map(Self::TranscriptResult),
571 "control_request" => parse(payload).map(Self::ControlRequest),
572 "control_response" => parse(payload).map(Self::ControlResponse),
573 "error" => parse(payload).map(Self::Error),
574 "rate_limit_event" => parse(payload).map(Self::RateLimitEvent),
575 "stream_event" => parse(payload).map(Self::StreamEvent),
576 "tool_progress" => parse(payload).map(Self::ToolProgress),
577 "auth_status" => parse(payload).map(Self::AuthStatus),
578 "tool_use_summary" => parse(payload).map(Self::ToolUseSummary),
579 "prompt_suggestion" => parse(payload).map(Self::PromptSuggestion),
580 "conversation_reset" => parse(payload).map(Self::ConversationReset),
581 "progress" => parse(payload).map(Self::Progress),
582 "queue-operation" => parse(payload).map(Self::QueueOperation),
583 "pr-link" => parse(payload).map(Self::PrLink),
584 "file-history-snapshot" => parse(payload).map(Self::FileHistorySnapshot),
585 "summary" => parse(payload).map(Self::Summary),
586 "mode" => parse(payload).map(Self::Mode),
587 "permission-mode" => parse(payload).map(Self::PermissionMode),
588 "attachment" => parse(payload).map(Self::Attachment),
589 "ai-title" => parse(payload).map(Self::AiTitle),
590 "last-prompt" => parse(payload).map(Self::LastPrompt),
591 "started" => parse(payload).map(Self::Started),
592 other => Err(serde::de::Error::unknown_variant(
593 other,
594 &[
595 "system",
596 "user",
597 "assistant",
598 "result",
599 "control_request",
600 "control_response",
601 "error",
602 "rate_limit_event",
603 "stream_event",
604 "tool_progress",
605 "auth_status",
606 "tool_use_summary",
607 "prompt_suggestion",
608 "conversation_reset",
609 "progress",
610 "queue-operation",
611 "pr-link",
612 "file-history-snapshot",
613 "summary",
614 "mode",
615 "permission-mode",
616 "attachment",
617 "ai-title",
618 "last-prompt",
619 "started",
620 ],
621 )),
622 }
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629
630 #[test]
631 fn test_deserialize_assistant_message() {
632 let json = r#"{
633 "type": "assistant",
634 "message": {
635 "id": "msg_123",
636 "role": "assistant",
637 "model": "claude-3-sonnet",
638 "content": [{"type": "text", "text": "Hello! How can I help you?"}]
639 },
640 "session_id": "123"
641 }"#;
642
643 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
644 assert!(output.is_assistant_message());
645 }
646
647 #[test]
648 fn test_deserialize_new_top_level_message_types() {
649 let cases = [
650 (
651 r#"{"type":"stream_event","event":{"type":"content_block_delta"},"parent_tool_use_id":null,"uuid":"u1","session_id":"s1","ttft_ms":12}"#,
652 "stream_event",
653 ),
654 (
655 r#"{"type":"tool_progress","tool_use_id":"toolu_1","tool_name":"Bash","parent_tool_use_id":null,"elapsed_time_seconds":1.25,"task_id":"task-1","uuid":"u2","session_id":"s2"}"#,
656 "tool_progress",
657 ),
658 (
659 r#"{"type":"auth_status","isAuthenticating":true,"output":["login"],"uuid":"u3","session_id":"s3"}"#,
660 "auth_status",
661 ),
662 (
663 r#"{"type":"tool_use_summary","summary":"read files","preceding_tool_use_ids":["toolu_1"],"uuid":"u4","session_id":"s4","timestamp":"2026-07-09T17:46:33Z"}"#,
664 "tool_use_summary",
665 ),
666 (
667 r#"{"type":"prompt_suggestion","suggestion":"Run tests","uuid":"u5","session_id":"s5"}"#,
668 "prompt_suggestion",
669 ),
670 (
671 r#"{"type":"conversation_reset","new_conversation_id":"new-session","uuid":"u6","session_id":"s6"}"#,
672 "conversation_reset",
673 ),
674 ];
675
676 for (json, message_type) in cases {
677 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
678 assert_eq!(output.message_type(), message_type);
679 assert!(output.session_id().is_some());
680 }
681 }
682
683 #[test]
684 fn test_deserialize_transcript_only_message_types() {
685 let cases = [
686 (
687 r#"{"type":"progress","sessionId":"s1","content":"delta"}"#,
688 "progress",
689 Some("s1"),
690 ),
691 (
692 r#"{"type":"queue-operation","sessionId":"s2","operation":"push"}"#,
693 "queue-operation",
694 Some("s2"),
695 ),
696 (
697 r#"{"type":"pr-link","sessionId":"s3","url":"https://example.invalid/pr"}"#,
698 "pr-link",
699 Some("s3"),
700 ),
701 (
702 r#"{"type":"file-history-snapshot","sessionId":"s4","files":[]}"#,
703 "file-history-snapshot",
704 Some("s4"),
705 ),
706 (r#"{"type":"summary","summary":"Done"}"#, "summary", None),
707 (
708 r#"{"type":"mode","mode":"normal","sessionId":"s5"}"#,
709 "mode",
710 Some("s5"),
711 ),
712 (
713 r#"{"type":"permission-mode","permissionMode":"default","sessionId":"s6"}"#,
714 "permission-mode",
715 Some("s6"),
716 ),
717 (
718 r#"{"type":"attachment","attachment":{"type":"task_reminder"},"sessionId":"s7"}"#,
719 "attachment",
720 Some("s7"),
721 ),
722 (
723 r#"{"type":"ai-title","aiTitle":"Title","sessionId":"s8"}"#,
724 "ai-title",
725 Some("s8"),
726 ),
727 (
728 r#"{"type":"last-prompt","lastPrompt":"prompt","sessionId":"s9"}"#,
729 "last-prompt",
730 Some("s9"),
731 ),
732 (
733 r#"{"type":"started","sessionId":"s10"}"#,
734 "started",
735 Some("s10"),
736 ),
737 (
738 r#"{"type":"result","key":"k","agentId":"a","result":{"ok":true}}"#,
739 "result",
740 None,
741 ),
742 ];
743
744 for (json, message_type, session_id) in cases {
745 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
746 assert_eq!(output.message_type(), message_type);
747 assert_eq!(output.session_id(), session_id);
748 }
749 }
750
751 #[test]
752 fn test_transcript_assistant_accepts_camel_case_session_id() {
753 let json = r#"{
754 "type": "assistant",
755 "sessionId": "camel-session",
756 "message": {
757 "id": "msg_123",
758 "role": "assistant",
759 "model": "claude-3-sonnet",
760 "content": [{"type": "text", "text": "Hello"}]
761 }
762 }"#;
763
764 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
765 assert_eq!(output.session_id(), Some("camel-session"));
766 }
767
768 #[test]
769 fn test_is_system_init() {
770 let init_json = r#"{
771 "type": "system",
772 "subtype": "init",
773 "session_id": "test-session"
774 }"#;
775 let output: ClaudeOutput = serde_json::from_str(init_json).unwrap();
776 assert!(output.is_system_init());
777
778 let status_json = r#"{
779 "type": "system",
780 "subtype": "status",
781 "session_id": "test-session"
782 }"#;
783 let output: ClaudeOutput = serde_json::from_str(status_json).unwrap();
784 assert!(!output.is_system_init());
785 }
786
787 #[test]
788 fn test_session_id() {
789 let result_json = r#"{
791 "type": "result",
792 "subtype": "success",
793 "is_error": false,
794 "duration_ms": 100,
795 "duration_api_ms": 200,
796 "num_turns": 1,
797 "session_id": "result-session",
798 "total_cost_usd": 0.01
799 }"#;
800 let output: ClaudeOutput = serde_json::from_str(result_json).unwrap();
801 assert_eq!(output.session_id(), Some("result-session"));
802
803 let assistant_json = r#"{
805 "type": "assistant",
806 "message": {
807 "id": "msg_1",
808 "role": "assistant",
809 "model": "claude-3",
810 "content": []
811 },
812 "session_id": "assistant-session"
813 }"#;
814 let output: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
815 assert_eq!(output.session_id(), Some("assistant-session"));
816
817 let system_json = r#"{
819 "type": "system",
820 "subtype": "init",
821 "session_id": "system-session"
822 }"#;
823 let output: ClaudeOutput = serde_json::from_str(system_json).unwrap();
824 assert_eq!(output.session_id(), Some("system-session"));
825 }
826
827 #[test]
828 fn test_as_tool_use() {
829 let json = r#"{
830 "type": "assistant",
831 "message": {
832 "id": "msg_1",
833 "role": "assistant",
834 "model": "claude-3",
835 "content": [
836 {"type": "text", "text": "Let me run that command."},
837 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls -la"}},
838 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/test"}}
839 ]
840 },
841 "session_id": "abc"
842 }"#;
843 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
844
845 let bash = output.as_tool_use("Bash");
847 assert!(bash.is_some());
848 assert_eq!(bash.unwrap().id, "tu_1");
849
850 let read = output.as_tool_use("Read");
852 assert!(read.is_some());
853 assert_eq!(read.unwrap().id, "tu_2");
854
855 assert!(output.as_tool_use("Write").is_none());
857
858 let result_json = r#"{
860 "type": "result",
861 "subtype": "success",
862 "is_error": false,
863 "duration_ms": 100,
864 "duration_api_ms": 200,
865 "num_turns": 1,
866 "session_id": "abc",
867 "total_cost_usd": 0.01
868 }"#;
869 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
870 assert!(result.as_tool_use("Bash").is_none());
871 }
872
873 #[test]
874 fn test_tool_uses() {
875 let json = r#"{
876 "type": "assistant",
877 "message": {
878 "id": "msg_1",
879 "role": "assistant",
880 "model": "claude-3",
881 "content": [
882 {"type": "text", "text": "Running commands..."},
883 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}},
884 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/a"}},
885 {"type": "tool_use", "id": "tu_3", "name": "Write", "input": {"file_path": "/tmp/b", "content": "x"}}
886 ]
887 },
888 "session_id": "abc"
889 }"#;
890 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
891
892 let tools: Vec<_> = output.tool_uses().collect();
893 assert_eq!(tools.len(), 3);
894 assert_eq!(tools[0].name, "Bash");
895 assert_eq!(tools[1].name, "Read");
896 assert_eq!(tools[2].name, "Write");
897 }
898
899 #[test]
900 fn test_text_content() {
901 let json = r#"{
903 "type": "assistant",
904 "message": {
905 "id": "msg_1",
906 "role": "assistant",
907 "model": "claude-3",
908 "content": [{"type": "text", "text": "Hello, world!"}]
909 },
910 "session_id": "abc"
911 }"#;
912 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
913 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
914
915 let json = r#"{
917 "type": "assistant",
918 "message": {
919 "id": "msg_1",
920 "role": "assistant",
921 "model": "claude-3",
922 "content": [
923 {"type": "text", "text": "Hello, "},
924 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}},
925 {"type": "text", "text": "world!"}
926 ]
927 },
928 "session_id": "abc"
929 }"#;
930 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
931 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
932
933 let json = r#"{
935 "type": "assistant",
936 "message": {
937 "id": "msg_1",
938 "role": "assistant",
939 "model": "claude-3",
940 "content": [{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}}]
941 },
942 "session_id": "abc"
943 }"#;
944 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
945 assert_eq!(output.text_content(), None);
946
947 let json = r#"{
949 "type": "result",
950 "subtype": "success",
951 "is_error": false,
952 "duration_ms": 100,
953 "duration_api_ms": 200,
954 "num_turns": 1,
955 "session_id": "abc",
956 "total_cost_usd": 0.01
957 }"#;
958 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
959 assert_eq!(output.text_content(), None);
960 }
961
962 #[test]
963 fn test_as_assistant() {
964 let json = r#"{
965 "type": "assistant",
966 "message": {
967 "id": "msg_1",
968 "role": "assistant",
969 "model": "claude-sonnet-4",
970 "content": []
971 },
972 "session_id": "abc"
973 }"#;
974 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
975
976 let assistant = output.as_assistant();
977 assert!(assistant.is_some());
978 assert_eq!(assistant.unwrap().message.model, "claude-sonnet-4");
979
980 let result_json = r#"{
982 "type": "result",
983 "subtype": "success",
984 "is_error": false,
985 "duration_ms": 100,
986 "duration_api_ms": 200,
987 "num_turns": 1,
988 "session_id": "abc",
989 "total_cost_usd": 0.01
990 }"#;
991 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
992 assert!(result.as_assistant().is_none());
993 }
994
995 #[test]
996 fn test_as_result() {
997 let json = r#"{
998 "type": "result",
999 "subtype": "success",
1000 "is_error": false,
1001 "duration_ms": 100,
1002 "duration_api_ms": 200,
1003 "num_turns": 5,
1004 "session_id": "abc",
1005 "total_cost_usd": 0.05
1006 }"#;
1007 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1008
1009 let result = output.as_result();
1010 assert!(result.is_some());
1011 assert_eq!(result.unwrap().num_turns, 5);
1012 assert_eq!(result.unwrap().total_cost_usd, 0.05);
1013
1014 let assistant_json = r#"{
1016 "type": "assistant",
1017 "message": {
1018 "id": "msg_1",
1019 "role": "assistant",
1020 "model": "claude-3",
1021 "content": []
1022 },
1023 "session_id": "abc"
1024 }"#;
1025 let assistant: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1026 assert!(assistant.as_result().is_none());
1027 }
1028
1029 #[test]
1030 fn test_as_system() {
1031 let json = r#"{
1032 "type": "system",
1033 "subtype": "init",
1034 "session_id": "abc",
1035 "model": "claude-3"
1036 }"#;
1037 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1038
1039 let system = output.as_system();
1040 assert!(system.is_some());
1041 assert!(system.unwrap().is_init());
1042
1043 let result_json = r#"{
1045 "type": "result",
1046 "subtype": "success",
1047 "is_error": false,
1048 "duration_ms": 100,
1049 "duration_api_ms": 200,
1050 "num_turns": 1,
1051 "session_id": "abc",
1052 "total_cost_usd": 0.01
1053 }"#;
1054 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1055 assert!(result.as_system().is_none());
1056 }
1057}