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 {
179 if let Some(ref task) = session.task {
180 end_lines.push(format!("Task (active): {}", task.description));
181 }
182 }
183
184 end_lines.push(format!(
186 "ACTIVE SESSION v{} | {} calls | {} tok saved",
187 session.version, session.stats.total_tool_calls, session.stats.total_tokens_saved
188 ));
189
190 PositionedOutput {
191 begin_block: begin_lines.join("\n"),
192 end_block: end_lines.join("\n"),
193 }
194}
195
196#[cfg(test)]
197pub fn compute_litm_efficiency(
198 begin_tokens: usize,
199 middle_tokens: usize,
200 end_tokens: usize,
201 ccp_begin_tokens: usize,
202 ccp_end_tokens: usize,
203) -> (f64, f64) {
204 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
205 let effective_without =
206 _ALPHA * begin_tokens as f64 + _BETA * middle_tokens as f64 + _GAMMA * end_tokens as f64;
207
208 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
209 let effective_with = _ALPHA * ccp_begin_tokens as f64 + _GAMMA * ccp_end_tokens as f64;
210
211 let eff_without = if total_without > 0.0 {
212 effective_without / total_without * 100.0
213 } else {
214 0.0
215 };
216 let eff_with = if total_with > 0.0 {
217 effective_with / total_with * 100.0
218 } else {
219 0.0
220 };
221
222 (eff_without, eff_with)
223}
224
225#[cfg(test)]
226pub fn compute_litm_efficiency_for_profile(
227 begin_tokens: usize,
228 middle_tokens: usize,
229 end_tokens: usize,
230 ccp_begin_tokens: usize,
231 ccp_end_tokens: usize,
232 profile: &LitmProfile,
233) -> (f64, f64) {
234 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
235 let effective_without = profile.alpha * begin_tokens as f64
236 + profile.beta * middle_tokens as f64
237 + profile.gamma * end_tokens as f64;
238
239 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
240 let effective_with =
241 profile.alpha * ccp_begin_tokens as f64 + profile.gamma * ccp_end_tokens as f64;
242
243 let eff_without = if total_without > 0.0 {
244 effective_without / total_without * 100.0
245 } else {
246 0.0
247 };
248 let eff_with = if total_with > 0.0 {
249 effective_with / total_with * 100.0
250 } else {
251 0.0
252 };
253
254 (eff_without, eff_with)
255}
256
257#[cfg(test)]
258pub fn content_attention_efficiency(content: &str, profile: &LitmProfile) -> f64 {
259 use crate::core::attention_model;
260
261 let lines: Vec<&str> = content.lines().collect();
262 if lines.is_empty() {
263 return 0.0;
264 }
265
266 let importances: Vec<f64> = lines
267 .iter()
268 .enumerate()
269 .map(|(i, line)| {
270 let pos = i as f64 / (lines.len() - 1).max(1) as f64;
271 attention_model::combined_attention(
272 line,
273 pos,
274 profile.alpha,
275 profile.beta,
276 profile.gamma,
277 )
278 })
279 .collect();
280
281 attention_model::attention_efficiency(&importances, profile.alpha, profile.beta, profile.gamma)
282}
283
284fn short_path(path: &str) -> String {
285 let parts: Vec<&str> = path.split('/').collect();
286 if parts.len() <= 2 {
287 return path.to_string();
288 }
289 parts.last().copied().unwrap_or(path).to_string()
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn litm_efficiency_without_ccp_lower() {
298 let (eff_without, eff_with) = compute_litm_efficiency(100, 500, 100, 300, 200);
299 assert!(
300 eff_with > eff_without,
301 "CCP should improve LITM efficiency: without={eff_without:.1}%, with={eff_with:.1}%"
302 );
303 }
304
305 #[test]
306 fn litm_efficiency_zero_tokens() {
307 let (eff_without, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 0);
308 assert_eq!(eff_without, 0.0);
309 assert_eq!(eff_with, 0.0);
310 }
311
312 #[test]
313 fn litm_all_at_begin_is_alpha() {
314 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 100, 0);
315 assert!((eff_with - 90.0).abs() < 0.1, "all begin should be ~90%");
316 }
317
318 #[test]
319 fn litm_all_at_end_is_gamma() {
320 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 100);
321 assert!((eff_with - 85.0).abs() < 0.1, "all end should be ~85%");
322 }
323
324 #[test]
325 fn litm_middle_heavy_is_worst() {
326 let (eff_middle, _) = compute_litm_efficiency(10, 1000, 10, 0, 0);
327 let (eff_balanced, _) = compute_litm_efficiency(500, 20, 500, 0, 0);
328 assert!(
329 eff_balanced > eff_middle,
330 "middle-heavy should be less efficient"
331 );
332 }
333
334 #[test]
335 fn calibrated_share_moves_progress_to_end() {
336 let mut session = SessionState::new();
337 session.task = Some(crate::core::session::TaskInfo {
338 description: "fix webhook".to_string(),
339 intent: None,
340 progress_pct: None,
341 });
342 session.progress.push(crate::core::session::ProgressEntry {
343 action: "deployed billing".to_string(),
344 detail: None,
345 timestamp: chrono::Utc::now(),
346 });
347
348 let default_layout = position_optimize_with_share(&session, 0.7);
349 assert!(default_layout.begin_block.contains("Progress:"));
350 assert!(!default_layout.end_block.contains("Task (active)"));
351
352 let weak_begin = position_optimize_with_share(&session, 0.45);
353 assert!(!weak_begin.begin_block.contains("Progress:"));
354 assert!(weak_begin.end_block.contains("Progress:"));
355 assert!(weak_begin.end_block.contains("Task (active): fix webhook"));
356 }
357
358 #[test]
359 fn default_share_is_byte_identical_to_uncalibrated() {
360 let mut session = SessionState::new();
361 session.task = Some(crate::core::session::TaskInfo {
362 description: "t".to_string(),
363 intent: None,
364 progress_pct: Some(50),
365 });
366 let a = position_optimize(&session);
367 let b = position_optimize_with_share(
368 &session,
369 crate::core::litm_calibration::DEFAULT_BEGIN_SHARE,
370 );
371 assert_eq!(a.begin_block, b.begin_block);
372 assert_eq!(a.end_block, b.end_block);
373 }
374
375 #[test]
376 fn short_path_simple() {
377 assert_eq!(short_path("file.rs"), "file.rs");
378 assert_eq!(short_path("src/file.rs"), "src/file.rs");
379 assert_eq!(short_path("a/b/c/file.rs"), "file.rs");
380 }
381
382 #[test]
383 fn litm_profile_from_client_claude() {
384 let p = LitmProfile::from_client_name("Claude Desktop");
385 assert_eq!(p.name, "claude");
386 assert!((p.alpha - 0.92).abs() < f64::EPSILON);
387 }
388
389 #[test]
390 fn litm_profile_from_client_cursor() {
391 let p = LitmProfile::from_client_name("Cursor");
392 assert_eq!(p.name, "claude");
393 }
394
395 #[test]
396 fn litm_profile_from_client_gemini() {
397 let p = LitmProfile::from_client_name("Gemini CLI");
398 assert_eq!(p.name, "gemini");
399 assert!((p.beta - 0.60).abs() < f64::EPSILON);
400 }
401
402 #[test]
403 fn litm_profile_unknown_defaults_to_gpt() {
404 let p = LitmProfile::from_client_name("unknown-tool");
405 assert_eq!(p.name, "gpt");
406 }
407
408 #[test]
409 fn litm_profile_efficiency_differs_by_model() {
410 let (_, claude_eff) =
411 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::CLAUDE);
412 let (_, gemini_eff) =
413 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::GEMINI);
414 assert!(
415 (claude_eff - gemini_eff).abs() > 0.1,
416 "different profiles should yield different efficiencies"
417 );
418 }
419}