返回 CodeWhale
retry.rs
根目录 / crates / tui / src / core / runtime_contract / retry.rs
1 use serde::{Deserialize, Serialize};
2
3 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4 #[serde(rename_all = "snake_case")]
5 pub enum OperationClass {
6 ModelRequest,
7 ModelStream,
8 ToolTransport,
9 ToolExecution,
10 ContextCompaction,
11 Verification,
12 ChildRun,
13 }
14
15 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16 #[serde(rename_all = "snake_case")]
17 pub enum Idempotency {
18 ReadOnly,
19 IdempotentWrite,
20 NonIdempotentWrite,
21 Unknown,
22 }
23
24 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25 pub struct RetryAttempt {
26 pub operation: OperationClass,
27 pub idempotency: Idempotency,
28 pub attempt: u32,
29 pub max_attempts: u32,
30 pub content_observed: bool,
31 pub side_effect_observed: bool,
32 pub canceled: bool,
33 pub reason: String,
34 }
35
36 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37 #[serde(rename_all = "snake_case")]
38 pub enum RetryDecision {
39 Retry,
40 Stop { reason: String },
41 }
42
43 #[must_use]
44 pub fn decide_retry(attempt: &RetryAttempt) -> RetryDecision {
45 if attempt.canceled {
46 return RetryDecision::Stop {
47 reason: "operation was canceled".to_string(),
48 };
49 }
50 if attempt.attempt >= attempt.max_attempts {
51 return RetryDecision::Stop {
52 reason: "retry budget exhausted".to_string(),
53 };
54 }
55 if attempt.side_effect_observed
56 && matches!(
57 attempt.idempotency,
58 Idempotency::NonIdempotentWrite | Idempotency::Unknown
59 )
60 {
61 return RetryDecision::Stop {
62 reason: "uncertain write side effect prevents an automatic retry".to_string(),
63 };
64 }
65 if attempt.content_observed && attempt.operation == OperationClass::ModelStream {
66 return RetryDecision::Stop {
67 reason: "model stream already emitted content".to_string(),
68 };
69 }
70 RetryDecision::Retry
71 }
72
73 #[cfg(test)]
74 mod tests {
75 use super::*;
76
77 fn attempt() -> RetryAttempt {
78 RetryAttempt {
79 operation: OperationClass::ModelStream,
80 idempotency: Idempotency::ReadOnly,
81 attempt: 0,
82 max_attempts: 2,
83 content_observed: false,
84 side_effect_observed: false,
85 canceled: false,
86 reason: "network".to_string(),
87 }
88 }
89
90 #[test]
91 fn model_stream_retries_only_before_content() {
92 assert_eq!(decide_retry(&attempt()), RetryDecision::Retry);
93 let mut after_content = attempt();
94 after_content.content_observed = true;
95 assert!(matches!(
96 decide_retry(&after_content),
97 RetryDecision::Stop { .. }
98 ));
99 }
100
101 #[test]
102 fn uncertain_write_never_retries_after_side_effect() {
103 let mut write = attempt();
104 write.operation = OperationClass::ToolExecution;
105 write.idempotency = Idempotency::Unknown;
106 write.side_effect_observed = true;
107 assert!(matches!(decide_retry(&write), RetryDecision::Stop { .. }));
108 }
109 }
110
110 lines RUST