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    pub gotcha: GotchaPolicy,
12}
13
14impl MemoryPolicy {
15    pub fn apply_env_overrides(&mut self) {
16        self.knowledge.apply_env_overrides();
17        self.episodic.apply_env_overrides();
18        self.procedural.apply_env_overrides();
19        self.lifecycle.apply_env_overrides();
20        self.embeddings.apply_env_overrides();
21        self.gotcha.apply_env_overrides();
22    }
23
24    pub fn apply_overrides(&mut self, o: &MemoryPolicyOverrides) {
25        self.knowledge.apply_overrides(&o.knowledge);
26        self.lifecycle.apply_overrides(&o.lifecycle);
27    }
28
29    pub fn validate(&self) -> Result<(), String> {
30        self.knowledge.validate()?;
31        self.episodic.validate()?;
32        self.procedural.validate()?;
33        self.lifecycle.validate()?;
34        self.embeddings.validate()?;
35        self.gotcha.validate()?;
36        Ok(())
37    }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, Default)]
41#[serde(default)]
42pub struct MemoryPolicyOverrides {
43    pub knowledge: KnowledgePolicyOverrides,
44    pub lifecycle: LifecyclePolicyOverrides,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, Default)]
48#[serde(default)]
49pub struct KnowledgePolicyOverrides {
50    pub max_facts: Option<usize>,
51    pub max_patterns: Option<usize>,
52    pub max_history: Option<usize>,
53    pub contradiction_threshold: Option<f32>,
54    pub recall_facts_limit: Option<usize>,
55    pub rooms_limit: Option<usize>,
56    pub timeline_limit: Option<usize>,
57    pub relations_limit: Option<usize>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, Default)]
61#[serde(default)]
62pub struct LifecyclePolicyOverrides {
63    pub decay_rate: Option<f32>,
64    pub low_confidence_threshold: Option<f32>,
65    pub stale_days: Option<i64>,
66    pub similarity_threshold: Option<f32>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70#[serde(default)]
71pub struct KnowledgePolicy {
72    pub max_facts: usize,
73    pub max_patterns: usize,
74    pub max_history: usize,
75    pub contradiction_threshold: f32,
76    /// Maximum number of facts returned by recall operations.
77    pub recall_facts_limit: usize,
78    /// Maximum number of rooms returned by `ctx_knowledge action=rooms`.
79    pub rooms_limit: usize,
80    /// Maximum number of timeline entries returned by `ctx_knowledge action=timeline`.
81    pub timeline_limit: usize,
82    /// Maximum number of relations/edges returned by relations queries/diagrams.
83    pub relations_limit: usize,
84}
85
86impl Default for KnowledgePolicy {
87    fn default() -> Self {
88        Self {
89            max_facts: 200,
90            max_patterns: 50,
91            max_history: 100,
92            contradiction_threshold: 0.5,
93            recall_facts_limit: crate::core::budgets::KNOWLEDGE_RECALL_FACTS_LIMIT,
94            rooms_limit: crate::core::budgets::KNOWLEDGE_ROOMS_LIMIT,
95            timeline_limit: crate::core::budgets::KNOWLEDGE_TIMELINE_LIMIT,
96            relations_limit: 40,
97        }
98    }
99}
100
101impl KnowledgePolicy {
102    fn apply_env_overrides(&mut self) {
103        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_FACTS") {
104            if let Ok(n) = v.parse() {
105                self.max_facts = n;
106            }
107        }
108        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_PATTERNS") {
109            if let Ok(n) = v.parse() {
110                self.max_patterns = n;
111            }
112        }
113        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_HISTORY") {
114            if let Ok(n) = v.parse() {
115                self.max_history = n;
116            }
117        }
118        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_CONTRADICTION_THRESHOLD") {
119            if let Ok(n) = v.parse() {
120                self.contradiction_threshold = n;
121            }
122        }
123        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_RECALL_FACTS_LIMIT") {
124            if let Ok(n) = v.parse() {
125                self.recall_facts_limit = n;
126            }
127        }
128        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_ROOMS_LIMIT") {
129            if let Ok(n) = v.parse() {
130                self.rooms_limit = n;
131            }
132        }
133        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_TIMELINE_LIMIT") {
134            if let Ok(n) = v.parse() {
135                self.timeline_limit = n;
136            }
137        }
138        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_RELATIONS_LIMIT") {
139            if let Ok(n) = v.parse() {
140                self.relations_limit = n;
141            }
142        }
143    }
144
145    fn validate(&self) -> Result<(), String> {
146        if self.max_facts == 0 {
147            return Err("memory.knowledge.max_facts must be > 0".to_string());
148        }
149        if self.max_patterns == 0 {
150            return Err("memory.knowledge.max_patterns must be > 0".to_string());
151        }
152        if self.max_history == 0 {
153            return Err("memory.knowledge.max_history must be > 0".to_string());
154        }
155        if !(0.0..=1.0).contains(&self.contradiction_threshold) {
156            return Err(
157                "memory.knowledge.contradiction_threshold must be in [0.0, 1.0]".to_string(),
158            );
159        }
160        if self.recall_facts_limit == 0 {
161            return Err("memory.knowledge.recall_facts_limit must be > 0".to_string());
162        }
163        if self.rooms_limit == 0 {
164            return Err("memory.knowledge.rooms_limit must be > 0".to_string());
165        }
166        if self.timeline_limit == 0 {
167            return Err("memory.knowledge.timeline_limit must be > 0".to_string());
168        }
169        if self.relations_limit == 0 {
170            return Err("memory.knowledge.relations_limit must be > 0".to_string());
171        }
172        Ok(())
173    }
174
175    fn apply_overrides(&mut self, o: &KnowledgePolicyOverrides) {
176        if let Some(v) = o.max_facts {
177            self.max_facts = v;
178        }
179        if let Some(v) = o.max_patterns {
180            self.max_patterns = v;
181        }
182        if let Some(v) = o.max_history {
183            self.max_history = v;
184        }
185        if let Some(v) = o.contradiction_threshold {
186            self.contradiction_threshold = v;
187        }
188        if let Some(v) = o.recall_facts_limit {
189            self.recall_facts_limit = v;
190        }
191        if let Some(v) = o.rooms_limit {
192            self.rooms_limit = v;
193        }
194        if let Some(v) = o.timeline_limit {
195            self.timeline_limit = v;
196        }
197        if let Some(v) = o.relations_limit {
198            self.relations_limit = v;
199        }
200    }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[serde(default)]
205pub struct EpisodicPolicy {
206    pub max_episodes: usize,
207    pub max_actions_per_episode: usize,
208    pub summary_max_chars: usize,
209}
210
211impl Default for EpisodicPolicy {
212    fn default() -> Self {
213        Self {
214            max_episodes: 500,
215            max_actions_per_episode: 50,
216            summary_max_chars: 200,
217        }
218    }
219}
220
221impl EpisodicPolicy {
222    fn apply_env_overrides(&mut self) {
223        if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_MAX_EPISODES") {
224            if let Ok(n) = v.parse() {
225                self.max_episodes = n;
226            }
227        }
228        if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_MAX_ACTIONS_PER_EPISODE") {
229            if let Ok(n) = v.parse() {
230                self.max_actions_per_episode = n;
231            }
232        }
233        if let Ok(v) = std::env::var("LEAN_CTX_EPISODIC_SUMMARY_MAX_CHARS") {
234            if let Ok(n) = v.parse() {
235                self.summary_max_chars = n;
236            }
237        }
238    }
239
240    fn validate(&self) -> Result<(), String> {
241        if self.max_episodes == 0 {
242            return Err("memory.episodic.max_episodes must be > 0".to_string());
243        }
244        if self.max_actions_per_episode == 0 {
245            return Err("memory.episodic.max_actions_per_episode must be > 0".to_string());
246        }
247        if self.summary_max_chars < 40 {
248            return Err("memory.episodic.summary_max_chars must be >= 40".to_string());
249        }
250        Ok(())
251    }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(default)]
256pub struct ProceduralPolicy {
257    pub min_repetitions: usize,
258    pub min_sequence_len: usize,
259    pub max_procedures: usize,
260    pub max_window_size: usize,
261}
262
263impl Default for ProceduralPolicy {
264    fn default() -> Self {
265        Self {
266            min_repetitions: 3,
267            min_sequence_len: 2,
268            max_procedures: 100,
269            max_window_size: 10,
270        }
271    }
272}
273
274impl ProceduralPolicy {
275    fn apply_env_overrides(&mut self) {
276        if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS") {
277            if let Ok(n) = v.parse() {
278                self.min_repetitions = n;
279            }
280        }
281        if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MIN_SEQUENCE_LEN") {
282            if let Ok(n) = v.parse() {
283                self.min_sequence_len = n;
284            }
285        }
286        if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MAX_PROCEDURES") {
287            if let Ok(n) = v.parse() {
288                self.max_procedures = n;
289            }
290        }
291        if let Ok(v) = std::env::var("LEAN_CTX_PROCEDURAL_MAX_WINDOW_SIZE") {
292            if let Ok(n) = v.parse() {
293                self.max_window_size = n;
294            }
295        }
296    }
297
298    fn validate(&self) -> Result<(), String> {
299        if self.min_repetitions == 0 {
300            return Err("memory.procedural.min_repetitions must be > 0".to_string());
301        }
302        if self.min_sequence_len < 2 {
303            return Err("memory.procedural.min_sequence_len must be >= 2".to_string());
304        }
305        if self.max_procedures == 0 {
306            return Err("memory.procedural.max_procedures must be > 0".to_string());
307        }
308        if self.max_window_size < self.min_sequence_len {
309            return Err(
310                "memory.procedural.max_window_size must be >= min_sequence_len".to_string(),
311            );
312        }
313        Ok(())
314    }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(default)]
319pub struct LifecyclePolicy {
320    pub decay_rate: f32,
321    pub low_confidence_threshold: f32,
322    pub stale_days: i64,
323    pub similarity_threshold: f32,
324}
325
326impl Default for LifecyclePolicy {
327    fn default() -> Self {
328        Self {
329            decay_rate: 0.01,
330            low_confidence_threshold: 0.3,
331            stale_days: 30,
332            similarity_threshold: 0.85,
333        }
334    }
335}
336
337impl LifecyclePolicy {
338    fn apply_env_overrides(&mut self) {
339        if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_DECAY_RATE") {
340            if let Ok(n) = v.parse() {
341                self.decay_rate = n;
342            }
343        }
344        if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_LOW_CONFIDENCE_THRESHOLD") {
345            if let Ok(n) = v.parse() {
346                self.low_confidence_threshold = n;
347            }
348        }
349        if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_STALE_DAYS") {
350            if let Ok(n) = v.parse() {
351                self.stale_days = n;
352            }
353        }
354        if let Ok(v) = std::env::var("LEAN_CTX_LIFECYCLE_SIMILARITY_THRESHOLD") {
355            if let Ok(n) = v.parse() {
356                self.similarity_threshold = n;
357            }
358        }
359    }
360
361    fn validate(&self) -> Result<(), String> {
362        if !(0.0..=1.0).contains(&self.decay_rate) {
363            return Err("memory.lifecycle.decay_rate must be in [0.0, 1.0]".to_string());
364        }
365        if !(0.0..=1.0).contains(&self.low_confidence_threshold) {
366            return Err(
367                "memory.lifecycle.low_confidence_threshold must be in [0.0, 1.0]".to_string(),
368            );
369        }
370        if self.stale_days < 0 {
371            return Err("memory.lifecycle.stale_days must be >= 0".to_string());
372        }
373        if !(0.0..=1.0).contains(&self.similarity_threshold) {
374            return Err("memory.lifecycle.similarity_threshold must be in [0.0, 1.0]".to_string());
375        }
376        Ok(())
377    }
378
379    fn apply_overrides(&mut self, o: &LifecyclePolicyOverrides) {
380        if let Some(v) = o.decay_rate {
381            self.decay_rate = v;
382        }
383        if let Some(v) = o.low_confidence_threshold {
384            self.low_confidence_threshold = v;
385        }
386        if let Some(v) = o.stale_days {
387            self.stale_days = v;
388        }
389        if let Some(v) = o.similarity_threshold {
390            self.similarity_threshold = v;
391        }
392    }
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
396#[serde(default)]
397pub struct EmbeddingsPolicy {
398    pub max_facts: usize,
399}
400
401impl Default for EmbeddingsPolicy {
402    fn default() -> Self {
403        Self { max_facts: 2000 }
404    }
405}
406
407impl EmbeddingsPolicy {
408    fn apply_env_overrides(&mut self) {
409        if let Ok(v) = std::env::var("LEAN_CTX_KNOWLEDGE_EMBEDDINGS_MAX_FACTS") {
410            if let Ok(n) = v.parse() {
411                self.max_facts = n;
412            }
413        }
414    }
415
416    fn validate(&self) -> Result<(), String> {
417        if self.max_facts == 0 {
418            return Err("memory.embeddings.max_facts must be > 0".to_string());
419        }
420        Ok(())
421    }
422}
423
424use std::collections::HashMap;
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
427#[serde(default)]
428pub struct GotchaPolicy {
429    pub max_gotchas_per_project: usize,
430    pub retrieval_budget_per_room: usize,
431    pub default_decay_rate: f32,
432    pub category_decay_overrides: HashMap<String, f32>,
433    pub auto_expire_days: Option<i64>,
434}
435
436impl Default for GotchaPolicy {
437    fn default() -> Self {
438        Self {
439            max_gotchas_per_project: 100,
440            retrieval_budget_per_room: 10,
441            default_decay_rate: 0.03,
442            category_decay_overrides: HashMap::new(),
443            auto_expire_days: None,
444        }
445    }
446}
447
448impl GotchaPolicy {
449    fn apply_env_overrides(&mut self) {
450        if let Ok(v) = std::env::var("LEAN_CTX_GOTCHA_MAX_PER_PROJECT") {
451            if let Ok(n) = v.parse() {
452                self.max_gotchas_per_project = n;
453            }
454        }
455        if let Ok(v) = std::env::var("LEAN_CTX_GOTCHA_RETRIEVAL_BUDGET") {
456            if let Ok(n) = v.parse() {
457                self.retrieval_budget_per_room = n;
458            }
459        }
460    }
461
462    fn validate(&self) -> Result<(), String> {
463        if self.max_gotchas_per_project == 0 {
464            return Err("memory.gotcha.max_gotchas_per_project must be > 0".to_string());
465        }
466        if self.retrieval_budget_per_room == 0 {
467            return Err("memory.gotcha.retrieval_budget_per_room must be > 0".to_string());
468        }
469        if !(0.0..=1.0).contains(&self.default_decay_rate) {
470            return Err("memory.gotcha.default_decay_rate must be 0.0-1.0".to_string());
471        }
472        Ok(())
473    }
474
475    pub fn effective_decay_rate(&self, category: &str) -> f32 {
476        self.category_decay_overrides
477            .get(category)
478            .copied()
479            .unwrap_or(self.default_decay_rate)
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    fn restore_env(key: &str, prev: Option<String>) {
488        match prev {
489            Some(v) => std::env::set_var(key, v),
490            None => std::env::remove_var(key),
491        }
492    }
493
494    #[test]
495    fn default_policy_is_valid() {
496        let p = MemoryPolicy::default();
497        p.validate().expect("default policy must be valid");
498    }
499
500    #[test]
501    fn env_overrides_apply() {
502        let _lock = crate::core::data_dir::test_env_lock();
503
504        let prev_facts = std::env::var("LEAN_CTX_KNOWLEDGE_MAX_FACTS").ok();
505        let prev_stale = std::env::var("LEAN_CTX_LIFECYCLE_STALE_DAYS").ok();
506        let prev_rep = std::env::var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS").ok();
507
508        std::env::set_var("LEAN_CTX_KNOWLEDGE_MAX_FACTS", "123");
509        std::env::set_var("LEAN_CTX_LIFECYCLE_STALE_DAYS", "7");
510        std::env::set_var("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS", "4");
511
512        let mut p = MemoryPolicy::default();
513        p.apply_env_overrides();
514
515        assert_eq!(p.knowledge.max_facts, 123);
516        assert_eq!(p.lifecycle.stale_days, 7);
517        assert_eq!(p.procedural.min_repetitions, 4);
518
519        restore_env("LEAN_CTX_KNOWLEDGE_MAX_FACTS", prev_facts);
520        restore_env("LEAN_CTX_LIFECYCLE_STALE_DAYS", prev_stale);
521        restore_env("LEAN_CTX_PROCEDURAL_MIN_REPETITIONS", prev_rep);
522    }
523
524    #[test]
525    fn validate_rejects_invalid_values() {
526        let mut p = MemoryPolicy::default();
527        p.knowledge.max_facts = 0;
528        assert!(p.validate().is_err());
529
530        let mut p = MemoryPolicy::default();
531        p.lifecycle.decay_rate = 2.0;
532        assert!(p.validate().is_err());
533
534        let mut p = MemoryPolicy::default();
535        p.procedural.min_sequence_len = 1;
536        assert!(p.validate().is_err());
537    }
538}