lean_ctx/tools/
autonomy.rs1use 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}