Skip to main content

deepstrike_core/context/
policy.rs

1use super::config::ContextConfig;
2
3pub const PPM_SCALE: u32 = 1_000_000;
4
5/// Stable public context policy. Algorithm-only compactor knobs intentionally stay in
6/// `ContextConfig` and are not part of this replay contract.
7#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct ContextPolicy {
10    pub pressure_thresholds_ppm: PressureThresholdsPpm,
11    pub target_after_compress_ppm: u32,
12    pub preserve_recent_turns: u32,
13    pub renewal_carryover_ppm: u32,
14    pub collapse_old_assistant_narration: bool,
15    pub idle_micro_compact_minutes: u32,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct PressureThresholdsPpm {
21    pub snip: u32,
22    pub micro: u32,
23    pub collapse: u32,
24    pub auto: u32,
25    pub renewal: u32,
26}
27
28impl ContextPolicy {
29    pub fn validate(&self) -> Result<(), String> {
30        let p = &self.pressure_thresholds_ppm;
31        if [p.snip, p.micro, p.collapse, p.auto, p.renewal]
32            .into_iter()
33            .any(|value| value > PPM_SCALE)
34        {
35            return Err("context pressure thresholds must be between 0 and 1000000 ppm".into());
36        }
37        if !(p.snip < p.micro && p.micro < p.collapse && p.collapse < p.auto && p.auto < p.renewal)
38        {
39            return Err(
40                "context pressure thresholds must satisfy snip < micro < collapse < auto < renewal"
41                    .into(),
42            );
43        }
44        if self.target_after_compress_ppm >= p.snip {
45            return Err("target_after_compress_ppm must be lower than the snip threshold".into());
46        }
47        if self.renewal_carryover_ppm > PPM_SCALE {
48            return Err("renewal_carryover_ppm must be at most 1000000".into());
49        }
50        if self.preserve_recent_turns == 0 {
51            return Err("preserve_recent_turns must be greater than zero".into());
52        }
53        Ok(())
54    }
55
56    pub(crate) fn apply_to(&self, config: &mut ContextConfig) {
57        let p = &self.pressure_thresholds_ppm;
58        config.snip_threshold = ppm_ratio(p.snip);
59        config.micro_threshold = ppm_ratio(p.micro);
60        config.collapse_threshold = ppm_ratio(p.collapse);
61        config.auto_threshold = ppm_ratio(p.auto);
62        config.renewal_threshold = ppm_ratio(p.renewal);
63        config.target_after_compress = ppm_ratio(self.target_after_compress_ppm);
64        config.preserve_recent_turns = self.preserve_recent_turns as usize;
65        config.carryover_ratio = ppm_ratio(self.renewal_carryover_ppm);
66        config.collapse_assistant_narration = self.collapse_old_assistant_narration;
67        config.micro_compact_idle_minutes = self.idle_micro_compact_minutes;
68    }
69}
70
71fn ppm_ratio(value: u32) -> f64 {
72    value as f64 / PPM_SCALE as f64
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    fn policy() -> ContextPolicy {
80        ContextPolicy {
81            pressure_thresholds_ppm: PressureThresholdsPpm {
82                snip: 700_000,
83                micro: 800_000,
84                collapse: 900_000,
85                auto: 950_000,
86                renewal: 980_000,
87            },
88            target_after_compress_ppm: 650_000,
89            preserve_recent_turns: 2,
90            renewal_carryover_ppm: 50_000,
91            collapse_old_assistant_narration: true,
92            idle_micro_compact_minutes: 60,
93        }
94    }
95
96    #[test]
97    fn applies_the_stable_integer_wire_to_internal_config() {
98        let mut config = ContextConfig::default();
99        policy().apply_to(&mut config);
100        assert_eq!(config.snip_threshold, 0.7);
101        assert_eq!(config.target_after_compress, 0.65);
102        assert_eq!(config.carryover_ratio, 0.05);
103        assert_eq!(config.preserve_recent_turns, 2);
104    }
105
106    #[test]
107    fn validates_the_policy_as_one_atomic_unit() {
108        let mut invalid = policy();
109        invalid.pressure_thresholds_ppm.micro = 699_999;
110        assert!(invalid.validate().unwrap_err().contains("snip < micro"));
111        let mut invalid = policy();
112        invalid.target_after_compress_ppm = 700_000;
113        assert!(invalid.validate().unwrap_err().contains("lower than"));
114    }
115
116    #[test]
117    fn rejects_the_removed_version_field() {
118        let mut value = serde_json::to_value(policy()).unwrap();
119        value["version"] = serde_json::json!(1);
120        assert!(serde_json::from_value::<ContextPolicy>(value).is_err());
121    }
122}