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]
670 pub fn as_str(&self) -> &'static str {
671 match self {
672 Self::Qdrant => "qdrant",
673 Self::Sqlite => "sqlite",
674 }
675 }
676}
677
678#[derive(Debug, Deserialize, Serialize)]
694#[allow(clippy::struct_excessive_bools)]
695pub struct MemoryConfig {
696 #[serde(default)]
697 pub compression_guidelines: zeph_memory::CompressionGuidelinesConfig,
698 #[serde(default = "default_sqlite_path_field")]
699 pub sqlite_path: String,
700 pub history_limit: u32,
701 #[serde(default = "default_qdrant_url")]
702 pub qdrant_url: String,
703 #[serde(default)]
704 pub semantic: SemanticConfig,
705 #[serde(default = "default_summarization_threshold")]
706 pub summarization_threshold: usize,
707 #[serde(default = "default_context_budget_tokens")]
708 pub context_budget_tokens: usize,
709 #[serde(default = "default_soft_compaction_threshold")]
710 pub soft_compaction_threshold: f32,
711 #[serde(
712 default = "default_hard_compaction_threshold",
713 alias = "compaction_threshold"
714 )]
715 pub hard_compaction_threshold: f32,
716 #[serde(default = "default_compaction_preserve_tail")]
717 pub compaction_preserve_tail: usize,
718 #[serde(default = "default_compaction_cooldown_turns")]
719 pub compaction_cooldown_turns: u8,
720 #[serde(default = "default_auto_budget")]
721 pub auto_budget: bool,
722 #[serde(default = "default_prune_protect_tokens")]
723 pub prune_protect_tokens: usize,
724 #[serde(default = "default_cross_session_score_threshold")]
725 pub cross_session_score_threshold: f32,
726 #[serde(default)]
727 pub vector_backend: VectorBackend,
728 #[serde(default = "default_token_safety_margin")]
729 pub token_safety_margin: f32,
730 #[serde(default = "default_redact_credentials")]
731 pub redact_credentials: bool,
732 #[serde(default = "default_true")]
733 pub autosave_assistant: bool,
734 #[serde(default = "default_autosave_min_length")]
735 pub autosave_min_length: usize,
736 #[serde(default = "default_tool_call_cutoff")]
737 pub tool_call_cutoff: usize,
738 #[serde(default = "default_sqlite_pool_size")]
739 pub sqlite_pool_size: u32,
740 #[serde(default)]
741 pub sessions: SessionsConfig,
742 #[serde(default)]
743 pub documents: DocumentConfig,
744 #[serde(default)]
745 pub eviction: zeph_memory::EvictionConfig,
746 #[serde(default)]
747 pub compression: CompressionConfig,
748 #[serde(default)]
749 pub sidequest: SidequestConfig,
750 #[serde(default)]
751 pub graph: GraphConfig,
752 #[serde(default = "default_shutdown_summary")]
756 pub shutdown_summary: bool,
757 #[serde(default = "default_shutdown_summary_min_messages")]
760 pub shutdown_summary_min_messages: usize,
761 #[serde(default = "default_shutdown_summary_max_messages")]
765 pub shutdown_summary_max_messages: usize,
766 #[serde(default = "default_shutdown_summary_timeout_secs")]
770 pub shutdown_summary_timeout_secs: u64,
771 #[serde(default)]
777 pub structured_summaries: bool,
778 #[serde(default)]
783 pub tiers: TierConfig,
784 #[serde(default)]
789 pub admission: AdmissionConfig,
790 #[serde(default)]
792 pub digest: DigestConfig,
793 #[serde(default)]
795 pub context_strategy: ContextStrategy,
796 #[serde(default = "default_crossover_turn_threshold")]
798 pub crossover_turn_threshold: u32,
799 #[serde(default)]
804 pub consolidation: ConsolidationConfig,
805 #[serde(default)]
810 pub forgetting: ForgettingConfig,
811 #[serde(default)]
818 pub database_url: Option<String>,
819 #[serde(default)]
824 pub store_routing: StoreRoutingConfig,
825 #[serde(default)]
830 pub persona: PersonaConfig,
831 #[serde(default)]
833 pub trajectory: TrajectoryConfig,
834 #[serde(default)]
836 pub category: CategoryConfig,
837 #[serde(default)]
839 pub tree: TreeConfig,
840 #[serde(default)]
845 pub microcompact: MicrocompactConfig,
846 #[serde(default)]
851 pub autodream: AutoDreamConfig,
852 #[serde(default = "default_key_facts_dedup_threshold")]
859 pub key_facts_dedup_threshold: f32,
860}
861
862fn default_crossover_turn_threshold() -> u32 {
863 20
864}
865
866fn default_key_facts_dedup_threshold() -> f32 {
867 0.95
868}
869
870#[derive(Debug, Clone, Deserialize, Serialize)]
872#[serde(default)]
873pub struct DigestConfig {
874 pub enabled: bool,
876 pub provider: String,
879 pub max_tokens: usize,
881 pub max_input_messages: usize,
883}
884
885impl Default for DigestConfig {
886 fn default() -> Self {
887 Self {
888 enabled: false,
889 provider: String::new(),
890 max_tokens: 500,
891 max_input_messages: 50,
892 }
893 }
894}
895
896#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
898#[serde(rename_all = "snake_case")]
899pub enum ContextStrategy {
900 #[default]
903 FullHistory,
904 MemoryFirst,
907 Adaptive,
910}
911
912#[derive(Debug, Clone, Deserialize, Serialize)]
914#[serde(default)]
915pub struct SessionsConfig {
916 #[serde(default = "default_max_history")]
918 pub max_history: usize,
919 #[serde(default = "default_title_max_chars")]
921 pub title_max_chars: usize,
922}
923
924impl Default for SessionsConfig {
925 fn default() -> Self {
926 Self {
927 max_history: default_max_history(),
928 title_max_chars: default_title_max_chars(),
929 }
930 }
931}
932
933#[derive(Debug, Clone, Deserialize, Serialize)]
935pub struct DocumentConfig {
936 #[serde(default = "default_document_collection")]
937 pub collection: String,
938 #[serde(default = "default_document_chunk_size")]
939 pub chunk_size: usize,
940 #[serde(default = "default_document_chunk_overlap")]
941 pub chunk_overlap: usize,
942 #[serde(default = "default_document_top_k")]
944 pub top_k: usize,
945 #[serde(default)]
947 pub rag_enabled: bool,
948}
949
950impl Default for DocumentConfig {
951 fn default() -> Self {
952 Self {
953 collection: default_document_collection(),
954 chunk_size: default_document_chunk_size(),
955 chunk_overlap: default_document_chunk_overlap(),
956 top_k: default_document_top_k(),
957 rag_enabled: false,
958 }
959 }
960}
961
962#[derive(Debug, Deserialize, Serialize)]
978#[allow(clippy::struct_excessive_bools)]
979pub struct SemanticConfig {
980 #[serde(default = "default_semantic_enabled")]
982 pub enabled: bool,
983 #[serde(default = "default_recall_limit")]
984 pub recall_limit: usize,
985 #[serde(default = "default_vector_weight")]
986 pub vector_weight: f64,
987 #[serde(default = "default_keyword_weight")]
988 pub keyword_weight: f64,
989 #[serde(default = "default_true")]
990 pub temporal_decay_enabled: bool,
991 #[serde(default = "default_temporal_decay_half_life_days")]
992 pub temporal_decay_half_life_days: u32,
993 #[serde(default = "default_true")]
994 pub mmr_enabled: bool,
995 #[serde(default = "default_mmr_lambda")]
996 pub mmr_lambda: f32,
997 #[serde(default = "default_true")]
998 pub importance_enabled: bool,
999 #[serde(
1000 default = "default_importance_weight",
1001 deserialize_with = "validate_importance_weight"
1002 )]
1003 pub importance_weight: f64,
1004 #[serde(default)]
1009 pub embed_provider: Option<String>,
1010}
1011
1012impl Default for SemanticConfig {
1013 fn default() -> Self {
1014 Self {
1015 enabled: default_semantic_enabled(),
1016 recall_limit: default_recall_limit(),
1017 vector_weight: default_vector_weight(),
1018 keyword_weight: default_keyword_weight(),
1019 temporal_decay_enabled: true,
1020 temporal_decay_half_life_days: default_temporal_decay_half_life_days(),
1021 mmr_enabled: true,
1022 mmr_lambda: default_mmr_lambda(),
1023 importance_enabled: true,
1024 importance_weight: default_importance_weight(),
1025 embed_provider: None,
1026 }
1027 }
1028}
1029
1030#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
1032#[serde(tag = "strategy", rename_all = "snake_case")]
1033pub enum CompressionStrategy {
1034 #[default]
1036 Reactive,
1037 Proactive {
1039 threshold_tokens: usize,
1041 max_summary_tokens: usize,
1043 },
1044 Autonomous,
1047 Focus,
1052}
1053
1054#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
1059#[serde(rename_all = "snake_case")]
1060pub enum PruningStrategy {
1061 #[default]
1063 Reactive,
1064 TaskAware,
1067 Mig,
1070 Subgoal,
1074 SubgoalMig,
1077}
1078
1079impl PruningStrategy {
1080 #[must_use]
1082 pub fn is_subgoal(self) -> bool {
1083 matches!(self, Self::Subgoal | Self::SubgoalMig)
1084 }
1085}
1086
1087impl<'de> serde::Deserialize<'de> for PruningStrategy {
1090 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1091 let s = String::deserialize(deserializer)?;
1092 s.parse().map_err(serde::de::Error::custom)
1093 }
1094}
1095
1096impl std::str::FromStr for PruningStrategy {
1097 type Err = String;
1098
1099 fn from_str(s: &str) -> Result<Self, Self::Err> {
1100 match s {
1101 "reactive" => Ok(Self::Reactive),
1102 "task_aware" | "task-aware" => Ok(Self::TaskAware),
1103 "mig" => Ok(Self::Mig),
1104 "task_aware_mig" | "task-aware-mig" => {
1107 tracing::warn!(
1108 "pruning strategy `task_aware_mig` has been removed; \
1109 falling back to `reactive`. Use `task_aware` or `mig` instead."
1110 );
1111 Ok(Self::Reactive)
1112 }
1113 "subgoal" => Ok(Self::Subgoal),
1114 "subgoal_mig" | "subgoal-mig" => Ok(Self::SubgoalMig),
1115 other => Err(format!(
1116 "unknown pruning strategy `{other}`, expected \
1117 reactive|task_aware|mig|subgoal|subgoal_mig"
1118 )),
1119 }
1120 }
1121}
1122
1123fn default_high_density_budget() -> f32 {
1124 0.7
1125}
1126
1127fn default_low_density_budget() -> f32 {
1128 0.3
1129}
1130
1131#[derive(Debug, Clone, Deserialize, Serialize)]
1138#[serde(default)]
1139pub struct CompressionPredictorConfig {
1140 pub enabled: bool,
1142 pub min_samples: u64,
1144 pub candidate_ratios: Vec<f32>,
1147 pub retrain_interval: u64,
1149 pub max_training_samples: usize,
1151}
1152
1153impl Default for CompressionPredictorConfig {
1154 fn default() -> Self {
1155 Self {
1156 enabled: false,
1157 min_samples: 10,
1158 candidate_ratios: vec![0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
1159 retrain_interval: 5,
1160 max_training_samples: 200,
1161 }
1162 }
1163}
1164
1165#[derive(Debug, Clone, Deserialize, Serialize)]
1171#[serde(default)]
1172pub struct ForgettingConfig {
1173 pub enabled: bool,
1175 pub decay_rate: f32,
1177 pub forgetting_floor: f32,
1179 pub sweep_interval_secs: u64,
1181 pub sweep_batch_size: usize,
1183 pub replay_window_hours: u32,
1185 pub replay_min_access_count: u32,
1187 pub protect_recent_hours: u32,
1189 pub protect_min_access_count: u32,
1191}
1192
1193impl Default for ForgettingConfig {
1194 fn default() -> Self {
1195 Self {
1196 enabled: false,
1197 decay_rate: 0.1,
1198 forgetting_floor: 0.05,
1199 sweep_interval_secs: 7200,
1200 sweep_batch_size: 500,
1201 replay_window_hours: 24,
1202 replay_min_access_count: 3,
1203 protect_recent_hours: 24,
1204 protect_min_access_count: 3,
1205 }
1206 }
1207}
1208
1209#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1211#[serde(default)]
1212pub struct CompressionConfig {
1213 #[serde(flatten)]
1215 pub strategy: CompressionStrategy,
1216 pub pruning_strategy: PruningStrategy,
1218 pub model: String,
1223 pub compress_provider: ProviderName,
1226 #[serde(default)]
1228 pub probe: zeph_memory::CompactionProbeConfig,
1229 #[serde(default)]
1237 pub archive_tool_outputs: bool,
1238 pub focus_scorer_provider: ProviderName,
1241 #[serde(default = "default_high_density_budget")]
1244 pub high_density_budget: f32,
1245 #[serde(default = "default_low_density_budget")]
1248 pub low_density_budget: f32,
1249 #[serde(default)]
1251 pub predictor: CompressionPredictorConfig,
1252}
1253
1254fn default_sidequest_interval_turns() -> u32 {
1255 4
1256}
1257
1258fn default_sidequest_max_eviction_ratio() -> f32 {
1259 0.5
1260}
1261
1262fn default_sidequest_max_cursors() -> usize {
1263 30
1264}
1265
1266fn default_sidequest_min_cursor_tokens() -> usize {
1267 100
1268}
1269
1270#[derive(Debug, Clone, Deserialize, Serialize)]
1272#[serde(default)]
1273pub struct SidequestConfig {
1274 pub enabled: bool,
1276 #[serde(default = "default_sidequest_interval_turns")]
1278 pub interval_turns: u32,
1279 #[serde(default = "default_sidequest_max_eviction_ratio")]
1281 pub max_eviction_ratio: f32,
1282 #[serde(default = "default_sidequest_max_cursors")]
1284 pub max_cursors: usize,
1285 #[serde(default = "default_sidequest_min_cursor_tokens")]
1288 pub min_cursor_tokens: usize,
1289}
1290
1291impl Default for SidequestConfig {
1292 fn default() -> Self {
1293 Self {
1294 enabled: false,
1295 interval_turns: default_sidequest_interval_turns(),
1296 max_eviction_ratio: default_sidequest_max_eviction_ratio(),
1297 max_cursors: default_sidequest_max_cursors(),
1298 min_cursor_tokens: default_sidequest_min_cursor_tokens(),
1299 }
1300 }
1301}
1302
1303#[derive(Debug, Clone, Deserialize, Serialize)]
1312#[serde(default)]
1313pub struct GraphConfig {
1314 pub enabled: bool,
1315 pub extract_model: String,
1316 #[serde(default = "default_graph_max_entities_per_message")]
1317 pub max_entities_per_message: usize,
1318 #[serde(default = "default_graph_max_edges_per_message")]
1319 pub max_edges_per_message: usize,
1320 #[serde(default = "default_graph_community_refresh_interval")]
1321 pub community_refresh_interval: usize,
1322 #[serde(default = "default_graph_entity_similarity_threshold")]
1323 pub entity_similarity_threshold: f32,
1324 #[serde(default = "default_graph_extraction_timeout_secs")]
1325 pub extraction_timeout_secs: u64,
1326 #[serde(default)]
1327 pub use_embedding_resolution: bool,
1328 #[serde(default = "default_graph_entity_ambiguous_threshold")]
1329 pub entity_ambiguous_threshold: f32,
1330 #[serde(default = "default_graph_max_hops")]
1331 pub max_hops: u32,
1332 #[serde(default = "default_graph_recall_limit")]
1333 pub recall_limit: usize,
1334 #[serde(default = "default_graph_expired_edge_retention_days")]
1336 pub expired_edge_retention_days: u32,
1337 #[serde(default)]
1339 pub max_entities: usize,
1340 #[serde(default = "default_graph_community_summary_max_prompt_bytes")]
1342 pub community_summary_max_prompt_bytes: usize,
1343 #[serde(default = "default_graph_community_summary_concurrency")]
1345 pub community_summary_concurrency: usize,
1346 #[serde(default = "default_lpa_edge_chunk_size")]
1349 pub lpa_edge_chunk_size: usize,
1350 #[serde(
1356 default = "default_graph_temporal_decay_rate",
1357 deserialize_with = "validate_temporal_decay_rate"
1358 )]
1359 pub temporal_decay_rate: f64,
1360 #[serde(default = "default_graph_edge_history_limit")]
1366 pub edge_history_limit: usize,
1367 #[serde(default)]
1373 pub note_linking: NoteLinkingConfig,
1374 #[serde(default)]
1379 pub spreading_activation: SpreadingActivationConfig,
1380 #[serde(
1383 default = "default_link_weight_decay_lambda",
1384 deserialize_with = "validate_link_weight_decay_lambda"
1385 )]
1386 pub link_weight_decay_lambda: f64,
1387 #[serde(default = "default_link_weight_decay_interval_secs")]
1389 pub link_weight_decay_interval_secs: u64,
1390 #[serde(default)]
1396 pub belief_revision: BeliefRevisionConfig,
1397 #[serde(default)]
1402 pub rpe: RpeConfig,
1403 #[serde(default = "default_graph_pool_size")]
1409 pub pool_size: u32,
1410}
1411
1412fn default_graph_pool_size() -> u32 {
1413 3
1414}
1415
1416impl Default for GraphConfig {
1417 fn default() -> Self {
1418 Self {
1419 enabled: false,
1420 extract_model: String::new(),
1421 max_entities_per_message: default_graph_max_entities_per_message(),
1422 max_edges_per_message: default_graph_max_edges_per_message(),
1423 community_refresh_interval: default_graph_community_refresh_interval(),
1424 entity_similarity_threshold: default_graph_entity_similarity_threshold(),
1425 extraction_timeout_secs: default_graph_extraction_timeout_secs(),
1426 use_embedding_resolution: false,
1427 entity_ambiguous_threshold: default_graph_entity_ambiguous_threshold(),
1428 max_hops: default_graph_max_hops(),
1429 recall_limit: default_graph_recall_limit(),
1430 expired_edge_retention_days: default_graph_expired_edge_retention_days(),
1431 max_entities: 0,
1432 community_summary_max_prompt_bytes: default_graph_community_summary_max_prompt_bytes(),
1433 community_summary_concurrency: default_graph_community_summary_concurrency(),
1434 lpa_edge_chunk_size: default_lpa_edge_chunk_size(),
1435 temporal_decay_rate: default_graph_temporal_decay_rate(),
1436 edge_history_limit: default_graph_edge_history_limit(),
1437 note_linking: NoteLinkingConfig::default(),
1438 spreading_activation: SpreadingActivationConfig::default(),
1439 link_weight_decay_lambda: default_link_weight_decay_lambda(),
1440 link_weight_decay_interval_secs: default_link_weight_decay_interval_secs(),
1441 belief_revision: BeliefRevisionConfig::default(),
1442 rpe: RpeConfig::default(),
1443 pool_size: default_graph_pool_size(),
1444 }
1445 }
1446}
1447
1448fn default_consolidation_confidence_threshold() -> f32 {
1449 0.7
1450}
1451
1452fn default_consolidation_sweep_interval_secs() -> u64 {
1453 3600
1454}
1455
1456fn default_consolidation_sweep_batch_size() -> usize {
1457 50
1458}
1459
1460fn default_consolidation_similarity_threshold() -> f32 {
1461 0.85
1462}
1463
1464#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1470#[serde(default)]
1471pub struct ConsolidationConfig {
1472 pub enabled: bool,
1474 #[serde(default)]
1477 pub consolidation_provider: ProviderName,
1478 #[serde(default = "default_consolidation_confidence_threshold")]
1480 pub confidence_threshold: f32,
1481 #[serde(default = "default_consolidation_sweep_interval_secs")]
1483 pub sweep_interval_secs: u64,
1484 #[serde(default = "default_consolidation_sweep_batch_size")]
1486 pub sweep_batch_size: usize,
1487 #[serde(default = "default_consolidation_similarity_threshold")]
1490 pub similarity_threshold: f32,
1491}
1492
1493impl Default for ConsolidationConfig {
1494 fn default() -> Self {
1495 Self {
1496 enabled: false,
1497 consolidation_provider: ProviderName::default(),
1498 confidence_threshold: default_consolidation_confidence_threshold(),
1499 sweep_interval_secs: default_consolidation_sweep_interval_secs(),
1500 sweep_batch_size: default_consolidation_sweep_batch_size(),
1501 similarity_threshold: default_consolidation_similarity_threshold(),
1502 }
1503 }
1504}
1505
1506fn default_link_weight_decay_lambda() -> f64 {
1507 0.95
1508}
1509
1510fn default_link_weight_decay_interval_secs() -> u64 {
1511 86400
1512}
1513
1514fn validate_link_weight_decay_lambda<'de, D>(deserializer: D) -> Result<f64, D::Error>
1515where
1516 D: serde::Deserializer<'de>,
1517{
1518 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
1519 if value.is_nan() || value.is_infinite() {
1520 return Err(serde::de::Error::custom(
1521 "link_weight_decay_lambda must be a finite number",
1522 ));
1523 }
1524 if !(value > 0.0 && value <= 1.0) {
1525 return Err(serde::de::Error::custom(
1526 "link_weight_decay_lambda must be in (0.0, 1.0]",
1527 ));
1528 }
1529 Ok(value)
1530}
1531
1532fn validate_admission_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
1533where
1534 D: serde::Deserializer<'de>,
1535{
1536 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1537 if value.is_nan() || value.is_infinite() {
1538 return Err(serde::de::Error::custom(
1539 "threshold must be a finite number",
1540 ));
1541 }
1542 if !(0.0..=1.0).contains(&value) {
1543 return Err(serde::de::Error::custom("threshold must be in [0.0, 1.0]"));
1544 }
1545 Ok(value)
1546}
1547
1548fn validate_admission_fast_path_margin<'de, D>(deserializer: D) -> Result<f32, D::Error>
1549where
1550 D: serde::Deserializer<'de>,
1551{
1552 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1553 if value.is_nan() || value.is_infinite() {
1554 return Err(serde::de::Error::custom(
1555 "fast_path_margin must be a finite number",
1556 ));
1557 }
1558 if !(0.0..=1.0).contains(&value) {
1559 return Err(serde::de::Error::custom(
1560 "fast_path_margin must be in [0.0, 1.0]",
1561 ));
1562 }
1563 Ok(value)
1564}
1565
1566fn default_admission_threshold() -> f32 {
1567 0.40
1568}
1569
1570fn default_admission_fast_path_margin() -> f32 {
1571 0.15
1572}
1573
1574fn default_rl_min_samples() -> u32 {
1575 500
1576}
1577
1578fn default_rl_retrain_interval_secs() -> u64 {
1579 3600
1580}
1581
1582#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1587#[serde(rename_all = "snake_case")]
1588pub enum AdmissionStrategy {
1589 #[default]
1591 Heuristic,
1592 Rl,
1595}
1596
1597fn validate_admission_weight<'de, D>(deserializer: D) -> Result<f32, D::Error>
1598where
1599 D: serde::Deserializer<'de>,
1600{
1601 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1602 if value < 0.0 {
1603 return Err(serde::de::Error::custom(
1604 "admission weight must be non-negative (>= 0.0)",
1605 ));
1606 }
1607 Ok(value)
1608}
1609
1610#[derive(Debug, Clone, Deserialize, Serialize)]
1615#[serde(default)]
1616pub struct AdmissionWeights {
1617 #[serde(deserialize_with = "validate_admission_weight")]
1619 pub future_utility: f32,
1620 #[serde(deserialize_with = "validate_admission_weight")]
1622 pub factual_confidence: f32,
1623 #[serde(deserialize_with = "validate_admission_weight")]
1625 pub semantic_novelty: f32,
1626 #[serde(deserialize_with = "validate_admission_weight")]
1628 pub temporal_recency: f32,
1629 #[serde(deserialize_with = "validate_admission_weight")]
1631 pub content_type_prior: f32,
1632 #[serde(deserialize_with = "validate_admission_weight")]
1636 pub goal_utility: f32,
1637}
1638
1639impl Default for AdmissionWeights {
1640 fn default() -> Self {
1641 Self {
1642 future_utility: 0.30,
1643 factual_confidence: 0.15,
1644 semantic_novelty: 0.30,
1645 temporal_recency: 0.10,
1646 content_type_prior: 0.15,
1647 goal_utility: 0.0,
1648 }
1649 }
1650}
1651
1652impl AdmissionWeights {
1653 #[must_use]
1657 pub fn normalized(&self) -> Self {
1658 let sum = self.future_utility
1659 + self.factual_confidence
1660 + self.semantic_novelty
1661 + self.temporal_recency
1662 + self.content_type_prior
1663 + self.goal_utility;
1664 if sum <= f32::EPSILON {
1665 return Self::default();
1666 }
1667 Self {
1668 future_utility: self.future_utility / sum,
1669 factual_confidence: self.factual_confidence / sum,
1670 semantic_novelty: self.semantic_novelty / sum,
1671 temporal_recency: self.temporal_recency / sum,
1672 content_type_prior: self.content_type_prior / sum,
1673 goal_utility: self.goal_utility / sum,
1674 }
1675 }
1676}
1677
1678#[derive(Debug, Clone, Deserialize, Serialize)]
1683#[serde(default)]
1684pub struct AdmissionConfig {
1685 pub enabled: bool,
1687 #[serde(deserialize_with = "validate_admission_threshold")]
1690 pub threshold: f32,
1691 #[serde(deserialize_with = "validate_admission_fast_path_margin")]
1694 pub fast_path_margin: f32,
1695 pub admission_provider: ProviderName,
1698 pub weights: AdmissionWeights,
1700 #[serde(default)]
1702 pub admission_strategy: AdmissionStrategy,
1703 #[serde(default = "default_rl_min_samples")]
1706 pub rl_min_samples: u32,
1707 #[serde(default = "default_rl_retrain_interval_secs")]
1709 pub rl_retrain_interval_secs: u64,
1710 #[serde(default)]
1714 pub goal_conditioned_write: bool,
1715 #[serde(default)]
1719 pub goal_utility_provider: ProviderName,
1720 #[serde(default = "default_goal_utility_threshold")]
1723 pub goal_utility_threshold: f32,
1724 #[serde(default = "default_goal_utility_weight")]
1727 pub goal_utility_weight: f32,
1728}
1729
1730fn default_goal_utility_threshold() -> f32 {
1731 0.4
1732}
1733
1734fn default_goal_utility_weight() -> f32 {
1735 0.25
1736}
1737
1738impl Default for AdmissionConfig {
1739 fn default() -> Self {
1740 Self {
1741 enabled: false,
1742 threshold: default_admission_threshold(),
1743 fast_path_margin: default_admission_fast_path_margin(),
1744 admission_provider: ProviderName::default(),
1745 weights: AdmissionWeights::default(),
1746 admission_strategy: AdmissionStrategy::default(),
1747 rl_min_samples: default_rl_min_samples(),
1748 rl_retrain_interval_secs: default_rl_retrain_interval_secs(),
1749 goal_conditioned_write: false,
1750 goal_utility_provider: ProviderName::default(),
1751 goal_utility_threshold: default_goal_utility_threshold(),
1752 goal_utility_weight: default_goal_utility_weight(),
1753 }
1754 }
1755}
1756
1757#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
1759#[serde(rename_all = "snake_case")]
1760pub enum StoreRoutingStrategy {
1761 #[default]
1763 Heuristic,
1764 Llm,
1766 Hybrid,
1768}
1769
1770#[derive(Debug, Clone, Deserialize, Serialize)]
1775#[serde(default)]
1776pub struct StoreRoutingConfig {
1777 pub enabled: bool,
1780 pub strategy: StoreRoutingStrategy,
1782 pub routing_classifier_provider: ProviderName,
1785 pub fallback_route: String,
1788 pub confidence_threshold: f32,
1791}
1792
1793impl Default for StoreRoutingConfig {
1794 fn default() -> Self {
1795 Self {
1796 enabled: false,
1797 strategy: StoreRoutingStrategy::Heuristic,
1798 routing_classifier_provider: ProviderName::default(),
1799 fallback_route: "hybrid".into(),
1800 confidence_threshold: 0.7,
1801 }
1802 }
1803}
1804
1805#[derive(Debug, Clone, Deserialize, Serialize)]
1810#[serde(default)]
1811pub struct PersonaConfig {
1812 pub enabled: bool,
1814 pub persona_provider: ProviderName,
1817 pub min_confidence: f64,
1819 pub min_messages: usize,
1821 pub max_messages: usize,
1823 pub extraction_timeout_secs: u64,
1825 pub context_budget_tokens: usize,
1827}
1828
1829impl Default for PersonaConfig {
1830 fn default() -> Self {
1831 Self {
1832 enabled: false,
1833 persona_provider: ProviderName::default(),
1834 min_confidence: 0.6,
1835 min_messages: 3,
1836 max_messages: 10,
1837 extraction_timeout_secs: 10,
1838 context_budget_tokens: 500,
1839 }
1840 }
1841}
1842
1843#[derive(Debug, Clone, Deserialize, Serialize)]
1849#[serde(default)]
1850pub struct TrajectoryConfig {
1851 pub enabled: bool,
1853 pub trajectory_provider: ProviderName,
1856 pub context_budget_tokens: usize,
1858 pub max_messages: usize,
1860 pub extraction_timeout_secs: u64,
1862 pub recall_top_k: usize,
1864 pub min_confidence: f64,
1866}
1867
1868impl Default for TrajectoryConfig {
1869 fn default() -> Self {
1870 Self {
1871 enabled: false,
1872 trajectory_provider: ProviderName::default(),
1873 context_budget_tokens: 400,
1874 max_messages: 10,
1875 extraction_timeout_secs: 10,
1876 recall_top_k: 5,
1877 min_confidence: 0.6,
1878 }
1879 }
1880}
1881
1882#[derive(Debug, Clone, Deserialize, Serialize)]
1888#[serde(default)]
1889pub struct CategoryConfig {
1890 pub enabled: bool,
1892 pub auto_tag: bool,
1894}
1895
1896impl Default for CategoryConfig {
1897 fn default() -> Self {
1898 Self {
1899 enabled: false,
1900 auto_tag: true,
1901 }
1902 }
1903}
1904
1905#[derive(Debug, Clone, Deserialize, Serialize)]
1911#[serde(default)]
1912pub struct TreeConfig {
1913 pub enabled: bool,
1915 pub consolidation_provider: ProviderName,
1918 pub sweep_interval_secs: u64,
1920 pub batch_size: usize,
1922 pub similarity_threshold: f32,
1924 pub max_level: u32,
1926 pub context_budget_tokens: usize,
1928 pub recall_top_k: usize,
1930 pub min_cluster_size: usize,
1932}
1933
1934impl Default for TreeConfig {
1935 fn default() -> Self {
1936 Self {
1937 enabled: false,
1938 consolidation_provider: ProviderName::default(),
1939 sweep_interval_secs: 300,
1940 batch_size: 20,
1941 similarity_threshold: 0.8,
1942 max_level: 3,
1943 context_budget_tokens: 400,
1944 recall_top_k: 5,
1945 min_cluster_size: 2,
1946 }
1947 }
1948}
1949
1950#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
1956#[serde(default)]
1957pub struct MicrocompactConfig {
1958 pub enabled: bool,
1960 pub gap_threshold_minutes: u32,
1962 pub keep_recent: usize,
1964}
1965
1966impl Default for MicrocompactConfig {
1967 fn default() -> Self {
1968 Self {
1969 enabled: false,
1970 gap_threshold_minutes: 60,
1971 keep_recent: 3,
1972 }
1973 }
1974}
1975
1976#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
1981#[serde(default)]
1982pub struct AutoDreamConfig {
1983 pub enabled: bool,
1985 pub min_sessions: u32,
1987 pub min_hours: u32,
1989 pub consolidation_provider: ProviderName,
1992 pub max_iterations: u8,
1994}
1995
1996impl Default for AutoDreamConfig {
1997 fn default() -> Self {
1998 Self {
1999 enabled: false,
2000 min_sessions: 3,
2001 min_hours: 24,
2002 consolidation_provider: ProviderName::default(),
2003 max_iterations: 8,
2004 }
2005 }
2006}
2007
2008#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
2013#[serde(default)]
2014pub struct MagicDocsConfig {
2015 pub enabled: bool,
2017 pub min_turns_between_updates: u32,
2019 pub update_provider: ProviderName,
2022 pub max_iterations: u8,
2024}
2025
2026impl Default for MagicDocsConfig {
2027 fn default() -> Self {
2028 Self {
2029 enabled: false,
2030 min_turns_between_updates: 5,
2031 update_provider: ProviderName::default(),
2032 max_iterations: 4,
2033 }
2034 }
2035}
2036
2037#[cfg(test)]
2038mod tests {
2039 use super::*;
2040
2041 #[test]
2044 fn pruning_strategy_toml_task_aware_mig_falls_back_to_reactive() {
2045 #[derive(serde::Deserialize)]
2046 struct Wrapper {
2047 #[allow(dead_code)]
2048 pruning_strategy: PruningStrategy,
2049 }
2050 let toml = r#"pruning_strategy = "task_aware_mig""#;
2051 let w: Wrapper = toml::from_str(toml).expect("should deserialize without error");
2052 assert_eq!(
2053 w.pruning_strategy,
2054 PruningStrategy::Reactive,
2055 "task_aware_mig must fall back to Reactive"
2056 );
2057 }
2058
2059 #[test]
2060 fn pruning_strategy_toml_round_trip() {
2061 #[derive(serde::Deserialize)]
2062 struct Wrapper {
2063 #[allow(dead_code)]
2064 pruning_strategy: PruningStrategy,
2065 }
2066 for (input, expected) in [
2067 ("reactive", PruningStrategy::Reactive),
2068 ("task_aware", PruningStrategy::TaskAware),
2069 ("mig", PruningStrategy::Mig),
2070 ] {
2071 let toml = format!(r#"pruning_strategy = "{input}""#);
2072 let w: Wrapper = toml::from_str(&toml)
2073 .unwrap_or_else(|e| panic!("failed to deserialize `{input}`: {e}"));
2074 assert_eq!(w.pruning_strategy, expected, "mismatch for `{input}`");
2075 }
2076 }
2077
2078 #[test]
2079 fn pruning_strategy_toml_unknown_value_errors() {
2080 #[derive(serde::Deserialize)]
2081 #[allow(dead_code)]
2082 struct Wrapper {
2083 pruning_strategy: PruningStrategy,
2084 }
2085 let toml = r#"pruning_strategy = "nonexistent_strategy""#;
2086 assert!(
2087 toml::from_str::<Wrapper>(toml).is_err(),
2088 "unknown strategy must produce an error"
2089 );
2090 }
2091
2092 #[test]
2093 fn tier_config_defaults_are_correct() {
2094 let cfg = TierConfig::default();
2095 assert!(!cfg.enabled);
2096 assert_eq!(cfg.promotion_min_sessions, 3);
2097 assert!((cfg.similarity_threshold - 0.92).abs() < f32::EPSILON);
2098 assert_eq!(cfg.sweep_interval_secs, 3600);
2099 assert_eq!(cfg.sweep_batch_size, 100);
2100 }
2101
2102 #[test]
2103 fn tier_config_rejects_min_sessions_below_2() {
2104 let toml = "promotion_min_sessions = 1";
2105 assert!(toml::from_str::<TierConfig>(toml).is_err());
2106 }
2107
2108 #[test]
2109 fn tier_config_rejects_similarity_threshold_below_0_5() {
2110 let toml = "similarity_threshold = 0.4";
2111 assert!(toml::from_str::<TierConfig>(toml).is_err());
2112 }
2113
2114 #[test]
2115 fn tier_config_rejects_zero_sweep_batch_size() {
2116 let toml = "sweep_batch_size = 0";
2117 assert!(toml::from_str::<TierConfig>(toml).is_err());
2118 }
2119
2120 fn deserialize_importance_weight(toml_val: &str) -> Result<SemanticConfig, toml::de::Error> {
2121 let input = format!("importance_weight = {toml_val}");
2122 toml::from_str::<SemanticConfig>(&input)
2123 }
2124
2125 #[test]
2126 fn importance_weight_default_is_0_15() {
2127 let cfg = SemanticConfig::default();
2128 assert!((cfg.importance_weight - 0.15).abs() < f64::EPSILON);
2129 }
2130
2131 #[test]
2132 fn importance_weight_valid_zero() {
2133 let cfg = deserialize_importance_weight("0.0").unwrap();
2134 assert!((cfg.importance_weight - 0.0_f64).abs() < f64::EPSILON);
2135 }
2136
2137 #[test]
2138 fn importance_weight_valid_one() {
2139 let cfg = deserialize_importance_weight("1.0").unwrap();
2140 assert!((cfg.importance_weight - 1.0_f64).abs() < f64::EPSILON);
2141 }
2142
2143 #[test]
2144 fn importance_weight_rejects_near_zero_negative() {
2145 let result = deserialize_importance_weight("-0.01");
2150 assert!(
2151 result.is_err(),
2152 "negative importance_weight must be rejected"
2153 );
2154 }
2155
2156 #[test]
2157 fn importance_weight_rejects_negative() {
2158 let result = deserialize_importance_weight("-1.0");
2159 assert!(result.is_err(), "negative value must be rejected");
2160 }
2161
2162 #[test]
2163 fn importance_weight_rejects_greater_than_one() {
2164 let result = deserialize_importance_weight("1.01");
2165 assert!(result.is_err(), "value > 1.0 must be rejected");
2166 }
2167
2168 #[test]
2172 fn admission_weights_normalized_sums_to_one() {
2173 let w = AdmissionWeights {
2174 future_utility: 2.0,
2175 factual_confidence: 1.0,
2176 semantic_novelty: 3.0,
2177 temporal_recency: 1.0,
2178 content_type_prior: 3.0,
2179 goal_utility: 0.0,
2180 };
2181 let n = w.normalized();
2182 let sum = n.future_utility
2183 + n.factual_confidence
2184 + n.semantic_novelty
2185 + n.temporal_recency
2186 + n.content_type_prior;
2187 assert!(
2188 (sum - 1.0).abs() < 0.001,
2189 "normalized weights must sum to 1.0, got {sum}"
2190 );
2191 }
2192
2193 #[test]
2195 fn admission_weights_normalized_preserves_already_unit_sum() {
2196 let w = AdmissionWeights::default();
2197 let n = w.normalized();
2198 let sum = n.future_utility
2199 + n.factual_confidence
2200 + n.semantic_novelty
2201 + n.temporal_recency
2202 + n.content_type_prior;
2203 assert!(
2204 (sum - 1.0).abs() < 0.001,
2205 "default weights sum to ~1.0 after normalization"
2206 );
2207 }
2208
2209 #[test]
2211 fn admission_weights_normalized_zero_sum_falls_back_to_default() {
2212 let w = AdmissionWeights {
2213 future_utility: 0.0,
2214 factual_confidence: 0.0,
2215 semantic_novelty: 0.0,
2216 temporal_recency: 0.0,
2217 content_type_prior: 0.0,
2218 goal_utility: 0.0,
2219 };
2220 let n = w.normalized();
2221 let default = AdmissionWeights::default();
2222 assert!(
2223 (n.future_utility - default.future_utility).abs() < 0.001,
2224 "zero-sum weights must fall back to defaults"
2225 );
2226 }
2227
2228 #[test]
2230 fn admission_config_defaults() {
2231 let cfg = AdmissionConfig::default();
2232 assert!(!cfg.enabled);
2233 assert!((cfg.threshold - 0.40).abs() < 0.001);
2234 assert!((cfg.fast_path_margin - 0.15).abs() < 0.001);
2235 assert!(cfg.admission_provider.is_empty());
2236 }
2237
2238 #[test]
2241 fn spreading_activation_default_recall_timeout_ms_is_1000() {
2242 let cfg = SpreadingActivationConfig::default();
2243 assert_eq!(
2244 cfg.recall_timeout_ms, 1000,
2245 "default recall_timeout_ms must be 1000ms"
2246 );
2247 }
2248
2249 #[test]
2250 fn spreading_activation_toml_recall_timeout_ms_round_trip() {
2251 #[derive(serde::Deserialize)]
2252 struct Wrapper {
2253 recall_timeout_ms: u64,
2254 }
2255 let toml = "recall_timeout_ms = 500";
2256 let w: Wrapper = toml::from_str(toml).unwrap();
2257 assert_eq!(w.recall_timeout_ms, 500);
2258 }
2259
2260 #[test]
2261 fn spreading_activation_validate_cross_field_constraints() {
2262 let mut cfg = SpreadingActivationConfig::default();
2263 assert!(cfg.validate().is_ok());
2265
2266 cfg.activation_threshold = 0.5;
2268 cfg.inhibition_threshold = 0.5;
2269 assert!(cfg.validate().is_err());
2270 }
2271
2272 #[test]
2275 fn compression_config_focus_strategy_deserializes() {
2276 let toml = r#"strategy = "focus""#;
2277 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2278 assert_eq!(cfg.strategy, CompressionStrategy::Focus);
2279 }
2280
2281 #[test]
2282 fn compression_config_density_budget_defaults_on_deserialize() {
2283 let toml = r#"strategy = "reactive""#;
2286 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2287 assert!((cfg.high_density_budget - 0.7).abs() < 1e-6);
2288 assert!((cfg.low_density_budget - 0.3).abs() < 1e-6);
2289 }
2290
2291 #[test]
2292 fn compression_config_density_budget_round_trip() {
2293 let toml = "strategy = \"reactive\"\nhigh_density_budget = 0.6\nlow_density_budget = 0.4";
2294 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2295 assert!((cfg.high_density_budget - 0.6).abs() < f32::EPSILON);
2296 assert!((cfg.low_density_budget - 0.4).abs() < f32::EPSILON);
2297 }
2298
2299 #[test]
2300 fn compression_config_focus_scorer_provider_default_empty() {
2301 let cfg = CompressionConfig::default();
2302 assert!(cfg.focus_scorer_provider.is_empty());
2303 }
2304
2305 #[test]
2306 fn compression_config_focus_scorer_provider_round_trip() {
2307 let toml = "strategy = \"focus\"\nfocus_scorer_provider = \"fast\"";
2308 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2309 assert_eq!(cfg.focus_scorer_provider.as_str(), "fast");
2310 }
2311}