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