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