Skip to main content

lean_ctx/core/
homeostasis.rs

1//! Homeostatic Memory Guard — Proactive multi-level resource management.
2//!
3//! Scientific basis: Homeostasis (biological systems) — maintains equilibrium by
4//! continuously monitoring internal state and applying graduated corrective responses.
5//! Unlike reactive systems that only respond to crisis, homeostasis proactively
6//! maintains optimal operating conditions through negative feedback loops.
7//!
8//! 4 escalation levels based on memory pressure:
9//! Level 1 (70%): Trim cached outputs (soft pressure)
10//! Level 2 (80%): Evict probationary cache entries
11//! Level 3 (90%): Unload indices (BM25, embeddings)
12//! Level 4 (95%): Aggressive eviction of protected entries
13
14/// Pressure levels as fraction of budget consumed (0.0 - 1.0).
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum PressureLevel {
17    Nominal,   // < 70%
18    Elevated,  // 70-80%
19    High,      // 80-90%
20    Critical,  // 90-95%
21    Emergency, // > 95%
22}
23
24impl PressureLevel {
25    pub fn from_utilization(util: f64) -> Self {
26        if util >= 0.95 {
27            Self::Emergency
28        } else if util >= 0.90 {
29            Self::Critical
30        } else if util >= 0.80 {
31            Self::High
32        } else if util >= 0.70 {
33            Self::Elevated
34        } else {
35            Self::Nominal
36        }
37    }
38}
39
40/// Actions the homeostasis system can recommend.
41#[derive(Debug, Clone, PartialEq)]
42pub enum HomeostasisAction {
43    /// No action needed.
44    None,
45    /// Trim compressed outputs in cache (max 2 per entry instead of 3).
46    TrimOutputs,
47    /// Evict probationary cache entries (read_count <= 1).
48    EvictProbationary { target_tokens: usize },
49    /// Unload heavy indices from memory.
50    UnloadIndices,
51    /// Evict protected cache entries with lowest Boltzmann energy.
52    EvictProtected { target_tokens: usize },
53    /// Emergency: drop all non-essential structures.
54    EmergencyDrop,
55}
56
57/// Homeostatic controller with feedback loop.
58pub struct HomeostasisController {
59    /// Last observed pressure level.
60    last_level: PressureLevel,
61    /// Number of consecutive cycles at the same or higher level (hysteresis).
62    consecutive_at_level: u32,
63    /// Token budget (max cache size in tokens).
64    budget_tokens: usize,
65    /// Whether the last action successfully reduced pressure.
66    last_action_effective: bool,
67}
68
69impl HomeostasisController {
70    pub fn new(budget_tokens: usize) -> Self {
71        Self {
72            last_level: PressureLevel::Nominal,
73            consecutive_at_level: 0,
74            budget_tokens,
75            last_action_effective: true,
76        }
77    }
78
79    /// Evaluate current state and determine the appropriate action.
80    /// The feedback loop: if the last action didn't help, escalate faster.
81    pub fn evaluate(&mut self, current_tokens: usize) -> HomeostasisAction {
82        let util = if self.budget_tokens == 0 {
83            0.0
84        } else {
85            current_tokens as f64 / self.budget_tokens as f64
86        };
87
88        let level = PressureLevel::from_utilization(util);
89
90        // Hysteresis: track consecutive cycles at elevated+ levels
91        if level as u8 >= self.last_level as u8 && level != PressureLevel::Nominal {
92            self.consecutive_at_level += 1;
93        } else {
94            self.consecutive_at_level = 0;
95            self.last_action_effective = true;
96        }
97
98        self.last_level = level;
99
100        // If last action didn't help and we're still under pressure, escalate
101        let escalate = !self.last_action_effective && self.consecutive_at_level > 2;
102
103        match level {
104            PressureLevel::Nominal => HomeostasisAction::None,
105            PressureLevel::Elevated => {
106                if escalate {
107                    HomeostasisAction::EvictProbationary {
108                        target_tokens: self.target_free(0.60),
109                    }
110                } else {
111                    HomeostasisAction::TrimOutputs
112                }
113            }
114            PressureLevel::High => HomeostasisAction::EvictProbationary {
115                target_tokens: self.target_free(0.70),
116            },
117            PressureLevel::Critical => {
118                if escalate {
119                    HomeostasisAction::EvictProtected {
120                        target_tokens: self.target_free(0.75),
121                    }
122                } else {
123                    HomeostasisAction::UnloadIndices
124                }
125            }
126            PressureLevel::Emergency => HomeostasisAction::EmergencyDrop,
127        }
128    }
129
130    /// Report whether the last action was effective (reduced pressure).
131    pub fn report_outcome(&mut self, pressure_reduced: bool) {
132        self.last_action_effective = pressure_reduced;
133    }
134
135    /// Calculate token target to free down to a given utilization fraction.
136    fn target_free(&self, target_util: f64) -> usize {
137        (self.budget_tokens as f64 * target_util) as usize
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn nominal_pressure_no_action() {
147        let mut ctrl = HomeostasisController::new(100_000);
148        let action = ctrl.evaluate(50_000); // 50% utilization
149        assert_eq!(action, HomeostasisAction::None);
150    }
151
152    #[test]
153    fn elevated_pressure_trims_outputs() {
154        let mut ctrl = HomeostasisController::new(100_000);
155        let action = ctrl.evaluate(72_000); // 72% utilization
156        assert_eq!(action, HomeostasisAction::TrimOutputs);
157    }
158
159    #[test]
160    fn high_pressure_evicts_probationary() {
161        let mut ctrl = HomeostasisController::new(100_000);
162        let action = ctrl.evaluate(85_000); // 85% utilization
163        assert!(matches!(
164            action,
165            HomeostasisAction::EvictProbationary { .. }
166        ));
167    }
168
169    #[test]
170    fn critical_pressure_unloads_indices() {
171        let mut ctrl = HomeostasisController::new(100_000);
172        let action = ctrl.evaluate(92_000); // 92% utilization
173        assert_eq!(action, HomeostasisAction::UnloadIndices);
174    }
175
176    #[test]
177    fn emergency_drops_everything() {
178        let mut ctrl = HomeostasisController::new(100_000);
179        let action = ctrl.evaluate(96_000); // 96% utilization
180        assert_eq!(action, HomeostasisAction::EmergencyDrop);
181    }
182
183    #[test]
184    fn escalation_on_ineffective_action() {
185        let mut ctrl = HomeostasisController::new(100_000);
186
187        // Sustained critical pressure without relief
188        ctrl.evaluate(92_000);
189        ctrl.report_outcome(false);
190        ctrl.evaluate(92_000);
191        ctrl.report_outcome(false);
192        ctrl.evaluate(92_000);
193        ctrl.report_outcome(false);
194        let action = ctrl.evaluate(92_000);
195
196        // Should escalate to EvictProtected since UnloadIndices didn't work
197        assert!(matches!(action, HomeostasisAction::EvictProtected { .. }));
198    }
199
200    #[test]
201    fn recovery_resets_escalation() {
202        let mut ctrl = HomeostasisController::new(100_000);
203
204        // Build up pressure
205        ctrl.evaluate(92_000);
206        ctrl.report_outcome(false);
207        ctrl.evaluate(92_000);
208
209        // Pressure drops
210        let action = ctrl.evaluate(50_000);
211        assert_eq!(action, HomeostasisAction::None);
212
213        // Next time pressure rises, starts fresh (no immediate escalation)
214        let action = ctrl.evaluate(72_000);
215        assert_eq!(action, HomeostasisAction::TrimOutputs);
216    }
217}