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_alert_threshold")]
71 pub alert_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 alert_threshold: default_alert_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.alert_threshold),
98 "Token budget alert_threshold must be between 0.0 and 1.0"
99 );
100 ensure!(
101 self.warning_threshold <= self.alert_threshold,
102 "Token budget warning_threshold must be less than or equal to alert_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_alert_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 ContextFeaturesConfig {
141 #[serde(default)]
142 pub ledger: LedgerConfig,
143 #[serde(default)]
144 pub token_budget: TokenBudgetConfig,
145 #[serde(default = "default_max_context_tokens")]
146 pub max_context_tokens: usize,
147 #[serde(default = "default_trim_to_percent")]
148 pub trim_to_percent: u8,
149 #[serde(default = "default_preserve_recent_turns")]
150 pub preserve_recent_turns: usize,
151 #[serde(default = "default_semantic_compression_enabled")]
152 pub semantic_compression: bool,
153 #[serde(default = "default_tool_aware_retention_enabled")]
154 pub tool_aware_retention: bool,
155 #[serde(default = "default_max_structural_depth")]
156 pub max_structural_depth: usize,
157 #[serde(default = "default_preserve_recent_tools")]
158 pub preserve_recent_tools: usize,
159 #[serde(default = "default_model_input_token_budget")]
162 pub model_input_token_budget: usize,
163
164 #[serde(default = "default_model_input_byte_fuse")]
167 pub model_input_byte_fuse: usize,
168}
169
170impl Default for ContextFeaturesConfig {
171 fn default() -> Self {
172 Self {
173 ledger: LedgerConfig::default(),
174 token_budget: TokenBudgetConfig::default(),
175 max_context_tokens: default_max_context_tokens(),
176 trim_to_percent: default_trim_to_percent(),
177 preserve_recent_turns: default_preserve_recent_turns(),
178 semantic_compression: default_semantic_compression_enabled(),
179 tool_aware_retention: default_tool_aware_retention_enabled(),
180 max_structural_depth: default_max_structural_depth(),
181 preserve_recent_tools: default_preserve_recent_tools(),
182 model_input_token_budget: default_model_input_token_budget(),
183 model_input_byte_fuse: default_model_input_byte_fuse(),
184 }
185 }
186}
187
188impl ContextFeaturesConfig {
189 pub fn validate(&self) -> Result<()> {
190 self.ledger
191 .validate()
192 .context("Invalid ledger configuration")?;
193 self.token_budget
194 .validate()
195 .context("Invalid token_budget configuration")?;
196
197 ensure!(
198 self.max_context_tokens > 0,
199 "Context features max_context_tokens must be greater than zero"
200 );
201 ensure!(
202 (1..=100).contains(&self.trim_to_percent),
203 "Context features trim_to_percent must be between 1 and 100"
204 );
205 ensure!(
206 self.preserve_recent_turns > 0,
207 "Context features preserve_recent_turns must be greater than zero"
208 );
209
210 ensure!(
211 (context_defaults::MIN_STRUCTURAL_DEPTH..=context_defaults::MAX_STRUCTURAL_DEPTH)
212 .contains(&self.max_structural_depth),
213 "Context features max_structural_depth must be between {} and {}",
214 context_defaults::MIN_STRUCTURAL_DEPTH,
215 context_defaults::MAX_STRUCTURAL_DEPTH,
216 );
217
218 ensure!(
219 (context_defaults::MIN_PRESERVE_RECENT_TOOLS
220 ..=context_defaults::MAX_PRESERVE_RECENT_TOOLS)
221 .contains(&self.preserve_recent_tools),
222 "Context features preserve_recent_tools must be between {} and {}",
223 context_defaults::MIN_PRESERVE_RECENT_TOOLS,
224 context_defaults::MAX_PRESERVE_RECENT_TOOLS,
225 );
226
227 Ok(())
228 }
229}
230
231fn default_max_context_tokens() -> usize {
232 context_defaults::DEFAULT_MAX_TOKENS
233}
234
235fn default_trim_to_percent() -> u8 {
236 context_defaults::DEFAULT_TRIM_TO_PERCENT
237}
238
239fn default_preserve_recent_turns() -> usize {
240 context_defaults::DEFAULT_PRESERVE_RECENT_TURNS
241}
242
243fn default_semantic_compression_enabled() -> bool {
244 context_defaults::DEFAULT_SEMANTIC_COMPRESSION_ENABLED
245}
246
247fn default_tool_aware_retention_enabled() -> bool {
248 context_defaults::DEFAULT_TOOL_AWARE_RETENTION_ENABLED
249}
250
251fn default_max_structural_depth() -> usize {
252 context_defaults::DEFAULT_MAX_STRUCTURAL_DEPTH
253}
254
255fn default_preserve_recent_tools() -> usize {
256 context_defaults::DEFAULT_PRESERVE_RECENT_TOOLS
257}
258
259fn default_model_input_token_budget() -> usize {
260 crate::constants::context::DEFAULT_MODEL_INPUT_TOKEN_BUDGET
261}
262
263fn default_model_input_byte_fuse() -> usize {
264 crate::constants::context::DEFAULT_MODEL_INPUT_BYTE_FUSE
265}