Skip to main content

lean_ctx/core/
litm.rs

1use crate::core::session::SessionState;
2
3#[derive(Debug, Clone, Copy)]
4pub struct LitmProfile {
5    pub alpha: f64,
6    pub beta: f64,
7    pub gamma: f64,
8    pub name: &'static str,
9}
10
11impl LitmProfile {
12    pub const CLAUDE: Self = Self {
13        alpha: 0.92,
14        beta: 0.50,
15        gamma: 0.88,
16        name: "claude",
17    };
18    pub const GPT: Self = Self {
19        alpha: 0.90,
20        beta: 0.55,
21        gamma: 0.85,
22        name: "gpt",
23    };
24    pub const GEMINI: Self = Self {
25        alpha: 0.88,
26        beta: 0.60,
27        gamma: 0.82,
28        name: "gemini",
29    };
30    pub const DEFAULT: Self = Self::GPT;
31
32    pub fn from_client_name(client: &str) -> Self {
33        if let Ok(override_val) = std::env::var("LEAN_CTX_LITM_PROFILE") {
34            return Self::from_name(&override_val);
35        }
36        let lower = client.to_lowercase();
37        if lower.contains("claude") || lower.contains("cursor") {
38            Self::CLAUDE
39        } else if lower.contains("gemini") {
40            Self::GEMINI
41        } else {
42            Self::GPT
43        }
44    }
45
46    pub fn from_name(name: &str) -> Self {
47        match name.to_lowercase().as_str() {
48            "claude" | "cursor" => Self::CLAUDE,
49            "gemini" => Self::GEMINI,
50            "gpt" | "openai" | "codex" => Self::GPT,
51            _ => Self::DEFAULT,
52        }
53    }
54}
55
56#[cfg(test)]
57const _ALPHA: f64 = 0.9;
58#[cfg(test)]
59const _BETA: f64 = 0.55;
60#[cfg(test)]
61const _GAMMA: f64 = 0.85;
62
63pub struct PositionedOutput {
64    pub begin_block: String,
65    pub end_block: String,
66}
67
68/// Sorts session state fields by attention priority:
69///   P1 (begin): task, decisions, project topology, file refs
70///   P2 (end): recent findings, test results, next steps
71///   P3 (dropped): old completed tasks, historical reads beyond limit
72pub fn position_optimize(session: &SessionState) -> PositionedOutput {
73    let mut begin_lines = Vec::new();
74    let mut end_lines = Vec::new();
75
76    begin_lines.push(format!(
77        "ACTIVE SESSION v{} | {} calls | {} tok saved",
78        session.version, session.stats.total_tool_calls, session.stats.total_tokens_saved
79    ));
80
81    if let Some(ref task) = session.task {
82        let pct = task
83            .progress_pct
84            .map_or(String::new(), |p| format!(" [{p}%]"));
85        begin_lines.push(format!("Task: {}{pct}", task.description));
86    }
87
88    if let Some(ref root) = session.project_root {
89        begin_lines.push(format!("Root: {root}"));
90    }
91
92    if !session.decisions.is_empty() {
93        let items: Vec<&str> = session
94            .decisions
95            .iter()
96            .rev()
97            .take(5)
98            .map(|d| d.summary.as_str())
99            .collect();
100        begin_lines.push(format!("Decisions: {}", items.join(" | ")));
101    }
102
103    if !session.files_touched.is_empty() {
104        let items: Vec<String> = session
105            .files_touched
106            .iter()
107            .rev()
108            .take(15)
109            .map(|f| {
110                let r = f.file_ref.as_deref().unwrap_or("?");
111                let status = if f.modified { "mod" } else { &f.last_mode };
112                format!("{r}={} [{status}]", short_path(&f.path))
113            })
114            .collect();
115        begin_lines.push(format!("Files: {}", items.join(" ")));
116    }
117
118    if !session.findings.is_empty() {
119        let items: Vec<String> = session
120            .findings
121            .iter()
122            .rev()
123            .take(5)
124            .map(|f| match (&f.file, f.line) {
125                (Some(file), Some(line)) => format!("{}:{line} — {}", short_path(file), f.summary),
126                (Some(file), None) => format!("{} — {}", short_path(file), f.summary),
127                _ => f.summary.clone(),
128            })
129            .collect();
130        end_lines.push(format!("Findings: {}", items.join(" | ")));
131    }
132
133    if let Some(ref tests) = session.test_results {
134        let status = if tests.failed > 0 { "FAIL" } else { "PASS" };
135        end_lines.push(format!(
136            "Tests [{status}]: {}/{} ({})",
137            tests.passed, tests.total, tests.command
138        ));
139    }
140
141    if !session.next_steps.is_empty() {
142        end_lines.push(format!("Next: {}", session.next_steps.join(" → ")));
143    }
144
145    // Tool-preference line lives in the static LITM-END header (instructions.rs)
146    // — not duplicated here.
147
148    PositionedOutput {
149        begin_block: begin_lines.join("\n"),
150        end_block: end_lines.join("\n"),
151    }
152}
153
154#[cfg(test)]
155pub fn compute_litm_efficiency(
156    begin_tokens: usize,
157    middle_tokens: usize,
158    end_tokens: usize,
159    ccp_begin_tokens: usize,
160    ccp_end_tokens: usize,
161) -> (f64, f64) {
162    let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
163    let effective_without =
164        _ALPHA * begin_tokens as f64 + _BETA * middle_tokens as f64 + _GAMMA * end_tokens as f64;
165
166    let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
167    let effective_with = _ALPHA * ccp_begin_tokens as f64 + _GAMMA * ccp_end_tokens as f64;
168
169    let eff_without = if total_without > 0.0 {
170        effective_without / total_without * 100.0
171    } else {
172        0.0
173    };
174    let eff_with = if total_with > 0.0 {
175        effective_with / total_with * 100.0
176    } else {
177        0.0
178    };
179
180    (eff_without, eff_with)
181}
182
183#[cfg(test)]
184pub fn compute_litm_efficiency_for_profile(
185    begin_tokens: usize,
186    middle_tokens: usize,
187    end_tokens: usize,
188    ccp_begin_tokens: usize,
189    ccp_end_tokens: usize,
190    profile: &LitmProfile,
191) -> (f64, f64) {
192    let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
193    let effective_without = profile.alpha * begin_tokens as f64
194        + profile.beta * middle_tokens as f64
195        + profile.gamma * end_tokens as f64;
196
197    let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
198    let effective_with =
199        profile.alpha * ccp_begin_tokens as f64 + profile.gamma * ccp_end_tokens as f64;
200
201    let eff_without = if total_without > 0.0 {
202        effective_without / total_without * 100.0
203    } else {
204        0.0
205    };
206    let eff_with = if total_with > 0.0 {
207        effective_with / total_with * 100.0
208    } else {
209        0.0
210    };
211
212    (eff_without, eff_with)
213}
214
215#[cfg(test)]
216pub fn content_attention_efficiency(content: &str, profile: &LitmProfile) -> f64 {
217    use crate::core::attention_model;
218
219    let lines: Vec<&str> = content.lines().collect();
220    if lines.is_empty() {
221        return 0.0;
222    }
223
224    let importances: Vec<f64> = lines
225        .iter()
226        .enumerate()
227        .map(|(i, line)| {
228            let pos = i as f64 / (lines.len() - 1).max(1) as f64;
229            attention_model::combined_attention(
230                line,
231                pos,
232                profile.alpha,
233                profile.beta,
234                profile.gamma,
235            )
236        })
237        .collect();
238
239    attention_model::attention_efficiency(&importances, profile.alpha, profile.beta, profile.gamma)
240}
241
242fn short_path(path: &str) -> String {
243    let parts: Vec<&str> = path.split('/').collect();
244    if parts.len() <= 2 {
245        return path.to_string();
246    }
247    parts.last().copied().unwrap_or(path).to_string()
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn litm_efficiency_without_ccp_lower() {
256        let (eff_without, eff_with) = compute_litm_efficiency(100, 500, 100, 300, 200);
257        assert!(
258            eff_with > eff_without,
259            "CCP should improve LITM efficiency: without={eff_without:.1}%, with={eff_with:.1}%"
260        );
261    }
262
263    #[test]
264    fn litm_efficiency_zero_tokens() {
265        let (eff_without, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 0);
266        assert_eq!(eff_without, 0.0);
267        assert_eq!(eff_with, 0.0);
268    }
269
270    #[test]
271    fn litm_all_at_begin_is_alpha() {
272        let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 100, 0);
273        assert!((eff_with - 90.0).abs() < 0.1, "all begin should be ~90%");
274    }
275
276    #[test]
277    fn litm_all_at_end_is_gamma() {
278        let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 100);
279        assert!((eff_with - 85.0).abs() < 0.1, "all end should be ~85%");
280    }
281
282    #[test]
283    fn litm_middle_heavy_is_worst() {
284        let (eff_middle, _) = compute_litm_efficiency(10, 1000, 10, 0, 0);
285        let (eff_balanced, _) = compute_litm_efficiency(500, 20, 500, 0, 0);
286        assert!(
287            eff_balanced > eff_middle,
288            "middle-heavy should be less efficient"
289        );
290    }
291
292    #[test]
293    fn short_path_simple() {
294        assert_eq!(short_path("file.rs"), "file.rs");
295        assert_eq!(short_path("src/file.rs"), "src/file.rs");
296        assert_eq!(short_path("a/b/c/file.rs"), "file.rs");
297    }
298
299    #[test]
300    fn litm_profile_from_client_claude() {
301        let p = LitmProfile::from_client_name("Claude Desktop");
302        assert_eq!(p.name, "claude");
303        assert!((p.alpha - 0.92).abs() < f64::EPSILON);
304    }
305
306    #[test]
307    fn litm_profile_from_client_cursor() {
308        let p = LitmProfile::from_client_name("Cursor");
309        assert_eq!(p.name, "claude");
310    }
311
312    #[test]
313    fn litm_profile_from_client_gemini() {
314        let p = LitmProfile::from_client_name("Gemini CLI");
315        assert_eq!(p.name, "gemini");
316        assert!((p.beta - 0.60).abs() < f64::EPSILON);
317    }
318
319    #[test]
320    fn litm_profile_unknown_defaults_to_gpt() {
321        let p = LitmProfile::from_client_name("unknown-tool");
322        assert_eq!(p.name, "gpt");
323    }
324
325    #[test]
326    fn litm_profile_efficiency_differs_by_model() {
327        let (_, claude_eff) =
328            compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::CLAUDE);
329        let (_, gemini_eff) =
330            compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::GEMINI);
331        assert!(
332            (claude_eff - gemini_eff).abs() > 0.1,
333            "different profiles should yield different efficiencies"
334        );
335    }
336}