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
17pub fn record_file_read(
19 path: &str,
20 mode: &str,
21 original_tokens: usize,
22 output_tokens: usize,
23 is_cache_hit: bool,
24) {
25 let saved = original_tokens.saturating_sub(output_tokens);
26 let tool_key = format!("cli_{mode}");
27
28 stats::record(&tool_key, original_tokens, output_tokens);
29 heatmap::record_file_access(path, original_tokens, saved);
30
31 if let Some(mut session) = SessionState::load_latest() {
32 session.touch_file(path, None, mode, original_tokens);
33 if is_cache_hit {
34 session.record_cache_hit();
35 }
36
37 if session.active_structured_intent.is_none() && session.files_touched.len() >= 2 {
38 let touched: Vec<String> = session
39 .files_touched
40 .iter()
41 .map(|ft| ft.path.clone())
42 .collect();
43 let inferred = StructuredIntent::from_file_patterns(&touched);
44 if inferred.confidence >= 0.4 {
45 session.active_structured_intent = Some(inferred);
46 }
47 }
48
49 let project_root = session.project_root.clone();
50 let calls = session.stats.total_tool_calls;
51 let _ = session.save();
52
53 maybe_consolidate(project_root.as_deref(), calls);
54 }
55
56 let mut ledger = ContextLedger::load();
57 ledger.record(path, mode, original_tokens, output_tokens);
58 ledger.save();
59}
60
61pub fn record_search(original_tokens: usize, output_tokens: usize) {
63 stats::record("cli_grep", original_tokens, output_tokens);
64
65 if let Some(mut session) = SessionState::load_latest() {
66 session.record_command();
67 let project_root = session.project_root.clone();
68 let calls = session.stats.total_tool_calls;
69 let _ = session.save();
70
71 maybe_consolidate(project_root.as_deref(), calls);
72 }
73}
74
75pub fn record_tree(original_tokens: usize, output_tokens: usize) {
77 stats::record("cli_ls", original_tokens, output_tokens);
78
79 if let Some(mut session) = SessionState::load_latest() {
80 session.record_command();
81 let _ = session.save();
82 }
83}
84
85pub fn record_shell_command(original_tokens: usize, output_tokens: usize) {
89 stats::record("cli_shell", original_tokens, output_tokens);
90
91 if let Some(mut session) = SessionState::load_latest() {
92 session.record_command();
93 let project_root = session.project_root.clone();
94 let calls = session.stats.total_tool_calls;
95 let _ = session.save();
96
97 if original_tokens > 0 {
98 maybe_consolidate(project_root.as_deref(), calls);
99 }
100 }
101}
102
103fn maybe_consolidate(project_root: Option<&str>, calls: u32) {
104 let Some(root) = project_root else { return };
105 let autonomy = crate::tools::autonomy::AutonomyState::new();
106 if crate::tools::autonomy::should_auto_consolidate(&autonomy, calls) {
107 let root = root.to_string();
108 let _ = crate::core::consolidation_engine::consolidate_latest(
109 &root,
110 crate::core::consolidation_engine::ConsolidationBudgets::default(),
111 );
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn record_file_read_does_not_panic_without_session() {
121 record_file_read("/tmp/nonexistent.rs", "full", 100, 50, false);
122 }
123
124 #[test]
125 fn record_search_does_not_panic_without_session() {
126 record_search(200, 150);
127 }
128
129 #[test]
130 fn record_tree_does_not_panic_without_session() {
131 record_tree(100, 80);
132 }
133
134 #[test]
135 fn record_shell_does_not_panic_without_session() {
136 record_shell_command(500, 200);
137 }
138}