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")]
49 pub time_to_request_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
50
51 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub turn_start_resume_kind: Option<TurnStartResumeKind>,
59
60 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub process_turn_index: Option<u64>,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub time_to_request_cpu_ms: Option<u64>,
80
81 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub time_to_request_loop_lag_ms: Option<u64>,
89
90 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub time_to_request_major_faults: Option<u64>,
97
98 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub input_attachments_detail: Option<InputAttachmentsDetail>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub time_to_request_from_spawn_ms: Option<u64>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub warm_spare_claimed: Option<bool>,
112
113 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub flag_fetch_kick: Option<String>,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub time_origin_ms: Option<u64>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
131 pub request_sent_wall_ms: Option<f64>,
132
133 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub first_request_input_tokens: Option<u64>,
141
142 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub first_content_frame_ms: Option<u64>,
146
147 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub first_stream_post_ms: Option<u64>,
151
152 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub first_stream_post_ack_ms: Option<u64>,
156
157 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub first_stream_post_queue_wait_ms: Option<u64>,
161
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub first_stream_post_queued_behind: Option<StreamPostQueuedBehind>,
165
166 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub first_stream_post_wall_ms: Option<f64>,
170
171 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub frame_received_wall_ms: Option<f64>,
183
184 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub frame_enqueued_wall_ms: Option<f64>,
189
190 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub frame_intake_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
208
209 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub turn_started_wall_ms: Option<f64>,
217
218 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub turn_start_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
235
236 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub turn_start_control_requests_ms: Option<u64>,
245
246 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub first_text_post_ms: Option<u64>,
250
251 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub first_text_post_queue_wait_ms: Option<u64>,
255
256 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub first_text_post_queued_behind: Option<StreamPostQueuedBehind>,
261
262 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub first_text_post_wall_ms: Option<f64>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub user_message_uuid: Option<String>,
270
271 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub user_message_server_received_wall_ms: Option<f64>,
279
280 #[serde(default, skip_serializing_if = "Vec::is_empty")]
287 pub user_message_uuids: Vec<String>,
288
289 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub resume_reason: Option<String>,
298
299 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub local_command: Option<String>,
304
305 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub queued_turn_count: Option<u64>,
312
313 pub num_turns: i32,
314
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub result: Option<String>,
317
318 #[serde(alias = "sessionId")]
319 pub session_id: String,
320 pub total_cost_usd: f64,
321
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub usage: Option<UsageInfo>,
324
325 #[serde(default)]
327 pub permission_denials: Vec<PermissionDenial>,
328
329 #[serde(default)]
334 pub errors: Vec<String>,
335
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub uuid: Option<String>,
338
339 #[serde(skip_serializing_if = "Option::is_none")]
341 pub api_error_status: Option<u16>,
342
343 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub api_error_code: Option<String>,
350
351 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub api_error: Option<String>,
357
358 #[serde(skip_serializing_if = "Option::is_none")]
360 pub stop_reason: Option<String>,
361
362 #[serde(skip_serializing_if = "Option::is_none")]
364 pub terminal_reason: Option<String>,
365
366 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub result_index: Option<u64>,
375
376 #[serde(skip_serializing_if = "Option::is_none")]
378 pub fast_mode_state: Option<String>,
379
380 #[serde(skip_serializing_if = "Option::is_none")]
384 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
385
386 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
388 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
389
390 #[serde(skip_serializing_if = "Option::is_none")]
394 pub subagent_stats: Option<SubagentStats>,
395
396 #[serde(skip_serializing_if = "Option::is_none")]
398 pub structured_output: Option<Value>,
399
400 #[serde(skip_serializing_if = "Option::is_none")]
402 pub deferred_tool_use: Option<DeferredToolUse>,
403
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub origin: Option<super::message_types::MessageOrigin>,
407
408 #[serde(default, skip_serializing_if = "Option::is_none")]
412 pub runner_exit: Option<RunnerExit>,
413
414 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub startup_failure_reason: Option<StartupFailureReason>,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
431pub struct InputAttachmentsDetail {
432 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub slowest_producer: Option<String>,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub slowest_producer_ms: Option<u64>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub cpu_ms: Option<u64>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
449 pub major_faults: Option<u64>,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub changed_files_reread: Option<u64>,
456}
457
458#[derive(Debug, Clone, PartialEq, Eq, Hash)]
459pub enum StartupFailureReason {
460 OrgPinApiKeyConflict,
463 ProviderNotAllowed,
467 OrgVerifyFailed,
470 OrgPinMismatch,
472 ManagedSettingsInvalid,
477 RemoteSettingsRequiredUnavailable,
479 GatewaySigninRequired,
481 GatewayAccessDenied,
483 ProxyInvalid,
485 TempDirUnusable,
487 CwdUnavailable,
489 ShellToolMissing,
492 SessionHeldByBackground,
495 WorktreeResumeRefused,
499 WorktreeUnverified,
502 CliVersionTooOld,
504 BypassRoot,
506 Unknown(String),
508}
509
510impl StartupFailureReason {
511 pub fn as_str(&self) -> &str {
512 match self {
513 Self::OrgPinApiKeyConflict => "org_pin_api_key_conflict",
514 Self::ProviderNotAllowed => "provider_not_allowed",
515 Self::OrgVerifyFailed => "org_verify_failed",
516 Self::OrgPinMismatch => "org_pin_mismatch",
517 Self::ManagedSettingsInvalid => "managed_settings_invalid",
518 Self::RemoteSettingsRequiredUnavailable => "remote_settings_required_unavailable",
519 Self::GatewaySigninRequired => "gateway_signin_required",
520 Self::GatewayAccessDenied => "gateway_access_denied",
521 Self::ProxyInvalid => "proxy_invalid",
522 Self::TempDirUnusable => "temp_dir_unusable",
523 Self::CwdUnavailable => "cwd_unavailable",
524 Self::ShellToolMissing => "shell_tool_missing",
525 Self::SessionHeldByBackground => "session_held_by_background",
526 Self::WorktreeResumeRefused => "worktree_resume_refused",
527 Self::WorktreeUnverified => "worktree_unverified",
528 Self::CliVersionTooOld => "cli_version_too_old",
529 Self::BypassRoot => "bypass_root",
530 Self::Unknown(s) => s.as_str(),
531 }
532 }
533}
534
535impl fmt::Display for StartupFailureReason {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 f.write_str(self.as_str())
538 }
539}
540
541impl From<&str> for StartupFailureReason {
542 fn from(s: &str) -> Self {
543 match s {
544 "org_pin_api_key_conflict" => Self::OrgPinApiKeyConflict,
545 "provider_not_allowed" => Self::ProviderNotAllowed,
546 "org_verify_failed" => Self::OrgVerifyFailed,
547 "org_pin_mismatch" => Self::OrgPinMismatch,
548 "managed_settings_invalid" => Self::ManagedSettingsInvalid,
549 "remote_settings_required_unavailable" => Self::RemoteSettingsRequiredUnavailable,
550 "gateway_signin_required" => Self::GatewaySigninRequired,
551 "gateway_access_denied" => Self::GatewayAccessDenied,
552 "proxy_invalid" => Self::ProxyInvalid,
553 "temp_dir_unusable" => Self::TempDirUnusable,
554 "cwd_unavailable" => Self::CwdUnavailable,
555 "shell_tool_missing" => Self::ShellToolMissing,
556 "session_held_by_background" => Self::SessionHeldByBackground,
557 "worktree_resume_refused" => Self::WorktreeResumeRefused,
558 "worktree_unverified" => Self::WorktreeUnverified,
559 "cli_version_too_old" => Self::CliVersionTooOld,
560 "bypass_root" => Self::BypassRoot,
561 other => Self::Unknown(other.to_string()),
562 }
563 }
564}
565
566impl Serialize for StartupFailureReason {
567 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
568 serializer.serialize_str(self.as_str())
569 }
570}
571
572impl<'de> Deserialize<'de> for StartupFailureReason {
573 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
574 let s = String::deserialize(deserializer)?;
575 Ok(Self::from(s.as_str()))
576 }
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
583pub struct RunnerExit {
584 pub phase: RunnerExitPhase,
586 #[serde(default, skip_serializing_if = "Option::is_none")]
589 pub exit_code: Option<i64>,
590 #[serde(default, skip_serializing_if = "Option::is_none")]
593 pub signal: Option<String>,
594}
595
596#[derive(Debug, Clone, PartialEq, Eq, Hash)]
600pub enum RunnerExitPhase {
601 Setup,
603 Run,
605 Unknown(String),
607}
608
609impl RunnerExitPhase {
610 pub fn as_str(&self) -> &str {
611 match self {
612 Self::Setup => "setup",
613 Self::Run => "run",
614 Self::Unknown(s) => s.as_str(),
615 }
616 }
617}
618
619impl std::fmt::Display for RunnerExitPhase {
620 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621 f.write_str(self.as_str())
622 }
623}
624
625impl From<&str> for RunnerExitPhase {
626 fn from(s: &str) -> Self {
627 match s {
628 "setup" => Self::Setup,
629 "run" => Self::Run,
630 other => Self::Unknown(other.to_string()),
631 }
632 }
633}
634
635impl Serialize for RunnerExitPhase {
636 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
637 serializer.serialize_str(self.as_str())
638 }
639}
640
641impl<'de> Deserialize<'de> for RunnerExitPhase {
642 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
643 let s = String::deserialize(deserializer)?;
644 Ok(Self::from(s.as_str()))
645 }
646}
647
648#[derive(Debug, Clone, Default, Serialize, Deserialize)]
654#[serde(rename_all = "camelCase")]
655pub struct ModelUsageEntry {
656 #[serde(default)]
657 pub input_tokens: u64,
658 #[serde(default)]
659 pub output_tokens: u64,
660 #[serde(default)]
661 pub cache_read_input_tokens: u64,
662 #[serde(default)]
663 pub cache_creation_input_tokens: u64,
664 #[serde(default, rename = "costUSD")]
665 pub cost_usd: f64,
666 #[serde(default)]
667 pub web_search_requests: u32,
668 #[serde(default)]
669 pub context_window: u64,
670 #[serde(default)]
671 pub max_output_tokens: u64,
672 #[serde(flatten)]
673 pub extra: serde_json::Map<String, serde_json::Value>,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
678pub struct DeferredToolUse {
679 pub id: String,
680 pub name: String,
681 pub input: Value,
682}
683
684#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
693pub struct SubagentStats {
694 pub spawned: u64,
696 pub requested: SubagentSpawnRequests,
699 pub started_in_background: u64,
702 #[serde(default)]
704 pub by_type: std::collections::BTreeMap<String, u64>,
705 pub max_depth: u64,
708 pub spawned_by_subagents: u64,
710 pub completed: u64,
711 pub failed: u64,
712 pub killed: SubagentKillCounts,
714 pub refused: SubagentRefusalCounts,
717}
718
719#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
722pub struct SubagentSpawnRequests {
723 pub background: u64,
724 pub foreground: u64,
725 pub unset: u64,
726}
727
728#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
732pub struct SubagentKillCounts {
733 pub parent: u64,
734 pub user: u64,
735 pub system: u64,
736}
737
738#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
740pub struct SubagentRefusalCounts {
741 pub depth_limit: u64,
742 pub concurrency_limit: u64,
743 pub budget: u64,
744}
745
746#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
751pub struct PermissionDenial {
752 pub tool_name: String,
754
755 pub tool_input: Value,
757
758 pub tool_use_id: String,
760}
761
762#[derive(Debug, Clone, PartialEq, Eq, Hash)]
766pub enum StreamPostQueuedBehind {
767 DurablePost,
769 EphemeralPost,
771 RetryBackoff,
773 Hold,
775 None,
777 Unknown(String),
779}
780
781impl StreamPostQueuedBehind {
782 pub fn as_str(&self) -> &str {
783 match self {
784 Self::DurablePost => "durable_post",
785 Self::EphemeralPost => "ephemeral_post",
786 Self::RetryBackoff => "retry_backoff",
787 Self::Hold => "hold",
788 Self::None => "none",
789 Self::Unknown(s) => s.as_str(),
790 }
791 }
792}
793
794impl fmt::Display for StreamPostQueuedBehind {
795 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
796 f.write_str(self.as_str())
797 }
798}
799
800impl From<&str> for StreamPostQueuedBehind {
801 fn from(s: &str) -> Self {
802 match s {
803 "durable_post" => Self::DurablePost,
804 "ephemeral_post" => Self::EphemeralPost,
805 "retry_backoff" => Self::RetryBackoff,
806 "hold" => Self::Hold,
807 "none" => Self::None,
808 other => Self::Unknown(other.to_string()),
809 }
810 }
811}
812
813impl Serialize for StreamPostQueuedBehind {
814 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
815 serializer.serialize_str(self.as_str())
816 }
817}
818
819impl<'de> Deserialize<'de> for StreamPostQueuedBehind {
820 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
821 let s = String::deserialize(deserializer)?;
822 Ok(Self::from(s.as_str()))
823 }
824}
825
826#[derive(Debug, Clone, PartialEq, Eq, Hash)]
830pub enum TurnStartResumeKind {
831 OrphanedPermission,
835 TurnHandoff,
838 DeferredToolUse,
840 None,
843 Unknown(String),
845}
846
847impl TurnStartResumeKind {
848 pub fn as_str(&self) -> &str {
849 match self {
850 Self::OrphanedPermission => "orphaned_permission",
851 Self::TurnHandoff => "turn_handoff",
852 Self::DeferredToolUse => "deferred_tool_use",
853 Self::None => "none",
854 Self::Unknown(s) => s.as_str(),
855 }
856 }
857}
858
859impl fmt::Display for TurnStartResumeKind {
860 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
861 f.write_str(self.as_str())
862 }
863}
864
865impl From<&str> for TurnStartResumeKind {
866 fn from(s: &str) -> Self {
867 match s {
868 "orphaned_permission" => Self::OrphanedPermission,
869 "turn_handoff" => Self::TurnHandoff,
870 "deferred_tool_use" => Self::DeferredToolUse,
871 "none" => Self::None,
872 other => Self::Unknown(other.to_string()),
873 }
874 }
875}
876
877impl Serialize for TurnStartResumeKind {
878 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
879 serializer.serialize_str(self.as_str())
880 }
881}
882
883impl<'de> Deserialize<'de> for TurnStartResumeKind {
884 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
885 let s = String::deserialize(deserializer)?;
886 Ok(Self::from(s.as_str()))
887 }
888}
889
890#[derive(Debug, Clone, PartialEq, Eq, Hash)]
893pub enum FastModeDisabledReason {
894 Free,
896 Preference,
898 ExtraUsageDisabled,
900 NetworkError,
902 UnknownReason,
904 NotFirstParty,
906 DisabledByEnv,
908 ModelNotAllowed,
910 SdkOptInRequired,
912 Pending,
914 Unknown(String),
916}
917
918impl FastModeDisabledReason {
919 pub fn as_str(&self) -> &str {
920 match self {
921 Self::Free => "free",
922 Self::Preference => "preference",
923 Self::ExtraUsageDisabled => "extra_usage_disabled",
924 Self::NetworkError => "network_error",
925 Self::UnknownReason => "unknown",
926 Self::NotFirstParty => "not_first_party",
927 Self::DisabledByEnv => "disabled_by_env",
928 Self::ModelNotAllowed => "model_not_allowed",
929 Self::SdkOptInRequired => "sdk_opt_in_required",
930 Self::Pending => "pending",
931 Self::Unknown(s) => s.as_str(),
932 }
933 }
934}
935
936impl fmt::Display for FastModeDisabledReason {
937 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
938 f.write_str(self.as_str())
939 }
940}
941
942impl From<&str> for FastModeDisabledReason {
943 fn from(s: &str) -> Self {
944 match s {
945 "free" => Self::Free,
946 "preference" => Self::Preference,
947 "extra_usage_disabled" => Self::ExtraUsageDisabled,
948 "network_error" => Self::NetworkError,
949 "unknown" => Self::UnknownReason,
950 "not_first_party" => Self::NotFirstParty,
951 "disabled_by_env" => Self::DisabledByEnv,
952 "model_not_allowed" => Self::ModelNotAllowed,
953 "sdk_opt_in_required" => Self::SdkOptInRequired,
954 "pending" => Self::Pending,
955 other => Self::Unknown(other.to_string()),
956 }
957 }
958}
959
960impl Serialize for FastModeDisabledReason {
961 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
962 serializer.serialize_str(self.as_str())
963 }
964}
965
966impl<'de> Deserialize<'de> for FastModeDisabledReason {
967 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
968 let s = String::deserialize(deserializer)?;
969 Ok(Self::from(s.as_str()))
970 }
971}
972
973#[derive(Debug, Clone, PartialEq, Eq, Hash)]
975pub enum ResultSubtype {
976 Success,
977 ErrorMaxTurns,
978 ErrorDuringExecution,
979 ErrorMaxBudgetUsd,
980 ErrorMaxStructuredOutputRetries,
981 Unknown(String),
982}
983
984impl ResultSubtype {
985 pub fn as_str(&self) -> &str {
986 match self {
987 Self::Success => "success",
988 Self::ErrorMaxTurns => "error_max_turns",
989 Self::ErrorDuringExecution => "error_during_execution",
990 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
991 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
992 Self::Unknown(s) => s.as_str(),
993 }
994 }
995}
996
997impl fmt::Display for ResultSubtype {
998 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
999 f.write_str(self.as_str())
1000 }
1001}
1002
1003impl From<&str> for ResultSubtype {
1004 fn from(s: &str) -> Self {
1005 match s {
1006 "success" => Self::Success,
1007 "error_max_turns" => Self::ErrorMaxTurns,
1008 "error_during_execution" => Self::ErrorDuringExecution,
1009 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
1010 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
1011 other => Self::Unknown(other.to_string()),
1012 }
1013 }
1014}
1015
1016impl Serialize for ResultSubtype {
1017 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1018 serializer.serialize_str(self.as_str())
1019 }
1020}
1021
1022impl<'de> Deserialize<'de> for ResultSubtype {
1023 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1024 let s = String::deserialize(deserializer)?;
1025 Ok(Self::from(s.as_str()))
1026 }
1027}
1028
1029#[derive(Debug, Clone, Serialize, Deserialize)]
1047pub struct UsageInfo {
1048 #[serde(default)]
1050 pub input_tokens: u32,
1051 #[serde(default)]
1053 pub cache_creation_input_tokens: u32,
1054 #[serde(default)]
1058 pub cache_read_input_tokens: u32,
1059 #[serde(default)]
1061 pub output_tokens: u32,
1062 #[serde(default)]
1063 pub server_tool_use: ServerToolUse,
1064 #[serde(default)]
1065 pub service_tier: String,
1066
1067 #[serde(skip_serializing_if = "Option::is_none")]
1069 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
1070
1071 #[serde(skip_serializing_if = "Option::is_none")]
1073 pub inference_geo: Option<String>,
1074
1075 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1079 pub iterations: Vec<UsageIteration>,
1080
1081 #[serde(skip_serializing_if = "Option::is_none")]
1083 pub speed: Option<String>,
1084
1085 #[serde(skip_serializing_if = "Option::is_none")]
1088 pub output_tokens_details: Option<OutputTokensDetails>,
1089
1090 #[serde(default, skip_serializing_if = "Option::is_none")]
1093 pub fallback_credit: Option<Value>,
1094}
1095
1096#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1099pub struct OutputTokensDetails {
1100 #[serde(skip_serializing_if = "Option::is_none")]
1102 pub thinking_tokens: Option<u64>,
1103}
1104
1105#[derive(Debug, Clone, Serialize, Deserialize)]
1114pub struct UsageIteration {
1115 #[serde(default)]
1116 pub input_tokens: u32,
1117 #[serde(default)]
1118 pub output_tokens: u32,
1119 #[serde(default, skip_serializing_if = "Option::is_none")]
1121 pub cache_read_input_tokens: Option<u32>,
1122 #[serde(default, skip_serializing_if = "Option::is_none")]
1124 pub cache_creation_input_tokens: Option<u32>,
1125 #[serde(default, skip_serializing_if = "Option::is_none")]
1127 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
1128 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
1130 pub kind: Option<String>,
1131}
1132
1133#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1135pub struct ServerToolUse {
1136 #[serde(default)]
1137 pub web_search_requests: u32,
1138 #[serde(default)]
1140 pub web_fetch_requests: u32,
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145 use super::*;
1146 use crate::io::ClaudeOutput;
1147
1148 #[test]
1149 fn test_deserialize_result_message() {
1150 let json = r#"{
1151 "type": "result",
1152 "subtype": "success",
1153 "is_error": false,
1154 "duration_ms": 100,
1155 "duration_api_ms": 200,
1156 "num_turns": 1,
1157 "result": "Done",
1158 "session_id": "123",
1159 "total_cost_usd": 0.01,
1160 "permission_denials": []
1161 }"#;
1162
1163 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1164 assert!(!output.is_error());
1165 }
1166
1167 #[test]
1168 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
1169 let json = r#"{
1170 "type": "result",
1171 "subtype": "error_max_budget_usd",
1172 "is_error": true,
1173 "duration_ms": 100,
1174 "duration_api_ms": 200,
1175 "num_turns": 1,
1176 "session_id": "123",
1177 "total_cost_usd": 0.01
1178 }"#;
1179
1180 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1181 let ClaudeOutput::Result(result) = output else {
1182 panic!("Expected Result");
1183 };
1184 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
1185
1186 let json = r#"{
1187 "type": "result",
1188 "subtype": "future_result_subtype",
1189 "is_error": true,
1190 "duration_ms": 100,
1191 "duration_api_ms": 200,
1192 "num_turns": 1,
1193 "session_id": "123",
1194 "total_cost_usd": 0.01
1195 }"#;
1196
1197 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1198 let ClaudeOutput::Result(result) = output else {
1199 panic!("Expected Result");
1200 };
1201 assert_eq!(
1202 result.subtype,
1203 ResultSubtype::Unknown("future_result_subtype".to_string())
1204 );
1205 }
1206
1207 #[test]
1208 fn test_deserialize_result_with_permission_denials() {
1209 let json = r#"{
1210 "type": "result",
1211 "subtype": "success",
1212 "is_error": false,
1213 "duration_ms": 100,
1214 "duration_api_ms": 200,
1215 "num_turns": 2,
1216 "result": "Done",
1217 "session_id": "123",
1218 "total_cost_usd": 0.01,
1219 "permission_denials": [
1220 {
1221 "tool_name": "Bash",
1222 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1223 "tool_use_id": "toolu_123"
1224 }
1225 ]
1226 }"#;
1227
1228 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1229 if let ClaudeOutput::Result(result) = output {
1230 assert_eq!(result.permission_denials.len(), 1);
1231 assert_eq!(result.permission_denials[0].tool_name, "Bash");
1232 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1233 assert_eq!(
1234 result.permission_denials[0]
1235 .tool_input
1236 .get("command")
1237 .unwrap(),
1238 "rm -rf /"
1239 );
1240 } else {
1241 panic!("Expected Result");
1242 }
1243 }
1244
1245 #[test]
1246 fn test_permission_denial_roundtrip() {
1247 let denial = PermissionDenial {
1248 tool_name: "Write".to_string(),
1249 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1250 tool_use_id: "toolu_456".to_string(),
1251 };
1252
1253 let json = serde_json::to_string(&denial).unwrap();
1254 assert!(json.contains("\"tool_name\":\"Write\""));
1255 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1256 assert!(json.contains("/etc/passwd"));
1257
1258 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1259 assert_eq!(parsed, denial);
1260 }
1261
1262 #[test]
1263 fn test_deserialize_result_message_with_errors() {
1264 let json = r#"{
1265 "type": "result",
1266 "subtype": "error_during_execution",
1267 "duration_ms": 0,
1268 "duration_api_ms": 0,
1269 "is_error": true,
1270 "num_turns": 0,
1271 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
1272 "total_cost_usd": 0,
1273 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
1274 "permission_denials": []
1275 }"#;
1276
1277 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1278 assert!(output.is_error());
1279
1280 if let ClaudeOutput::Result(res) = output {
1281 assert!(res.is_error);
1282 assert_eq!(res.errors.len(), 1);
1283 assert!(res.errors[0].contains("No conversation found"));
1284 } else {
1285 panic!("Expected Result message");
1286 }
1287 }
1288
1289 #[test]
1290 fn test_deserialize_result_message_errors_defaults_empty() {
1291 let json = r#"{
1292 "type": "result",
1293 "subtype": "success",
1294 "is_error": false,
1295 "duration_ms": 100,
1296 "duration_api_ms": 200,
1297 "num_turns": 1,
1298 "session_id": "123",
1299 "total_cost_usd": 0.01
1300 }"#;
1301
1302 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1303 if let ClaudeOutput::Result(res) = output {
1304 assert!(res.errors.is_empty());
1305 } else {
1306 panic!("Expected Result message");
1307 }
1308 }
1309
1310 #[test]
1311 fn test_result_message_errors_roundtrip() {
1312 let json = r#"{
1313 "type": "result",
1314 "subtype": "error_during_execution",
1315 "is_error": true,
1316 "duration_ms": 0,
1317 "duration_api_ms": 0,
1318 "num_turns": 0,
1319 "session_id": "test-session",
1320 "total_cost_usd": 0.0,
1321 "errors": ["Error 1", "Error 2"]
1322 }"#;
1323
1324 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1325 let reserialized = serde_json::to_string(&output).unwrap();
1326
1327 assert!(reserialized.contains("Error 1"));
1328 assert!(reserialized.contains("Error 2"));
1329 }
1330
1331 #[test]
1332 fn test_result_with_new_fields() {
1333 let json = r#"{
1334 "type": "result",
1335 "subtype": "success",
1336 "is_error": false,
1337 "duration_ms": 5000,
1338 "duration_api_ms": 4500,
1339 "num_turns": 1,
1340 "result": "Done",
1341 "session_id": "abc",
1342 "total_cost_usd": 0.06,
1343 "api_error_status": null,
1344 "stop_reason": "end_turn",
1345 "terminal_reason": "completed",
1346 "fast_mode_state": "off",
1347 "modelUsage": {
1348 "claude-opus-4-7[1m]": {
1349 "inputTokens": 3817,
1350 "outputTokens": 14,
1351 "costUSD": 0.06
1352 }
1353 },
1354 "usage": {
1355 "input_tokens": 3817,
1356 "output_tokens": 14,
1357 "cache_creation_input_tokens": 3540,
1358 "cache_read_input_tokens": 0,
1359 "server_tool_use": {
1360 "web_search_requests": 0,
1361 "web_fetch_requests": 2
1362 },
1363 "service_tier": "standard",
1364 "inference_geo": "not_available",
1365 "speed": "standard",
1366 "iterations": [
1367 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
1368 ]
1369 }
1370 }"#;
1371
1372 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1373 if let ClaudeOutput::Result(res) = output {
1374 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
1375 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
1376 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
1377 let model_usage = res.model_usage.as_ref().unwrap();
1378 let entry = model_usage
1379 .get("claude-opus-4-7[1m]")
1380 .expect("per-model entry present");
1381 assert_eq!(entry.input_tokens, 3817);
1382 assert_eq!(entry.output_tokens, 14);
1383 assert_eq!(entry.cost_usd, 0.06);
1384 assert!(res.api_error_status.is_none());
1385
1386 let usage = res.usage.unwrap();
1387 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
1388 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1389 assert_eq!(usage.speed.as_deref(), Some("standard"));
1390 assert_eq!(usage.iterations.len(), 1);
1391 assert_eq!(usage.iterations[0].input_tokens, 3817);
1392 assert_eq!(usage.iterations[0].output_tokens, 14);
1393 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
1394 } else {
1395 panic!("Expected Result");
1396 }
1397 }
1398
1399 #[test]
1400 fn test_result_backwards_compatible_without_new_fields() {
1401 let json = r#"{
1403 "type": "result",
1404 "subtype": "success",
1405 "is_error": false,
1406 "duration_ms": 100,
1407 "duration_api_ms": 200,
1408 "num_turns": 1,
1409 "session_id": "abc",
1410 "total_cost_usd": 0.01
1411 }"#;
1412
1413 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1414 if let ClaudeOutput::Result(res) = output {
1415 assert!(res.api_error_status.is_none());
1416 assert!(res.stop_reason.is_none());
1417 assert!(res.terminal_reason.is_none());
1418 assert!(res.fast_mode_state.is_none());
1419 assert!(res.model_usage.is_none());
1420 } else {
1421 panic!("Expected Result");
1422 }
1423 }
1424
1425 #[test]
1426 fn test_result_fast_mode_disabled_reason() {
1427 let json = r#"{
1428 "type":"result","subtype":"success","is_error":false,
1429 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1430 "session_id":"s1","total_cost_usd":0.01,
1431 "fast_mode_state":"off",
1432 "fast_mode_disabled_reason":"sdk_opt_in_required"
1433 }"#;
1434 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1435 let crate::ClaudeOutput::Result(res) = &output else {
1436 panic!("expected Result");
1437 };
1438 assert_eq!(
1439 res.fast_mode_disabled_reason,
1440 Some(FastModeDisabledReason::SdkOptInRequired)
1441 );
1442 assert!(serde_json::to_string(&output)
1443 .unwrap()
1444 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
1445
1446 assert_eq!(
1449 FastModeDisabledReason::from("unknown"),
1450 FastModeDisabledReason::UnknownReason
1451 );
1452 let novel = FastModeDisabledReason::from("solar_flare");
1453 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
1454 assert_eq!(novel.as_str(), "solar_flare");
1455 }
1456
1457 #[test]
1458 fn test_result_timing_and_user_message_uuid_fields() {
1459 let json = r#"{
1460 "type":"result","subtype":"success","is_error":false,
1461 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1462 "session_id":"s1","total_cost_usd":0.01,
1463 "request_sent_wall_ms":1753212345678.25,
1464 "user_message_uuid":"um-1"
1465 }"#;
1466 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1467 let crate::ClaudeOutput::Result(res) = &output else {
1468 panic!("expected Result");
1469 };
1470 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
1471 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
1472
1473 let reserialized = serde_json::to_string(&output).unwrap();
1474 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
1475 }
1476
1477 #[test]
1478 fn test_result_queued_turn_count_and_user_message_uuids() {
1479 let json = r#"{
1481 "type":"result","subtype":"success","is_error":false,
1482 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1483 "session_id":"s1","total_cost_usd":0.01,
1484 "user_message_uuid":"um-2",
1485 "user_message_uuids":["um-1","um-2"],
1486 "queued_turn_count":3
1487 }"#;
1488 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1489 let crate::ClaudeOutput::Result(res) = &output else {
1490 panic!("expected Result");
1491 };
1492 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
1493 assert_eq!(res.queued_turn_count, Some(3));
1494
1495 let reserialized = serde_json::to_string(&output).unwrap();
1496 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
1497 assert!(reserialized.contains("\"queued_turn_count\":3"));
1498
1499 let old = r#"{
1501 "type":"result","subtype":"success","is_error":false,
1502 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1503 "session_id":"s1","total_cost_usd":0.01
1504 }"#;
1505 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
1506 let crate::ClaudeOutput::Result(res) = &output else {
1507 panic!("expected Result");
1508 };
1509 assert!(res.user_message_uuids.is_empty());
1510 assert_eq!(res.queued_turn_count, None);
1511 let reserialized = serde_json::to_string(&output).unwrap();
1512 assert!(!reserialized.contains("user_message_uuids"));
1513 assert!(!reserialized.contains("queued_turn_count"));
1514 }
1515}