lean_ctx/core/
autonomy.rs1use std::collections::HashMap;
2use std::sync::Mutex;
3use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
5
6use crate::core::config::AutonomyConfig;
7
8fn search_repeat_idle_reset() -> Duration {
12 #[cfg(any(test, feature = "test-support"))]
13 if crate::core::data_dir::sandboxed() {
14 return Duration::from_millis(500);
15 }
16 Duration::from_mins(5)
17}
18
19#[derive(Debug, Clone)]
21pub struct SearchHistory {
22 pub call_count: u32,
23 pub last_call: Instant,
24}
25
26pub struct AutonomyState {
28 pub session_briefed: AtomicBool,
30 pub packages_applied: AtomicBool,
32 pub packages_loaded: Mutex<Vec<String>>,
34 pub last_consolidation_unix: AtomicU64,
35 pub config: AutonomyConfig,
36 pub search_repetition: Mutex<HashMap<String, SearchHistory>>,
38}
39
40impl Default for AutonomyState {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl AutonomyState {
47 pub fn new() -> Self {
49 Self {
50 session_briefed: AtomicBool::new(false),
51 packages_applied: AtomicBool::new(false),
52 packages_loaded: Mutex::new(Vec::new()),
53 last_consolidation_unix: AtomicU64::new(0),
54 config: AutonomyConfig::load(),
55 search_repetition: Mutex::new(HashMap::new()),
56 }
57 }
58
59 pub fn is_enabled(&self) -> bool {
61 self.config.enabled
62 }
63
64 pub fn track_search(&self, pattern: &str, path: &str) -> Option<String> {
69 if !autonomy_enabled_effective(self) {
70 return None;
71 }
72 let key = format!("{pattern}|{path}");
73 let now = Instant::now();
74 let mut map = self
75 .search_repetition
76 .lock()
77 .unwrap_or_else(std::sync::PoisonError::into_inner);
78 let hist = map.entry(key).or_insert(SearchHistory {
79 call_count: 0,
80 last_call: now,
81 });
82 if hist.last_call.elapsed() >= search_repeat_idle_reset() {
83 hist.call_count = 0;
84 }
85 hist.call_count = hist.call_count.saturating_add(1);
86 hist.last_call = now;
87 let n = hist.call_count;
88
89 match n {
90 1..=3 => None,
91 4..=6 => Some(format!(
92 "[hint: repeated search ({n}/6). Consider ctx_knowledge remember to store findings]"
93 )),
94 _ => Some(format!(
95 "[throttle: search repeated {n} times on same pattern. Use ctx_pack or ctx_knowledge to consolidate]"
96 )),
97 }
98 }
99}
100
101fn autonomy_enabled_effective(state: &AutonomyState) -> bool {
102 state.is_enabled()
103 && crate::core::profiles::active_profile()
104 .autonomy
105 .enabled_effective()
106}
107
108pub fn should_auto_consolidate(state: &AutonomyState, tool_calls: u32) -> bool {
110 if !state.is_enabled() || !state.config.auto_consolidate {
111 return false;
112 }
113 let every = state.config.consolidate_every_calls.max(1);
114 if !tool_calls.is_multiple_of(every) {
115 return false;
116 }
117
118 let now = SystemTime::now()
119 .duration_since(UNIX_EPOCH)
120 .map_or(0, |d| d.as_secs());
121 let last = state.last_consolidation_unix.load(Ordering::SeqCst);
122 if now.saturating_sub(last) < state.config.consolidate_cooldown_secs {
123 return false;
124 }
125 state.last_consolidation_unix.store(now, Ordering::SeqCst);
126 true
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn consolidation_respects_call_interval_and_cooldown() {
135 let mut state = AutonomyState::new();
136 state.config.enabled = true;
137 state.config.auto_consolidate = true;
138 state.config.consolidate_every_calls = 5;
139 state.config.consolidate_cooldown_secs = 60;
140
141 assert!(!should_auto_consolidate(&state, 4));
142 assert!(should_auto_consolidate(&state, 5));
143 assert!(!should_auto_consolidate(&state, 10));
144 }
145
146 #[test]
147 fn consolidation_disabled_never_triggers() {
148 let mut state = AutonomyState::new();
149 state.config.enabled = false;
150 state.config.auto_consolidate = true;
151 state.config.consolidate_every_calls = 1;
152 state.config.consolidate_cooldown_secs = 0;
153
154 assert!(!should_auto_consolidate(&state, 1));
155 }
156}