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")]
102 pub first_request_input_tokens: Option<u64>,
103
104 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub first_content_frame_ms: Option<u64>,
108
109 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub first_stream_post_ms: Option<u64>,
113
114 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub first_stream_post_ack_ms: Option<u64>,
118
119 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub first_stream_post_queue_wait_ms: Option<u64>,
123
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub first_stream_post_queued_behind: Option<StreamPostQueuedBehind>,
127
128 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub first_stream_post_wall_ms: Option<f64>,
132
133 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub frame_received_wall_ms: Option<f64>,
145
146 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub frame_enqueued_wall_ms: Option<f64>,
151
152 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub frame_intake_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
170
171 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub turn_started_wall_ms: Option<f64>,
179
180 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub turn_start_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
197
198 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub turn_start_control_requests_ms: Option<u64>,
207
208 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub first_text_post_ms: Option<u64>,
212
213 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub first_text_post_wall_ms: Option<f64>,
217
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub user_message_uuid: Option<String>,
221
222 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub user_message_server_received_wall_ms: Option<f64>,
230
231 #[serde(default, skip_serializing_if = "Vec::is_empty")]
238 pub user_message_uuids: Vec<String>,
239
240 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub resume_reason: Option<String>,
249
250 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub local_command: Option<String>,
255
256 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub queued_turn_count: Option<u64>,
263
264 pub num_turns: i32,
265
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub result: Option<String>,
268
269 #[serde(alias = "sessionId")]
270 pub session_id: String,
271 pub total_cost_usd: f64,
272
273 #[serde(skip_serializing_if = "Option::is_none")]
274 pub usage: Option<UsageInfo>,
275
276 #[serde(default)]
278 pub permission_denials: Vec<PermissionDenial>,
279
280 #[serde(default)]
285 pub errors: Vec<String>,
286
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub uuid: Option<String>,
289
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub api_error_status: Option<u16>,
293
294 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub api_error_code: Option<String>,
301
302 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub api_error: Option<String>,
308
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub stop_reason: Option<String>,
312
313 #[serde(skip_serializing_if = "Option::is_none")]
315 pub terminal_reason: Option<String>,
316
317 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub result_index: Option<u64>,
326
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub fast_mode_state: Option<String>,
330
331 #[serde(skip_serializing_if = "Option::is_none")]
335 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
336
337 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
339 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
340
341 #[serde(skip_serializing_if = "Option::is_none")]
345 pub subagent_stats: Option<SubagentStats>,
346
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub structured_output: Option<Value>,
350
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub deferred_tool_use: Option<DeferredToolUse>,
354
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub origin: Option<super::message_types::MessageOrigin>,
358
359 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub runner_exit: Option<RunnerExit>,
364
365 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub startup_failure_reason: Option<StartupFailureReason>,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Hash)]
380pub enum StartupFailureReason {
381 OrgPinApiKeyConflict,
384 ProviderNotAllowed,
388 OrgVerifyFailed,
391 OrgPinMismatch,
393 ManagedSettingsInvalid,
398 RemoteSettingsRequiredUnavailable,
400 GatewaySigninRequired,
402 GatewayAccessDenied,
404 ProxyInvalid,
406 TempDirUnusable,
408 CwdUnavailable,
410 ShellToolMissing,
413 SessionHeldByBackground,
416 WorktreeResumeRefused,
420 WorktreeUnverified,
423 CliVersionTooOld,
425 BypassRoot,
427 Unknown(String),
429}
430
431impl StartupFailureReason {
432 pub fn as_str(&self) -> &str {
433 match self {
434 Self::OrgPinApiKeyConflict => "org_pin_api_key_conflict",
435 Self::ProviderNotAllowed => "provider_not_allowed",
436 Self::OrgVerifyFailed => "org_verify_failed",
437 Self::OrgPinMismatch => "org_pin_mismatch",
438 Self::ManagedSettingsInvalid => "managed_settings_invalid",
439 Self::RemoteSettingsRequiredUnavailable => "remote_settings_required_unavailable",
440 Self::GatewaySigninRequired => "gateway_signin_required",
441 Self::GatewayAccessDenied => "gateway_access_denied",
442 Self::ProxyInvalid => "proxy_invalid",
443 Self::TempDirUnusable => "temp_dir_unusable",
444 Self::CwdUnavailable => "cwd_unavailable",
445 Self::ShellToolMissing => "shell_tool_missing",
446 Self::SessionHeldByBackground => "session_held_by_background",
447 Self::WorktreeResumeRefused => "worktree_resume_refused",
448 Self::WorktreeUnverified => "worktree_unverified",
449 Self::CliVersionTooOld => "cli_version_too_old",
450 Self::BypassRoot => "bypass_root",
451 Self::Unknown(s) => s.as_str(),
452 }
453 }
454}
455
456impl fmt::Display for StartupFailureReason {
457 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
458 f.write_str(self.as_str())
459 }
460}
461
462impl From<&str> for StartupFailureReason {
463 fn from(s: &str) -> Self {
464 match s {
465 "org_pin_api_key_conflict" => Self::OrgPinApiKeyConflict,
466 "provider_not_allowed" => Self::ProviderNotAllowed,
467 "org_verify_failed" => Self::OrgVerifyFailed,
468 "org_pin_mismatch" => Self::OrgPinMismatch,
469 "managed_settings_invalid" => Self::ManagedSettingsInvalid,
470 "remote_settings_required_unavailable" => Self::RemoteSettingsRequiredUnavailable,
471 "gateway_signin_required" => Self::GatewaySigninRequired,
472 "gateway_access_denied" => Self::GatewayAccessDenied,
473 "proxy_invalid" => Self::ProxyInvalid,
474 "temp_dir_unusable" => Self::TempDirUnusable,
475 "cwd_unavailable" => Self::CwdUnavailable,
476 "shell_tool_missing" => Self::ShellToolMissing,
477 "session_held_by_background" => Self::SessionHeldByBackground,
478 "worktree_resume_refused" => Self::WorktreeResumeRefused,
479 "worktree_unverified" => Self::WorktreeUnverified,
480 "cli_version_too_old" => Self::CliVersionTooOld,
481 "bypass_root" => Self::BypassRoot,
482 other => Self::Unknown(other.to_string()),
483 }
484 }
485}
486
487impl Serialize for StartupFailureReason {
488 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
489 serializer.serialize_str(self.as_str())
490 }
491}
492
493impl<'de> Deserialize<'de> for StartupFailureReason {
494 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
495 let s = String::deserialize(deserializer)?;
496 Ok(Self::from(s.as_str()))
497 }
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
504pub struct RunnerExit {
505 pub phase: RunnerExitPhase,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub exit_code: Option<i64>,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub signal: Option<String>,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, Hash)]
521pub enum RunnerExitPhase {
522 Setup,
524 Run,
526 Unknown(String),
528}
529
530impl RunnerExitPhase {
531 pub fn as_str(&self) -> &str {
532 match self {
533 Self::Setup => "setup",
534 Self::Run => "run",
535 Self::Unknown(s) => s.as_str(),
536 }
537 }
538}
539
540impl std::fmt::Display for RunnerExitPhase {
541 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542 f.write_str(self.as_str())
543 }
544}
545
546impl From<&str> for RunnerExitPhase {
547 fn from(s: &str) -> Self {
548 match s {
549 "setup" => Self::Setup,
550 "run" => Self::Run,
551 other => Self::Unknown(other.to_string()),
552 }
553 }
554}
555
556impl Serialize for RunnerExitPhase {
557 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
558 serializer.serialize_str(self.as_str())
559 }
560}
561
562impl<'de> Deserialize<'de> for RunnerExitPhase {
563 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
564 let s = String::deserialize(deserializer)?;
565 Ok(Self::from(s.as_str()))
566 }
567}
568
569#[derive(Debug, Clone, Default, Serialize, Deserialize)]
575#[serde(rename_all = "camelCase")]
576pub struct ModelUsageEntry {
577 #[serde(default)]
578 pub input_tokens: u64,
579 #[serde(default)]
580 pub output_tokens: u64,
581 #[serde(default)]
582 pub cache_read_input_tokens: u64,
583 #[serde(default)]
584 pub cache_creation_input_tokens: u64,
585 #[serde(default, rename = "costUSD")]
586 pub cost_usd: f64,
587 #[serde(default)]
588 pub web_search_requests: u32,
589 #[serde(default)]
590 pub context_window: u64,
591 #[serde(default)]
592 pub max_output_tokens: u64,
593 #[serde(flatten)]
594 pub extra: serde_json::Map<String, serde_json::Value>,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
599pub struct DeferredToolUse {
600 pub id: String,
601 pub name: String,
602 pub input: Value,
603}
604
605#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
614pub struct SubagentStats {
615 pub spawned: u64,
617 pub requested: SubagentSpawnRequests,
620 pub started_in_background: u64,
623 #[serde(default)]
625 pub by_type: std::collections::BTreeMap<String, u64>,
626 pub max_depth: u64,
629 pub spawned_by_subagents: u64,
631 pub completed: u64,
632 pub failed: u64,
633 pub killed: SubagentKillCounts,
635 pub refused: SubagentRefusalCounts,
638}
639
640#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
643pub struct SubagentSpawnRequests {
644 pub background: u64,
645 pub foreground: u64,
646 pub unset: u64,
647}
648
649#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
653pub struct SubagentKillCounts {
654 pub parent: u64,
655 pub user: u64,
656 pub system: u64,
657}
658
659#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
661pub struct SubagentRefusalCounts {
662 pub depth_limit: u64,
663 pub concurrency_limit: u64,
664 pub budget: u64,
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
672pub struct PermissionDenial {
673 pub tool_name: String,
675
676 pub tool_input: Value,
678
679 pub tool_use_id: String,
681}
682
683#[derive(Debug, Clone, PartialEq, Eq, Hash)]
687pub enum StreamPostQueuedBehind {
688 DurablePost,
690 EphemeralPost,
692 RetryBackoff,
694 Hold,
696 None,
698 Unknown(String),
700}
701
702impl StreamPostQueuedBehind {
703 pub fn as_str(&self) -> &str {
704 match self {
705 Self::DurablePost => "durable_post",
706 Self::EphemeralPost => "ephemeral_post",
707 Self::RetryBackoff => "retry_backoff",
708 Self::Hold => "hold",
709 Self::None => "none",
710 Self::Unknown(s) => s.as_str(),
711 }
712 }
713}
714
715impl fmt::Display for StreamPostQueuedBehind {
716 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
717 f.write_str(self.as_str())
718 }
719}
720
721impl From<&str> for StreamPostQueuedBehind {
722 fn from(s: &str) -> Self {
723 match s {
724 "durable_post" => Self::DurablePost,
725 "ephemeral_post" => Self::EphemeralPost,
726 "retry_backoff" => Self::RetryBackoff,
727 "hold" => Self::Hold,
728 "none" => Self::None,
729 other => Self::Unknown(other.to_string()),
730 }
731 }
732}
733
734impl Serialize for StreamPostQueuedBehind {
735 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
736 serializer.serialize_str(self.as_str())
737 }
738}
739
740impl<'de> Deserialize<'de> for StreamPostQueuedBehind {
741 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
742 let s = String::deserialize(deserializer)?;
743 Ok(Self::from(s.as_str()))
744 }
745}
746
747#[derive(Debug, Clone, PartialEq, Eq, Hash)]
750pub enum FastModeDisabledReason {
751 Free,
753 Preference,
755 ExtraUsageDisabled,
757 NetworkError,
759 UnknownReason,
761 NotFirstParty,
763 DisabledByEnv,
765 ModelNotAllowed,
767 SdkOptInRequired,
769 Pending,
771 Unknown(String),
773}
774
775impl FastModeDisabledReason {
776 pub fn as_str(&self) -> &str {
777 match self {
778 Self::Free => "free",
779 Self::Preference => "preference",
780 Self::ExtraUsageDisabled => "extra_usage_disabled",
781 Self::NetworkError => "network_error",
782 Self::UnknownReason => "unknown",
783 Self::NotFirstParty => "not_first_party",
784 Self::DisabledByEnv => "disabled_by_env",
785 Self::ModelNotAllowed => "model_not_allowed",
786 Self::SdkOptInRequired => "sdk_opt_in_required",
787 Self::Pending => "pending",
788 Self::Unknown(s) => s.as_str(),
789 }
790 }
791}
792
793impl fmt::Display for FastModeDisabledReason {
794 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
795 f.write_str(self.as_str())
796 }
797}
798
799impl From<&str> for FastModeDisabledReason {
800 fn from(s: &str) -> Self {
801 match s {
802 "free" => Self::Free,
803 "preference" => Self::Preference,
804 "extra_usage_disabled" => Self::ExtraUsageDisabled,
805 "network_error" => Self::NetworkError,
806 "unknown" => Self::UnknownReason,
807 "not_first_party" => Self::NotFirstParty,
808 "disabled_by_env" => Self::DisabledByEnv,
809 "model_not_allowed" => Self::ModelNotAllowed,
810 "sdk_opt_in_required" => Self::SdkOptInRequired,
811 "pending" => Self::Pending,
812 other => Self::Unknown(other.to_string()),
813 }
814 }
815}
816
817impl Serialize for FastModeDisabledReason {
818 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
819 serializer.serialize_str(self.as_str())
820 }
821}
822
823impl<'de> Deserialize<'de> for FastModeDisabledReason {
824 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
825 let s = String::deserialize(deserializer)?;
826 Ok(Self::from(s.as_str()))
827 }
828}
829
830#[derive(Debug, Clone, PartialEq, Eq, Hash)]
832pub enum ResultSubtype {
833 Success,
834 ErrorMaxTurns,
835 ErrorDuringExecution,
836 ErrorMaxBudgetUsd,
837 ErrorMaxStructuredOutputRetries,
838 Unknown(String),
839}
840
841impl ResultSubtype {
842 pub fn as_str(&self) -> &str {
843 match self {
844 Self::Success => "success",
845 Self::ErrorMaxTurns => "error_max_turns",
846 Self::ErrorDuringExecution => "error_during_execution",
847 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
848 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
849 Self::Unknown(s) => s.as_str(),
850 }
851 }
852}
853
854impl fmt::Display for ResultSubtype {
855 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
856 f.write_str(self.as_str())
857 }
858}
859
860impl From<&str> for ResultSubtype {
861 fn from(s: &str) -> Self {
862 match s {
863 "success" => Self::Success,
864 "error_max_turns" => Self::ErrorMaxTurns,
865 "error_during_execution" => Self::ErrorDuringExecution,
866 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
867 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
868 other => Self::Unknown(other.to_string()),
869 }
870 }
871}
872
873impl Serialize for ResultSubtype {
874 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
875 serializer.serialize_str(self.as_str())
876 }
877}
878
879impl<'de> Deserialize<'de> for ResultSubtype {
880 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
881 let s = String::deserialize(deserializer)?;
882 Ok(Self::from(s.as_str()))
883 }
884}
885
886#[derive(Debug, Clone, Serialize, Deserialize)]
904pub struct UsageInfo {
905 #[serde(default)]
907 pub input_tokens: u32,
908 #[serde(default)]
910 pub cache_creation_input_tokens: u32,
911 #[serde(default)]
915 pub cache_read_input_tokens: u32,
916 #[serde(default)]
918 pub output_tokens: u32,
919 #[serde(default)]
920 pub server_tool_use: ServerToolUse,
921 #[serde(default)]
922 pub service_tier: String,
923
924 #[serde(skip_serializing_if = "Option::is_none")]
926 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
927
928 #[serde(skip_serializing_if = "Option::is_none")]
930 pub inference_geo: Option<String>,
931
932 #[serde(default, skip_serializing_if = "Vec::is_empty")]
936 pub iterations: Vec<UsageIteration>,
937
938 #[serde(skip_serializing_if = "Option::is_none")]
940 pub speed: Option<String>,
941
942 #[serde(skip_serializing_if = "Option::is_none")]
945 pub output_tokens_details: Option<OutputTokensDetails>,
946
947 #[serde(default, skip_serializing_if = "Option::is_none")]
950 pub fallback_credit: Option<Value>,
951}
952
953#[derive(Debug, Clone, Serialize, Deserialize, Default)]
956pub struct OutputTokensDetails {
957 #[serde(skip_serializing_if = "Option::is_none")]
959 pub thinking_tokens: Option<u64>,
960}
961
962#[derive(Debug, Clone, Serialize, Deserialize)]
971pub struct UsageIteration {
972 #[serde(default)]
973 pub input_tokens: u32,
974 #[serde(default)]
975 pub output_tokens: u32,
976 #[serde(default, skip_serializing_if = "Option::is_none")]
978 pub cache_read_input_tokens: Option<u32>,
979 #[serde(default, skip_serializing_if = "Option::is_none")]
981 pub cache_creation_input_tokens: Option<u32>,
982 #[serde(default, skip_serializing_if = "Option::is_none")]
984 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
985 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
987 pub kind: Option<String>,
988}
989
990#[derive(Debug, Clone, Default, Serialize, Deserialize)]
992pub struct ServerToolUse {
993 #[serde(default)]
994 pub web_search_requests: u32,
995 #[serde(default)]
997 pub web_fetch_requests: u32,
998}
999
1000#[cfg(test)]
1001mod tests {
1002 use super::*;
1003 use crate::io::ClaudeOutput;
1004
1005 #[test]
1006 fn test_deserialize_result_message() {
1007 let json = r#"{
1008 "type": "result",
1009 "subtype": "success",
1010 "is_error": false,
1011 "duration_ms": 100,
1012 "duration_api_ms": 200,
1013 "num_turns": 1,
1014 "result": "Done",
1015 "session_id": "123",
1016 "total_cost_usd": 0.01,
1017 "permission_denials": []
1018 }"#;
1019
1020 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1021 assert!(!output.is_error());
1022 }
1023
1024 #[test]
1025 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
1026 let json = r#"{
1027 "type": "result",
1028 "subtype": "error_max_budget_usd",
1029 "is_error": true,
1030 "duration_ms": 100,
1031 "duration_api_ms": 200,
1032 "num_turns": 1,
1033 "session_id": "123",
1034 "total_cost_usd": 0.01
1035 }"#;
1036
1037 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1038 let ClaudeOutput::Result(result) = output else {
1039 panic!("Expected Result");
1040 };
1041 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
1042
1043 let json = r#"{
1044 "type": "result",
1045 "subtype": "future_result_subtype",
1046 "is_error": true,
1047 "duration_ms": 100,
1048 "duration_api_ms": 200,
1049 "num_turns": 1,
1050 "session_id": "123",
1051 "total_cost_usd": 0.01
1052 }"#;
1053
1054 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1055 let ClaudeOutput::Result(result) = output else {
1056 panic!("Expected Result");
1057 };
1058 assert_eq!(
1059 result.subtype,
1060 ResultSubtype::Unknown("future_result_subtype".to_string())
1061 );
1062 }
1063
1064 #[test]
1065 fn test_deserialize_result_with_permission_denials() {
1066 let json = r#"{
1067 "type": "result",
1068 "subtype": "success",
1069 "is_error": false,
1070 "duration_ms": 100,
1071 "duration_api_ms": 200,
1072 "num_turns": 2,
1073 "result": "Done",
1074 "session_id": "123",
1075 "total_cost_usd": 0.01,
1076 "permission_denials": [
1077 {
1078 "tool_name": "Bash",
1079 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1080 "tool_use_id": "toolu_123"
1081 }
1082 ]
1083 }"#;
1084
1085 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1086 if let ClaudeOutput::Result(result) = output {
1087 assert_eq!(result.permission_denials.len(), 1);
1088 assert_eq!(result.permission_denials[0].tool_name, "Bash");
1089 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1090 assert_eq!(
1091 result.permission_denials[0]
1092 .tool_input
1093 .get("command")
1094 .unwrap(),
1095 "rm -rf /"
1096 );
1097 } else {
1098 panic!("Expected Result");
1099 }
1100 }
1101
1102 #[test]
1103 fn test_permission_denial_roundtrip() {
1104 let denial = PermissionDenial {
1105 tool_name: "Write".to_string(),
1106 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1107 tool_use_id: "toolu_456".to_string(),
1108 };
1109
1110 let json = serde_json::to_string(&denial).unwrap();
1111 assert!(json.contains("\"tool_name\":\"Write\""));
1112 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1113 assert!(json.contains("/etc/passwd"));
1114
1115 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1116 assert_eq!(parsed, denial);
1117 }
1118
1119 #[test]
1120 fn test_deserialize_result_message_with_errors() {
1121 let json = r#"{
1122 "type": "result",
1123 "subtype": "error_during_execution",
1124 "duration_ms": 0,
1125 "duration_api_ms": 0,
1126 "is_error": true,
1127 "num_turns": 0,
1128 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
1129 "total_cost_usd": 0,
1130 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
1131 "permission_denials": []
1132 }"#;
1133
1134 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1135 assert!(output.is_error());
1136
1137 if let ClaudeOutput::Result(res) = output {
1138 assert!(res.is_error);
1139 assert_eq!(res.errors.len(), 1);
1140 assert!(res.errors[0].contains("No conversation found"));
1141 } else {
1142 panic!("Expected Result message");
1143 }
1144 }
1145
1146 #[test]
1147 fn test_deserialize_result_message_errors_defaults_empty() {
1148 let json = r#"{
1149 "type": "result",
1150 "subtype": "success",
1151 "is_error": false,
1152 "duration_ms": 100,
1153 "duration_api_ms": 200,
1154 "num_turns": 1,
1155 "session_id": "123",
1156 "total_cost_usd": 0.01
1157 }"#;
1158
1159 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1160 if let ClaudeOutput::Result(res) = output {
1161 assert!(res.errors.is_empty());
1162 } else {
1163 panic!("Expected Result message");
1164 }
1165 }
1166
1167 #[test]
1168 fn test_result_message_errors_roundtrip() {
1169 let json = r#"{
1170 "type": "result",
1171 "subtype": "error_during_execution",
1172 "is_error": true,
1173 "duration_ms": 0,
1174 "duration_api_ms": 0,
1175 "num_turns": 0,
1176 "session_id": "test-session",
1177 "total_cost_usd": 0.0,
1178 "errors": ["Error 1", "Error 2"]
1179 }"#;
1180
1181 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1182 let reserialized = serde_json::to_string(&output).unwrap();
1183
1184 assert!(reserialized.contains("Error 1"));
1185 assert!(reserialized.contains("Error 2"));
1186 }
1187
1188 #[test]
1189 fn test_result_with_new_fields() {
1190 let json = r#"{
1191 "type": "result",
1192 "subtype": "success",
1193 "is_error": false,
1194 "duration_ms": 5000,
1195 "duration_api_ms": 4500,
1196 "num_turns": 1,
1197 "result": "Done",
1198 "session_id": "abc",
1199 "total_cost_usd": 0.06,
1200 "api_error_status": null,
1201 "stop_reason": "end_turn",
1202 "terminal_reason": "completed",
1203 "fast_mode_state": "off",
1204 "modelUsage": {
1205 "claude-opus-4-7[1m]": {
1206 "inputTokens": 3817,
1207 "outputTokens": 14,
1208 "costUSD": 0.06
1209 }
1210 },
1211 "usage": {
1212 "input_tokens": 3817,
1213 "output_tokens": 14,
1214 "cache_creation_input_tokens": 3540,
1215 "cache_read_input_tokens": 0,
1216 "server_tool_use": {
1217 "web_search_requests": 0,
1218 "web_fetch_requests": 2
1219 },
1220 "service_tier": "standard",
1221 "inference_geo": "not_available",
1222 "speed": "standard",
1223 "iterations": [
1224 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
1225 ]
1226 }
1227 }"#;
1228
1229 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1230 if let ClaudeOutput::Result(res) = output {
1231 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
1232 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
1233 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
1234 let model_usage = res.model_usage.as_ref().unwrap();
1235 let entry = model_usage
1236 .get("claude-opus-4-7[1m]")
1237 .expect("per-model entry present");
1238 assert_eq!(entry.input_tokens, 3817);
1239 assert_eq!(entry.output_tokens, 14);
1240 assert_eq!(entry.cost_usd, 0.06);
1241 assert!(res.api_error_status.is_none());
1242
1243 let usage = res.usage.unwrap();
1244 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
1245 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1246 assert_eq!(usage.speed.as_deref(), Some("standard"));
1247 assert_eq!(usage.iterations.len(), 1);
1248 assert_eq!(usage.iterations[0].input_tokens, 3817);
1249 assert_eq!(usage.iterations[0].output_tokens, 14);
1250 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
1251 } else {
1252 panic!("Expected Result");
1253 }
1254 }
1255
1256 #[test]
1257 fn test_result_backwards_compatible_without_new_fields() {
1258 let json = r#"{
1260 "type": "result",
1261 "subtype": "success",
1262 "is_error": false,
1263 "duration_ms": 100,
1264 "duration_api_ms": 200,
1265 "num_turns": 1,
1266 "session_id": "abc",
1267 "total_cost_usd": 0.01
1268 }"#;
1269
1270 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1271 if let ClaudeOutput::Result(res) = output {
1272 assert!(res.api_error_status.is_none());
1273 assert!(res.stop_reason.is_none());
1274 assert!(res.terminal_reason.is_none());
1275 assert!(res.fast_mode_state.is_none());
1276 assert!(res.model_usage.is_none());
1277 } else {
1278 panic!("Expected Result");
1279 }
1280 }
1281
1282 #[test]
1283 fn test_result_fast_mode_disabled_reason() {
1284 let json = r#"{
1285 "type":"result","subtype":"success","is_error":false,
1286 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1287 "session_id":"s1","total_cost_usd":0.01,
1288 "fast_mode_state":"off",
1289 "fast_mode_disabled_reason":"sdk_opt_in_required"
1290 }"#;
1291 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1292 let crate::ClaudeOutput::Result(res) = &output else {
1293 panic!("expected Result");
1294 };
1295 assert_eq!(
1296 res.fast_mode_disabled_reason,
1297 Some(FastModeDisabledReason::SdkOptInRequired)
1298 );
1299 assert!(serde_json::to_string(&output)
1300 .unwrap()
1301 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
1302
1303 assert_eq!(
1306 FastModeDisabledReason::from("unknown"),
1307 FastModeDisabledReason::UnknownReason
1308 );
1309 let novel = FastModeDisabledReason::from("solar_flare");
1310 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
1311 assert_eq!(novel.as_str(), "solar_flare");
1312 }
1313
1314 #[test]
1315 fn test_result_timing_and_user_message_uuid_fields() {
1316 let json = r#"{
1317 "type":"result","subtype":"success","is_error":false,
1318 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1319 "session_id":"s1","total_cost_usd":0.01,
1320 "request_sent_wall_ms":1753212345678.25,
1321 "user_message_uuid":"um-1"
1322 }"#;
1323 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1324 let crate::ClaudeOutput::Result(res) = &output else {
1325 panic!("expected Result");
1326 };
1327 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
1328 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
1329
1330 let reserialized = serde_json::to_string(&output).unwrap();
1331 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
1332 }
1333
1334 #[test]
1335 fn test_result_queued_turn_count_and_user_message_uuids() {
1336 let json = r#"{
1338 "type":"result","subtype":"success","is_error":false,
1339 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1340 "session_id":"s1","total_cost_usd":0.01,
1341 "user_message_uuid":"um-2",
1342 "user_message_uuids":["um-1","um-2"],
1343 "queued_turn_count":3
1344 }"#;
1345 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1346 let crate::ClaudeOutput::Result(res) = &output else {
1347 panic!("expected Result");
1348 };
1349 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
1350 assert_eq!(res.queued_turn_count, Some(3));
1351
1352 let reserialized = serde_json::to_string(&output).unwrap();
1353 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
1354 assert!(reserialized.contains("\"queued_turn_count\":3"));
1355
1356 let old = r#"{
1358 "type":"result","subtype":"success","is_error":false,
1359 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1360 "session_id":"s1","total_cost_usd":0.01
1361 }"#;
1362 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
1363 let crate::ClaudeOutput::Result(res) = &output else {
1364 panic!("expected Result");
1365 };
1366 assert!(res.user_message_uuids.is_empty());
1367 assert_eq!(res.queued_turn_count, None);
1368 let reserialized = serde_json::to_string(&output).unwrap();
1369 assert!(!reserialized.contains("user_message_uuids"));
1370 assert!(!reserialized.contains("queued_turn_count"));
1371 }
1372}