1use crate::providers::ProviderName;
10use serde::{Deserialize, Serialize};
11
12fn fidelity_lookahead_depth_default() -> u8 {
13 FidelityConfig::default_lookahead_depth()
14}
15
16#[derive(Debug, Clone, Deserialize, Serialize)]
31#[serde(default)]
32pub struct FidelityConfig {
33 pub enabled: bool,
35 #[serde(alias = "w_keyword")]
39 pub w_semantic: f32,
40 pub w_temporal: f32,
42 pub w_importance: f32,
44 pub w_plan: f32,
46 pub full_threshold: f32,
48 pub compressed_threshold: f32,
50 pub compressed_max_tokens: usize,
52 pub regrade_threshold: f32,
54 pub min_query_length: usize,
56 pub max_scored_messages: usize,
58 #[serde(default)]
64 pub exempt_tail_messages: usize,
65 #[serde(default)]
68 pub compress_provider: Option<ProviderName>,
69 #[serde(default)]
72 pub semantic_scoring_provider: Option<ProviderName>,
73 #[serde(default = "fidelity_lookahead_depth_default")]
79 pub lookahead_depth: u8,
80 #[serde(default = "default_embed_concurrency")]
85 pub embed_concurrency: usize,
86 #[serde(default)]
91 pub max_embed_input_tokens: Option<usize>,
92 #[serde(default)]
97 pub max_compress_input_tokens: Option<usize>,
98 #[serde(default = "default_thirty")]
103 pub embed_timeout_secs: u64,
104 #[serde(default = "default_thirty")]
109 pub compress_timeout_secs: u64,
110}
111
112fn default_embed_concurrency() -> usize {
113 32
114}
115
116fn default_thirty() -> u64 {
117 30
118}
119
120impl FidelityConfig {
121 #[must_use]
126 pub fn default_lookahead_depth() -> u8 {
127 3
128 }
129
130 #[must_use = "validation result must be checked"]
152 pub fn validate(&self) -> Result<(), String> {
153 if self.compressed_threshold < 0.0 {
154 return Err("memory.fidelity: compressed_threshold must be >= 0.0".into());
155 }
156 if self.full_threshold > 1.0 {
157 return Err("memory.fidelity: full_threshold must be <= 1.0".into());
158 }
159 if self.full_threshold < self.compressed_threshold {
160 return Err(format!(
161 "memory.fidelity: full_threshold ({}) must be >= compressed_threshold ({})",
162 self.full_threshold, self.compressed_threshold
163 ));
164 }
165 if self.lookahead_depth > 5 {
166 return Err(format!(
167 "memory.fidelity: lookahead_depth ({}) must be <= 5",
168 self.lookahead_depth
169 ));
170 }
171 if self.embed_timeout_secs == 0 {
172 return Err(
173 "memory.fidelity: embed_timeout_secs must be > 0 (zero causes immediate timeout)"
174 .into(),
175 );
176 }
177 if self.compress_timeout_secs == 0 {
178 return Err(
179 "memory.fidelity: compress_timeout_secs must be > 0 (zero causes immediate timeout)"
180 .into(),
181 );
182 }
183 Ok(())
184 }
185}
186
187impl Default for FidelityConfig {
188 fn default() -> Self {
189 Self {
190 enabled: false,
191 w_semantic: 0.3,
192 w_temporal: 0.3,
193 w_importance: 0.2,
194 w_plan: 0.2,
195 full_threshold: 0.7,
196 compressed_threshold: 0.3,
197 compressed_max_tokens: 50,
198 regrade_threshold: 0.6,
199 min_query_length: 8,
200 max_scored_messages: 500,
201 exempt_tail_messages: 0,
202 compress_provider: None,
203 semantic_scoring_provider: None,
204 lookahead_depth: Self::default_lookahead_depth(),
205 embed_concurrency: default_embed_concurrency(),
206 max_embed_input_tokens: None,
207 max_compress_input_tokens: None,
208 embed_timeout_secs: default_thirty(),
209 compress_timeout_secs: default_thirty(),
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn default_disabled() {
220 let cfg = FidelityConfig::default();
221 assert!(!cfg.enabled);
222 }
223
224 #[test]
225 fn deserialize_enabled() {
226 let toml_str = r"
227 enabled = true
228 w_semantic = 0.4
229 regrade_threshold = 0.7
230 ";
231 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
232 assert!(cfg.enabled);
233 assert!((cfg.w_semantic - 0.4).abs() < f32::EPSILON);
234 assert!((cfg.regrade_threshold - 0.7).abs() < f32::EPSILON);
235 }
236
237 #[test]
238 fn deserialize_w_keyword_alias() {
239 let toml_str = r"
240 enabled = true
241 w_keyword = 0.25
242 ";
243 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
244 assert!((cfg.w_semantic - 0.25).abs() < f32::EPSILON);
245 }
246
247 #[test]
248 fn deserialize_semantic_scoring_provider() {
249 let toml_str = r#"
250 enabled = true
251 semantic_scoring_provider = "embed-fast"
252 "#;
253 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
254 assert_eq!(
255 cfg.semantic_scoring_provider
256 .as_ref()
257 .map(ProviderName::as_str),
258 Some("embed-fast")
259 );
260 }
261
262 #[test]
263 fn deserialize_defaults_for_omitted_fields() {
264 let cfg: FidelityConfig = toml::from_str("enabled = false").unwrap();
265 assert!((cfg.w_temporal - 0.3).abs() < f32::EPSILON);
266 assert_eq!(cfg.compressed_max_tokens, 50);
267 assert_eq!(cfg.max_scored_messages, 500);
268 }
269
270 #[test]
271 fn validate_defaults_ok() {
272 assert!(FidelityConfig::default().validate().is_ok());
273 }
274
275 #[test]
276 fn validate_inverted_thresholds_err() {
277 let cfg = FidelityConfig {
278 full_threshold: 0.2,
279 compressed_threshold: 0.5,
280 ..FidelityConfig::default()
281 };
282 let err = cfg.validate().unwrap_err();
283 assert!(
284 err.contains("full_threshold"),
285 "error should mention full_threshold: {err}"
286 );
287 }
288
289 #[test]
290 fn validate_negative_compressed_threshold_err() {
291 let cfg = FidelityConfig {
292 compressed_threshold: -0.1,
293 ..FidelityConfig::default()
294 };
295 assert!(cfg.validate().is_err());
296 }
297
298 #[test]
299 fn validate_full_threshold_above_one_err() {
300 let cfg = FidelityConfig {
301 full_threshold: 1.1,
302 ..FidelityConfig::default()
303 };
304 assert!(cfg.validate().is_err());
305 }
306
307 #[test]
308 fn default_lookahead_depth_is_three() {
309 assert_eq!(FidelityConfig::default().lookahead_depth, 3);
310 }
311
312 #[test]
313 fn lookahead_depth_zero_is_valid() {
314 let cfg = FidelityConfig {
315 lookahead_depth: 0,
316 ..FidelityConfig::default()
317 };
318 assert!(cfg.validate().is_ok());
319 }
320
321 #[test]
322 fn lookahead_depth_five_is_valid() {
323 let cfg = FidelityConfig {
324 lookahead_depth: 5,
325 ..FidelityConfig::default()
326 };
327 assert!(cfg.validate().is_ok());
328 }
329
330 #[test]
331 fn lookahead_depth_above_five_is_err() {
332 let cfg = FidelityConfig {
333 lookahead_depth: 6,
334 ..FidelityConfig::default()
335 };
336 let err = cfg.validate().unwrap_err();
337 assert!(
338 err.contains("lookahead_depth"),
339 "error should mention lookahead_depth: {err}"
340 );
341 }
342
343 #[test]
344 fn deserialize_lookahead_depth() {
345 let toml_str = "enabled = true\nlookahead_depth = 2";
346 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
347 assert_eq!(cfg.lookahead_depth, 2);
348 }
349
350 #[test]
351 fn deserialize_defaults_lookahead_depth_when_omitted() {
352 let cfg: FidelityConfig = toml::from_str("enabled = false").unwrap();
353 assert_eq!(cfg.lookahead_depth, 3);
354 }
355
356 #[test]
357 fn deserialize_new_perf_fields_defaults() {
358 let cfg: FidelityConfig = toml::from_str("enabled = false").unwrap();
359 assert_eq!(cfg.embed_concurrency, 32);
360 assert!(cfg.max_embed_input_tokens.is_none());
361 assert!(cfg.max_compress_input_tokens.is_none());
362 }
363
364 #[test]
365 fn deserialize_new_perf_fields_custom() {
366 let toml_str = r"
367 enabled = true
368 embed_concurrency = 8
369 max_embed_input_tokens = 512
370 max_compress_input_tokens = 1024
371 ";
372 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
373 assert_eq!(cfg.embed_concurrency, 8);
374 assert_eq!(cfg.max_embed_input_tokens, Some(512));
375 assert_eq!(cfg.max_compress_input_tokens, Some(1024));
376 }
377
378 #[test]
379 fn default_timeout_fields_are_thirty() {
380 let cfg = FidelityConfig::default();
381 assert_eq!(cfg.embed_timeout_secs, 30);
382 assert_eq!(cfg.compress_timeout_secs, 30);
383 }
384
385 #[test]
386 fn deserialize_timeout_fields_custom() {
387 let toml_str = r"
388 enabled = true
389 embed_timeout_secs = 60
390 compress_timeout_secs = 120
391 ";
392 let cfg: FidelityConfig = toml::from_str(toml_str).unwrap();
393 assert_eq!(cfg.embed_timeout_secs, 60);
394 assert_eq!(cfg.compress_timeout_secs, 120);
395 }
396
397 #[test]
398 fn deserialize_timeout_fields_default_when_omitted() {
399 let cfg: FidelityConfig = toml::from_str("enabled = false").unwrap();
400 assert_eq!(cfg.embed_timeout_secs, 30);
401 assert_eq!(cfg.compress_timeout_secs, 30);
402 }
403
404 #[test]
405 fn validate_embed_timeout_zero_is_err() {
406 let cfg = FidelityConfig {
407 embed_timeout_secs: 0,
408 ..FidelityConfig::default()
409 };
410 let err = cfg.validate().unwrap_err();
411 assert!(
412 err.contains("embed_timeout_secs"),
413 "error should mention embed_timeout_secs: {err}"
414 );
415 }
416
417 #[test]
418 fn validate_compress_timeout_zero_is_err() {
419 let cfg = FidelityConfig {
420 compress_timeout_secs: 0,
421 ..FidelityConfig::default()
422 };
423 let err = cfg.validate().unwrap_err();
424 assert!(
425 err.contains("compress_timeout_secs"),
426 "error should mention compress_timeout_secs: {err}"
427 );
428 }
429
430 #[test]
431 fn validate_timeout_one_is_ok() {
432 let cfg = FidelityConfig {
433 embed_timeout_secs: 1,
434 compress_timeout_secs: 1,
435 ..FidelityConfig::default()
436 };
437 assert!(cfg.validate().is_ok());
438 }
439}