Skip to main content

lean_ctx/tools/
autonomy.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2
3use crate::core::cache::SessionCache;
4use crate::core::config::AutonomyConfig;
5use crate::core::graph_index::ProjectIndex;
6use crate::core::protocol;
7use crate::core::tokens::count_tokens;
8use crate::tools::CrpMode;
9
10pub struct AutonomyState {
11    pub session_initialized: AtomicBool,
12    pub dedup_applied: AtomicBool,
13    pub config: AutonomyConfig,
14}
15
16impl Default for AutonomyState {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl AutonomyState {
23    pub fn new() -> Self {
24        Self {
25            session_initialized: AtomicBool::new(false),
26            dedup_applied: AtomicBool::new(false),
27            config: AutonomyConfig::load(),
28        }
29    }
30
31    pub fn is_enabled(&self) -> bool {
32        self.config.enabled
33    }
34}
35
36pub fn session_lifecycle_pre_hook(
37    state: &AutonomyState,
38    tool_name: &str,
39    cache: &mut SessionCache,
40    task: Option<&str>,
41    project_root: Option<&str>,
42    crp_mode: CrpMode,
43) -> Option<String> {
44    if !state.is_enabled() || !state.config.auto_preload {
45        return None;
46    }
47
48    if tool_name == "ctx_overview" || tool_name == "ctx_preload" {
49        return None;
50    }
51
52    let root = match project_root {
53        Some(r) if !r.is_empty() && r != "." => r.to_string(),
54        _ => return None,
55    };
56
57    if state
58        .session_initialized
59        .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
60        .is_err()
61    {
62        return None;
63    }
64
65    let result = if let Some(task_desc) = task {
66        crate::tools::ctx_preload::handle(cache, task_desc, Some(&root), crp_mode)
67    } else {
68        let cache_readonly = &*cache;
69        crate::tools::ctx_overview::handle(cache_readonly, None, Some(&root), crp_mode)
70    };
71
72    if result.contains("No directly relevant files") || result.trim().is_empty() {
73        return None;
74    }
75
76    Some(format!(
77        "--- AUTO CONTEXT ---\n{result}\n--- END AUTO CONTEXT ---"
78    ))
79}
80
81pub fn enrich_after_read(
82    state: &AutonomyState,
83    cache: &mut SessionCache,
84    file_path: &str,
85    project_root: Option<&str>,
86) -> EnrichResult {
87    let mut result = EnrichResult::default();
88
89    if !state.is_enabled() {
90        return result;
91    }
92
93    let root = match project_root {
94        Some(r) if !r.is_empty() && r != "." => r.to_string(),
95        _ => return result,
96    };
97
98    let index = crate::core::graph_index::load_or_build(&root);
99    if index.files.is_empty() {
100        return result;
101    }
102
103    if state.config.auto_related {
104        result.related_hint = build_related_hints(cache, file_path, &index);
105    }
106
107    if state.config.silent_preload {
108        silent_preload_imports(cache, file_path, &index, &root);
109    }
110
111    result
112}
113
114#[derive(Default)]
115pub struct EnrichResult {
116    pub related_hint: Option<String>,
117}
118
119fn build_related_hints(
120    cache: &SessionCache,
121    file_path: &str,
122    index: &ProjectIndex,
123) -> Option<String> {
124    let related: Vec<_> = index
125        .edges
126        .iter()
127        .filter(|e| e.from == file_path || e.to == file_path)
128        .map(|e| if e.from == file_path { &e.to } else { &e.from })
129        .filter(|path| cache.get(path).is_none())
130        .take(3)
131        .collect();
132
133    if related.is_empty() {
134        return None;
135    }
136
137    let hints: Vec<String> = related.iter().map(|p| protocol::shorten_path(p)).collect();
138
139    Some(format!("[related: {}]", hints.join(", ")))
140}
141
142fn silent_preload_imports(
143    cache: &mut SessionCache,
144    file_path: &str,
145    index: &ProjectIndex,
146    _project_root: &str,
147) {
148    let imports: Vec<String> = index
149        .edges
150        .iter()
151        .filter(|e| e.from == file_path)
152        .map(|e| e.to.clone())
153        .filter(|path| cache.get(path).is_none())
154        .take(2)
155        .collect();
156
157    for path in imports {
158        if let Ok(content) = std::fs::read_to_string(&path) {
159            let tokens = count_tokens(&content);
160            if tokens < 5000 {
161                cache.store(&path, content);
162            }
163        }
164    }
165}
166
167pub fn maybe_auto_dedup(state: &AutonomyState, cache: &mut SessionCache) {
168    if !state.is_enabled() || !state.config.auto_dedup {
169        return;
170    }
171
172    if state
173        .dedup_applied
174        .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
175        .is_err()
176    {
177        return;
178    }
179
180    let entries = cache.get_all_entries();
181    if entries.len() < state.config.dedup_threshold {
182        state.dedup_applied.store(false, Ordering::SeqCst);
183        return;
184    }
185
186    crate::tools::ctx_dedup::handle_action(cache, "apply");
187}
188
189pub fn shell_efficiency_hint(
190    state: &AutonomyState,
191    command: &str,
192    input_tokens: usize,
193    output_tokens: usize,
194) -> Option<String> {
195    if !state.is_enabled() {
196        return None;
197    }
198
199    if input_tokens == 0 {
200        return None;
201    }
202
203    let savings_pct = ((input_tokens - output_tokens) as f64 / input_tokens as f64) * 100.0;
204    if savings_pct >= 20.0 {
205        return None;
206    }
207
208    let cmd_lower = command.to_lowercase();
209    if cmd_lower.starts_with("grep ")
210        || cmd_lower.starts_with("rg ")
211        || cmd_lower.starts_with("find ")
212        || cmd_lower.starts_with("ag ")
213    {
214        return Some("[hint: ctx_search is more token-efficient for code search]".to_string());
215    }
216
217    if cmd_lower.starts_with("cat ") || cmd_lower.starts_with("head ") {
218        return Some("[hint: ctx_read provides cached, compressed file access]".to_string());
219    }
220
221    None
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn autonomy_state_starts_uninitialized() {
230        let state = AutonomyState::new();
231        assert!(!state.session_initialized.load(Ordering::SeqCst));
232        assert!(!state.dedup_applied.load(Ordering::SeqCst));
233    }
234
235    #[test]
236    fn session_initialized_fires_once() {
237        let state = AutonomyState::new();
238        let first = state.session_initialized.compare_exchange(
239            false,
240            true,
241            Ordering::SeqCst,
242            Ordering::SeqCst,
243        );
244        assert!(first.is_ok());
245        let second = state.session_initialized.compare_exchange(
246            false,
247            true,
248            Ordering::SeqCst,
249            Ordering::SeqCst,
250        );
251        assert!(second.is_err());
252    }
253
254    #[test]
255    fn shell_hint_for_grep() {
256        let state = AutonomyState::new();
257        let hint = shell_efficiency_hint(&state, "grep -rn foo .", 100, 95);
258        assert!(hint.is_some());
259        assert!(hint.unwrap().contains("ctx_search"));
260    }
261
262    #[test]
263    fn shell_hint_none_when_good_savings() {
264        let state = AutonomyState::new();
265        let hint = shell_efficiency_hint(&state, "grep -rn foo .", 100, 50);
266        assert!(hint.is_none());
267    }
268
269    #[test]
270    fn shell_hint_none_for_unknown_command() {
271        let state = AutonomyState::new();
272        let hint = shell_efficiency_hint(&state, "cargo build", 100, 95);
273        assert!(hint.is_none());
274    }
275
276    #[test]
277    fn disabled_state_blocks_all() {
278        let mut state = AutonomyState::new();
279        state.config.enabled = false;
280        assert!(!state.is_enabled());
281        let hint = shell_efficiency_hint(&state, "grep foo", 100, 95);
282        assert!(hint.is_none());
283    }
284}