1use serde::{Deserialize, Serialize};
10
11fn fidelity_lookahead_depth_default() -> u8 {
12 FidelityConfig::default_lookahead_depth()
13}
14
15#[derive(Debug, Clone, Deserialize, Serialize)]
30#[serde(default)]
31pub struct FidelityConfig {
32 pub enabled: bool,
34 #[serde(alias = "w_keyword")]
38 pub w_semantic: f32,
39 pub w_temporal: f32,
41 pub w_importance: f32,
43 pub w_plan: f32,
45 pub full_threshold: f32,
47 pub compressed_threshold: f32,
49 pub compressed_max_tokens: usize,
51 pub regrade_threshold: f32,
53 pub min_query_length: usize,
55 pub max_scored_messages: usize,
57 #[serde(default)]
63 pub exempt_tail_messages: usize,
64 #[serde(default)]
67 pub compress_provider: Option<String>,
68 #[serde(default)]
71 pub semantic_scoring_provider: Option<String>,
72 #[serde(default = "fidelity_lookahead_depth_default")]
78 pub lookahead_depth: u8,
79 #[serde(default = "default_embed_concurrency")]
84 pub embed_concurrency: usize,
85 #[serde(default)]
90 pub max_embed_input_tokens: Option<usize>,
91 #[serde(default)]
96 pub max_compress_input_tokens: Option<usize>,
97}
98
99fn default_embed_concurrency() -> usize {
100 32
101}
102
103impl FidelityConfig {
104 #[must_use]
109 pub fn default_lookahead_depth() -> u8 {
110 3
111 }
112
113 pub fn validate(&self) -> Result<(), String> {
135 if self.compressed_threshold < 0.0 {
136 return Err("context.fidelity: compressed_threshold must be >= 0.0".into());
137 }
138 if self.full_threshold > 1.0 {
139 return Err("context.fidelity: full_threshold must be <= 1.0".into());
140 }
141 if self.full_threshold < self.compressed_threshold {
142 return Err(format!(
143 "context.fidelity: full_threshold ({}) must be >= compressed_threshold ({})",
144 self.full_threshold, self.compressed_threshold
145 ));
146 }
147 if self.lookahead_depth > 5 {
148 return Err(format!(
149 "context.fidelity: lookahead_depth ({}) must be <= 5",
150 self.lookahead_depth
151 ));
152 }
153 Ok(())
154 }
155}
156
157impl Default for FidelityConfig {
158 fn default() -> Self {
159 Self {
160 enabled: false,
161 w_semantic: 0.3,
162 w_temporal: 0.3,
163 w_importance: 0.2,
164 w_plan: 0.2,
165 full_threshold: 0.7,
166 compressed_threshold: 0.3,
167 compressed_max_tokens: 50,
168 regrade_threshold: 0.6,
169 min_query_length: 8,
170 max_scored_messages: 500,
171 exempt_tail_messages: 0,
172 compress_provider: None,
173 semantic_scoring_provider: None,
174 lookahead_depth: Self::default_lookahead_depth(),
175 embed_concurrency: default_embed_concurrency(),
176 max_embed_input_tokens: None,
177 max_compress_input_tokens: None,
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn default_disabled() {
188 let cfg = FidelityConfig::default();
189 assert!(!cfg.enabled);
190 }
191
192 #[test]
193 fn deserialize_enabled() {
194 let toml_str = r"
195 enabled = true
196 w_semantic = 0.4
197 regrade_threshold = 0.7
198 ";
199 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
200 assert!(cfg.enabled);
201 assert!((cfg.w_semantic - 0.4).abs() < f32::EPSILON);
202 assert!((cfg.regrade_threshold - 0.7).abs() < f32::EPSILON);
203 }
204
205 #[test]
206 fn deserialize_w_keyword_alias() {
207 let toml_str = r"
208 enabled = true
209 w_keyword = 0.25
210 ";
211 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
212 assert!((cfg.w_semantic - 0.25).abs() < f32::EPSILON);
213 }
214
215 #[test]
216 fn deserialize_semantic_scoring_provider() {
217 let toml_str = r#"
218 enabled = true
219 semantic_scoring_provider = "embed-fast"
220 "#;
221 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
222 assert_eq!(cfg.semantic_scoring_provider.as_deref(), Some("embed-fast"));
223 }
224
225 #[test]
226 fn deserialize_defaults_for_omitted_fields() {
227 let cfg: FidelityConfig = toml::from_str("enabled = false").unwrap();
228 assert!((cfg.w_temporal - 0.3).abs() < f32::EPSILON);
229 assert_eq!(cfg.compressed_max_tokens, 50);
230 assert_eq!(cfg.max_scored_messages, 500);
231 }
232
233 #[test]
234 fn validate_defaults_ok() {
235 assert!(FidelityConfig::default().validate().is_ok());
236 }
237
238 #[test]
239 fn validate_inverted_thresholds_err() {
240 let cfg = FidelityConfig {
241 full_threshold: 0.2,
242 compressed_threshold: 0.5,
243 ..FidelityConfig::default()
244 };
245 let err = cfg.validate().unwrap_err();
246 assert!(
247 err.contains("full_threshold"),
248 "error should mention full_threshold: {err}"
249 );
250 }
251
252 #[test]
253 fn validate_negative_compressed_threshold_err() {
254 let cfg = FidelityConfig {
255 compressed_threshold: -0.1,
256 ..FidelityConfig::default()
257 };
258 assert!(cfg.validate().is_err());
259 }
260
261 #[test]
262 fn validate_full_threshold_above_one_err() {
263 let cfg = FidelityConfig {
264 full_threshold: 1.1,
265 ..FidelityConfig::default()
266 };
267 assert!(cfg.validate().is_err());
268 }
269
270 #[test]
271 fn default_lookahead_depth_is_three() {
272 assert_eq!(FidelityConfig::default().lookahead_depth, 3);
273 }
274
275 #[test]
276 fn lookahead_depth_zero_is_valid() {
277 let cfg = FidelityConfig {
278 lookahead_depth: 0,
279 ..FidelityConfig::default()
280 };
281 assert!(cfg.validate().is_ok());
282 }
283
284 #[test]
285 fn lookahead_depth_five_is_valid() {
286 let cfg = FidelityConfig {
287 lookahead_depth: 5,
288 ..FidelityConfig::default()
289 };
290 assert!(cfg.validate().is_ok());
291 }
292
293 #[test]
294 fn lookahead_depth_above_five_is_err() {
295 let cfg = FidelityConfig {
296 lookahead_depth: 6,
297 ..FidelityConfig::default()
298 };
299 let err = cfg.validate().unwrap_err();
300 assert!(
301 err.contains("lookahead_depth"),
302 "error should mention lookahead_depth: {err}"
303 );
304 }
305
306 #[test]
307 fn deserialize_lookahead_depth() {
308 let toml_str = "enabled = true\nlookahead_depth = 2";
309 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
310 assert_eq!(cfg.lookahead_depth, 2);
311 }
312
313 #[test]
314 fn deserialize_defaults_lookahead_depth_when_omitted() {
315 let cfg: FidelityConfig = toml::from_str("enabled = false").unwrap();
316 assert_eq!(cfg.lookahead_depth, 3);
317 }
318
319 #[test]
320 fn deserialize_new_perf_fields_defaults() {
321 let cfg: FidelityConfig = toml::from_str("enabled = false").unwrap();
322 assert_eq!(cfg.embed_concurrency, 32);
323 assert!(cfg.max_embed_input_tokens.is_none());
324 assert!(cfg.max_compress_input_tokens.is_none());
325 }
326
327 #[test]
328 fn deserialize_new_perf_fields_custom() {
329 let toml_str = r"
330 enabled = true
331 embed_concurrency = 8
332 max_embed_input_tokens = 512
333 max_compress_input_tokens = 1024
334 ";
335 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
336 assert_eq!(cfg.embed_concurrency, 8);
337 assert_eq!(cfg.max_embed_input_tokens, Some(512));
338 assert_eq!(cfg.max_compress_input_tokens, Some(1024));
339 }
340}