1use serde::{Deserialize, Serialize};
5
6use crate::defaults::{default_sqlite_path_field, default_true};
7use crate::providers::ProviderName;
8
9fn default_sqlite_pool_size() -> u32 {
10 5
11}
12
13fn default_max_history() -> usize {
14 100
15}
16
17fn default_title_max_chars() -> usize {
18 60
19}
20
21fn default_document_collection() -> String {
22 "zeph_documents".into()
23}
24
25fn default_document_chunk_size() -> usize {
26 1000
27}
28
29fn default_document_chunk_overlap() -> usize {
30 100
31}
32
33fn default_document_top_k() -> usize {
34 3
35}
36
37fn default_autosave_min_length() -> usize {
38 20
39}
40
41fn default_tool_call_cutoff() -> usize {
42 6
43}
44
45fn default_token_safety_margin() -> f32 {
46 1.0
47}
48
49fn default_redact_credentials() -> bool {
50 true
51}
52
53fn default_qdrant_url() -> String {
54 "http://localhost:6334".into()
55}
56
57fn default_summarization_threshold() -> usize {
58 50
59}
60
61fn default_context_budget_tokens() -> usize {
62 0
63}
64
65fn default_soft_compaction_threshold() -> f32 {
66 0.60
67}
68
69fn default_hard_compaction_threshold() -> f32 {
70 0.90
71}
72
73fn default_compaction_preserve_tail() -> usize {
74 6
75}
76
77fn default_compaction_cooldown_turns() -> u8 {
78 2
79}
80
81fn default_auto_budget() -> bool {
82 true
83}
84
85fn default_prune_protect_tokens() -> usize {
86 40_000
87}
88
89fn default_cross_session_score_threshold() -> f32 {
90 0.35
91}
92
93fn default_temporal_decay_half_life_days() -> u32 {
94 30
95}
96
97fn default_mmr_lambda() -> f32 {
98 0.7
99}
100
101fn default_semantic_enabled() -> bool {
102 true
103}
104
105fn default_recall_limit() -> usize {
106 5
107}
108
109fn default_vector_weight() -> f64 {
110 0.7
111}
112
113fn default_keyword_weight() -> f64 {
114 0.3
115}
116
117fn default_graph_max_entities_per_message() -> usize {
118 10
119}
120
121fn default_graph_max_edges_per_message() -> usize {
122 15
123}
124
125fn default_graph_community_refresh_interval() -> usize {
126 100
127}
128
129fn default_graph_community_summary_max_prompt_bytes() -> usize {
130 8192
131}
132
133fn default_graph_community_summary_concurrency() -> usize {
134 4
135}
136
137fn default_lpa_edge_chunk_size() -> usize {
138 10_000
139}
140
141fn default_graph_entity_similarity_threshold() -> f32 {
142 0.85
143}
144
145fn default_graph_entity_ambiguous_threshold() -> f32 {
146 0.70
147}
148
149fn default_graph_extraction_timeout_secs() -> u64 {
150 15
151}
152
153fn default_graph_max_hops() -> u32 {
154 2
155}
156
157fn default_graph_recall_limit() -> usize {
158 10
159}
160
161fn default_graph_expired_edge_retention_days() -> u32 {
162 90
163}
164
165fn default_graph_temporal_decay_rate() -> f64 {
166 0.0
167}
168
169fn default_graph_edge_history_limit() -> usize {
170 100
171}
172
173fn default_spreading_activation_decay_lambda() -> f32 {
174 0.85
175}
176
177fn default_spreading_activation_max_hops() -> u32 {
178 3
179}
180
181fn default_spreading_activation_activation_threshold() -> f32 {
182 0.1
183}
184
185fn default_spreading_activation_inhibition_threshold() -> f32 {
186 0.8
187}
188
189fn default_spreading_activation_max_activated_nodes() -> usize {
190 50
191}
192
193fn default_spreading_activation_recall_timeout_ms() -> u64 {
194 1000
195}
196
197fn default_note_linking_similarity_threshold() -> f32 {
198 0.85
199}
200
201fn default_note_linking_top_k() -> usize {
202 10
203}
204
205fn default_note_linking_timeout_secs() -> u64 {
206 5
207}
208
209fn default_shutdown_summary() -> bool {
210 true
211}
212
213fn default_shutdown_summary_min_messages() -> usize {
214 4
215}
216
217fn default_shutdown_summary_max_messages() -> usize {
218 20
219}
220
221fn default_shutdown_summary_timeout_secs() -> u64 {
222 10
223}
224
225fn validate_tier_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
226where
227 D: serde::Deserializer<'de>,
228{
229 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
230 if value.is_nan() || value.is_infinite() {
231 return Err(serde::de::Error::custom(
232 "similarity_threshold must be a finite number",
233 ));
234 }
235 if !(0.5..=1.0).contains(&value) {
236 return Err(serde::de::Error::custom(
237 "similarity_threshold must be in [0.5, 1.0]",
238 ));
239 }
240 Ok(value)
241}
242
243fn validate_tier_promotion_min_sessions<'de, D>(deserializer: D) -> Result<u32, D::Error>
244where
245 D: serde::Deserializer<'de>,
246{
247 let value = <u32 as serde::Deserialize>::deserialize(deserializer)?;
248 if value < 2 {
249 return Err(serde::de::Error::custom(
250 "promotion_min_sessions must be >= 2",
251 ));
252 }
253 Ok(value)
254}
255
256fn validate_tier_sweep_batch_size<'de, D>(deserializer: D) -> Result<usize, D::Error>
257where
258 D: serde::Deserializer<'de>,
259{
260 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
261 if value == 0 {
262 return Err(serde::de::Error::custom("sweep_batch_size must be >= 1"));
263 }
264 Ok(value)
265}
266
267fn default_tier_promotion_min_sessions() -> u32 {
268 3
269}
270
271fn default_tier_similarity_threshold() -> f32 {
272 0.92
273}
274
275fn default_tier_sweep_interval_secs() -> u64 {
276 3600
277}
278
279fn default_tier_sweep_batch_size() -> usize {
280 100
281}
282
283fn default_scene_similarity_threshold() -> f32 {
284 0.80
285}
286
287fn default_scene_batch_size() -> usize {
288 50
289}
290
291fn validate_scene_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
292where
293 D: serde::Deserializer<'de>,
294{
295 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
296 if value.is_nan() || value.is_infinite() {
297 return Err(serde::de::Error::custom(
298 "scene_similarity_threshold must be a finite number",
299 ));
300 }
301 if !(0.5..=1.0).contains(&value) {
302 return Err(serde::de::Error::custom(
303 "scene_similarity_threshold must be in [0.5, 1.0]",
304 ));
305 }
306 Ok(value)
307}
308
309fn validate_scene_batch_size<'de, D>(deserializer: D) -> Result<usize, D::Error>
310where
311 D: serde::Deserializer<'de>,
312{
313 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
314 if value == 0 {
315 return Err(serde::de::Error::custom("scene_batch_size must be >= 1"));
316 }
317 Ok(value)
318}
319
320#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
334#[serde(default)]
335pub struct TierConfig {
336 pub enabled: bool,
339 #[serde(deserialize_with = "validate_tier_promotion_min_sessions")]
342 pub promotion_min_sessions: u32,
343 #[serde(deserialize_with = "validate_tier_similarity_threshold")]
346 pub similarity_threshold: f32,
347 pub sweep_interval_secs: u64,
349 #[serde(deserialize_with = "validate_tier_sweep_batch_size")]
351 pub sweep_batch_size: usize,
352 pub scene_enabled: bool,
354 #[serde(deserialize_with = "validate_scene_similarity_threshold")]
356 pub scene_similarity_threshold: f32,
357 #[serde(deserialize_with = "validate_scene_batch_size")]
359 pub scene_batch_size: usize,
360 pub scene_provider: ProviderName,
363 pub scene_sweep_interval_secs: u64,
365}
366
367fn default_scene_sweep_interval_secs() -> u64 {
368 7200
369}
370
371impl Default for TierConfig {
372 fn default() -> Self {
373 Self {
374 enabled: false,
375 promotion_min_sessions: default_tier_promotion_min_sessions(),
376 similarity_threshold: default_tier_similarity_threshold(),
377 sweep_interval_secs: default_tier_sweep_interval_secs(),
378 sweep_batch_size: default_tier_sweep_batch_size(),
379 scene_enabled: false,
380 scene_similarity_threshold: default_scene_similarity_threshold(),
381 scene_batch_size: default_scene_batch_size(),
382 scene_provider: ProviderName::default(),
383 scene_sweep_interval_secs: default_scene_sweep_interval_secs(),
384 }
385 }
386}
387
388fn validate_temporal_decay_rate<'de, D>(deserializer: D) -> Result<f64, D::Error>
389where
390 D: serde::Deserializer<'de>,
391{
392 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
393 if value.is_nan() || value.is_infinite() {
394 return Err(serde::de::Error::custom(
395 "temporal_decay_rate must be a finite number",
396 ));
397 }
398 if !(0.0..=10.0).contains(&value) {
399 return Err(serde::de::Error::custom(
400 "temporal_decay_rate must be in [0.0, 10.0]",
401 ));
402 }
403 Ok(value)
404}
405
406fn validate_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
407where
408 D: serde::Deserializer<'de>,
409{
410 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
411 if value.is_nan() || value.is_infinite() {
412 return Err(serde::de::Error::custom(
413 "similarity_threshold must be a finite number",
414 ));
415 }
416 if !(0.0..=1.0).contains(&value) {
417 return Err(serde::de::Error::custom(
418 "similarity_threshold must be in [0.0, 1.0]",
419 ));
420 }
421 Ok(value)
422}
423
424fn validate_importance_weight<'de, D>(deserializer: D) -> Result<f64, D::Error>
425where
426 D: serde::Deserializer<'de>,
427{
428 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
429 if value.is_nan() || value.is_infinite() {
430 return Err(serde::de::Error::custom(
431 "importance_weight must be a finite number",
432 ));
433 }
434 if value < 0.0 {
435 return Err(serde::de::Error::custom(
436 "importance_weight must be non-negative",
437 ));
438 }
439 if value > 1.0 {
440 return Err(serde::de::Error::custom("importance_weight must be <= 1.0"));
441 }
442 Ok(value)
443}
444
445fn default_importance_weight() -> f64 {
446 0.15
447}
448
449#[derive(Debug, Clone, Deserialize, Serialize)]
463#[serde(default)]
464pub struct SpreadingActivationConfig {
465 pub enabled: bool,
467 #[serde(deserialize_with = "validate_decay_lambda")]
469 pub decay_lambda: f32,
470 #[serde(deserialize_with = "validate_max_hops")]
472 pub max_hops: u32,
473 pub activation_threshold: f32,
475 pub inhibition_threshold: f32,
477 pub max_activated_nodes: usize,
479 #[serde(default = "default_seed_structural_weight")]
481 pub seed_structural_weight: f32,
482 #[serde(default = "default_seed_community_cap")]
484 pub seed_community_cap: usize,
485 #[serde(default = "default_spreading_activation_recall_timeout_ms")]
489 pub recall_timeout_ms: u64,
490}
491
492fn validate_decay_lambda<'de, D>(deserializer: D) -> Result<f32, D::Error>
493where
494 D: serde::Deserializer<'de>,
495{
496 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
497 if value.is_nan() || value.is_infinite() {
498 return Err(serde::de::Error::custom(
499 "decay_lambda must be a finite number",
500 ));
501 }
502 if !(value > 0.0 && value <= 1.0) {
503 return Err(serde::de::Error::custom(
504 "decay_lambda must be in (0.0, 1.0]",
505 ));
506 }
507 Ok(value)
508}
509
510fn validate_max_hops<'de, D>(deserializer: D) -> Result<u32, D::Error>
511where
512 D: serde::Deserializer<'de>,
513{
514 let value = <u32 as serde::Deserialize>::deserialize(deserializer)?;
515 if value == 0 {
516 return Err(serde::de::Error::custom("max_hops must be >= 1"));
517 }
518 Ok(value)
519}
520
521impl SpreadingActivationConfig {
522 pub fn validate(&self) -> Result<(), String> {
528 if self.activation_threshold >= self.inhibition_threshold {
529 return Err(format!(
530 "activation_threshold ({}) must be < inhibition_threshold ({})",
531 self.activation_threshold, self.inhibition_threshold
532 ));
533 }
534 Ok(())
535 }
536}
537
538fn default_seed_structural_weight() -> f32 {
539 0.4
540}
541
542fn default_seed_community_cap() -> usize {
543 3
544}
545
546impl Default for SpreadingActivationConfig {
547 fn default() -> Self {
548 Self {
549 enabled: false,
550 decay_lambda: default_spreading_activation_decay_lambda(),
551 max_hops: default_spreading_activation_max_hops(),
552 activation_threshold: default_spreading_activation_activation_threshold(),
553 inhibition_threshold: default_spreading_activation_inhibition_threshold(),
554 max_activated_nodes: default_spreading_activation_max_activated_nodes(),
555 seed_structural_weight: default_seed_structural_weight(),
556 seed_community_cap: default_seed_community_cap(),
557 recall_timeout_ms: default_spreading_activation_recall_timeout_ms(),
558 }
559 }
560}
561
562#[derive(Debug, Clone, Deserialize, Serialize)]
564#[serde(default)]
565pub struct BeliefRevisionConfig {
566 pub enabled: bool,
568 #[serde(deserialize_with = "validate_similarity_threshold")]
571 pub similarity_threshold: f32,
572}
573
574fn default_belief_revision_similarity_threshold() -> f32 {
575 0.85
576}
577
578impl Default for BeliefRevisionConfig {
579 fn default() -> Self {
580 Self {
581 enabled: false,
582 similarity_threshold: default_belief_revision_similarity_threshold(),
583 }
584 }
585}
586
587#[derive(Debug, Clone, Deserialize, Serialize)]
589#[serde(default)]
590pub struct RpeConfig {
591 pub enabled: bool,
593 #[serde(deserialize_with = "validate_similarity_threshold")]
596 pub threshold: f32,
597 pub max_skip_turns: u32,
599}
600
601fn default_rpe_threshold() -> f32 {
602 0.3
603}
604
605fn default_rpe_max_skip_turns() -> u32 {
606 5
607}
608
609impl Default for RpeConfig {
610 fn default() -> Self {
611 Self {
612 enabled: false,
613 threshold: default_rpe_threshold(),
614 max_skip_turns: default_rpe_max_skip_turns(),
615 }
616 }
617}
618
619#[derive(Debug, Clone, Deserialize, Serialize)]
625#[serde(default)]
626pub struct NoteLinkingConfig {
627 pub enabled: bool,
629 #[serde(deserialize_with = "validate_similarity_threshold")]
631 pub similarity_threshold: f32,
632 pub top_k: usize,
634 pub timeout_secs: u64,
636}
637
638impl Default for NoteLinkingConfig {
639 fn default() -> Self {
640 Self {
641 enabled: false,
642 similarity_threshold: default_note_linking_similarity_threshold(),
643 top_k: default_note_linking_top_k(),
644 timeout_secs: default_note_linking_timeout_secs(),
645 }
646 }
647}
648
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
651#[serde(rename_all = "lowercase")]
652pub enum VectorBackend {
653 Qdrant,
654 #[default]
655 Sqlite,
656}
657
658impl VectorBackend {
659 #[must_use]
660 pub fn as_str(&self) -> &'static str {
661 match self {
662 Self::Qdrant => "qdrant",
663 Self::Sqlite => "sqlite",
664 }
665 }
666}
667
668#[derive(Debug, Deserialize, Serialize)]
669#[allow(clippy::struct_excessive_bools)]
670pub struct MemoryConfig {
671 #[serde(default)]
672 pub compression_guidelines: zeph_memory::CompressionGuidelinesConfig,
673 #[serde(default = "default_sqlite_path_field")]
674 pub sqlite_path: String,
675 pub history_limit: u32,
676 #[serde(default = "default_qdrant_url")]
677 pub qdrant_url: String,
678 #[serde(default)]
679 pub semantic: SemanticConfig,
680 #[serde(default = "default_summarization_threshold")]
681 pub summarization_threshold: usize,
682 #[serde(default = "default_context_budget_tokens")]
683 pub context_budget_tokens: usize,
684 #[serde(default = "default_soft_compaction_threshold")]
685 pub soft_compaction_threshold: f32,
686 #[serde(
687 default = "default_hard_compaction_threshold",
688 alias = "compaction_threshold"
689 )]
690 pub hard_compaction_threshold: f32,
691 #[serde(default = "default_compaction_preserve_tail")]
692 pub compaction_preserve_tail: usize,
693 #[serde(default = "default_compaction_cooldown_turns")]
694 pub compaction_cooldown_turns: u8,
695 #[serde(default = "default_auto_budget")]
696 pub auto_budget: bool,
697 #[serde(default = "default_prune_protect_tokens")]
698 pub prune_protect_tokens: usize,
699 #[serde(default = "default_cross_session_score_threshold")]
700 pub cross_session_score_threshold: f32,
701 #[serde(default)]
702 pub vector_backend: VectorBackend,
703 #[serde(default = "default_token_safety_margin")]
704 pub token_safety_margin: f32,
705 #[serde(default = "default_redact_credentials")]
706 pub redact_credentials: bool,
707 #[serde(default = "default_true")]
708 pub autosave_assistant: bool,
709 #[serde(default = "default_autosave_min_length")]
710 pub autosave_min_length: usize,
711 #[serde(default = "default_tool_call_cutoff")]
712 pub tool_call_cutoff: usize,
713 #[serde(default = "default_sqlite_pool_size")]
714 pub sqlite_pool_size: u32,
715 #[serde(default)]
716 pub sessions: SessionsConfig,
717 #[serde(default)]
718 pub documents: DocumentConfig,
719 #[serde(default)]
720 pub eviction: zeph_memory::EvictionConfig,
721 #[serde(default)]
722 pub compression: CompressionConfig,
723 #[serde(default)]
724 pub sidequest: SidequestConfig,
725 #[serde(default)]
726 pub graph: GraphConfig,
727 #[serde(default = "default_shutdown_summary")]
731 pub shutdown_summary: bool,
732 #[serde(default = "default_shutdown_summary_min_messages")]
735 pub shutdown_summary_min_messages: usize,
736 #[serde(default = "default_shutdown_summary_max_messages")]
740 pub shutdown_summary_max_messages: usize,
741 #[serde(default = "default_shutdown_summary_timeout_secs")]
745 pub shutdown_summary_timeout_secs: u64,
746 #[serde(default)]
752 pub structured_summaries: bool,
753 #[serde(default)]
758 pub tiers: TierConfig,
759 #[serde(default)]
764 pub admission: AdmissionConfig,
765 #[serde(default)]
767 pub digest: DigestConfig,
768 #[serde(default)]
770 pub context_strategy: ContextStrategy,
771 #[serde(default = "default_crossover_turn_threshold")]
773 pub crossover_turn_threshold: u32,
774 #[serde(default)]
779 pub consolidation: ConsolidationConfig,
780 #[serde(default)]
785 pub forgetting: ForgettingConfig,
786 #[serde(default)]
793 pub database_url: Option<String>,
794 #[serde(default)]
799 pub store_routing: StoreRoutingConfig,
800 #[serde(default)]
805 pub persona: PersonaConfig,
806 #[serde(default)]
808 pub trajectory: TrajectoryConfig,
809 #[serde(default)]
811 pub category: CategoryConfig,
812 #[serde(default)]
814 pub tree: TreeConfig,
815 #[serde(default)]
820 pub microcompact: MicrocompactConfig,
821 #[serde(default)]
826 pub autodream: AutoDreamConfig,
827 #[serde(default = "default_key_facts_dedup_threshold")]
834 pub key_facts_dedup_threshold: f32,
835}
836
837fn default_crossover_turn_threshold() -> u32 {
838 20
839}
840
841fn default_key_facts_dedup_threshold() -> f32 {
842 0.95
843}
844
845#[derive(Debug, Clone, Deserialize, Serialize)]
847#[serde(default)]
848pub struct DigestConfig {
849 pub enabled: bool,
851 pub provider: String,
854 pub max_tokens: usize,
856 pub max_input_messages: usize,
858}
859
860impl Default for DigestConfig {
861 fn default() -> Self {
862 Self {
863 enabled: false,
864 provider: String::new(),
865 max_tokens: 500,
866 max_input_messages: 50,
867 }
868 }
869}
870
871#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
873#[serde(rename_all = "snake_case")]
874pub enum ContextStrategy {
875 #[default]
878 FullHistory,
879 MemoryFirst,
882 Adaptive,
885}
886
887#[derive(Debug, Clone, Deserialize, Serialize)]
888#[serde(default)]
889pub struct SessionsConfig {
890 #[serde(default = "default_max_history")]
892 pub max_history: usize,
893 #[serde(default = "default_title_max_chars")]
895 pub title_max_chars: usize,
896}
897
898impl Default for SessionsConfig {
899 fn default() -> Self {
900 Self {
901 max_history: default_max_history(),
902 title_max_chars: default_title_max_chars(),
903 }
904 }
905}
906
907#[derive(Debug, Clone, Deserialize, Serialize)]
909pub struct DocumentConfig {
910 #[serde(default = "default_document_collection")]
911 pub collection: String,
912 #[serde(default = "default_document_chunk_size")]
913 pub chunk_size: usize,
914 #[serde(default = "default_document_chunk_overlap")]
915 pub chunk_overlap: usize,
916 #[serde(default = "default_document_top_k")]
918 pub top_k: usize,
919 #[serde(default)]
921 pub rag_enabled: bool,
922}
923
924impl Default for DocumentConfig {
925 fn default() -> Self {
926 Self {
927 collection: default_document_collection(),
928 chunk_size: default_document_chunk_size(),
929 chunk_overlap: default_document_chunk_overlap(),
930 top_k: default_document_top_k(),
931 rag_enabled: false,
932 }
933 }
934}
935
936#[derive(Debug, Deserialize, Serialize)]
937#[allow(clippy::struct_excessive_bools)]
938pub struct SemanticConfig {
939 #[serde(default = "default_semantic_enabled")]
940 pub enabled: bool,
941 #[serde(default = "default_recall_limit")]
942 pub recall_limit: usize,
943 #[serde(default = "default_vector_weight")]
944 pub vector_weight: f64,
945 #[serde(default = "default_keyword_weight")]
946 pub keyword_weight: f64,
947 #[serde(default = "default_true")]
948 pub temporal_decay_enabled: bool,
949 #[serde(default = "default_temporal_decay_half_life_days")]
950 pub temporal_decay_half_life_days: u32,
951 #[serde(default = "default_true")]
952 pub mmr_enabled: bool,
953 #[serde(default = "default_mmr_lambda")]
954 pub mmr_lambda: f32,
955 #[serde(default = "default_true")]
956 pub importance_enabled: bool,
957 #[serde(
958 default = "default_importance_weight",
959 deserialize_with = "validate_importance_weight"
960 )]
961 pub importance_weight: f64,
962 #[serde(default)]
967 pub embed_provider: Option<String>,
968}
969
970impl Default for SemanticConfig {
971 fn default() -> Self {
972 Self {
973 enabled: default_semantic_enabled(),
974 recall_limit: default_recall_limit(),
975 vector_weight: default_vector_weight(),
976 keyword_weight: default_keyword_weight(),
977 temporal_decay_enabled: true,
978 temporal_decay_half_life_days: default_temporal_decay_half_life_days(),
979 mmr_enabled: true,
980 mmr_lambda: default_mmr_lambda(),
981 importance_enabled: true,
982 importance_weight: default_importance_weight(),
983 embed_provider: None,
984 }
985 }
986}
987
988#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
990#[serde(tag = "strategy", rename_all = "snake_case")]
991pub enum CompressionStrategy {
992 #[default]
994 Reactive,
995 Proactive {
997 threshold_tokens: usize,
999 max_summary_tokens: usize,
1001 },
1002 Autonomous,
1005 Focus,
1010}
1011
1012#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
1017#[serde(rename_all = "snake_case")]
1018pub enum PruningStrategy {
1019 #[default]
1021 Reactive,
1022 TaskAware,
1025 Mig,
1028 Subgoal,
1032 SubgoalMig,
1035}
1036
1037impl PruningStrategy {
1038 #[must_use]
1040 pub fn is_subgoal(self) -> bool {
1041 matches!(self, Self::Subgoal | Self::SubgoalMig)
1042 }
1043}
1044
1045impl<'de> serde::Deserialize<'de> for PruningStrategy {
1048 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1049 let s = String::deserialize(deserializer)?;
1050 s.parse().map_err(serde::de::Error::custom)
1051 }
1052}
1053
1054impl std::str::FromStr for PruningStrategy {
1055 type Err = String;
1056
1057 fn from_str(s: &str) -> Result<Self, Self::Err> {
1058 match s {
1059 "reactive" => Ok(Self::Reactive),
1060 "task_aware" | "task-aware" => Ok(Self::TaskAware),
1061 "mig" => Ok(Self::Mig),
1062 "task_aware_mig" | "task-aware-mig" => {
1065 tracing::warn!(
1066 "pruning strategy `task_aware_mig` has been removed; \
1067 falling back to `reactive`. Use `task_aware` or `mig` instead."
1068 );
1069 Ok(Self::Reactive)
1070 }
1071 "subgoal" => Ok(Self::Subgoal),
1072 "subgoal_mig" | "subgoal-mig" => Ok(Self::SubgoalMig),
1073 other => Err(format!(
1074 "unknown pruning strategy `{other}`, expected \
1075 reactive|task_aware|mig|subgoal|subgoal_mig"
1076 )),
1077 }
1078 }
1079}
1080
1081fn default_high_density_budget() -> f32 {
1082 0.7
1083}
1084
1085fn default_low_density_budget() -> f32 {
1086 0.3
1087}
1088
1089#[derive(Debug, Clone, Deserialize, Serialize)]
1096#[serde(default)]
1097pub struct CompressionPredictorConfig {
1098 pub enabled: bool,
1100 pub min_samples: u64,
1102 pub candidate_ratios: Vec<f32>,
1105 pub retrain_interval: u64,
1107 pub max_training_samples: usize,
1109}
1110
1111impl Default for CompressionPredictorConfig {
1112 fn default() -> Self {
1113 Self {
1114 enabled: false,
1115 min_samples: 10,
1116 candidate_ratios: vec![0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
1117 retrain_interval: 5,
1118 max_training_samples: 200,
1119 }
1120 }
1121}
1122
1123#[derive(Debug, Clone, Deserialize, Serialize)]
1129#[serde(default)]
1130pub struct ForgettingConfig {
1131 pub enabled: bool,
1133 pub decay_rate: f32,
1135 pub forgetting_floor: f32,
1137 pub sweep_interval_secs: u64,
1139 pub sweep_batch_size: usize,
1141 pub replay_window_hours: u32,
1143 pub replay_min_access_count: u32,
1145 pub protect_recent_hours: u32,
1147 pub protect_min_access_count: u32,
1149}
1150
1151impl Default for ForgettingConfig {
1152 fn default() -> Self {
1153 Self {
1154 enabled: false,
1155 decay_rate: 0.1,
1156 forgetting_floor: 0.05,
1157 sweep_interval_secs: 7200,
1158 sweep_batch_size: 500,
1159 replay_window_hours: 24,
1160 replay_min_access_count: 3,
1161 protect_recent_hours: 24,
1162 protect_min_access_count: 3,
1163 }
1164 }
1165}
1166
1167#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1169#[serde(default)]
1170pub struct CompressionConfig {
1171 #[serde(flatten)]
1173 pub strategy: CompressionStrategy,
1174 pub pruning_strategy: PruningStrategy,
1176 pub model: String,
1181 pub compress_provider: ProviderName,
1184 #[serde(default)]
1186 pub probe: zeph_memory::CompactionProbeConfig,
1187 #[serde(default)]
1195 pub archive_tool_outputs: bool,
1196 pub focus_scorer_provider: ProviderName,
1199 #[serde(default = "default_high_density_budget")]
1202 pub high_density_budget: f32,
1203 #[serde(default = "default_low_density_budget")]
1206 pub low_density_budget: f32,
1207 #[serde(default)]
1209 pub predictor: CompressionPredictorConfig,
1210}
1211
1212fn default_sidequest_interval_turns() -> u32 {
1213 4
1214}
1215
1216fn default_sidequest_max_eviction_ratio() -> f32 {
1217 0.5
1218}
1219
1220fn default_sidequest_max_cursors() -> usize {
1221 30
1222}
1223
1224fn default_sidequest_min_cursor_tokens() -> usize {
1225 100
1226}
1227
1228#[derive(Debug, Clone, Deserialize, Serialize)]
1230#[serde(default)]
1231pub struct SidequestConfig {
1232 pub enabled: bool,
1234 #[serde(default = "default_sidequest_interval_turns")]
1236 pub interval_turns: u32,
1237 #[serde(default = "default_sidequest_max_eviction_ratio")]
1239 pub max_eviction_ratio: f32,
1240 #[serde(default = "default_sidequest_max_cursors")]
1242 pub max_cursors: usize,
1243 #[serde(default = "default_sidequest_min_cursor_tokens")]
1246 pub min_cursor_tokens: usize,
1247}
1248
1249impl Default for SidequestConfig {
1250 fn default() -> Self {
1251 Self {
1252 enabled: false,
1253 interval_turns: default_sidequest_interval_turns(),
1254 max_eviction_ratio: default_sidequest_max_eviction_ratio(),
1255 max_cursors: default_sidequest_max_cursors(),
1256 min_cursor_tokens: default_sidequest_min_cursor_tokens(),
1257 }
1258 }
1259}
1260
1261#[derive(Debug, Clone, Deserialize, Serialize)]
1270#[serde(default)]
1271pub struct GraphConfig {
1272 pub enabled: bool,
1273 pub extract_model: String,
1274 #[serde(default = "default_graph_max_entities_per_message")]
1275 pub max_entities_per_message: usize,
1276 #[serde(default = "default_graph_max_edges_per_message")]
1277 pub max_edges_per_message: usize,
1278 #[serde(default = "default_graph_community_refresh_interval")]
1279 pub community_refresh_interval: usize,
1280 #[serde(default = "default_graph_entity_similarity_threshold")]
1281 pub entity_similarity_threshold: f32,
1282 #[serde(default = "default_graph_extraction_timeout_secs")]
1283 pub extraction_timeout_secs: u64,
1284 #[serde(default)]
1285 pub use_embedding_resolution: bool,
1286 #[serde(default = "default_graph_entity_ambiguous_threshold")]
1287 pub entity_ambiguous_threshold: f32,
1288 #[serde(default = "default_graph_max_hops")]
1289 pub max_hops: u32,
1290 #[serde(default = "default_graph_recall_limit")]
1291 pub recall_limit: usize,
1292 #[serde(default = "default_graph_expired_edge_retention_days")]
1294 pub expired_edge_retention_days: u32,
1295 #[serde(default)]
1297 pub max_entities: usize,
1298 #[serde(default = "default_graph_community_summary_max_prompt_bytes")]
1300 pub community_summary_max_prompt_bytes: usize,
1301 #[serde(default = "default_graph_community_summary_concurrency")]
1303 pub community_summary_concurrency: usize,
1304 #[serde(default = "default_lpa_edge_chunk_size")]
1307 pub lpa_edge_chunk_size: usize,
1308 #[serde(
1314 default = "default_graph_temporal_decay_rate",
1315 deserialize_with = "validate_temporal_decay_rate"
1316 )]
1317 pub temporal_decay_rate: f64,
1318 #[serde(default = "default_graph_edge_history_limit")]
1324 pub edge_history_limit: usize,
1325 #[serde(default)]
1331 pub note_linking: NoteLinkingConfig,
1332 #[serde(default)]
1337 pub spreading_activation: SpreadingActivationConfig,
1338 #[serde(
1341 default = "default_link_weight_decay_lambda",
1342 deserialize_with = "validate_link_weight_decay_lambda"
1343 )]
1344 pub link_weight_decay_lambda: f64,
1345 #[serde(default = "default_link_weight_decay_interval_secs")]
1347 pub link_weight_decay_interval_secs: u64,
1348 #[serde(default)]
1354 pub belief_revision: BeliefRevisionConfig,
1355 #[serde(default)]
1360 pub rpe: RpeConfig,
1361 #[serde(default = "default_graph_pool_size")]
1367 pub pool_size: u32,
1368}
1369
1370fn default_graph_pool_size() -> u32 {
1371 3
1372}
1373
1374impl Default for GraphConfig {
1375 fn default() -> Self {
1376 Self {
1377 enabled: false,
1378 extract_model: String::new(),
1379 max_entities_per_message: default_graph_max_entities_per_message(),
1380 max_edges_per_message: default_graph_max_edges_per_message(),
1381 community_refresh_interval: default_graph_community_refresh_interval(),
1382 entity_similarity_threshold: default_graph_entity_similarity_threshold(),
1383 extraction_timeout_secs: default_graph_extraction_timeout_secs(),
1384 use_embedding_resolution: false,
1385 entity_ambiguous_threshold: default_graph_entity_ambiguous_threshold(),
1386 max_hops: default_graph_max_hops(),
1387 recall_limit: default_graph_recall_limit(),
1388 expired_edge_retention_days: default_graph_expired_edge_retention_days(),
1389 max_entities: 0,
1390 community_summary_max_prompt_bytes: default_graph_community_summary_max_prompt_bytes(),
1391 community_summary_concurrency: default_graph_community_summary_concurrency(),
1392 lpa_edge_chunk_size: default_lpa_edge_chunk_size(),
1393 temporal_decay_rate: default_graph_temporal_decay_rate(),
1394 edge_history_limit: default_graph_edge_history_limit(),
1395 note_linking: NoteLinkingConfig::default(),
1396 spreading_activation: SpreadingActivationConfig::default(),
1397 link_weight_decay_lambda: default_link_weight_decay_lambda(),
1398 link_weight_decay_interval_secs: default_link_weight_decay_interval_secs(),
1399 belief_revision: BeliefRevisionConfig::default(),
1400 rpe: RpeConfig::default(),
1401 pool_size: default_graph_pool_size(),
1402 }
1403 }
1404}
1405
1406fn default_consolidation_confidence_threshold() -> f32 {
1407 0.7
1408}
1409
1410fn default_consolidation_sweep_interval_secs() -> u64 {
1411 3600
1412}
1413
1414fn default_consolidation_sweep_batch_size() -> usize {
1415 50
1416}
1417
1418fn default_consolidation_similarity_threshold() -> f32 {
1419 0.85
1420}
1421
1422#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1428#[serde(default)]
1429pub struct ConsolidationConfig {
1430 pub enabled: bool,
1432 #[serde(default)]
1435 pub consolidation_provider: ProviderName,
1436 #[serde(default = "default_consolidation_confidence_threshold")]
1438 pub confidence_threshold: f32,
1439 #[serde(default = "default_consolidation_sweep_interval_secs")]
1441 pub sweep_interval_secs: u64,
1442 #[serde(default = "default_consolidation_sweep_batch_size")]
1444 pub sweep_batch_size: usize,
1445 #[serde(default = "default_consolidation_similarity_threshold")]
1448 pub similarity_threshold: f32,
1449}
1450
1451impl Default for ConsolidationConfig {
1452 fn default() -> Self {
1453 Self {
1454 enabled: false,
1455 consolidation_provider: ProviderName::default(),
1456 confidence_threshold: default_consolidation_confidence_threshold(),
1457 sweep_interval_secs: default_consolidation_sweep_interval_secs(),
1458 sweep_batch_size: default_consolidation_sweep_batch_size(),
1459 similarity_threshold: default_consolidation_similarity_threshold(),
1460 }
1461 }
1462}
1463
1464fn default_link_weight_decay_lambda() -> f64 {
1465 0.95
1466}
1467
1468fn default_link_weight_decay_interval_secs() -> u64 {
1469 86400
1470}
1471
1472fn validate_link_weight_decay_lambda<'de, D>(deserializer: D) -> Result<f64, D::Error>
1473where
1474 D: serde::Deserializer<'de>,
1475{
1476 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
1477 if value.is_nan() || value.is_infinite() {
1478 return Err(serde::de::Error::custom(
1479 "link_weight_decay_lambda must be a finite number",
1480 ));
1481 }
1482 if !(value > 0.0 && value <= 1.0) {
1483 return Err(serde::de::Error::custom(
1484 "link_weight_decay_lambda must be in (0.0, 1.0]",
1485 ));
1486 }
1487 Ok(value)
1488}
1489
1490fn validate_admission_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
1491where
1492 D: serde::Deserializer<'de>,
1493{
1494 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1495 if value.is_nan() || value.is_infinite() {
1496 return Err(serde::de::Error::custom(
1497 "threshold must be a finite number",
1498 ));
1499 }
1500 if !(0.0..=1.0).contains(&value) {
1501 return Err(serde::de::Error::custom("threshold must be in [0.0, 1.0]"));
1502 }
1503 Ok(value)
1504}
1505
1506fn validate_admission_fast_path_margin<'de, D>(deserializer: D) -> Result<f32, D::Error>
1507where
1508 D: serde::Deserializer<'de>,
1509{
1510 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1511 if value.is_nan() || value.is_infinite() {
1512 return Err(serde::de::Error::custom(
1513 "fast_path_margin must be a finite number",
1514 ));
1515 }
1516 if !(0.0..=1.0).contains(&value) {
1517 return Err(serde::de::Error::custom(
1518 "fast_path_margin must be in [0.0, 1.0]",
1519 ));
1520 }
1521 Ok(value)
1522}
1523
1524fn default_admission_threshold() -> f32 {
1525 0.40
1526}
1527
1528fn default_admission_fast_path_margin() -> f32 {
1529 0.15
1530}
1531
1532fn default_rl_min_samples() -> u32 {
1533 500
1534}
1535
1536fn default_rl_retrain_interval_secs() -> u64 {
1537 3600
1538}
1539
1540#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1545#[serde(rename_all = "snake_case")]
1546pub enum AdmissionStrategy {
1547 #[default]
1549 Heuristic,
1550 Rl,
1553}
1554
1555fn validate_admission_weight<'de, D>(deserializer: D) -> Result<f32, D::Error>
1556where
1557 D: serde::Deserializer<'de>,
1558{
1559 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1560 if value < 0.0 {
1561 return Err(serde::de::Error::custom(
1562 "admission weight must be non-negative (>= 0.0)",
1563 ));
1564 }
1565 Ok(value)
1566}
1567
1568#[derive(Debug, Clone, Deserialize, Serialize)]
1573#[serde(default)]
1574pub struct AdmissionWeights {
1575 #[serde(deserialize_with = "validate_admission_weight")]
1577 pub future_utility: f32,
1578 #[serde(deserialize_with = "validate_admission_weight")]
1580 pub factual_confidence: f32,
1581 #[serde(deserialize_with = "validate_admission_weight")]
1583 pub semantic_novelty: f32,
1584 #[serde(deserialize_with = "validate_admission_weight")]
1586 pub temporal_recency: f32,
1587 #[serde(deserialize_with = "validate_admission_weight")]
1589 pub content_type_prior: f32,
1590 #[serde(deserialize_with = "validate_admission_weight")]
1594 pub goal_utility: f32,
1595}
1596
1597impl Default for AdmissionWeights {
1598 fn default() -> Self {
1599 Self {
1600 future_utility: 0.30,
1601 factual_confidence: 0.15,
1602 semantic_novelty: 0.30,
1603 temporal_recency: 0.10,
1604 content_type_prior: 0.15,
1605 goal_utility: 0.0,
1606 }
1607 }
1608}
1609
1610impl AdmissionWeights {
1611 #[must_use]
1615 pub fn normalized(&self) -> Self {
1616 let sum = self.future_utility
1617 + self.factual_confidence
1618 + self.semantic_novelty
1619 + self.temporal_recency
1620 + self.content_type_prior
1621 + self.goal_utility;
1622 if sum <= f32::EPSILON {
1623 return Self::default();
1624 }
1625 Self {
1626 future_utility: self.future_utility / sum,
1627 factual_confidence: self.factual_confidence / sum,
1628 semantic_novelty: self.semantic_novelty / sum,
1629 temporal_recency: self.temporal_recency / sum,
1630 content_type_prior: self.content_type_prior / sum,
1631 goal_utility: self.goal_utility / sum,
1632 }
1633 }
1634}
1635
1636#[derive(Debug, Clone, Deserialize, Serialize)]
1641#[serde(default)]
1642pub struct AdmissionConfig {
1643 pub enabled: bool,
1645 #[serde(deserialize_with = "validate_admission_threshold")]
1648 pub threshold: f32,
1649 #[serde(deserialize_with = "validate_admission_fast_path_margin")]
1652 pub fast_path_margin: f32,
1653 pub admission_provider: ProviderName,
1656 pub weights: AdmissionWeights,
1658 #[serde(default)]
1660 pub admission_strategy: AdmissionStrategy,
1661 #[serde(default = "default_rl_min_samples")]
1664 pub rl_min_samples: u32,
1665 #[serde(default = "default_rl_retrain_interval_secs")]
1667 pub rl_retrain_interval_secs: u64,
1668 #[serde(default)]
1672 pub goal_conditioned_write: bool,
1673 #[serde(default)]
1677 pub goal_utility_provider: ProviderName,
1678 #[serde(default = "default_goal_utility_threshold")]
1681 pub goal_utility_threshold: f32,
1682 #[serde(default = "default_goal_utility_weight")]
1685 pub goal_utility_weight: f32,
1686}
1687
1688fn default_goal_utility_threshold() -> f32 {
1689 0.4
1690}
1691
1692fn default_goal_utility_weight() -> f32 {
1693 0.25
1694}
1695
1696impl Default for AdmissionConfig {
1697 fn default() -> Self {
1698 Self {
1699 enabled: false,
1700 threshold: default_admission_threshold(),
1701 fast_path_margin: default_admission_fast_path_margin(),
1702 admission_provider: ProviderName::default(),
1703 weights: AdmissionWeights::default(),
1704 admission_strategy: AdmissionStrategy::default(),
1705 rl_min_samples: default_rl_min_samples(),
1706 rl_retrain_interval_secs: default_rl_retrain_interval_secs(),
1707 goal_conditioned_write: false,
1708 goal_utility_provider: ProviderName::default(),
1709 goal_utility_threshold: default_goal_utility_threshold(),
1710 goal_utility_weight: default_goal_utility_weight(),
1711 }
1712 }
1713}
1714
1715#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
1717#[serde(rename_all = "snake_case")]
1718pub enum StoreRoutingStrategy {
1719 #[default]
1721 Heuristic,
1722 Llm,
1724 Hybrid,
1726}
1727
1728#[derive(Debug, Clone, Deserialize, Serialize)]
1733#[serde(default)]
1734pub struct StoreRoutingConfig {
1735 pub enabled: bool,
1738 pub strategy: StoreRoutingStrategy,
1740 pub routing_classifier_provider: ProviderName,
1743 pub fallback_route: String,
1746 pub confidence_threshold: f32,
1749}
1750
1751impl Default for StoreRoutingConfig {
1752 fn default() -> Self {
1753 Self {
1754 enabled: false,
1755 strategy: StoreRoutingStrategy::Heuristic,
1756 routing_classifier_provider: ProviderName::default(),
1757 fallback_route: "hybrid".into(),
1758 confidence_threshold: 0.7,
1759 }
1760 }
1761}
1762
1763#[derive(Debug, Clone, Deserialize, Serialize)]
1768#[serde(default)]
1769pub struct PersonaConfig {
1770 pub enabled: bool,
1772 pub persona_provider: ProviderName,
1775 pub min_confidence: f64,
1777 pub min_messages: usize,
1779 pub max_messages: usize,
1781 pub extraction_timeout_secs: u64,
1783 pub context_budget_tokens: usize,
1785}
1786
1787impl Default for PersonaConfig {
1788 fn default() -> Self {
1789 Self {
1790 enabled: false,
1791 persona_provider: ProviderName::default(),
1792 min_confidence: 0.6,
1793 min_messages: 3,
1794 max_messages: 10,
1795 extraction_timeout_secs: 10,
1796 context_budget_tokens: 500,
1797 }
1798 }
1799}
1800
1801#[derive(Debug, Clone, Deserialize, Serialize)]
1807#[serde(default)]
1808pub struct TrajectoryConfig {
1809 pub enabled: bool,
1811 pub trajectory_provider: ProviderName,
1814 pub context_budget_tokens: usize,
1816 pub max_messages: usize,
1818 pub extraction_timeout_secs: u64,
1820 pub recall_top_k: usize,
1822 pub min_confidence: f64,
1824}
1825
1826impl Default for TrajectoryConfig {
1827 fn default() -> Self {
1828 Self {
1829 enabled: false,
1830 trajectory_provider: ProviderName::default(),
1831 context_budget_tokens: 400,
1832 max_messages: 10,
1833 extraction_timeout_secs: 10,
1834 recall_top_k: 5,
1835 min_confidence: 0.6,
1836 }
1837 }
1838}
1839
1840#[derive(Debug, Clone, Deserialize, Serialize)]
1846#[serde(default)]
1847pub struct CategoryConfig {
1848 pub enabled: bool,
1850 pub auto_tag: bool,
1852}
1853
1854impl Default for CategoryConfig {
1855 fn default() -> Self {
1856 Self {
1857 enabled: false,
1858 auto_tag: true,
1859 }
1860 }
1861}
1862
1863#[derive(Debug, Clone, Deserialize, Serialize)]
1869#[serde(default)]
1870pub struct TreeConfig {
1871 pub enabled: bool,
1873 pub consolidation_provider: ProviderName,
1876 pub sweep_interval_secs: u64,
1878 pub batch_size: usize,
1880 pub similarity_threshold: f32,
1882 pub max_level: u32,
1884 pub context_budget_tokens: usize,
1886 pub recall_top_k: usize,
1888 pub min_cluster_size: usize,
1890}
1891
1892impl Default for TreeConfig {
1893 fn default() -> Self {
1894 Self {
1895 enabled: false,
1896 consolidation_provider: ProviderName::default(),
1897 sweep_interval_secs: 300,
1898 batch_size: 20,
1899 similarity_threshold: 0.8,
1900 max_level: 3,
1901 context_budget_tokens: 400,
1902 recall_top_k: 5,
1903 min_cluster_size: 2,
1904 }
1905 }
1906}
1907
1908#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
1914#[serde(default)]
1915pub struct MicrocompactConfig {
1916 pub enabled: bool,
1918 pub gap_threshold_minutes: u32,
1920 pub keep_recent: usize,
1922}
1923
1924impl Default for MicrocompactConfig {
1925 fn default() -> Self {
1926 Self {
1927 enabled: false,
1928 gap_threshold_minutes: 60,
1929 keep_recent: 3,
1930 }
1931 }
1932}
1933
1934#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
1939#[serde(default)]
1940pub struct AutoDreamConfig {
1941 pub enabled: bool,
1943 pub min_sessions: u32,
1945 pub min_hours: u32,
1947 pub consolidation_provider: ProviderName,
1950 pub max_iterations: u8,
1952}
1953
1954impl Default for AutoDreamConfig {
1955 fn default() -> Self {
1956 Self {
1957 enabled: false,
1958 min_sessions: 3,
1959 min_hours: 24,
1960 consolidation_provider: ProviderName::default(),
1961 max_iterations: 8,
1962 }
1963 }
1964}
1965
1966#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
1971#[serde(default)]
1972pub struct MagicDocsConfig {
1973 pub enabled: bool,
1975 pub min_turns_between_updates: u32,
1977 pub update_provider: ProviderName,
1980 pub max_iterations: u8,
1982}
1983
1984impl Default for MagicDocsConfig {
1985 fn default() -> Self {
1986 Self {
1987 enabled: false,
1988 min_turns_between_updates: 5,
1989 update_provider: ProviderName::default(),
1990 max_iterations: 4,
1991 }
1992 }
1993}
1994
1995#[cfg(test)]
1996mod tests {
1997 use super::*;
1998
1999 #[test]
2002 fn pruning_strategy_toml_task_aware_mig_falls_back_to_reactive() {
2003 #[derive(serde::Deserialize)]
2004 struct Wrapper {
2005 #[allow(dead_code)]
2006 pruning_strategy: PruningStrategy,
2007 }
2008 let toml = r#"pruning_strategy = "task_aware_mig""#;
2009 let w: Wrapper = toml::from_str(toml).expect("should deserialize without error");
2010 assert_eq!(
2011 w.pruning_strategy,
2012 PruningStrategy::Reactive,
2013 "task_aware_mig must fall back to Reactive"
2014 );
2015 }
2016
2017 #[test]
2018 fn pruning_strategy_toml_round_trip() {
2019 #[derive(serde::Deserialize)]
2020 struct Wrapper {
2021 #[allow(dead_code)]
2022 pruning_strategy: PruningStrategy,
2023 }
2024 for (input, expected) in [
2025 ("reactive", PruningStrategy::Reactive),
2026 ("task_aware", PruningStrategy::TaskAware),
2027 ("mig", PruningStrategy::Mig),
2028 ] {
2029 let toml = format!(r#"pruning_strategy = "{input}""#);
2030 let w: Wrapper = toml::from_str(&toml)
2031 .unwrap_or_else(|e| panic!("failed to deserialize `{input}`: {e}"));
2032 assert_eq!(w.pruning_strategy, expected, "mismatch for `{input}`");
2033 }
2034 }
2035
2036 #[test]
2037 fn pruning_strategy_toml_unknown_value_errors() {
2038 #[derive(serde::Deserialize)]
2039 #[allow(dead_code)]
2040 struct Wrapper {
2041 pruning_strategy: PruningStrategy,
2042 }
2043 let toml = r#"pruning_strategy = "nonexistent_strategy""#;
2044 assert!(
2045 toml::from_str::<Wrapper>(toml).is_err(),
2046 "unknown strategy must produce an error"
2047 );
2048 }
2049
2050 #[test]
2051 fn tier_config_defaults_are_correct() {
2052 let cfg = TierConfig::default();
2053 assert!(!cfg.enabled);
2054 assert_eq!(cfg.promotion_min_sessions, 3);
2055 assert!((cfg.similarity_threshold - 0.92).abs() < f32::EPSILON);
2056 assert_eq!(cfg.sweep_interval_secs, 3600);
2057 assert_eq!(cfg.sweep_batch_size, 100);
2058 }
2059
2060 #[test]
2061 fn tier_config_rejects_min_sessions_below_2() {
2062 let toml = "promotion_min_sessions = 1";
2063 assert!(toml::from_str::<TierConfig>(toml).is_err());
2064 }
2065
2066 #[test]
2067 fn tier_config_rejects_similarity_threshold_below_0_5() {
2068 let toml = "similarity_threshold = 0.4";
2069 assert!(toml::from_str::<TierConfig>(toml).is_err());
2070 }
2071
2072 #[test]
2073 fn tier_config_rejects_zero_sweep_batch_size() {
2074 let toml = "sweep_batch_size = 0";
2075 assert!(toml::from_str::<TierConfig>(toml).is_err());
2076 }
2077
2078 fn deserialize_importance_weight(toml_val: &str) -> Result<SemanticConfig, toml::de::Error> {
2079 let input = format!("importance_weight = {toml_val}");
2080 toml::from_str::<SemanticConfig>(&input)
2081 }
2082
2083 #[test]
2084 fn importance_weight_default_is_0_15() {
2085 let cfg = SemanticConfig::default();
2086 assert!((cfg.importance_weight - 0.15).abs() < f64::EPSILON);
2087 }
2088
2089 #[test]
2090 fn importance_weight_valid_zero() {
2091 let cfg = deserialize_importance_weight("0.0").unwrap();
2092 assert!((cfg.importance_weight - 0.0_f64).abs() < f64::EPSILON);
2093 }
2094
2095 #[test]
2096 fn importance_weight_valid_one() {
2097 let cfg = deserialize_importance_weight("1.0").unwrap();
2098 assert!((cfg.importance_weight - 1.0_f64).abs() < f64::EPSILON);
2099 }
2100
2101 #[test]
2102 fn importance_weight_rejects_near_zero_negative() {
2103 let result = deserialize_importance_weight("-0.01");
2108 assert!(
2109 result.is_err(),
2110 "negative importance_weight must be rejected"
2111 );
2112 }
2113
2114 #[test]
2115 fn importance_weight_rejects_negative() {
2116 let result = deserialize_importance_weight("-1.0");
2117 assert!(result.is_err(), "negative value must be rejected");
2118 }
2119
2120 #[test]
2121 fn importance_weight_rejects_greater_than_one() {
2122 let result = deserialize_importance_weight("1.01");
2123 assert!(result.is_err(), "value > 1.0 must be rejected");
2124 }
2125
2126 #[test]
2130 fn admission_weights_normalized_sums_to_one() {
2131 let w = AdmissionWeights {
2132 future_utility: 2.0,
2133 factual_confidence: 1.0,
2134 semantic_novelty: 3.0,
2135 temporal_recency: 1.0,
2136 content_type_prior: 3.0,
2137 goal_utility: 0.0,
2138 };
2139 let n = w.normalized();
2140 let sum = n.future_utility
2141 + n.factual_confidence
2142 + n.semantic_novelty
2143 + n.temporal_recency
2144 + n.content_type_prior;
2145 assert!(
2146 (sum - 1.0).abs() < 0.001,
2147 "normalized weights must sum to 1.0, got {sum}"
2148 );
2149 }
2150
2151 #[test]
2153 fn admission_weights_normalized_preserves_already_unit_sum() {
2154 let w = AdmissionWeights::default();
2155 let n = w.normalized();
2156 let sum = n.future_utility
2157 + n.factual_confidence
2158 + n.semantic_novelty
2159 + n.temporal_recency
2160 + n.content_type_prior;
2161 assert!(
2162 (sum - 1.0).abs() < 0.001,
2163 "default weights sum to ~1.0 after normalization"
2164 );
2165 }
2166
2167 #[test]
2169 fn admission_weights_normalized_zero_sum_falls_back_to_default() {
2170 let w = AdmissionWeights {
2171 future_utility: 0.0,
2172 factual_confidence: 0.0,
2173 semantic_novelty: 0.0,
2174 temporal_recency: 0.0,
2175 content_type_prior: 0.0,
2176 goal_utility: 0.0,
2177 };
2178 let n = w.normalized();
2179 let default = AdmissionWeights::default();
2180 assert!(
2181 (n.future_utility - default.future_utility).abs() < 0.001,
2182 "zero-sum weights must fall back to defaults"
2183 );
2184 }
2185
2186 #[test]
2188 fn admission_config_defaults() {
2189 let cfg = AdmissionConfig::default();
2190 assert!(!cfg.enabled);
2191 assert!((cfg.threshold - 0.40).abs() < 0.001);
2192 assert!((cfg.fast_path_margin - 0.15).abs() < 0.001);
2193 assert!(cfg.admission_provider.is_empty());
2194 }
2195
2196 #[test]
2199 fn spreading_activation_default_recall_timeout_ms_is_1000() {
2200 let cfg = SpreadingActivationConfig::default();
2201 assert_eq!(
2202 cfg.recall_timeout_ms, 1000,
2203 "default recall_timeout_ms must be 1000ms"
2204 );
2205 }
2206
2207 #[test]
2208 fn spreading_activation_toml_recall_timeout_ms_round_trip() {
2209 #[derive(serde::Deserialize)]
2210 struct Wrapper {
2211 recall_timeout_ms: u64,
2212 }
2213 let toml = "recall_timeout_ms = 500";
2214 let w: Wrapper = toml::from_str(toml).unwrap();
2215 assert_eq!(w.recall_timeout_ms, 500);
2216 }
2217
2218 #[test]
2219 fn spreading_activation_validate_cross_field_constraints() {
2220 let mut cfg = SpreadingActivationConfig::default();
2221 assert!(cfg.validate().is_ok());
2223
2224 cfg.activation_threshold = 0.5;
2226 cfg.inhibition_threshold = 0.5;
2227 assert!(cfg.validate().is_err());
2228 }
2229
2230 #[test]
2233 fn compression_config_focus_strategy_deserializes() {
2234 let toml = r#"strategy = "focus""#;
2235 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2236 assert_eq!(cfg.strategy, CompressionStrategy::Focus);
2237 }
2238
2239 #[test]
2240 fn compression_config_density_budget_defaults_on_deserialize() {
2241 let toml = r#"strategy = "reactive""#;
2244 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2245 assert!((cfg.high_density_budget - 0.7).abs() < 1e-6);
2246 assert!((cfg.low_density_budget - 0.3).abs() < 1e-6);
2247 }
2248
2249 #[test]
2250 fn compression_config_density_budget_round_trip() {
2251 let toml = "strategy = \"reactive\"\nhigh_density_budget = 0.6\nlow_density_budget = 0.4";
2252 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2253 assert!((cfg.high_density_budget - 0.6).abs() < f32::EPSILON);
2254 assert!((cfg.low_density_budget - 0.4).abs() < f32::EPSILON);
2255 }
2256
2257 #[test]
2258 fn compression_config_focus_scorer_provider_default_empty() {
2259 let cfg = CompressionConfig::default();
2260 assert!(cfg.focus_scorer_provider.is_empty());
2261 }
2262
2263 #[test]
2264 fn compression_config_focus_scorer_provider_round_trip() {
2265 let toml = "strategy = \"focus\"\nfocus_scorer_provider = \"fast\"";
2266 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2267 assert_eq!(cfg.focus_scorer_provider.as_str(), "fast");
2268 }
2269}