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" | "codebuddy" | "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
68pub fn position_optimize(session: &SessionState) -> PositionedOutput {
73 position_optimize_with_share(session, crate::core::litm_calibration::DEFAULT_BEGIN_SHARE)
74}
75
76pub fn position_optimize_with_share(session: &SessionState, begin_share: f64) -> PositionedOutput {
82 let begin_weak = begin_share < 0.6;
83 let mut begin_lines = Vec::new();
84 let mut end_lines = Vec::new();
85
86 if let Some(ref root) = session.project_root {
89 begin_lines.push(format!("Root: {root}"));
90 }
91
92 if let Some(ref task) = session.task {
93 let pct = task
94 .progress_pct
95 .map_or(String::new(), |p| format!(" [{p}%]"));
96 begin_lines.push(format!("Task: {}{pct}", task.description));
97 }
98
99 if !session.decisions.is_empty() {
100 let items: Vec<&str> = session
101 .decisions
102 .iter()
103 .rev()
104 .take(5)
105 .map(|d| d.summary.as_str())
106 .collect();
107 begin_lines.push(format!("Decisions: {}", items.join(" | ")));
108 }
109
110 if !session.files_touched.is_empty() {
111 let items: Vec<String> = session
112 .files_touched
113 .iter()
114 .rev()
115 .take(15)
116 .map(|f| {
117 let r = f.file_ref.as_deref().unwrap_or("?");
118 let status = if f.modified { "mod" } else { &f.last_mode };
119 let summary_hint = f
120 .summary
121 .as_deref()
122 .map_or(String::new(), |s| format!(", \"{s}\""));
123 format!("{r}={} [{status}{summary_hint}]", short_path(&f.path))
124 })
125 .collect();
126 begin_lines.push(format!("Files: {}", items.join(" ")));
127 }
128
129 if !session.progress.is_empty() {
133 let items: Vec<String> = session
134 .progress
135 .iter()
136 .rev()
137 .take(5)
138 .map(|p| {
139 p.detail
140 .as_deref()
141 .map_or_else(|| p.action.clone(), |d| format!("{}: {d}", p.action))
142 })
143 .collect();
144 let line = format!("Progress: {}", items.join(" | "));
145 if begin_weak {
146 end_lines.push(line);
147 } else {
148 begin_lines.push(line);
149 }
150 }
151
152 if !session.findings.is_empty() {
153 let items: Vec<String> = session
154 .findings
155 .iter()
156 .rev()
157 .take(8)
158 .map(|f| f.summary.clone())
159 .collect();
160 end_lines.push(format!("Findings: {}", items.join(" | ")));
161 }
162
163 if let Some(ref tests) = session.test_results {
164 let status = if tests.failed > 0 { "FAIL" } else { "PASS" };
165 end_lines.push(format!(
166 "Tests [{status}]: {}/{} ({})",
167 tests.passed, tests.total, tests.command
168 ));
169 }
170
171 if !session.next_steps.is_empty() {
172 end_lines.push(format!("Next: {}", session.next_steps.join(" → ")));
173 }
174
175 if begin_weak && let Some(ref task) = session.task {
179 end_lines.push(format!("Task (active): {}", task.description));
180 }
181
182 end_lines.push(format!(
184 "ACTIVE SESSION v{} | {} calls | {} tok saved",
185 session.version, session.stats.total_tool_calls, session.stats.total_tokens_saved
186 ));
187
188 PositionedOutput {
189 begin_block: begin_lines.join("\n"),
190 end_block: end_lines.join("\n"),
191 }
192}
193
194#[cfg(test)]
195pub fn compute_litm_efficiency(
196 begin_tokens: usize,
197 middle_tokens: usize,
198 end_tokens: usize,
199 ccp_begin_tokens: usize,
200 ccp_end_tokens: usize,
201) -> (f64, f64) {
202 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
203 let effective_without =
204 _ALPHA * begin_tokens as f64 + _BETA * middle_tokens as f64 + _GAMMA * end_tokens as f64;
205
206 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
207 let effective_with = _ALPHA * ccp_begin_tokens as f64 + _GAMMA * ccp_end_tokens as f64;
208
209 let eff_without = if total_without > 0.0 {
210 effective_without / total_without * 100.0
211 } else {
212 0.0
213 };
214 let eff_with = if total_with > 0.0 {
215 effective_with / total_with * 100.0
216 } else {
217 0.0
218 };
219
220 (eff_without, eff_with)
221}
222
223#[cfg(test)]
224pub fn compute_litm_efficiency_for_profile(
225 begin_tokens: usize,
226 middle_tokens: usize,
227 end_tokens: usize,
228 ccp_begin_tokens: usize,
229 ccp_end_tokens: usize,
230 profile: &LitmProfile,
231) -> (f64, f64) {
232 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
233 let effective_without = profile.alpha * begin_tokens as f64
234 + profile.beta * middle_tokens as f64
235 + profile.gamma * end_tokens as f64;
236
237 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
238 let effective_with =
239 profile.alpha * ccp_begin_tokens as f64 + profile.gamma * ccp_end_tokens as f64;
240
241 let eff_without = if total_without > 0.0 {
242 effective_without / total_without * 100.0
243 } else {
244 0.0
245 };
246 let eff_with = if total_with > 0.0 {
247 effective_with / total_with * 100.0
248 } else {
249 0.0
250 };
251
252 (eff_without, eff_with)
253}
254
255#[cfg(test)]
256pub fn content_attention_efficiency(content: &str, profile: &LitmProfile) -> f64 {
257 use crate::core::attention_model;
258
259 let lines: Vec<&str> = content.lines().collect();
260 if lines.is_empty() {
261 return 0.0;
262 }
263
264 let importances: Vec<f64> = lines
265 .iter()
266 .enumerate()
267 .map(|(i, line)| {
268 let pos = i as f64 / (lines.len() - 1).max(1) as f64;
269 attention_model::combined_attention(
270 line,
271 pos,
272 profile.alpha,
273 profile.beta,
274 profile.gamma,
275 )
276 })
277 .collect();
278
279 attention_model::attention_efficiency(&importances, profile.alpha, profile.beta, profile.gamma)
280}
281
282fn short_path(path: &str) -> String {
283 let parts: Vec<&str> = path.split('/').collect();
284 if parts.len() <= 2 {
285 return path.to_string();
286 }
287 parts.last().copied().unwrap_or(path).to_string()
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn litm_efficiency_without_ccp_lower() {
296 let (eff_without, eff_with) = compute_litm_efficiency(100, 500, 100, 300, 200);
297 assert!(
298 eff_with > eff_without,
299 "CCP should improve LITM efficiency: without={eff_without:.1}%, with={eff_with:.1}%"
300 );
301 }
302
303 #[test]
304 fn litm_efficiency_zero_tokens() {
305 let (eff_without, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 0);
306 assert_eq!(eff_without, 0.0);
307 assert_eq!(eff_with, 0.0);
308 }
309
310 #[test]
311 fn litm_all_at_begin_is_alpha() {
312 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 100, 0);
313 assert!((eff_with - 90.0).abs() < 0.1, "all begin should be ~90%");
314 }
315
316 #[test]
317 fn litm_all_at_end_is_gamma() {
318 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 100);
319 assert!((eff_with - 85.0).abs() < 0.1, "all end should be ~85%");
320 }
321
322 #[test]
323 fn litm_middle_heavy_is_worst() {
324 let (eff_middle, _) = compute_litm_efficiency(10, 1000, 10, 0, 0);
325 let (eff_balanced, _) = compute_litm_efficiency(500, 20, 500, 0, 0);
326 assert!(
327 eff_balanced > eff_middle,
328 "middle-heavy should be less efficient"
329 );
330 }
331
332 #[test]
333 fn calibrated_share_moves_progress_to_end() {
334 let mut session = SessionState::new();
335 session.task = Some(crate::core::session::TaskInfo {
336 description: "fix webhook".to_string(),
337 intent: None,
338 progress_pct: None,
339 });
340 session.progress.push(crate::core::session::ProgressEntry {
341 action: "deployed billing".to_string(),
342 detail: None,
343 timestamp: chrono::Utc::now(),
344 });
345
346 let default_layout = position_optimize_with_share(&session, 0.7);
347 assert!(default_layout.begin_block.contains("Progress:"));
348 assert!(!default_layout.end_block.contains("Task (active)"));
349
350 let weak_begin = position_optimize_with_share(&session, 0.45);
351 assert!(!weak_begin.begin_block.contains("Progress:"));
352 assert!(weak_begin.end_block.contains("Progress:"));
353 assert!(weak_begin.end_block.contains("Task (active): fix webhook"));
354 }
355
356 #[test]
357 fn default_share_is_byte_identical_to_uncalibrated() {
358 let mut session = SessionState::new();
359 session.task = Some(crate::core::session::TaskInfo {
360 description: "t".to_string(),
361 intent: None,
362 progress_pct: Some(50),
363 });
364 let a = position_optimize(&session);
365 let b = position_optimize_with_share(
366 &session,
367 crate::core::litm_calibration::DEFAULT_BEGIN_SHARE,
368 );
369 assert_eq!(a.begin_block, b.begin_block);
370 assert_eq!(a.end_block, b.end_block);
371 }
372
373 #[test]
374 fn short_path_simple() {
375 assert_eq!(short_path("file.rs"), "file.rs");
376 assert_eq!(short_path("src/file.rs"), "src/file.rs");
377 assert_eq!(short_path("a/b/c/file.rs"), "file.rs");
378 }
379
380 #[test]
381 fn litm_profile_from_client_claude() {
382 let p = LitmProfile::from_client_name("Claude Desktop");
383 assert_eq!(p.name, "claude");
384 assert!((p.alpha - 0.92).abs() < f64::EPSILON);
385 }
386
387 #[test]
388 fn litm_profile_from_client_cursor() {
389 let p = LitmProfile::from_client_name("Cursor");
390 assert_eq!(p.name, "claude");
391 }
392
393 #[test]
394 fn litm_profile_from_client_gemini() {
395 let p = LitmProfile::from_client_name("Gemini CLI");
396 assert_eq!(p.name, "gemini");
397 assert!((p.beta - 0.60).abs() < f64::EPSILON);
398 }
399
400 #[test]
401 fn litm_profile_unknown_defaults_to_gpt() {
402 let p = LitmProfile::from_client_name("unknown-tool");
403 assert_eq!(p.name, "gpt");
404 }
405
406 #[test]
407 fn litm_profile_efficiency_differs_by_model() {
408 let (_, claude_eff) =
409 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::CLAUDE);
410 let (_, gemini_eff) =
411 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::GEMINI);
412 assert!(
413 (claude_eff - gemini_eff).abs() > 0.1,
414 "different profiles should yield different efficiencies"
415 );
416 }
417}