1use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum CascadeLevel {
26 Retry = 0,
28 Fallback = 1,
31 Refine = 2,
35 Escalate = 3,
37 Contain = 4,
40}
41
42impl CascadeLevel {
43 fn next(self) -> CascadeLevel {
44 match self {
45 CascadeLevel::Retry => CascadeLevel::Fallback,
46 CascadeLevel::Fallback => CascadeLevel::Refine,
47 CascadeLevel::Refine => CascadeLevel::Escalate,
48 CascadeLevel::Escalate | CascadeLevel::Contain => CascadeLevel::Contain,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum FailureKind {
58 GateRejection,
59 ToolFailure,
60 ProviderTransport,
61 ProviderRateLimit,
62 CapabilityMismatch,
63 MeasuredPlateau,
64 ScopeTooBroad,
65 NeedsApproval,
66 NeedsCapability,
67 Unknown,
68}
69
70pub fn classify_failure(kind: FailureKind) -> CascadeLevel {
73 match kind {
74 FailureKind::GateRejection | FailureKind::ToolFailure => CascadeLevel::Retry,
75 FailureKind::ProviderTransport
76 | FailureKind::ProviderRateLimit
77 | FailureKind::CapabilityMismatch => CascadeLevel::Fallback,
78 FailureKind::MeasuredPlateau | FailureKind::ScopeTooBroad => CascadeLevel::Refine,
79 FailureKind::NeedsApproval | FailureKind::NeedsCapability => CascadeLevel::Escalate,
80 FailureKind::Unknown => CascadeLevel::Contain,
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum CascadeClass {
89 Committed,
91 Compensated,
95 HumanEscalation,
97 Contained,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct GrantedControl {
104 pub level: CascadeLevel,
105 pub terminal: bool,
107 pub forced_escalation: bool,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct RecoveryCascade {
114 pub budget: u32,
116 pub spent: u32,
117 pub current_level: CascadeLevel,
118 pub escalations_used: u32,
119 pub steps: u32,
120}
121
122impl RecoveryCascade {
123 pub fn new(budget: u32) -> Self {
124 Self {
125 budget,
126 spent: 0,
127 current_level: CascadeLevel::Retry,
128 escalations_used: 0,
129 steps: 0,
130 }
131 }
132
133 pub const K: u32 = 4;
135
136 pub fn step_bound(&self) -> u32 {
138 self.budget + Self::K
139 }
140
141 pub fn grant(&mut self, requested: CascadeLevel) -> GrantedControl {
147 if self.current_level == CascadeLevel::Contain {
148 return GrantedControl {
149 level: CascadeLevel::Contain,
150 terminal: true,
151 forced_escalation: false,
152 };
153 }
154 self.steps += 1;
155 if requested > self.current_level {
156 self.current_level = requested;
158 self.escalations_used += 1;
159 return GrantedControl {
160 level: requested,
161 terminal: requested == CascadeLevel::Contain,
162 forced_escalation: false,
163 };
164 }
165 if self.spent < self.budget {
167 self.spent += 1;
168 return GrantedControl {
169 level: requested,
170 terminal: false,
171 forced_escalation: false,
172 };
173 }
174 let forced = self.current_level.next();
176 self.current_level = forced;
177 self.escalations_used += 1;
178 GrantedControl {
179 level: forced,
180 terminal: forced == CascadeLevel::Contain,
181 forced_escalation: true,
182 }
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
192 fn always_failing_adversary_terminates_within_b_plus_k() {
193 let b = 5;
194 let mut cascade = RecoveryCascade::new(b);
195 let mut steps = 0;
196 loop {
197 let granted = cascade.grant(CascadeLevel::Retry);
198 steps += 1;
199 if granted.terminal {
200 break;
201 }
202 assert!(steps <= cascade.step_bound(), "exceeded b + k");
203 }
204 assert!(steps <= b + RecoveryCascade::K);
205 assert_eq!(cascade.current_level, CascadeLevel::Contain);
206 }
207
208 #[test]
211 fn horizontal_repetitions_at_any_level_draw_on_the_one_pool() {
212 let b = 6;
213 let mut distributed = RecoveryCascade::new(b);
216 distributed.grant(CascadeLevel::Retry);
217 distributed.grant(CascadeLevel::Retry);
218 distributed.grant(CascadeLevel::Fallback); distributed.grant(CascadeLevel::Fallback); distributed.grant(CascadeLevel::Fallback); distributed.grant(CascadeLevel::Refine); distributed.grant(CascadeLevel::Refine); assert_eq!(distributed.spent, 5);
224 let granted = distributed.grant(CascadeLevel::Refine); assert_eq!(distributed.spent, 6);
226 assert!(!granted.forced_escalation);
227
228 let mut level0 = RecoveryCascade::new(b);
230 for _ in 0..b {
231 level0.grant(CascadeLevel::Retry);
232 }
233 assert_eq!(level0.spent, distributed.spent);
234
235 let forced = distributed.grant(CascadeLevel::Retry);
237 assert!(forced.forced_escalation);
238 assert!(forced.level > CascadeLevel::Refine);
239 let forced0 = level0.grant(CascadeLevel::Retry);
240 assert!(forced0.forced_escalation);
241 }
242
243 #[test]
246 fn exhaustion_forces_strict_escalation() {
247 let mut cascade = RecoveryCascade::new(1);
248 cascade.grant(CascadeLevel::Retry); let g1 = cascade.grant(CascadeLevel::Retry);
250 assert!(g1.forced_escalation);
251 assert_eq!(g1.level, CascadeLevel::Fallback);
252 let g2 = cascade.grant(CascadeLevel::Retry);
253 assert!(g2.forced_escalation);
254 assert_eq!(g2.level, CascadeLevel::Refine);
255 let g3 = cascade.grant(CascadeLevel::Fallback);
256 assert_eq!(g3.level, CascadeLevel::Escalate);
257 let g4 = cascade.grant(CascadeLevel::Retry);
258 assert_eq!(g4.level, CascadeLevel::Contain);
259 assert!(g4.terminal);
260 }
261
262 #[test]
264 fn contain_is_unconditionally_terminating() {
265 let mut cascade = RecoveryCascade::new(3);
266 cascade.grant(CascadeLevel::Contain);
267 for _ in 0..5 {
268 let granted = cascade.grant(CascadeLevel::Retry);
269 assert!(granted.terminal);
270 assert_eq!(granted.level, CascadeLevel::Contain);
271 }
272 }
273
274 #[test]
278 fn every_failure_class_maps_to_a_control() {
279 assert_eq!(
280 classify_failure(FailureKind::ProviderTransport),
281 CascadeLevel::Fallback
282 );
283 assert_eq!(
284 classify_failure(FailureKind::MeasuredPlateau),
285 CascadeLevel::Refine
286 );
287 assert_eq!(
288 classify_failure(FailureKind::Unknown),
289 CascadeLevel::Contain
290 );
291 }
292}