Skip to main content

lean_ctx/core/verbosity/
recommender.rs

1//! Thompson-sampling compression-level recommender from behavioral signals.
2//!
3//! Maps observed agent behavior to a [`VerbosityProfile`] with deterministic
4//! beta-posterior means for reproducible recommendations.
5
6use super::signals::BehaviorSignal;
7use crate::core::config::CompressionLevel;
8
9/// Recommended verbosity profile.
10#[derive(Debug, Clone)]
11pub struct VerbosityProfile {
12    /// Recommended compression level.
13    pub level: CompressionLevel,
14    /// Confidence in the recommendation (0.0–1.0).
15    pub confidence: f64,
16    /// Reason for the recommendation.
17    pub reason: String,
18}
19
20/// Recommend a compression level based on behavioral signals.
21///
22/// Uses the deterministic posterior mean of Thompson-sampling beta priors so
23/// identical signal histories always produce identical recommendations.
24pub(crate) fn recommend_level(signals: &[BehaviorSignal]) -> VerbosityProfile {
25    let mut priors: [(f64, f64); 5] = [(1.0, 1.0), (3.0, 1.0), (2.0, 1.0), (1.5, 1.0), (1.0, 1.5)];
26
27    for signal in signals {
28        match signal {
29            BehaviorSignal::ReRead { .. } => {
30                priors[0].0 += 1.0;
31                priors[2].1 += 0.5;
32                priors[3].1 += 1.0;
33                priors[4].1 += 1.5;
34            }
35            BehaviorSignal::ModeSwitch { from, to } => {
36                if let Some(index) = level_index(from) {
37                    priors[index].1 += 0.5;
38                }
39                if let Some(index) = level_index(to) {
40                    priors[index].0 += 0.5;
41                }
42            }
43            BehaviorSignal::FullContentRequest { .. } => {
44                priors[0].0 += 1.5;
45                priors[2].1 += 0.5;
46                priors[3].1 += 1.0;
47                priors[4].1 += 1.0;
48            }
49            BehaviorSignal::TaskComplete { reads_count } => {
50                if *reads_count <= 3 {
51                    priors[2].0 += 1.0;
52                    priors[3].0 += 0.5;
53                } else {
54                    priors[1].0 += 0.25;
55                }
56            }
57            BehaviorSignal::ExpandFollowUp { .. } => {
58                priors[0].0 += 0.5;
59                priors[2].1 += 0.5;
60                priors[3].1 += 1.0;
61            }
62        }
63    }
64
65    let mut best_idx = 1;
66    let mut best_score = 0.0;
67    for (index, (alpha, beta)) in priors.iter().enumerate() {
68        let score = alpha / (alpha + beta);
69        if score >= best_score {
70            best_score = score;
71            best_idx = index;
72        }
73    }
74
75    let level = match best_idx {
76        0 => CompressionLevel::Off,
77        2 => CompressionLevel::Standard,
78        3 => CompressionLevel::Max,
79        4 => CompressionLevel::Raw,
80        _ => CompressionLevel::Lite, // includes 1 (Lite)
81    };
82
83    VerbosityProfile {
84        level,
85        confidence: best_score.clamp(0.0, 1.0),
86        reason: format!("Based on {} behavioral signals", signals.len()),
87    }
88}
89
90fn level_index(level: &str) -> Option<usize> {
91    match level.to_ascii_lowercase().as_str() {
92        "off" | "full" => Some(0),
93        "lite" => Some(1),
94        "standard" => Some(2),
95        "max" => Some(3),
96        "raw" => Some(4),
97        _ => None,
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn no_signals_recommends_lite() {
107        assert_eq!(recommend_level(&[]).level, CompressionLevel::Lite);
108    }
109
110    #[test]
111    fn many_rereads_recommends_less_compression() {
112        let signals: Vec<_> = (0..4)
113            .map(|_| BehaviorSignal::ReRead {
114                path: "a.rs".to_owned(),
115                gap_seconds: 1.0,
116            })
117            .collect();
118        assert_eq!(recommend_level(&signals).level, CompressionLevel::Off);
119    }
120
121    #[test]
122    fn task_complete_boosts_current_level() {
123        let profile = recommend_level(&[BehaviorSignal::TaskComplete { reads_count: 2 }]);
124        assert_eq!(profile.level, CompressionLevel::Standard);
125    }
126
127    #[test]
128    fn confidence_between_zero_and_one() {
129        let confidence = recommend_level(&[]).confidence;
130        assert!((0.0..=1.0).contains(&confidence));
131    }
132}