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")]
97 pub turn_started_wall_ms: Option<f64>,
98
99 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub first_text_post_ms: Option<u64>,
103
104 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub first_text_post_wall_ms: Option<f64>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub user_message_uuid: Option<String>,
112
113 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub user_message_uuids: Vec<String>,
121
122 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub resume_reason: Option<String>,
131
132 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub local_command: Option<String>,
137
138 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub queued_turn_count: Option<u64>,
145
146 pub num_turns: i32,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub result: Option<String>,
150
151 #[serde(alias = "sessionId")]
152 pub session_id: String,
153 pub total_cost_usd: f64,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub usage: Option<UsageInfo>,
157
158 #[serde(default)]
160 pub permission_denials: Vec<PermissionDenial>,
161
162 #[serde(default)]
167 pub errors: Vec<String>,
168
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub uuid: Option<String>,
171
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub api_error_status: Option<u16>,
175
176 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub api_error_code: Option<String>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub stop_reason: Option<String>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub terminal_reason: Option<String>,
191
192 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub result_index: Option<u64>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub fast_mode_state: Option<String>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
210 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
211
212 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
214 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
215
216 #[serde(skip_serializing_if = "Option::is_none")]
220 pub subagent_stats: Option<SubagentStats>,
221
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub structured_output: Option<Value>,
225
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub deferred_tool_use: Option<DeferredToolUse>,
229
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub origin: Option<super::message_types::MessageOrigin>,
233
234 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub runner_exit: Option<RunnerExit>,
239
240 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub startup_failure_reason: Option<StartupFailureReason>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Hash)]
255pub enum StartupFailureReason {
256 OrgPinApiKeyConflict,
259 OrgVerifyFailed,
262 OrgPinMismatch,
264 ManagedSettingsInvalid,
267 RemoteSettingsRequiredUnavailable,
269 GatewaySigninRequired,
271 GatewayAccessDenied,
273 ProxyInvalid,
275 TempDirUnusable,
277 CwdUnavailable,
279 ShellToolMissing,
282 SessionHeldByBackground,
285 WorktreeResumeRefused,
289 WorktreeUnverified,
292 CliVersionTooOld,
294 BypassRoot,
296 Unknown(String),
298}
299
300impl StartupFailureReason {
301 pub fn as_str(&self) -> &str {
302 match self {
303 Self::OrgPinApiKeyConflict => "org_pin_api_key_conflict",
304 Self::OrgVerifyFailed => "org_verify_failed",
305 Self::OrgPinMismatch => "org_pin_mismatch",
306 Self::ManagedSettingsInvalid => "managed_settings_invalid",
307 Self::RemoteSettingsRequiredUnavailable => "remote_settings_required_unavailable",
308 Self::GatewaySigninRequired => "gateway_signin_required",
309 Self::GatewayAccessDenied => "gateway_access_denied",
310 Self::ProxyInvalid => "proxy_invalid",
311 Self::TempDirUnusable => "temp_dir_unusable",
312 Self::CwdUnavailable => "cwd_unavailable",
313 Self::ShellToolMissing => "shell_tool_missing",
314 Self::SessionHeldByBackground => "session_held_by_background",
315 Self::WorktreeResumeRefused => "worktree_resume_refused",
316 Self::WorktreeUnverified => "worktree_unverified",
317 Self::CliVersionTooOld => "cli_version_too_old",
318 Self::BypassRoot => "bypass_root",
319 Self::Unknown(s) => s.as_str(),
320 }
321 }
322}
323
324impl fmt::Display for StartupFailureReason {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 f.write_str(self.as_str())
327 }
328}
329
330impl From<&str> for StartupFailureReason {
331 fn from(s: &str) -> Self {
332 match s {
333 "org_pin_api_key_conflict" => Self::OrgPinApiKeyConflict,
334 "org_verify_failed" => Self::OrgVerifyFailed,
335 "org_pin_mismatch" => Self::OrgPinMismatch,
336 "managed_settings_invalid" => Self::ManagedSettingsInvalid,
337 "remote_settings_required_unavailable" => Self::RemoteSettingsRequiredUnavailable,
338 "gateway_signin_required" => Self::GatewaySigninRequired,
339 "gateway_access_denied" => Self::GatewayAccessDenied,
340 "proxy_invalid" => Self::ProxyInvalid,
341 "temp_dir_unusable" => Self::TempDirUnusable,
342 "cwd_unavailable" => Self::CwdUnavailable,
343 "shell_tool_missing" => Self::ShellToolMissing,
344 "session_held_by_background" => Self::SessionHeldByBackground,
345 "worktree_resume_refused" => Self::WorktreeResumeRefused,
346 "worktree_unverified" => Self::WorktreeUnverified,
347 "cli_version_too_old" => Self::CliVersionTooOld,
348 "bypass_root" => Self::BypassRoot,
349 other => Self::Unknown(other.to_string()),
350 }
351 }
352}
353
354impl Serialize for StartupFailureReason {
355 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
356 serializer.serialize_str(self.as_str())
357 }
358}
359
360impl<'de> Deserialize<'de> for StartupFailureReason {
361 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
362 let s = String::deserialize(deserializer)?;
363 Ok(Self::from(s.as_str()))
364 }
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
371pub struct RunnerExit {
372 pub phase: RunnerExitPhase,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub exit_code: Option<i64>,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub signal: Option<String>,
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, Hash)]
388pub enum RunnerExitPhase {
389 Setup,
391 Run,
393 Unknown(String),
395}
396
397impl RunnerExitPhase {
398 pub fn as_str(&self) -> &str {
399 match self {
400 Self::Setup => "setup",
401 Self::Run => "run",
402 Self::Unknown(s) => s.as_str(),
403 }
404 }
405}
406
407impl std::fmt::Display for RunnerExitPhase {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 f.write_str(self.as_str())
410 }
411}
412
413impl From<&str> for RunnerExitPhase {
414 fn from(s: &str) -> Self {
415 match s {
416 "setup" => Self::Setup,
417 "run" => Self::Run,
418 other => Self::Unknown(other.to_string()),
419 }
420 }
421}
422
423impl Serialize for RunnerExitPhase {
424 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
425 serializer.serialize_str(self.as_str())
426 }
427}
428
429impl<'de> Deserialize<'de> for RunnerExitPhase {
430 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
431 let s = String::deserialize(deserializer)?;
432 Ok(Self::from(s.as_str()))
433 }
434}
435
436#[derive(Debug, Clone, Default, Serialize, Deserialize)]
442#[serde(rename_all = "camelCase")]
443pub struct ModelUsageEntry {
444 #[serde(default)]
445 pub input_tokens: u64,
446 #[serde(default)]
447 pub output_tokens: u64,
448 #[serde(default)]
449 pub cache_read_input_tokens: u64,
450 #[serde(default)]
451 pub cache_creation_input_tokens: u64,
452 #[serde(default, rename = "costUSD")]
453 pub cost_usd: f64,
454 #[serde(default)]
455 pub web_search_requests: u32,
456 #[serde(default)]
457 pub context_window: u64,
458 #[serde(default)]
459 pub max_output_tokens: u64,
460 #[serde(flatten)]
461 pub extra: serde_json::Map<String, serde_json::Value>,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
466pub struct DeferredToolUse {
467 pub id: String,
468 pub name: String,
469 pub input: Value,
470}
471
472#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
481pub struct SubagentStats {
482 pub spawned: u64,
484 pub requested: SubagentSpawnRequests,
487 pub started_in_background: u64,
490 #[serde(default)]
492 pub by_type: std::collections::BTreeMap<String, u64>,
493 pub max_depth: u64,
496 pub spawned_by_subagents: u64,
498 pub completed: u64,
499 pub failed: u64,
500 pub killed: SubagentKillCounts,
502 pub refused: SubagentRefusalCounts,
505}
506
507#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
510pub struct SubagentSpawnRequests {
511 pub background: u64,
512 pub foreground: u64,
513 pub unset: u64,
514}
515
516#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
520pub struct SubagentKillCounts {
521 pub parent: u64,
522 pub user: u64,
523 pub system: u64,
524}
525
526#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
528pub struct SubagentRefusalCounts {
529 pub depth_limit: u64,
530 pub concurrency_limit: u64,
531 pub budget: u64,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
539pub struct PermissionDenial {
540 pub tool_name: String,
542
543 pub tool_input: Value,
545
546 pub tool_use_id: String,
548}
549
550#[derive(Debug, Clone, PartialEq, Eq, Hash)]
554pub enum StreamPostQueuedBehind {
555 DurablePost,
557 EphemeralPost,
559 RetryBackoff,
561 Hold,
563 None,
565 Unknown(String),
567}
568
569impl StreamPostQueuedBehind {
570 pub fn as_str(&self) -> &str {
571 match self {
572 Self::DurablePost => "durable_post",
573 Self::EphemeralPost => "ephemeral_post",
574 Self::RetryBackoff => "retry_backoff",
575 Self::Hold => "hold",
576 Self::None => "none",
577 Self::Unknown(s) => s.as_str(),
578 }
579 }
580}
581
582impl fmt::Display for StreamPostQueuedBehind {
583 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
584 f.write_str(self.as_str())
585 }
586}
587
588impl From<&str> for StreamPostQueuedBehind {
589 fn from(s: &str) -> Self {
590 match s {
591 "durable_post" => Self::DurablePost,
592 "ephemeral_post" => Self::EphemeralPost,
593 "retry_backoff" => Self::RetryBackoff,
594 "hold" => Self::Hold,
595 "none" => Self::None,
596 other => Self::Unknown(other.to_string()),
597 }
598 }
599}
600
601impl Serialize for StreamPostQueuedBehind {
602 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
603 serializer.serialize_str(self.as_str())
604 }
605}
606
607impl<'de> Deserialize<'de> for StreamPostQueuedBehind {
608 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
609 let s = String::deserialize(deserializer)?;
610 Ok(Self::from(s.as_str()))
611 }
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, Hash)]
617pub enum FastModeDisabledReason {
618 Free,
620 Preference,
622 ExtraUsageDisabled,
624 NetworkError,
626 UnknownReason,
628 NotFirstParty,
630 DisabledByEnv,
632 ModelNotAllowed,
634 SdkOptInRequired,
636 Pending,
638 Unknown(String),
640}
641
642impl FastModeDisabledReason {
643 pub fn as_str(&self) -> &str {
644 match self {
645 Self::Free => "free",
646 Self::Preference => "preference",
647 Self::ExtraUsageDisabled => "extra_usage_disabled",
648 Self::NetworkError => "network_error",
649 Self::UnknownReason => "unknown",
650 Self::NotFirstParty => "not_first_party",
651 Self::DisabledByEnv => "disabled_by_env",
652 Self::ModelNotAllowed => "model_not_allowed",
653 Self::SdkOptInRequired => "sdk_opt_in_required",
654 Self::Pending => "pending",
655 Self::Unknown(s) => s.as_str(),
656 }
657 }
658}
659
660impl fmt::Display for FastModeDisabledReason {
661 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
662 f.write_str(self.as_str())
663 }
664}
665
666impl From<&str> for FastModeDisabledReason {
667 fn from(s: &str) -> Self {
668 match s {
669 "free" => Self::Free,
670 "preference" => Self::Preference,
671 "extra_usage_disabled" => Self::ExtraUsageDisabled,
672 "network_error" => Self::NetworkError,
673 "unknown" => Self::UnknownReason,
674 "not_first_party" => Self::NotFirstParty,
675 "disabled_by_env" => Self::DisabledByEnv,
676 "model_not_allowed" => Self::ModelNotAllowed,
677 "sdk_opt_in_required" => Self::SdkOptInRequired,
678 "pending" => Self::Pending,
679 other => Self::Unknown(other.to_string()),
680 }
681 }
682}
683
684impl Serialize for FastModeDisabledReason {
685 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
686 serializer.serialize_str(self.as_str())
687 }
688}
689
690impl<'de> Deserialize<'de> for FastModeDisabledReason {
691 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
692 let s = String::deserialize(deserializer)?;
693 Ok(Self::from(s.as_str()))
694 }
695}
696
697#[derive(Debug, Clone, PartialEq, Eq, Hash)]
699pub enum ResultSubtype {
700 Success,
701 ErrorMaxTurns,
702 ErrorDuringExecution,
703 ErrorMaxBudgetUsd,
704 ErrorMaxStructuredOutputRetries,
705 Unknown(String),
706}
707
708impl ResultSubtype {
709 pub fn as_str(&self) -> &str {
710 match self {
711 Self::Success => "success",
712 Self::ErrorMaxTurns => "error_max_turns",
713 Self::ErrorDuringExecution => "error_during_execution",
714 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
715 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
716 Self::Unknown(s) => s.as_str(),
717 }
718 }
719}
720
721impl fmt::Display for ResultSubtype {
722 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
723 f.write_str(self.as_str())
724 }
725}
726
727impl From<&str> for ResultSubtype {
728 fn from(s: &str) -> Self {
729 match s {
730 "success" => Self::Success,
731 "error_max_turns" => Self::ErrorMaxTurns,
732 "error_during_execution" => Self::ErrorDuringExecution,
733 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
734 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
735 other => Self::Unknown(other.to_string()),
736 }
737 }
738}
739
740impl Serialize for ResultSubtype {
741 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
742 serializer.serialize_str(self.as_str())
743 }
744}
745
746impl<'de> Deserialize<'de> for ResultSubtype {
747 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
748 let s = String::deserialize(deserializer)?;
749 Ok(Self::from(s.as_str()))
750 }
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize)]
771pub struct UsageInfo {
772 #[serde(default)]
774 pub input_tokens: u32,
775 #[serde(default)]
777 pub cache_creation_input_tokens: u32,
778 #[serde(default)]
782 pub cache_read_input_tokens: u32,
783 #[serde(default)]
785 pub output_tokens: u32,
786 #[serde(default)]
787 pub server_tool_use: ServerToolUse,
788 #[serde(default)]
789 pub service_tier: String,
790
791 #[serde(skip_serializing_if = "Option::is_none")]
793 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
794
795 #[serde(skip_serializing_if = "Option::is_none")]
797 pub inference_geo: Option<String>,
798
799 #[serde(default, skip_serializing_if = "Vec::is_empty")]
803 pub iterations: Vec<UsageIteration>,
804
805 #[serde(skip_serializing_if = "Option::is_none")]
807 pub speed: Option<String>,
808
809 #[serde(skip_serializing_if = "Option::is_none")]
812 pub output_tokens_details: Option<OutputTokensDetails>,
813}
814
815#[derive(Debug, Clone, Serialize, Deserialize, Default)]
818pub struct OutputTokensDetails {
819 #[serde(skip_serializing_if = "Option::is_none")]
821 pub thinking_tokens: Option<u64>,
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize)]
833pub struct UsageIteration {
834 #[serde(default)]
835 pub input_tokens: u32,
836 #[serde(default)]
837 pub output_tokens: u32,
838 #[serde(default, skip_serializing_if = "Option::is_none")]
840 pub cache_read_input_tokens: Option<u32>,
841 #[serde(default, skip_serializing_if = "Option::is_none")]
843 pub cache_creation_input_tokens: Option<u32>,
844 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
847 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
849 pub kind: Option<String>,
850}
851
852#[derive(Debug, Clone, Default, Serialize, Deserialize)]
854pub struct ServerToolUse {
855 #[serde(default)]
856 pub web_search_requests: u32,
857 #[serde(default)]
859 pub web_fetch_requests: u32,
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865 use crate::io::ClaudeOutput;
866
867 #[test]
868 fn test_deserialize_result_message() {
869 let json = r#"{
870 "type": "result",
871 "subtype": "success",
872 "is_error": false,
873 "duration_ms": 100,
874 "duration_api_ms": 200,
875 "num_turns": 1,
876 "result": "Done",
877 "session_id": "123",
878 "total_cost_usd": 0.01,
879 "permission_denials": []
880 }"#;
881
882 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
883 assert!(!output.is_error());
884 }
885
886 #[test]
887 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
888 let json = r#"{
889 "type": "result",
890 "subtype": "error_max_budget_usd",
891 "is_error": true,
892 "duration_ms": 100,
893 "duration_api_ms": 200,
894 "num_turns": 1,
895 "session_id": "123",
896 "total_cost_usd": 0.01
897 }"#;
898
899 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
900 let ClaudeOutput::Result(result) = output else {
901 panic!("Expected Result");
902 };
903 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
904
905 let json = r#"{
906 "type": "result",
907 "subtype": "future_result_subtype",
908 "is_error": true,
909 "duration_ms": 100,
910 "duration_api_ms": 200,
911 "num_turns": 1,
912 "session_id": "123",
913 "total_cost_usd": 0.01
914 }"#;
915
916 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
917 let ClaudeOutput::Result(result) = output else {
918 panic!("Expected Result");
919 };
920 assert_eq!(
921 result.subtype,
922 ResultSubtype::Unknown("future_result_subtype".to_string())
923 );
924 }
925
926 #[test]
927 fn test_deserialize_result_with_permission_denials() {
928 let json = r#"{
929 "type": "result",
930 "subtype": "success",
931 "is_error": false,
932 "duration_ms": 100,
933 "duration_api_ms": 200,
934 "num_turns": 2,
935 "result": "Done",
936 "session_id": "123",
937 "total_cost_usd": 0.01,
938 "permission_denials": [
939 {
940 "tool_name": "Bash",
941 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
942 "tool_use_id": "toolu_123"
943 }
944 ]
945 }"#;
946
947 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
948 if let ClaudeOutput::Result(result) = output {
949 assert_eq!(result.permission_denials.len(), 1);
950 assert_eq!(result.permission_denials[0].tool_name, "Bash");
951 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
952 assert_eq!(
953 result.permission_denials[0]
954 .tool_input
955 .get("command")
956 .unwrap(),
957 "rm -rf /"
958 );
959 } else {
960 panic!("Expected Result");
961 }
962 }
963
964 #[test]
965 fn test_permission_denial_roundtrip() {
966 let denial = PermissionDenial {
967 tool_name: "Write".to_string(),
968 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
969 tool_use_id: "toolu_456".to_string(),
970 };
971
972 let json = serde_json::to_string(&denial).unwrap();
973 assert!(json.contains("\"tool_name\":\"Write\""));
974 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
975 assert!(json.contains("/etc/passwd"));
976
977 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
978 assert_eq!(parsed, denial);
979 }
980
981 #[test]
982 fn test_deserialize_result_message_with_errors() {
983 let json = r#"{
984 "type": "result",
985 "subtype": "error_during_execution",
986 "duration_ms": 0,
987 "duration_api_ms": 0,
988 "is_error": true,
989 "num_turns": 0,
990 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
991 "total_cost_usd": 0,
992 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
993 "permission_denials": []
994 }"#;
995
996 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
997 assert!(output.is_error());
998
999 if let ClaudeOutput::Result(res) = output {
1000 assert!(res.is_error);
1001 assert_eq!(res.errors.len(), 1);
1002 assert!(res.errors[0].contains("No conversation found"));
1003 } else {
1004 panic!("Expected Result message");
1005 }
1006 }
1007
1008 #[test]
1009 fn test_deserialize_result_message_errors_defaults_empty() {
1010 let json = r#"{
1011 "type": "result",
1012 "subtype": "success",
1013 "is_error": false,
1014 "duration_ms": 100,
1015 "duration_api_ms": 200,
1016 "num_turns": 1,
1017 "session_id": "123",
1018 "total_cost_usd": 0.01
1019 }"#;
1020
1021 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1022 if let ClaudeOutput::Result(res) = output {
1023 assert!(res.errors.is_empty());
1024 } else {
1025 panic!("Expected Result message");
1026 }
1027 }
1028
1029 #[test]
1030 fn test_result_message_errors_roundtrip() {
1031 let json = r#"{
1032 "type": "result",
1033 "subtype": "error_during_execution",
1034 "is_error": true,
1035 "duration_ms": 0,
1036 "duration_api_ms": 0,
1037 "num_turns": 0,
1038 "session_id": "test-session",
1039 "total_cost_usd": 0.0,
1040 "errors": ["Error 1", "Error 2"]
1041 }"#;
1042
1043 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1044 let reserialized = serde_json::to_string(&output).unwrap();
1045
1046 assert!(reserialized.contains("Error 1"));
1047 assert!(reserialized.contains("Error 2"));
1048 }
1049
1050 #[test]
1051 fn test_result_with_new_fields() {
1052 let json = r#"{
1053 "type": "result",
1054 "subtype": "success",
1055 "is_error": false,
1056 "duration_ms": 5000,
1057 "duration_api_ms": 4500,
1058 "num_turns": 1,
1059 "result": "Done",
1060 "session_id": "abc",
1061 "total_cost_usd": 0.06,
1062 "api_error_status": null,
1063 "stop_reason": "end_turn",
1064 "terminal_reason": "completed",
1065 "fast_mode_state": "off",
1066 "modelUsage": {
1067 "claude-opus-4-7[1m]": {
1068 "inputTokens": 3817,
1069 "outputTokens": 14,
1070 "costUSD": 0.06
1071 }
1072 },
1073 "usage": {
1074 "input_tokens": 3817,
1075 "output_tokens": 14,
1076 "cache_creation_input_tokens": 3540,
1077 "cache_read_input_tokens": 0,
1078 "server_tool_use": {
1079 "web_search_requests": 0,
1080 "web_fetch_requests": 2
1081 },
1082 "service_tier": "standard",
1083 "inference_geo": "not_available",
1084 "speed": "standard",
1085 "iterations": [
1086 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
1087 ]
1088 }
1089 }"#;
1090
1091 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1092 if let ClaudeOutput::Result(res) = output {
1093 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
1094 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
1095 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
1096 let model_usage = res.model_usage.as_ref().unwrap();
1097 let entry = model_usage
1098 .get("claude-opus-4-7[1m]")
1099 .expect("per-model entry present");
1100 assert_eq!(entry.input_tokens, 3817);
1101 assert_eq!(entry.output_tokens, 14);
1102 assert_eq!(entry.cost_usd, 0.06);
1103 assert!(res.api_error_status.is_none());
1104
1105 let usage = res.usage.unwrap();
1106 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
1107 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
1108 assert_eq!(usage.speed.as_deref(), Some("standard"));
1109 assert_eq!(usage.iterations.len(), 1);
1110 assert_eq!(usage.iterations[0].input_tokens, 3817);
1111 assert_eq!(usage.iterations[0].output_tokens, 14);
1112 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
1113 } else {
1114 panic!("Expected Result");
1115 }
1116 }
1117
1118 #[test]
1119 fn test_result_backwards_compatible_without_new_fields() {
1120 let json = r#"{
1122 "type": "result",
1123 "subtype": "success",
1124 "is_error": false,
1125 "duration_ms": 100,
1126 "duration_api_ms": 200,
1127 "num_turns": 1,
1128 "session_id": "abc",
1129 "total_cost_usd": 0.01
1130 }"#;
1131
1132 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1133 if let ClaudeOutput::Result(res) = output {
1134 assert!(res.api_error_status.is_none());
1135 assert!(res.stop_reason.is_none());
1136 assert!(res.terminal_reason.is_none());
1137 assert!(res.fast_mode_state.is_none());
1138 assert!(res.model_usage.is_none());
1139 } else {
1140 panic!("Expected Result");
1141 }
1142 }
1143
1144 #[test]
1145 fn test_result_fast_mode_disabled_reason() {
1146 let json = r#"{
1147 "type":"result","subtype":"success","is_error":false,
1148 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1149 "session_id":"s1","total_cost_usd":0.01,
1150 "fast_mode_state":"off",
1151 "fast_mode_disabled_reason":"sdk_opt_in_required"
1152 }"#;
1153 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1154 let crate::ClaudeOutput::Result(res) = &output else {
1155 panic!("expected Result");
1156 };
1157 assert_eq!(
1158 res.fast_mode_disabled_reason,
1159 Some(FastModeDisabledReason::SdkOptInRequired)
1160 );
1161 assert!(serde_json::to_string(&output)
1162 .unwrap()
1163 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
1164
1165 assert_eq!(
1168 FastModeDisabledReason::from("unknown"),
1169 FastModeDisabledReason::UnknownReason
1170 );
1171 let novel = FastModeDisabledReason::from("solar_flare");
1172 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
1173 assert_eq!(novel.as_str(), "solar_flare");
1174 }
1175
1176 #[test]
1177 fn test_result_timing_and_user_message_uuid_fields() {
1178 let json = r#"{
1179 "type":"result","subtype":"success","is_error":false,
1180 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1181 "session_id":"s1","total_cost_usd":0.01,
1182 "request_sent_wall_ms":1753212345678.25,
1183 "user_message_uuid":"um-1"
1184 }"#;
1185 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1186 let crate::ClaudeOutput::Result(res) = &output else {
1187 panic!("expected Result");
1188 };
1189 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
1190 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
1191
1192 let reserialized = serde_json::to_string(&output).unwrap();
1193 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
1194 }
1195
1196 #[test]
1197 fn test_result_queued_turn_count_and_user_message_uuids() {
1198 let json = r#"{
1200 "type":"result","subtype":"success","is_error":false,
1201 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1202 "session_id":"s1","total_cost_usd":0.01,
1203 "user_message_uuid":"um-2",
1204 "user_message_uuids":["um-1","um-2"],
1205 "queued_turn_count":3
1206 }"#;
1207 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
1208 let crate::ClaudeOutput::Result(res) = &output else {
1209 panic!("expected Result");
1210 };
1211 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
1212 assert_eq!(res.queued_turn_count, Some(3));
1213
1214 let reserialized = serde_json::to_string(&output).unwrap();
1215 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
1216 assert!(reserialized.contains("\"queued_turn_count\":3"));
1217
1218 let old = r#"{
1220 "type":"result","subtype":"success","is_error":false,
1221 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
1222 "session_id":"s1","total_cost_usd":0.01
1223 }"#;
1224 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
1225 let crate::ClaudeOutput::Result(res) = &output else {
1226 panic!("expected Result");
1227 };
1228 assert!(res.user_message_uuids.is_empty());
1229 assert_eq!(res.queued_turn_count, None);
1230 let reserialized = serde_json::to_string(&output).unwrap();
1231 assert!(!reserialized.contains("user_message_uuids"));
1232 assert!(!reserialized.contains("queued_turn_count"));
1233 }
1234}