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 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}