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 fn validate(&self) -> Result<()> {
68 ensure!(self.tool_output_threshold >= 1024, "Tool output threshold must be at least 1024 bytes");
69 ensure!(self.max_spooled_files > 0, "Max spooled files must be greater than zero");
70 ensure!(self.retained_user_messages > 0, "Retained user messages must be greater than zero");
71 Ok(())
72 }
73}
74
75fn default_dynamic_enabled() -> bool {
76 true
77}
78
79fn default_tool_output_threshold() -> usize {
80 8192 }
82
83fn default_sync_terminals() -> bool {
84 true
85}
86
87fn default_persist_history() -> bool {
88 true
89}
90
91fn default_retained_user_messages() -> usize {
92 4
93}
94
95fn default_sync_mcp_tools() -> bool {
96 true
97}
98
99fn default_sync_skills() -> bool {
100 true
101}
102
103fn default_spool_max_age_secs() -> u64 {
104 3600 }
106
107fn default_max_spooled_files() -> usize {
108 100
109}
110
111#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
112#[derive(Debug, Clone, Deserialize, Serialize)]
113pub struct LedgerConfig {
114 #[serde(default = "default_enabled")]
115 pub enabled: bool,
116 #[serde(default = "default_max_entries")]
117 pub max_entries: usize,
118 #[serde(default = "default_include_in_prompt")]
120 include_in_prompt: bool,
121 #[serde(default = "default_preserve_in_compression")]
123 preserve_in_compression: bool,
124}
125
126impl Default for LedgerConfig {
127 fn default() -> Self {
128 Self {
129 enabled: default_enabled(),
130 max_entries: default_max_entries(),
131 include_in_prompt: default_include_in_prompt(),
132 preserve_in_compression: default_preserve_in_compression(),
133 }
134 }
135}
136
137impl LedgerConfig {
138 fn validate(&self) -> Result<()> {
139 ensure!(self.max_entries > 0, "Ledger max_entries must be greater than zero");
140 Ok(())
141 }
142}
143
144#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
145#[derive(Debug, Clone, Deserialize, Serialize)]
146pub struct ContextFeaturesConfig {
147 #[serde(default = "default_max_context_tokens")]
150 pub max_context_tokens: usize,
151
152 #[serde(default = "default_trim_to_percent")]
155 trim_to_percent: u8,
156
157 #[serde(default = "default_preserve_recent_turns")]
160 pub preserve_recent_turns: usize,
161
162 #[serde(default)]
163 pub ledger: LedgerConfig,
164
165 #[serde(default)]
167 pub dynamic: DynamicContextConfig,
168}
169
170impl Default for ContextFeaturesConfig {
171 fn default() -> Self {
172 Self {
173 max_context_tokens: default_max_context_tokens(),
174 trim_to_percent: default_trim_to_percent(),
175 preserve_recent_turns: default_preserve_recent_turns(),
176 ledger: LedgerConfig::default(),
177 dynamic: DynamicContextConfig::default(),
178 }
179 }
180}
181
182impl ContextFeaturesConfig {
183 pub(crate) fn validate(&self) -> Result<()> {
184 self.ledger.validate().context("Invalid ledger configuration")?;
185 self.dynamic.validate().context("Invalid dynamic context configuration")?;
186 Ok(())
187 }
188}
189
190fn default_max_entries() -> usize {
191 12
192}
193fn default_include_in_prompt() -> bool {
194 true
195}
196pub fn default_max_context_tokens() -> usize {
197 0
198}
199
200fn default_trim_to_percent() -> u8 {
201 60
202}
203
204fn default_preserve_recent_turns() -> usize {
205 10
206}
207
208fn default_preserve_in_compression() -> bool {
209 true
210}
211
212#[cfg(test)]
213mod tests {
214 use super::DynamicContextConfig;
215
216 #[test]
217 fn dynamic_context_defaults_retain_four_user_messages() {
218 let config = DynamicContextConfig::default();
219
220 assert_eq!(config.retained_user_messages, 4);
221 }
222
223 #[test]
224 fn dynamic_context_validation_rejects_zero_retained_user_messages() {
225 let config = DynamicContextConfig {
226 retained_user_messages: 0,
227 ..DynamicContextConfig::default()
228 };
229
230 assert!(config.validate().is_err());
231 }
232}