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}
801
802fn default_crossover_turn_threshold() -> u32 {
803 20
804}
805
806#[derive(Debug, Clone, Deserialize, Serialize)]
808#[serde(default)]
809pub struct DigestConfig {
810 pub enabled: bool,
812 pub provider: String,
815 pub max_tokens: usize,
817 pub max_input_messages: usize,
819}
820
821impl Default for DigestConfig {
822 fn default() -> Self {
823 Self {
824 enabled: false,
825 provider: String::new(),
826 max_tokens: 500,
827 max_input_messages: 50,
828 }
829 }
830}
831
832#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
834#[serde(rename_all = "snake_case")]
835pub enum ContextStrategy {
836 #[default]
839 FullHistory,
840 MemoryFirst,
843 Adaptive,
846}
847
848#[derive(Debug, Clone, Deserialize, Serialize)]
849#[serde(default)]
850pub struct SessionsConfig {
851 #[serde(default = "default_max_history")]
853 pub max_history: usize,
854 #[serde(default = "default_title_max_chars")]
856 pub title_max_chars: usize,
857}
858
859impl Default for SessionsConfig {
860 fn default() -> Self {
861 Self {
862 max_history: default_max_history(),
863 title_max_chars: default_title_max_chars(),
864 }
865 }
866}
867
868#[derive(Debug, Clone, Deserialize, Serialize)]
870pub struct DocumentConfig {
871 #[serde(default = "default_document_collection")]
872 pub collection: String,
873 #[serde(default = "default_document_chunk_size")]
874 pub chunk_size: usize,
875 #[serde(default = "default_document_chunk_overlap")]
876 pub chunk_overlap: usize,
877 #[serde(default = "default_document_top_k")]
879 pub top_k: usize,
880 #[serde(default)]
882 pub rag_enabled: bool,
883}
884
885impl Default for DocumentConfig {
886 fn default() -> Self {
887 Self {
888 collection: default_document_collection(),
889 chunk_size: default_document_chunk_size(),
890 chunk_overlap: default_document_chunk_overlap(),
891 top_k: default_document_top_k(),
892 rag_enabled: false,
893 }
894 }
895}
896
897#[derive(Debug, Deserialize, Serialize)]
898#[allow(clippy::struct_excessive_bools)]
899pub struct SemanticConfig {
900 #[serde(default = "default_semantic_enabled")]
901 pub enabled: bool,
902 #[serde(default = "default_recall_limit")]
903 pub recall_limit: usize,
904 #[serde(default = "default_vector_weight")]
905 pub vector_weight: f64,
906 #[serde(default = "default_keyword_weight")]
907 pub keyword_weight: f64,
908 #[serde(default = "default_true")]
909 pub temporal_decay_enabled: bool,
910 #[serde(default = "default_temporal_decay_half_life_days")]
911 pub temporal_decay_half_life_days: u32,
912 #[serde(default = "default_true")]
913 pub mmr_enabled: bool,
914 #[serde(default = "default_mmr_lambda")]
915 pub mmr_lambda: f32,
916 #[serde(default = "default_true")]
917 pub importance_enabled: bool,
918 #[serde(
919 default = "default_importance_weight",
920 deserialize_with = "validate_importance_weight"
921 )]
922 pub importance_weight: f64,
923}
924
925impl Default for SemanticConfig {
926 fn default() -> Self {
927 Self {
928 enabled: default_semantic_enabled(),
929 recall_limit: default_recall_limit(),
930 vector_weight: default_vector_weight(),
931 keyword_weight: default_keyword_weight(),
932 temporal_decay_enabled: true,
933 temporal_decay_half_life_days: default_temporal_decay_half_life_days(),
934 mmr_enabled: true,
935 mmr_lambda: default_mmr_lambda(),
936 importance_enabled: true,
937 importance_weight: default_importance_weight(),
938 }
939 }
940}
941
942#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
944#[serde(tag = "strategy", rename_all = "snake_case")]
945pub enum CompressionStrategy {
946 #[default]
948 Reactive,
949 Proactive {
951 threshold_tokens: usize,
953 max_summary_tokens: usize,
955 },
956 Autonomous,
959 Focus,
964}
965
966#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
971#[serde(rename_all = "snake_case")]
972pub enum PruningStrategy {
973 #[default]
975 Reactive,
976 TaskAware,
979 Mig,
982 Subgoal,
986 SubgoalMig,
989}
990
991impl PruningStrategy {
992 #[must_use]
994 pub fn is_subgoal(self) -> bool {
995 matches!(self, Self::Subgoal | Self::SubgoalMig)
996 }
997}
998
999impl<'de> serde::Deserialize<'de> for PruningStrategy {
1002 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1003 let s = String::deserialize(deserializer)?;
1004 s.parse().map_err(serde::de::Error::custom)
1005 }
1006}
1007
1008impl std::str::FromStr for PruningStrategy {
1009 type Err = String;
1010
1011 fn from_str(s: &str) -> Result<Self, Self::Err> {
1012 match s {
1013 "reactive" => Ok(Self::Reactive),
1014 "task_aware" | "task-aware" => Ok(Self::TaskAware),
1015 "mig" => Ok(Self::Mig),
1016 "task_aware_mig" | "task-aware-mig" => {
1019 tracing::warn!(
1020 "pruning strategy `task_aware_mig` has been removed; \
1021 falling back to `reactive`. Use `task_aware` or `mig` instead."
1022 );
1023 Ok(Self::Reactive)
1024 }
1025 "subgoal" => Ok(Self::Subgoal),
1026 "subgoal_mig" | "subgoal-mig" => Ok(Self::SubgoalMig),
1027 other => Err(format!(
1028 "unknown pruning strategy `{other}`, expected \
1029 reactive|task_aware|mig|subgoal|subgoal_mig"
1030 )),
1031 }
1032 }
1033}
1034
1035fn default_high_density_budget() -> f32 {
1036 0.7
1037}
1038
1039fn default_low_density_budget() -> f32 {
1040 0.3
1041}
1042
1043#[derive(Debug, Clone, Deserialize, Serialize)]
1050#[serde(default)]
1051pub struct CompressionPredictorConfig {
1052 pub enabled: bool,
1054 pub min_samples: u64,
1056 pub candidate_ratios: Vec<f32>,
1059 pub retrain_interval: u64,
1061 pub max_training_samples: usize,
1063}
1064
1065impl Default for CompressionPredictorConfig {
1066 fn default() -> Self {
1067 Self {
1068 enabled: false,
1069 min_samples: 10,
1070 candidate_ratios: vec![0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
1071 retrain_interval: 5,
1072 max_training_samples: 200,
1073 }
1074 }
1075}
1076
1077#[derive(Debug, Clone, Deserialize, Serialize)]
1083#[serde(default)]
1084pub struct ForgettingConfig {
1085 pub enabled: bool,
1087 pub decay_rate: f32,
1089 pub forgetting_floor: f32,
1091 pub sweep_interval_secs: u64,
1093 pub sweep_batch_size: usize,
1095 pub replay_window_hours: u32,
1097 pub replay_min_access_count: u32,
1099 pub protect_recent_hours: u32,
1101 pub protect_min_access_count: u32,
1103}
1104
1105impl Default for ForgettingConfig {
1106 fn default() -> Self {
1107 Self {
1108 enabled: false,
1109 decay_rate: 0.1,
1110 forgetting_floor: 0.05,
1111 sweep_interval_secs: 7200,
1112 sweep_batch_size: 500,
1113 replay_window_hours: 24,
1114 replay_min_access_count: 3,
1115 protect_recent_hours: 24,
1116 protect_min_access_count: 3,
1117 }
1118 }
1119}
1120
1121#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1123#[serde(default)]
1124pub struct CompressionConfig {
1125 #[serde(flatten)]
1127 pub strategy: CompressionStrategy,
1128 pub pruning_strategy: PruningStrategy,
1130 pub model: String,
1135 pub compress_provider: ProviderName,
1138 #[serde(default)]
1140 pub probe: zeph_memory::CompactionProbeConfig,
1141 #[serde(default)]
1149 pub archive_tool_outputs: bool,
1150 pub focus_scorer_provider: ProviderName,
1153 #[serde(default = "default_high_density_budget")]
1156 pub high_density_budget: f32,
1157 #[serde(default = "default_low_density_budget")]
1160 pub low_density_budget: f32,
1161 #[serde(default)]
1163 pub predictor: CompressionPredictorConfig,
1164}
1165
1166fn default_sidequest_interval_turns() -> u32 {
1167 4
1168}
1169
1170fn default_sidequest_max_eviction_ratio() -> f32 {
1171 0.5
1172}
1173
1174fn default_sidequest_max_cursors() -> usize {
1175 30
1176}
1177
1178fn default_sidequest_min_cursor_tokens() -> usize {
1179 100
1180}
1181
1182#[derive(Debug, Clone, Deserialize, Serialize)]
1184#[serde(default)]
1185pub struct SidequestConfig {
1186 pub enabled: bool,
1188 #[serde(default = "default_sidequest_interval_turns")]
1190 pub interval_turns: u32,
1191 #[serde(default = "default_sidequest_max_eviction_ratio")]
1193 pub max_eviction_ratio: f32,
1194 #[serde(default = "default_sidequest_max_cursors")]
1196 pub max_cursors: usize,
1197 #[serde(default = "default_sidequest_min_cursor_tokens")]
1200 pub min_cursor_tokens: usize,
1201}
1202
1203impl Default for SidequestConfig {
1204 fn default() -> Self {
1205 Self {
1206 enabled: false,
1207 interval_turns: default_sidequest_interval_turns(),
1208 max_eviction_ratio: default_sidequest_max_eviction_ratio(),
1209 max_cursors: default_sidequest_max_cursors(),
1210 min_cursor_tokens: default_sidequest_min_cursor_tokens(),
1211 }
1212 }
1213}
1214
1215#[derive(Debug, Clone, Deserialize, Serialize)]
1224#[serde(default)]
1225pub struct GraphConfig {
1226 pub enabled: bool,
1227 pub extract_model: String,
1228 #[serde(default = "default_graph_max_entities_per_message")]
1229 pub max_entities_per_message: usize,
1230 #[serde(default = "default_graph_max_edges_per_message")]
1231 pub max_edges_per_message: usize,
1232 #[serde(default = "default_graph_community_refresh_interval")]
1233 pub community_refresh_interval: usize,
1234 #[serde(default = "default_graph_entity_similarity_threshold")]
1235 pub entity_similarity_threshold: f32,
1236 #[serde(default = "default_graph_extraction_timeout_secs")]
1237 pub extraction_timeout_secs: u64,
1238 #[serde(default)]
1239 pub use_embedding_resolution: bool,
1240 #[serde(default = "default_graph_entity_ambiguous_threshold")]
1241 pub entity_ambiguous_threshold: f32,
1242 #[serde(default = "default_graph_max_hops")]
1243 pub max_hops: u32,
1244 #[serde(default = "default_graph_recall_limit")]
1245 pub recall_limit: usize,
1246 #[serde(default = "default_graph_expired_edge_retention_days")]
1248 pub expired_edge_retention_days: u32,
1249 #[serde(default)]
1251 pub max_entities: usize,
1252 #[serde(default = "default_graph_community_summary_max_prompt_bytes")]
1254 pub community_summary_max_prompt_bytes: usize,
1255 #[serde(default = "default_graph_community_summary_concurrency")]
1257 pub community_summary_concurrency: usize,
1258 #[serde(default = "default_lpa_edge_chunk_size")]
1261 pub lpa_edge_chunk_size: usize,
1262 #[serde(
1268 default = "default_graph_temporal_decay_rate",
1269 deserialize_with = "validate_temporal_decay_rate"
1270 )]
1271 pub temporal_decay_rate: f64,
1272 #[serde(default = "default_graph_edge_history_limit")]
1278 pub edge_history_limit: usize,
1279 #[serde(default)]
1285 pub note_linking: NoteLinkingConfig,
1286 #[serde(default)]
1291 pub spreading_activation: SpreadingActivationConfig,
1292 #[serde(
1295 default = "default_link_weight_decay_lambda",
1296 deserialize_with = "validate_link_weight_decay_lambda"
1297 )]
1298 pub link_weight_decay_lambda: f64,
1299 #[serde(default = "default_link_weight_decay_interval_secs")]
1301 pub link_weight_decay_interval_secs: u64,
1302 #[serde(default)]
1308 pub belief_revision: BeliefRevisionConfig,
1309 #[serde(default)]
1314 pub rpe: RpeConfig,
1315 #[serde(default = "default_graph_pool_size")]
1321 pub pool_size: u32,
1322}
1323
1324fn default_graph_pool_size() -> u32 {
1325 3
1326}
1327
1328impl Default for GraphConfig {
1329 fn default() -> Self {
1330 Self {
1331 enabled: false,
1332 extract_model: String::new(),
1333 max_entities_per_message: default_graph_max_entities_per_message(),
1334 max_edges_per_message: default_graph_max_edges_per_message(),
1335 community_refresh_interval: default_graph_community_refresh_interval(),
1336 entity_similarity_threshold: default_graph_entity_similarity_threshold(),
1337 extraction_timeout_secs: default_graph_extraction_timeout_secs(),
1338 use_embedding_resolution: false,
1339 entity_ambiguous_threshold: default_graph_entity_ambiguous_threshold(),
1340 max_hops: default_graph_max_hops(),
1341 recall_limit: default_graph_recall_limit(),
1342 expired_edge_retention_days: default_graph_expired_edge_retention_days(),
1343 max_entities: 0,
1344 community_summary_max_prompt_bytes: default_graph_community_summary_max_prompt_bytes(),
1345 community_summary_concurrency: default_graph_community_summary_concurrency(),
1346 lpa_edge_chunk_size: default_lpa_edge_chunk_size(),
1347 temporal_decay_rate: default_graph_temporal_decay_rate(),
1348 edge_history_limit: default_graph_edge_history_limit(),
1349 note_linking: NoteLinkingConfig::default(),
1350 spreading_activation: SpreadingActivationConfig::default(),
1351 link_weight_decay_lambda: default_link_weight_decay_lambda(),
1352 link_weight_decay_interval_secs: default_link_weight_decay_interval_secs(),
1353 belief_revision: BeliefRevisionConfig::default(),
1354 rpe: RpeConfig::default(),
1355 pool_size: default_graph_pool_size(),
1356 }
1357 }
1358}
1359
1360fn default_consolidation_confidence_threshold() -> f32 {
1361 0.7
1362}
1363
1364fn default_consolidation_sweep_interval_secs() -> u64 {
1365 3600
1366}
1367
1368fn default_consolidation_sweep_batch_size() -> usize {
1369 50
1370}
1371
1372fn default_consolidation_similarity_threshold() -> f32 {
1373 0.85
1374}
1375
1376#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1382#[serde(default)]
1383pub struct ConsolidationConfig {
1384 pub enabled: bool,
1386 #[serde(default)]
1389 pub consolidation_provider: ProviderName,
1390 #[serde(default = "default_consolidation_confidence_threshold")]
1392 pub confidence_threshold: f32,
1393 #[serde(default = "default_consolidation_sweep_interval_secs")]
1395 pub sweep_interval_secs: u64,
1396 #[serde(default = "default_consolidation_sweep_batch_size")]
1398 pub sweep_batch_size: usize,
1399 #[serde(default = "default_consolidation_similarity_threshold")]
1402 pub similarity_threshold: f32,
1403}
1404
1405impl Default for ConsolidationConfig {
1406 fn default() -> Self {
1407 Self {
1408 enabled: false,
1409 consolidation_provider: ProviderName::default(),
1410 confidence_threshold: default_consolidation_confidence_threshold(),
1411 sweep_interval_secs: default_consolidation_sweep_interval_secs(),
1412 sweep_batch_size: default_consolidation_sweep_batch_size(),
1413 similarity_threshold: default_consolidation_similarity_threshold(),
1414 }
1415 }
1416}
1417
1418fn default_link_weight_decay_lambda() -> f64 {
1419 0.95
1420}
1421
1422fn default_link_weight_decay_interval_secs() -> u64 {
1423 86400
1424}
1425
1426fn validate_link_weight_decay_lambda<'de, D>(deserializer: D) -> Result<f64, D::Error>
1427where
1428 D: serde::Deserializer<'de>,
1429{
1430 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
1431 if value.is_nan() || value.is_infinite() {
1432 return Err(serde::de::Error::custom(
1433 "link_weight_decay_lambda must be a finite number",
1434 ));
1435 }
1436 if !(value > 0.0 && value <= 1.0) {
1437 return Err(serde::de::Error::custom(
1438 "link_weight_decay_lambda must be in (0.0, 1.0]",
1439 ));
1440 }
1441 Ok(value)
1442}
1443
1444fn validate_admission_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
1445where
1446 D: serde::Deserializer<'de>,
1447{
1448 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1449 if value.is_nan() || value.is_infinite() {
1450 return Err(serde::de::Error::custom(
1451 "threshold must be a finite number",
1452 ));
1453 }
1454 if !(0.0..=1.0).contains(&value) {
1455 return Err(serde::de::Error::custom("threshold must be in [0.0, 1.0]"));
1456 }
1457 Ok(value)
1458}
1459
1460fn validate_admission_fast_path_margin<'de, D>(deserializer: D) -> Result<f32, D::Error>
1461where
1462 D: serde::Deserializer<'de>,
1463{
1464 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1465 if value.is_nan() || value.is_infinite() {
1466 return Err(serde::de::Error::custom(
1467 "fast_path_margin must be a finite number",
1468 ));
1469 }
1470 if !(0.0..=1.0).contains(&value) {
1471 return Err(serde::de::Error::custom(
1472 "fast_path_margin must be in [0.0, 1.0]",
1473 ));
1474 }
1475 Ok(value)
1476}
1477
1478fn default_admission_threshold() -> f32 {
1479 0.40
1480}
1481
1482fn default_admission_fast_path_margin() -> f32 {
1483 0.15
1484}
1485
1486fn default_rl_min_samples() -> u32 {
1487 500
1488}
1489
1490fn default_rl_retrain_interval_secs() -> u64 {
1491 3600
1492}
1493
1494#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1499#[serde(rename_all = "snake_case")]
1500pub enum AdmissionStrategy {
1501 #[default]
1503 Heuristic,
1504 Rl,
1507}
1508
1509fn validate_admission_weight<'de, D>(deserializer: D) -> Result<f32, D::Error>
1510where
1511 D: serde::Deserializer<'de>,
1512{
1513 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1514 if value < 0.0 {
1515 return Err(serde::de::Error::custom(
1516 "admission weight must be non-negative (>= 0.0)",
1517 ));
1518 }
1519 Ok(value)
1520}
1521
1522#[derive(Debug, Clone, Deserialize, Serialize)]
1527#[serde(default)]
1528pub struct AdmissionWeights {
1529 #[serde(deserialize_with = "validate_admission_weight")]
1531 pub future_utility: f32,
1532 #[serde(deserialize_with = "validate_admission_weight")]
1534 pub factual_confidence: f32,
1535 #[serde(deserialize_with = "validate_admission_weight")]
1537 pub semantic_novelty: f32,
1538 #[serde(deserialize_with = "validate_admission_weight")]
1540 pub temporal_recency: f32,
1541 #[serde(deserialize_with = "validate_admission_weight")]
1543 pub content_type_prior: f32,
1544 #[serde(deserialize_with = "validate_admission_weight")]
1548 pub goal_utility: f32,
1549}
1550
1551impl Default for AdmissionWeights {
1552 fn default() -> Self {
1553 Self {
1554 future_utility: 0.30,
1555 factual_confidence: 0.15,
1556 semantic_novelty: 0.30,
1557 temporal_recency: 0.10,
1558 content_type_prior: 0.15,
1559 goal_utility: 0.0,
1560 }
1561 }
1562}
1563
1564impl AdmissionWeights {
1565 #[must_use]
1569 pub fn normalized(&self) -> Self {
1570 let sum = self.future_utility
1571 + self.factual_confidence
1572 + self.semantic_novelty
1573 + self.temporal_recency
1574 + self.content_type_prior
1575 + self.goal_utility;
1576 if sum <= f32::EPSILON {
1577 return Self::default();
1578 }
1579 Self {
1580 future_utility: self.future_utility / sum,
1581 factual_confidence: self.factual_confidence / sum,
1582 semantic_novelty: self.semantic_novelty / sum,
1583 temporal_recency: self.temporal_recency / sum,
1584 content_type_prior: self.content_type_prior / sum,
1585 goal_utility: self.goal_utility / sum,
1586 }
1587 }
1588}
1589
1590#[derive(Debug, Clone, Deserialize, Serialize)]
1595#[serde(default)]
1596pub struct AdmissionConfig {
1597 pub enabled: bool,
1599 #[serde(deserialize_with = "validate_admission_threshold")]
1602 pub threshold: f32,
1603 #[serde(deserialize_with = "validate_admission_fast_path_margin")]
1606 pub fast_path_margin: f32,
1607 pub admission_provider: ProviderName,
1610 pub weights: AdmissionWeights,
1612 #[serde(default)]
1614 pub admission_strategy: AdmissionStrategy,
1615 #[serde(default = "default_rl_min_samples")]
1618 pub rl_min_samples: u32,
1619 #[serde(default = "default_rl_retrain_interval_secs")]
1621 pub rl_retrain_interval_secs: u64,
1622 #[serde(default)]
1626 pub goal_conditioned_write: bool,
1627 #[serde(default)]
1631 pub goal_utility_provider: ProviderName,
1632 #[serde(default = "default_goal_utility_threshold")]
1635 pub goal_utility_threshold: f32,
1636 #[serde(default = "default_goal_utility_weight")]
1639 pub goal_utility_weight: f32,
1640}
1641
1642fn default_goal_utility_threshold() -> f32 {
1643 0.4
1644}
1645
1646fn default_goal_utility_weight() -> f32 {
1647 0.25
1648}
1649
1650impl Default for AdmissionConfig {
1651 fn default() -> Self {
1652 Self {
1653 enabled: false,
1654 threshold: default_admission_threshold(),
1655 fast_path_margin: default_admission_fast_path_margin(),
1656 admission_provider: ProviderName::default(),
1657 weights: AdmissionWeights::default(),
1658 admission_strategy: AdmissionStrategy::default(),
1659 rl_min_samples: default_rl_min_samples(),
1660 rl_retrain_interval_secs: default_rl_retrain_interval_secs(),
1661 goal_conditioned_write: false,
1662 goal_utility_provider: ProviderName::default(),
1663 goal_utility_threshold: default_goal_utility_threshold(),
1664 goal_utility_weight: default_goal_utility_weight(),
1665 }
1666 }
1667}
1668
1669#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
1671#[serde(rename_all = "snake_case")]
1672pub enum StoreRoutingStrategy {
1673 #[default]
1675 Heuristic,
1676 Llm,
1678 Hybrid,
1680}
1681
1682#[derive(Debug, Clone, Deserialize, Serialize)]
1687#[serde(default)]
1688pub struct StoreRoutingConfig {
1689 pub enabled: bool,
1692 pub strategy: StoreRoutingStrategy,
1694 pub routing_classifier_provider: ProviderName,
1697 pub fallback_route: String,
1700 pub confidence_threshold: f32,
1703}
1704
1705impl Default for StoreRoutingConfig {
1706 fn default() -> Self {
1707 Self {
1708 enabled: false,
1709 strategy: StoreRoutingStrategy::Heuristic,
1710 routing_classifier_provider: ProviderName::default(),
1711 fallback_route: "hybrid".into(),
1712 confidence_threshold: 0.7,
1713 }
1714 }
1715}
1716
1717#[cfg(test)]
1718mod tests {
1719 use super::*;
1720
1721 #[test]
1724 fn pruning_strategy_toml_task_aware_mig_falls_back_to_reactive() {
1725 #[derive(serde::Deserialize)]
1726 struct Wrapper {
1727 #[allow(dead_code)]
1728 pruning_strategy: PruningStrategy,
1729 }
1730 let toml = r#"pruning_strategy = "task_aware_mig""#;
1731 let w: Wrapper = toml::from_str(toml).expect("should deserialize without error");
1732 assert_eq!(
1733 w.pruning_strategy,
1734 PruningStrategy::Reactive,
1735 "task_aware_mig must fall back to Reactive"
1736 );
1737 }
1738
1739 #[test]
1740 fn pruning_strategy_toml_round_trip() {
1741 #[derive(serde::Deserialize)]
1742 struct Wrapper {
1743 #[allow(dead_code)]
1744 pruning_strategy: PruningStrategy,
1745 }
1746 for (input, expected) in [
1747 ("reactive", PruningStrategy::Reactive),
1748 ("task_aware", PruningStrategy::TaskAware),
1749 ("mig", PruningStrategy::Mig),
1750 ] {
1751 let toml = format!(r#"pruning_strategy = "{input}""#);
1752 let w: Wrapper = toml::from_str(&toml)
1753 .unwrap_or_else(|e| panic!("failed to deserialize `{input}`: {e}"));
1754 assert_eq!(w.pruning_strategy, expected, "mismatch for `{input}`");
1755 }
1756 }
1757
1758 #[test]
1759 fn pruning_strategy_toml_unknown_value_errors() {
1760 #[derive(serde::Deserialize)]
1761 #[allow(dead_code)]
1762 struct Wrapper {
1763 pruning_strategy: PruningStrategy,
1764 }
1765 let toml = r#"pruning_strategy = "nonexistent_strategy""#;
1766 assert!(
1767 toml::from_str::<Wrapper>(toml).is_err(),
1768 "unknown strategy must produce an error"
1769 );
1770 }
1771
1772 #[test]
1773 fn tier_config_defaults_are_correct() {
1774 let cfg = TierConfig::default();
1775 assert!(!cfg.enabled);
1776 assert_eq!(cfg.promotion_min_sessions, 3);
1777 assert!((cfg.similarity_threshold - 0.92).abs() < f32::EPSILON);
1778 assert_eq!(cfg.sweep_interval_secs, 3600);
1779 assert_eq!(cfg.sweep_batch_size, 100);
1780 }
1781
1782 #[test]
1783 fn tier_config_rejects_min_sessions_below_2() {
1784 let toml = "promotion_min_sessions = 1";
1785 assert!(toml::from_str::<TierConfig>(toml).is_err());
1786 }
1787
1788 #[test]
1789 fn tier_config_rejects_similarity_threshold_below_0_5() {
1790 let toml = "similarity_threshold = 0.4";
1791 assert!(toml::from_str::<TierConfig>(toml).is_err());
1792 }
1793
1794 #[test]
1795 fn tier_config_rejects_zero_sweep_batch_size() {
1796 let toml = "sweep_batch_size = 0";
1797 assert!(toml::from_str::<TierConfig>(toml).is_err());
1798 }
1799
1800 fn deserialize_importance_weight(toml_val: &str) -> Result<SemanticConfig, toml::de::Error> {
1801 let input = format!("importance_weight = {toml_val}");
1802 toml::from_str::<SemanticConfig>(&input)
1803 }
1804
1805 #[test]
1806 fn importance_weight_default_is_0_15() {
1807 let cfg = SemanticConfig::default();
1808 assert!((cfg.importance_weight - 0.15).abs() < f64::EPSILON);
1809 }
1810
1811 #[test]
1812 fn importance_weight_valid_zero() {
1813 let cfg = deserialize_importance_weight("0.0").unwrap();
1814 assert!((cfg.importance_weight - 0.0_f64).abs() < f64::EPSILON);
1815 }
1816
1817 #[test]
1818 fn importance_weight_valid_one() {
1819 let cfg = deserialize_importance_weight("1.0").unwrap();
1820 assert!((cfg.importance_weight - 1.0_f64).abs() < f64::EPSILON);
1821 }
1822
1823 #[test]
1824 fn importance_weight_rejects_near_zero_negative() {
1825 let result = deserialize_importance_weight("-0.01");
1830 assert!(
1831 result.is_err(),
1832 "negative importance_weight must be rejected"
1833 );
1834 }
1835
1836 #[test]
1837 fn importance_weight_rejects_negative() {
1838 let result = deserialize_importance_weight("-1.0");
1839 assert!(result.is_err(), "negative value must be rejected");
1840 }
1841
1842 #[test]
1843 fn importance_weight_rejects_greater_than_one() {
1844 let result = deserialize_importance_weight("1.01");
1845 assert!(result.is_err(), "value > 1.0 must be rejected");
1846 }
1847
1848 #[test]
1852 fn admission_weights_normalized_sums_to_one() {
1853 let w = AdmissionWeights {
1854 future_utility: 2.0,
1855 factual_confidence: 1.0,
1856 semantic_novelty: 3.0,
1857 temporal_recency: 1.0,
1858 content_type_prior: 3.0,
1859 goal_utility: 0.0,
1860 };
1861 let n = w.normalized();
1862 let sum = n.future_utility
1863 + n.factual_confidence
1864 + n.semantic_novelty
1865 + n.temporal_recency
1866 + n.content_type_prior;
1867 assert!(
1868 (sum - 1.0).abs() < 0.001,
1869 "normalized weights must sum to 1.0, got {sum}"
1870 );
1871 }
1872
1873 #[test]
1875 fn admission_weights_normalized_preserves_already_unit_sum() {
1876 let w = AdmissionWeights::default();
1877 let n = w.normalized();
1878 let sum = n.future_utility
1879 + n.factual_confidence
1880 + n.semantic_novelty
1881 + n.temporal_recency
1882 + n.content_type_prior;
1883 assert!(
1884 (sum - 1.0).abs() < 0.001,
1885 "default weights sum to ~1.0 after normalization"
1886 );
1887 }
1888
1889 #[test]
1891 fn admission_weights_normalized_zero_sum_falls_back_to_default() {
1892 let w = AdmissionWeights {
1893 future_utility: 0.0,
1894 factual_confidence: 0.0,
1895 semantic_novelty: 0.0,
1896 temporal_recency: 0.0,
1897 content_type_prior: 0.0,
1898 goal_utility: 0.0,
1899 };
1900 let n = w.normalized();
1901 let default = AdmissionWeights::default();
1902 assert!(
1903 (n.future_utility - default.future_utility).abs() < 0.001,
1904 "zero-sum weights must fall back to defaults"
1905 );
1906 }
1907
1908 #[test]
1910 fn admission_config_defaults() {
1911 let cfg = AdmissionConfig::default();
1912 assert!(!cfg.enabled);
1913 assert!((cfg.threshold - 0.40).abs() < 0.001);
1914 assert!((cfg.fast_path_margin - 0.15).abs() < 0.001);
1915 assert!(cfg.admission_provider.is_empty());
1916 }
1917
1918 #[test]
1921 fn spreading_activation_default_recall_timeout_ms_is_1000() {
1922 let cfg = SpreadingActivationConfig::default();
1923 assert_eq!(
1924 cfg.recall_timeout_ms, 1000,
1925 "default recall_timeout_ms must be 1000ms"
1926 );
1927 }
1928
1929 #[test]
1930 fn spreading_activation_toml_recall_timeout_ms_round_trip() {
1931 #[derive(serde::Deserialize)]
1932 struct Wrapper {
1933 recall_timeout_ms: u64,
1934 }
1935 let toml = "recall_timeout_ms = 500";
1936 let w: Wrapper = toml::from_str(toml).unwrap();
1937 assert_eq!(w.recall_timeout_ms, 500);
1938 }
1939
1940 #[test]
1941 fn spreading_activation_validate_cross_field_constraints() {
1942 let mut cfg = SpreadingActivationConfig::default();
1943 assert!(cfg.validate().is_ok());
1945
1946 cfg.activation_threshold = 0.5;
1948 cfg.inhibition_threshold = 0.5;
1949 assert!(cfg.validate().is_err());
1950 }
1951
1952 #[test]
1955 fn compression_config_focus_strategy_deserializes() {
1956 let toml = r#"strategy = "focus""#;
1957 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
1958 assert_eq!(cfg.strategy, CompressionStrategy::Focus);
1959 }
1960
1961 #[test]
1962 fn compression_config_density_budget_defaults_on_deserialize() {
1963 let toml = r#"strategy = "reactive""#;
1966 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
1967 assert!((cfg.high_density_budget - 0.7).abs() < 1e-6);
1968 assert!((cfg.low_density_budget - 0.3).abs() < 1e-6);
1969 }
1970
1971 #[test]
1972 fn compression_config_density_budget_round_trip() {
1973 let toml = "strategy = \"reactive\"\nhigh_density_budget = 0.6\nlow_density_budget = 0.4";
1974 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
1975 assert!((cfg.high_density_budget - 0.6).abs() < f32::EPSILON);
1976 assert!((cfg.low_density_budget - 0.4).abs() < f32::EPSILON);
1977 }
1978
1979 #[test]
1980 fn compression_config_focus_scorer_provider_default_empty() {
1981 let cfg = CompressionConfig::default();
1982 assert!(cfg.focus_scorer_provider.is_empty());
1983 }
1984
1985 #[test]
1986 fn compression_config_focus_scorer_provider_round_trip() {
1987 let toml = "strategy = \"focus\"\nfocus_scorer_provider = \"fast\"";
1988 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
1989 assert_eq!(cfg.focus_scorer_provider.as_str(), "fast");
1990 }
1991}