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}
160
161impl Default for ContextFeaturesConfig {
162 fn default() -> Self {
163 Self {
164 ledger: LedgerConfig::default(),
165 token_budget: TokenBudgetConfig::default(),
166 max_context_tokens: default_max_context_tokens(),
167 trim_to_percent: default_trim_to_percent(),
168 preserve_recent_turns: default_preserve_recent_turns(),
169 semantic_compression: default_semantic_compression_enabled(),
170 tool_aware_retention: default_tool_aware_retention_enabled(),
171 max_structural_depth: default_max_structural_depth(),
172 preserve_recent_tools: default_preserve_recent_tools(),
173 }
174 }
175}
176
177impl ContextFeaturesConfig {
178 pub fn validate(&self) -> Result<()> {
179 self.ledger
180 .validate()
181 .context("Invalid ledger configuration")?;
182 self.token_budget
183 .validate()
184 .context("Invalid token_budget configuration")?;
185
186 ensure!(
187 self.max_context_tokens > 0,
188 "Context features max_context_tokens must be greater than zero"
189 );
190 ensure!(
191 (1..=100).contains(&self.trim_to_percent),
192 "Context features trim_to_percent must be between 1 and 100"
193 );
194 ensure!(
195 self.preserve_recent_turns > 0,
196 "Context features preserve_recent_turns must be greater than zero"
197 );
198
199 ensure!(
200 (context_defaults::MIN_STRUCTURAL_DEPTH..=context_defaults::MAX_STRUCTURAL_DEPTH)
201 .contains(&self.max_structural_depth),
202 "Context features max_structural_depth must be between {} and {}",
203 context_defaults::MIN_STRUCTURAL_DEPTH,
204 context_defaults::MAX_STRUCTURAL_DEPTH,
205 );
206
207 ensure!(
208 (context_defaults::MIN_PRESERVE_RECENT_TOOLS
209 ..=context_defaults::MAX_PRESERVE_RECENT_TOOLS)
210 .contains(&self.preserve_recent_tools),
211 "Context features preserve_recent_tools must be between {} and {}",
212 context_defaults::MIN_PRESERVE_RECENT_TOOLS,
213 context_defaults::MAX_PRESERVE_RECENT_TOOLS,
214 );
215
216 Ok(())
217 }
218}
219
220fn default_max_context_tokens() -> usize {
221 context_defaults::DEFAULT_MAX_TOKENS
222}
223
224fn default_trim_to_percent() -> u8 {
225 context_defaults::DEFAULT_TRIM_TO_PERCENT
226}
227
228fn default_preserve_recent_turns() -> usize {
229 context_defaults::DEFAULT_PRESERVE_RECENT_TURNS
230}
231
232fn default_semantic_compression_enabled() -> bool {
233 context_defaults::DEFAULT_SEMANTIC_COMPRESSION_ENABLED
234}
235
236fn default_tool_aware_retention_enabled() -> bool {
237 context_defaults::DEFAULT_TOOL_AWARE_RETENTION_ENABLED
238}
239
240fn default_max_structural_depth() -> usize {
241 context_defaults::DEFAULT_MAX_STRUCTURAL_DEPTH
242}
243
244fn default_preserve_recent_tools() -> usize {
245 context_defaults::DEFAULT_PRESERVE_RECENT_TOOLS
246}