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