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 pub recall_facts_limit: usize,
78 pub rooms_limit: usize,
80 pub timeline_limit: usize,
82 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}