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 crate::core::savings_ledger::record_read_event(original_tokens, saved);
62
63 if let Some(mut session) = SessionState::load_latest() {
64 session.touch_file(path, None, mode, original_tokens);
65 if is_cache_hit {
66 session.record_cache_hit();
67 }
68
69 if session.active_structured_intent.is_none() && session.files_touched.len() >= 2 {
70 let touched: Vec<String> = session
71 .files_touched
72 .iter()
73 .map(|ft| ft.path.clone())
74 .collect();
75 let inferred = StructuredIntent::from_file_patterns(&touched);
76 if inferred.confidence >= 0.4 {
77 session.active_structured_intent = Some(inferred);
78 }
79 }
80
81 let project_root = session.project_root.clone();
82 let calls = session.stats.total_tool_calls;
83
84 let working_set = recent_working_set(&session, path);
87
88 let _ = session.save();
89
90 if let Some(root) = usable_root(project_root.as_deref()) {
91 crate::core::cooccurrence::record_focus_access(root, path, &working_set);
92 }
93 maybe_consolidate(project_root.as_deref(), calls);
94 }
95
96 if std::path::Path::new(path).is_file() {
100 let mut ledger = ContextLedger::load();
101 ledger.record(path, mode, original_tokens, output_tokens);
102 ledger.save();
103 }
104}
105
106pub fn record_search(modeled_baseline: usize, observed_tokens: usize, output_tokens: usize) {
112 stats::record("cli_grep", modeled_baseline, output_tokens);
113 crate::core::savings_ledger::record_tool_event("cli_grep", observed_tokens, output_tokens);
114
115 if let Some(mut session) = SessionState::load_latest() {
116 session.record_command();
117 let project_root = session.project_root.clone();
118 let calls = session.stats.total_tool_calls;
119 let _ = session.save();
120
121 maybe_consolidate(project_root.as_deref(), calls);
122 }
123}
124
125pub fn record_tree(original_tokens: usize, output_tokens: usize) {
127 stats::record("cli_ls", original_tokens, output_tokens);
128
129 if let Some(mut session) = SessionState::load_latest() {
130 session.record_command();
131 let _ = session.save();
132 }
133}
134
135pub fn record_shell_command(original_tokens: usize, output_tokens: usize) {
139 stats::record("cli_shell", original_tokens, output_tokens);
140 crate::core::savings_ledger::record_tool_event("cli_shell", original_tokens, output_tokens);
143
144 if let Some(mut session) = SessionState::load_latest() {
145 session.record_command();
146 let project_root = session.project_root.clone();
147 let calls = session.stats.total_tool_calls;
148 let _ = session.save();
149
150 if original_tokens > 0 {
151 maybe_consolidate(project_root.as_deref(), calls);
152 }
153 }
154}
155
156fn maybe_consolidate(project_root: Option<&str>, calls: u32) {
159 let Some(root) = project_root else { return };
160 let autonomy = crate::tools::autonomy::AutonomyState::new();
161 if crate::tools::autonomy::should_auto_consolidate(&autonomy, calls) {
162 let root = root.to_string();
163 let _ = crate::core::consolidation_engine::consolidate_latest(
164 &root,
165 crate::core::consolidation_engine::ConsolidationBudgets::default(),
166 );
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn record_file_read_does_not_panic_without_session() {
176 record_file_read("/tmp/nonexistent.rs", "full", 100, 50, false);
177 }
178
179 #[test]
180 fn record_search_does_not_panic_without_session() {
181 record_search(500, 200, 150);
182 }
183
184 #[test]
185 fn record_tree_does_not_panic_without_session() {
186 record_tree(100, 80);
187 }
188
189 #[test]
190 fn record_shell_does_not_panic_without_session() {
191 record_shell_command(500, 200);
192 }
193}