1use serde::{Deserialize, Serialize};
5
6use crate::defaults::{default_sqlite_path_field, default_true};
7use crate::providers::ProviderName;
8
9fn default_sqlite_pool_size() -> u32 {
10 5
11}
12
13fn default_max_history() -> usize {
14 100
15}
16
17fn default_title_max_chars() -> usize {
18 60
19}
20
21fn default_document_collection() -> String {
22 "zeph_documents".into()
23}
24
25fn default_document_chunk_size() -> usize {
26 1000
27}
28
29fn default_document_chunk_overlap() -> usize {
30 100
31}
32
33fn default_document_top_k() -> usize {
34 3
35}
36
37fn default_autosave_min_length() -> usize {
38 20
39}
40
41fn default_tool_call_cutoff() -> usize {
42 6
43}
44
45fn default_token_safety_margin() -> f32 {
46 1.0
47}
48
49fn default_redact_credentials() -> bool {
50 true
51}
52
53fn default_qdrant_url() -> String {
54 "http://localhost:6334".into()
55}
56
57fn default_summarization_threshold() -> usize {
58 50
59}
60
61fn default_context_budget_tokens() -> usize {
62 0
63}
64
65fn default_soft_compaction_threshold() -> f32 {
66 0.60
67}
68
69fn default_hard_compaction_threshold() -> f32 {
70 0.90
71}
72
73fn default_compaction_preserve_tail() -> usize {
74 6
75}
76
77fn default_compaction_cooldown_turns() -> u8 {
78 2
79}
80
81fn default_auto_budget() -> bool {
82 true
83}
84
85fn default_prune_protect_tokens() -> usize {
86 40_000
87}
88
89fn default_cross_session_score_threshold() -> f32 {
90 0.35
91}
92
93fn default_temporal_decay_half_life_days() -> u32 {
94 30
95}
96
97fn default_mmr_lambda() -> f32 {
98 0.7
99}
100
101fn default_semantic_enabled() -> bool {
102 true
103}
104
105fn default_recall_limit() -> usize {
106 5
107}
108
109fn default_vector_weight() -> f64 {
110 0.7
111}
112
113fn default_keyword_weight() -> f64 {
114 0.3
115}
116
117fn default_graph_max_entities_per_message() -> usize {
118 10
119}
120
121fn default_graph_max_edges_per_message() -> usize {
122 15
123}
124
125fn default_graph_community_refresh_interval() -> usize {
126 100
127}
128
129fn default_graph_community_summary_max_prompt_bytes() -> usize {
130 8192
131}
132
133fn default_graph_community_summary_concurrency() -> usize {
134 4
135}
136
137fn default_lpa_edge_chunk_size() -> usize {
138 10_000
139}
140
141fn default_graph_entity_similarity_threshold() -> f32 {
142 0.85
143}
144
145fn default_graph_entity_ambiguous_threshold() -> f32 {
146 0.70
147}
148
149fn default_graph_extraction_timeout_secs() -> u64 {
150 15
151}
152
153fn default_graph_max_hops() -> u32 {
154 2
155}
156
157fn default_graph_recall_limit() -> usize {
158 10
159}
160
161fn default_graph_expired_edge_retention_days() -> u32 {
162 90
163}
164
165fn default_graph_temporal_decay_rate() -> f64 {
166 0.0
167}
168
169fn default_graph_edge_history_limit() -> usize {
170 100
171}
172
173fn default_spreading_activation_decay_lambda() -> f32 {
174 0.85
175}
176
177fn default_spreading_activation_max_hops() -> u32 {
178 3
179}
180
181fn default_spreading_activation_activation_threshold() -> f32 {
182 0.1
183}
184
185fn default_spreading_activation_inhibition_threshold() -> f32 {
186 0.8
187}
188
189fn default_spreading_activation_max_activated_nodes() -> usize {
190 50
191}
192
193fn default_spreading_activation_recall_timeout_ms() -> u64 {
194 1000
195}
196
197fn default_note_linking_similarity_threshold() -> f32 {
198 0.85
199}
200
201fn default_note_linking_top_k() -> usize {
202 10
203}
204
205fn default_note_linking_timeout_secs() -> u64 {
206 5
207}
208
209fn default_shutdown_summary() -> bool {
210 true
211}
212
213fn default_shutdown_summary_min_messages() -> usize {
214 4
215}
216
217fn default_shutdown_summary_max_messages() -> usize {
218 20
219}
220
221fn default_shutdown_summary_timeout_secs() -> u64 {
222 10
223}
224
225fn validate_tier_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
226where
227 D: serde::Deserializer<'de>,
228{
229 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
230 if value.is_nan() || value.is_infinite() {
231 return Err(serde::de::Error::custom(
232 "similarity_threshold must be a finite number",
233 ));
234 }
235 if !(0.5..=1.0).contains(&value) {
236 return Err(serde::de::Error::custom(
237 "similarity_threshold must be in [0.5, 1.0]",
238 ));
239 }
240 Ok(value)
241}
242
243fn validate_tier_promotion_min_sessions<'de, D>(deserializer: D) -> Result<u32, D::Error>
244where
245 D: serde::Deserializer<'de>,
246{
247 let value = <u32 as serde::Deserialize>::deserialize(deserializer)?;
248 if value < 2 {
249 return Err(serde::de::Error::custom(
250 "promotion_min_sessions must be >= 2",
251 ));
252 }
253 Ok(value)
254}
255
256fn validate_tier_sweep_batch_size<'de, D>(deserializer: D) -> Result<usize, D::Error>
257where
258 D: serde::Deserializer<'de>,
259{
260 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
261 if value == 0 {
262 return Err(serde::de::Error::custom("sweep_batch_size must be >= 1"));
263 }
264 Ok(value)
265}
266
267fn default_tier_promotion_min_sessions() -> u32 {
268 3
269}
270
271fn default_tier_similarity_threshold() -> f32 {
272 0.92
273}
274
275fn default_tier_sweep_interval_secs() -> u64 {
276 3600
277}
278
279fn default_tier_sweep_batch_size() -> usize {
280 100
281}
282
283fn default_scene_similarity_threshold() -> f32 {
284 0.80
285}
286
287fn default_scene_batch_size() -> usize {
288 50
289}
290
291fn validate_scene_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
292where
293 D: serde::Deserializer<'de>,
294{
295 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
296 if value.is_nan() || value.is_infinite() {
297 return Err(serde::de::Error::custom(
298 "scene_similarity_threshold must be a finite number",
299 ));
300 }
301 if !(0.5..=1.0).contains(&value) {
302 return Err(serde::de::Error::custom(
303 "scene_similarity_threshold must be in [0.5, 1.0]",
304 ));
305 }
306 Ok(value)
307}
308
309fn validate_scene_batch_size<'de, D>(deserializer: D) -> Result<usize, D::Error>
310where
311 D: serde::Deserializer<'de>,
312{
313 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
314 if value == 0 {
315 return Err(serde::de::Error::custom("scene_batch_size must be >= 1"));
316 }
317 Ok(value)
318}
319
320#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
334#[serde(default)]
335pub struct TierConfig {
336 pub enabled: bool,
339 #[serde(deserialize_with = "validate_tier_promotion_min_sessions")]
342 pub promotion_min_sessions: u32,
343 #[serde(deserialize_with = "validate_tier_similarity_threshold")]
346 pub similarity_threshold: f32,
347 pub sweep_interval_secs: u64,
349 #[serde(deserialize_with = "validate_tier_sweep_batch_size")]
351 pub sweep_batch_size: usize,
352 pub scene_enabled: bool,
354 #[serde(deserialize_with = "validate_scene_similarity_threshold")]
356 pub scene_similarity_threshold: f32,
357 #[serde(deserialize_with = "validate_scene_batch_size")]
359 pub scene_batch_size: usize,
360 pub scene_provider: ProviderName,
363 pub scene_sweep_interval_secs: u64,
365}
366
367fn default_scene_sweep_interval_secs() -> u64 {
368 7200
369}
370
371impl Default for TierConfig {
372 fn default() -> Self {
373 Self {
374 enabled: false,
375 promotion_min_sessions: default_tier_promotion_min_sessions(),
376 similarity_threshold: default_tier_similarity_threshold(),
377 sweep_interval_secs: default_tier_sweep_interval_secs(),
378 sweep_batch_size: default_tier_sweep_batch_size(),
379 scene_enabled: false,
380 scene_similarity_threshold: default_scene_similarity_threshold(),
381 scene_batch_size: default_scene_batch_size(),
382 scene_provider: ProviderName::default(),
383 scene_sweep_interval_secs: default_scene_sweep_interval_secs(),
384 }
385 }
386}
387
388fn validate_temporal_decay_rate<'de, D>(deserializer: D) -> Result<f64, D::Error>
389where
390 D: serde::Deserializer<'de>,
391{
392 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
393 if value.is_nan() || value.is_infinite() {
394 return Err(serde::de::Error::custom(
395 "temporal_decay_rate must be a finite number",
396 ));
397 }
398 if !(0.0..=10.0).contains(&value) {
399 return Err(serde::de::Error::custom(
400 "temporal_decay_rate must be in [0.0, 10.0]",
401 ));
402 }
403 Ok(value)
404}
405
406fn validate_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
407where
408 D: serde::Deserializer<'de>,
409{
410 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
411 if value.is_nan() || value.is_infinite() {
412 return Err(serde::de::Error::custom(
413 "similarity_threshold must be a finite number",
414 ));
415 }
416 if !(0.0..=1.0).contains(&value) {
417 return Err(serde::de::Error::custom(
418 "similarity_threshold must be in [0.0, 1.0]",
419 ));
420 }
421 Ok(value)
422}
423
424fn validate_importance_weight<'de, D>(deserializer: D) -> Result<f64, D::Error>
425where
426 D: serde::Deserializer<'de>,
427{
428 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
429 if value.is_nan() || value.is_infinite() {
430 return Err(serde::de::Error::custom(
431 "importance_weight must be a finite number",
432 ));
433 }
434 if value < 0.0 {
435 return Err(serde::de::Error::custom(
436 "importance_weight must be non-negative",
437 ));
438 }
439 if value > 1.0 {
440 return Err(serde::de::Error::custom("importance_weight must be <= 1.0"));
441 }
442 Ok(value)
443}
444
445fn default_importance_weight() -> f64 {
446 0.15
447}
448
449#[derive(Debug, Clone, Deserialize, Serialize)]
463#[serde(default)]
464pub struct SpreadingActivationConfig {
465 pub enabled: bool,
467 #[serde(deserialize_with = "validate_decay_lambda")]
469 pub decay_lambda: f32,
470 #[serde(deserialize_with = "validate_max_hops")]
472 pub max_hops: u32,
473 pub activation_threshold: f32,
475 pub inhibition_threshold: f32,
477 pub max_activated_nodes: usize,
479 #[serde(default = "default_seed_structural_weight")]
481 pub seed_structural_weight: f32,
482 #[serde(default = "default_seed_community_cap")]
484 pub seed_community_cap: usize,
485 #[serde(default = "default_spreading_activation_recall_timeout_ms")]
489 pub recall_timeout_ms: u64,
490}
491
492fn validate_decay_lambda<'de, D>(deserializer: D) -> Result<f32, D::Error>
493where
494 D: serde::Deserializer<'de>,
495{
496 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
497 if value.is_nan() || value.is_infinite() {
498 return Err(serde::de::Error::custom(
499 "decay_lambda must be a finite number",
500 ));
501 }
502 if !(value > 0.0 && value <= 1.0) {
503 return Err(serde::de::Error::custom(
504 "decay_lambda must be in (0.0, 1.0]",
505 ));
506 }
507 Ok(value)
508}
509
510fn validate_max_hops<'de, D>(deserializer: D) -> Result<u32, D::Error>
511where
512 D: serde::Deserializer<'de>,
513{
514 let value = <u32 as serde::Deserialize>::deserialize(deserializer)?;
515 if value == 0 {
516 return Err(serde::de::Error::custom("max_hops must be >= 1"));
517 }
518 Ok(value)
519}
520
521impl SpreadingActivationConfig {
522 pub fn validate(&self) -> Result<(), String> {
528 if self.activation_threshold >= self.inhibition_threshold {
529 return Err(format!(
530 "activation_threshold ({}) must be < inhibition_threshold ({})",
531 self.activation_threshold, self.inhibition_threshold
532 ));
533 }
534 Ok(())
535 }
536}
537
538fn default_seed_structural_weight() -> f32 {
539 0.4
540}
541
542fn default_seed_community_cap() -> usize {
543 3
544}
545
546impl Default for SpreadingActivationConfig {
547 fn default() -> Self {
548 Self {
549 enabled: false,
550 decay_lambda: default_spreading_activation_decay_lambda(),
551 max_hops: default_spreading_activation_max_hops(),
552 activation_threshold: default_spreading_activation_activation_threshold(),
553 inhibition_threshold: default_spreading_activation_inhibition_threshold(),
554 max_activated_nodes: default_spreading_activation_max_activated_nodes(),
555 seed_structural_weight: default_seed_structural_weight(),
556 seed_community_cap: default_seed_community_cap(),
557 recall_timeout_ms: default_spreading_activation_recall_timeout_ms(),
558 }
559 }
560}
561
562#[derive(Debug, Clone, Deserialize, Serialize)]
564#[serde(default)]
565pub struct BeliefRevisionConfig {
566 pub enabled: bool,
568 #[serde(deserialize_with = "validate_similarity_threshold")]
571 pub similarity_threshold: f32,
572}
573
574fn default_belief_revision_similarity_threshold() -> f32 {
575 0.85
576}
577
578impl Default for BeliefRevisionConfig {
579 fn default() -> Self {
580 Self {
581 enabled: false,
582 similarity_threshold: default_belief_revision_similarity_threshold(),
583 }
584 }
585}
586
587#[derive(Debug, Clone, Deserialize, Serialize)]
589#[serde(default)]
590pub struct RpeConfig {
591 pub enabled: bool,
593 #[serde(deserialize_with = "validate_similarity_threshold")]
596 pub threshold: f32,
597 pub max_skip_turns: u32,
599}
600
601fn default_rpe_threshold() -> f32 {
602 0.3
603}
604
605fn default_rpe_max_skip_turns() -> u32 {
606 5
607}
608
609impl Default for RpeConfig {
610 fn default() -> Self {
611 Self {
612 enabled: false,
613 threshold: default_rpe_threshold(),
614 max_skip_turns: default_rpe_max_skip_turns(),
615 }
616 }
617}
618
619#[derive(Debug, Clone, Deserialize, Serialize)]
625#[serde(default)]
626pub struct NoteLinkingConfig {
627 pub enabled: bool,
629 #[serde(deserialize_with = "validate_similarity_threshold")]
631 pub similarity_threshold: f32,
632 pub top_k: usize,
634 pub timeout_secs: u64,
636}
637
638impl Default for NoteLinkingConfig {
639 fn default() -> Self {
640 Self {
641 enabled: false,
642 similarity_threshold: default_note_linking_similarity_threshold(),
643 top_k: default_note_linking_top_k(),
644 timeout_secs: default_note_linking_timeout_secs(),
645 }
646 }
647}
648
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
651#[serde(rename_all = "lowercase")]
652pub enum VectorBackend {
653 Qdrant,
654 #[default]
655 Sqlite,
656}
657
658impl VectorBackend {
659 #[must_use]
660 pub fn as_str(&self) -> &'static str {
661 match self {
662 Self::Qdrant => "qdrant",
663 Self::Sqlite => "sqlite",
664 }
665 }
666}
667
668#[derive(Debug, Deserialize, Serialize)]
669#[allow(clippy::struct_excessive_bools)]
670pub struct MemoryConfig {
671 #[serde(default)]
672 pub compression_guidelines: zeph_memory::CompressionGuidelinesConfig,
673 #[serde(default = "default_sqlite_path_field")]
674 pub sqlite_path: String,
675 pub history_limit: u32,
676 #[serde(default = "default_qdrant_url")]
677 pub qdrant_url: String,
678 #[serde(default)]
679 pub semantic: SemanticConfig,
680 #[serde(default = "default_summarization_threshold")]
681 pub summarization_threshold: usize,
682 #[serde(default = "default_context_budget_tokens")]
683 pub context_budget_tokens: usize,
684 #[serde(default = "default_soft_compaction_threshold")]
685 pub soft_compaction_threshold: f32,
686 #[serde(
687 default = "default_hard_compaction_threshold",
688 alias = "compaction_threshold"
689 )]
690 pub hard_compaction_threshold: f32,
691 #[serde(default = "default_compaction_preserve_tail")]
692 pub compaction_preserve_tail: usize,
693 #[serde(default = "default_compaction_cooldown_turns")]
694 pub compaction_cooldown_turns: u8,
695 #[serde(default = "default_auto_budget")]
696 pub auto_budget: bool,
697 #[serde(default = "default_prune_protect_tokens")]
698 pub prune_protect_tokens: usize,
699 #[serde(default = "default_cross_session_score_threshold")]
700 pub cross_session_score_threshold: f32,
701 #[serde(default)]
702 pub vector_backend: VectorBackend,
703 #[serde(default = "default_token_safety_margin")]
704 pub token_safety_margin: f32,
705 #[serde(default = "default_redact_credentials")]
706 pub redact_credentials: bool,
707 #[serde(default = "default_true")]
708 pub autosave_assistant: bool,
709 #[serde(default = "default_autosave_min_length")]
710 pub autosave_min_length: usize,
711 #[serde(default = "default_tool_call_cutoff")]
712 pub tool_call_cutoff: usize,
713 #[serde(default = "default_sqlite_pool_size")]
714 pub sqlite_pool_size: u32,
715 #[serde(default)]
716 pub sessions: SessionsConfig,
717 #[serde(default)]
718 pub documents: DocumentConfig,
719 #[serde(default)]
720 pub eviction: zeph_memory::EvictionConfig,
721 #[serde(default)]
722 pub compression: CompressionConfig,
723 #[serde(default)]
724 pub sidequest: SidequestConfig,
725 #[serde(default)]
726 pub graph: GraphConfig,
727 #[serde(default = "default_shutdown_summary")]
731 pub shutdown_summary: bool,
732 #[serde(default = "default_shutdown_summary_min_messages")]
735 pub shutdown_summary_min_messages: usize,
736 #[serde(default = "default_shutdown_summary_max_messages")]
740 pub shutdown_summary_max_messages: usize,
741 #[serde(default = "default_shutdown_summary_timeout_secs")]
745 pub shutdown_summary_timeout_secs: u64,
746 #[serde(default)]
752 pub structured_summaries: bool,
753 #[serde(default)]
758 pub tiers: TierConfig,
759 #[serde(default)]
764 pub admission: AdmissionConfig,
765 #[serde(default)]
767 pub digest: DigestConfig,
768 #[serde(default)]
770 pub context_strategy: ContextStrategy,
771 #[serde(default = "default_crossover_turn_threshold")]
773 pub crossover_turn_threshold: u32,
774 #[serde(default)]
779 pub consolidation: ConsolidationConfig,
780 #[serde(default)]
785 pub forgetting: ForgettingConfig,
786 #[serde(default)]
793 pub database_url: Option<String>,
794 #[serde(default)]
799 pub store_routing: StoreRoutingConfig,
800 #[serde(default)]
805 pub persona: PersonaConfig,
806}
807
808fn default_crossover_turn_threshold() -> u32 {
809 20
810}
811
812#[derive(Debug, Clone, Deserialize, Serialize)]
814#[serde(default)]
815pub struct DigestConfig {
816 pub enabled: bool,
818 pub provider: String,
821 pub max_tokens: usize,
823 pub max_input_messages: usize,
825}
826
827impl Default for DigestConfig {
828 fn default() -> Self {
829 Self {
830 enabled: false,
831 provider: String::new(),
832 max_tokens: 500,
833 max_input_messages: 50,
834 }
835 }
836}
837
838#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
840#[serde(rename_all = "snake_case")]
841pub enum ContextStrategy {
842 #[default]
845 FullHistory,
846 MemoryFirst,
849 Adaptive,
852}
853
854#[derive(Debug, Clone, Deserialize, Serialize)]
855#[serde(default)]
856pub struct SessionsConfig {
857 #[serde(default = "default_max_history")]
859 pub max_history: usize,
860 #[serde(default = "default_title_max_chars")]
862 pub title_max_chars: usize,
863}
864
865impl Default for SessionsConfig {
866 fn default() -> Self {
867 Self {
868 max_history: default_max_history(),
869 title_max_chars: default_title_max_chars(),
870 }
871 }
872}
873
874#[derive(Debug, Clone, Deserialize, Serialize)]
876pub struct DocumentConfig {
877 #[serde(default = "default_document_collection")]
878 pub collection: String,
879 #[serde(default = "default_document_chunk_size")]
880 pub chunk_size: usize,
881 #[serde(default = "default_document_chunk_overlap")]
882 pub chunk_overlap: usize,
883 #[serde(default = "default_document_top_k")]
885 pub top_k: usize,
886 #[serde(default)]
888 pub rag_enabled: bool,
889}
890
891impl Default for DocumentConfig {
892 fn default() -> Self {
893 Self {
894 collection: default_document_collection(),
895 chunk_size: default_document_chunk_size(),
896 chunk_overlap: default_document_chunk_overlap(),
897 top_k: default_document_top_k(),
898 rag_enabled: false,
899 }
900 }
901}
902
903#[derive(Debug, Deserialize, Serialize)]
904#[allow(clippy::struct_excessive_bools)]
905pub struct SemanticConfig {
906 #[serde(default = "default_semantic_enabled")]
907 pub enabled: bool,
908 #[serde(default = "default_recall_limit")]
909 pub recall_limit: usize,
910 #[serde(default = "default_vector_weight")]
911 pub vector_weight: f64,
912 #[serde(default = "default_keyword_weight")]
913 pub keyword_weight: f64,
914 #[serde(default = "default_true")]
915 pub temporal_decay_enabled: bool,
916 #[serde(default = "default_temporal_decay_half_life_days")]
917 pub temporal_decay_half_life_days: u32,
918 #[serde(default = "default_true")]
919 pub mmr_enabled: bool,
920 #[serde(default = "default_mmr_lambda")]
921 pub mmr_lambda: f32,
922 #[serde(default = "default_true")]
923 pub importance_enabled: bool,
924 #[serde(
925 default = "default_importance_weight",
926 deserialize_with = "validate_importance_weight"
927 )]
928 pub importance_weight: f64,
929 #[serde(default)]
934 pub embed_provider: Option<String>,
935}
936
937impl Default for SemanticConfig {
938 fn default() -> Self {
939 Self {
940 enabled: default_semantic_enabled(),
941 recall_limit: default_recall_limit(),
942 vector_weight: default_vector_weight(),
943 keyword_weight: default_keyword_weight(),
944 temporal_decay_enabled: true,
945 temporal_decay_half_life_days: default_temporal_decay_half_life_days(),
946 mmr_enabled: true,
947 mmr_lambda: default_mmr_lambda(),
948 importance_enabled: true,
949 importance_weight: default_importance_weight(),
950 embed_provider: None,
951 }
952 }
953}
954
955#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
957#[serde(tag = "strategy", rename_all = "snake_case")]
958pub enum CompressionStrategy {
959 #[default]
961 Reactive,
962 Proactive {
964 threshold_tokens: usize,
966 max_summary_tokens: usize,
968 },
969 Autonomous,
972 Focus,
977}
978
979#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
984#[serde(rename_all = "snake_case")]
985pub enum PruningStrategy {
986 #[default]
988 Reactive,
989 TaskAware,
992 Mig,
995 Subgoal,
999 SubgoalMig,
1002}
1003
1004impl PruningStrategy {
1005 #[must_use]
1007 pub fn is_subgoal(self) -> bool {
1008 matches!(self, Self::Subgoal | Self::SubgoalMig)
1009 }
1010}
1011
1012impl<'de> serde::Deserialize<'de> for PruningStrategy {
1015 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1016 let s = String::deserialize(deserializer)?;
1017 s.parse().map_err(serde::de::Error::custom)
1018 }
1019}
1020
1021impl std::str::FromStr for PruningStrategy {
1022 type Err = String;
1023
1024 fn from_str(s: &str) -> Result<Self, Self::Err> {
1025 match s {
1026 "reactive" => Ok(Self::Reactive),
1027 "task_aware" | "task-aware" => Ok(Self::TaskAware),
1028 "mig" => Ok(Self::Mig),
1029 "task_aware_mig" | "task-aware-mig" => {
1032 tracing::warn!(
1033 "pruning strategy `task_aware_mig` has been removed; \
1034 falling back to `reactive`. Use `task_aware` or `mig` instead."
1035 );
1036 Ok(Self::Reactive)
1037 }
1038 "subgoal" => Ok(Self::Subgoal),
1039 "subgoal_mig" | "subgoal-mig" => Ok(Self::SubgoalMig),
1040 other => Err(format!(
1041 "unknown pruning strategy `{other}`, expected \
1042 reactive|task_aware|mig|subgoal|subgoal_mig"
1043 )),
1044 }
1045 }
1046}
1047
1048fn default_high_density_budget() -> f32 {
1049 0.7
1050}
1051
1052fn default_low_density_budget() -> f32 {
1053 0.3
1054}
1055
1056#[derive(Debug, Clone, Deserialize, Serialize)]
1063#[serde(default)]
1064pub struct CompressionPredictorConfig {
1065 pub enabled: bool,
1067 pub min_samples: u64,
1069 pub candidate_ratios: Vec<f32>,
1072 pub retrain_interval: u64,
1074 pub max_training_samples: usize,
1076}
1077
1078impl Default for CompressionPredictorConfig {
1079 fn default() -> Self {
1080 Self {
1081 enabled: false,
1082 min_samples: 10,
1083 candidate_ratios: vec![0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
1084 retrain_interval: 5,
1085 max_training_samples: 200,
1086 }
1087 }
1088}
1089
1090#[derive(Debug, Clone, Deserialize, Serialize)]
1096#[serde(default)]
1097pub struct ForgettingConfig {
1098 pub enabled: bool,
1100 pub decay_rate: f32,
1102 pub forgetting_floor: f32,
1104 pub sweep_interval_secs: u64,
1106 pub sweep_batch_size: usize,
1108 pub replay_window_hours: u32,
1110 pub replay_min_access_count: u32,
1112 pub protect_recent_hours: u32,
1114 pub protect_min_access_count: u32,
1116}
1117
1118impl Default for ForgettingConfig {
1119 fn default() -> Self {
1120 Self {
1121 enabled: false,
1122 decay_rate: 0.1,
1123 forgetting_floor: 0.05,
1124 sweep_interval_secs: 7200,
1125 sweep_batch_size: 500,
1126 replay_window_hours: 24,
1127 replay_min_access_count: 3,
1128 protect_recent_hours: 24,
1129 protect_min_access_count: 3,
1130 }
1131 }
1132}
1133
1134#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1136#[serde(default)]
1137pub struct CompressionConfig {
1138 #[serde(flatten)]
1140 pub strategy: CompressionStrategy,
1141 pub pruning_strategy: PruningStrategy,
1143 pub model: String,
1148 pub compress_provider: ProviderName,
1151 #[serde(default)]
1153 pub probe: zeph_memory::CompactionProbeConfig,
1154 #[serde(default)]
1162 pub archive_tool_outputs: bool,
1163 pub focus_scorer_provider: ProviderName,
1166 #[serde(default = "default_high_density_budget")]
1169 pub high_density_budget: f32,
1170 #[serde(default = "default_low_density_budget")]
1173 pub low_density_budget: f32,
1174 #[serde(default)]
1176 pub predictor: CompressionPredictorConfig,
1177}
1178
1179fn default_sidequest_interval_turns() -> u32 {
1180 4
1181}
1182
1183fn default_sidequest_max_eviction_ratio() -> f32 {
1184 0.5
1185}
1186
1187fn default_sidequest_max_cursors() -> usize {
1188 30
1189}
1190
1191fn default_sidequest_min_cursor_tokens() -> usize {
1192 100
1193}
1194
1195#[derive(Debug, Clone, Deserialize, Serialize)]
1197#[serde(default)]
1198pub struct SidequestConfig {
1199 pub enabled: bool,
1201 #[serde(default = "default_sidequest_interval_turns")]
1203 pub interval_turns: u32,
1204 #[serde(default = "default_sidequest_max_eviction_ratio")]
1206 pub max_eviction_ratio: f32,
1207 #[serde(default = "default_sidequest_max_cursors")]
1209 pub max_cursors: usize,
1210 #[serde(default = "default_sidequest_min_cursor_tokens")]
1213 pub min_cursor_tokens: usize,
1214}
1215
1216impl Default for SidequestConfig {
1217 fn default() -> Self {
1218 Self {
1219 enabled: false,
1220 interval_turns: default_sidequest_interval_turns(),
1221 max_eviction_ratio: default_sidequest_max_eviction_ratio(),
1222 max_cursors: default_sidequest_max_cursors(),
1223 min_cursor_tokens: default_sidequest_min_cursor_tokens(),
1224 }
1225 }
1226}
1227
1228#[derive(Debug, Clone, Deserialize, Serialize)]
1237#[serde(default)]
1238pub struct GraphConfig {
1239 pub enabled: bool,
1240 pub extract_model: String,
1241 #[serde(default = "default_graph_max_entities_per_message")]
1242 pub max_entities_per_message: usize,
1243 #[serde(default = "default_graph_max_edges_per_message")]
1244 pub max_edges_per_message: usize,
1245 #[serde(default = "default_graph_community_refresh_interval")]
1246 pub community_refresh_interval: usize,
1247 #[serde(default = "default_graph_entity_similarity_threshold")]
1248 pub entity_similarity_threshold: f32,
1249 #[serde(default = "default_graph_extraction_timeout_secs")]
1250 pub extraction_timeout_secs: u64,
1251 #[serde(default)]
1252 pub use_embedding_resolution: bool,
1253 #[serde(default = "default_graph_entity_ambiguous_threshold")]
1254 pub entity_ambiguous_threshold: f32,
1255 #[serde(default = "default_graph_max_hops")]
1256 pub max_hops: u32,
1257 #[serde(default = "default_graph_recall_limit")]
1258 pub recall_limit: usize,
1259 #[serde(default = "default_graph_expired_edge_retention_days")]
1261 pub expired_edge_retention_days: u32,
1262 #[serde(default)]
1264 pub max_entities: usize,
1265 #[serde(default = "default_graph_community_summary_max_prompt_bytes")]
1267 pub community_summary_max_prompt_bytes: usize,
1268 #[serde(default = "default_graph_community_summary_concurrency")]
1270 pub community_summary_concurrency: usize,
1271 #[serde(default = "default_lpa_edge_chunk_size")]
1274 pub lpa_edge_chunk_size: usize,
1275 #[serde(
1281 default = "default_graph_temporal_decay_rate",
1282 deserialize_with = "validate_temporal_decay_rate"
1283 )]
1284 pub temporal_decay_rate: f64,
1285 #[serde(default = "default_graph_edge_history_limit")]
1291 pub edge_history_limit: usize,
1292 #[serde(default)]
1298 pub note_linking: NoteLinkingConfig,
1299 #[serde(default)]
1304 pub spreading_activation: SpreadingActivationConfig,
1305 #[serde(
1308 default = "default_link_weight_decay_lambda",
1309 deserialize_with = "validate_link_weight_decay_lambda"
1310 )]
1311 pub link_weight_decay_lambda: f64,
1312 #[serde(default = "default_link_weight_decay_interval_secs")]
1314 pub link_weight_decay_interval_secs: u64,
1315 #[serde(default)]
1321 pub belief_revision: BeliefRevisionConfig,
1322 #[serde(default)]
1327 pub rpe: RpeConfig,
1328 #[serde(default = "default_graph_pool_size")]
1334 pub pool_size: u32,
1335}
1336
1337fn default_graph_pool_size() -> u32 {
1338 3
1339}
1340
1341impl Default for GraphConfig {
1342 fn default() -> Self {
1343 Self {
1344 enabled: false,
1345 extract_model: String::new(),
1346 max_entities_per_message: default_graph_max_entities_per_message(),
1347 max_edges_per_message: default_graph_max_edges_per_message(),
1348 community_refresh_interval: default_graph_community_refresh_interval(),
1349 entity_similarity_threshold: default_graph_entity_similarity_threshold(),
1350 extraction_timeout_secs: default_graph_extraction_timeout_secs(),
1351 use_embedding_resolution: false,
1352 entity_ambiguous_threshold: default_graph_entity_ambiguous_threshold(),
1353 max_hops: default_graph_max_hops(),
1354 recall_limit: default_graph_recall_limit(),
1355 expired_edge_retention_days: default_graph_expired_edge_retention_days(),
1356 max_entities: 0,
1357 community_summary_max_prompt_bytes: default_graph_community_summary_max_prompt_bytes(),
1358 community_summary_concurrency: default_graph_community_summary_concurrency(),
1359 lpa_edge_chunk_size: default_lpa_edge_chunk_size(),
1360 temporal_decay_rate: default_graph_temporal_decay_rate(),
1361 edge_history_limit: default_graph_edge_history_limit(),
1362 note_linking: NoteLinkingConfig::default(),
1363 spreading_activation: SpreadingActivationConfig::default(),
1364 link_weight_decay_lambda: default_link_weight_decay_lambda(),
1365 link_weight_decay_interval_secs: default_link_weight_decay_interval_secs(),
1366 belief_revision: BeliefRevisionConfig::default(),
1367 rpe: RpeConfig::default(),
1368 pool_size: default_graph_pool_size(),
1369 }
1370 }
1371}
1372
1373fn default_consolidation_confidence_threshold() -> f32 {
1374 0.7
1375}
1376
1377fn default_consolidation_sweep_interval_secs() -> u64 {
1378 3600
1379}
1380
1381fn default_consolidation_sweep_batch_size() -> usize {
1382 50
1383}
1384
1385fn default_consolidation_similarity_threshold() -> f32 {
1386 0.85
1387}
1388
1389#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1395#[serde(default)]
1396pub struct ConsolidationConfig {
1397 pub enabled: bool,
1399 #[serde(default)]
1402 pub consolidation_provider: ProviderName,
1403 #[serde(default = "default_consolidation_confidence_threshold")]
1405 pub confidence_threshold: f32,
1406 #[serde(default = "default_consolidation_sweep_interval_secs")]
1408 pub sweep_interval_secs: u64,
1409 #[serde(default = "default_consolidation_sweep_batch_size")]
1411 pub sweep_batch_size: usize,
1412 #[serde(default = "default_consolidation_similarity_threshold")]
1415 pub similarity_threshold: f32,
1416}
1417
1418impl Default for ConsolidationConfig {
1419 fn default() -> Self {
1420 Self {
1421 enabled: false,
1422 consolidation_provider: ProviderName::default(),
1423 confidence_threshold: default_consolidation_confidence_threshold(),
1424 sweep_interval_secs: default_consolidation_sweep_interval_secs(),
1425 sweep_batch_size: default_consolidation_sweep_batch_size(),
1426 similarity_threshold: default_consolidation_similarity_threshold(),
1427 }
1428 }
1429}
1430
1431fn default_link_weight_decay_lambda() -> f64 {
1432 0.95
1433}
1434
1435fn default_link_weight_decay_interval_secs() -> u64 {
1436 86400
1437}
1438
1439fn validate_link_weight_decay_lambda<'de, D>(deserializer: D) -> Result<f64, D::Error>
1440where
1441 D: serde::Deserializer<'de>,
1442{
1443 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
1444 if value.is_nan() || value.is_infinite() {
1445 return Err(serde::de::Error::custom(
1446 "link_weight_decay_lambda must be a finite number",
1447 ));
1448 }
1449 if !(value > 0.0 && value <= 1.0) {
1450 return Err(serde::de::Error::custom(
1451 "link_weight_decay_lambda must be in (0.0, 1.0]",
1452 ));
1453 }
1454 Ok(value)
1455}
1456
1457fn validate_admission_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
1458where
1459 D: serde::Deserializer<'de>,
1460{
1461 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1462 if value.is_nan() || value.is_infinite() {
1463 return Err(serde::de::Error::custom(
1464 "threshold must be a finite number",
1465 ));
1466 }
1467 if !(0.0..=1.0).contains(&value) {
1468 return Err(serde::de::Error::custom("threshold must be in [0.0, 1.0]"));
1469 }
1470 Ok(value)
1471}
1472
1473fn validate_admission_fast_path_margin<'de, D>(deserializer: D) -> Result<f32, D::Error>
1474where
1475 D: serde::Deserializer<'de>,
1476{
1477 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1478 if value.is_nan() || value.is_infinite() {
1479 return Err(serde::de::Error::custom(
1480 "fast_path_margin must be a finite number",
1481 ));
1482 }
1483 if !(0.0..=1.0).contains(&value) {
1484 return Err(serde::de::Error::custom(
1485 "fast_path_margin must be in [0.0, 1.0]",
1486 ));
1487 }
1488 Ok(value)
1489}
1490
1491fn default_admission_threshold() -> f32 {
1492 0.40
1493}
1494
1495fn default_admission_fast_path_margin() -> f32 {
1496 0.15
1497}
1498
1499fn default_rl_min_samples() -> u32 {
1500 500
1501}
1502
1503fn default_rl_retrain_interval_secs() -> u64 {
1504 3600
1505}
1506
1507#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1512#[serde(rename_all = "snake_case")]
1513pub enum AdmissionStrategy {
1514 #[default]
1516 Heuristic,
1517 Rl,
1520}
1521
1522fn validate_admission_weight<'de, D>(deserializer: D) -> Result<f32, D::Error>
1523where
1524 D: serde::Deserializer<'de>,
1525{
1526 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1527 if value < 0.0 {
1528 return Err(serde::de::Error::custom(
1529 "admission weight must be non-negative (>= 0.0)",
1530 ));
1531 }
1532 Ok(value)
1533}
1534
1535#[derive(Debug, Clone, Deserialize, Serialize)]
1540#[serde(default)]
1541pub struct AdmissionWeights {
1542 #[serde(deserialize_with = "validate_admission_weight")]
1544 pub future_utility: f32,
1545 #[serde(deserialize_with = "validate_admission_weight")]
1547 pub factual_confidence: f32,
1548 #[serde(deserialize_with = "validate_admission_weight")]
1550 pub semantic_novelty: f32,
1551 #[serde(deserialize_with = "validate_admission_weight")]
1553 pub temporal_recency: f32,
1554 #[serde(deserialize_with = "validate_admission_weight")]
1556 pub content_type_prior: f32,
1557 #[serde(deserialize_with = "validate_admission_weight")]
1561 pub goal_utility: f32,
1562}
1563
1564impl Default for AdmissionWeights {
1565 fn default() -> Self {
1566 Self {
1567 future_utility: 0.30,
1568 factual_confidence: 0.15,
1569 semantic_novelty: 0.30,
1570 temporal_recency: 0.10,
1571 content_type_prior: 0.15,
1572 goal_utility: 0.0,
1573 }
1574 }
1575}
1576
1577impl AdmissionWeights {
1578 #[must_use]
1582 pub fn normalized(&self) -> Self {
1583 let sum = self.future_utility
1584 + self.factual_confidence
1585 + self.semantic_novelty
1586 + self.temporal_recency
1587 + self.content_type_prior
1588 + self.goal_utility;
1589 if sum <= f32::EPSILON {
1590 return Self::default();
1591 }
1592 Self {
1593 future_utility: self.future_utility / sum,
1594 factual_confidence: self.factual_confidence / sum,
1595 semantic_novelty: self.semantic_novelty / sum,
1596 temporal_recency: self.temporal_recency / sum,
1597 content_type_prior: self.content_type_prior / sum,
1598 goal_utility: self.goal_utility / sum,
1599 }
1600 }
1601}
1602
1603#[derive(Debug, Clone, Deserialize, Serialize)]
1608#[serde(default)]
1609pub struct AdmissionConfig {
1610 pub enabled: bool,
1612 #[serde(deserialize_with = "validate_admission_threshold")]
1615 pub threshold: f32,
1616 #[serde(deserialize_with = "validate_admission_fast_path_margin")]
1619 pub fast_path_margin: f32,
1620 pub admission_provider: ProviderName,
1623 pub weights: AdmissionWeights,
1625 #[serde(default)]
1627 pub admission_strategy: AdmissionStrategy,
1628 #[serde(default = "default_rl_min_samples")]
1631 pub rl_min_samples: u32,
1632 #[serde(default = "default_rl_retrain_interval_secs")]
1634 pub rl_retrain_interval_secs: u64,
1635 #[serde(default)]
1639 pub goal_conditioned_write: bool,
1640 #[serde(default)]
1644 pub goal_utility_provider: ProviderName,
1645 #[serde(default = "default_goal_utility_threshold")]
1648 pub goal_utility_threshold: f32,
1649 #[serde(default = "default_goal_utility_weight")]
1652 pub goal_utility_weight: f32,
1653}
1654
1655fn default_goal_utility_threshold() -> f32 {
1656 0.4
1657}
1658
1659fn default_goal_utility_weight() -> f32 {
1660 0.25
1661}
1662
1663impl Default for AdmissionConfig {
1664 fn default() -> Self {
1665 Self {
1666 enabled: false,
1667 threshold: default_admission_threshold(),
1668 fast_path_margin: default_admission_fast_path_margin(),
1669 admission_provider: ProviderName::default(),
1670 weights: AdmissionWeights::default(),
1671 admission_strategy: AdmissionStrategy::default(),
1672 rl_min_samples: default_rl_min_samples(),
1673 rl_retrain_interval_secs: default_rl_retrain_interval_secs(),
1674 goal_conditioned_write: false,
1675 goal_utility_provider: ProviderName::default(),
1676 goal_utility_threshold: default_goal_utility_threshold(),
1677 goal_utility_weight: default_goal_utility_weight(),
1678 }
1679 }
1680}
1681
1682#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
1684#[serde(rename_all = "snake_case")]
1685pub enum StoreRoutingStrategy {
1686 #[default]
1688 Heuristic,
1689 Llm,
1691 Hybrid,
1693}
1694
1695#[derive(Debug, Clone, Deserialize, Serialize)]
1700#[serde(default)]
1701pub struct StoreRoutingConfig {
1702 pub enabled: bool,
1705 pub strategy: StoreRoutingStrategy,
1707 pub routing_classifier_provider: ProviderName,
1710 pub fallback_route: String,
1713 pub confidence_threshold: f32,
1716}
1717
1718impl Default for StoreRoutingConfig {
1719 fn default() -> Self {
1720 Self {
1721 enabled: false,
1722 strategy: StoreRoutingStrategy::Heuristic,
1723 routing_classifier_provider: ProviderName::default(),
1724 fallback_route: "hybrid".into(),
1725 confidence_threshold: 0.7,
1726 }
1727 }
1728}
1729
1730#[derive(Debug, Clone, Deserialize, Serialize)]
1735#[serde(default)]
1736pub struct PersonaConfig {
1737 pub enabled: bool,
1739 pub persona_provider: ProviderName,
1742 pub min_confidence: f64,
1744 pub min_messages: usize,
1746 pub max_messages: usize,
1748 pub extraction_timeout_secs: u64,
1750 pub context_budget_tokens: usize,
1752}
1753
1754impl Default for PersonaConfig {
1755 fn default() -> Self {
1756 Self {
1757 enabled: false,
1758 persona_provider: ProviderName::default(),
1759 min_confidence: 0.6,
1760 min_messages: 3,
1761 max_messages: 10,
1762 extraction_timeout_secs: 10,
1763 context_budget_tokens: 500,
1764 }
1765 }
1766}
1767
1768#[cfg(test)]
1769mod tests {
1770 use super::*;
1771
1772 #[test]
1775 fn pruning_strategy_toml_task_aware_mig_falls_back_to_reactive() {
1776 #[derive(serde::Deserialize)]
1777 struct Wrapper {
1778 #[allow(dead_code)]
1779 pruning_strategy: PruningStrategy,
1780 }
1781 let toml = r#"pruning_strategy = "task_aware_mig""#;
1782 let w: Wrapper = toml::from_str(toml).expect("should deserialize without error");
1783 assert_eq!(
1784 w.pruning_strategy,
1785 PruningStrategy::Reactive,
1786 "task_aware_mig must fall back to Reactive"
1787 );
1788 }
1789
1790 #[test]
1791 fn pruning_strategy_toml_round_trip() {
1792 #[derive(serde::Deserialize)]
1793 struct Wrapper {
1794 #[allow(dead_code)]
1795 pruning_strategy: PruningStrategy,
1796 }
1797 for (input, expected) in [
1798 ("reactive", PruningStrategy::Reactive),
1799 ("task_aware", PruningStrategy::TaskAware),
1800 ("mig", PruningStrategy::Mig),
1801 ] {
1802 let toml = format!(r#"pruning_strategy = "{input}""#);
1803 let w: Wrapper = toml::from_str(&toml)
1804 .unwrap_or_else(|e| panic!("failed to deserialize `{input}`: {e}"));
1805 assert_eq!(w.pruning_strategy, expected, "mismatch for `{input}`");
1806 }
1807 }
1808
1809 #[test]
1810 fn pruning_strategy_toml_unknown_value_errors() {
1811 #[derive(serde::Deserialize)]
1812 #[allow(dead_code)]
1813 struct Wrapper {
1814 pruning_strategy: PruningStrategy,
1815 }
1816 let toml = r#"pruning_strategy = "nonexistent_strategy""#;
1817 assert!(
1818 toml::from_str::<Wrapper>(toml).is_err(),
1819 "unknown strategy must produce an error"
1820 );
1821 }
1822
1823 #[test]
1824 fn tier_config_defaults_are_correct() {
1825 let cfg = TierConfig::default();
1826 assert!(!cfg.enabled);
1827 assert_eq!(cfg.promotion_min_sessions, 3);
1828 assert!((cfg.similarity_threshold - 0.92).abs() < f32::EPSILON);
1829 assert_eq!(cfg.sweep_interval_secs, 3600);
1830 assert_eq!(cfg.sweep_batch_size, 100);
1831 }
1832
1833 #[test]
1834 fn tier_config_rejects_min_sessions_below_2() {
1835 let toml = "promotion_min_sessions = 1";
1836 assert!(toml::from_str::<TierConfig>(toml).is_err());
1837 }
1838
1839 #[test]
1840 fn tier_config_rejects_similarity_threshold_below_0_5() {
1841 let toml = "similarity_threshold = 0.4";
1842 assert!(toml::from_str::<TierConfig>(toml).is_err());
1843 }
1844
1845 #[test]
1846 fn tier_config_rejects_zero_sweep_batch_size() {
1847 let toml = "sweep_batch_size = 0";
1848 assert!(toml::from_str::<TierConfig>(toml).is_err());
1849 }
1850
1851 fn deserialize_importance_weight(toml_val: &str) -> Result<SemanticConfig, toml::de::Error> {
1852 let input = format!("importance_weight = {toml_val}");
1853 toml::from_str::<SemanticConfig>(&input)
1854 }
1855
1856 #[test]
1857 fn importance_weight_default_is_0_15() {
1858 let cfg = SemanticConfig::default();
1859 assert!((cfg.importance_weight - 0.15).abs() < f64::EPSILON);
1860 }
1861
1862 #[test]
1863 fn importance_weight_valid_zero() {
1864 let cfg = deserialize_importance_weight("0.0").unwrap();
1865 assert!((cfg.importance_weight - 0.0_f64).abs() < f64::EPSILON);
1866 }
1867
1868 #[test]
1869 fn importance_weight_valid_one() {
1870 let cfg = deserialize_importance_weight("1.0").unwrap();
1871 assert!((cfg.importance_weight - 1.0_f64).abs() < f64::EPSILON);
1872 }
1873
1874 #[test]
1875 fn importance_weight_rejects_near_zero_negative() {
1876 let result = deserialize_importance_weight("-0.01");
1881 assert!(
1882 result.is_err(),
1883 "negative importance_weight must be rejected"
1884 );
1885 }
1886
1887 #[test]
1888 fn importance_weight_rejects_negative() {
1889 let result = deserialize_importance_weight("-1.0");
1890 assert!(result.is_err(), "negative value must be rejected");
1891 }
1892
1893 #[test]
1894 fn importance_weight_rejects_greater_than_one() {
1895 let result = deserialize_importance_weight("1.01");
1896 assert!(result.is_err(), "value > 1.0 must be rejected");
1897 }
1898
1899 #[test]
1903 fn admission_weights_normalized_sums_to_one() {
1904 let w = AdmissionWeights {
1905 future_utility: 2.0,
1906 factual_confidence: 1.0,
1907 semantic_novelty: 3.0,
1908 temporal_recency: 1.0,
1909 content_type_prior: 3.0,
1910 goal_utility: 0.0,
1911 };
1912 let n = w.normalized();
1913 let sum = n.future_utility
1914 + n.factual_confidence
1915 + n.semantic_novelty
1916 + n.temporal_recency
1917 + n.content_type_prior;
1918 assert!(
1919 (sum - 1.0).abs() < 0.001,
1920 "normalized weights must sum to 1.0, got {sum}"
1921 );
1922 }
1923
1924 #[test]
1926 fn admission_weights_normalized_preserves_already_unit_sum() {
1927 let w = AdmissionWeights::default();
1928 let n = w.normalized();
1929 let sum = n.future_utility
1930 + n.factual_confidence
1931 + n.semantic_novelty
1932 + n.temporal_recency
1933 + n.content_type_prior;
1934 assert!(
1935 (sum - 1.0).abs() < 0.001,
1936 "default weights sum to ~1.0 after normalization"
1937 );
1938 }
1939
1940 #[test]
1942 fn admission_weights_normalized_zero_sum_falls_back_to_default() {
1943 let w = AdmissionWeights {
1944 future_utility: 0.0,
1945 factual_confidence: 0.0,
1946 semantic_novelty: 0.0,
1947 temporal_recency: 0.0,
1948 content_type_prior: 0.0,
1949 goal_utility: 0.0,
1950 };
1951 let n = w.normalized();
1952 let default = AdmissionWeights::default();
1953 assert!(
1954 (n.future_utility - default.future_utility).abs() < 0.001,
1955 "zero-sum weights must fall back to defaults"
1956 );
1957 }
1958
1959 #[test]
1961 fn admission_config_defaults() {
1962 let cfg = AdmissionConfig::default();
1963 assert!(!cfg.enabled);
1964 assert!((cfg.threshold - 0.40).abs() < 0.001);
1965 assert!((cfg.fast_path_margin - 0.15).abs() < 0.001);
1966 assert!(cfg.admission_provider.is_empty());
1967 }
1968
1969 #[test]
1972 fn spreading_activation_default_recall_timeout_ms_is_1000() {
1973 let cfg = SpreadingActivationConfig::default();
1974 assert_eq!(
1975 cfg.recall_timeout_ms, 1000,
1976 "default recall_timeout_ms must be 1000ms"
1977 );
1978 }
1979
1980 #[test]
1981 fn spreading_activation_toml_recall_timeout_ms_round_trip() {
1982 #[derive(serde::Deserialize)]
1983 struct Wrapper {
1984 recall_timeout_ms: u64,
1985 }
1986 let toml = "recall_timeout_ms = 500";
1987 let w: Wrapper = toml::from_str(toml).unwrap();
1988 assert_eq!(w.recall_timeout_ms, 500);
1989 }
1990
1991 #[test]
1992 fn spreading_activation_validate_cross_field_constraints() {
1993 let mut cfg = SpreadingActivationConfig::default();
1994 assert!(cfg.validate().is_ok());
1996
1997 cfg.activation_threshold = 0.5;
1999 cfg.inhibition_threshold = 0.5;
2000 assert!(cfg.validate().is_err());
2001 }
2002
2003 #[test]
2006 fn compression_config_focus_strategy_deserializes() {
2007 let toml = r#"strategy = "focus""#;
2008 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2009 assert_eq!(cfg.strategy, CompressionStrategy::Focus);
2010 }
2011
2012 #[test]
2013 fn compression_config_density_budget_defaults_on_deserialize() {
2014 let toml = r#"strategy = "reactive""#;
2017 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2018 assert!((cfg.high_density_budget - 0.7).abs() < 1e-6);
2019 assert!((cfg.low_density_budget - 0.3).abs() < 1e-6);
2020 }
2021
2022 #[test]
2023 fn compression_config_density_budget_round_trip() {
2024 let toml = "strategy = \"reactive\"\nhigh_density_budget = 0.6\nlow_density_budget = 0.4";
2025 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2026 assert!((cfg.high_density_budget - 0.6).abs() < f32::EPSILON);
2027 assert!((cfg.low_density_budget - 0.4).abs() < f32::EPSILON);
2028 }
2029
2030 #[test]
2031 fn compression_config_focus_scorer_provider_default_empty() {
2032 let cfg = CompressionConfig::default();
2033 assert!(cfg.focus_scorer_provider.is_empty());
2034 }
2035
2036 #[test]
2037 fn compression_config_focus_scorer_provider_round_trip() {
2038 let toml = "strategy = \"focus\"\nfocus_scorer_provider = \"fast\"";
2039 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2040 assert_eq!(cfg.focus_scorer_provider.as_str(), "fast");
2041 }
2042}