Skip to main content

lean_ctx/core/
memory_policy.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, Default)]
4#[serde(default)]
5pub struct MemoryPolicy {
6    pub knowledge: KnowledgePolicy,
7    pub episodic: EpisodicPolicy,
8    pub procedural: ProceduralPolicy,
9    pub lifecycle: LifecyclePolicy,
10    pub embeddings: EmbeddingsPolicy,
11}
12
13impl MemoryPolicy {
14    pub fn apply_env_overrides(&mut self) {
15        self.knowledge.apply_env_overrides();
16        self.episodic.apply_env_overrides();
17        self.procedural.apply_env_overrides();
18        self.lifecycle.apply_env_overrides();
19        self.embeddings.apply_env_overrides();
20    }
21
22    pub fn validate(&self) -> Result<(), String> {
23        self.knowledge.validate()?;
24        self.episodic.validate()?;
25        self.procedural.validate()?;
26        self.lifecycle.validate()?;
27        self.embeddings.validate()?;
28        Ok(())
29    }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(default)]
34pub struct KnowledgePolicy {
35    pub max_facts: usize,
36    pub max_patterns: usize,
37    pub max_history: usize,
38    pub contradiction_threshold: f32,
39}
40
41impl Default for KnowledgePolicy {
42    fn default() -> Self {
43        Self {
44            max_facts: 200,
45            max_patterns: 50,
46            max_history: 100,
47            contradiction_threshold: 0.5,
48        }
49    }
50}
51
52impl KnowledgePolicy {
53    fn apply_env_overrides(&mut self) {
54        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_FACTS") {
55            if let Ok(n) = v.parse() {
56                self.max_facts = n;
57            }
58        }
59        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_PATTERNS") {
60            if let Ok(n) = v.parse() {
61                self.max_patterns = n;
62            }
63        }
64        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_HISTORY") {
65            if let Ok(n) = v.parse() {
66                self.max_history = n;
67            }
68        }
69        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_CONTRADICTION_THRESHOLD") {
70            if let Ok(n) = v.parse() {
71                self.contradiction_threshold = n;
72            }
73        }
74    }
75
76    fn validate(&self) -> Result<(), String> {
77        if self.max_facts == 0 {
78            return Err("memory.knowledge.max_facts must be > 0".to_string());
79        }
80        if self.max_patterns == 0 {
81            return Err("memory.knowledge.max_patterns must be > 0".to_string());
82        }
83        if self.max_history == 0 {
84            return Err("memory.knowledge.max_history must be > 0".to_string());
85        }
86        if !(0.0..=1.0).contains(&self.contradiction_threshold) {
87            return Err(
88                "memory.knowledge.contradiction_threshold must be in [0.0, 1.0]".to_string(),
89            );
90        }
91        Ok(())
92    }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96#[serde(default)]
97pub struct EpisodicPolicy {
98    pub max_episodes: usize,
99    pub max_actions_per_episode: usize,
100    pub summary_max_chars: usize,
101}
102
103impl Default for EpisodicPolicy {
104    fn default() -> Self {
105        Self {
106            max_episodes: 500,
107            max_actions_per_episode: 50,
108            summary_max_chars: 200,
109        }
110    }
111}
112
113impl EpisodicPolicy {
114    fn apply_env_overrides(&mut self) {
115        if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_MAX_EPISODES") {
116            if let Ok(n) = v.parse() {
117                self.max_episodes = n;
118            }
119        }
120        if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_MAX_ACTIONS_PER_EPISODE") {
121            if let Ok(n) = v.parse() {
122                self.max_actions_per_episode = n;
123            }
124        }
125        if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_SUMMARY_MAX_CHARS") {
126            if let Ok(n) = v.parse() {
127                self.summary_max_chars = n;
128            }
129        }
130    }
131
132    fn validate(&self) -> Result<(), String> {
133        if self.max_episodes == 0 {
134            return Err("memory.episodic.max_episodes must be > 0".to_string());
135        }
136        if self.max_actions_per_episode == 0 {
137            return Err("memory.episodic.max_actions_per_episode must be > 0".to_string());
138        }
139        if self.summary_max_chars < 40 {
140            return Err("memory.episodic.summary_max_chars must be >= 40".to_string());
141        }
142        Ok(())
143    }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(default)]
148pub struct ProceduralPolicy {
149    pub min_repetitions: usize,
150    pub min_sequence_len: usize,
151    pub max_procedures: usize,
152    pub max_window_size: usize,
153}
154
155impl Default for ProceduralPolicy {
156    fn default() -> Self {
157        Self {
158            min_repetitions: 3,
159            min_sequence_len: 2,
160            max_procedures: 100,
161            max_window_size: 10,
162        }
163    }
164}
165
166impl ProceduralPolicy {
167    fn apply_env_overrides(&mut self) {
168        if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS") {
169            if let Ok(n) = v.parse() {
170                self.min_repetitions = n;
171            }
172        }
173        if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MIN_SEQUENCE_LEN") {
174            if let Ok(n) = v.parse() {
175                self.min_sequence_len = n;
176            }
177        }
178        if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MAX_PROCEDURES") {
179            if let Ok(n) = v.parse() {
180                self.max_procedures = n;
181            }
182        }
183        if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MAX_WINDOW_SIZE") {
184            if let Ok(n) = v.parse() {
185                self.max_window_size = n;
186            }
187        }
188    }
189
190    fn validate(&self) -> Result<(), String> {
191        if self.min_repetitions == 0 {
192            return Err("memory.procedural.min_repetitions must be > 0".to_string());
193        }
194        if self.min_sequence_len < 2 {
195            return Err("memory.procedural.min_sequence_len must be >= 2".to_string());
196        }
197        if self.max_procedures == 0 {
198            return Err("memory.procedural.max_procedures must be > 0".to_string());
199        }
200        if self.max_window_size < self.min_sequence_len {
201            return Err(
202                "memory.procedural.max_window_size must be >= min_sequence_len".to_string(),
203            );
204        }
205        Ok(())
206    }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210#[serde(default)]
211pub struct LifecyclePolicy {
212    pub decay_rate: f32,
213    pub low_confidence_threshold: f32,
214    pub stale_days: i64,
215    pub similarity_threshold: f32,
216}
217
218impl Default for LifecyclePolicy {
219    fn default() -> Self {
220        Self {
221            decay_rate: 0.01,
222            low_confidence_threshold: 0.3,
223            stale_days: 30,
224            similarity_threshold: 0.85,
225        }
226    }
227}
228
229impl LifecyclePolicy {
230    fn apply_env_overrides(&mut self) {
231        if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_DECAY_RATE") {
232            if let Ok(n) = v.parse() {
233                self.decay_rate = n;
234            }
235        }
236        if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_LOW_CONFIDENCE_THRESHOLD") {
237            if let Ok(n) = v.parse() {
238                self.low_confidence_threshold = n;
239            }
240        }
241        if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_STALE_DAYS") {
242            if let Ok(n) = v.parse() {
243                self.stale_days = n;
244            }
245        }
246        if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_SIMILARITY_THRESHOLD") {
247            if let Ok(n) = v.parse() {
248                self.similarity_threshold = n;
249            }
250        }
251    }
252
253    fn validate(&self) -> Result<(), String> {
254        if !(0.0..=1.0).contains(&self.decay_rate) {
255            return Err("memory.lifecycle.decay_rate must be in [0.0, 1.0]".to_string());
256        }
257        if !(0.0..=1.0).contains(&self.low_confidence_threshold) {
258            return Err(
259                "memory.lifecycle.low_confidence_threshold must be in [0.0, 1.0]".to_string(),
260            );
261        }
262        if self.stale_days < 0 {
263            return Err("memory.lifecycle.stale_days must be >= 0".to_string());
264        }
265        if !(0.0..=1.0).contains(&self.similarity_threshold) {
266            return Err("memory.lifecycle.similarity_threshold must be in [0.0, 1.0]".to_string());
267        }
268        Ok(())
269    }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
273#[serde(default)]
274pub struct EmbeddingsPolicy {
275    pub max_facts: usize,
276}
277
278impl Default for EmbeddingsPolicy {
279    fn default() -> Self {
280        Self { max_facts: 2000 }
281    }
282}
283
284impl EmbeddingsPolicy {
285    fn apply_env_overrides(&mut self) {
286        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_EMBEDDINGS_MAX_FACTS") {
287            if let Ok(n) = v.parse() {
288                self.max_facts = n;
289            }
290        }
291    }
292
293    fn validate(&self) -> Result<(), String> {
294        if self.max_facts == 0 {
295            return Err("memory.embeddings.max_facts must be > 0".to_string());
296        }
297        Ok(())
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    fn restore_env(key: &str, prev: Option<String>) {
306        match prev {
307            Some(v) => std::env::set_var(key, v),
308            None => std::env::remove_var(key),
309        }
310    }
311
312    #[test]
313    fn default_policy_is_valid() {
314        let p = MemoryPolicy::default();
315        p.validate().expect("default policy must be valid");
316    }
317
318    #[test]
319    fn env_overrides_apply() {
320        let _lock = crate::core::data_dir::test_env_lock();
321
322        let prev_facts = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_FACTS").ok();
323        let prev_stale = std::env::var("LEAN_CTX_LIFECYCLE_STALE_DAYS").ok();
324        let prev_rep = std::env::var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS").ok();
325
326        std::env::set_var("LEAN_CTX_KNOWLEDGE_MAX_FACTS", "123");
327        std::env::set_var("LEAN_CTX_LIFECYCLE_STALE_DAYS", "7");
328        std::env::set_var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS", "4");
329
330        let mut p = MemoryPolicy::default();
331        p.apply_env_overrides();
332
333        assert_eq!(p.knowledge.max_facts, 123);
334        assert_eq!(p.lifecycle.stale_days, 7);
335        assert_eq!(p.procedural.min_repetitions, 4);
336
337        restore_env("LEAN_CTX_KNOWLEDGE_MAX_FACTS", prev_facts);
338        restore_env("LEAN_CTX_LIFECYCLE_STALE_DAYS", prev_stale);
339        restore_env("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS", prev_rep);
340    }
341
342    #[test]
343    fn validate_rejects_invalid_values() {
344        let mut p = MemoryPolicy::default();
345        p.knowledge.max_facts = 0;
346        assert!(p.validate().is_err());
347
348        let mut p = MemoryPolicy::default();
349        p.lifecycle.decay_rate = 2.0;
350        assert!(p.validate().is_err());
351
352        let mut p = MemoryPolicy::default();
353        p.procedural.min_sequence_len = 1;
354        assert!(p.validate().is_err());
355    }
356}