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 #[serde(default)]
864 pub compression_spectrum: crate::features::CompressionSpectrumConfig,
865 #[serde(default)]
880 pub retrieval: RetrievalConfig,
881 #[serde(default)]
889 pub reasoning: ReasoningConfig,
890 #[serde(default)]
903 pub hebbian: HebbianConfig,
904}
905
906fn default_crossover_turn_threshold() -> u32 {
907 20
908}
909
910fn default_key_facts_dedup_threshold() -> f32 {
911 0.95
912}
913
914#[derive(Debug, Clone, Deserialize, Serialize)]
916#[serde(default)]
917pub struct DigestConfig {
918 pub enabled: bool,
920 pub provider: String,
923 pub max_tokens: usize,
925 pub max_input_messages: usize,
927}
928
929impl Default for DigestConfig {
930 fn default() -> Self {
931 Self {
932 enabled: false,
933 provider: String::new(),
934 max_tokens: 500,
935 max_input_messages: 50,
936 }
937 }
938}
939
940#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
942#[serde(rename_all = "snake_case")]
943pub enum ContextStrategy {
944 #[default]
947 FullHistory,
948 MemoryFirst,
951 Adaptive,
954}
955
956#[derive(Debug, Clone, Deserialize, Serialize)]
958#[serde(default)]
959pub struct SessionsConfig {
960 #[serde(default = "default_max_history")]
962 pub max_history: usize,
963 #[serde(default = "default_title_max_chars")]
965 pub title_max_chars: usize,
966}
967
968impl Default for SessionsConfig {
969 fn default() -> Self {
970 Self {
971 max_history: default_max_history(),
972 title_max_chars: default_title_max_chars(),
973 }
974 }
975}
976
977#[derive(Debug, Clone, Deserialize, Serialize)]
979pub struct DocumentConfig {
980 #[serde(default = "default_document_collection")]
981 pub collection: String,
982 #[serde(default = "default_document_chunk_size")]
983 pub chunk_size: usize,
984 #[serde(default = "default_document_chunk_overlap")]
985 pub chunk_overlap: usize,
986 #[serde(default = "default_document_top_k")]
988 pub top_k: usize,
989 #[serde(default)]
991 pub rag_enabled: bool,
992}
993
994impl Default for DocumentConfig {
995 fn default() -> Self {
996 Self {
997 collection: default_document_collection(),
998 chunk_size: default_document_chunk_size(),
999 chunk_overlap: default_document_chunk_overlap(),
1000 top_k: default_document_top_k(),
1001 rag_enabled: false,
1002 }
1003 }
1004}
1005
1006#[derive(Debug, Deserialize, Serialize)]
1022#[allow(clippy::struct_excessive_bools)]
1023pub struct SemanticConfig {
1024 #[serde(default = "default_semantic_enabled")]
1026 pub enabled: bool,
1027 #[serde(default = "default_recall_limit")]
1028 pub recall_limit: usize,
1029 #[serde(default = "default_vector_weight")]
1030 pub vector_weight: f64,
1031 #[serde(default = "default_keyword_weight")]
1032 pub keyword_weight: f64,
1033 #[serde(default = "default_true")]
1034 pub temporal_decay_enabled: bool,
1035 #[serde(default = "default_temporal_decay_half_life_days")]
1036 pub temporal_decay_half_life_days: u32,
1037 #[serde(default = "default_true")]
1038 pub mmr_enabled: bool,
1039 #[serde(default = "default_mmr_lambda")]
1040 pub mmr_lambda: f32,
1041 #[serde(default = "default_true")]
1042 pub importance_enabled: bool,
1043 #[serde(
1044 default = "default_importance_weight",
1045 deserialize_with = "validate_importance_weight"
1046 )]
1047 pub importance_weight: f64,
1048 #[serde(default)]
1053 pub embed_provider: Option<String>,
1054}
1055
1056impl Default for SemanticConfig {
1057 fn default() -> Self {
1058 Self {
1059 enabled: default_semantic_enabled(),
1060 recall_limit: default_recall_limit(),
1061 vector_weight: default_vector_weight(),
1062 keyword_weight: default_keyword_weight(),
1063 temporal_decay_enabled: true,
1064 temporal_decay_half_life_days: default_temporal_decay_half_life_days(),
1065 mmr_enabled: true,
1066 mmr_lambda: default_mmr_lambda(),
1067 importance_enabled: true,
1068 importance_weight: default_importance_weight(),
1069 embed_provider: None,
1070 }
1071 }
1072}
1073
1074#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, Hash)]
1086#[serde(rename_all = "snake_case")]
1087pub enum ContextFormat {
1088 #[default]
1094 Structured,
1095 Plain,
1100}
1101
1102#[derive(Debug, Clone, Deserialize, Serialize)]
1117#[serde(default)]
1118pub struct RetrievalConfig {
1119 pub depth: u32,
1131 pub search_prompt_template: String,
1139 pub context_format: ContextFormat,
1144 #[serde(default = "default_query_bias_correction")]
1152 pub query_bias_correction: bool,
1153 #[serde(default = "default_query_bias_profile_weight")]
1158 pub query_bias_profile_weight: f32,
1159 #[serde(default = "default_query_bias_centroid_ttl_secs")]
1164 pub query_bias_centroid_ttl_secs: u64,
1165}
1166
1167fn default_query_bias_correction() -> bool {
1168 true
1169}
1170
1171fn default_query_bias_profile_weight() -> f32 {
1172 0.25
1173}
1174
1175fn default_query_bias_centroid_ttl_secs() -> u64 {
1176 300
1177}
1178
1179impl Default for RetrievalConfig {
1180 fn default() -> Self {
1181 Self {
1182 depth: 0,
1183 search_prompt_template: String::new(),
1184 context_format: ContextFormat::default(),
1185 query_bias_correction: default_query_bias_correction(),
1186 query_bias_profile_weight: default_query_bias_profile_weight(),
1187 query_bias_centroid_ttl_secs: default_query_bias_centroid_ttl_secs(),
1188 }
1189 }
1190}
1191
1192#[derive(Debug, Clone, Deserialize, Serialize)]
1200#[serde(default)]
1201pub struct HebbianConfig {
1202 pub enabled: bool,
1205 pub hebbian_lr: f32,
1210 pub consolidation_interval_secs: u64,
1215 pub consolidation_threshold: f64,
1218 pub consolidate_provider: String,
1222 pub max_candidates_per_sweep: usize,
1224 pub consolidation_cooldown_secs: u64,
1229 pub consolidation_prompt_timeout_secs: u64,
1232 pub consolidation_max_neighbors: usize,
1235 pub spreading_activation: bool,
1240 pub spread_depth: u32,
1242 pub spread_edge_types: Vec<String>,
1247 pub step_budget_ms: u64,
1252}
1253
1254impl Default for HebbianConfig {
1255 fn default() -> Self {
1256 Self {
1257 enabled: false,
1258 hebbian_lr: 0.1,
1259 consolidation_interval_secs: 3600,
1260 consolidation_threshold: 5.0,
1261 consolidate_provider: "fast".to_owned(),
1262 max_candidates_per_sweep: 10,
1263 consolidation_cooldown_secs: 86_400,
1264 consolidation_prompt_timeout_secs: 30,
1265 consolidation_max_neighbors: 20,
1266 spreading_activation: false,
1267 spread_depth: 2,
1268 spread_edge_types: Vec::new(),
1269 step_budget_ms: 8,
1270 }
1271 }
1272}
1273
1274#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
1276#[serde(tag = "strategy", rename_all = "snake_case")]
1277pub enum CompressionStrategy {
1278 #[default]
1280 Reactive,
1281 Proactive {
1283 threshold_tokens: usize,
1285 max_summary_tokens: usize,
1287 },
1288 Autonomous,
1291 Focus,
1296}
1297
1298#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
1303#[serde(rename_all = "snake_case")]
1304pub enum PruningStrategy {
1305 #[default]
1307 Reactive,
1308 TaskAware,
1311 Mig,
1314 Subgoal,
1318 SubgoalMig,
1321}
1322
1323impl PruningStrategy {
1324 #[must_use]
1326 pub fn is_subgoal(self) -> bool {
1327 matches!(self, Self::Subgoal | Self::SubgoalMig)
1328 }
1329}
1330
1331impl<'de> serde::Deserialize<'de> for PruningStrategy {
1334 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1335 let s = String::deserialize(deserializer)?;
1336 s.parse().map_err(serde::de::Error::custom)
1337 }
1338}
1339
1340impl std::str::FromStr for PruningStrategy {
1341 type Err = String;
1342
1343 fn from_str(s: &str) -> Result<Self, Self::Err> {
1344 match s {
1345 "reactive" => Ok(Self::Reactive),
1346 "task_aware" | "task-aware" => Ok(Self::TaskAware),
1347 "mig" => Ok(Self::Mig),
1348 "task_aware_mig" | "task-aware-mig" => {
1351 tracing::warn!(
1352 "pruning strategy `task_aware_mig` has been removed; \
1353 falling back to `reactive`. Use `task_aware` or `mig` instead."
1354 );
1355 Ok(Self::Reactive)
1356 }
1357 "subgoal" => Ok(Self::Subgoal),
1358 "subgoal_mig" | "subgoal-mig" => Ok(Self::SubgoalMig),
1359 other => Err(format!(
1360 "unknown pruning strategy `{other}`, expected \
1361 reactive|task_aware|mig|subgoal|subgoal_mig"
1362 )),
1363 }
1364 }
1365}
1366
1367fn default_high_density_budget() -> f32 {
1368 0.7
1369}
1370
1371fn default_low_density_budget() -> f32 {
1372 0.3
1373}
1374
1375#[derive(Debug, Clone, Deserialize, Serialize)]
1381#[serde(default)]
1382pub struct ForgettingConfig {
1383 pub enabled: bool,
1385 pub decay_rate: f32,
1387 pub forgetting_floor: f32,
1389 pub sweep_interval_secs: u64,
1391 pub sweep_batch_size: usize,
1393 pub replay_window_hours: u32,
1395 pub replay_min_access_count: u32,
1397 pub protect_recent_hours: u32,
1399 pub protect_min_access_count: u32,
1401}
1402
1403impl Default for ForgettingConfig {
1404 fn default() -> Self {
1405 Self {
1406 enabled: false,
1407 decay_rate: 0.1,
1408 forgetting_floor: 0.05,
1409 sweep_interval_secs: 7200,
1410 sweep_batch_size: 500,
1411 replay_window_hours: 24,
1412 replay_min_access_count: 3,
1413 protect_recent_hours: 24,
1414 protect_min_access_count: 3,
1415 }
1416 }
1417}
1418
1419#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1421#[serde(default)]
1422pub struct CompressionConfig {
1423 #[serde(flatten)]
1425 pub strategy: CompressionStrategy,
1426 pub pruning_strategy: PruningStrategy,
1428 pub model: String,
1433 pub compress_provider: ProviderName,
1436 #[serde(default)]
1438 pub probe: zeph_memory::CompactionProbeConfig,
1439 #[serde(default)]
1447 pub archive_tool_outputs: bool,
1448 pub focus_scorer_provider: ProviderName,
1452 #[serde(default = "default_high_density_budget")]
1455 pub high_density_budget: f32,
1456 #[serde(default = "default_low_density_budget")]
1459 pub low_density_budget: f32,
1460}
1461
1462fn default_sidequest_interval_turns() -> u32 {
1463 4
1464}
1465
1466fn default_sidequest_max_eviction_ratio() -> f32 {
1467 0.5
1468}
1469
1470fn default_sidequest_max_cursors() -> usize {
1471 30
1472}
1473
1474fn default_sidequest_min_cursor_tokens() -> usize {
1475 100
1476}
1477
1478#[derive(Debug, Clone, Deserialize, Serialize)]
1480#[serde(default)]
1481pub struct SidequestConfig {
1482 pub enabled: bool,
1484 #[serde(default = "default_sidequest_interval_turns")]
1486 pub interval_turns: u32,
1487 #[serde(default = "default_sidequest_max_eviction_ratio")]
1489 pub max_eviction_ratio: f32,
1490 #[serde(default = "default_sidequest_max_cursors")]
1492 pub max_cursors: usize,
1493 #[serde(default = "default_sidequest_min_cursor_tokens")]
1496 pub min_cursor_tokens: usize,
1497}
1498
1499impl Default for SidequestConfig {
1500 fn default() -> Self {
1501 Self {
1502 enabled: false,
1503 interval_turns: default_sidequest_interval_turns(),
1504 max_eviction_ratio: default_sidequest_max_eviction_ratio(),
1505 max_cursors: default_sidequest_max_cursors(),
1506 min_cursor_tokens: default_sidequest_min_cursor_tokens(),
1507 }
1508 }
1509}
1510
1511#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
1516#[serde(rename_all = "snake_case")]
1517pub enum GraphRetrievalStrategy {
1518 #[default]
1520 Synapse,
1521 Bfs,
1523 #[serde(rename = "astar")]
1525 AStar,
1526 WaterCircles,
1528 BeamSearch,
1530 Hybrid,
1532}
1533
1534fn default_beam_width() -> usize {
1535 10
1536}
1537
1538#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1543pub struct BeamSearchConfig {
1544 #[serde(default = "default_beam_width")]
1546 pub beam_width: usize,
1547}
1548
1549impl Default for BeamSearchConfig {
1550 fn default() -> Self {
1551 Self {
1552 beam_width: default_beam_width(),
1553 }
1554 }
1555}
1556
1557#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
1561pub struct WaterCirclesConfig {
1562 #[serde(default)]
1564 pub ring_limit: usize,
1565}
1566
1567fn default_evolution_sweep_interval() -> usize {
1568 50
1569}
1570
1571fn default_confidence_prune_threshold() -> f32 {
1572 0.1
1573}
1574
1575#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1579pub struct ExperienceConfig {
1580 #[serde(default)]
1582 pub enabled: bool,
1583 #[serde(default)]
1585 pub evolution_sweep_enabled: bool,
1586 #[serde(default = "default_confidence_prune_threshold")]
1588 pub confidence_prune_threshold: f32,
1589 #[serde(default = "default_evolution_sweep_interval")]
1591 pub evolution_sweep_interval: usize,
1592}
1593
1594impl Default for ExperienceConfig {
1595 fn default() -> Self {
1596 Self {
1597 enabled: false,
1598 evolution_sweep_enabled: false,
1599 confidence_prune_threshold: default_confidence_prune_threshold(),
1600 evolution_sweep_interval: default_evolution_sweep_interval(),
1601 }
1602 }
1603}
1604
1605#[derive(Debug, Clone, Deserialize, Serialize)]
1614#[serde(default)]
1615pub struct GraphConfig {
1616 pub enabled: bool,
1617 pub extract_model: String,
1618 #[serde(default = "default_graph_max_entities_per_message")]
1619 pub max_entities_per_message: usize,
1620 #[serde(default = "default_graph_max_edges_per_message")]
1621 pub max_edges_per_message: usize,
1622 #[serde(default = "default_graph_community_refresh_interval")]
1623 pub community_refresh_interval: usize,
1624 #[serde(default = "default_graph_entity_similarity_threshold")]
1625 pub entity_similarity_threshold: f32,
1626 #[serde(default = "default_graph_extraction_timeout_secs")]
1627 pub extraction_timeout_secs: u64,
1628 #[serde(default)]
1629 pub use_embedding_resolution: bool,
1630 #[serde(default = "default_graph_entity_ambiguous_threshold")]
1631 pub entity_ambiguous_threshold: f32,
1632 #[serde(default = "default_graph_max_hops")]
1633 pub max_hops: u32,
1634 #[serde(default = "default_graph_recall_limit")]
1635 pub recall_limit: usize,
1636 #[serde(default = "default_graph_expired_edge_retention_days")]
1638 pub expired_edge_retention_days: u32,
1639 #[serde(default)]
1641 pub max_entities: usize,
1642 #[serde(default = "default_graph_community_summary_max_prompt_bytes")]
1644 pub community_summary_max_prompt_bytes: usize,
1645 #[serde(default = "default_graph_community_summary_concurrency")]
1647 pub community_summary_concurrency: usize,
1648 #[serde(default = "default_lpa_edge_chunk_size")]
1651 pub lpa_edge_chunk_size: usize,
1652 #[serde(
1658 default = "default_graph_temporal_decay_rate",
1659 deserialize_with = "validate_temporal_decay_rate"
1660 )]
1661 pub temporal_decay_rate: f64,
1662 #[serde(default = "default_graph_edge_history_limit")]
1668 pub edge_history_limit: usize,
1669 #[serde(default)]
1675 pub note_linking: NoteLinkingConfig,
1676 #[serde(default)]
1681 pub spreading_activation: SpreadingActivationConfig,
1682 #[serde(default)]
1687 pub retrieval_strategy: GraphRetrievalStrategy,
1688 #[serde(default)]
1690 pub strategy_classifier_provider: String,
1691 #[serde(default)]
1693 pub beam_search: BeamSearchConfig,
1694 #[serde(default)]
1696 pub watercircles: WaterCirclesConfig,
1697 #[serde(default)]
1699 pub experience: ExperienceConfig,
1700 #[serde(
1703 default = "default_link_weight_decay_lambda",
1704 deserialize_with = "validate_link_weight_decay_lambda"
1705 )]
1706 pub link_weight_decay_lambda: f64,
1707 #[serde(default = "default_link_weight_decay_interval_secs")]
1709 pub link_weight_decay_interval_secs: u64,
1710 #[serde(default)]
1716 pub belief_revision: BeliefRevisionConfig,
1717 #[serde(default)]
1722 pub rpe: RpeConfig,
1723 #[serde(default = "default_graph_pool_size")]
1729 pub pool_size: u32,
1730}
1731
1732fn default_graph_pool_size() -> u32 {
1733 3
1734}
1735
1736impl Default for GraphConfig {
1737 fn default() -> Self {
1738 Self {
1739 enabled: false,
1740 extract_model: String::new(),
1741 max_entities_per_message: default_graph_max_entities_per_message(),
1742 max_edges_per_message: default_graph_max_edges_per_message(),
1743 community_refresh_interval: default_graph_community_refresh_interval(),
1744 entity_similarity_threshold: default_graph_entity_similarity_threshold(),
1745 extraction_timeout_secs: default_graph_extraction_timeout_secs(),
1746 use_embedding_resolution: false,
1747 entity_ambiguous_threshold: default_graph_entity_ambiguous_threshold(),
1748 max_hops: default_graph_max_hops(),
1749 recall_limit: default_graph_recall_limit(),
1750 expired_edge_retention_days: default_graph_expired_edge_retention_days(),
1751 max_entities: 0,
1752 community_summary_max_prompt_bytes: default_graph_community_summary_max_prompt_bytes(),
1753 community_summary_concurrency: default_graph_community_summary_concurrency(),
1754 lpa_edge_chunk_size: default_lpa_edge_chunk_size(),
1755 temporal_decay_rate: default_graph_temporal_decay_rate(),
1756 edge_history_limit: default_graph_edge_history_limit(),
1757 note_linking: NoteLinkingConfig::default(),
1758 spreading_activation: SpreadingActivationConfig::default(),
1759 retrieval_strategy: GraphRetrievalStrategy::default(),
1760 strategy_classifier_provider: String::new(),
1761 beam_search: BeamSearchConfig::default(),
1762 watercircles: WaterCirclesConfig::default(),
1763 experience: ExperienceConfig::default(),
1764 link_weight_decay_lambda: default_link_weight_decay_lambda(),
1765 link_weight_decay_interval_secs: default_link_weight_decay_interval_secs(),
1766 belief_revision: BeliefRevisionConfig::default(),
1767 rpe: RpeConfig::default(),
1768 pool_size: default_graph_pool_size(),
1769 }
1770 }
1771}
1772
1773fn default_consolidation_confidence_threshold() -> f32 {
1774 0.7
1775}
1776
1777fn default_consolidation_sweep_interval_secs() -> u64 {
1778 3600
1779}
1780
1781fn default_consolidation_sweep_batch_size() -> usize {
1782 50
1783}
1784
1785fn default_consolidation_similarity_threshold() -> f32 {
1786 0.85
1787}
1788
1789#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1795#[serde(default)]
1796pub struct ConsolidationConfig {
1797 pub enabled: bool,
1799 #[serde(default)]
1802 pub consolidation_provider: ProviderName,
1803 #[serde(default = "default_consolidation_confidence_threshold")]
1805 pub confidence_threshold: f32,
1806 #[serde(default = "default_consolidation_sweep_interval_secs")]
1808 pub sweep_interval_secs: u64,
1809 #[serde(default = "default_consolidation_sweep_batch_size")]
1811 pub sweep_batch_size: usize,
1812 #[serde(default = "default_consolidation_similarity_threshold")]
1815 pub similarity_threshold: f32,
1816}
1817
1818impl Default for ConsolidationConfig {
1819 fn default() -> Self {
1820 Self {
1821 enabled: false,
1822 consolidation_provider: ProviderName::default(),
1823 confidence_threshold: default_consolidation_confidence_threshold(),
1824 sweep_interval_secs: default_consolidation_sweep_interval_secs(),
1825 sweep_batch_size: default_consolidation_sweep_batch_size(),
1826 similarity_threshold: default_consolidation_similarity_threshold(),
1827 }
1828 }
1829}
1830
1831fn default_link_weight_decay_lambda() -> f64 {
1832 0.95
1833}
1834
1835fn default_link_weight_decay_interval_secs() -> u64 {
1836 86400
1837}
1838
1839fn validate_link_weight_decay_lambda<'de, D>(deserializer: D) -> Result<f64, D::Error>
1840where
1841 D: serde::Deserializer<'de>,
1842{
1843 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
1844 if value.is_nan() || value.is_infinite() {
1845 return Err(serde::de::Error::custom(
1846 "link_weight_decay_lambda must be a finite number",
1847 ));
1848 }
1849 if !(value > 0.0 && value <= 1.0) {
1850 return Err(serde::de::Error::custom(
1851 "link_weight_decay_lambda must be in (0.0, 1.0]",
1852 ));
1853 }
1854 Ok(value)
1855}
1856
1857fn validate_admission_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
1858where
1859 D: serde::Deserializer<'de>,
1860{
1861 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1862 if value.is_nan() || value.is_infinite() {
1863 return Err(serde::de::Error::custom(
1864 "threshold must be a finite number",
1865 ));
1866 }
1867 if !(0.0..=1.0).contains(&value) {
1868 return Err(serde::de::Error::custom("threshold must be in [0.0, 1.0]"));
1869 }
1870 Ok(value)
1871}
1872
1873fn validate_admission_fast_path_margin<'de, D>(deserializer: D) -> Result<f32, D::Error>
1874where
1875 D: serde::Deserializer<'de>,
1876{
1877 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1878 if value.is_nan() || value.is_infinite() {
1879 return Err(serde::de::Error::custom(
1880 "fast_path_margin must be a finite number",
1881 ));
1882 }
1883 if !(0.0..=1.0).contains(&value) {
1884 return Err(serde::de::Error::custom(
1885 "fast_path_margin must be in [0.0, 1.0]",
1886 ));
1887 }
1888 Ok(value)
1889}
1890
1891fn default_admission_threshold() -> f32 {
1892 0.40
1893}
1894
1895fn default_admission_fast_path_margin() -> f32 {
1896 0.15
1897}
1898
1899fn default_rl_min_samples() -> u32 {
1900 500
1901}
1902
1903fn default_rl_retrain_interval_secs() -> u64 {
1904 3600
1905}
1906
1907#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1912#[serde(rename_all = "snake_case")]
1913pub enum AdmissionStrategy {
1914 #[default]
1916 Heuristic,
1917 Rl,
1920}
1921
1922fn validate_admission_weight<'de, D>(deserializer: D) -> Result<f32, D::Error>
1923where
1924 D: serde::Deserializer<'de>,
1925{
1926 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1927 if value < 0.0 {
1928 return Err(serde::de::Error::custom(
1929 "admission weight must be non-negative (>= 0.0)",
1930 ));
1931 }
1932 Ok(value)
1933}
1934
1935#[derive(Debug, Clone, Deserialize, Serialize)]
1940#[serde(default)]
1941pub struct AdmissionWeights {
1942 #[serde(deserialize_with = "validate_admission_weight")]
1944 pub future_utility: f32,
1945 #[serde(deserialize_with = "validate_admission_weight")]
1947 pub factual_confidence: f32,
1948 #[serde(deserialize_with = "validate_admission_weight")]
1950 pub semantic_novelty: f32,
1951 #[serde(deserialize_with = "validate_admission_weight")]
1953 pub temporal_recency: f32,
1954 #[serde(deserialize_with = "validate_admission_weight")]
1956 pub content_type_prior: f32,
1957 #[serde(deserialize_with = "validate_admission_weight")]
1961 pub goal_utility: f32,
1962}
1963
1964impl Default for AdmissionWeights {
1965 fn default() -> Self {
1966 Self {
1967 future_utility: 0.30,
1968 factual_confidence: 0.15,
1969 semantic_novelty: 0.30,
1970 temporal_recency: 0.10,
1971 content_type_prior: 0.15,
1972 goal_utility: 0.0,
1973 }
1974 }
1975}
1976
1977impl AdmissionWeights {
1978 #[must_use]
1982 pub fn normalized(&self) -> Self {
1983 let sum = self.future_utility
1984 + self.factual_confidence
1985 + self.semantic_novelty
1986 + self.temporal_recency
1987 + self.content_type_prior
1988 + self.goal_utility;
1989 if sum <= f32::EPSILON {
1990 return Self::default();
1991 }
1992 Self {
1993 future_utility: self.future_utility / sum,
1994 factual_confidence: self.factual_confidence / sum,
1995 semantic_novelty: self.semantic_novelty / sum,
1996 temporal_recency: self.temporal_recency / sum,
1997 content_type_prior: self.content_type_prior / sum,
1998 goal_utility: self.goal_utility / sum,
1999 }
2000 }
2001}
2002
2003#[derive(Debug, Clone, Deserialize, Serialize)]
2008#[serde(default)]
2009pub struct AdmissionConfig {
2010 pub enabled: bool,
2012 #[serde(deserialize_with = "validate_admission_threshold")]
2015 pub threshold: f32,
2016 #[serde(deserialize_with = "validate_admission_fast_path_margin")]
2019 pub fast_path_margin: f32,
2020 pub admission_provider: ProviderName,
2023 pub weights: AdmissionWeights,
2025 #[serde(default)]
2027 pub admission_strategy: AdmissionStrategy,
2028 #[serde(default = "default_rl_min_samples")]
2031 pub rl_min_samples: u32,
2032 #[serde(default = "default_rl_retrain_interval_secs")]
2034 pub rl_retrain_interval_secs: u64,
2035 #[serde(default)]
2039 pub goal_conditioned_write: bool,
2040 #[serde(default)]
2044 pub goal_utility_provider: ProviderName,
2045 #[serde(default = "default_goal_utility_threshold")]
2048 pub goal_utility_threshold: f32,
2049 #[serde(default = "default_goal_utility_weight")]
2052 pub goal_utility_weight: f32,
2053}
2054
2055fn default_goal_utility_threshold() -> f32 {
2056 0.4
2057}
2058
2059fn default_goal_utility_weight() -> f32 {
2060 0.25
2061}
2062
2063impl Default for AdmissionConfig {
2064 fn default() -> Self {
2065 Self {
2066 enabled: false,
2067 threshold: default_admission_threshold(),
2068 fast_path_margin: default_admission_fast_path_margin(),
2069 admission_provider: ProviderName::default(),
2070 weights: AdmissionWeights::default(),
2071 admission_strategy: AdmissionStrategy::default(),
2072 rl_min_samples: default_rl_min_samples(),
2073 rl_retrain_interval_secs: default_rl_retrain_interval_secs(),
2074 goal_conditioned_write: false,
2075 goal_utility_provider: ProviderName::default(),
2076 goal_utility_threshold: default_goal_utility_threshold(),
2077 goal_utility_weight: default_goal_utility_weight(),
2078 }
2079 }
2080}
2081
2082#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
2084#[serde(rename_all = "snake_case")]
2085pub enum StoreRoutingStrategy {
2086 #[default]
2088 Heuristic,
2089 Llm,
2091 Hybrid,
2093}
2094
2095#[derive(Debug, Clone, Deserialize, Serialize)]
2100#[serde(default)]
2101pub struct StoreRoutingConfig {
2102 pub enabled: bool,
2105 pub strategy: StoreRoutingStrategy,
2107 pub routing_classifier_provider: ProviderName,
2110 pub fallback_route: String,
2113 pub confidence_threshold: f32,
2116}
2117
2118impl Default for StoreRoutingConfig {
2119 fn default() -> Self {
2120 Self {
2121 enabled: false,
2122 strategy: StoreRoutingStrategy::Heuristic,
2123 routing_classifier_provider: ProviderName::default(),
2124 fallback_route: "hybrid".into(),
2125 confidence_threshold: 0.7,
2126 }
2127 }
2128}
2129
2130#[derive(Debug, Clone, Deserialize, Serialize)]
2135#[serde(default)]
2136pub struct PersonaConfig {
2137 pub enabled: bool,
2139 pub persona_provider: ProviderName,
2142 pub min_confidence: f64,
2144 pub min_messages: usize,
2146 pub max_messages: usize,
2148 pub extraction_timeout_secs: u64,
2150 pub context_budget_tokens: usize,
2152}
2153
2154impl Default for PersonaConfig {
2155 fn default() -> Self {
2156 Self {
2157 enabled: false,
2158 persona_provider: ProviderName::default(),
2159 min_confidence: 0.6,
2160 min_messages: 3,
2161 max_messages: 10,
2162 extraction_timeout_secs: 10,
2163 context_budget_tokens: 500,
2164 }
2165 }
2166}
2167
2168#[derive(Debug, Clone, Deserialize, Serialize)]
2174#[serde(default)]
2175pub struct TrajectoryConfig {
2176 pub enabled: bool,
2178 pub trajectory_provider: ProviderName,
2181 pub context_budget_tokens: usize,
2183 pub max_messages: usize,
2185 pub extraction_timeout_secs: u64,
2187 pub recall_top_k: usize,
2189 pub min_confidence: f64,
2191}
2192
2193impl Default for TrajectoryConfig {
2194 fn default() -> Self {
2195 Self {
2196 enabled: false,
2197 trajectory_provider: ProviderName::default(),
2198 context_budget_tokens: 400,
2199 max_messages: 10,
2200 extraction_timeout_secs: 10,
2201 recall_top_k: 5,
2202 min_confidence: 0.6,
2203 }
2204 }
2205}
2206
2207#[derive(Debug, Clone, Deserialize, Serialize)]
2213#[serde(default)]
2214pub struct CategoryConfig {
2215 pub enabled: bool,
2217 pub auto_tag: bool,
2219}
2220
2221impl Default for CategoryConfig {
2222 fn default() -> Self {
2223 Self {
2224 enabled: false,
2225 auto_tag: true,
2226 }
2227 }
2228}
2229
2230#[derive(Debug, Clone, Deserialize, Serialize)]
2236#[serde(default)]
2237pub struct TreeConfig {
2238 pub enabled: bool,
2240 pub consolidation_provider: ProviderName,
2243 pub sweep_interval_secs: u64,
2245 pub batch_size: usize,
2247 pub similarity_threshold: f32,
2249 pub max_level: u32,
2251 pub context_budget_tokens: usize,
2253 pub recall_top_k: usize,
2255 pub min_cluster_size: usize,
2257}
2258
2259impl Default for TreeConfig {
2260 fn default() -> Self {
2261 Self {
2262 enabled: false,
2263 consolidation_provider: ProviderName::default(),
2264 sweep_interval_secs: 300,
2265 batch_size: 20,
2266 similarity_threshold: 0.8,
2267 max_level: 3,
2268 context_budget_tokens: 400,
2269 recall_top_k: 5,
2270 min_cluster_size: 2,
2271 }
2272 }
2273}
2274
2275#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
2281#[serde(default)]
2282pub struct MicrocompactConfig {
2283 pub enabled: bool,
2285 pub gap_threshold_minutes: u32,
2287 pub keep_recent: usize,
2289}
2290
2291impl Default for MicrocompactConfig {
2292 fn default() -> Self {
2293 Self {
2294 enabled: false,
2295 gap_threshold_minutes: 60,
2296 keep_recent: 3,
2297 }
2298 }
2299}
2300
2301#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
2306#[serde(default)]
2307pub struct AutoDreamConfig {
2308 pub enabled: bool,
2310 pub min_sessions: u32,
2312 pub min_hours: u32,
2314 pub consolidation_provider: ProviderName,
2317 pub max_iterations: u8,
2319}
2320
2321impl Default for AutoDreamConfig {
2322 fn default() -> Self {
2323 Self {
2324 enabled: false,
2325 min_sessions: 3,
2326 min_hours: 24,
2327 consolidation_provider: ProviderName::default(),
2328 max_iterations: 8,
2329 }
2330 }
2331}
2332
2333#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
2338#[serde(default)]
2339pub struct MagicDocsConfig {
2340 pub enabled: bool,
2342 pub min_turns_between_updates: u32,
2344 pub update_provider: ProviderName,
2347 pub max_iterations: u8,
2349}
2350
2351impl Default for MagicDocsConfig {
2352 fn default() -> Self {
2353 Self {
2354 enabled: false,
2355 min_turns_between_updates: 5,
2356 update_provider: ProviderName::default(),
2357 max_iterations: 4,
2358 }
2359 }
2360}
2361
2362#[cfg(test)]
2363mod tests {
2364 use super::*;
2365
2366 #[test]
2369 fn pruning_strategy_toml_task_aware_mig_falls_back_to_reactive() {
2370 #[derive(serde::Deserialize)]
2371 struct Wrapper {
2372 #[allow(dead_code)]
2373 pruning_strategy: PruningStrategy,
2374 }
2375 let toml = r#"pruning_strategy = "task_aware_mig""#;
2376 let w: Wrapper = toml::from_str(toml).expect("should deserialize without error");
2377 assert_eq!(
2378 w.pruning_strategy,
2379 PruningStrategy::Reactive,
2380 "task_aware_mig must fall back to Reactive"
2381 );
2382 }
2383
2384 #[test]
2385 fn pruning_strategy_toml_round_trip() {
2386 #[derive(serde::Deserialize)]
2387 struct Wrapper {
2388 #[allow(dead_code)]
2389 pruning_strategy: PruningStrategy,
2390 }
2391 for (input, expected) in [
2392 ("reactive", PruningStrategy::Reactive),
2393 ("task_aware", PruningStrategy::TaskAware),
2394 ("mig", PruningStrategy::Mig),
2395 ] {
2396 let toml = format!(r#"pruning_strategy = "{input}""#);
2397 let w: Wrapper = toml::from_str(&toml)
2398 .unwrap_or_else(|e| panic!("failed to deserialize `{input}`: {e}"));
2399 assert_eq!(w.pruning_strategy, expected, "mismatch for `{input}`");
2400 }
2401 }
2402
2403 #[test]
2404 fn pruning_strategy_toml_unknown_value_errors() {
2405 #[derive(serde::Deserialize)]
2406 #[allow(dead_code)]
2407 struct Wrapper {
2408 pruning_strategy: PruningStrategy,
2409 }
2410 let toml = r#"pruning_strategy = "nonexistent_strategy""#;
2411 assert!(
2412 toml::from_str::<Wrapper>(toml).is_err(),
2413 "unknown strategy must produce an error"
2414 );
2415 }
2416
2417 #[test]
2418 fn tier_config_defaults_are_correct() {
2419 let cfg = TierConfig::default();
2420 assert!(!cfg.enabled);
2421 assert_eq!(cfg.promotion_min_sessions, 3);
2422 assert!((cfg.similarity_threshold - 0.92).abs() < f32::EPSILON);
2423 assert_eq!(cfg.sweep_interval_secs, 3600);
2424 assert_eq!(cfg.sweep_batch_size, 100);
2425 }
2426
2427 #[test]
2428 fn tier_config_rejects_min_sessions_below_2() {
2429 let toml = "promotion_min_sessions = 1";
2430 assert!(toml::from_str::<TierConfig>(toml).is_err());
2431 }
2432
2433 #[test]
2434 fn tier_config_rejects_similarity_threshold_below_0_5() {
2435 let toml = "similarity_threshold = 0.4";
2436 assert!(toml::from_str::<TierConfig>(toml).is_err());
2437 }
2438
2439 #[test]
2440 fn tier_config_rejects_zero_sweep_batch_size() {
2441 let toml = "sweep_batch_size = 0";
2442 assert!(toml::from_str::<TierConfig>(toml).is_err());
2443 }
2444
2445 fn deserialize_importance_weight(toml_val: &str) -> Result<SemanticConfig, toml::de::Error> {
2446 let input = format!("importance_weight = {toml_val}");
2447 toml::from_str::<SemanticConfig>(&input)
2448 }
2449
2450 #[test]
2451 fn importance_weight_default_is_0_15() {
2452 let cfg = SemanticConfig::default();
2453 assert!((cfg.importance_weight - 0.15).abs() < f64::EPSILON);
2454 }
2455
2456 #[test]
2457 fn importance_weight_valid_zero() {
2458 let cfg = deserialize_importance_weight("0.0").unwrap();
2459 assert!((cfg.importance_weight - 0.0_f64).abs() < f64::EPSILON);
2460 }
2461
2462 #[test]
2463 fn importance_weight_valid_one() {
2464 let cfg = deserialize_importance_weight("1.0").unwrap();
2465 assert!((cfg.importance_weight - 1.0_f64).abs() < f64::EPSILON);
2466 }
2467
2468 #[test]
2469 fn importance_weight_rejects_near_zero_negative() {
2470 let result = deserialize_importance_weight("-0.01");
2475 assert!(
2476 result.is_err(),
2477 "negative importance_weight must be rejected"
2478 );
2479 }
2480
2481 #[test]
2482 fn importance_weight_rejects_negative() {
2483 let result = deserialize_importance_weight("-1.0");
2484 assert!(result.is_err(), "negative value must be rejected");
2485 }
2486
2487 #[test]
2488 fn importance_weight_rejects_greater_than_one() {
2489 let result = deserialize_importance_weight("1.01");
2490 assert!(result.is_err(), "value > 1.0 must be rejected");
2491 }
2492
2493 #[test]
2497 fn admission_weights_normalized_sums_to_one() {
2498 let w = AdmissionWeights {
2499 future_utility: 2.0,
2500 factual_confidence: 1.0,
2501 semantic_novelty: 3.0,
2502 temporal_recency: 1.0,
2503 content_type_prior: 3.0,
2504 goal_utility: 0.0,
2505 };
2506 let n = w.normalized();
2507 let sum = n.future_utility
2508 + n.factual_confidence
2509 + n.semantic_novelty
2510 + n.temporal_recency
2511 + n.content_type_prior;
2512 assert!(
2513 (sum - 1.0).abs() < 0.001,
2514 "normalized weights must sum to 1.0, got {sum}"
2515 );
2516 }
2517
2518 #[test]
2520 fn admission_weights_normalized_preserves_already_unit_sum() {
2521 let w = AdmissionWeights::default();
2522 let n = w.normalized();
2523 let sum = n.future_utility
2524 + n.factual_confidence
2525 + n.semantic_novelty
2526 + n.temporal_recency
2527 + n.content_type_prior;
2528 assert!(
2529 (sum - 1.0).abs() < 0.001,
2530 "default weights sum to ~1.0 after normalization"
2531 );
2532 }
2533
2534 #[test]
2536 fn admission_weights_normalized_zero_sum_falls_back_to_default() {
2537 let w = AdmissionWeights {
2538 future_utility: 0.0,
2539 factual_confidence: 0.0,
2540 semantic_novelty: 0.0,
2541 temporal_recency: 0.0,
2542 content_type_prior: 0.0,
2543 goal_utility: 0.0,
2544 };
2545 let n = w.normalized();
2546 let default = AdmissionWeights::default();
2547 assert!(
2548 (n.future_utility - default.future_utility).abs() < 0.001,
2549 "zero-sum weights must fall back to defaults"
2550 );
2551 }
2552
2553 #[test]
2555 fn admission_config_defaults() {
2556 let cfg = AdmissionConfig::default();
2557 assert!(!cfg.enabled);
2558 assert!((cfg.threshold - 0.40).abs() < 0.001);
2559 assert!((cfg.fast_path_margin - 0.15).abs() < 0.001);
2560 assert!(cfg.admission_provider.is_empty());
2561 }
2562
2563 #[test]
2566 fn spreading_activation_default_recall_timeout_ms_is_1000() {
2567 let cfg = SpreadingActivationConfig::default();
2568 assert_eq!(
2569 cfg.recall_timeout_ms, 1000,
2570 "default recall_timeout_ms must be 1000ms"
2571 );
2572 }
2573
2574 #[test]
2575 fn spreading_activation_toml_recall_timeout_ms_round_trip() {
2576 #[derive(serde::Deserialize)]
2577 struct Wrapper {
2578 recall_timeout_ms: u64,
2579 }
2580 let toml = "recall_timeout_ms = 500";
2581 let w: Wrapper = toml::from_str(toml).unwrap();
2582 assert_eq!(w.recall_timeout_ms, 500);
2583 }
2584
2585 #[test]
2586 fn spreading_activation_validate_cross_field_constraints() {
2587 let mut cfg = SpreadingActivationConfig::default();
2588 assert!(cfg.validate().is_ok());
2590
2591 cfg.activation_threshold = 0.5;
2593 cfg.inhibition_threshold = 0.5;
2594 assert!(cfg.validate().is_err());
2595 }
2596
2597 #[test]
2600 fn compression_config_focus_strategy_deserializes() {
2601 let toml = r#"strategy = "focus""#;
2602 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2603 assert_eq!(cfg.strategy, CompressionStrategy::Focus);
2604 }
2605
2606 #[test]
2607 fn compression_config_density_budget_defaults_on_deserialize() {
2608 let toml = r#"strategy = "reactive""#;
2611 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2612 assert!((cfg.high_density_budget - 0.7).abs() < 1e-6);
2613 assert!((cfg.low_density_budget - 0.3).abs() < 1e-6);
2614 }
2615
2616 #[test]
2617 fn compression_config_density_budget_round_trip() {
2618 let toml = "strategy = \"reactive\"\nhigh_density_budget = 0.6\nlow_density_budget = 0.4";
2619 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2620 assert!((cfg.high_density_budget - 0.6).abs() < f32::EPSILON);
2621 assert!((cfg.low_density_budget - 0.4).abs() < f32::EPSILON);
2622 }
2623
2624 #[test]
2625 fn compression_config_focus_scorer_provider_default_empty() {
2626 let cfg = CompressionConfig::default();
2627 assert!(cfg.focus_scorer_provider.is_empty());
2628 }
2629
2630 #[test]
2631 fn compression_config_focus_scorer_provider_round_trip() {
2632 let toml = "strategy = \"focus\"\nfocus_scorer_provider = \"fast\"";
2633 let cfg: CompressionConfig = toml::from_str(toml).unwrap();
2634 assert_eq!(cfg.focus_scorer_provider.as_str(), "fast");
2635 }
2636}
2637
2638#[derive(Debug, Clone, Deserialize, Serialize)]
2658#[serde(default)]
2659pub struct ReasoningConfig {
2660 pub enabled: bool,
2662 pub extract_provider: ProviderName,
2665 pub distill_provider: ProviderName,
2668 pub top_k: usize,
2670 pub store_limit: usize,
2672 pub max_messages: usize,
2674 pub max_message_chars: usize,
2676 pub context_budget_tokens: usize,
2678 pub min_messages: usize,
2680 pub extraction_timeout_secs: u64,
2682 pub distill_timeout_secs: u64,
2684 pub self_judge_window: usize,
2688 pub min_assistant_chars: usize,
2691}
2692
2693impl Default for ReasoningConfig {
2694 fn default() -> Self {
2695 Self {
2696 enabled: false,
2697 extract_provider: ProviderName::default(),
2698 distill_provider: ProviderName::default(),
2699 top_k: 3,
2700 store_limit: 1000,
2701 max_messages: 6,
2702 max_message_chars: 2000,
2703 context_budget_tokens: 500,
2704 min_messages: 2,
2705 extraction_timeout_secs: 30,
2706 distill_timeout_secs: 30,
2707 self_judge_window: 2,
2708 min_assistant_chars: 50,
2709 }
2710 }
2711}