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(skip_serializing_if = "Option::is_none")]
100 pub time_to_request_from_spawn_ms: Option<u64>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub warm_spare_claimed: Option<bool>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub time_origin_ms: Option<u64>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
113 pub request_sent_wall_ms: Option<f64>,
114
115 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub first_request_input_tokens: Option<u64>,
123
124 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub first_content_frame_ms: Option<u64>,
128
129 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub first_stream_post_ms: Option<u64>,
133
134 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub first_stream_post_ack_ms: Option<u64>,
138
139 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub first_stream_post_queue_wait_ms: Option<u64>,
143
144 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub first_stream_post_queued_behind: Option<StreamPostQueuedBehind>,
147
148 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub first_stream_post_wall_ms: Option<f64>,
152
153 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub frame_received_wall_ms: Option<f64>,
165
166 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub frame_enqueued_wall_ms: Option<f64>,
171
172 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub frame_intake_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
190
191 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub turn_started_wall_ms: Option<f64>,
199
200 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub turn_start_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
217
218 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub turn_start_control_requests_ms: Option<u64>,
227
228 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub first_text_post_ms: Option<u64>,
232
233 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub first_text_post_queue_wait_ms: Option<u64>,
237
238 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub first_text_post_queued_behind: Option<StreamPostQueuedBehind>,
243
244 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub first_text_post_wall_ms: Option<f64>,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub user_message_uuid: Option<String>,
252
253 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub user_message_server_received_wall_ms: Option<f64>,
261
262 #[serde(default, skip_serializing_if = "Vec::is_empty")]
269 pub user_message_uuids: Vec<String>,
270
271 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub resume_reason: Option<String>,
280
281 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub local_command: Option<String>,
286
287 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub queued_turn_count: Option<u64>,
294
295 pub num_turns: i32,
296
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub result: Option<String>,
299
300 #[serde(alias = "sessionId")]
301 pub session_id: String,
302 pub total_cost_usd: f64,
303
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub usage: Option<UsageInfo>,
306
307 #[serde(default)]
309 pub permission_denials: Vec<PermissionDenial>,
310
311 #[serde(default)]
316 pub errors: Vec<String>,
317
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub uuid: Option<String>,
320
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub api_error_status: Option<u16>,
324
325 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub api_error_code: Option<String>,
332
333 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub api_error: Option<String>,
339
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub stop_reason: Option<String>,
343
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub terminal_reason: Option<String>,
347
348 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub result_index: Option<u64>,
357
358 #[serde(skip_serializing_if = "Option::is_none")]
360 pub fast_mode_state: Option<String>,
361
362 #[serde(skip_serializing_if = "Option::is_none")]
366 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
367
368 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
370 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
371
372 #[serde(skip_serializing_if = "Option::is_none")]
376 pub subagent_stats: Option<SubagentStats>,
377
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub structured_output: Option<Value>,
381
382 #[serde(skip_serializing_if = "Option::is_none")]
384 pub deferred_tool_use: Option<DeferredToolUse>,
385
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub origin: Option<super::message_types::MessageOrigin>,
389
390 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub runner_exit: Option<RunnerExit>,
395
396 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub startup_failure_reason: Option<StartupFailureReason>,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Hash)]
411pub enum StartupFailureReason {
412 OrgPinApiKeyConflict,
415 ProviderNotAllowed,
419 OrgVerifyFailed,
422 OrgPinMismatch,
424 ManagedSettingsInvalid,
429 RemoteSettingsRequiredUnavailable,
431 GatewaySigninRequired,
433 GatewayAccessDenied,
435 ProxyInvalid,
437 TempDirUnusable,
439 CwdUnavailable,
441 ShellToolMissing,
444 SessionHeldByBackground,
447 WorktreeResumeRefused,
451 WorktreeUnverified,
454 CliVersionTooOld,
456 BypassRoot,
458 Unknown(String),
460}
461
462impl StartupFailureReason {
463 pub fn as_str(&self) -> &str {
464 match self {
465 Self::OrgPinApiKeyConflict => "org_pin_api_key_conflict",
466 Self::ProviderNotAllowed => "provider_not_allowed",
467 Self::OrgVerifyFailed => "org_verify_failed",
468 Self::OrgPinMismatch => "org_pin_mismatch",
469 Self::ManagedSettingsInvalid => "managed_settings_invalid",
470 Self::RemoteSettingsRequiredUnavailable => "remote_settings_required_unavailable",
471 Self::GatewaySigninRequired => "gateway_signin_required",
472 Self::GatewayAccessDenied => "gateway_access_denied",
473 Self::ProxyInvalid => "proxy_invalid",
474 Self::TempDirUnusable => "temp_dir_unusable",
475 Self::CwdUnavailable => "cwd_unavailable",
476 Self::ShellToolMissing => "shell_tool_missing",
477 Self::SessionHeldByBackground => "session_held_by_background",
478 Self::WorktreeResumeRefused => "worktree_resume_refused",
479 Self::WorktreeUnverified => "worktree_unverified",
480 Self::CliVersionTooOld => "cli_version_too_old",
481 Self::BypassRoot => "bypass_root",
482 Self::Unknown(s) => s.as_str(),
483 }
484 }
485}
486
487impl fmt::Display for StartupFailureReason {
488 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
489 f.write_str(self.as_str())
490 }
491}
492
493impl From<&str> for StartupFailureReason {
494 fn from(s: &str) -> Self {
495 match s {
496 "org_pin_api_key_conflict" => Self::OrgPinApiKeyConflict,
497 "provider_not_allowed" => Self::ProviderNotAllowed,
498 "org_verify_failed" => Self::OrgVerifyFailed,
499 "org_pin_mismatch" => Self::OrgPinMismatch,
500 "managed_settings_invalid" => Self::ManagedSettingsInvalid,
501 "remote_settings_required_unavailable" => Self::RemoteSettingsRequiredUnavailable,
502 "gateway_signin_required" => Self::GatewaySigninRequired,
503 "gateway_access_denied" => Self::GatewayAccessDenied,
504 "proxy_invalid" => Self::ProxyInvalid,
505 "temp_dir_unusable" => Self::TempDirUnusable,
506 "cwd_unavailable" => Self::CwdUnavailable,
507 "shell_tool_missing" => Self::ShellToolMissing,
508 "session_held_by_background" => Self::SessionHeldByBackground,
509 "worktree_resume_refused" => Self::WorktreeResumeRefused,
510 "worktree_unverified" => Self::WorktreeUnverified,
511 "cli_version_too_old" => Self::CliVersionTooOld,
512 "bypass_root" => Self::BypassRoot,
513 other => Self::Unknown(other.to_string()),
514 }
515 }
516}
517
518impl Serialize for StartupFailureReason {
519 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
520 serializer.serialize_str(self.as_str())
521 }
522}
523
524impl<'de> Deserialize<'de> for StartupFailureReason {
525 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
526 let s = String::deserialize(deserializer)?;
527 Ok(Self::from(s.as_str()))
528 }
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
535pub struct RunnerExit {
536 pub phase: RunnerExitPhase,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
541 pub exit_code: Option<i64>,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub signal: Option<String>,
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Hash)]
552pub enum RunnerExitPhase {
553 Setup,
555 Run,
557 Unknown(String),
559}
560
561impl RunnerExitPhase {
562 pub fn as_str(&self) -> &str {
563 match self {
564 Self::Setup => "setup",
565 Self::Run => "run",
566 Self::Unknown(s) => s.as_str(),
567 }
568 }
569}
570
571impl std::fmt::Display for RunnerExitPhase {
572 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
573 f.write_str(self.as_str())
574 }
575}
576
577impl From<&str> for RunnerExitPhase {
578 fn from(s: &str) -> Self {
579 match s {
580 "setup" => Self::Setup,
581 "run" => Self::Run,
582 other => Self::Unknown(other.to_string()),
583 }
584 }
585}
586
587impl Serialize for RunnerExitPhase {
588 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
589 serializer.serialize_str(self.as_str())
590 }
591}
592
593impl<'de> Deserialize<'de> for RunnerExitPhase {
594 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
595 let s = String::deserialize(deserializer)?;
596 Ok(Self::from(s.as_str()))
597 }
598}
599
600#[derive(Debug, Clone, Default, Serialize, Deserialize)]
606#[serde(rename_all = "camelCase")]
607pub struct ModelUsageEntry {
608 #[serde(default)]
609 pub input_tokens: u64,
610 #[serde(default)]
611 pub output_tokens: u64,
612 #[serde(default)]
613 pub cache_read_input_tokens: u64,
614 #[serde(default)]
615 pub cache_creation_input_tokens: u64,
616 #[serde(default, rename = "costUSD")]
617 pub cost_usd: f64,
618 #[serde(default)]
619 pub web_search_requests: u32,
620 #[serde(default)]
621 pub context_window: u64,
622 #[serde(default)]
623 pub max_output_tokens: u64,
624 #[serde(flatten)]
625 pub extra: serde_json::Map<String, serde_json::Value>,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
630pub struct DeferredToolUse {
631 pub id: String,
632 pub name: String,
633 pub input: Value,
634}
635
636#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
645pub struct SubagentStats {
646 pub spawned: u64,
648 pub requested: SubagentSpawnRequests,
651 pub started_in_background: u64,
654 #[serde(default)]
656 pub by_type: std::collections::BTreeMap<String, u64>,
657 pub max_depth: u64,
660 pub spawned_by_subagents: u64,
662 pub completed: u64,
663 pub failed: u64,
664 pub killed: SubagentKillCounts,
666 pub refused: SubagentRefusalCounts,
669}
670
671#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
674pub struct SubagentSpawnRequests {
675 pub background: u64,
676 pub foreground: u64,
677 pub unset: u64,
678}
679
680#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
684pub struct SubagentKillCounts {
685 pub parent: u64,
686 pub user: u64,
687 pub system: u64,
688}
689
690#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
692pub struct SubagentRefusalCounts {
693 pub depth_limit: u64,
694 pub concurrency_limit: u64,
695 pub budget: u64,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
703pub struct PermissionDenial {
704 pub tool_name: String,
706
707 pub tool_input: Value,
709
710 pub tool_use_id: String,
712}
713
714#[derive(Debug, Clone, PartialEq, Eq, Hash)]
718pub enum StreamPostQueuedBehind {
719 DurablePost,
721 EphemeralPost,
723 RetryBackoff,
725 Hold,
727 None,
729 Unknown(String),
731}
732
733impl StreamPostQueuedBehind {
734 pub fn as_str(&self) -> &str {
735 match self {
736 Self::DurablePost => "durable_post",
737 Self::EphemeralPost => "ephemeral_post",
738 Self::RetryBackoff => "retry_backoff",
739 Self::Hold => "hold",
740 Self::None => "none",
741 Self::Unknown(s) => s.as_str(),
742 }
743 }
744}
745
746impl fmt::Display for StreamPostQueuedBehind {
747 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
748 f.write_str(self.as_str())
749 }
750}
751
752impl From<&str> for StreamPostQueuedBehind {
753 fn from(s: &str) -> Self {
754 match s {
755 "durable_post" => Self::DurablePost,
756 "ephemeral_post" => Self::EphemeralPost,
757 "retry_backoff" => Self::RetryBackoff,
758 "hold" => Self::Hold,
759 "none" => Self::None,
760 other => Self::Unknown(other.to_string()),
761 }
762 }
763}
764
765impl Serialize for StreamPostQueuedBehind {
766 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
767 serializer.serialize_str(self.as_str())
768 }
769}
770
771impl<'de> Deserialize<'de> for StreamPostQueuedBehind {
772 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
773 let s = String::deserialize(deserializer)?;
774 Ok(Self::from(s.as_str()))
775 }
776}
777
778#[derive(Debug, Clone, PartialEq, Eq, Hash)]
782pub enum TurnStartResumeKind {
783 OrphanedPermission,
787 TurnHandoff,
790 DeferredToolUse,
792 None,
795 Unknown(String),
797}
798
799impl TurnStartResumeKind {
800 pub fn as_str(&self) -> &str {
801 match self {
802 Self::OrphanedPermission => "orphaned_permission",
803 Self::TurnHandoff => "turn_handoff",
804 Self::DeferredToolUse => "deferred_tool_use",
805 Self::None => "none",
806 Self::Unknown(s) => s.as_str(),
807 }
808 }
809}
810
811impl fmt::Display for TurnStartResumeKind {
812 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813 f.write_str(self.as_str())
814 }
815}
816
817impl From<&str> for TurnStartResumeKind {
818 fn from(s: &str) -> Self {
819 match s {
820 "orphaned_permission" => Self::OrphanedPermission,
821 "turn_handoff" => Self::TurnHandoff,
822 "deferred_tool_use" => Self::DeferredToolUse,
823 "none" => Self::None,
824 other => Self::Unknown(other.to_string()),
825 }
826 }
827}
828
829impl Serialize for TurnStartResumeKind {
830 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
831 serializer.serialize_str(self.as_str())
832 }
833}
834
835impl<'de> Deserialize<'de> for TurnStartResumeKind {
836 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
837 let s = String::deserialize(deserializer)?;
838 Ok(Self::from(s.as_str()))
839 }
840}
841
842#[derive(Debug, Clone, PartialEq, Eq, Hash)]
845pub enum FastModeDisabledReason {
846 Free,
848 Preference,
850 ExtraUsageDisabled,
852 NetworkError,
854 UnknownReason,
856 NotFirstParty,
858 DisabledByEnv,
860 ModelNotAllowed,
862 SdkOptInRequired,
864 Pending,
866 Unknown(String),
868}
869
870impl FastModeDisabledReason {
871 pub fn as_str(&self) -> &str {
872 match self {
873 Self::Free => "free",
874 Self::Preference => "preference",
875 Self::ExtraUsageDisabled => "extra_usage_disabled",
876 Self::NetworkError => "network_error",
877 Self::UnknownReason => "unknown",
878 Self::NotFirstParty => "not_first_party",
879 Self::DisabledByEnv => "disabled_by_env",
880 Self::ModelNotAllowed => "model_not_allowed",
881 Self::SdkOptInRequired => "sdk_opt_in_required",
882 Self::Pending => "pending",
883 Self::Unknown(s) => s.as_str(),
884 }
885 }
886}
887
888impl fmt::Display for FastModeDisabledReason {
889 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
890 f.write_str(self.as_str())
891 }
892}
893
894impl From<&str> for FastModeDisabledReason {
895 fn from(s: &str) -> Self {
896 match s {
897 "free" => Self::Free,
898 "preference" => Self::Preference,
899 "extra_usage_disabled" => Self::ExtraUsageDisabled,
900 "network_error" => Self::NetworkError,
901 "unknown" => Self::UnknownReason,
902 "not_first_party" => Self::NotFirstParty,
903 "disabled_by_env" => Self::DisabledByEnv,
904 "model_not_allowed" => Self::ModelNotAllowed,
905 "sdk_opt_in_required" => Self::SdkOptInRequired,
906 "pending" => Self::Pending,
907 other => Self::Unknown(other.to_string()),
908 }
909 }
910}
911
912impl Serialize for FastModeDisabledReason {
913 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
914 serializer.serialize_str(self.as_str())
915 }
916}
917
918impl<'de> Deserialize<'de> for FastModeDisabledReason {
919 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
920 let s = String::deserialize(deserializer)?;
921 Ok(Self::from(s.as_str()))
922 }
923}
924
925#[derive(Debug, Clone, PartialEq, Eq, Hash)]
927pub enum ResultSubtype {
928 Success,
929 ErrorMaxTurns,
930 ErrorDuringExecution,
931 ErrorMaxBudgetUsd,
932 ErrorMaxStructuredOutputRetries,
933 Unknown(String),
934}
935
936impl ResultSubtype {
937 pub fn as_str(&self) -> &str {
938 match self {
939 Self::Success => "success",
940 Self::ErrorMaxTurns => "error_max_turns",
941 Self::ErrorDuringExecution => "error_during_execution",
942 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
943 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
944 Self::Unknown(s) => s.as_str(),
945 }
946 }
947}
948
949impl fmt::Display for ResultSubtype {
950 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
951 f.write_str(self.as_str())
952 }
953}
954
955impl From<&str> for ResultSubtype {
956 fn from(s: &str) -> Self {
957 match s {
958 "success" => Self::Success,
959 "error_max_turns" => Self::ErrorMaxTurns,
960 "error_during_execution" => Self::ErrorDuringExecution,
961 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
962 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
963 other => Self::Unknown(other.to_string()),
964 }
965 }
966}
967
968impl Serialize for ResultSubtype {
969 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
970 serializer.serialize_str(self.as_str())
971 }
972}
973
974impl<'de> Deserialize<'de> for ResultSubtype {
975 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
976 let s = String::deserialize(deserializer)?;
977 Ok(Self::from(s.as_str()))
978 }
979}
980
981#[derive(Debug, Clone, Serialize, Deserialize)]
999pub struct UsageInfo {
1000 #[serde(default)]
1002 pub input_tokens: u32,
1003 #[serde(default)]
1005 pub cache_creation_input_tokens: u32,
1006 #[serde(default)]
1010 pub cache_read_input_tokens: u32,
1011 #[serde(default)]
1013 pub output_tokens: u32,
1014 #[serde(default)]
1015 pub server_tool_use: ServerToolUse,
1016 #[serde(default)]
1017 pub service_tier: String,
1018
1019 #[serde(skip_serializing_if = "Option::is_none")]
1021 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
1022
1023 #[serde(skip_serializing_if = "Option::is_none")]
1025 pub inference_geo: Option<String>,
1026
1027 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1031 pub iterations: Vec<UsageIteration>,
1032
1033 #[serde(skip_serializing_if = "Option::is_none")]
1035 pub speed: Option<String>,
1036
1037 #[serde(skip_serializing_if = "Option::is_none")]
1040 pub output_tokens_details: Option<OutputTokensDetails>,
1041
1042 #[serde(default, skip_serializing_if = "Option::is_none")]
1045 pub fallback_credit: Option<Value>,
1046}
1047
1048#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1051pub struct OutputTokensDetails {
1052 #[serde(skip_serializing_if = "Option::is_none")]
1054 pub thinking_tokens: Option<u64>,
1055}
1056
1057#[derive(Debug, Clone, Serialize, Deserialize)]
1066pub struct UsageIteration {
1067 #[serde(default)]
1068 pub input_tokens: u32,
1069 #[serde(default)]
1070 pub output_tokens: u32,
1071 #[serde(default, skip_serializing_if = "Option::is_none")]
1073 pub cache_read_input_tokens: Option<u32>,
1074 #[serde(default, skip_serializing_if = "Option::is_none")]
1076 pub cache_creation_input_tokens: Option<u32>,
1077 #[serde(default, skip_serializing_if = "Option::is_none")]
1079 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
1080 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
1082 pub kind: Option<String>,
1083}
1084
1085#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1087pub struct ServerToolUse {
1088 #[serde(default)]
1089 pub web_search_requests: u32,
1090 #[serde(default)]
1092 pub web_fetch_requests: u32,
1093}
1094
1095#[cfg(test)]
1096mod tests {
1097 use super::*;
1098 use crate::io::ClaudeOutput;
1099
1100 #[test]
1101 fn test_deserialize_result_message() {
1102 let json = r#"{
1103 "type": "result",
1104 "subtype": "success",
1105 "is_error": false,
1106 "duration_ms": 100,
1107 "duration_api_ms": 200,
1108 "num_turns": 1,
1109 "result": "Done",
1110 "session_id": "123",
1111 "total_cost_usd": 0.01,
1112 "permission_denials": []
1113 }"#;
1114
1115 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1116 assert!(!output.is_error());
1117 }
1118
1119 #[test]
1120 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
1121 let json = r#"{
1122 "type": "result",
1123 "subtype": "error_max_budget_usd",
1124 "is_error": true,
1125 "duration_ms": 100,
1126 "duration_api_ms": 200,
1127 "num_turns": 1,
1128 "session_id": "123",
1129 "total_cost_usd": 0.01
1130 }"#;
1131
1132 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1133 let ClaudeOutput::Result(result) = output else {
1134 panic!("Expected Result");
1135 };
1136 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
1137
1138 let json = r#"{
1139 "type": "result",
1140 "subtype": "future_result_subtype",
1141 "is_error": true,
1142 "duration_ms": 100,
1143 "duration_api_ms": 200,
1144 "num_turns": 1,
1145 "session_id": "123",
1146 "total_cost_usd": 0.01
1147 }"#;
1148
1149 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1150 let ClaudeOutput::Result(result) = output else {
1151 panic!("Expected Result");
1152 };
1153 assert_eq!(
1154 result.subtype,
1155 ResultSubtype::Unknown("future_result_subtype".to_string())
1156 );
1157 }
1158
1159 #[test]
1160 fn test_deserialize_result_with_permission_denials() {
1161 let json = r#"{
1162 "type": "result",
1163 "subtype": "success",
1164 "is_error": false,
1165 "duration_ms": 100,
1166 "duration_api_ms": 200,
1167 "num_turns": 2,
1168 "result": "Done",
1169 "session_id": "123",
1170 "total_cost_usd": 0.01,
1171 "permission_denials": [
1172 {
1173 "tool_name": "Bash",
1174 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1175 "tool_use_id": "toolu_123"
1176 }
1177 ]
1178 }"#;
1179
1180 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1181 if let ClaudeOutput::Result(result) = output {
1182 assert_eq!(result.permission_denials.len(), 1);
1183 assert_eq!(result.permission_denials[0].tool_name, "Bash");
1184 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1185 assert_eq!(
1186 result.permission_denials[0]
1187 .tool_input
1188 .get("command")
1189 .unwrap(),
1190 "rm -rf /"
1191 );
1192 } else {
1193 panic!("Expected Result");
1194 }
1195 }
1196
1197 #[test]
1198 fn test_permission_denial_roundtrip() {
1199 let denial = PermissionDenial {
1200 tool_name: "Write".to_string(),
1201 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1202 tool_use_id: "toolu_456".to_string(),
1203 };
1204
1205 let json = serde_json::to_string(&denial).unwrap();
1206 assert!(json.contains("\"tool_name\":\"Write\""));
1207 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1208 assert!(json.contains("/etc/passwd"));
1209
1210 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1211 assert_eq!(parsed, denial);
1212 }
1213
1214 #[test]
1215 fn test_deserialize_result_message_with_errors() {
1216 let json = r#"{
1217 "type": "result",
1218 "subtype": "error_during_execution",
1219 "duration_ms": 0,
1220 "duration_api_ms": 0,
1221 "is_error": true,
1222 "num_turns": 0,
1223 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
1224 "total_cost_usd": 0,
1225 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
1226 "permission_denials": []
1227 }"#;
1228
1229 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1230 assert!(output.is_error());
1231
1232 if let ClaudeOutput::Result(res) = output {
1233 assert!(res.is_error);
1234 assert_eq!(res.errors.len(), 1);
1235 assert!(res.errors[0].contains("No conversation found"));
1236 } else {
1237 panic!("Expected Result message");
1238 }
1239 }
1240
1241 #[test]
1242 fn test_deserialize_result_message_errors_defaults_empty() {
1243 let json = r#"{
1244 "type": "result",
1245 "subtype": "success",
1246 "is_error": false,
1247 "duration_ms": 100,
1248 "duration_api_ms": 200,
1249 "num_turns": 1,
1250 "session_id": "123",
1251 "total_cost_usd": 0.01
1252 }"#;
1253
1254 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1255 if let ClaudeOutput::Result(res) = output {
1256 assert!(res.errors.is_empty());
1257 } else {
1258 panic!("Expected Result message");
1259 }
1260 }
1261
1262 #[test]
1263 fn test_result_message_errors_roundtrip() {
1264 let json = r#"{
1265 "type": "result",
1266 "subtype": "error_during_execution",
1267 "is_error": true,
1268 "duration_ms": 0,
1269 "duration_api_ms": 0,
1270 "num_turns": 0,
1271 "session_id": "test-session",
1272 "total_cost_usd": 0.0,
1273 "errors": ["Error 1", "Error 2"]
1274 }"#;
1275
1276 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1277 let reserialized = serde_json::to_string(&output).unwrap();
1278
1279 assert!(reserialized.contains("Error 1"));
1280 assert!(reserialized.contains("Error 2"));
1281 }
1282
1283 #[test]
1284 fn test_result_with_new_fields() {
1285 let json = r#"{
1286 "type": "result",
1287 "subtype": "success",
1288 "is_error": false,
1289 "duration_ms": 5000,
1290 "duration_api_ms": 4500,
1291 "num_turns": 1,
1292 "result": "Done",
1293 "session_id": "abc",
1294 "total_cost_usd": 0.06,
1295 "api_error_status": null,
1296 "stop_reason": "end_turn",
1297 "terminal_reason": "completed",
1298 "fast_mode_state": "off",
1299 "modelUsage": {
1300 "claude-opus-4-7[1m]": {
1301 "inputTokens": 3817,
1302 "outputTokens": 14,
1303 "costUSD": 0.06
1304 }
1305 },
1306 "usage": {
1307 "input_tokens": 3817,
1308 "output_tokens": 14,
1309 "cache_creation_input_tokens": 3540,
1310 "cache_read_input_tokens": 0,
1311 "server_tool_use": {
1312 "web_search_requests": 0,
1313 "web_fetch_requests": 2
1314 },
1315 "service_tier": "standard",
1316 "inference_geo": "not_available",
1317 "speed": "standard",
1318 "iterations": [
1319 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
1320 ]
1321 }
1322 }"#;
1323
1324 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1325 if let ClaudeOutput::Result(res) = output {
1326 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
1327 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
1328 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
1329 let model_usage = res.model_usage.as_ref().unwrap();
1330 let entry = model_usage
1331 .get("claude-opus-4-7[1m]")
1332 .expect("per-model entry present");
1333 assert_eq!(entry.input_tokens, 3817);
1334 assert_eq!(entry.output_tokens, 14);
1335 assert_eq!(entry.cost_usd, 0.06);
1336 assert!(res.api_error_status.is_none());
1337
1338 let usage = res.usage.unwrap();
1339 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
1340 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1341 assert_eq!(usage.speed.as_deref(), Some("standard"));
1342 assert_eq!(usage.iterations.len(), 1);
1343 assert_eq!(usage.iterations[0].input_tokens, 3817);
1344 assert_eq!(usage.iterations[0].output_tokens, 14);
1345 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
1346 } else {
1347 panic!("Expected Result");
1348 }
1349 }
1350
1351 #[test]
1352 fn test_result_backwards_compatible_without_new_fields() {
1353 let json = r#"{
1355 "type": "result",
1356 "subtype": "success",
1357 "is_error": false,
1358 "duration_ms": 100,
1359 "duration_api_ms": 200,
1360 "num_turns": 1,
1361 "session_id": "abc",
1362 "total_cost_usd": 0.01
1363 }"#;
1364
1365 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1366 if let ClaudeOutput::Result(res) = output {
1367 assert!(res.api_error_status.is_none());
1368 assert!(res.stop_reason.is_none());
1369 assert!(res.terminal_reason.is_none());
1370 assert!(res.fast_mode_state.is_none());
1371 assert!(res.model_usage.is_none());
1372 } else {
1373 panic!("Expected Result");
1374 }
1375 }
1376
1377 #[test]
1378 fn test_result_fast_mode_disabled_reason() {
1379 let json = r#"{
1380 "type":"result","subtype":"success","is_error":false,
1381 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1382 "session_id":"s1","total_cost_usd":0.01,
1383 "fast_mode_state":"off",
1384 "fast_mode_disabled_reason":"sdk_opt_in_required"
1385 }"#;
1386 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1387 let crate::ClaudeOutput::Result(res) = &output else {
1388 panic!("expected Result");
1389 };
1390 assert_eq!(
1391 res.fast_mode_disabled_reason,
1392 Some(FastModeDisabledReason::SdkOptInRequired)
1393 );
1394 assert!(serde_json::to_string(&output)
1395 .unwrap()
1396 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
1397
1398 assert_eq!(
1401 FastModeDisabledReason::from("unknown"),
1402 FastModeDisabledReason::UnknownReason
1403 );
1404 let novel = FastModeDisabledReason::from("solar_flare");
1405 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
1406 assert_eq!(novel.as_str(), "solar_flare");
1407 }
1408
1409 #[test]
1410 fn test_result_timing_and_user_message_uuid_fields() {
1411 let json = r#"{
1412 "type":"result","subtype":"success","is_error":false,
1413 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1414 "session_id":"s1","total_cost_usd":0.01,
1415 "request_sent_wall_ms":1753212345678.25,
1416 "user_message_uuid":"um-1"
1417 }"#;
1418 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1419 let crate::ClaudeOutput::Result(res) = &output else {
1420 panic!("expected Result");
1421 };
1422 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
1423 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
1424
1425 let reserialized = serde_json::to_string(&output).unwrap();
1426 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
1427 }
1428
1429 #[test]
1430 fn test_result_queued_turn_count_and_user_message_uuids() {
1431 let json = r#"{
1433 "type":"result","subtype":"success","is_error":false,
1434 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1435 "session_id":"s1","total_cost_usd":0.01,
1436 "user_message_uuid":"um-2",
1437 "user_message_uuids":["um-1","um-2"],
1438 "queued_turn_count":3
1439 }"#;
1440 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1441 let crate::ClaudeOutput::Result(res) = &output else {
1442 panic!("expected Result");
1443 };
1444 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
1445 assert_eq!(res.queued_turn_count, Some(3));
1446
1447 let reserialized = serde_json::to_string(&output).unwrap();
1448 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
1449 assert!(reserialized.contains("\"queued_turn_count\":3"));
1450
1451 let old = r#"{
1453 "type":"result","subtype":"success","is_error":false,
1454 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1455 "session_id":"s1","total_cost_usd":0.01
1456 }"#;
1457 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
1458 let crate::ClaudeOutput::Result(res) = &output else {
1459 panic!("expected Result");
1460 };
1461 assert!(res.user_message_uuids.is_empty());
1462 assert_eq!(res.queued_turn_count, None);
1463 let reserialized = serde_json::to_string(&output).unwrap();
1464 assert!(!reserialized.contains("user_message_uuids"));
1465 assert!(!reserialized.contains("queued_turn_count"));
1466 }
1467}