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