1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ResultMessage {
8 pub subtype: ResultSubtype,
9 pub is_error: bool,
10 pub duration_ms: u64,
11 pub duration_api_ms: u64,
12
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub ttft_ms: Option<u64>,
16
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub ttft_stream_ms: Option<u64>,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub time_to_request_ms: Option<u64>,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub time_to_request_from_spawn_ms: Option<u64>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub warm_spare_claimed: Option<bool>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub time_origin_ms: Option<u64>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
40 pub request_sent_wall_ms: Option<f64>,
41
42 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub first_content_frame_ms: Option<u64>,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub first_stream_post_ms: Option<u64>,
51
52 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub first_stream_post_ack_ms: Option<u64>,
56
57 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub first_stream_post_queue_wait_ms: Option<u64>,
61
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub first_stream_post_queued_behind: Option<StreamPostQueuedBehind>,
65
66 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub first_stream_post_wall_ms: Option<f64>,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub frame_received_wall_ms: Option<f64>,
83
84 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub frame_enqueued_wall_ms: Option<f64>,
89
90 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub frame_intake_phases_ms: Option<std::collections::BTreeMap<String, u64>>,
108
109 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub turn_started_wall_ms: Option<f64>,
117
118 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub first_text_post_ms: Option<u64>,
122
123 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub first_text_post_wall_ms: Option<f64>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub user_message_uuid: Option<String>,
131
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
139 pub user_message_uuids: Vec<String>,
140
141 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub resume_reason: Option<String>,
150
151 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub local_command: Option<String>,
156
157 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub queued_turn_count: Option<u64>,
164
165 pub num_turns: i32,
166
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub result: Option<String>,
169
170 #[serde(alias = "sessionId")]
171 pub session_id: String,
172 pub total_cost_usd: f64,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub usage: Option<UsageInfo>,
176
177 #[serde(default)]
179 pub permission_denials: Vec<PermissionDenial>,
180
181 #[serde(default)]
186 pub errors: Vec<String>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub uuid: Option<String>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub api_error_status: Option<u16>,
194
195 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub api_error_code: Option<String>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub stop_reason: Option<String>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub terminal_reason: Option<String>,
210
211 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub result_index: Option<u64>,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub fast_mode_state: Option<String>,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
229 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
230
231 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
233 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
239 pub subagent_stats: Option<SubagentStats>,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub structured_output: Option<Value>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub deferred_tool_use: Option<DeferredToolUse>,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub origin: Option<super::message_types::MessageOrigin>,
252
253 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub runner_exit: Option<RunnerExit>,
258
259 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub startup_failure_reason: Option<StartupFailureReason>,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Hash)]
274pub enum StartupFailureReason {
275 OrgPinApiKeyConflict,
278 OrgVerifyFailed,
281 OrgPinMismatch,
283 ManagedSettingsInvalid,
286 RemoteSettingsRequiredUnavailable,
288 GatewaySigninRequired,
290 GatewayAccessDenied,
292 ProxyInvalid,
294 TempDirUnusable,
296 CwdUnavailable,
298 ShellToolMissing,
301 SessionHeldByBackground,
304 WorktreeResumeRefused,
308 WorktreeUnverified,
311 CliVersionTooOld,
313 BypassRoot,
315 Unknown(String),
317}
318
319impl StartupFailureReason {
320 pub fn as_str(&self) -> &str {
321 match self {
322 Self::OrgPinApiKeyConflict => "org_pin_api_key_conflict",
323 Self::OrgVerifyFailed => "org_verify_failed",
324 Self::OrgPinMismatch => "org_pin_mismatch",
325 Self::ManagedSettingsInvalid => "managed_settings_invalid",
326 Self::RemoteSettingsRequiredUnavailable => "remote_settings_required_unavailable",
327 Self::GatewaySigninRequired => "gateway_signin_required",
328 Self::GatewayAccessDenied => "gateway_access_denied",
329 Self::ProxyInvalid => "proxy_invalid",
330 Self::TempDirUnusable => "temp_dir_unusable",
331 Self::CwdUnavailable => "cwd_unavailable",
332 Self::ShellToolMissing => "shell_tool_missing",
333 Self::SessionHeldByBackground => "session_held_by_background",
334 Self::WorktreeResumeRefused => "worktree_resume_refused",
335 Self::WorktreeUnverified => "worktree_unverified",
336 Self::CliVersionTooOld => "cli_version_too_old",
337 Self::BypassRoot => "bypass_root",
338 Self::Unknown(s) => s.as_str(),
339 }
340 }
341}
342
343impl fmt::Display for StartupFailureReason {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 f.write_str(self.as_str())
346 }
347}
348
349impl From<&str> for StartupFailureReason {
350 fn from(s: &str) -> Self {
351 match s {
352 "org_pin_api_key_conflict" => Self::OrgPinApiKeyConflict,
353 "org_verify_failed" => Self::OrgVerifyFailed,
354 "org_pin_mismatch" => Self::OrgPinMismatch,
355 "managed_settings_invalid" => Self::ManagedSettingsInvalid,
356 "remote_settings_required_unavailable" => Self::RemoteSettingsRequiredUnavailable,
357 "gateway_signin_required" => Self::GatewaySigninRequired,
358 "gateway_access_denied" => Self::GatewayAccessDenied,
359 "proxy_invalid" => Self::ProxyInvalid,
360 "temp_dir_unusable" => Self::TempDirUnusable,
361 "cwd_unavailable" => Self::CwdUnavailable,
362 "shell_tool_missing" => Self::ShellToolMissing,
363 "session_held_by_background" => Self::SessionHeldByBackground,
364 "worktree_resume_refused" => Self::WorktreeResumeRefused,
365 "worktree_unverified" => Self::WorktreeUnverified,
366 "cli_version_too_old" => Self::CliVersionTooOld,
367 "bypass_root" => Self::BypassRoot,
368 other => Self::Unknown(other.to_string()),
369 }
370 }
371}
372
373impl Serialize for StartupFailureReason {
374 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
375 serializer.serialize_str(self.as_str())
376 }
377}
378
379impl<'de> Deserialize<'de> for StartupFailureReason {
380 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
381 let s = String::deserialize(deserializer)?;
382 Ok(Self::from(s.as_str()))
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
390pub struct RunnerExit {
391 pub phase: RunnerExitPhase,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub exit_code: Option<i64>,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub signal: Option<String>,
401}
402
403#[derive(Debug, Clone, PartialEq, Eq, Hash)]
407pub enum RunnerExitPhase {
408 Setup,
410 Run,
412 Unknown(String),
414}
415
416impl RunnerExitPhase {
417 pub fn as_str(&self) -> &str {
418 match self {
419 Self::Setup => "setup",
420 Self::Run => "run",
421 Self::Unknown(s) => s.as_str(),
422 }
423 }
424}
425
426impl std::fmt::Display for RunnerExitPhase {
427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428 f.write_str(self.as_str())
429 }
430}
431
432impl From<&str> for RunnerExitPhase {
433 fn from(s: &str) -> Self {
434 match s {
435 "setup" => Self::Setup,
436 "run" => Self::Run,
437 other => Self::Unknown(other.to_string()),
438 }
439 }
440}
441
442impl Serialize for RunnerExitPhase {
443 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
444 serializer.serialize_str(self.as_str())
445 }
446}
447
448impl<'de> Deserialize<'de> for RunnerExitPhase {
449 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
450 let s = String::deserialize(deserializer)?;
451 Ok(Self::from(s.as_str()))
452 }
453}
454
455#[derive(Debug, Clone, Default, Serialize, Deserialize)]
461#[serde(rename_all = "camelCase")]
462pub struct ModelUsageEntry {
463 #[serde(default)]
464 pub input_tokens: u64,
465 #[serde(default)]
466 pub output_tokens: u64,
467 #[serde(default)]
468 pub cache_read_input_tokens: u64,
469 #[serde(default)]
470 pub cache_creation_input_tokens: u64,
471 #[serde(default, rename = "costUSD")]
472 pub cost_usd: f64,
473 #[serde(default)]
474 pub web_search_requests: u32,
475 #[serde(default)]
476 pub context_window: u64,
477 #[serde(default)]
478 pub max_output_tokens: u64,
479 #[serde(flatten)]
480 pub extra: serde_json::Map<String, serde_json::Value>,
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
485pub struct DeferredToolUse {
486 pub id: String,
487 pub name: String,
488 pub input: Value,
489}
490
491#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
500pub struct SubagentStats {
501 pub spawned: u64,
503 pub requested: SubagentSpawnRequests,
506 pub started_in_background: u64,
509 #[serde(default)]
511 pub by_type: std::collections::BTreeMap<String, u64>,
512 pub max_depth: u64,
515 pub spawned_by_subagents: u64,
517 pub completed: u64,
518 pub failed: u64,
519 pub killed: SubagentKillCounts,
521 pub refused: SubagentRefusalCounts,
524}
525
526#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
529pub struct SubagentSpawnRequests {
530 pub background: u64,
531 pub foreground: u64,
532 pub unset: u64,
533}
534
535#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
539pub struct SubagentKillCounts {
540 pub parent: u64,
541 pub user: u64,
542 pub system: u64,
543}
544
545#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
547pub struct SubagentRefusalCounts {
548 pub depth_limit: u64,
549 pub concurrency_limit: u64,
550 pub budget: u64,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
558pub struct PermissionDenial {
559 pub tool_name: String,
561
562 pub tool_input: Value,
564
565 pub tool_use_id: String,
567}
568
569#[derive(Debug, Clone, PartialEq, Eq, Hash)]
573pub enum StreamPostQueuedBehind {
574 DurablePost,
576 EphemeralPost,
578 RetryBackoff,
580 Hold,
582 None,
584 Unknown(String),
586}
587
588impl StreamPostQueuedBehind {
589 pub fn as_str(&self) -> &str {
590 match self {
591 Self::DurablePost => "durable_post",
592 Self::EphemeralPost => "ephemeral_post",
593 Self::RetryBackoff => "retry_backoff",
594 Self::Hold => "hold",
595 Self::None => "none",
596 Self::Unknown(s) => s.as_str(),
597 }
598 }
599}
600
601impl fmt::Display for StreamPostQueuedBehind {
602 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
603 f.write_str(self.as_str())
604 }
605}
606
607impl From<&str> for StreamPostQueuedBehind {
608 fn from(s: &str) -> Self {
609 match s {
610 "durable_post" => Self::DurablePost,
611 "ephemeral_post" => Self::EphemeralPost,
612 "retry_backoff" => Self::RetryBackoff,
613 "hold" => Self::Hold,
614 "none" => Self::None,
615 other => Self::Unknown(other.to_string()),
616 }
617 }
618}
619
620impl Serialize for StreamPostQueuedBehind {
621 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
622 serializer.serialize_str(self.as_str())
623 }
624}
625
626impl<'de> Deserialize<'de> for StreamPostQueuedBehind {
627 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
628 let s = String::deserialize(deserializer)?;
629 Ok(Self::from(s.as_str()))
630 }
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, Hash)]
636pub enum FastModeDisabledReason {
637 Free,
639 Preference,
641 ExtraUsageDisabled,
643 NetworkError,
645 UnknownReason,
647 NotFirstParty,
649 DisabledByEnv,
651 ModelNotAllowed,
653 SdkOptInRequired,
655 Pending,
657 Unknown(String),
659}
660
661impl FastModeDisabledReason {
662 pub fn as_str(&self) -> &str {
663 match self {
664 Self::Free => "free",
665 Self::Preference => "preference",
666 Self::ExtraUsageDisabled => "extra_usage_disabled",
667 Self::NetworkError => "network_error",
668 Self::UnknownReason => "unknown",
669 Self::NotFirstParty => "not_first_party",
670 Self::DisabledByEnv => "disabled_by_env",
671 Self::ModelNotAllowed => "model_not_allowed",
672 Self::SdkOptInRequired => "sdk_opt_in_required",
673 Self::Pending => "pending",
674 Self::Unknown(s) => s.as_str(),
675 }
676 }
677}
678
679impl fmt::Display for FastModeDisabledReason {
680 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
681 f.write_str(self.as_str())
682 }
683}
684
685impl From<&str> for FastModeDisabledReason {
686 fn from(s: &str) -> Self {
687 match s {
688 "free" => Self::Free,
689 "preference" => Self::Preference,
690 "extra_usage_disabled" => Self::ExtraUsageDisabled,
691 "network_error" => Self::NetworkError,
692 "unknown" => Self::UnknownReason,
693 "not_first_party" => Self::NotFirstParty,
694 "disabled_by_env" => Self::DisabledByEnv,
695 "model_not_allowed" => Self::ModelNotAllowed,
696 "sdk_opt_in_required" => Self::SdkOptInRequired,
697 "pending" => Self::Pending,
698 other => Self::Unknown(other.to_string()),
699 }
700 }
701}
702
703impl Serialize for FastModeDisabledReason {
704 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
705 serializer.serialize_str(self.as_str())
706 }
707}
708
709impl<'de> Deserialize<'de> for FastModeDisabledReason {
710 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
711 let s = String::deserialize(deserializer)?;
712 Ok(Self::from(s.as_str()))
713 }
714}
715
716#[derive(Debug, Clone, PartialEq, Eq, Hash)]
718pub enum ResultSubtype {
719 Success,
720 ErrorMaxTurns,
721 ErrorDuringExecution,
722 ErrorMaxBudgetUsd,
723 ErrorMaxStructuredOutputRetries,
724 Unknown(String),
725}
726
727impl ResultSubtype {
728 pub fn as_str(&self) -> &str {
729 match self {
730 Self::Success => "success",
731 Self::ErrorMaxTurns => "error_max_turns",
732 Self::ErrorDuringExecution => "error_during_execution",
733 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
734 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
735 Self::Unknown(s) => s.as_str(),
736 }
737 }
738}
739
740impl fmt::Display for ResultSubtype {
741 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742 f.write_str(self.as_str())
743 }
744}
745
746impl From<&str> for ResultSubtype {
747 fn from(s: &str) -> Self {
748 match s {
749 "success" => Self::Success,
750 "error_max_turns" => Self::ErrorMaxTurns,
751 "error_during_execution" => Self::ErrorDuringExecution,
752 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
753 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
754 other => Self::Unknown(other.to_string()),
755 }
756 }
757}
758
759impl Serialize for ResultSubtype {
760 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
761 serializer.serialize_str(self.as_str())
762 }
763}
764
765impl<'de> Deserialize<'de> for ResultSubtype {
766 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
767 let s = String::deserialize(deserializer)?;
768 Ok(Self::from(s.as_str()))
769 }
770}
771
772#[derive(Debug, Clone, Serialize, Deserialize)]
790pub struct UsageInfo {
791 #[serde(default)]
793 pub input_tokens: u32,
794 #[serde(default)]
796 pub cache_creation_input_tokens: u32,
797 #[serde(default)]
801 pub cache_read_input_tokens: u32,
802 #[serde(default)]
804 pub output_tokens: u32,
805 #[serde(default)]
806 pub server_tool_use: ServerToolUse,
807 #[serde(default)]
808 pub service_tier: String,
809
810 #[serde(skip_serializing_if = "Option::is_none")]
812 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
813
814 #[serde(skip_serializing_if = "Option::is_none")]
816 pub inference_geo: Option<String>,
817
818 #[serde(default, skip_serializing_if = "Vec::is_empty")]
822 pub iterations: Vec<UsageIteration>,
823
824 #[serde(skip_serializing_if = "Option::is_none")]
826 pub speed: Option<String>,
827
828 #[serde(skip_serializing_if = "Option::is_none")]
831 pub output_tokens_details: Option<OutputTokensDetails>,
832}
833
834#[derive(Debug, Clone, Serialize, Deserialize, Default)]
837pub struct OutputTokensDetails {
838 #[serde(skip_serializing_if = "Option::is_none")]
840 pub thinking_tokens: Option<u64>,
841}
842
843#[derive(Debug, Clone, Serialize, Deserialize)]
852pub struct UsageIteration {
853 #[serde(default)]
854 pub input_tokens: u32,
855 #[serde(default)]
856 pub output_tokens: u32,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
859 pub cache_read_input_tokens: Option<u32>,
860 #[serde(default, skip_serializing_if = "Option::is_none")]
862 pub cache_creation_input_tokens: Option<u32>,
863 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
866 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
868 pub kind: Option<String>,
869}
870
871#[derive(Debug, Clone, Default, Serialize, Deserialize)]
873pub struct ServerToolUse {
874 #[serde(default)]
875 pub web_search_requests: u32,
876 #[serde(default)]
878 pub web_fetch_requests: u32,
879}
880
881#[cfg(test)]
882mod tests {
883 use super::*;
884 use crate::io::ClaudeOutput;
885
886 #[test]
887 fn test_deserialize_result_message() {
888 let json = r#"{
889 "type": "result",
890 "subtype": "success",
891 "is_error": false,
892 "duration_ms": 100,
893 "duration_api_ms": 200,
894 "num_turns": 1,
895 "result": "Done",
896 "session_id": "123",
897 "total_cost_usd": 0.01,
898 "permission_denials": []
899 }"#;
900
901 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
902 assert!(!output.is_error());
903 }
904
905 #[test]
906 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
907 let json = r#"{
908 "type": "result",
909 "subtype": "error_max_budget_usd",
910 "is_error": true,
911 "duration_ms": 100,
912 "duration_api_ms": 200,
913 "num_turns": 1,
914 "session_id": "123",
915 "total_cost_usd": 0.01
916 }"#;
917
918 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
919 let ClaudeOutput::Result(result) = output else {
920 panic!("Expected Result");
921 };
922 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
923
924 let json = r#"{
925 "type": "result",
926 "subtype": "future_result_subtype",
927 "is_error": true,
928 "duration_ms": 100,
929 "duration_api_ms": 200,
930 "num_turns": 1,
931 "session_id": "123",
932 "total_cost_usd": 0.01
933 }"#;
934
935 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
936 let ClaudeOutput::Result(result) = output else {
937 panic!("Expected Result");
938 };
939 assert_eq!(
940 result.subtype,
941 ResultSubtype::Unknown("future_result_subtype".to_string())
942 );
943 }
944
945 #[test]
946 fn test_deserialize_result_with_permission_denials() {
947 let json = r#"{
948 "type": "result",
949 "subtype": "success",
950 "is_error": false,
951 "duration_ms": 100,
952 "duration_api_ms": 200,
953 "num_turns": 2,
954 "result": "Done",
955 "session_id": "123",
956 "total_cost_usd": 0.01,
957 "permission_denials": [
958 {
959 "tool_name": "Bash",
960 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
961 "tool_use_id": "toolu_123"
962 }
963 ]
964 }"#;
965
966 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
967 if let ClaudeOutput::Result(result) = output {
968 assert_eq!(result.permission_denials.len(), 1);
969 assert_eq!(result.permission_denials[0].tool_name, "Bash");
970 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
971 assert_eq!(
972 result.permission_denials[0]
973 .tool_input
974 .get("command")
975 .unwrap(),
976 "rm -rf /"
977 );
978 } else {
979 panic!("Expected Result");
980 }
981 }
982
983 #[test]
984 fn test_permission_denial_roundtrip() {
985 let denial = PermissionDenial {
986 tool_name: "Write".to_string(),
987 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
988 tool_use_id: "toolu_456".to_string(),
989 };
990
991 let json = serde_json::to_string(&denial).unwrap();
992 assert!(json.contains("\"tool_name\":\"Write\""));
993 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
994 assert!(json.contains("/etc/passwd"));
995
996 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
997 assert_eq!(parsed, denial);
998 }
999
1000 #[test]
1001 fn test_deserialize_result_message_with_errors() {
1002 let json = r#"{
1003 "type": "result",
1004 "subtype": "error_during_execution",
1005 "duration_ms": 0,
1006 "duration_api_ms": 0,
1007 "is_error": true,
1008 "num_turns": 0,
1009 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
1010 "total_cost_usd": 0,
1011 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
1012 "permission_denials": []
1013 }"#;
1014
1015 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1016 assert!(output.is_error());
1017
1018 if let ClaudeOutput::Result(res) = output {
1019 assert!(res.is_error);
1020 assert_eq!(res.errors.len(), 1);
1021 assert!(res.errors[0].contains("No conversation found"));
1022 } else {
1023 panic!("Expected Result message");
1024 }
1025 }
1026
1027 #[test]
1028 fn test_deserialize_result_message_errors_defaults_empty() {
1029 let json = r#"{
1030 "type": "result",
1031 "subtype": "success",
1032 "is_error": false,
1033 "duration_ms": 100,
1034 "duration_api_ms": 200,
1035 "num_turns": 1,
1036 "session_id": "123",
1037 "total_cost_usd": 0.01
1038 }"#;
1039
1040 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1041 if let ClaudeOutput::Result(res) = output {
1042 assert!(res.errors.is_empty());
1043 } else {
1044 panic!("Expected Result message");
1045 }
1046 }
1047
1048 #[test]
1049 fn test_result_message_errors_roundtrip() {
1050 let json = r#"{
1051 "type": "result",
1052 "subtype": "error_during_execution",
1053 "is_error": true,
1054 "duration_ms": 0,
1055 "duration_api_ms": 0,
1056 "num_turns": 0,
1057 "session_id": "test-session",
1058 "total_cost_usd": 0.0,
1059 "errors": ["Error 1", "Error 2"]
1060 }"#;
1061
1062 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1063 let reserialized = serde_json::to_string(&output).unwrap();
1064
1065 assert!(reserialized.contains("Error 1"));
1066 assert!(reserialized.contains("Error 2"));
1067 }
1068
1069 #[test]
1070 fn test_result_with_new_fields() {
1071 let json = r#"{
1072 "type": "result",
1073 "subtype": "success",
1074 "is_error": false,
1075 "duration_ms": 5000,
1076 "duration_api_ms": 4500,
1077 "num_turns": 1,
1078 "result": "Done",
1079 "session_id": "abc",
1080 "total_cost_usd": 0.06,
1081 "api_error_status": null,
1082 "stop_reason": "end_turn",
1083 "terminal_reason": "completed",
1084 "fast_mode_state": "off",
1085 "modelUsage": {
1086 "claude-opus-4-7[1m]": {
1087 "inputTokens": 3817,
1088 "outputTokens": 14,
1089 "costUSD": 0.06
1090 }
1091 },
1092 "usage": {
1093 "input_tokens": 3817,
1094 "output_tokens": 14,
1095 "cache_creation_input_tokens": 3540,
1096 "cache_read_input_tokens": 0,
1097 "server_tool_use": {
1098 "web_search_requests": 0,
1099 "web_fetch_requests": 2
1100 },
1101 "service_tier": "standard",
1102 "inference_geo": "not_available",
1103 "speed": "standard",
1104 "iterations": [
1105 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
1106 ]
1107 }
1108 }"#;
1109
1110 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1111 if let ClaudeOutput::Result(res) = output {
1112 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
1113 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
1114 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
1115 let model_usage = res.model_usage.as_ref().unwrap();
1116 let entry = model_usage
1117 .get("claude-opus-4-7[1m]")
1118 .expect("per-model entry present");
1119 assert_eq!(entry.input_tokens, 3817);
1120 assert_eq!(entry.output_tokens, 14);
1121 assert_eq!(entry.cost_usd, 0.06);
1122 assert!(res.api_error_status.is_none());
1123
1124 let usage = res.usage.unwrap();
1125 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
1126 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1127 assert_eq!(usage.speed.as_deref(), Some("standard"));
1128 assert_eq!(usage.iterations.len(), 1);
1129 assert_eq!(usage.iterations[0].input_tokens, 3817);
1130 assert_eq!(usage.iterations[0].output_tokens, 14);
1131 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
1132 } else {
1133 panic!("Expected Result");
1134 }
1135 }
1136
1137 #[test]
1138 fn test_result_backwards_compatible_without_new_fields() {
1139 let json = r#"{
1141 "type": "result",
1142 "subtype": "success",
1143 "is_error": false,
1144 "duration_ms": 100,
1145 "duration_api_ms": 200,
1146 "num_turns": 1,
1147 "session_id": "abc",
1148 "total_cost_usd": 0.01
1149 }"#;
1150
1151 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1152 if let ClaudeOutput::Result(res) = output {
1153 assert!(res.api_error_status.is_none());
1154 assert!(res.stop_reason.is_none());
1155 assert!(res.terminal_reason.is_none());
1156 assert!(res.fast_mode_state.is_none());
1157 assert!(res.model_usage.is_none());
1158 } else {
1159 panic!("Expected Result");
1160 }
1161 }
1162
1163 #[test]
1164 fn test_result_fast_mode_disabled_reason() {
1165 let json = r#"{
1166 "type":"result","subtype":"success","is_error":false,
1167 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1168 "session_id":"s1","total_cost_usd":0.01,
1169 "fast_mode_state":"off",
1170 "fast_mode_disabled_reason":"sdk_opt_in_required"
1171 }"#;
1172 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1173 let crate::ClaudeOutput::Result(res) = &output else {
1174 panic!("expected Result");
1175 };
1176 assert_eq!(
1177 res.fast_mode_disabled_reason,
1178 Some(FastModeDisabledReason::SdkOptInRequired)
1179 );
1180 assert!(serde_json::to_string(&output)
1181 .unwrap()
1182 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
1183
1184 assert_eq!(
1187 FastModeDisabledReason::from("unknown"),
1188 FastModeDisabledReason::UnknownReason
1189 );
1190 let novel = FastModeDisabledReason::from("solar_flare");
1191 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
1192 assert_eq!(novel.as_str(), "solar_flare");
1193 }
1194
1195 #[test]
1196 fn test_result_timing_and_user_message_uuid_fields() {
1197 let json = r#"{
1198 "type":"result","subtype":"success","is_error":false,
1199 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1200 "session_id":"s1","total_cost_usd":0.01,
1201 "request_sent_wall_ms":1753212345678.25,
1202 "user_message_uuid":"um-1"
1203 }"#;
1204 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1205 let crate::ClaudeOutput::Result(res) = &output else {
1206 panic!("expected Result");
1207 };
1208 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
1209 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
1210
1211 let reserialized = serde_json::to_string(&output).unwrap();
1212 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
1213 }
1214
1215 #[test]
1216 fn test_result_queued_turn_count_and_user_message_uuids() {
1217 let json = r#"{
1219 "type":"result","subtype":"success","is_error":false,
1220 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1221 "session_id":"s1","total_cost_usd":0.01,
1222 "user_message_uuid":"um-2",
1223 "user_message_uuids":["um-1","um-2"],
1224 "queued_turn_count":3
1225 }"#;
1226 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1227 let crate::ClaudeOutput::Result(res) = &output else {
1228 panic!("expected Result");
1229 };
1230 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
1231 assert_eq!(res.queued_turn_count, Some(3));
1232
1233 let reserialized = serde_json::to_string(&output).unwrap();
1234 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
1235 assert!(reserialized.contains("\"queued_turn_count\":3"));
1236
1237 let old = r#"{
1239 "type":"result","subtype":"success","is_error":false,
1240 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1241 "session_id":"s1","total_cost_usd":0.01
1242 }"#;
1243 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
1244 let crate::ClaudeOutput::Result(res) = &output else {
1245 panic!("expected Result");
1246 };
1247 assert!(res.user_message_uuids.is_empty());
1248 assert_eq!(res.queued_turn_count, None);
1249 let reserialized = serde_json::to_string(&output).unwrap();
1250 assert!(!reserialized.contains("user_message_uuids"));
1251 assert!(!reserialized.contains("queued_turn_count"));
1252 }
1253}