lean_ctx_server/tools/
autonomy.rs1use std::sync::atomic::Ordering;
8
9pub use lean_ctx::core::autonomy::{AutonomyState, SearchHistory, should_auto_consolidate};
10use lean_ctx::core::tokens::count_tokens;
11
12fn profile_autonomy() -> lean_ctx::core::profiles::ProfileAutonomy {
13 lean_ctx::core::profiles::active_profile().autonomy
14}
15
16fn autonomy_enabled_effective(state: &AutonomyState) -> bool {
17 state.is_enabled() && profile_autonomy().enabled_effective()
18}
19
20pub fn delivers_session_briefing(
25 name: &str,
26 args: Option<&serde_json::Map<String, serde_json::Value>>,
27) -> bool {
28 let (name, args) = if name == "ctx_call" {
29 (
30 args.and_then(|a| a.get("name"))
31 .and_then(serde_json::Value::as_str)
32 .unwrap_or(""),
33 args.and_then(|a| a.get("arguments"))
34 .and_then(serde_json::Value::as_object),
35 )
36 } else {
37 (name, args)
38 };
39 name == "ctx_session"
40 && matches!(
41 args.and_then(|a| a.get("action"))
42 .and_then(serde_json::Value::as_str),
43 Some("load" | "status" | "show")
44 )
45}
46
47pub fn apply_auto_load_packages_once(state: &AutonomyState, project_root: Option<&str>) {
51 if lean_ctx::core::policy::runtime::is_active() || !autonomy_enabled_effective(state) {
52 return;
53 }
54 let Some(root) = project_root.filter(|r| !r.is_empty() && *r != ".") else {
55 return;
56 };
57 if state
58 .packages_applied
59 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
60 .is_ok()
61 {
62 let loaded = lean_ctx::core::context_package::auto_load_packages(root);
63 state
64 .packages_loaded
65 .lock()
66 .unwrap_or_else(std::sync::PoisonError::into_inner)
67 .extend(loaded);
68 }
69}
70
71pub fn session_wakeup_briefing(
81 state: &AutonomyState,
82 tool_name: &str,
83 task: Option<&str>,
84 project_root: Option<&str>,
85) -> Option<String> {
86 if lean_ctx::core::policy::runtime::is_active() || !autonomy_enabled_effective(state) {
88 return None;
89 }
90 if tool_name == "ctx_preload" {
92 return None;
93 }
94 let root = project_root.filter(|r| !r.is_empty() && *r != ".")?;
95 if state
96 .session_briefed
97 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
98 .is_err()
99 {
100 return None;
101 }
102 let mut loaded = std::mem::take(
105 &mut *state
106 .packages_loaded
107 .lock()
108 .unwrap_or_else(std::sync::PoisonError::into_inner),
109 );
110 if !state.packages_applied.swap(true, Ordering::SeqCst) {
111 loaded.extend(lean_ctx::core::context_package::auto_load_packages(root));
112 }
113 let packages = (!loaded.is_empty())
114 .then(|| format!("CONTEXT PACKAGES AUTO-LOADED: {}", loaded.join(", ")));
115 let tasks = lean_ctx_hive::tasks::briefing(std::path::Path::new(root));
117 let frame = |briefing: &str| format!("{AUTO_CONTEXT_OPEN}\n{briefing}\n{AUTO_CONTEXT_CLOSE}");
118 let briefing = if lean_ctx::core::config::Config::load().enable_wakeup_ctx {
119 let reserved = count_tokens(&frame(&format!(
120 "{}\n{}",
121 packages.as_deref().unwrap_or(""),
122 tasks.as_deref().unwrap_or("")
123 )));
124 let budget = AUTO_CONTEXT_MAX_TOKENS.saturating_sub(reserved);
125 crate::tools::ctx_overview::build_wakeup_briefing_within(root, task, budget)
126 } else {
127 String::new()
128 };
129 let body = [
130 packages.unwrap_or_default(),
131 tasks.unwrap_or_default(),
132 briefing,
133 ]
134 .into_iter()
135 .filter(|part| !part.trim().is_empty())
136 .collect::<Vec<_>>()
137 .join("\n");
138 (!body.is_empty()).then(|| frame(&body))
139}
140
141pub const AUTO_CONTEXT_MAX_TOKENS: usize = 400;
144const AUTO_CONTEXT_OPEN: &str = "--- AUTO CONTEXT ---";
145const AUTO_CONTEXT_CLOSE: &str = "--- END AUTO CONTEXT ---";
146
147pub fn shell_efficiency_hint(
149 state: &AutonomyState,
150 command: &str,
151 input_tokens: usize,
152 output_tokens: usize,
153) -> Option<String> {
154 if !autonomy_enabled_effective(state) {
155 return None;
156 }
157
158 if input_tokens == 0 {
159 return None;
160 }
161
162 let savings_pct =
163 (input_tokens.saturating_sub(output_tokens) as f64 / input_tokens as f64) * 100.0;
164 if savings_pct >= 20.0 {
165 return None;
166 }
167
168 let cmd_lower = command.to_lowercase();
169 if cmd_lower.starts_with("grep ")
170 || cmd_lower.starts_with("rg ")
171 || cmd_lower.starts_with("find ")
172 || cmd_lower.starts_with("ag ")
173 {
174 return Some("[hint: ctx_search is more token-efficient for code search]".to_string());
175 }
176
177 if cmd_lower.starts_with("cat ") || cmd_lower.starts_with("head ") {
178 return Some("[hint: ctx_read provides cached, compressed file access]".to_string());
179 }
180
181 None
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn only_ctx_session_load_or_status_delivers_the_briefing() {
190 let args = |v: serde_json::Value| v.as_object().cloned();
191 let status = args(serde_json::json!({"action": "status"}));
192 assert!(delivers_session_briefing("ctx_session", status.as_ref()));
193 let wrapped =
194 args(serde_json::json!({"name": "ctx_session", "arguments": {"action": "load"}}));
195 assert!(delivers_session_briefing("ctx_call", wrapped.as_ref()));
196 let task = args(serde_json::json!({"action": "task", "value": "x"}));
197 assert!(!delivers_session_briefing("ctx_session", task.as_ref()));
198 for tool in ["ctx_compose", "ctx_search", "ctx_read", "ctx_shell"] {
199 assert!(!delivers_session_briefing(tool, status.as_ref()), "{tool}");
200 }
201 }
202
203 #[test]
204 fn shell_hint_for_grep() {
205 let state = AutonomyState::new();
206 let hint = shell_efficiency_hint(&state, "grep -rn foo .", 100, 95);
207 assert!(hint.is_some());
208 assert!(hint.unwrap().contains("ctx_search"));
209 let hint = shell_efficiency_hint(&state, "cat src/main.rs", 100, 95);
210 assert!(hint.is_some_and(|h| h.contains("ctx_read")));
211 }
212
213 #[test]
214 fn shell_hint_none_when_good_savings() {
215 let state = AutonomyState::new();
216 let hint = shell_efficiency_hint(&state, "grep -rn foo .", 100, 50);
217 assert!(hint.is_none());
218 }
219
220 #[test]
221 fn shell_hint_none_for_unknown_command() {
222 let state = AutonomyState::new();
223 let hint = shell_efficiency_hint(&state, "cargo build", 100, 95);
224 assert!(hint.is_none());
225 }
226
227 #[test]
228 fn disabled_state_blocks_all() {
229 let mut state = AutonomyState::new();
230 state.config.enabled = false;
231 assert!(!state.is_enabled());
232 let hint = shell_efficiency_hint(&state, "grep foo", 100, 95);
233 assert!(hint.is_none());
234 }
235
236 #[test]
237 fn track_search_none_first_three() {
238 let _lock = lean_ctx::core::data_dir::test_env_lock();
239 let state = AutonomyState::new();
240 assert!(state.track_search("foo", "src").is_none());
241 assert!(state.track_search("foo", "src").is_none());
242 assert!(state.track_search("foo", "src").is_none());
243 }
244
245 #[test]
246 fn track_search_hint_band() {
247 let _lock = lean_ctx::core::data_dir::test_env_lock();
248 let state = AutonomyState::new();
249 for _ in 0..3 {
250 assert!(state.track_search("bar", ".").is_none());
251 }
252 let h = state.track_search("bar", ".").expect("hint on 4th");
253 assert!(h.starts_with("[hint: repeated search (4/6)."));
254 assert!(h.contains("ctx_knowledge"));
255 }
256
257 #[test]
258 fn track_search_throttle_seventh() {
259 let _lock = lean_ctx::core::data_dir::test_env_lock();
260 let state = AutonomyState::new();
261 for _ in 0..6 {
262 let _ = state.track_search("baz", "p");
263 }
264 let h = state.track_search("baz", "p").expect("throttle on 7th");
265 assert!(h.starts_with("[throttle: search repeated 7 times"));
266 assert!(h.contains("ctx_pack"));
267 }
268
269 #[test]
270 fn track_search_resets_after_idle() {
271 let _lock = lean_ctx::core::data_dir::test_env_lock();
272 let state = AutonomyState::new();
273 for _ in 0..3 {
274 assert!(state.track_search("idle", "x").is_none());
275 }
276 std::thread::sleep(std::time::Duration::from_millis(600));
277 assert!(
278 state.track_search("idle", "x").is_none(),
279 "count should reset after idle window"
280 );
281 }
282
283 #[test]
284 fn track_search_disabled_no_tracking_messages() {
285 let _lock = lean_ctx::core::data_dir::test_env_lock();
286 let mut state = AutonomyState::new();
287 state.config.enabled = false;
288 for _ in 0..8 {
289 assert!(state.track_search("q", "/").is_none());
290 }
291 }
292
293 #[test]
294 fn track_search_distinct_keys() {
295 let _lock = lean_ctx::core::data_dir::test_env_lock();
296 let state = AutonomyState::new();
297 assert!(state.track_search("pat", "a").is_none());
298 assert!(state.track_search("pat", "a").is_none());
299 assert!(state.track_search("pat", "a").is_none());
300 assert!(state.track_search("pat", "a").is_some());
301 assert!(state.track_search("pat", "b").is_none());
302 }
303}