1use serde::{Deserialize, Deserializer, Serialize, Serializer};
33use serde_json::Value;
34use std::fmt;
35use uuid::Uuid;
36
37fn serialize_optional_uuid<S>(uuid: &Option<Uuid>, serializer: S) -> Result<S::Ok, S::Error>
39where
40 S: Serializer,
41{
42 match uuid {
43 Some(id) => serializer.serialize_str(&id.to_string()),
44 None => serializer.serialize_none(),
45 }
46}
47
48fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
50where
51 D: Deserializer<'de>,
52{
53 let opt_str: Option<String> = Option::deserialize(deserializer)?;
54 match opt_str {
55 Some(s) => Uuid::parse_str(&s)
56 .map(Some)
57 .map_err(serde::de::Error::custom),
58 None => Ok(None),
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum ClaudeInput {
66 User(UserMessage),
68
69 ControlRequest(ControlRequest),
71
72 ControlResponse(ControlResponse),
74
75 #[serde(untagged)]
77 Raw(Value),
78}
79
80#[derive(Debug, Clone)]
82pub struct ParseError {
83 pub raw_json: Value,
85 pub error_message: String,
87}
88
89impl fmt::Display for ParseError {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 write!(f, "Failed to parse ClaudeOutput: {}", self.error_message)
92 }
93}
94
95impl std::error::Error for ParseError {}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(tag = "type", rename_all = "snake_case")]
100pub enum ClaudeOutput {
101 System(SystemMessage),
103
104 User(UserMessage),
106
107 Assistant(AssistantMessage),
109
110 Result(ResultMessage),
112
113 ControlRequest(ControlRequest),
115
116 ControlResponse(ControlResponse),
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct UserMessage {
123 pub message: MessageContent,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 #[serde(
126 serialize_with = "serialize_optional_uuid",
127 deserialize_with = "deserialize_optional_uuid"
128 )]
129 pub session_id: Option<Uuid>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct MessageContent {
135 pub role: String,
136 #[serde(deserialize_with = "deserialize_content_blocks")]
137 pub content: Vec<ContentBlock>,
138}
139
140fn deserialize_content_blocks<'de, D>(deserializer: D) -> Result<Vec<ContentBlock>, D::Error>
142where
143 D: Deserializer<'de>,
144{
145 let value: Value = Value::deserialize(deserializer)?;
146 match value {
147 Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock { text: s })]),
148 Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
149 _ => Err(serde::de::Error::custom(
150 "content must be a string or array",
151 )),
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SystemMessage {
158 pub subtype: String,
159 #[serde(flatten)]
160 pub data: Value, }
162
163impl SystemMessage {
164 pub fn is_init(&self) -> bool {
166 self.subtype == "init"
167 }
168
169 pub fn is_status(&self) -> bool {
171 self.subtype == "status"
172 }
173
174 pub fn is_compact_boundary(&self) -> bool {
176 self.subtype == "compact_boundary"
177 }
178
179 pub fn as_init(&self) -> Option<InitMessage> {
181 if self.subtype != "init" {
182 return None;
183 }
184 serde_json::from_value(self.data.clone()).ok()
185 }
186
187 pub fn as_status(&self) -> Option<StatusMessage> {
189 if self.subtype != "status" {
190 return None;
191 }
192 serde_json::from_value(self.data.clone()).ok()
193 }
194
195 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
197 if self.subtype != "compact_boundary" {
198 return None;
199 }
200 serde_json::from_value(self.data.clone()).ok()
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct InitMessage {
207 pub session_id: String,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub cwd: Option<String>,
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub model: Option<String>,
215 #[serde(default, skip_serializing_if = "Vec::is_empty")]
217 pub tools: Vec<String>,
218 #[serde(default, skip_serializing_if = "Vec::is_empty")]
220 pub mcp_servers: Vec<Value>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct StatusMessage {
226 pub session_id: String,
228 pub status: Option<String>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub uuid: Option<String>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct CompactBoundaryMessage {
238 pub session_id: String,
240 pub compact_metadata: CompactMetadata,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub uuid: Option<String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct CompactMetadata {
250 pub pre_tokens: u64,
252 pub trigger: String,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct AssistantMessage {
259 pub message: AssistantMessageContent,
260 pub session_id: String,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub uuid: Option<String>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub parent_tool_use_id: Option<String>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct AssistantMessageContent {
270 pub id: String,
271 pub role: String,
272 pub model: String,
273 pub content: Vec<ContentBlock>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub stop_reason: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub stop_sequence: Option<String>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub usage: Option<AssistantUsage>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct AssistantUsage {
285 #[serde(default)]
287 pub input_tokens: u32,
288
289 #[serde(default)]
291 pub output_tokens: u32,
292
293 #[serde(default)]
295 pub cache_creation_input_tokens: u32,
296
297 #[serde(default)]
299 pub cache_read_input_tokens: u32,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub service_tier: Option<String>,
304
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub cache_creation: Option<CacheCreationDetails>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct CacheCreationDetails {
313 #[serde(default)]
315 pub ephemeral_1h_input_tokens: u32,
316
317 #[serde(default)]
319 pub ephemeral_5m_input_tokens: u32,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(tag = "type", rename_all = "snake_case")]
325pub enum ContentBlock {
326 Text(TextBlock),
327 Image(ImageBlock),
328 Thinking(ThinkingBlock),
329 ToolUse(ToolUseBlock),
330 ToolResult(ToolResultBlock),
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct TextBlock {
336 pub text: String,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ImageBlock {
342 pub source: ImageSource,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ImageSource {
348 #[serde(rename = "type")]
349 pub source_type: String, pub media_type: String, pub data: String, }
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct ThinkingBlock {
357 pub thinking: String,
358 pub signature: String,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct ToolUseBlock {
364 pub id: String,
365 pub name: String,
366 pub input: Value,
367}
368
369impl ToolUseBlock {
370 pub fn typed_input(&self) -> Option<crate::tool_inputs::ToolInput> {
392 serde_json::from_value(self.input.clone()).ok()
393 }
394
395 pub fn try_typed_input(&self) -> Result<crate::tool_inputs::ToolInput, serde_json::Error> {
399 serde_json::from_value(self.input.clone())
400 }
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct ToolResultBlock {
406 pub tool_use_id: String,
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub content: Option<ToolResultContent>,
409 #[serde(skip_serializing_if = "Option::is_none")]
410 pub is_error: Option<bool>,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(untagged)]
416pub enum ToolResultContent {
417 Text(String),
418 Structured(Vec<Value>),
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct ResultMessage {
424 pub subtype: ResultSubtype,
425 pub is_error: bool,
426 pub duration_ms: u64,
427 pub duration_api_ms: u64,
428 pub num_turns: i32,
429
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub result: Option<String>,
432
433 pub session_id: String,
434 pub total_cost_usd: f64,
435
436 #[serde(skip_serializing_if = "Option::is_none")]
437 pub usage: Option<UsageInfo>,
438
439 #[serde(default)]
441 pub permission_denials: Vec<PermissionDenial>,
442
443 #[serde(default)]
448 pub errors: Vec<String>,
449
450 #[serde(skip_serializing_if = "Option::is_none")]
451 pub uuid: Option<String>,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
459pub struct PermissionDenial {
460 pub tool_name: String,
462
463 pub tool_input: Value,
465
466 pub tool_use_id: String,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472#[serde(rename_all = "snake_case")]
473pub enum ResultSubtype {
474 Success,
475 ErrorMaxTurns,
476 ErrorDuringExecution,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481#[serde(tag = "type", rename_all = "snake_case")]
482pub enum McpServerConfig {
483 Stdio(McpStdioServerConfig),
484 Sse(McpSseServerConfig),
485 Http(McpHttpServerConfig),
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct McpStdioServerConfig {
491 pub command: String,
492 #[serde(skip_serializing_if = "Option::is_none")]
493 pub args: Option<Vec<String>>,
494 #[serde(skip_serializing_if = "Option::is_none")]
495 pub env: Option<std::collections::HashMap<String, String>>,
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct McpSseServerConfig {
501 pub url: String,
502 #[serde(skip_serializing_if = "Option::is_none")]
503 pub headers: Option<std::collections::HashMap<String, String>>,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct McpHttpServerConfig {
509 pub url: String,
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub headers: Option<std::collections::HashMap<String, String>>,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize)]
516#[serde(rename_all = "camelCase")]
517pub enum PermissionMode {
518 Default,
519 AcceptEdits,
520 BypassPermissions,
521 Plan,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct ControlRequest {
535 pub request_id: String,
537 pub request: ControlRequestPayload,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
543#[serde(tag = "subtype", rename_all = "snake_case")]
544pub enum ControlRequestPayload {
545 CanUseTool(ToolPermissionRequest),
547 HookCallback(HookCallbackRequest),
549 McpMessage(McpMessageRequest),
551 Initialize(InitializeRequest),
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
561pub struct PermissionSuggestion {
562 pub tool: String,
564 pub prompt: String,
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize)]
594pub struct ToolPermissionRequest {
595 pub tool_name: String,
597 pub input: Value,
599 #[serde(default)]
601 pub permission_suggestions: Vec<PermissionSuggestion>,
602 #[serde(skip_serializing_if = "Option::is_none")]
604 pub blocked_path: Option<String>,
605}
606
607impl ToolPermissionRequest {
608 pub fn allow(&self, request_id: &str) -> ControlResponse {
623 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
624 }
625
626 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
646 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
647 }
648
649 pub fn allow_with_permissions(
651 &self,
652 modified_input: Value,
653 permissions: Vec<Value>,
654 request_id: &str,
655 ) -> ControlResponse {
656 ControlResponse::from_result(
657 request_id,
658 PermissionResult::allow_with_permissions(modified_input, permissions),
659 )
660 }
661
662 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
679 ControlResponse::from_result(request_id, PermissionResult::deny(message))
680 }
681
682 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
686 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
687 }
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize)]
695#[serde(tag = "behavior", rename_all = "snake_case")]
696pub enum PermissionResult {
697 Allow {
699 #[serde(rename = "updatedInput")]
701 updated_input: Value,
702 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
704 updated_permissions: Option<Vec<Value>>,
705 },
706 Deny {
708 message: String,
710 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
712 interrupt: bool,
713 },
714}
715
716impl PermissionResult {
717 pub fn allow(input: Value) -> Self {
719 PermissionResult::Allow {
720 updated_input: input,
721 updated_permissions: None,
722 }
723 }
724
725 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
727 PermissionResult::Allow {
728 updated_input: input,
729 updated_permissions: Some(permissions),
730 }
731 }
732
733 pub fn deny(message: impl Into<String>) -> Self {
735 PermissionResult::Deny {
736 message: message.into(),
737 interrupt: false,
738 }
739 }
740
741 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
743 PermissionResult::Deny {
744 message: message.into(),
745 interrupt: true,
746 }
747 }
748}
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct HookCallbackRequest {
753 pub callback_id: String,
754 pub input: Value,
755 #[serde(skip_serializing_if = "Option::is_none")]
756 pub tool_use_id: Option<String>,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct McpMessageRequest {
762 pub server_name: String,
763 pub message: Value,
764}
765
766#[derive(Debug, Clone, Serialize, Deserialize)]
768pub struct InitializeRequest {
769 #[serde(skip_serializing_if = "Option::is_none")]
770 pub hooks: Option<Value>,
771}
772
773#[derive(Debug, Clone, Serialize, Deserialize)]
778pub struct ControlResponse {
779 pub response: ControlResponsePayload,
781}
782
783impl ControlResponse {
784 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
788 let response_value = serde_json::to_value(&result)
790 .expect("PermissionResult serialization should never fail");
791 ControlResponse {
792 response: ControlResponsePayload::Success {
793 request_id: request_id.to_string(),
794 response: Some(response_value),
795 },
796 }
797 }
798
799 pub fn success(request_id: &str, response_data: Value) -> Self {
801 ControlResponse {
802 response: ControlResponsePayload::Success {
803 request_id: request_id.to_string(),
804 response: Some(response_data),
805 },
806 }
807 }
808
809 pub fn success_empty(request_id: &str) -> Self {
811 ControlResponse {
812 response: ControlResponsePayload::Success {
813 request_id: request_id.to_string(),
814 response: None,
815 },
816 }
817 }
818
819 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
821 ControlResponse {
822 response: ControlResponsePayload::Error {
823 request_id: request_id.to_string(),
824 error: error_message.into(),
825 },
826 }
827 }
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize)]
832#[serde(tag = "subtype", rename_all = "snake_case")]
833pub enum ControlResponsePayload {
834 Success {
835 request_id: String,
836 #[serde(skip_serializing_if = "Option::is_none")]
837 response: Option<Value>,
838 },
839 Error {
840 request_id: String,
841 error: String,
842 },
843}
844
845#[derive(Debug, Clone, Serialize, Deserialize)]
847pub struct ControlResponseMessage {
848 #[serde(rename = "type")]
849 pub message_type: String,
850 pub response: ControlResponsePayload,
851}
852
853impl From<ControlResponse> for ControlResponseMessage {
854 fn from(resp: ControlResponse) -> Self {
855 ControlResponseMessage {
856 message_type: "control_response".to_string(),
857 response: resp.response,
858 }
859 }
860}
861
862#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct ControlRequestMessage {
865 #[serde(rename = "type")]
866 pub message_type: String,
867 pub request_id: String,
868 pub request: ControlRequestPayload,
869}
870
871impl ControlRequestMessage {
872 pub fn initialize(request_id: impl Into<String>) -> Self {
874 ControlRequestMessage {
875 message_type: "control_request".to_string(),
876 request_id: request_id.into(),
877 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
878 }
879 }
880
881 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
883 ControlRequestMessage {
884 message_type: "control_request".to_string(),
885 request_id: request_id.into(),
886 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
887 }
888 }
889}
890
891#[derive(Debug, Clone, Serialize, Deserialize)]
893pub struct UsageInfo {
894 pub input_tokens: u32,
895 pub cache_creation_input_tokens: u32,
896 pub cache_read_input_tokens: u32,
897 pub output_tokens: u32,
898 pub server_tool_use: ServerToolUse,
899 pub service_tier: String,
900}
901
902#[derive(Debug, Clone, Serialize, Deserialize)]
904pub struct ServerToolUse {
905 pub web_search_requests: u32,
906}
907
908impl ClaudeInput {
909 pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
911 ClaudeInput::User(UserMessage {
912 message: MessageContent {
913 role: "user".to_string(),
914 content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
915 },
916 session_id: Some(session_id),
917 })
918 }
919
920 pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
922 ClaudeInput::User(UserMessage {
923 message: MessageContent {
924 role: "user".to_string(),
925 content: blocks,
926 },
927 session_id: Some(session_id),
928 })
929 }
930
931 pub fn user_message_with_image(
934 image_data: String,
935 media_type: String,
936 text: Option<String>,
937 session_id: Uuid,
938 ) -> Result<Self, String> {
939 let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
941
942 if !valid_types.contains(&media_type.as_str()) {
943 return Err(format!(
944 "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
945 media_type
946 ));
947 }
948
949 let mut blocks = vec![ContentBlock::Image(ImageBlock {
950 source: ImageSource {
951 source_type: "base64".to_string(),
952 media_type,
953 data: image_data,
954 },
955 })];
956
957 if let Some(text_content) = text {
958 blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
959 }
960
961 Ok(Self::user_message_blocks(blocks, session_id))
962 }
963}
964
965impl ClaudeOutput {
966 pub fn message_type(&self) -> String {
968 match self {
969 ClaudeOutput::System(_) => "system".to_string(),
970 ClaudeOutput::User(_) => "user".to_string(),
971 ClaudeOutput::Assistant(_) => "assistant".to_string(),
972 ClaudeOutput::Result(_) => "result".to_string(),
973 ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
974 ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
975 }
976 }
977
978 pub fn is_control_request(&self) -> bool {
980 matches!(self, ClaudeOutput::ControlRequest(_))
981 }
982
983 pub fn is_control_response(&self) -> bool {
985 matches!(self, ClaudeOutput::ControlResponse(_))
986 }
987
988 pub fn as_control_request(&self) -> Option<&ControlRequest> {
990 match self {
991 ClaudeOutput::ControlRequest(req) => Some(req),
992 _ => None,
993 }
994 }
995
996 pub fn is_error(&self) -> bool {
998 matches!(self, ClaudeOutput::Result(r) if r.is_error)
999 }
1000
1001 pub fn is_assistant_message(&self) -> bool {
1003 matches!(self, ClaudeOutput::Assistant(_))
1004 }
1005
1006 pub fn is_system_message(&self) -> bool {
1008 matches!(self, ClaudeOutput::System(_))
1009 }
1010
1011 pub fn is_system_init(&self) -> bool {
1022 matches!(self, ClaudeOutput::System(sys) if sys.is_init())
1023 }
1024
1025 pub fn session_id(&self) -> Option<&str> {
1041 match self {
1042 ClaudeOutput::System(sys) => sys.data.get("session_id").and_then(|v| v.as_str()),
1043 ClaudeOutput::Assistant(ass) => Some(&ass.session_id),
1044 ClaudeOutput::Result(res) => Some(&res.session_id),
1045 ClaudeOutput::User(_) => None,
1046 ClaudeOutput::ControlRequest(_) => None,
1047 ClaudeOutput::ControlResponse(_) => None,
1048 }
1049 }
1050
1051 pub fn as_tool_use(&self, tool_name: &str) -> Option<&ToolUseBlock> {
1070 match self {
1071 ClaudeOutput::Assistant(ass) => {
1072 ass.message.content.iter().find_map(|block| match block {
1073 ContentBlock::ToolUse(tu) if tu.name == tool_name => Some(tu),
1074 _ => None,
1075 })
1076 }
1077 _ => None,
1078 }
1079 }
1080
1081 pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUseBlock> {
1101 let content = match self {
1102 ClaudeOutput::Assistant(ass) => Some(&ass.message.content),
1103 _ => None,
1104 };
1105
1106 content
1107 .into_iter()
1108 .flat_map(|c| c.iter())
1109 .filter_map(|block| match block {
1110 ContentBlock::ToolUse(tu) => Some(tu),
1111 _ => None,
1112 })
1113 }
1114
1115 pub fn text_content(&self) -> Option<String> {
1131 match self {
1132 ClaudeOutput::Assistant(ass) => {
1133 let texts: Vec<&str> = ass
1134 .message
1135 .content
1136 .iter()
1137 .filter_map(|block| match block {
1138 ContentBlock::Text(t) => Some(t.text.as_str()),
1139 _ => None,
1140 })
1141 .collect();
1142
1143 if texts.is_empty() {
1144 None
1145 } else {
1146 Some(texts.join(""))
1147 }
1148 }
1149 _ => None,
1150 }
1151 }
1152
1153 pub fn as_assistant(&self) -> Option<&AssistantMessage> {
1168 match self {
1169 ClaudeOutput::Assistant(ass) => Some(ass),
1170 _ => None,
1171 }
1172 }
1173
1174 pub fn as_result(&self) -> Option<&ResultMessage> {
1190 match self {
1191 ClaudeOutput::Result(res) => Some(res),
1192 _ => None,
1193 }
1194 }
1195
1196 pub fn as_system(&self) -> Option<&SystemMessage> {
1198 match self {
1199 ClaudeOutput::System(sys) => Some(sys),
1200 _ => None,
1201 }
1202 }
1203
1204 pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
1209 match Self::parse_json(s) {
1211 Ok(output) => Ok(output),
1212 Err(first_error) => {
1213 if let Some(json_start) = s.find('{') {
1215 let trimmed = &s[json_start..];
1216 match Self::parse_json(trimmed) {
1217 Ok(output) => Ok(output),
1218 Err(_) => {
1219 Err(first_error)
1221 }
1222 }
1223 } else {
1224 Err(first_error)
1225 }
1226 }
1227 }
1228 }
1229
1230 pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
1232 let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
1234 raw_json: Value::String(s.to_string()),
1235 error_message: format!("Invalid JSON: {}", e),
1236 })?;
1237
1238 serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
1240 raw_json: value,
1241 error_message: e.to_string(),
1242 })
1243 }
1244}
1245
1246#[cfg(test)]
1247mod tests {
1248 use super::*;
1249
1250 #[test]
1251 fn test_serialize_user_message() {
1252 let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1253 let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
1254 let json = serde_json::to_string(&input).unwrap();
1255 assert!(json.contains("\"type\":\"user\""));
1256 assert!(json.contains("\"role\":\"user\""));
1257 assert!(json.contains("\"text\":\"Hello, Claude!\""));
1258 assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
1259 }
1260
1261 #[test]
1262 fn test_deserialize_assistant_message() {
1263 let json = r#"{
1264 "type": "assistant",
1265 "message": {
1266 "id": "msg_123",
1267 "role": "assistant",
1268 "model": "claude-3-sonnet",
1269 "content": [{"type": "text", "text": "Hello! How can I help you?"}]
1270 },
1271 "session_id": "123"
1272 }"#;
1273
1274 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1275 assert!(output.is_assistant_message());
1276 }
1277
1278 #[test]
1279 fn test_deserialize_result_message() {
1280 let json = r#"{
1281 "type": "result",
1282 "subtype": "success",
1283 "is_error": false,
1284 "duration_ms": 100,
1285 "duration_api_ms": 200,
1286 "num_turns": 1,
1287 "result": "Done",
1288 "session_id": "123",
1289 "total_cost_usd": 0.01,
1290 "permission_denials": []
1291 }"#;
1292
1293 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1294 assert!(!output.is_error());
1295 }
1296
1297 #[test]
1298 fn test_deserialize_result_with_permission_denials() {
1299 let json = r#"{
1300 "type": "result",
1301 "subtype": "success",
1302 "is_error": false,
1303 "duration_ms": 100,
1304 "duration_api_ms": 200,
1305 "num_turns": 2,
1306 "result": "Done",
1307 "session_id": "123",
1308 "total_cost_usd": 0.01,
1309 "permission_denials": [
1310 {
1311 "tool_name": "Bash",
1312 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1313 "tool_use_id": "toolu_123"
1314 }
1315 ]
1316 }"#;
1317
1318 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1319 if let ClaudeOutput::Result(result) = output {
1320 assert_eq!(result.permission_denials.len(), 1);
1321 assert_eq!(result.permission_denials[0].tool_name, "Bash");
1322 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1323 assert_eq!(
1324 result.permission_denials[0]
1325 .tool_input
1326 .get("command")
1327 .unwrap(),
1328 "rm -rf /"
1329 );
1330 } else {
1331 panic!("Expected Result");
1332 }
1333 }
1334
1335 #[test]
1336 fn test_permission_denial_roundtrip() {
1337 let denial = PermissionDenial {
1338 tool_name: "Write".to_string(),
1339 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1340 tool_use_id: "toolu_456".to_string(),
1341 };
1342
1343 let json = serde_json::to_string(&denial).unwrap();
1344 assert!(json.contains("\"tool_name\":\"Write\""));
1345 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1346 assert!(json.contains("/etc/passwd"));
1347
1348 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1349 assert_eq!(parsed, denial);
1350 }
1351
1352 #[test]
1357 fn test_deserialize_control_request_can_use_tool() {
1358 let json = r#"{
1359 "type": "control_request",
1360 "request_id": "perm-abc123",
1361 "request": {
1362 "subtype": "can_use_tool",
1363 "tool_name": "Write",
1364 "input": {
1365 "file_path": "/home/user/hello.py",
1366 "content": "print('hello')"
1367 },
1368 "permission_suggestions": [],
1369 "blocked_path": null
1370 }
1371 }"#;
1372
1373 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1374 assert!(output.is_control_request());
1375
1376 if let ClaudeOutput::ControlRequest(req) = output {
1377 assert_eq!(req.request_id, "perm-abc123");
1378 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1379 assert_eq!(perm_req.tool_name, "Write");
1380 assert_eq!(
1381 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1382 "/home/user/hello.py"
1383 );
1384 } else {
1385 panic!("Expected CanUseTool payload");
1386 }
1387 } else {
1388 panic!("Expected ControlRequest");
1389 }
1390 }
1391
1392 #[test]
1393 fn test_tool_permission_request_allow() {
1394 let req = ToolPermissionRequest {
1395 tool_name: "Read".to_string(),
1396 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1397 permission_suggestions: vec![],
1398 blocked_path: None,
1399 };
1400
1401 let response = req.allow("req-123");
1402 let message: ControlResponseMessage = response.into();
1403
1404 let json = serde_json::to_string(&message).unwrap();
1405 assert!(json.contains("\"type\":\"control_response\""));
1406 assert!(json.contains("\"subtype\":\"success\""));
1407 assert!(json.contains("\"request_id\":\"req-123\""));
1408 assert!(json.contains("\"behavior\":\"allow\""));
1409 assert!(json.contains("\"updatedInput\""));
1410 }
1411
1412 #[test]
1413 fn test_tool_permission_request_allow_with_modified_input() {
1414 let req = ToolPermissionRequest {
1415 tool_name: "Write".to_string(),
1416 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1417 permission_suggestions: vec![],
1418 blocked_path: None,
1419 };
1420
1421 let modified_input = serde_json::json!({
1422 "file_path": "/tmp/safe/passwd",
1423 "content": "test"
1424 });
1425 let response = req.allow_with(modified_input, "req-456");
1426 let message: ControlResponseMessage = response.into();
1427
1428 let json = serde_json::to_string(&message).unwrap();
1429 assert!(json.contains("/tmp/safe/passwd"));
1430 assert!(!json.contains("/etc/passwd"));
1431 }
1432
1433 #[test]
1434 fn test_tool_permission_request_deny() {
1435 let req = ToolPermissionRequest {
1436 tool_name: "Bash".to_string(),
1437 input: serde_json::json!({"command": "sudo rm -rf /"}),
1438 permission_suggestions: vec![],
1439 blocked_path: None,
1440 };
1441
1442 let response = req.deny("Dangerous command blocked", "req-789");
1443 let message: ControlResponseMessage = response.into();
1444
1445 let json = serde_json::to_string(&message).unwrap();
1446 assert!(json.contains("\"behavior\":\"deny\""));
1447 assert!(json.contains("Dangerous command blocked"));
1448 assert!(!json.contains("\"interrupt\":true"));
1449 }
1450
1451 #[test]
1452 fn test_tool_permission_request_deny_and_stop() {
1453 let req = ToolPermissionRequest {
1454 tool_name: "Bash".to_string(),
1455 input: serde_json::json!({"command": "rm -rf /"}),
1456 permission_suggestions: vec![],
1457 blocked_path: None,
1458 };
1459
1460 let response = req.deny_and_stop("Security violation", "req-000");
1461 let message: ControlResponseMessage = response.into();
1462
1463 let json = serde_json::to_string(&message).unwrap();
1464 assert!(json.contains("\"behavior\":\"deny\""));
1465 assert!(json.contains("\"interrupt\":true"));
1466 }
1467
1468 #[test]
1469 fn test_permission_result_serialization() {
1470 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1472 let json = serde_json::to_string(&allow).unwrap();
1473 assert!(json.contains("\"behavior\":\"allow\""));
1474 assert!(json.contains("\"updatedInput\""));
1475
1476 let deny = PermissionResult::deny("Not allowed");
1478 let json = serde_json::to_string(&deny).unwrap();
1479 assert!(json.contains("\"behavior\":\"deny\""));
1480 assert!(json.contains("\"message\":\"Not allowed\""));
1481 assert!(!json.contains("\"interrupt\""));
1482
1483 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1485 let json = serde_json::to_string(&deny_stop).unwrap();
1486 assert!(json.contains("\"interrupt\":true"));
1487 }
1488
1489 #[test]
1490 fn test_control_request_message_initialize() {
1491 let init = ControlRequestMessage::initialize("init-1");
1492
1493 let json = serde_json::to_string(&init).unwrap();
1494 assert!(json.contains("\"type\":\"control_request\""));
1495 assert!(json.contains("\"request_id\":\"init-1\""));
1496 assert!(json.contains("\"subtype\":\"initialize\""));
1497 }
1498
1499 #[test]
1500 fn test_control_response_error() {
1501 let response = ControlResponse::error("req-err", "Something went wrong");
1502 let message: ControlResponseMessage = response.into();
1503
1504 let json = serde_json::to_string(&message).unwrap();
1505 assert!(json.contains("\"subtype\":\"error\""));
1506 assert!(json.contains("\"error\":\"Something went wrong\""));
1507 }
1508
1509 #[test]
1510 fn test_roundtrip_control_request() {
1511 let original_json = r#"{
1513 "type": "control_request",
1514 "request_id": "test-123",
1515 "request": {
1516 "subtype": "can_use_tool",
1517 "tool_name": "Bash",
1518 "input": {"command": "ls -la"},
1519 "permission_suggestions": []
1520 }
1521 }"#;
1522
1523 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1525
1526 let reserialized = serde_json::to_string(&output).unwrap();
1528 assert!(reserialized.contains("control_request"));
1529 assert!(reserialized.contains("test-123"));
1530 assert!(reserialized.contains("Bash"));
1531 }
1532
1533 #[test]
1534 fn test_permission_suggestions_parsing() {
1535 let json = r#"{
1537 "type": "control_request",
1538 "request_id": "perm-456",
1539 "request": {
1540 "subtype": "can_use_tool",
1541 "tool_name": "Bash",
1542 "input": {"command": "npm test"},
1543 "permission_suggestions": [
1544 {"tool": "Bash", "prompt": "run tests"},
1545 {"tool": "Bash", "prompt": "install dependencies"}
1546 ]
1547 }
1548 }"#;
1549
1550 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1551 if let ClaudeOutput::ControlRequest(req) = output {
1552 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1553 assert_eq!(perm_req.permission_suggestions.len(), 2);
1554 assert_eq!(perm_req.permission_suggestions[0].tool, "Bash");
1555 assert_eq!(perm_req.permission_suggestions[0].prompt, "run tests");
1556 assert_eq!(perm_req.permission_suggestions[1].tool, "Bash");
1557 assert_eq!(
1558 perm_req.permission_suggestions[1].prompt,
1559 "install dependencies"
1560 );
1561 } else {
1562 panic!("Expected CanUseTool payload");
1563 }
1564 } else {
1565 panic!("Expected ControlRequest");
1566 }
1567 }
1568
1569 #[test]
1570 fn test_permission_suggestion_roundtrip() {
1571 let suggestion = PermissionSuggestion {
1572 tool: "Bash".to_string(),
1573 prompt: "run tests".to_string(),
1574 };
1575
1576 let json = serde_json::to_string(&suggestion).unwrap();
1577 assert!(json.contains("\"tool\":\"Bash\""));
1578 assert!(json.contains("\"prompt\":\"run tests\""));
1579
1580 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1581 assert_eq!(parsed, suggestion);
1582 }
1583
1584 #[test]
1589 fn test_system_message_init() {
1590 let json = r#"{
1591 "type": "system",
1592 "subtype": "init",
1593 "session_id": "test-session-123",
1594 "cwd": "/home/user/project",
1595 "model": "claude-sonnet-4",
1596 "tools": ["Bash", "Read", "Write"],
1597 "mcp_servers": []
1598 }"#;
1599
1600 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1601 if let ClaudeOutput::System(sys) = output {
1602 assert!(sys.is_init());
1603 assert!(!sys.is_status());
1604 assert!(!sys.is_compact_boundary());
1605
1606 let init = sys.as_init().expect("Should parse as init");
1607 assert_eq!(init.session_id, "test-session-123");
1608 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
1609 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
1610 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
1611 } else {
1612 panic!("Expected System message");
1613 }
1614 }
1615
1616 #[test]
1617 fn test_system_message_status() {
1618 let json = r#"{
1619 "type": "system",
1620 "subtype": "status",
1621 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1622 "status": "compacting",
1623 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
1624 }"#;
1625
1626 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1627 if let ClaudeOutput::System(sys) = output {
1628 assert!(sys.is_status());
1629 assert!(!sys.is_init());
1630
1631 let status = sys.as_status().expect("Should parse as status");
1632 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1633 assert_eq!(status.status, Some("compacting".to_string()));
1634 assert_eq!(
1635 status.uuid,
1636 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
1637 );
1638 } else {
1639 panic!("Expected System message");
1640 }
1641 }
1642
1643 #[test]
1644 fn test_system_message_status_null() {
1645 let json = r#"{
1646 "type": "system",
1647 "subtype": "status",
1648 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1649 "status": null,
1650 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
1651 }"#;
1652
1653 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1654 if let ClaudeOutput::System(sys) = output {
1655 let status = sys.as_status().expect("Should parse as status");
1656 assert_eq!(status.status, None);
1657 } else {
1658 panic!("Expected System message");
1659 }
1660 }
1661
1662 #[test]
1663 fn test_system_message_compact_boundary() {
1664 let json = r#"{
1665 "type": "system",
1666 "subtype": "compact_boundary",
1667 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1668 "compact_metadata": {
1669 "pre_tokens": 155285,
1670 "trigger": "auto"
1671 },
1672 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1673 }"#;
1674
1675 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1676 if let ClaudeOutput::System(sys) = output {
1677 assert!(sys.is_compact_boundary());
1678 assert!(!sys.is_init());
1679 assert!(!sys.is_status());
1680
1681 let compact = sys
1682 .as_compact_boundary()
1683 .expect("Should parse as compact_boundary");
1684 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1685 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1686 assert_eq!(compact.compact_metadata.trigger, "auto");
1687 } else {
1688 panic!("Expected System message");
1689 }
1690 }
1691
1692 #[test]
1697 fn test_is_system_init() {
1698 let init_json = r#"{
1699 "type": "system",
1700 "subtype": "init",
1701 "session_id": "test-session"
1702 }"#;
1703 let output: ClaudeOutput = serde_json::from_str(init_json).unwrap();
1704 assert!(output.is_system_init());
1705
1706 let status_json = r#"{
1707 "type": "system",
1708 "subtype": "status",
1709 "session_id": "test-session"
1710 }"#;
1711 let output: ClaudeOutput = serde_json::from_str(status_json).unwrap();
1712 assert!(!output.is_system_init());
1713 }
1714
1715 #[test]
1716 fn test_session_id() {
1717 let result_json = r#"{
1719 "type": "result",
1720 "subtype": "success",
1721 "is_error": false,
1722 "duration_ms": 100,
1723 "duration_api_ms": 200,
1724 "num_turns": 1,
1725 "session_id": "result-session",
1726 "total_cost_usd": 0.01
1727 }"#;
1728 let output: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1729 assert_eq!(output.session_id(), Some("result-session"));
1730
1731 let assistant_json = r#"{
1733 "type": "assistant",
1734 "message": {
1735 "id": "msg_1",
1736 "role": "assistant",
1737 "model": "claude-3",
1738 "content": []
1739 },
1740 "session_id": "assistant-session"
1741 }"#;
1742 let output: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1743 assert_eq!(output.session_id(), Some("assistant-session"));
1744
1745 let system_json = r#"{
1747 "type": "system",
1748 "subtype": "init",
1749 "session_id": "system-session"
1750 }"#;
1751 let output: ClaudeOutput = serde_json::from_str(system_json).unwrap();
1752 assert_eq!(output.session_id(), Some("system-session"));
1753 }
1754
1755 #[test]
1756 fn test_as_tool_use() {
1757 let json = r#"{
1758 "type": "assistant",
1759 "message": {
1760 "id": "msg_1",
1761 "role": "assistant",
1762 "model": "claude-3",
1763 "content": [
1764 {"type": "text", "text": "Let me run that command."},
1765 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls -la"}},
1766 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/test"}}
1767 ]
1768 },
1769 "session_id": "abc"
1770 }"#;
1771 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1772
1773 let bash = output.as_tool_use("Bash");
1775 assert!(bash.is_some());
1776 assert_eq!(bash.unwrap().id, "tu_1");
1777
1778 let read = output.as_tool_use("Read");
1780 assert!(read.is_some());
1781 assert_eq!(read.unwrap().id, "tu_2");
1782
1783 assert!(output.as_tool_use("Write").is_none());
1785
1786 let result_json = r#"{
1788 "type": "result",
1789 "subtype": "success",
1790 "is_error": false,
1791 "duration_ms": 100,
1792 "duration_api_ms": 200,
1793 "num_turns": 1,
1794 "session_id": "abc",
1795 "total_cost_usd": 0.01
1796 }"#;
1797 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1798 assert!(result.as_tool_use("Bash").is_none());
1799 }
1800
1801 #[test]
1802 fn test_tool_uses() {
1803 let json = r#"{
1804 "type": "assistant",
1805 "message": {
1806 "id": "msg_1",
1807 "role": "assistant",
1808 "model": "claude-3",
1809 "content": [
1810 {"type": "text", "text": "Running commands..."},
1811 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}},
1812 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/a"}},
1813 {"type": "tool_use", "id": "tu_3", "name": "Write", "input": {"file_path": "/tmp/b", "content": "x"}}
1814 ]
1815 },
1816 "session_id": "abc"
1817 }"#;
1818 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1819
1820 let tools: Vec<_> = output.tool_uses().collect();
1821 assert_eq!(tools.len(), 3);
1822 assert_eq!(tools[0].name, "Bash");
1823 assert_eq!(tools[1].name, "Read");
1824 assert_eq!(tools[2].name, "Write");
1825 }
1826
1827 #[test]
1828 fn test_text_content() {
1829 let json = r#"{
1831 "type": "assistant",
1832 "message": {
1833 "id": "msg_1",
1834 "role": "assistant",
1835 "model": "claude-3",
1836 "content": [{"type": "text", "text": "Hello, world!"}]
1837 },
1838 "session_id": "abc"
1839 }"#;
1840 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1841 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1842
1843 let json = r#"{
1845 "type": "assistant",
1846 "message": {
1847 "id": "msg_1",
1848 "role": "assistant",
1849 "model": "claude-3",
1850 "content": [
1851 {"type": "text", "text": "Hello, "},
1852 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}},
1853 {"type": "text", "text": "world!"}
1854 ]
1855 },
1856 "session_id": "abc"
1857 }"#;
1858 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1859 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
1860
1861 let json = r#"{
1863 "type": "assistant",
1864 "message": {
1865 "id": "msg_1",
1866 "role": "assistant",
1867 "model": "claude-3",
1868 "content": [{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}}]
1869 },
1870 "session_id": "abc"
1871 }"#;
1872 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1873 assert_eq!(output.text_content(), None);
1874
1875 let json = r#"{
1877 "type": "result",
1878 "subtype": "success",
1879 "is_error": false,
1880 "duration_ms": 100,
1881 "duration_api_ms": 200,
1882 "num_turns": 1,
1883 "session_id": "abc",
1884 "total_cost_usd": 0.01
1885 }"#;
1886 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1887 assert_eq!(output.text_content(), None);
1888 }
1889
1890 #[test]
1891 fn test_as_assistant() {
1892 let json = r#"{
1893 "type": "assistant",
1894 "message": {
1895 "id": "msg_1",
1896 "role": "assistant",
1897 "model": "claude-sonnet-4",
1898 "content": []
1899 },
1900 "session_id": "abc"
1901 }"#;
1902 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1903
1904 let assistant = output.as_assistant();
1905 assert!(assistant.is_some());
1906 assert_eq!(assistant.unwrap().message.model, "claude-sonnet-4");
1907
1908 let result_json = r#"{
1910 "type": "result",
1911 "subtype": "success",
1912 "is_error": false,
1913 "duration_ms": 100,
1914 "duration_api_ms": 200,
1915 "num_turns": 1,
1916 "session_id": "abc",
1917 "total_cost_usd": 0.01
1918 }"#;
1919 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1920 assert!(result.as_assistant().is_none());
1921 }
1922
1923 #[test]
1924 fn test_as_result() {
1925 let json = r#"{
1926 "type": "result",
1927 "subtype": "success",
1928 "is_error": false,
1929 "duration_ms": 100,
1930 "duration_api_ms": 200,
1931 "num_turns": 5,
1932 "session_id": "abc",
1933 "total_cost_usd": 0.05
1934 }"#;
1935 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1936
1937 let result = output.as_result();
1938 assert!(result.is_some());
1939 assert_eq!(result.unwrap().num_turns, 5);
1940 assert_eq!(result.unwrap().total_cost_usd, 0.05);
1941
1942 let assistant_json = r#"{
1944 "type": "assistant",
1945 "message": {
1946 "id": "msg_1",
1947 "role": "assistant",
1948 "model": "claude-3",
1949 "content": []
1950 },
1951 "session_id": "abc"
1952 }"#;
1953 let assistant: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
1954 assert!(assistant.as_result().is_none());
1955 }
1956
1957 #[test]
1958 fn test_as_system() {
1959 let json = r#"{
1960 "type": "system",
1961 "subtype": "init",
1962 "session_id": "abc",
1963 "model": "claude-3"
1964 }"#;
1965 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1966
1967 let system = output.as_system();
1968 assert!(system.is_some());
1969 assert!(system.unwrap().is_init());
1970
1971 let result_json = r#"{
1973 "type": "result",
1974 "subtype": "success",
1975 "is_error": false,
1976 "duration_ms": 100,
1977 "duration_api_ms": 200,
1978 "num_turns": 1,
1979 "session_id": "abc",
1980 "total_cost_usd": 0.01
1981 }"#;
1982 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
1983 assert!(result.as_system().is_none());
1984 }
1985
1986 #[test]
1991 fn test_deserialize_result_message_with_errors() {
1992 let json = r#"{
1993 "type": "result",
1994 "subtype": "error_during_execution",
1995 "duration_ms": 0,
1996 "duration_api_ms": 0,
1997 "is_error": true,
1998 "num_turns": 0,
1999 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
2000 "total_cost_usd": 0,
2001 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
2002 "permission_denials": []
2003 }"#;
2004
2005 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2006 assert!(output.is_error());
2007
2008 if let ClaudeOutput::Result(res) = output {
2009 assert!(res.is_error);
2010 assert_eq!(res.errors.len(), 1);
2011 assert!(res.errors[0].contains("No conversation found"));
2012 } else {
2013 panic!("Expected Result message");
2014 }
2015 }
2016
2017 #[test]
2018 fn test_deserialize_result_message_errors_defaults_empty() {
2019 let json = r#"{
2021 "type": "result",
2022 "subtype": "success",
2023 "is_error": false,
2024 "duration_ms": 100,
2025 "duration_api_ms": 200,
2026 "num_turns": 1,
2027 "session_id": "123",
2028 "total_cost_usd": 0.01
2029 }"#;
2030
2031 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2032 if let ClaudeOutput::Result(res) = output {
2033 assert!(res.errors.is_empty());
2034 } else {
2035 panic!("Expected Result message");
2036 }
2037 }
2038
2039 #[test]
2040 fn test_result_message_errors_roundtrip() {
2041 let json = r#"{
2042 "type": "result",
2043 "subtype": "error_during_execution",
2044 "is_error": true,
2045 "duration_ms": 0,
2046 "duration_api_ms": 0,
2047 "num_turns": 0,
2048 "session_id": "test-session",
2049 "total_cost_usd": 0.0,
2050 "errors": ["Error 1", "Error 2"]
2051 }"#;
2052
2053 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2054 let reserialized = serde_json::to_string(&output).unwrap();
2055
2056 assert!(reserialized.contains("Error 1"));
2058 assert!(reserialized.contains("Error 2"));
2059 }
2060}