Skip to main content

wm_core/
brain_wave.rs

1//! Brain-Wave Eco Mode — Five-State Resource Management
2//!
3//! The brain-wave system keeps `WhiteMagic` dormant when idle and active
4//! when needed, with zero monitoring overhead. Transitions are driven
5//! by actual event rates, not polling threads.
6
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::time::{Duration, Instant};
10
11/// The five brain-wave states, from most active to most dormant.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum BrainWave {
14    /// Full power. All subsystems, polyglot accelerators, inference.
15    /// Event rate > 10/min.
16    Gamma,
17    /// Inference active, memory R/W, no background consolidation.
18    /// Event rate > 0/min.
19    Beta,
20    /// No active requests for 30s+. Memory reads only.
21    /// Citta heartbeat at 1/10 speed. No embeddings, no dreaming.
22    Alpha,
23    /// 5+ min idle. Dream cycle runs once. Embeddings paused.
24    /// After dream completes, transitions to Delta.
25    Theta,
26    /// 30+ min idle. Only LMDB mmap is warm. Zero CPU.
27    /// Wake on stdin (MCP request) or scheduled timer.
28    Delta,
29}
30
31impl BrainWave {
32    /// Returns true if this state allows tool execution.
33    #[must_use]
34    pub const fn allows_tools(self) -> bool {
35        matches!(self, Self::Gamma | Self::Beta | Self::Alpha)
36    }
37
38    /// Returns true if this state allows background consolidation.
39    #[must_use]
40    pub const fn allows_consolidation(self) -> bool {
41        matches!(self, Self::Theta)
42    }
43
44    /// Returns true if this state is dormant (zero CPU expected).
45    #[must_use]
46    pub const fn is_dormant(self) -> bool {
47        matches!(self, Self::Delta)
48    }
49
50    /// Human-readable name.
51    #[must_use]
52    pub const fn name(self) -> &'static str {
53        match self {
54            Self::Gamma => "Gamma (active)",
55            Self::Beta => "Beta (working)",
56            Self::Alpha => "Alpha (idle)",
57            Self::Theta => "Theta (drowsy)",
58            Self::Delta => "Delta (dormant)",
59        }
60    }
61}
62
63impl fmt::Display for BrainWave {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}", self.name())
66    }
67}
68
69/// Configuration for brain-wave state transitions.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct BrainWaveConfig {
72    /// Event rate threshold for Gamma (events per minute)
73    pub gamma_rate: f64,
74    /// Idle time before transitioning to Alpha
75    pub alpha_idle: Duration,
76    /// Idle time before transitioning to Theta
77    pub theta_idle: Duration,
78    /// Idle time before transitioning to Delta
79    pub delta_idle: Duration,
80}
81
82impl Default for BrainWaveConfig {
83    fn default() -> Self {
84        Self {
85            gamma_rate: 10.0,
86            alpha_idle: Duration::from_secs(30),
87            theta_idle: Duration::from_secs(300),
88            delta_idle: Duration::from_secs(1800),
89        }
90    }
91}
92
93impl BrainWaveConfig {
94    /// Create a config from environment variables.
95    ///
96    /// Recognized env vars:
97    /// - `WM_GAMMA_RATE` — events per minute for Gamma (default: 10)
98    /// - `WM_ALPHA_IDLE` — seconds idle before Alpha (default: 30)
99    /// - `WM_THETA_IDLE` — seconds idle before Theta (default: 300)
100    /// - `WM_DELTA_IDLE` — seconds idle before Delta (default: 1800)
101    #[must_use]
102    pub fn from_env() -> Self {
103        let defaults = Self::default();
104        Self {
105            gamma_rate: std::env::var("WM_GAMMA_RATE")
106                .ok()
107                .and_then(|v| v.parse().ok())
108                .unwrap_or(defaults.gamma_rate),
109            alpha_idle: Duration::from_secs(
110                std::env::var("WM_ALPHA_IDLE")
111                    .ok()
112                    .and_then(|v| v.parse().ok())
113                    .unwrap_or(defaults.alpha_idle.as_secs()),
114            ),
115            theta_idle: Duration::from_secs(
116                std::env::var("WM_THETA_IDLE")
117                    .ok()
118                    .and_then(|v| v.parse().ok())
119                    .unwrap_or(defaults.theta_idle.as_secs()),
120            ),
121            delta_idle: Duration::from_secs(
122                std::env::var("WM_DELTA_IDLE")
123                    .ok()
124                    .and_then(|v| v.parse().ok())
125                    .unwrap_or(defaults.delta_idle.as_secs()),
126            ),
127        }
128    }
129}
130
131/// Tracks event timestamps and computes the current brain-wave state.
132///
133/// Uses a ring buffer of timestamps — no extra thread, no polling.
134/// Update is ~10ns (atomic push to ring buffer).
135pub struct BrainWaveTracker {
136    /// Configuration for state transitions
137    pub config: BrainWaveConfig,
138    timestamps: smallvec::SmallVec<[Instant; 64]>,
139    last_event: Instant,
140    current: BrainWave,
141}
142
143impl BrainWaveTracker {
144    /// Create a new tracker with the given config.
145    #[must_use]
146    pub fn new(config: BrainWaveConfig) -> Self {
147        let now = Instant::now();
148        Self {
149            config,
150            timestamps: smallvec::SmallVec::new(),
151            last_event: now,
152            current: BrainWave::Delta,
153        }
154    }
155
156    /// Record an event and update the brain-wave state.
157    pub fn record_event(&mut self) -> BrainWave {
158        let now = Instant::now();
159        self.last_event = now;
160        self.timestamps.push(now);
161        // Keep only last 60 seconds
162        let cutoff = now.checked_sub(Duration::from_secs(60)).unwrap();
163        self.timestamps.retain(|t| *t > cutoff);
164        self.recompute(now);
165        self.current
166    }
167
168    /// Recompute the current state without recording an event.
169    /// Called when checking state after a timer fires.
170    pub fn recompute(&mut self, now: Instant) -> BrainWave {
171        let rate = self.event_rate(now);
172        let idle = now.duration_since(self.last_event);
173
174        self.current = if rate > self.config.gamma_rate {
175            BrainWave::Gamma
176        } else if rate > 0.0 {
177            BrainWave::Beta
178        } else if idle > self.config.delta_idle {
179            BrainWave::Delta
180        } else if idle > self.config.theta_idle {
181            BrainWave::Theta
182        } else {
183            BrainWave::Alpha
184        };
185
186        self.current
187    }
188
189    /// Current brain-wave state.
190    #[must_use]
191    pub const fn current(&self) -> BrainWave {
192        self.current
193    }
194
195    /// Compute how long to sleep before the next state transition would occur.
196    ///
197    /// Returns `Duration::MAX` if no transition is pending (e.g., Delta with
198    /// no scheduled tasks). The caller should use this in a `tokio::select!`
199    /// branch alongside stdin readiness.
200    #[must_use]
201    pub fn next_transition_duration(&self) -> Duration {
202        let now = Instant::now();
203        let idle = now.duration_since(self.last_event);
204        match self.current {
205            BrainWave::Gamma | BrainWave::Beta => self.config.alpha_idle.saturating_sub(idle),
206            BrainWave::Alpha => self.config.theta_idle.saturating_sub(idle),
207            BrainWave::Theta => self.config.delta_idle.saturating_sub(idle),
208            BrainWave::Delta => Duration::from_secs(3600),
209        }
210    }
211
212    /// Time since the last event was recorded.
213    #[must_use]
214    pub fn idle_duration(&self) -> Duration {
215        Instant::now().duration_since(self.last_event)
216    }
217
218    /// Events per minute based on recent timestamps.
219    fn event_rate(&self, now: Instant) -> f64 {
220        if self.timestamps.is_empty() {
221            return 0.0;
222        }
223        let cutoff = now.checked_sub(Duration::from_secs(60)).unwrap();
224        let recent = self.timestamps.iter().filter(|&&t| t > cutoff).count();
225        recent as f64
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn brain_wave_allows_tools() {
235        assert!(BrainWave::Gamma.allows_tools());
236        assert!(BrainWave::Beta.allows_tools());
237        assert!(BrainWave::Alpha.allows_tools());
238        assert!(!BrainWave::Theta.allows_tools());
239        assert!(!BrainWave::Delta.allows_tools());
240    }
241
242    #[test]
243    fn tracker_starts_dormant() {
244        let tracker = BrainWaveTracker::new(BrainWaveConfig::default());
245        assert_eq!(tracker.current(), BrainWave::Delta);
246    }
247
248    #[test]
249    fn tracker_transitions_on_event() {
250        let mut tracker = BrainWaveTracker::new(BrainWaveConfig::default());
251        let _ = tracker.record_event();
252        assert_eq!(tracker.current(), BrainWave::Beta);
253    }
254
255    #[test]
256    fn tracker_gamma_on_burst() {
257        let mut tracker = BrainWaveTracker::new(BrainWaveConfig::default());
258        for _ in 0..15 {
259            let _ = tracker.record_event();
260        }
261        assert_eq!(tracker.current(), BrainWave::Gamma);
262    }
263
264    #[test]
265    fn next_transition_duration_beta_to_alpha() {
266        let mut tracker = BrainWaveTracker::new(BrainWaveConfig::default());
267        let _ = tracker.record_event();
268        assert_eq!(tracker.current(), BrainWave::Beta);
269        let d = tracker.next_transition_duration();
270        // alpha_idle is 30s, we just recorded an event so idle ≈ 0
271        assert!(d <= Duration::from_secs(30));
272        assert!(d > Duration::from_secs(28));
273    }
274
275    #[test]
276    fn next_transition_duration_delta_sleeps_long() {
277        let tracker = BrainWaveTracker::new(BrainWaveConfig::default());
278        assert_eq!(tracker.current(), BrainWave::Delta);
279        let d = tracker.next_transition_duration();
280        assert_eq!(d, Duration::from_secs(3600));
281    }
282
283    #[test]
284    fn idle_duration_grows_after_event() {
285        let mut tracker = BrainWaveTracker::new(BrainWaveConfig::default());
286        let _ = tracker.record_event();
287        let d1 = tracker.idle_duration();
288        std::thread::sleep(Duration::from_millis(50));
289        let d2 = tracker.idle_duration();
290        assert!(d2 > d1);
291    }
292}