use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CascadeLevel {
Retry = 0,
Fallback = 1,
Refine = 2,
Escalate = 3,
Contain = 4,
}
impl CascadeLevel {
fn next(self) -> CascadeLevel {
match self {
CascadeLevel::Retry => CascadeLevel::Fallback,
CascadeLevel::Fallback => CascadeLevel::Refine,
CascadeLevel::Refine => CascadeLevel::Escalate,
CascadeLevel::Escalate | CascadeLevel::Contain => CascadeLevel::Contain,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FailureKind {
GateRejection,
ToolFailure,
ProviderTransport,
ProviderRateLimit,
CapabilityMismatch,
MeasuredPlateau,
ScopeTooBroad,
NeedsApproval,
NeedsCapability,
Unknown,
}
pub fn classify_failure(kind: FailureKind) -> CascadeLevel {
match kind {
FailureKind::GateRejection | FailureKind::ToolFailure => CascadeLevel::Retry,
FailureKind::ProviderTransport
| FailureKind::ProviderRateLimit
| FailureKind::CapabilityMismatch => CascadeLevel::Fallback,
FailureKind::MeasuredPlateau | FailureKind::ScopeTooBroad => CascadeLevel::Refine,
FailureKind::NeedsApproval | FailureKind::NeedsCapability => CascadeLevel::Escalate,
FailureKind::Unknown => CascadeLevel::Contain,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CascadeClass {
Committed,
Compensated,
HumanEscalation,
Contained,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GrantedControl {
pub level: CascadeLevel,
pub terminal: bool,
pub forced_escalation: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoveryCascade {
pub budget: u32,
pub spent: u32,
pub current_level: CascadeLevel,
pub escalations_used: u32,
pub steps: u32,
}
impl RecoveryCascade {
pub fn new(budget: u32) -> Self {
Self {
budget,
spent: 0,
current_level: CascadeLevel::Retry,
escalations_used: 0,
steps: 0,
}
}
pub const K: u32 = 4;
pub fn step_bound(&self) -> u32 {
self.budget + Self::K
}
pub fn grant(&mut self, requested: CascadeLevel) -> GrantedControl {
if self.current_level == CascadeLevel::Contain {
return GrantedControl {
level: CascadeLevel::Contain,
terminal: true,
forced_escalation: false,
};
}
self.steps += 1;
if requested > self.current_level {
self.current_level = requested;
self.escalations_used += 1;
return GrantedControl {
level: requested,
terminal: requested == CascadeLevel::Contain,
forced_escalation: false,
};
}
if self.spent < self.budget {
self.spent += 1;
return GrantedControl {
level: requested,
terminal: false,
forced_escalation: false,
};
}
let forced = self.current_level.next();
self.current_level = forced;
self.escalations_used += 1;
GrantedControl {
level: forced,
terminal: forced == CascadeLevel::Contain,
forced_escalation: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn always_failing_adversary_terminates_within_b_plus_k() {
let b = 5;
let mut cascade = RecoveryCascade::new(b);
let mut steps = 0;
loop {
let granted = cascade.grant(CascadeLevel::Retry);
steps += 1;
if granted.terminal {
break;
}
assert!(steps <= cascade.step_bound(), "exceeded b + k");
}
assert!(steps <= b + RecoveryCascade::K);
assert_eq!(cascade.current_level, CascadeLevel::Contain);
}
#[test]
fn horizontal_repetitions_at_any_level_draw_on_the_one_pool() {
let b = 6;
let mut distributed = RecoveryCascade::new(b);
distributed.grant(CascadeLevel::Retry);
distributed.grant(CascadeLevel::Retry);
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);
let granted = distributed.grant(CascadeLevel::Refine); assert_eq!(distributed.spent, 6);
assert!(!granted.forced_escalation);
let mut level0 = RecoveryCascade::new(b);
for _ in 0..b {
level0.grant(CascadeLevel::Retry);
}
assert_eq!(level0.spent, distributed.spent);
let forced = distributed.grant(CascadeLevel::Retry);
assert!(forced.forced_escalation);
assert!(forced.level > CascadeLevel::Refine);
let forced0 = level0.grant(CascadeLevel::Retry);
assert!(forced0.forced_escalation);
}
#[test]
fn exhaustion_forces_strict_escalation() {
let mut cascade = RecoveryCascade::new(1);
cascade.grant(CascadeLevel::Retry); let g1 = cascade.grant(CascadeLevel::Retry);
assert!(g1.forced_escalation);
assert_eq!(g1.level, CascadeLevel::Fallback);
let g2 = cascade.grant(CascadeLevel::Retry);
assert!(g2.forced_escalation);
assert_eq!(g2.level, CascadeLevel::Refine);
let g3 = cascade.grant(CascadeLevel::Fallback);
assert_eq!(g3.level, CascadeLevel::Escalate);
let g4 = cascade.grant(CascadeLevel::Retry);
assert_eq!(g4.level, CascadeLevel::Contain);
assert!(g4.terminal);
}
#[test]
fn contain_is_unconditionally_terminating() {
let mut cascade = RecoveryCascade::new(3);
cascade.grant(CascadeLevel::Contain);
for _ in 0..5 {
let granted = cascade.grant(CascadeLevel::Retry);
assert!(granted.terminal);
assert_eq!(granted.level, CascadeLevel::Contain);
}
}
#[test]
fn every_failure_class_maps_to_a_control() {
assert_eq!(
classify_failure(FailureKind::ProviderTransport),
CascadeLevel::Fallback
);
assert_eq!(
classify_failure(FailureKind::MeasuredPlateau),
CascadeLevel::Refine
);
assert_eq!(
classify_failure(FailureKind::Unknown),
CascadeLevel::Contain
);
}
}