1use anyhow::{Context, Result, ensure};
2use serde::{Deserialize, Serialize};
3
4#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct DynamicContextConfig {
12 #[serde(default = "default_dynamic_enabled")]
14 pub enabled: bool,
15
16 #[serde(default = "default_tool_output_threshold")]
18 pub tool_output_threshold: usize,
19
20 #[serde(default = "default_sync_terminals")]
22 pub sync_terminals: bool,
23
24 #[serde(default = "default_persist_history")]
26 pub persist_history: bool,
27
28 #[serde(default = "default_retained_user_messages")]
30 pub retained_user_messages: usize,
31
32 #[serde(default = "default_sync_mcp_tools")]
34 pub sync_mcp_tools: bool,
35
36 #[serde(default = "default_sync_skills")]
38 pub sync_skills: bool,
39
40 #[serde(default = "default_spool_max_age_secs")]
42 pub spool_max_age_secs: u64,
43
44 #[serde(default = "default_max_spooled_files")]
46 pub max_spooled_files: usize,
47}
48
49impl Default for DynamicContextConfig {
50 fn default() -> Self {
51 Self {
52 enabled: default_dynamic_enabled(),
53 tool_output_threshold: default_tool_output_threshold(),
54 sync_terminals: default_sync_terminals(),
55 persist_history: default_persist_history(),
56 retained_user_messages: default_retained_user_messages(),
57 sync_mcp_tools: default_sync_mcp_tools(),
58 sync_skills: default_sync_skills(),
59 spool_max_age_secs: default_spool_max_age_secs(),
60 max_spooled_files: default_max_spooled_files(),
61 }
62 }
63}
64
65impl DynamicContextConfig {
66 pub fn validate(&self) -> Result<()> {
67 ensure!(
68 self.tool_output_threshold >= 1024,
69 "Tool output threshold must be at least 1024 bytes"
70 );
71 ensure!(
72 self.max_spooled_files > 0,
73 "Max spooled files must be greater than zero"
74 );
75 ensure!(
76 self.retained_user_messages > 0,
77 "Retained user messages must be greater than zero"
78 );
79 Ok(())
80 }
81}
82
83fn default_dynamic_enabled() -> bool {
84 true
85}
86
87fn default_tool_output_threshold() -> usize {
88 8192 }
90
91fn default_sync_terminals() -> bool {
92 true
93}
94
95fn default_persist_history() -> bool {
96 true
97}
98
99fn default_retained_user_messages() -> usize {
100 4
101}
102
103fn default_sync_mcp_tools() -> bool {
104 true
105}
106
107fn default_sync_skills() -> bool {
108 true
109}
110
111fn default_spool_max_age_secs() -> u64 {
112 3600 }
114
115fn default_max_spooled_files() -> usize {
116 100
117}
118
119#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
120#[derive(Debug, Clone, Deserialize, Serialize)]
121pub struct LedgerConfig {
122 #[serde(default = "default_enabled")]
123 pub enabled: bool,
124 #[serde(default = "default_max_entries")]
125 pub max_entries: usize,
126 #[serde(default = "default_include_in_prompt")]
128 pub include_in_prompt: bool,
129 #[serde(default = "default_preserve_in_compression")]
131 pub preserve_in_compression: bool,
132}
133
134impl Default for LedgerConfig {
135 fn default() -> Self {
136 Self {
137 enabled: default_enabled(),
138 max_entries: default_max_entries(),
139 include_in_prompt: default_include_in_prompt(),
140 preserve_in_compression: default_preserve_in_compression(),
141 }
142 }
143}
144
145impl LedgerConfig {
146 pub fn validate(&self) -> Result<()> {
147 ensure!(
148 self.max_entries > 0,
149 "Ledger max_entries must be greater than zero"
150 );
151 Ok(())
152 }
153}
154
155#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
156#[derive(Debug, Clone, Deserialize, Serialize)]
157pub struct ContextFeaturesConfig {
158 #[serde(default = "default_max_context_tokens")]
162 pub max_context_tokens: usize,
163
164 #[serde(default = "default_trim_to_percent")]
167 pub trim_to_percent: u8,
168
169 #[serde(default = "default_preserve_recent_turns")]
172 pub preserve_recent_turns: usize,
173
174 #[serde(default)]
175 pub ledger: LedgerConfig,
176
177 #[serde(default)]
179 pub dynamic: DynamicContextConfig,
180}
181
182impl Default for ContextFeaturesConfig {
183 fn default() -> Self {
184 Self {
185 max_context_tokens: default_max_context_tokens(),
186 trim_to_percent: default_trim_to_percent(),
187 preserve_recent_turns: default_preserve_recent_turns(),
188 ledger: LedgerConfig::default(),
189 dynamic: DynamicContextConfig::default(),
190 }
191 }
192}
193
194impl ContextFeaturesConfig {
195 pub fn validate(&self) -> Result<()> {
196 self.ledger
197 .validate()
198 .context("Invalid ledger configuration")?;
199 self.dynamic
200 .validate()
201 .context("Invalid dynamic context configuration")?;
202 Ok(())
203 }
204}
205
206fn default_enabled() -> bool {
207 true
208}
209fn default_max_entries() -> usize {
210 12
211}
212fn default_include_in_prompt() -> bool {
213 true
214}
215pub fn default_max_context_tokens() -> usize {
216 90000
217}
218
219fn default_trim_to_percent() -> u8 {
220 60
221}
222
223fn default_preserve_recent_turns() -> usize {
224 10
225}
226
227fn default_preserve_in_compression() -> bool {
228 true
229}
230
231#[cfg(test)]
232mod tests {
233 use super::DynamicContextConfig;
234
235 #[test]
236 fn dynamic_context_defaults_retain_four_user_messages() {
237 let config = DynamicContextConfig::default();
238
239 assert_eq!(config.retained_user_messages, 4);
240 }
241
242 #[test]
243 fn dynamic_context_validation_rejects_zero_retained_user_messages() {
244 let config = DynamicContextConfig {
245 retained_user_messages: 0,
246 ..DynamicContextConfig::default()
247 };
248
249 assert!(config.validate().is_err());
250 }
251}