1use crate::constants::context as context_defaults;
2use anyhow::{Context, Result, ensure};
3use serde::{Deserialize, Serialize};
4
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[derive(Debug, Clone, Deserialize, Serialize)]
7pub struct LedgerConfig {
8 #[serde(default = "default_enabled")]
9 pub enabled: bool,
10 #[serde(default = "default_max_entries")]
11 pub max_entries: usize,
12 #[serde(default = "default_include_in_prompt")]
14 pub include_in_prompt: bool,
15 #[serde(default = "default_preserve_in_compression")]
17 pub preserve_in_compression: bool,
18}
19
20impl Default for LedgerConfig {
21 fn default() -> Self {
22 Self {
23 enabled: default_enabled(),
24 max_entries: default_max_entries(),
25 include_in_prompt: default_include_in_prompt(),
26 preserve_in_compression: default_preserve_in_compression(),
27 }
28 }
29}
30
31impl LedgerConfig {
32 pub fn validate(&self) -> Result<()> {
33 ensure!(
34 self.max_entries > 0,
35 "Ledger max_entries must be greater than zero"
36 );
37 Ok(())
38 }
39}
40
41fn default_enabled() -> bool {
42 true
43}
44fn default_max_entries() -> usize {
45 12
46}
47fn default_include_in_prompt() -> bool {
48 true
49}
50fn default_preserve_in_compression() -> bool {
51 true
52}
53
54#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
55#[derive(Debug, Clone, Deserialize, Serialize)]
56pub struct TokenBudgetConfig {
57 #[serde(default = "default_token_budget_enabled")]
59 pub enabled: bool,
60 #[serde(default = "default_token_budget_model")]
62 pub model: String,
63 #[serde(default)]
65 pub tokenizer: Option<String>,
66 #[serde(default = "default_warning_threshold")]
68 pub warning_threshold: f64,
69 #[serde(default = "default_compaction_threshold")]
71 pub compaction_threshold: f64,
72 #[serde(default = "default_detailed_tracking")]
74 pub detailed_tracking: bool,
75}
76
77impl Default for TokenBudgetConfig {
78 fn default() -> Self {
79 Self {
80 enabled: default_token_budget_enabled(),
81 model: default_token_budget_model(),
82 tokenizer: None,
83 warning_threshold: default_warning_threshold(),
84 compaction_threshold: default_compaction_threshold(),
85 detailed_tracking: default_detailed_tracking(),
86 }
87 }
88}
89
90impl TokenBudgetConfig {
91 pub fn validate(&self) -> Result<()> {
92 ensure!(
93 (0.0..=1.0).contains(&self.warning_threshold),
94 "Token budget warning_threshold must be between 0.0 and 1.0"
95 );
96 ensure!(
97 (0.0..=1.0).contains(&self.compaction_threshold),
98 "Token budget compaction_threshold must be between 0.0 and 1.0"
99 );
100 ensure!(
101 self.warning_threshold <= self.compaction_threshold,
102 "Token budget warning_threshold must be less than or equal to compaction_threshold"
103 );
104
105 if self.enabled {
106 ensure!(
107 !self.model.trim().is_empty(),
108 "Token budget model must be provided when token budgeting is enabled"
109 );
110 if let Some(tokenizer) = &self.tokenizer {
111 ensure!(
112 !tokenizer.trim().is_empty(),
113 "Token budget tokenizer override cannot be empty"
114 );
115 }
116 }
117
118 Ok(())
119 }
120}
121
122fn default_token_budget_enabled() -> bool {
123 true
124}
125fn default_token_budget_model() -> String {
126 "gpt-5-nano".to_string()
127}
128fn default_warning_threshold() -> f64 {
129 0.75
130}
131fn default_compaction_threshold() -> f64 {
132 0.85
133}
134fn default_detailed_tracking() -> bool {
135 false
136}
137
138#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
139#[derive(Debug, Clone, Deserialize, Serialize)]
140pub struct ContextCurationConfig {
141 #[serde(default = "default_curation_enabled")]
143 pub enabled: bool,
144 #[serde(default = "default_max_tokens_per_turn")]
146 pub max_tokens_per_turn: usize,
147 #[serde(default = "default_preserve_recent_messages")]
149 pub preserve_recent_messages: usize,
150 #[serde(default = "default_max_tool_descriptions")]
152 pub max_tool_descriptions: usize,
153 #[serde(default = "default_include_ledger")]
155 pub include_ledger: bool,
156 #[serde(default = "default_ledger_max_entries")]
158 pub ledger_max_entries: usize,
159 #[serde(default = "default_include_recent_errors")]
161 pub include_recent_errors: bool,
162 #[serde(default = "default_max_recent_errors")]
164 pub max_recent_errors: usize,
165}
166
167impl Default for ContextCurationConfig {
168 fn default() -> Self {
169 Self {
170 enabled: default_curation_enabled(),
171 max_tokens_per_turn: default_max_tokens_per_turn(),
172 preserve_recent_messages: default_preserve_recent_messages(),
173 max_tool_descriptions: default_max_tool_descriptions(),
174 include_ledger: default_include_ledger(),
175 ledger_max_entries: default_ledger_max_entries(),
176 include_recent_errors: default_include_recent_errors(),
177 max_recent_errors: default_max_recent_errors(),
178 }
179 }
180}
181
182impl ContextCurationConfig {
183 pub fn validate(&self) -> Result<()> {
184 ensure!(
185 self.max_tokens_per_turn > 0,
186 "Context curation max_tokens_per_turn must be greater than zero"
187 );
188
189 if self.include_ledger {
190 ensure!(
191 self.ledger_max_entries > 0,
192 "Context curation ledger_max_entries must be greater than zero when ledger inclusion is enabled"
193 );
194 }
195
196 if self.include_recent_errors {
197 ensure!(
198 self.max_recent_errors > 0,
199 "Context curation max_recent_errors must be greater than zero when recent errors are included"
200 );
201 }
202
203 ensure!(
204 self.max_tool_descriptions > 0,
205 "Context curation max_tool_descriptions must be greater than zero"
206 );
207
208 Ok(())
209 }
210}
211
212fn default_curation_enabled() -> bool {
213 true
214}
215fn default_max_tokens_per_turn() -> usize {
216 100_000
217}
218fn default_preserve_recent_messages() -> usize {
219 5
220}
221fn default_max_tool_descriptions() -> usize {
222 10
223}
224fn default_include_ledger() -> bool {
225 true
226}
227fn default_ledger_max_entries() -> usize {
228 12
229}
230fn default_include_recent_errors() -> bool {
231 true
232}
233fn default_max_recent_errors() -> usize {
234 3
235}
236
237#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
238#[derive(Debug, Clone, Deserialize, Serialize)]
239pub struct ContextFeaturesConfig {
240 #[serde(default)]
241 pub ledger: LedgerConfig,
242 #[serde(default)]
243 pub token_budget: TokenBudgetConfig,
244 #[serde(default)]
245 pub curation: ContextCurationConfig,
246 #[serde(default = "default_max_context_tokens")]
247 pub max_context_tokens: usize,
248 #[serde(default = "default_trim_to_percent")]
249 pub trim_to_percent: u8,
250 #[serde(default = "default_preserve_recent_turns")]
251 pub preserve_recent_turns: usize,
252}
253
254impl Default for ContextFeaturesConfig {
255 fn default() -> Self {
256 Self {
257 ledger: LedgerConfig::default(),
258 token_budget: TokenBudgetConfig::default(),
259 curation: ContextCurationConfig::default(),
260 max_context_tokens: default_max_context_tokens(),
261 trim_to_percent: default_trim_to_percent(),
262 preserve_recent_turns: default_preserve_recent_turns(),
263 }
264 }
265}
266
267impl ContextFeaturesConfig {
268 pub fn validate(&self) -> Result<()> {
269 self.ledger
270 .validate()
271 .context("Invalid ledger configuration")?;
272 self.token_budget
273 .validate()
274 .context("Invalid token_budget configuration")?;
275 self.curation
276 .validate()
277 .context("Invalid context curation configuration")?;
278
279 ensure!(
280 self.max_context_tokens > 0,
281 "Context features max_context_tokens must be greater than zero"
282 );
283 ensure!(
284 (1..=100).contains(&self.trim_to_percent),
285 "Context features trim_to_percent must be between 1 and 100"
286 );
287 ensure!(
288 self.preserve_recent_turns > 0,
289 "Context features preserve_recent_turns must be greater than zero"
290 );
291
292 Ok(())
293 }
294}
295
296fn default_max_context_tokens() -> usize {
297 context_defaults::DEFAULT_MAX_TOKENS
298}
299
300fn default_trim_to_percent() -> u8 {
301 context_defaults::DEFAULT_TRIM_TO_PERCENT
302}
303
304fn default_preserve_recent_turns() -> usize {
305 context_defaults::DEFAULT_PRESERVE_RECENT_TURNS
306}