lean_ctx/core/
tool_lifecycle.rs1use crate::core::context_ledger::ContextLedger;
12use crate::core::heatmap;
13use crate::core::intent_engine::StructuredIntent;
14use crate::core::session::SessionState;
15use crate::core::stats;
16
17const TRAVERSAL_WINDOW: usize = 6;
21
22pub(crate) fn recent_working_set(session: &SessionState, current: &str) -> Vec<String> {
25 let mut out: Vec<String> = Vec::new();
26 for f in session.files_touched.iter().rev() {
27 if f.path == current || out.contains(&f.path) {
28 continue;
29 }
30 out.push(f.path.clone());
31 if out.len() >= TRAVERSAL_WINDOW {
32 break;
33 }
34 }
35 out
36}
37
38pub(crate) fn usable_root(root: Option<&str>) -> Option<&str> {
40 root.filter(|r| !r.trim().is_empty() && *r != ".")
41}
42
43pub fn record_file_read(
45 path: &str,
46 mode: &str,
47 original_tokens: usize,
48 output_tokens: usize,
49 is_cache_hit: bool,
50) {
51 let saved = original_tokens.saturating_sub(output_tokens);
52 let tool_key = format!("cli_{mode}");
53
54 stats::record(&tool_key, original_tokens, output_tokens);
55 heatmap::record_file_access(path, original_tokens, saved);
56
57 if let Some(mut session) = SessionState::load_latest() {
58 session.touch_file(path, None, mode, original_tokens);
59 if is_cache_hit {
60 session.record_cache_hit();
61 }
62
63 if session.active_structured_intent.is_none() && session.files_touched.len() >= 2 {
64 let touched: Vec<String> = session
65 .files_touched
66 .iter()
67 .map(|ft| ft.path.clone())
68 .collect();
69 let inferred = StructuredIntent::from_file_patterns(&touched);
70 if inferred.confidence >= 0.4 {
71 session.active_structured_intent = Some(inferred);
72 }
73 }
74
75 let project_root = session.project_root.clone();
76 let calls = session.stats.total_tool_calls;
77
78 let working_set = recent_working_set(&session, path);
81
82 let _ = session.save();
83
84 if let Some(root) = usable_root(project_root.as_deref()) {
85 crate::core::cooccurrence::record_focus_access(root, path, &working_set);
86 }
87 maybe_consolidate(project_root.as_deref(), calls);
88 }
89
90 if std::path::Path::new(path).is_file() {
94 let mut ledger = ContextLedger::load();
95 ledger.record(path, mode, original_tokens, output_tokens);
96 ledger.save();
97 }
98}
99
100pub fn record_search(modeled_baseline: usize, observed_tokens: usize, output_tokens: usize) {
106 stats::record("cli_grep", modeled_baseline, output_tokens);
107 crate::core::savings_ledger::record_tool_event("cli_grep", observed_tokens, output_tokens);
108
109 if let Some(mut session) = SessionState::load_latest() {
110 session.record_command();
111 let project_root = session.project_root.clone();
112 let calls = session.stats.total_tool_calls;
113 let _ = session.save();
114
115 maybe_consolidate(project_root.as_deref(), calls);
116 }
117}
118
119pub fn record_tree(original_tokens: usize, output_tokens: usize) {
121 stats::record("cli_ls", original_tokens, output_tokens);
122
123 if let Some(mut session) = SessionState::load_latest() {
124 session.record_command();
125 let _ = session.save();
126 }
127}
128
129pub fn record_shell_command(original_tokens: usize, output_tokens: usize) {
133 stats::record("cli_shell", original_tokens, output_tokens);
134 crate::core::savings_ledger::record_tool_event("cli_shell", original_tokens, output_tokens);
137
138 if let Some(mut session) = SessionState::load_latest() {
139 session.record_command();
140 let project_root = session.project_root.clone();
141 let calls = session.stats.total_tool_calls;
142 let _ = session.save();
143
144 if original_tokens > 0 {
145 maybe_consolidate(project_root.as_deref(), calls);
146 }
147 }
148}
149
150fn maybe_consolidate(project_root: Option<&str>, calls: u32) {
153 let Some(root) = project_root else { return };
154 let autonomy = crate::tools::autonomy::AutonomyState::new();
155 if crate::tools::autonomy::should_auto_consolidate(&autonomy, calls) {
156 let root = root.to_string();
157 let _ = crate::core::consolidation_engine::consolidate_latest(
158 &root,
159 crate::core::consolidation_engine::ConsolidationBudgets::default(),
160 );
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn record_file_read_does_not_panic_without_session() {
170 record_file_read("/tmp/nonexistent.rs", "full", 100, 50, false);
171 }
172
173 #[test]
174 fn record_search_does_not_panic_without_session() {
175 record_search(500, 200, 150);
176 }
177
178 #[test]
179 fn record_tree_does_not_panic_without_session() {
180 record_tree(100, 80);
181 }
182
183 #[test]
184 fn record_shell_does_not_panic_without_session() {
185 record_shell_command(500, 200);
186 }
187}