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(default, skip_serializing_if = "Option::is_none")]
38 pub time_to_request_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
39
40 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub process_turn_index: Option<u64>,
50
51 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub time_to_request_cpu_ms: Option<u64>,
60
61 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub time_to_request_loop_lag_ms: Option<u64>,
69
70 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub time_to_request_major_faults: Option<u64>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub time_to_request_from_spawn_ms: Option<u64>,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub warm_spare_claimed: Option<bool>,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub time_origin_ms: Option<u64>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
93 pub request_sent_wall_ms: Option<f64>,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub first_content_frame_ms: Option<u64>,
99
100 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub first_stream_post_ms: Option<u64>,
104
105 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub first_stream_post_ack_ms: Option<u64>,
109
110 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub first_stream_post_queue_wait_ms: Option<u64>,
114
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub first_stream_post_queued_behind: Option<StreamPostQueuedBehind>,
118
119 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub first_stream_post_wall_ms: Option<f64>,
123
124 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub frame_received_wall_ms: Option<f64>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub frame_enqueued_wall_ms: Option<f64>,
142
143 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub frame_intake_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
161
162 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub turn_started_wall_ms: Option<f64>,
170
171 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub turn_start_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
188
189 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub turn_start_control_requests_ms: Option<u64>,
198
199 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub first_text_post_ms: Option<u64>,
203
204 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub first_text_post_wall_ms: Option<f64>,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub user_message_uuid: Option<String>,
212
213 #[serde(default, skip_serializing_if = "Vec::is_empty")]
220 pub user_message_uuids: Vec<String>,
221
222 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub resume_reason: Option<String>,
231
232 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub local_command: Option<String>,
237
238 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub queued_turn_count: Option<u64>,
245
246 pub num_turns: i32,
247
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub result: Option<String>,
250
251 #[serde(alias = "sessionId")]
252 pub session_id: String,
253 pub total_cost_usd: f64,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub usage: Option<UsageInfo>,
257
258 #[serde(default)]
260 pub permission_denials: Vec<PermissionDenial>,
261
262 #[serde(default)]
267 pub errors: Vec<String>,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub uuid: Option<String>,
271
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub api_error_status: Option<u16>,
275
276 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub api_error_code: Option<String>,
283
284 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub api_error: Option<String>,
290
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub stop_reason: Option<String>,
294
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub terminal_reason: Option<String>,
298
299 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub result_index: Option<u64>,
308
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub fast_mode_state: Option<String>,
312
313 #[serde(skip_serializing_if = "Option::is_none")]
317 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
318
319 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
321 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
322
323 #[serde(skip_serializing_if = "Option::is_none")]
327 pub subagent_stats: Option<SubagentStats>,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub structured_output: Option<Value>,
332
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub deferred_tool_use: Option<DeferredToolUse>,
336
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub origin: Option<super::message_types::MessageOrigin>,
340
341 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub runner_exit: Option<RunnerExit>,
346
347 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub startup_failure_reason: Option<StartupFailureReason>,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Hash)]
362pub enum StartupFailureReason {
363 OrgPinApiKeyConflict,
366 ProviderNotAllowed,
370 OrgVerifyFailed,
373 OrgPinMismatch,
375 ManagedSettingsInvalid,
380 RemoteSettingsRequiredUnavailable,
382 GatewaySigninRequired,
384 GatewayAccessDenied,
386 ProxyInvalid,
388 TempDirUnusable,
390 CwdUnavailable,
392 ShellToolMissing,
395 SessionHeldByBackground,
398 WorktreeResumeRefused,
402 WorktreeUnverified,
405 CliVersionTooOld,
407 BypassRoot,
409 Unknown(String),
411}
412
413impl StartupFailureReason {
414 pub fn as_str(&self) -> &str {
415 match self {
416 Self::OrgPinApiKeyConflict => "org_pin_api_key_conflict",
417 Self::ProviderNotAllowed => "provider_not_allowed",
418 Self::OrgVerifyFailed => "org_verify_failed",
419 Self::OrgPinMismatch => "org_pin_mismatch",
420 Self::ManagedSettingsInvalid => "managed_settings_invalid",
421 Self::RemoteSettingsRequiredUnavailable => "remote_settings_required_unavailable",
422 Self::GatewaySigninRequired => "gateway_signin_required",
423 Self::GatewayAccessDenied => "gateway_access_denied",
424 Self::ProxyInvalid => "proxy_invalid",
425 Self::TempDirUnusable => "temp_dir_unusable",
426 Self::CwdUnavailable => "cwd_unavailable",
427 Self::ShellToolMissing => "shell_tool_missing",
428 Self::SessionHeldByBackground => "session_held_by_background",
429 Self::WorktreeResumeRefused => "worktree_resume_refused",
430 Self::WorktreeUnverified => "worktree_unverified",
431 Self::CliVersionTooOld => "cli_version_too_old",
432 Self::BypassRoot => "bypass_root",
433 Self::Unknown(s) => s.as_str(),
434 }
435 }
436}
437
438impl fmt::Display for StartupFailureReason {
439 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440 f.write_str(self.as_str())
441 }
442}
443
444impl From<&str> for StartupFailureReason {
445 fn from(s: &str) -> Self {
446 match s {
447 "org_pin_api_key_conflict" => Self::OrgPinApiKeyConflict,
448 "provider_not_allowed" => Self::ProviderNotAllowed,
449 "org_verify_failed" => Self::OrgVerifyFailed,
450 "org_pin_mismatch" => Self::OrgPinMismatch,
451 "managed_settings_invalid" => Self::ManagedSettingsInvalid,
452 "remote_settings_required_unavailable" => Self::RemoteSettingsRequiredUnavailable,
453 "gateway_signin_required" => Self::GatewaySigninRequired,
454 "gateway_access_denied" => Self::GatewayAccessDenied,
455 "proxy_invalid" => Self::ProxyInvalid,
456 "temp_dir_unusable" => Self::TempDirUnusable,
457 "cwd_unavailable" => Self::CwdUnavailable,
458 "shell_tool_missing" => Self::ShellToolMissing,
459 "session_held_by_background" => Self::SessionHeldByBackground,
460 "worktree_resume_refused" => Self::WorktreeResumeRefused,
461 "worktree_unverified" => Self::WorktreeUnverified,
462 "cli_version_too_old" => Self::CliVersionTooOld,
463 "bypass_root" => Self::BypassRoot,
464 other => Self::Unknown(other.to_string()),
465 }
466 }
467}
468
469impl Serialize for StartupFailureReason {
470 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
471 serializer.serialize_str(self.as_str())
472 }
473}
474
475impl<'de> Deserialize<'de> for StartupFailureReason {
476 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
477 let s = String::deserialize(deserializer)?;
478 Ok(Self::from(s.as_str()))
479 }
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
486pub struct RunnerExit {
487 pub phase: RunnerExitPhase,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub exit_code: Option<i64>,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub signal: Option<String>,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq, Hash)]
503pub enum RunnerExitPhase {
504 Setup,
506 Run,
508 Unknown(String),
510}
511
512impl RunnerExitPhase {
513 pub fn as_str(&self) -> &str {
514 match self {
515 Self::Setup => "setup",
516 Self::Run => "run",
517 Self::Unknown(s) => s.as_str(),
518 }
519 }
520}
521
522impl std::fmt::Display for RunnerExitPhase {
523 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
524 f.write_str(self.as_str())
525 }
526}
527
528impl From<&str> for RunnerExitPhase {
529 fn from(s: &str) -> Self {
530 match s {
531 "setup" => Self::Setup,
532 "run" => Self::Run,
533 other => Self::Unknown(other.to_string()),
534 }
535 }
536}
537
538impl Serialize for RunnerExitPhase {
539 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
540 serializer.serialize_str(self.as_str())
541 }
542}
543
544impl<'de> Deserialize<'de> for RunnerExitPhase {
545 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
546 let s = String::deserialize(deserializer)?;
547 Ok(Self::from(s.as_str()))
548 }
549}
550
551#[derive(Debug, Clone, Default, Serialize, Deserialize)]
557#[serde(rename_all = "camelCase")]
558pub struct ModelUsageEntry {
559 #[serde(default)]
560 pub input_tokens: u64,
561 #[serde(default)]
562 pub output_tokens: u64,
563 #[serde(default)]
564 pub cache_read_input_tokens: u64,
565 #[serde(default)]
566 pub cache_creation_input_tokens: u64,
567 #[serde(default, rename = "costUSD")]
568 pub cost_usd: f64,
569 #[serde(default)]
570 pub web_search_requests: u32,
571 #[serde(default)]
572 pub context_window: u64,
573 #[serde(default)]
574 pub max_output_tokens: u64,
575 #[serde(flatten)]
576 pub extra: serde_json::Map<String, serde_json::Value>,
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
581pub struct DeferredToolUse {
582 pub id: String,
583 pub name: String,
584 pub input: Value,
585}
586
587#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
596pub struct SubagentStats {
597 pub spawned: u64,
599 pub requested: SubagentSpawnRequests,
602 pub started_in_background: u64,
605 #[serde(default)]
607 pub by_type: std::collections::BTreeMap<String, u64>,
608 pub max_depth: u64,
611 pub spawned_by_subagents: u64,
613 pub completed: u64,
614 pub failed: u64,
615 pub killed: SubagentKillCounts,
617 pub refused: SubagentRefusalCounts,
620}
621
622#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
625pub struct SubagentSpawnRequests {
626 pub background: u64,
627 pub foreground: u64,
628 pub unset: u64,
629}
630
631#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
635pub struct SubagentKillCounts {
636 pub parent: u64,
637 pub user: u64,
638 pub system: u64,
639}
640
641#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
643pub struct SubagentRefusalCounts {
644 pub depth_limit: u64,
645 pub concurrency_limit: u64,
646 pub budget: u64,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
654pub struct PermissionDenial {
655 pub tool_name: String,
657
658 pub tool_input: Value,
660
661 pub tool_use_id: String,
663}
664
665#[derive(Debug, Clone, PartialEq, Eq, Hash)]
669pub enum StreamPostQueuedBehind {
670 DurablePost,
672 EphemeralPost,
674 RetryBackoff,
676 Hold,
678 None,
680 Unknown(String),
682}
683
684impl StreamPostQueuedBehind {
685 pub fn as_str(&self) -> &str {
686 match self {
687 Self::DurablePost => "durable_post",
688 Self::EphemeralPost => "ephemeral_post",
689 Self::RetryBackoff => "retry_backoff",
690 Self::Hold => "hold",
691 Self::None => "none",
692 Self::Unknown(s) => s.as_str(),
693 }
694 }
695}
696
697impl fmt::Display for StreamPostQueuedBehind {
698 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699 f.write_str(self.as_str())
700 }
701}
702
703impl From<&str> for StreamPostQueuedBehind {
704 fn from(s: &str) -> Self {
705 match s {
706 "durable_post" => Self::DurablePost,
707 "ephemeral_post" => Self::EphemeralPost,
708 "retry_backoff" => Self::RetryBackoff,
709 "hold" => Self::Hold,
710 "none" => Self::None,
711 other => Self::Unknown(other.to_string()),
712 }
713 }
714}
715
716impl Serialize for StreamPostQueuedBehind {
717 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
718 serializer.serialize_str(self.as_str())
719 }
720}
721
722impl<'de> Deserialize<'de> for StreamPostQueuedBehind {
723 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
724 let s = String::deserialize(deserializer)?;
725 Ok(Self::from(s.as_str()))
726 }
727}
728
729#[derive(Debug, Clone, PartialEq, Eq, Hash)]
732pub enum FastModeDisabledReason {
733 Free,
735 Preference,
737 ExtraUsageDisabled,
739 NetworkError,
741 UnknownReason,
743 NotFirstParty,
745 DisabledByEnv,
747 ModelNotAllowed,
749 SdkOptInRequired,
751 Pending,
753 Unknown(String),
755}
756
757impl FastModeDisabledReason {
758 pub fn as_str(&self) -> &str {
759 match self {
760 Self::Free => "free",
761 Self::Preference => "preference",
762 Self::ExtraUsageDisabled => "extra_usage_disabled",
763 Self::NetworkError => "network_error",
764 Self::UnknownReason => "unknown",
765 Self::NotFirstParty => "not_first_party",
766 Self::DisabledByEnv => "disabled_by_env",
767 Self::ModelNotAllowed => "model_not_allowed",
768 Self::SdkOptInRequired => "sdk_opt_in_required",
769 Self::Pending => "pending",
770 Self::Unknown(s) => s.as_str(),
771 }
772 }
773}
774
775impl fmt::Display for FastModeDisabledReason {
776 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
777 f.write_str(self.as_str())
778 }
779}
780
781impl From<&str> for FastModeDisabledReason {
782 fn from(s: &str) -> Self {
783 match s {
784 "free" => Self::Free,
785 "preference" => Self::Preference,
786 "extra_usage_disabled" => Self::ExtraUsageDisabled,
787 "network_error" => Self::NetworkError,
788 "unknown" => Self::UnknownReason,
789 "not_first_party" => Self::NotFirstParty,
790 "disabled_by_env" => Self::DisabledByEnv,
791 "model_not_allowed" => Self::ModelNotAllowed,
792 "sdk_opt_in_required" => Self::SdkOptInRequired,
793 "pending" => Self::Pending,
794 other => Self::Unknown(other.to_string()),
795 }
796 }
797}
798
799impl Serialize for FastModeDisabledReason {
800 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
801 serializer.serialize_str(self.as_str())
802 }
803}
804
805impl<'de> Deserialize<'de> for FastModeDisabledReason {
806 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
807 let s = String::deserialize(deserializer)?;
808 Ok(Self::from(s.as_str()))
809 }
810}
811
812#[derive(Debug, Clone, PartialEq, Eq, Hash)]
814pub enum ResultSubtype {
815 Success,
816 ErrorMaxTurns,
817 ErrorDuringExecution,
818 ErrorMaxBudgetUsd,
819 ErrorMaxStructuredOutputRetries,
820 Unknown(String),
821}
822
823impl ResultSubtype {
824 pub fn as_str(&self) -> &str {
825 match self {
826 Self::Success => "success",
827 Self::ErrorMaxTurns => "error_max_turns",
828 Self::ErrorDuringExecution => "error_during_execution",
829 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
830 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
831 Self::Unknown(s) => s.as_str(),
832 }
833 }
834}
835
836impl fmt::Display for ResultSubtype {
837 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
838 f.write_str(self.as_str())
839 }
840}
841
842impl From<&str> for ResultSubtype {
843 fn from(s: &str) -> Self {
844 match s {
845 "success" => Self::Success,
846 "error_max_turns" => Self::ErrorMaxTurns,
847 "error_during_execution" => Self::ErrorDuringExecution,
848 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
849 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
850 other => Self::Unknown(other.to_string()),
851 }
852 }
853}
854
855impl Serialize for ResultSubtype {
856 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
857 serializer.serialize_str(self.as_str())
858 }
859}
860
861impl<'de> Deserialize<'de> for ResultSubtype {
862 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
863 let s = String::deserialize(deserializer)?;
864 Ok(Self::from(s.as_str()))
865 }
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize)]
886pub struct UsageInfo {
887 #[serde(default)]
889 pub input_tokens: u32,
890 #[serde(default)]
892 pub cache_creation_input_tokens: u32,
893 #[serde(default)]
897 pub cache_read_input_tokens: u32,
898 #[serde(default)]
900 pub output_tokens: u32,
901 #[serde(default)]
902 pub server_tool_use: ServerToolUse,
903 #[serde(default)]
904 pub service_tier: String,
905
906 #[serde(skip_serializing_if = "Option::is_none")]
908 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
909
910 #[serde(skip_serializing_if = "Option::is_none")]
912 pub inference_geo: Option<String>,
913
914 #[serde(default, skip_serializing_if = "Vec::is_empty")]
918 pub iterations: Vec<UsageIteration>,
919
920 #[serde(skip_serializing_if = "Option::is_none")]
922 pub speed: Option<String>,
923
924 #[serde(skip_serializing_if = "Option::is_none")]
927 pub output_tokens_details: Option<OutputTokensDetails>,
928
929 #[serde(default, skip_serializing_if = "Option::is_none")]
932 pub fallback_credit: Option<Value>,
933}
934
935#[derive(Debug, Clone, Serialize, Deserialize, Default)]
938pub struct OutputTokensDetails {
939 #[serde(skip_serializing_if = "Option::is_none")]
941 pub thinking_tokens: Option<u64>,
942}
943
944#[derive(Debug, Clone, Serialize, Deserialize)]
953pub struct UsageIteration {
954 #[serde(default)]
955 pub input_tokens: u32,
956 #[serde(default)]
957 pub output_tokens: u32,
958 #[serde(default, skip_serializing_if = "Option::is_none")]
960 pub cache_read_input_tokens: Option<u32>,
961 #[serde(default, skip_serializing_if = "Option::is_none")]
963 pub cache_creation_input_tokens: Option<u32>,
964 #[serde(default, skip_serializing_if = "Option::is_none")]
966 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
967 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
969 pub kind: Option<String>,
970}
971
972#[derive(Debug, Clone, Default, Serialize, Deserialize)]
974pub struct ServerToolUse {
975 #[serde(default)]
976 pub web_search_requests: u32,
977 #[serde(default)]
979 pub web_fetch_requests: u32,
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985 use crate::io::ClaudeOutput;
986
987 #[test]
988 fn test_deserialize_result_message() {
989 let json = r#"{
990 "type": "result",
991 "subtype": "success",
992 "is_error": false,
993 "duration_ms": 100,
994 "duration_api_ms": 200,
995 "num_turns": 1,
996 "result": "Done",
997 "session_id": "123",
998 "total_cost_usd": 0.01,
999 "permission_denials": []
1000 }"#;
1001
1002 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1003 assert!(!output.is_error());
1004 }
1005
1006 #[test]
1007 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
1008 let json = r#"{
1009 "type": "result",
1010 "subtype": "error_max_budget_usd",
1011 "is_error": true,
1012 "duration_ms": 100,
1013 "duration_api_ms": 200,
1014 "num_turns": 1,
1015 "session_id": "123",
1016 "total_cost_usd": 0.01
1017 }"#;
1018
1019 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1020 let ClaudeOutput::Result(result) = output else {
1021 panic!("Expected Result");
1022 };
1023 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
1024
1025 let json = r#"{
1026 "type": "result",
1027 "subtype": "future_result_subtype",
1028 "is_error": true,
1029 "duration_ms": 100,
1030 "duration_api_ms": 200,
1031 "num_turns": 1,
1032 "session_id": "123",
1033 "total_cost_usd": 0.01
1034 }"#;
1035
1036 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1037 let ClaudeOutput::Result(result) = output else {
1038 panic!("Expected Result");
1039 };
1040 assert_eq!(
1041 result.subtype,
1042 ResultSubtype::Unknown("future_result_subtype".to_string())
1043 );
1044 }
1045
1046 #[test]
1047 fn test_deserialize_result_with_permission_denials() {
1048 let json = r#"{
1049 "type": "result",
1050 "subtype": "success",
1051 "is_error": false,
1052 "duration_ms": 100,
1053 "duration_api_ms": 200,
1054 "num_turns": 2,
1055 "result": "Done",
1056 "session_id": "123",
1057 "total_cost_usd": 0.01,
1058 "permission_denials": [
1059 {
1060 "tool_name": "Bash",
1061 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1062 "tool_use_id": "toolu_123"
1063 }
1064 ]
1065 }"#;
1066
1067 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1068 if let ClaudeOutput::Result(result) = output {
1069 assert_eq!(result.permission_denials.len(), 1);
1070 assert_eq!(result.permission_denials[0].tool_name, "Bash");
1071 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1072 assert_eq!(
1073 result.permission_denials[0]
1074 .tool_input
1075 .get("command")
1076 .unwrap(),
1077 "rm -rf /"
1078 );
1079 } else {
1080 panic!("Expected Result");
1081 }
1082 }
1083
1084 #[test]
1085 fn test_permission_denial_roundtrip() {
1086 let denial = PermissionDenial {
1087 tool_name: "Write".to_string(),
1088 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1089 tool_use_id: "toolu_456".to_string(),
1090 };
1091
1092 let json = serde_json::to_string(&denial).unwrap();
1093 assert!(json.contains("\"tool_name\":\"Write\""));
1094 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1095 assert!(json.contains("/etc/passwd"));
1096
1097 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1098 assert_eq!(parsed, denial);
1099 }
1100
1101 #[test]
1102 fn test_deserialize_result_message_with_errors() {
1103 let json = r#"{
1104 "type": "result",
1105 "subtype": "error_during_execution",
1106 "duration_ms": 0,
1107 "duration_api_ms": 0,
1108 "is_error": true,
1109 "num_turns": 0,
1110 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
1111 "total_cost_usd": 0,
1112 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
1113 "permission_denials": []
1114 }"#;
1115
1116 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1117 assert!(output.is_error());
1118
1119 if let ClaudeOutput::Result(res) = output {
1120 assert!(res.is_error);
1121 assert_eq!(res.errors.len(), 1);
1122 assert!(res.errors[0].contains("No conversation found"));
1123 } else {
1124 panic!("Expected Result message");
1125 }
1126 }
1127
1128 #[test]
1129 fn test_deserialize_result_message_errors_defaults_empty() {
1130 let json = r#"{
1131 "type": "result",
1132 "subtype": "success",
1133 "is_error": false,
1134 "duration_ms": 100,
1135 "duration_api_ms": 200,
1136 "num_turns": 1,
1137 "session_id": "123",
1138 "total_cost_usd": 0.01
1139 }"#;
1140
1141 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1142 if let ClaudeOutput::Result(res) = output {
1143 assert!(res.errors.is_empty());
1144 } else {
1145 panic!("Expected Result message");
1146 }
1147 }
1148
1149 #[test]
1150 fn test_result_message_errors_roundtrip() {
1151 let json = r#"{
1152 "type": "result",
1153 "subtype": "error_during_execution",
1154 "is_error": true,
1155 "duration_ms": 0,
1156 "duration_api_ms": 0,
1157 "num_turns": 0,
1158 "session_id": "test-session",
1159 "total_cost_usd": 0.0,
1160 "errors": ["Error 1", "Error 2"]
1161 }"#;
1162
1163 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1164 let reserialized = serde_json::to_string(&output).unwrap();
1165
1166 assert!(reserialized.contains("Error 1"));
1167 assert!(reserialized.contains("Error 2"));
1168 }
1169
1170 #[test]
1171 fn test_result_with_new_fields() {
1172 let json = r#"{
1173 "type": "result",
1174 "subtype": "success",
1175 "is_error": false,
1176 "duration_ms": 5000,
1177 "duration_api_ms": 4500,
1178 "num_turns": 1,
1179 "result": "Done",
1180 "session_id": "abc",
1181 "total_cost_usd": 0.06,
1182 "api_error_status": null,
1183 "stop_reason": "end_turn",
1184 "terminal_reason": "completed",
1185 "fast_mode_state": "off",
1186 "modelUsage": {
1187 "claude-opus-4-7[1m]": {
1188 "inputTokens": 3817,
1189 "outputTokens": 14,
1190 "costUSD": 0.06
1191 }
1192 },
1193 "usage": {
1194 "input_tokens": 3817,
1195 "output_tokens": 14,
1196 "cache_creation_input_tokens": 3540,
1197 "cache_read_input_tokens": 0,
1198 "server_tool_use": {
1199 "web_search_requests": 0,
1200 "web_fetch_requests": 2
1201 },
1202 "service_tier": "standard",
1203 "inference_geo": "not_available",
1204 "speed": "standard",
1205 "iterations": [
1206 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
1207 ]
1208 }
1209 }"#;
1210
1211 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1212 if let ClaudeOutput::Result(res) = output {
1213 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
1214 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
1215 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
1216 let model_usage = res.model_usage.as_ref().unwrap();
1217 let entry = model_usage
1218 .get("claude-opus-4-7[1m]")
1219 .expect("per-model entry present");
1220 assert_eq!(entry.input_tokens, 3817);
1221 assert_eq!(entry.output_tokens, 14);
1222 assert_eq!(entry.cost_usd, 0.06);
1223 assert!(res.api_error_status.is_none());
1224
1225 let usage = res.usage.unwrap();
1226 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
1227 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1228 assert_eq!(usage.speed.as_deref(), Some("standard"));
1229 assert_eq!(usage.iterations.len(), 1);
1230 assert_eq!(usage.iterations[0].input_tokens, 3817);
1231 assert_eq!(usage.iterations[0].output_tokens, 14);
1232 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
1233 } else {
1234 panic!("Expected Result");
1235 }
1236 }
1237
1238 #[test]
1239 fn test_result_backwards_compatible_without_new_fields() {
1240 let json = r#"{
1242 "type": "result",
1243 "subtype": "success",
1244 "is_error": false,
1245 "duration_ms": 100,
1246 "duration_api_ms": 200,
1247 "num_turns": 1,
1248 "session_id": "abc",
1249 "total_cost_usd": 0.01
1250 }"#;
1251
1252 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1253 if let ClaudeOutput::Result(res) = output {
1254 assert!(res.api_error_status.is_none());
1255 assert!(res.stop_reason.is_none());
1256 assert!(res.terminal_reason.is_none());
1257 assert!(res.fast_mode_state.is_none());
1258 assert!(res.model_usage.is_none());
1259 } else {
1260 panic!("Expected Result");
1261 }
1262 }
1263
1264 #[test]
1265 fn test_result_fast_mode_disabled_reason() {
1266 let json = r#"{
1267 "type":"result","subtype":"success","is_error":false,
1268 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1269 "session_id":"s1","total_cost_usd":0.01,
1270 "fast_mode_state":"off",
1271 "fast_mode_disabled_reason":"sdk_opt_in_required"
1272 }"#;
1273 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1274 let crate::ClaudeOutput::Result(res) = &output else {
1275 panic!("expected Result");
1276 };
1277 assert_eq!(
1278 res.fast_mode_disabled_reason,
1279 Some(FastModeDisabledReason::SdkOptInRequired)
1280 );
1281 assert!(serde_json::to_string(&output)
1282 .unwrap()
1283 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
1284
1285 assert_eq!(
1288 FastModeDisabledReason::from("unknown"),
1289 FastModeDisabledReason::UnknownReason
1290 );
1291 let novel = FastModeDisabledReason::from("solar_flare");
1292 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
1293 assert_eq!(novel.as_str(), "solar_flare");
1294 }
1295
1296 #[test]
1297 fn test_result_timing_and_user_message_uuid_fields() {
1298 let json = r#"{
1299 "type":"result","subtype":"success","is_error":false,
1300 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1301 "session_id":"s1","total_cost_usd":0.01,
1302 "request_sent_wall_ms":1753212345678.25,
1303 "user_message_uuid":"um-1"
1304 }"#;
1305 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1306 let crate::ClaudeOutput::Result(res) = &output else {
1307 panic!("expected Result");
1308 };
1309 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
1310 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
1311
1312 let reserialized = serde_json::to_string(&output).unwrap();
1313 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
1314 }
1315
1316 #[test]
1317 fn test_result_queued_turn_count_and_user_message_uuids() {
1318 let json = r#"{
1320 "type":"result","subtype":"success","is_error":false,
1321 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1322 "session_id":"s1","total_cost_usd":0.01,
1323 "user_message_uuid":"um-2",
1324 "user_message_uuids":["um-1","um-2"],
1325 "queued_turn_count":3
1326 }"#;
1327 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1328 let crate::ClaudeOutput::Result(res) = &output else {
1329 panic!("expected Result");
1330 };
1331 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
1332 assert_eq!(res.queued_turn_count, Some(3));
1333
1334 let reserialized = serde_json::to_string(&output).unwrap();
1335 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
1336 assert!(reserialized.contains("\"queued_turn_count\":3"));
1337
1338 let old = r#"{
1340 "type":"result","subtype":"success","is_error":false,
1341 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1342 "session_id":"s1","total_cost_usd":0.01
1343 }"#;
1344 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
1345 let crate::ClaudeOutput::Result(res) = &output else {
1346 panic!("expected Result");
1347 };
1348 assert!(res.user_message_uuids.is_empty());
1349 assert_eq!(res.queued_turn_count, None);
1350 let reserialized = serde_json::to_string(&output).unwrap();
1351 assert!(!reserialized.contains("user_message_uuids"));
1352 assert!(!reserialized.contains("queued_turn_count"));
1353 }
1354}