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(skip_serializing_if = "Option::is_none")]
44 pub user_message_uuid: Option<String>,
45
46 pub num_turns: i32,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub result: Option<String>,
50
51 #[serde(alias = "sessionId")]
52 pub session_id: String,
53 pub total_cost_usd: f64,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub usage: Option<UsageInfo>,
57
58 #[serde(default)]
60 pub permission_denials: Vec<PermissionDenial>,
61
62 #[serde(default)]
67 pub errors: Vec<String>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub uuid: Option<String>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub api_error_status: Option<u16>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub stop_reason: Option<String>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub terminal_reason: Option<String>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub fast_mode_state: Option<String>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
92 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
93
94 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
96 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
102 pub subagent_stats: Option<SubagentStats>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub structured_output: Option<Value>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub deferred_tool_use: Option<DeferredToolUse>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub origin: Option<super::message_types::MessageOrigin>,
115}
116
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct ModelUsageEntry {
125 #[serde(default)]
126 pub input_tokens: u64,
127 #[serde(default)]
128 pub output_tokens: u64,
129 #[serde(default)]
130 pub cache_read_input_tokens: u64,
131 #[serde(default)]
132 pub cache_creation_input_tokens: u64,
133 #[serde(default, rename = "costUSD")]
134 pub cost_usd: f64,
135 #[serde(default)]
136 pub web_search_requests: u32,
137 #[serde(default)]
138 pub context_window: u64,
139 #[serde(default)]
140 pub max_output_tokens: u64,
141 #[serde(flatten)]
142 pub extra: serde_json::Map<String, serde_json::Value>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147pub struct DeferredToolUse {
148 pub id: String,
149 pub name: String,
150 pub input: Value,
151}
152
153#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
162pub struct SubagentStats {
163 pub spawned: u64,
165 pub requested: SubagentSpawnRequests,
168 pub started_in_background: u64,
171 #[serde(default)]
173 pub by_type: std::collections::BTreeMap<String, u64>,
174 pub max_depth: u64,
177 pub spawned_by_subagents: u64,
179 pub completed: u64,
180 pub failed: u64,
181 pub killed: SubagentKillCounts,
183 pub refused: SubagentRefusalCounts,
186}
187
188#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
191pub struct SubagentSpawnRequests {
192 pub background: u64,
193 pub foreground: u64,
194 pub unset: u64,
195}
196
197#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
201pub struct SubagentKillCounts {
202 pub parent: u64,
203 pub user: u64,
204 pub system: u64,
205}
206
207#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
209pub struct SubagentRefusalCounts {
210 pub depth_limit: u64,
211 pub concurrency_limit: u64,
212 pub budget: u64,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
220pub struct PermissionDenial {
221 pub tool_name: String,
223
224 pub tool_input: Value,
226
227 pub tool_use_id: String,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Hash)]
234pub enum FastModeDisabledReason {
235 Free,
237 Preference,
239 ExtraUsageDisabled,
241 NetworkError,
243 UnknownReason,
245 NotFirstParty,
247 DisabledByEnv,
249 ModelNotAllowed,
251 SdkOptInRequired,
253 Pending,
255 Unknown(String),
257}
258
259impl FastModeDisabledReason {
260 pub fn as_str(&self) -> &str {
261 match self {
262 Self::Free => "free",
263 Self::Preference => "preference",
264 Self::ExtraUsageDisabled => "extra_usage_disabled",
265 Self::NetworkError => "network_error",
266 Self::UnknownReason => "unknown",
267 Self::NotFirstParty => "not_first_party",
268 Self::DisabledByEnv => "disabled_by_env",
269 Self::ModelNotAllowed => "model_not_allowed",
270 Self::SdkOptInRequired => "sdk_opt_in_required",
271 Self::Pending => "pending",
272 Self::Unknown(s) => s.as_str(),
273 }
274 }
275}
276
277impl fmt::Display for FastModeDisabledReason {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 f.write_str(self.as_str())
280 }
281}
282
283impl From<&str> for FastModeDisabledReason {
284 fn from(s: &str) -> Self {
285 match s {
286 "free" => Self::Free,
287 "preference" => Self::Preference,
288 "extra_usage_disabled" => Self::ExtraUsageDisabled,
289 "network_error" => Self::NetworkError,
290 "unknown" => Self::UnknownReason,
291 "not_first_party" => Self::NotFirstParty,
292 "disabled_by_env" => Self::DisabledByEnv,
293 "model_not_allowed" => Self::ModelNotAllowed,
294 "sdk_opt_in_required" => Self::SdkOptInRequired,
295 "pending" => Self::Pending,
296 other => Self::Unknown(other.to_string()),
297 }
298 }
299}
300
301impl Serialize for FastModeDisabledReason {
302 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
303 serializer.serialize_str(self.as_str())
304 }
305}
306
307impl<'de> Deserialize<'de> for FastModeDisabledReason {
308 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
309 let s = String::deserialize(deserializer)?;
310 Ok(Self::from(s.as_str()))
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, Hash)]
316pub enum ResultSubtype {
317 Success,
318 ErrorMaxTurns,
319 ErrorDuringExecution,
320 ErrorMaxBudgetUsd,
321 ErrorMaxStructuredOutputRetries,
322 Unknown(String),
323}
324
325impl ResultSubtype {
326 pub fn as_str(&self) -> &str {
327 match self {
328 Self::Success => "success",
329 Self::ErrorMaxTurns => "error_max_turns",
330 Self::ErrorDuringExecution => "error_during_execution",
331 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
332 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
333 Self::Unknown(s) => s.as_str(),
334 }
335 }
336}
337
338impl fmt::Display for ResultSubtype {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 f.write_str(self.as_str())
341 }
342}
343
344impl From<&str> for ResultSubtype {
345 fn from(s: &str) -> Self {
346 match s {
347 "success" => Self::Success,
348 "error_max_turns" => Self::ErrorMaxTurns,
349 "error_during_execution" => Self::ErrorDuringExecution,
350 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
351 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
352 other => Self::Unknown(other.to_string()),
353 }
354 }
355}
356
357impl Serialize for ResultSubtype {
358 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
359 serializer.serialize_str(self.as_str())
360 }
361}
362
363impl<'de> Deserialize<'de> for ResultSubtype {
364 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
365 let s = String::deserialize(deserializer)?;
366 Ok(Self::from(s.as_str()))
367 }
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct UsageInfo {
389 #[serde(default)]
391 pub input_tokens: u32,
392 #[serde(default)]
394 pub cache_creation_input_tokens: u32,
395 #[serde(default)]
399 pub cache_read_input_tokens: u32,
400 #[serde(default)]
402 pub output_tokens: u32,
403 #[serde(default)]
404 pub server_tool_use: ServerToolUse,
405 #[serde(default)]
406 pub service_tier: String,
407
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
411
412 #[serde(skip_serializing_if = "Option::is_none")]
414 pub inference_geo: Option<String>,
415
416 #[serde(default, skip_serializing_if = "Vec::is_empty")]
420 pub iterations: Vec<UsageIteration>,
421
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub speed: Option<String>,
425
426 #[serde(skip_serializing_if = "Option::is_none")]
429 pub output_tokens_details: Option<OutputTokensDetails>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize, Default)]
435pub struct OutputTokensDetails {
436 #[serde(skip_serializing_if = "Option::is_none")]
438 pub thinking_tokens: Option<u64>,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct UsageIteration {
451 #[serde(default)]
452 pub input_tokens: u32,
453 #[serde(default)]
454 pub output_tokens: u32,
455 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub cache_read_input_tokens: Option<u32>,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
460 pub cache_creation_input_tokens: Option<u32>,
461 #[serde(default, skip_serializing_if = "Option::is_none")]
463 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
464 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
466 pub kind: Option<String>,
467}
468
469#[derive(Debug, Clone, Default, Serialize, Deserialize)]
471pub struct ServerToolUse {
472 #[serde(default)]
473 pub web_search_requests: u32,
474 #[serde(default)]
476 pub web_fetch_requests: u32,
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use crate::io::ClaudeOutput;
483
484 #[test]
485 fn test_deserialize_result_message() {
486 let json = r#"{
487 "type": "result",
488 "subtype": "success",
489 "is_error": false,
490 "duration_ms": 100,
491 "duration_api_ms": 200,
492 "num_turns": 1,
493 "result": "Done",
494 "session_id": "123",
495 "total_cost_usd": 0.01,
496 "permission_denials": []
497 }"#;
498
499 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
500 assert!(!output.is_error());
501 }
502
503 #[test]
504 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
505 let json = r#"{
506 "type": "result",
507 "subtype": "error_max_budget_usd",
508 "is_error": true,
509 "duration_ms": 100,
510 "duration_api_ms": 200,
511 "num_turns": 1,
512 "session_id": "123",
513 "total_cost_usd": 0.01
514 }"#;
515
516 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
517 let ClaudeOutput::Result(result) = output else {
518 panic!("Expected Result");
519 };
520 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
521
522 let json = r#"{
523 "type": "result",
524 "subtype": "future_result_subtype",
525 "is_error": true,
526 "duration_ms": 100,
527 "duration_api_ms": 200,
528 "num_turns": 1,
529 "session_id": "123",
530 "total_cost_usd": 0.01
531 }"#;
532
533 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
534 let ClaudeOutput::Result(result) = output else {
535 panic!("Expected Result");
536 };
537 assert_eq!(
538 result.subtype,
539 ResultSubtype::Unknown("future_result_subtype".to_string())
540 );
541 }
542
543 #[test]
544 fn test_deserialize_result_with_permission_denials() {
545 let json = r#"{
546 "type": "result",
547 "subtype": "success",
548 "is_error": false,
549 "duration_ms": 100,
550 "duration_api_ms": 200,
551 "num_turns": 2,
552 "result": "Done",
553 "session_id": "123",
554 "total_cost_usd": 0.01,
555 "permission_denials": [
556 {
557 "tool_name": "Bash",
558 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
559 "tool_use_id": "toolu_123"
560 }
561 ]
562 }"#;
563
564 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
565 if let ClaudeOutput::Result(result) = output {
566 assert_eq!(result.permission_denials.len(), 1);
567 assert_eq!(result.permission_denials[0].tool_name, "Bash");
568 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
569 assert_eq!(
570 result.permission_denials[0]
571 .tool_input
572 .get("command")
573 .unwrap(),
574 "rm -rf /"
575 );
576 } else {
577 panic!("Expected Result");
578 }
579 }
580
581 #[test]
582 fn test_permission_denial_roundtrip() {
583 let denial = PermissionDenial {
584 tool_name: "Write".to_string(),
585 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
586 tool_use_id: "toolu_456".to_string(),
587 };
588
589 let json = serde_json::to_string(&denial).unwrap();
590 assert!(json.contains("\"tool_name\":\"Write\""));
591 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
592 assert!(json.contains("/etc/passwd"));
593
594 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
595 assert_eq!(parsed, denial);
596 }
597
598 #[test]
599 fn test_deserialize_result_message_with_errors() {
600 let json = r#"{
601 "type": "result",
602 "subtype": "error_during_execution",
603 "duration_ms": 0,
604 "duration_api_ms": 0,
605 "is_error": true,
606 "num_turns": 0,
607 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
608 "total_cost_usd": 0,
609 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
610 "permission_denials": []
611 }"#;
612
613 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
614 assert!(output.is_error());
615
616 if let ClaudeOutput::Result(res) = output {
617 assert!(res.is_error);
618 assert_eq!(res.errors.len(), 1);
619 assert!(res.errors[0].contains("No conversation found"));
620 } else {
621 panic!("Expected Result message");
622 }
623 }
624
625 #[test]
626 fn test_deserialize_result_message_errors_defaults_empty() {
627 let json = r#"{
628 "type": "result",
629 "subtype": "success",
630 "is_error": false,
631 "duration_ms": 100,
632 "duration_api_ms": 200,
633 "num_turns": 1,
634 "session_id": "123",
635 "total_cost_usd": 0.01
636 }"#;
637
638 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
639 if let ClaudeOutput::Result(res) = output {
640 assert!(res.errors.is_empty());
641 } else {
642 panic!("Expected Result message");
643 }
644 }
645
646 #[test]
647 fn test_result_message_errors_roundtrip() {
648 let json = r#"{
649 "type": "result",
650 "subtype": "error_during_execution",
651 "is_error": true,
652 "duration_ms": 0,
653 "duration_api_ms": 0,
654 "num_turns": 0,
655 "session_id": "test-session",
656 "total_cost_usd": 0.0,
657 "errors": ["Error 1", "Error 2"]
658 }"#;
659
660 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
661 let reserialized = serde_json::to_string(&output).unwrap();
662
663 assert!(reserialized.contains("Error 1"));
664 assert!(reserialized.contains("Error 2"));
665 }
666
667 #[test]
668 fn test_result_with_new_fields() {
669 let json = r#"{
670 "type": "result",
671 "subtype": "success",
672 "is_error": false,
673 "duration_ms": 5000,
674 "duration_api_ms": 4500,
675 "num_turns": 1,
676 "result": "Done",
677 "session_id": "abc",
678 "total_cost_usd": 0.06,
679 "api_error_status": null,
680 "stop_reason": "end_turn",
681 "terminal_reason": "completed",
682 "fast_mode_state": "off",
683 "modelUsage": {
684 "claude-opus-4-7[1m]": {
685 "inputTokens": 3817,
686 "outputTokens": 14,
687 "costUSD": 0.06
688 }
689 },
690 "usage": {
691 "input_tokens": 3817,
692 "output_tokens": 14,
693 "cache_creation_input_tokens": 3540,
694 "cache_read_input_tokens": 0,
695 "server_tool_use": {
696 "web_search_requests": 0,
697 "web_fetch_requests": 2
698 },
699 "service_tier": "standard",
700 "inference_geo": "not_available",
701 "speed": "standard",
702 "iterations": [
703 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
704 ]
705 }
706 }"#;
707
708 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
709 if let ClaudeOutput::Result(res) = output {
710 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
711 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
712 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
713 let model_usage = res.model_usage.as_ref().unwrap();
714 let entry = model_usage
715 .get("claude-opus-4-7[1m]")
716 .expect("per-model entry present");
717 assert_eq!(entry.input_tokens, 3817);
718 assert_eq!(entry.output_tokens, 14);
719 assert_eq!(entry.cost_usd, 0.06);
720 assert!(res.api_error_status.is_none());
721
722 let usage = res.usage.unwrap();
723 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
724 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
725 assert_eq!(usage.speed.as_deref(), Some("standard"));
726 assert_eq!(usage.iterations.len(), 1);
727 assert_eq!(usage.iterations[0].input_tokens, 3817);
728 assert_eq!(usage.iterations[0].output_tokens, 14);
729 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
730 } else {
731 panic!("Expected Result");
732 }
733 }
734
735 #[test]
736 fn test_result_backwards_compatible_without_new_fields() {
737 let json = r#"{
739 "type": "result",
740 "subtype": "success",
741 "is_error": false,
742 "duration_ms": 100,
743 "duration_api_ms": 200,
744 "num_turns": 1,
745 "session_id": "abc",
746 "total_cost_usd": 0.01
747 }"#;
748
749 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
750 if let ClaudeOutput::Result(res) = output {
751 assert!(res.api_error_status.is_none());
752 assert!(res.stop_reason.is_none());
753 assert!(res.terminal_reason.is_none());
754 assert!(res.fast_mode_state.is_none());
755 assert!(res.model_usage.is_none());
756 } else {
757 panic!("Expected Result");
758 }
759 }
760
761 #[test]
762 fn test_result_fast_mode_disabled_reason() {
763 let json = r#"{
764 "type":"result","subtype":"success","is_error":false,
765 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
766 "session_id":"s1","total_cost_usd":0.01,
767 "fast_mode_state":"off",
768 "fast_mode_disabled_reason":"sdk_opt_in_required"
769 }"#;
770 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
771 let crate::ClaudeOutput::Result(res) = &output else {
772 panic!("expected Result");
773 };
774 assert_eq!(
775 res.fast_mode_disabled_reason,
776 Some(FastModeDisabledReason::SdkOptInRequired)
777 );
778 assert!(serde_json::to_string(&output)
779 .unwrap()
780 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
781
782 assert_eq!(
785 FastModeDisabledReason::from("unknown"),
786 FastModeDisabledReason::UnknownReason
787 );
788 let novel = FastModeDisabledReason::from("solar_flare");
789 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
790 assert_eq!(novel.as_str(), "solar_flare");
791 }
792
793 #[test]
794 fn test_result_timing_and_user_message_uuid_fields() {
795 let json = r#"{
796 "type":"result","subtype":"success","is_error":false,
797 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
798 "session_id":"s1","total_cost_usd":0.01,
799 "request_sent_wall_ms":1753212345678.25,
800 "user_message_uuid":"um-1"
801 }"#;
802 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
803 let crate::ClaudeOutput::Result(res) = &output else {
804 panic!("expected Result");
805 };
806 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
807 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
808
809 let reserialized = serde_json::to_string(&output).unwrap();
810 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
811 }
812}