deepstrike_core/context/config.rs
1/// All compression and context management parameters expressed as fractions of
2/// `max_tokens`. This is the single control surface for the compression pipeline:
3/// changing `max_tokens` (e.g. switching model) rescales every derived limit
4/// automatically with no other configuration change required.
5///
6/// Invariant: snip < micro < collapse < auto < renewal (strictly increasing).
7#[derive(Debug, Clone)]
8pub struct ContextConfig {
9 // ── Pressure thresholds ─────────────────────────────────────────────────
10 pub snip_threshold: f64,
11 pub micro_threshold: f64,
12 pub collapse_threshold: f64,
13 pub auto_threshold: f64,
14 pub renewal_threshold: f64,
15
16 // ── Post-compression target ──────────────────────────────────────────────
17 /// Target rho after any compression pass. Must be < snip_threshold.
18 pub target_after_compress: f64,
19
20 // ── Per-compactor ratios ─────────────────────────────────────────────────
21 /// Fraction of max_tokens any single message may occupy after SnipCompact.
22 /// Messages smaller than this are never touched.
23 pub snip_per_msg_ratio: f64,
24
25 // ── Renewal ──────────────────────────────────────────────────────────────
26 /// Fraction of max_tokens worth of history tokens to carry across renewal.
27 /// Renewal stops carrying messages once this token budget is exhausted.
28 pub carryover_ratio: f64,
29
30 // ── Recovery / repair ────────────────────────────────────────────────────
31 /// Maximum fraction of max_tokens a recovery/replay payload may occupy.
32 pub recovery_content_ratio: f64,
33
34 /// Recent history messages always kept during render.
35 pub preserve_recent_msgs: usize,
36
37 /// Number of most-recent turns (user+assistant pairs) preserved by
38 /// CollapseCompactor and AutoCompactor. Each turn = 2 messages, so
39 /// the actual message count kept is `preserve_recent_turns * 2`.
40 /// Must be ≥ 1. Default: 2 (= 4 messages).
41 pub preserve_recent_turns: usize,
42
43 // ── Noise reduction ──────────────────────────────────────────────────────
44 /// Include the dashboard block in the rendered system context.
45 /// Defaults to false; enable only in explicit agent-os mode.
46 pub render_dashboard: bool,
47
48 /// Use verbose internal control notes (e.g. "[SYSTEM] Transaction rollback: …").
49 /// Defaults to false; uses concise natural-language notes instead.
50 pub verbose_control_notes: bool,
51
52 /// Collapse the *narration* text of OLD assistant turns (those past the
53 /// `preserve_recent_msgs` window that also carry tool calls) to a short stub at render time —
54 /// non-destructively (the full text stays in `partitions.history`). The model's user-facing
55 /// preamble ("好的,我来…先X") has no value once it has aged out of the recent window, but
56 /// re-feeding it verbatim every turn primes the model to keep emitting the same preamble (an
57 /// in-context repetition trap). Tool calls and pairing are untouched; current progress lives in
58 /// the TASK STATE turn. Defaults to true.
59 pub collapse_assistant_narration: bool,
60
61 // ── Layer 3: Time-based decay ───────────────────────────────────────
62
63 /// Minutes of inactivity before triggering Micro-Compact (Layer 3).
64 /// Defaults to 60 minutes — assumes Prompt Cache has expired by then.
65 pub micro_compact_idle_minutes: u32,
66
67 /// Number of recent tool results to preserve during Micro-Compact.
68 pub preserved_tool_results: usize,
69
70 // ── Layer 5: Auto-Compact buffer ─────────────────────────────────────
71
72 /// Buffer size for Auto-Compact trigger (Layer 5).
73 /// Trigger threshold = max_tokens - autocompact_buffer.
74 /// Defaults to 13K tokens (p99.99 of summarizer output length + safety margin).
75 pub autocompact_buffer: u32,
76
77 // ── Layer 1: Large-result spool ──────────────────────────────────────
78
79 /// Byte size above which a single tool result is spooled (Layer 1): the kernel
80 /// keeps only a preview in context and emits `LargeResultSpooled` for the SDK to
81 /// persist the full content to disk. Default: 50 KiB. `0` disables spooling.
82 pub spool_threshold_bytes: u32,
83
84 /// Preview byte budget kept in context when a tool result is spooled. Default: 2 KiB.
85 pub spool_preview_bytes: u32,
86
87 // ── K2: knowledge budget ─────────────────────────────────────────────
88
89 /// Max share of `max_tokens` the knowledge partition may occupy. Exceeding it emits a
90 /// `KnowledgeBudgetExceeded` observation (once per cache generation) and marks the OLDEST
91 /// unpinned, non-skill entries for eviction at the next compaction/renewal boundary until the
92 /// projected usage fits. Pinned entries and `skill:`-keyed pins are never budget-evicted
93 /// (skills are governed by deactivation/lease, not the budget). `0.0` disables (no cap).
94 /// Default: 0.25.
95 pub knowledge_budget_ratio: f64,
96}
97
98fn default_micro_compact_idle_minutes() -> u32 {
99 60
100}
101
102fn default_preserved_tool_results() -> usize {
103 5
104}
105
106fn default_autocompact_buffer() -> u32 {
107 13_000
108}
109
110impl Default for ContextConfig {
111 fn default() -> Self {
112 Self {
113 snip_threshold: 0.70,
114 micro_threshold: 0.80,
115 collapse_threshold: 0.90,
116 auto_threshold: 0.95,
117 renewal_threshold: 0.98,
118 target_after_compress: 0.65,
119 snip_per_msg_ratio: 0.05,
120 carryover_ratio: 0.05,
121 recovery_content_ratio: 0.25,
122 preserve_recent_msgs: 4,
123 preserve_recent_turns: 2,
124 render_dashboard: false,
125 verbose_control_notes: false,
126 collapse_assistant_narration: true,
127 micro_compact_idle_minutes: 60,
128 preserved_tool_results: 5,
129 autocompact_buffer: 13_000,
130 spool_threshold_bytes: 50 * 1024,
131 spool_preview_bytes: 2 * 1024,
132 knowledge_budget_ratio: 0.25,
133 }
134 }
135}
136
137impl ContextConfig {
138 /// Token budget to target after a compression pass.
139 pub fn target_tokens(&self, max_tokens: u32) -> u32 {
140 (max_tokens as f64 * self.target_after_compress) as u32
141 }
142
143 /// Per-message token cap used by SnipCompact.
144 /// Floor of 50 ensures very small context windows still get useful output.
145 pub fn snip_per_msg_tokens(&self, max_tokens: u32) -> u32 {
146 ((max_tokens as f64 * self.snip_per_msg_ratio) as u32).max(50)
147 }
148
149 /// Token budget for history carryover across renewal.
150 pub fn carryover_tokens(&self, max_tokens: u32) -> u32 {
151 ((max_tokens as f64 * self.carryover_ratio) as u32).max(100)
152 }
153
154 /// Token cap for a single recovery/replay payload.
155 pub fn recovery_content_tokens(&self, max_tokens: u32) -> u32 {
156 (max_tokens as f64 * self.recovery_content_ratio) as u32
157 }
158
159 /// Auto-Compact trigger threshold (Layer 5).
160 /// Returns `max_tokens - autocompact_buffer` (absolute value).
161 pub fn autocompact_threshold(&self, max_tokens: u32) -> u32 {
162 max_tokens.saturating_sub(self.autocompact_buffer)
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn noise_reduction_defaults_to_quiet() {
172 let c = ContextConfig::default();
173 assert!(!c.render_dashboard, "dashboard should be off by default");
174 assert!(!c.verbose_control_notes, "verbose notes should be off by default");
175 }
176
177 #[test]
178 fn default_thresholds_strictly_increasing() {
179 let c = ContextConfig::default();
180 assert!(c.snip_threshold < c.micro_threshold);
181 assert!(c.micro_threshold < c.collapse_threshold);
182 assert!(c.collapse_threshold < c.auto_threshold);
183 assert!(c.auto_threshold < c.renewal_threshold);
184 }
185
186 #[test]
187 fn target_after_compress_below_snip_threshold() {
188 let c = ContextConfig::default();
189 assert!(c.target_after_compress < c.snip_threshold);
190 }
191
192 #[test]
193 fn derived_limits_scale_with_max_tokens() {
194 let c = ContextConfig::default();
195 let small = 8_000u32;
196 let large = 200_000u32;
197 let ratio = c.snip_per_msg_tokens(large) as f64 / c.snip_per_msg_tokens(small) as f64;
198 assert!((ratio - 25.0).abs() < 1.0, "expected ~25×, got {ratio}");
199 }
200
201 #[test]
202 fn small_context_window_has_floor() {
203 let c = ContextConfig::default();
204 assert!(c.snip_per_msg_tokens(100) >= 50);
205 assert!(c.carryover_tokens(100) >= 100);
206 }
207}