1use crate::core::session::SessionState;
2use crate::core::tokens::count_tokens;
3
4pub const DEFAULT_ACTIVE_SESSION_BUDGET: usize = 800;
8
9pub const DEFAULT_BEGIN_SHARE: f64 = 0.7;
11
12#[must_use]
14pub fn active_session_budget() -> usize {
15 std::env::var("LEAN_CTX_ACTIVE_SESSION_BUDGET")
16 .ok()
17 .and_then(|v| v.parse().ok())
18 .filter(|&n| n > 0)
19 .unwrap_or(DEFAULT_ACTIVE_SESSION_BUDGET)
20}
21
22#[derive(Debug, Clone, Copy)]
23pub struct LitmProfile {
24 pub alpha: f64,
25 pub beta: f64,
26 pub gamma: f64,
27 pub name: &'static str,
28}
29
30impl LitmProfile {
31 pub const CLAUDE: Self = Self {
32 alpha: 0.92,
33 beta: 0.50,
34 gamma: 0.88,
35 name: "claude",
36 };
37 pub const GPT: Self = Self {
38 alpha: 0.90,
39 beta: 0.55,
40 gamma: 0.85,
41 name: "gpt",
42 };
43 pub const GEMINI: Self = Self {
44 alpha: 0.88,
45 beta: 0.60,
46 gamma: 0.82,
47 name: "gemini",
48 };
49 pub const DEFAULT: Self = Self::GPT;
50
51 pub fn from_client_name(client: &str) -> Self {
52 if let Ok(override_val) = std::env::var("LEAN_CTX_LITM_PROFILE") {
53 return Self::from_name(&override_val);
54 }
55 let lower = client.to_lowercase();
56 if lower.contains("claude") || lower.contains("cursor") {
57 Self::CLAUDE
58 } else if lower.contains("gemini") {
59 Self::GEMINI
60 } else {
61 Self::GPT
62 }
63 }
64
65 pub fn from_name(name: &str) -> Self {
66 match name.to_lowercase().as_str() {
67 "claude" | "codebuddy" | "cursor" => Self::CLAUDE,
68 "gemini" => Self::GEMINI,
69 "gpt" | "openai" | "codex" => Self::GPT,
70 _ => Self::DEFAULT,
71 }
72 }
73}
74
75#[cfg(test)]
76const _ALPHA: f64 = 0.9;
77#[cfg(test)]
78const _BETA: f64 = 0.55;
79#[cfg(test)]
80const _GAMMA: f64 = 0.85;
81
82pub struct PositionedOutput {
83 pub begin_block: String,
84 pub end_block: String,
85}
86
87impl PositionedOutput {
88 pub fn enforce_token_budget(&mut self, budget: usize) {
93 self.begin_block = trim_lines_to_budget(&self.begin_block, budget);
94 let remaining = budget.saturating_sub(count_tokens(&self.begin_block));
95 self.end_block = trim_lines_to_budget(&self.end_block, remaining);
96 }
97}
98
99fn trim_lines_to_budget(block: &str, budget: usize) -> String {
101 if block.is_empty() || count_tokens(block) <= budget {
102 return block.to_string();
103 }
104 let mut kept: Vec<&str> = Vec::new();
105 let mut used = 0usize;
106 for line in block.lines() {
107 let cost = count_tokens(line);
108 if used + cost > budget {
109 break;
110 }
111 used += cost;
112 kept.push(line);
113 }
114 kept.join("\n")
115}
116
117pub fn position_optimize(session: &SessionState) -> PositionedOutput {
122 position_optimize_with_share(session, DEFAULT_BEGIN_SHARE)
123}
124
125pub fn position_optimize_with_share(session: &SessionState, begin_share: f64) -> PositionedOutput {
131 let begin_weak = begin_share < 0.6;
132 let mut begin_lines = Vec::new();
133 let mut end_lines = Vec::new();
134
135 if let Some(ref root) = session.project_root {
138 begin_lines.push(format!("Root: {root}"));
139 }
140
141 if let Some(ref task) = session.task {
142 let pct = task
143 .progress_pct
144 .map_or(String::new(), |p| format!(" [{p}%]"));
145 begin_lines.push(format!("Task: {}{pct}", task.description));
146 }
147
148 if !session.decisions.is_empty() {
149 let items: Vec<&str> = session
150 .decisions
151 .iter()
152 .rev()
153 .take(5)
154 .map(|d| d.summary.as_str())
155 .collect();
156 begin_lines.push(format!("Decisions: {}", items.join(" | ")));
157 }
158
159 if !session.files_touched.is_empty() {
160 let items: Vec<String> = session
161 .files_touched
162 .iter()
163 .rev()
164 .take(15)
165 .map(|f| {
166 let r = f.file_ref.as_deref().unwrap_or("?");
167 let status = if f.modified { "mod" } else { &f.last_mode };
168 let summary_hint = f
169 .summary
170 .as_deref()
171 .map_or(String::new(), |s| format!(", \"{s}\""));
172 format!("{r}={} [{status}{summary_hint}]", short_path(&f.path))
173 })
174 .collect();
175 begin_lines.push(format!("Files: {}", items.join(" ")));
176 }
177
178 if !session.progress.is_empty() {
182 let items: Vec<String> = session
183 .progress
184 .iter()
185 .rev()
186 .take(5)
187 .map(|p| {
188 p.detail
189 .as_deref()
190 .map_or_else(|| p.action.clone(), |d| format!("{}: {d}", p.action))
191 })
192 .collect();
193 let line = format!("Progress: {}", items.join(" | "));
194 if begin_weak {
195 end_lines.push(line);
196 } else {
197 begin_lines.push(line);
198 }
199 }
200
201 let findings: Vec<String> = session
205 .findings
206 .iter()
207 .rev()
208 .filter(|f| session.finding_served(f))
209 .take(8)
210 .map(|f| f.summary.clone())
211 .collect();
212 if !findings.is_empty() {
213 end_lines.push(format!("Findings: {}", findings.join(" | ")));
214 }
215
216 if let Some(ref tests) = session.test_results {
217 let status = if tests.failed > 0 { "FAIL" } else { "PASS" };
218 end_lines.push(format!(
219 "Tests [{status}]: {}/{} ({})",
220 tests.passed, tests.total, tests.command
221 ));
222 }
223
224 if !session.next_steps.is_empty() {
225 end_lines.push(format!("Next: {}", session.next_steps.join(" → ")));
226 }
227
228 if begin_weak && let Some(ref task) = session.task {
232 end_lines.push(format!("Task (active): {}", task.description));
233 }
234
235 end_lines.push(format!(
237 "ACTIVE SESSION v{} | {} calls | {} tok saved",
238 session.version, session.stats.total_tool_calls, session.stats.total_tokens_saved
239 ));
240
241 PositionedOutput {
242 begin_block: begin_lines.join("\n"),
243 end_block: end_lines.join("\n"),
244 }
245}
246
247#[cfg(test)]
248pub fn compute_litm_efficiency(
249 begin_tokens: usize,
250 middle_tokens: usize,
251 end_tokens: usize,
252 ccp_begin_tokens: usize,
253 ccp_end_tokens: usize,
254) -> (f64, f64) {
255 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
256 let effective_without =
257 _ALPHA * begin_tokens as f64 + _BETA * middle_tokens as f64 + _GAMMA * end_tokens as f64;
258
259 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
260 let effective_with = _ALPHA * ccp_begin_tokens as f64 + _GAMMA * ccp_end_tokens as f64;
261
262 let eff_without = if total_without > 0.0 {
263 effective_without / total_without * 100.0
264 } else {
265 0.0
266 };
267 let eff_with = if total_with > 0.0 {
268 effective_with / total_with * 100.0
269 } else {
270 0.0
271 };
272
273 (eff_without, eff_with)
274}
275
276#[cfg(test)]
277pub fn compute_litm_efficiency_for_profile(
278 begin_tokens: usize,
279 middle_tokens: usize,
280 end_tokens: usize,
281 ccp_begin_tokens: usize,
282 ccp_end_tokens: usize,
283 profile: &LitmProfile,
284) -> (f64, f64) {
285 let total_without = (begin_tokens + middle_tokens + end_tokens) as f64;
286 let effective_without = profile.alpha * begin_tokens as f64
287 + profile.beta * middle_tokens as f64
288 + profile.gamma * end_tokens as f64;
289
290 let total_with = (ccp_begin_tokens + ccp_end_tokens) as f64;
291 let effective_with =
292 profile.alpha * ccp_begin_tokens as f64 + profile.gamma * ccp_end_tokens as f64;
293
294 let eff_without = if total_without > 0.0 {
295 effective_without / total_without * 100.0
296 } else {
297 0.0
298 };
299 let eff_with = if total_with > 0.0 {
300 effective_with / total_with * 100.0
301 } else {
302 0.0
303 };
304
305 (eff_without, eff_with)
306}
307
308fn short_path(path: &str) -> String {
309 let parts: Vec<&str> = path.split('/').collect();
310 if parts.len() <= 2 {
311 return path.to_string();
312 }
313 parts.last().copied().unwrap_or(path).to_string()
314}
315
316#[cfg(test)]
317pub mod tests {
318 use super::*;
319
320 #[test]
321 fn litm_efficiency_without_ccp_lower() {
322 let (eff_without, eff_with) = compute_litm_efficiency(100, 500, 100, 300, 200);
323 assert!(
324 eff_with > eff_without,
325 "CCP should improve LITM efficiency: without={eff_without:.1}%, with={eff_with:.1}%"
326 );
327 }
328
329 #[test]
330 fn litm_efficiency_zero_tokens() {
331 let (eff_without, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 0);
332 assert_eq!(eff_without, 0.0);
333 assert_eq!(eff_with, 0.0);
334 }
335
336 #[test]
337 fn litm_all_at_begin_is_alpha() {
338 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 100, 0);
339 assert!((eff_with - 90.0).abs() < 0.1, "all begin should be ~90%");
340 }
341
342 #[test]
343 fn litm_all_at_end_is_gamma() {
344 let (_, eff_with) = compute_litm_efficiency(0, 0, 0, 0, 100);
345 assert!((eff_with - 85.0).abs() < 0.1, "all end should be ~85%");
346 }
347
348 #[test]
349 fn litm_middle_heavy_is_worst() {
350 let (eff_middle, _) = compute_litm_efficiency(10, 1000, 10, 0, 0);
351 let (eff_balanced, _) = compute_litm_efficiency(500, 20, 500, 0, 0);
352 assert!(
353 eff_balanced > eff_middle,
354 "middle-heavy should be less efficient"
355 );
356 }
357
358 #[test]
359 fn calibrated_share_moves_progress_to_end() {
360 let mut session = SessionState::new();
361 session.task = Some(crate::core::session::TaskInfo {
362 description: "fix webhook".to_string(),
363 intent: None,
364 progress_pct: None,
365 });
366 session.progress.push(crate::core::session::ProgressEntry {
367 action: "deployed billing".to_string(),
368 detail: None,
369 timestamp: chrono::Utc::now(),
370 });
371
372 let default_layout = position_optimize_with_share(&session, 0.7);
373 assert!(default_layout.begin_block.contains("Progress:"));
374 assert!(!default_layout.end_block.contains("Task (active)"));
375
376 let weak_begin = position_optimize_with_share(&session, 0.45);
377 assert!(!weak_begin.begin_block.contains("Progress:"));
378 assert!(weak_begin.end_block.contains("Progress:"));
379 assert!(weak_begin.end_block.contains("Task (active): fix webhook"));
380 }
381
382 #[test]
383 fn default_share_is_byte_identical_to_uncalibrated() {
384 let mut session = SessionState::new();
385 session.task = Some(crate::core::session::TaskInfo {
386 description: "t".to_string(),
387 intent: None,
388 progress_pct: Some(50),
389 });
390 let a = position_optimize(&session);
391 let b = position_optimize_with_share(&session, DEFAULT_BEGIN_SHARE);
392 assert_eq!(a.begin_block, b.begin_block);
393 assert_eq!(a.end_block, b.end_block);
394 }
395
396 #[test]
397 fn short_path_simple() {
398 assert_eq!(short_path("file.rs"), "file.rs");
399 assert_eq!(short_path("src/file.rs"), "src/file.rs");
400 assert_eq!(short_path("a/b/c/file.rs"), "file.rs");
401 }
402
403 #[test]
404 fn enforce_token_budget_caps_block_deterministically() {
405 let mut session = SessionState::new();
406 session.project_root = Some("/tmp/x".to_string());
407 session.task = Some(crate::core::session::TaskInfo {
408 description: "deploy ".repeat(200),
409 intent: None,
410 progress_pct: None,
411 });
412
413 let mut a = position_optimize(&session);
414 let before = count_tokens(&a.begin_block);
415 a.enforce_token_budget(8);
416 let after = count_tokens(&a.begin_block);
417 assert!(
418 after <= 8,
419 "begin block must respect the budget, got {after}"
420 );
421 assert!(after < before, "an oversized block must actually shrink");
422
423 let mut b = position_optimize(&session);
424 b.enforce_token_budget(8);
425 assert_eq!(
426 a.begin_block, b.begin_block,
427 "trimming must be deterministic"
428 );
429 }
430
431 #[test]
432 fn enforce_token_budget_is_noop_under_budget() {
433 let mut session = SessionState::new();
434 session.task = Some(crate::core::session::TaskInfo {
435 description: "small task".to_string(),
436 intent: None,
437 progress_pct: Some(50),
438 });
439 let mut out = position_optimize(&session);
440 let original = out.begin_block.clone();
441 out.enforce_token_budget(DEFAULT_ACTIVE_SESSION_BUDGET);
442 assert_eq!(out.begin_block, original, "a small block is left untouched");
443 }
444
445 #[test]
446 fn litm_profile_from_client_claude() {
447 let p = LitmProfile::from_client_name("Claude Desktop");
448 assert_eq!(p.name, "claude");
449 assert!((p.alpha - 0.92).abs() < f64::EPSILON);
450 }
451
452 #[test]
453 fn litm_profile_from_client_cursor() {
454 let p = LitmProfile::from_client_name("Cursor");
455 assert_eq!(p.name, "claude");
456 }
457
458 #[test]
459 fn litm_profile_from_client_gemini() {
460 let p = LitmProfile::from_client_name("Gemini CLI");
461 assert_eq!(p.name, "gemini");
462 assert!((p.beta - 0.60).abs() < f64::EPSILON);
463 }
464
465 #[test]
466 fn litm_profile_unknown_defaults_to_gpt() {
467 let p = LitmProfile::from_client_name("unknown-tool");
468 assert_eq!(p.name, "gpt");
469 }
470
471 #[test]
472 fn litm_profile_efficiency_differs_by_model() {
473 let (_, claude_eff) =
474 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::CLAUDE);
475 let (_, gemini_eff) =
476 compute_litm_efficiency_for_profile(200, 0, 100, 200, 100, &LitmProfile::GEMINI);
477 assert!(
478 (claude_eff - gemini_eff).abs() > 0.1,
479 "different profiles should yield different efficiencies"
480 );
481 }
482
483 fn with_two_findings(root: &str) -> SessionState {
484 std::fs::write(format!("{root}/pool.rs"), "pub fn lease() {}\n").expect("source");
485 let mut session = SessionState::new();
486 session.project_root = Some(root.to_owned());
487 session.add_finding_for_file(Some("pool.rs"), Some(1), "the pool leaks one lease");
488 session.add_finding_for_file(None, None, "noted without a file");
489 session
490 }
491
492 #[test]
496 fn a_finding_whose_source_is_gone_leaves_the_injected_block() {
497 use crate::core::context_admission::stored::lineage_support::{governed, project, scoped};
498 let _data = governed();
499 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
500 let (_directory, root) = project();
501 let session = with_two_findings(&root);
502 let mut bound_only = session.clone();
503 bound_only.findings.pop();
504 let end_block = |session: &SessionState| {
505 scoped(&root, || {
506 position_optimize_with_share(session, 1.0).end_block
507 })
508 };
509
510 let shown = end_block(&session);
511 assert!(
512 shown.contains("Findings: noted without a file | the pool leaks one lease\n"),
513 "{shown}"
514 );
515 assert!(end_block(&bound_only).contains("Findings: the pool leaks one lease\n"));
516
517 std::fs::remove_file(format!("{root}/pool.rs")).expect("delete");
518 let withheld = end_block(&session);
519 assert!(
520 withheld.contains("Findings: noted without a file\n"),
521 "{withheld}"
522 );
523 assert!(!withheld.contains("leaks"), "{withheld}");
524 let none = end_block(&bound_only);
525 assert!(!none.contains("Findings:"), "{none}");
526 assert!(none.starts_with("ACTIVE SESSION v"), "{none}");
527 }
528
529 #[test]
531 fn findings_are_injected_as_before_in_developer_mode() {
532 use crate::core::context_admission::stored::lineage_support::project;
533 let _data = crate::core::data_dir::isolated_data_dir();
534 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
535 let (_directory, root) = project();
536 let session = with_two_findings(&root);
537 std::fs::remove_file(format!("{root}/pool.rs")).expect("delete");
538 let end_block = position_optimize_with_share(&session, 1.0).end_block;
539 assert!(
540 end_block.contains("Findings: noted without a file | the pool leaks one lease\n"),
541 "{end_block}"
542 );
543 }
544}