1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ResultMessage {
8 pub subtype: ResultSubtype,
9 pub is_error: bool,
10 pub duration_ms: u64,
11 pub duration_api_ms: u64,
12
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub ttft_ms: Option<u64>,
16
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub ttft_stream_ms: Option<u64>,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub time_to_request_ms: Option<u64>,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub time_to_request_from_spawn_ms: Option<u64>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub warm_spare_claimed: Option<bool>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub time_origin_ms: Option<u64>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
40 pub request_sent_wall_ms: Option<f64>,
41
42 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub first_content_frame_ms: Option<u64>,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub first_stream_post_ms: Option<u64>,
51
52 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub first_stream_post_ack_ms: Option<u64>,
56
57 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub first_stream_post_wall_ms: Option<f64>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub user_message_uuid: Option<String>,
65
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub user_message_uuids: Vec<String>,
74
75 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub resume_reason: Option<String>,
84
85 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub local_command: Option<String>,
90
91 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub queued_turn_count: Option<u64>,
98
99 pub num_turns: i32,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub result: Option<String>,
103
104 #[serde(alias = "sessionId")]
105 pub session_id: String,
106 pub total_cost_usd: f64,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub usage: Option<UsageInfo>,
110
111 #[serde(default)]
113 pub permission_denials: Vec<PermissionDenial>,
114
115 #[serde(default)]
120 pub errors: Vec<String>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub uuid: Option<String>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub api_error_status: Option<u16>,
128
129 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub api_error_code: Option<String>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub stop_reason: Option<String>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub terminal_reason: Option<String>,
144
145 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub result_index: Option<u64>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub fast_mode_state: Option<String>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
163 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
164
165 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
167 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
168
169 #[serde(skip_serializing_if = "Option::is_none")]
173 pub subagent_stats: Option<SubagentStats>,
174
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub structured_output: Option<Value>,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub deferred_tool_use: Option<DeferredToolUse>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub origin: Option<super::message_types::MessageOrigin>,
186
187 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub runner_exit: Option<RunnerExit>,
192
193 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub startup_failure_reason: Option<StartupFailureReason>,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Hash)]
208pub enum StartupFailureReason {
209 OrgPinApiKeyConflict,
212 OrgVerifyFailed,
215 OrgPinMismatch,
217 ManagedSettingsInvalid,
220 RemoteSettingsRequiredUnavailable,
222 GatewaySigninRequired,
224 GatewayAccessDenied,
226 ProxyInvalid,
228 TempDirUnusable,
230 CwdUnavailable,
232 ShellToolMissing,
235 SessionHeldByBackground,
238 WorktreeResumeRefused,
242 WorktreeUnverified,
245 CliVersionTooOld,
247 BypassRoot,
249 Unknown(String),
251}
252
253impl StartupFailureReason {
254 pub fn as_str(&self) -> &str {
255 match self {
256 Self::OrgPinApiKeyConflict => "org_pin_api_key_conflict",
257 Self::OrgVerifyFailed => "org_verify_failed",
258 Self::OrgPinMismatch => "org_pin_mismatch",
259 Self::ManagedSettingsInvalid => "managed_settings_invalid",
260 Self::RemoteSettingsRequiredUnavailable => "remote_settings_required_unavailable",
261 Self::GatewaySigninRequired => "gateway_signin_required",
262 Self::GatewayAccessDenied => "gateway_access_denied",
263 Self::ProxyInvalid => "proxy_invalid",
264 Self::TempDirUnusable => "temp_dir_unusable",
265 Self::CwdUnavailable => "cwd_unavailable",
266 Self::ShellToolMissing => "shell_tool_missing",
267 Self::SessionHeldByBackground => "session_held_by_background",
268 Self::WorktreeResumeRefused => "worktree_resume_refused",
269 Self::WorktreeUnverified => "worktree_unverified",
270 Self::CliVersionTooOld => "cli_version_too_old",
271 Self::BypassRoot => "bypass_root",
272 Self::Unknown(s) => s.as_str(),
273 }
274 }
275}
276
277impl fmt::Display for StartupFailureReason {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 f.write_str(self.as_str())
280 }
281}
282
283impl From<&str> for StartupFailureReason {
284 fn from(s: &str) -> Self {
285 match s {
286 "org_pin_api_key_conflict" => Self::OrgPinApiKeyConflict,
287 "org_verify_failed" => Self::OrgVerifyFailed,
288 "org_pin_mismatch" => Self::OrgPinMismatch,
289 "managed_settings_invalid" => Self::ManagedSettingsInvalid,
290 "remote_settings_required_unavailable" => Self::RemoteSettingsRequiredUnavailable,
291 "gateway_signin_required" => Self::GatewaySigninRequired,
292 "gateway_access_denied" => Self::GatewayAccessDenied,
293 "proxy_invalid" => Self::ProxyInvalid,
294 "temp_dir_unusable" => Self::TempDirUnusable,
295 "cwd_unavailable" => Self::CwdUnavailable,
296 "shell_tool_missing" => Self::ShellToolMissing,
297 "session_held_by_background" => Self::SessionHeldByBackground,
298 "worktree_resume_refused" => Self::WorktreeResumeRefused,
299 "worktree_unverified" => Self::WorktreeUnverified,
300 "cli_version_too_old" => Self::CliVersionTooOld,
301 "bypass_root" => Self::BypassRoot,
302 other => Self::Unknown(other.to_string()),
303 }
304 }
305}
306
307impl Serialize for StartupFailureReason {
308 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
309 serializer.serialize_str(self.as_str())
310 }
311}
312
313impl<'de> Deserialize<'de> for StartupFailureReason {
314 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
315 let s = String::deserialize(deserializer)?;
316 Ok(Self::from(s.as_str()))
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
324pub struct RunnerExit {
325 pub phase: RunnerExitPhase,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub exit_code: Option<i64>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub signal: Option<String>,
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Hash)]
341pub enum RunnerExitPhase {
342 Setup,
344 Run,
346 Unknown(String),
348}
349
350impl RunnerExitPhase {
351 pub fn as_str(&self) -> &str {
352 match self {
353 Self::Setup => "setup",
354 Self::Run => "run",
355 Self::Unknown(s) => s.as_str(),
356 }
357 }
358}
359
360impl std::fmt::Display for RunnerExitPhase {
361 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
362 f.write_str(self.as_str())
363 }
364}
365
366impl From<&str> for RunnerExitPhase {
367 fn from(s: &str) -> Self {
368 match s {
369 "setup" => Self::Setup,
370 "run" => Self::Run,
371 other => Self::Unknown(other.to_string()),
372 }
373 }
374}
375
376impl Serialize for RunnerExitPhase {
377 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
378 serializer.serialize_str(self.as_str())
379 }
380}
381
382impl<'de> Deserialize<'de> for RunnerExitPhase {
383 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
384 let s = String::deserialize(deserializer)?;
385 Ok(Self::from(s.as_str()))
386 }
387}
388
389#[derive(Debug, Clone, Default, Serialize, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub struct ModelUsageEntry {
397 #[serde(default)]
398 pub input_tokens: u64,
399 #[serde(default)]
400 pub output_tokens: u64,
401 #[serde(default)]
402 pub cache_read_input_tokens: u64,
403 #[serde(default)]
404 pub cache_creation_input_tokens: u64,
405 #[serde(default, rename = "costUSD")]
406 pub cost_usd: f64,
407 #[serde(default)]
408 pub web_search_requests: u32,
409 #[serde(default)]
410 pub context_window: u64,
411 #[serde(default)]
412 pub max_output_tokens: u64,
413 #[serde(flatten)]
414 pub extra: serde_json::Map<String, serde_json::Value>,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
419pub struct DeferredToolUse {
420 pub id: String,
421 pub name: String,
422 pub input: Value,
423}
424
425#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
434pub struct SubagentStats {
435 pub spawned: u64,
437 pub requested: SubagentSpawnRequests,
440 pub started_in_background: u64,
443 #[serde(default)]
445 pub by_type: std::collections::BTreeMap<String, u64>,
446 pub max_depth: u64,
449 pub spawned_by_subagents: u64,
451 pub completed: u64,
452 pub failed: u64,
453 pub killed: SubagentKillCounts,
455 pub refused: SubagentRefusalCounts,
458}
459
460#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
463pub struct SubagentSpawnRequests {
464 pub background: u64,
465 pub foreground: u64,
466 pub unset: u64,
467}
468
469#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
473pub struct SubagentKillCounts {
474 pub parent: u64,
475 pub user: u64,
476 pub system: u64,
477}
478
479#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
481pub struct SubagentRefusalCounts {
482 pub depth_limit: u64,
483 pub concurrency_limit: u64,
484 pub budget: u64,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
492pub struct PermissionDenial {
493 pub tool_name: String,
495
496 pub tool_input: Value,
498
499 pub tool_use_id: String,
501}
502
503#[derive(Debug, Clone, PartialEq, Eq, Hash)]
506pub enum FastModeDisabledReason {
507 Free,
509 Preference,
511 ExtraUsageDisabled,
513 NetworkError,
515 UnknownReason,
517 NotFirstParty,
519 DisabledByEnv,
521 ModelNotAllowed,
523 SdkOptInRequired,
525 Pending,
527 Unknown(String),
529}
530
531impl FastModeDisabledReason {
532 pub fn as_str(&self) -> &str {
533 match self {
534 Self::Free => "free",
535 Self::Preference => "preference",
536 Self::ExtraUsageDisabled => "extra_usage_disabled",
537 Self::NetworkError => "network_error",
538 Self::UnknownReason => "unknown",
539 Self::NotFirstParty => "not_first_party",
540 Self::DisabledByEnv => "disabled_by_env",
541 Self::ModelNotAllowed => "model_not_allowed",
542 Self::SdkOptInRequired => "sdk_opt_in_required",
543 Self::Pending => "pending",
544 Self::Unknown(s) => s.as_str(),
545 }
546 }
547}
548
549impl fmt::Display for FastModeDisabledReason {
550 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
551 f.write_str(self.as_str())
552 }
553}
554
555impl From<&str> for FastModeDisabledReason {
556 fn from(s: &str) -> Self {
557 match s {
558 "free" => Self::Free,
559 "preference" => Self::Preference,
560 "extra_usage_disabled" => Self::ExtraUsageDisabled,
561 "network_error" => Self::NetworkError,
562 "unknown" => Self::UnknownReason,
563 "not_first_party" => Self::NotFirstParty,
564 "disabled_by_env" => Self::DisabledByEnv,
565 "model_not_allowed" => Self::ModelNotAllowed,
566 "sdk_opt_in_required" => Self::SdkOptInRequired,
567 "pending" => Self::Pending,
568 other => Self::Unknown(other.to_string()),
569 }
570 }
571}
572
573impl Serialize for FastModeDisabledReason {
574 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
575 serializer.serialize_str(self.as_str())
576 }
577}
578
579impl<'de> Deserialize<'de> for FastModeDisabledReason {
580 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
581 let s = String::deserialize(deserializer)?;
582 Ok(Self::from(s.as_str()))
583 }
584}
585
586#[derive(Debug, Clone, PartialEq, Eq, Hash)]
588pub enum ResultSubtype {
589 Success,
590 ErrorMaxTurns,
591 ErrorDuringExecution,
592 ErrorMaxBudgetUsd,
593 ErrorMaxStructuredOutputRetries,
594 Unknown(String),
595}
596
597impl ResultSubtype {
598 pub fn as_str(&self) -> &str {
599 match self {
600 Self::Success => "success",
601 Self::ErrorMaxTurns => "error_max_turns",
602 Self::ErrorDuringExecution => "error_during_execution",
603 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
604 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
605 Self::Unknown(s) => s.as_str(),
606 }
607 }
608}
609
610impl fmt::Display for ResultSubtype {
611 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
612 f.write_str(self.as_str())
613 }
614}
615
616impl From<&str> for ResultSubtype {
617 fn from(s: &str) -> Self {
618 match s {
619 "success" => Self::Success,
620 "error_max_turns" => Self::ErrorMaxTurns,
621 "error_during_execution" => Self::ErrorDuringExecution,
622 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
623 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
624 other => Self::Unknown(other.to_string()),
625 }
626 }
627}
628
629impl Serialize for ResultSubtype {
630 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
631 serializer.serialize_str(self.as_str())
632 }
633}
634
635impl<'de> Deserialize<'de> for ResultSubtype {
636 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
637 let s = String::deserialize(deserializer)?;
638 Ok(Self::from(s.as_str()))
639 }
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct UsageInfo {
661 #[serde(default)]
663 pub input_tokens: u32,
664 #[serde(default)]
666 pub cache_creation_input_tokens: u32,
667 #[serde(default)]
671 pub cache_read_input_tokens: u32,
672 #[serde(default)]
674 pub output_tokens: u32,
675 #[serde(default)]
676 pub server_tool_use: ServerToolUse,
677 #[serde(default)]
678 pub service_tier: String,
679
680 #[serde(skip_serializing_if = "Option::is_none")]
682 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
683
684 #[serde(skip_serializing_if = "Option::is_none")]
686 pub inference_geo: Option<String>,
687
688 #[serde(default, skip_serializing_if = "Vec::is_empty")]
692 pub iterations: Vec<UsageIteration>,
693
694 #[serde(skip_serializing_if = "Option::is_none")]
696 pub speed: Option<String>,
697
698 #[serde(skip_serializing_if = "Option::is_none")]
701 pub output_tokens_details: Option<OutputTokensDetails>,
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize, Default)]
707pub struct OutputTokensDetails {
708 #[serde(skip_serializing_if = "Option::is_none")]
710 pub thinking_tokens: Option<u64>,
711}
712
713#[derive(Debug, Clone, Serialize, Deserialize)]
722pub struct UsageIteration {
723 #[serde(default)]
724 pub input_tokens: u32,
725 #[serde(default)]
726 pub output_tokens: u32,
727 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub cache_read_input_tokens: Option<u32>,
730 #[serde(default, skip_serializing_if = "Option::is_none")]
732 pub cache_creation_input_tokens: Option<u32>,
733 #[serde(default, skip_serializing_if = "Option::is_none")]
735 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
736 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
738 pub kind: Option<String>,
739}
740
741#[derive(Debug, Clone, Default, Serialize, Deserialize)]
743pub struct ServerToolUse {
744 #[serde(default)]
745 pub web_search_requests: u32,
746 #[serde(default)]
748 pub web_fetch_requests: u32,
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754 use crate::io::ClaudeOutput;
755
756 #[test]
757 fn test_deserialize_result_message() {
758 let json = r#"{
759 "type": "result",
760 "subtype": "success",
761 "is_error": false,
762 "duration_ms": 100,
763 "duration_api_ms": 200,
764 "num_turns": 1,
765 "result": "Done",
766 "session_id": "123",
767 "total_cost_usd": 0.01,
768 "permission_denials": []
769 }"#;
770
771 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
772 assert!(!output.is_error());
773 }
774
775 #[test]
776 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
777 let json = r#"{
778 "type": "result",
779 "subtype": "error_max_budget_usd",
780 "is_error": true,
781 "duration_ms": 100,
782 "duration_api_ms": 200,
783 "num_turns": 1,
784 "session_id": "123",
785 "total_cost_usd": 0.01
786 }"#;
787
788 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
789 let ClaudeOutput::Result(result) = output else {
790 panic!("Expected Result");
791 };
792 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
793
794 let json = r#"{
795 "type": "result",
796 "subtype": "future_result_subtype",
797 "is_error": true,
798 "duration_ms": 100,
799 "duration_api_ms": 200,
800 "num_turns": 1,
801 "session_id": "123",
802 "total_cost_usd": 0.01
803 }"#;
804
805 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
806 let ClaudeOutput::Result(result) = output else {
807 panic!("Expected Result");
808 };
809 assert_eq!(
810 result.subtype,
811 ResultSubtype::Unknown("future_result_subtype".to_string())
812 );
813 }
814
815 #[test]
816 fn test_deserialize_result_with_permission_denials() {
817 let json = r#"{
818 "type": "result",
819 "subtype": "success",
820 "is_error": false,
821 "duration_ms": 100,
822 "duration_api_ms": 200,
823 "num_turns": 2,
824 "result": "Done",
825 "session_id": "123",
826 "total_cost_usd": 0.01,
827 "permission_denials": [
828 {
829 "tool_name": "Bash",
830 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
831 "tool_use_id": "toolu_123"
832 }
833 ]
834 }"#;
835
836 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
837 if let ClaudeOutput::Result(result) = output {
838 assert_eq!(result.permission_denials.len(), 1);
839 assert_eq!(result.permission_denials[0].tool_name, "Bash");
840 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
841 assert_eq!(
842 result.permission_denials[0]
843 .tool_input
844 .get("command")
845 .unwrap(),
846 "rm -rf /"
847 );
848 } else {
849 panic!("Expected Result");
850 }
851 }
852
853 #[test]
854 fn test_permission_denial_roundtrip() {
855 let denial = PermissionDenial {
856 tool_name: "Write".to_string(),
857 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
858 tool_use_id: "toolu_456".to_string(),
859 };
860
861 let json = serde_json::to_string(&denial).unwrap();
862 assert!(json.contains("\"tool_name\":\"Write\""));
863 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
864 assert!(json.contains("/etc/passwd"));
865
866 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
867 assert_eq!(parsed, denial);
868 }
869
870 #[test]
871 fn test_deserialize_result_message_with_errors() {
872 let json = r#"{
873 "type": "result",
874 "subtype": "error_during_execution",
875 "duration_ms": 0,
876 "duration_api_ms": 0,
877 "is_error": true,
878 "num_turns": 0,
879 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
880 "total_cost_usd": 0,
881 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
882 "permission_denials": []
883 }"#;
884
885 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
886 assert!(output.is_error());
887
888 if let ClaudeOutput::Result(res) = output {
889 assert!(res.is_error);
890 assert_eq!(res.errors.len(), 1);
891 assert!(res.errors[0].contains("No conversation found"));
892 } else {
893 panic!("Expected Result message");
894 }
895 }
896
897 #[test]
898 fn test_deserialize_result_message_errors_defaults_empty() {
899 let json = r#"{
900 "type": "result",
901 "subtype": "success",
902 "is_error": false,
903 "duration_ms": 100,
904 "duration_api_ms": 200,
905 "num_turns": 1,
906 "session_id": "123",
907 "total_cost_usd": 0.01
908 }"#;
909
910 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
911 if let ClaudeOutput::Result(res) = output {
912 assert!(res.errors.is_empty());
913 } else {
914 panic!("Expected Result message");
915 }
916 }
917
918 #[test]
919 fn test_result_message_errors_roundtrip() {
920 let json = r#"{
921 "type": "result",
922 "subtype": "error_during_execution",
923 "is_error": true,
924 "duration_ms": 0,
925 "duration_api_ms": 0,
926 "num_turns": 0,
927 "session_id": "test-session",
928 "total_cost_usd": 0.0,
929 "errors": ["Error 1", "Error 2"]
930 }"#;
931
932 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
933 let reserialized = serde_json::to_string(&output).unwrap();
934
935 assert!(reserialized.contains("Error 1"));
936 assert!(reserialized.contains("Error 2"));
937 }
938
939 #[test]
940 fn test_result_with_new_fields() {
941 let json = r#"{
942 "type": "result",
943 "subtype": "success",
944 "is_error": false,
945 "duration_ms": 5000,
946 "duration_api_ms": 4500,
947 "num_turns": 1,
948 "result": "Done",
949 "session_id": "abc",
950 "total_cost_usd": 0.06,
951 "api_error_status": null,
952 "stop_reason": "end_turn",
953 "terminal_reason": "completed",
954 "fast_mode_state": "off",
955 "modelUsage": {
956 "claude-opus-4-7[1m]": {
957 "inputTokens": 3817,
958 "outputTokens": 14,
959 "costUSD": 0.06
960 }
961 },
962 "usage": {
963 "input_tokens": 3817,
964 "output_tokens": 14,
965 "cache_creation_input_tokens": 3540,
966 "cache_read_input_tokens": 0,
967 "server_tool_use": {
968 "web_search_requests": 0,
969 "web_fetch_requests": 2
970 },
971 "service_tier": "standard",
972 "inference_geo": "not_available",
973 "speed": "standard",
974 "iterations": [
975 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
976 ]
977 }
978 }"#;
979
980 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
981 if let ClaudeOutput::Result(res) = output {
982 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
983 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
984 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
985 let model_usage = res.model_usage.as_ref().unwrap();
986 let entry = model_usage
987 .get("claude-opus-4-7[1m]")
988 .expect("per-model entry present");
989 assert_eq!(entry.input_tokens, 3817);
990 assert_eq!(entry.output_tokens, 14);
991 assert_eq!(entry.cost_usd, 0.06);
992 assert!(res.api_error_status.is_none());
993
994 let usage = res.usage.unwrap();
995 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
996 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
997 assert_eq!(usage.speed.as_deref(), Some("standard"));
998 assert_eq!(usage.iterations.len(), 1);
999 assert_eq!(usage.iterations[0].input_tokens, 3817);
1000 assert_eq!(usage.iterations[0].output_tokens, 14);
1001 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
1002 } else {
1003 panic!("Expected Result");
1004 }
1005 }
1006
1007 #[test]
1008 fn test_result_backwards_compatible_without_new_fields() {
1009 let json = r#"{
1011 "type": "result",
1012 "subtype": "success",
1013 "is_error": false,
1014 "duration_ms": 100,
1015 "duration_api_ms": 200,
1016 "num_turns": 1,
1017 "session_id": "abc",
1018 "total_cost_usd": 0.01
1019 }"#;
1020
1021 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1022 if let ClaudeOutput::Result(res) = output {
1023 assert!(res.api_error_status.is_none());
1024 assert!(res.stop_reason.is_none());
1025 assert!(res.terminal_reason.is_none());
1026 assert!(res.fast_mode_state.is_none());
1027 assert!(res.model_usage.is_none());
1028 } else {
1029 panic!("Expected Result");
1030 }
1031 }
1032
1033 #[test]
1034 fn test_result_fast_mode_disabled_reason() {
1035 let json = r#"{
1036 "type":"result","subtype":"success","is_error":false,
1037 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1038 "session_id":"s1","total_cost_usd":0.01,
1039 "fast_mode_state":"off",
1040 "fast_mode_disabled_reason":"sdk_opt_in_required"
1041 }"#;
1042 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1043 let crate::ClaudeOutput::Result(res) = &output else {
1044 panic!("expected Result");
1045 };
1046 assert_eq!(
1047 res.fast_mode_disabled_reason,
1048 Some(FastModeDisabledReason::SdkOptInRequired)
1049 );
1050 assert!(serde_json::to_string(&output)
1051 .unwrap()
1052 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
1053
1054 assert_eq!(
1057 FastModeDisabledReason::from("unknown"),
1058 FastModeDisabledReason::UnknownReason
1059 );
1060 let novel = FastModeDisabledReason::from("solar_flare");
1061 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
1062 assert_eq!(novel.as_str(), "solar_flare");
1063 }
1064
1065 #[test]
1066 fn test_result_timing_and_user_message_uuid_fields() {
1067 let json = r#"{
1068 "type":"result","subtype":"success","is_error":false,
1069 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1070 "session_id":"s1","total_cost_usd":0.01,
1071 "request_sent_wall_ms":1753212345678.25,
1072 "user_message_uuid":"um-1"
1073 }"#;
1074 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1075 let crate::ClaudeOutput::Result(res) = &output else {
1076 panic!("expected Result");
1077 };
1078 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
1079 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
1080
1081 let reserialized = serde_json::to_string(&output).unwrap();
1082 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
1083 }
1084
1085 #[test]
1086 fn test_result_queued_turn_count_and_user_message_uuids() {
1087 let json = r#"{
1089 "type":"result","subtype":"success","is_error":false,
1090 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1091 "session_id":"s1","total_cost_usd":0.01,
1092 "user_message_uuid":"um-2",
1093 "user_message_uuids":["um-1","um-2"],
1094 "queued_turn_count":3
1095 }"#;
1096 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1097 let crate::ClaudeOutput::Result(res) = &output else {
1098 panic!("expected Result");
1099 };
1100 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
1101 assert_eq!(res.queued_turn_count, Some(3));
1102
1103 let reserialized = serde_json::to_string(&output).unwrap();
1104 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
1105 assert!(reserialized.contains("\"queued_turn_count\":3"));
1106
1107 let old = r#"{
1109 "type":"result","subtype":"success","is_error":false,
1110 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1111 "session_id":"s1","total_cost_usd":0.01
1112 }"#;
1113 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
1114 let crate::ClaudeOutput::Result(res) = &output else {
1115 panic!("expected Result");
1116 };
1117 assert!(res.user_message_uuids.is_empty());
1118 assert_eq!(res.queued_turn_count, None);
1119 let reserialized = serde_json::to_string(&output).unwrap();
1120 assert!(!reserialized.contains("user_message_uuids"));
1121 assert!(!reserialized.contains("queued_turn_count"));
1122 }
1123}