1use serde::{Deserialize, Serialize};
5
6use crate::defaults::{default_sqlite_path_field, default_true};
7
8fn default_sqlite_pool_size() -> u32 {
9 5
10}
11
12fn default_max_history() -> usize {
13 100
14}
15
16fn default_title_max_chars() -> usize {
17 60
18}
19
20fn default_document_collection() -> String {
21 "zeph_documents".into()
22}
23
24fn default_document_chunk_size() -> usize {
25 1000
26}
27
28fn default_document_chunk_overlap() -> usize {
29 100
30}
31
32fn default_document_top_k() -> usize {
33 3
34}
35
36fn default_autosave_min_length() -> usize {
37 20
38}
39
40fn default_tool_call_cutoff() -> usize {
41 6
42}
43
44fn default_token_safety_margin() -> f32 {
45 1.0
46}
47
48fn default_redact_credentials() -> bool {
49 true
50}
51
52fn default_qdrant_url() -> String {
53 "http://localhost:6334".into()
54}
55
56fn default_summarization_threshold() -> usize {
57 50
58}
59
60fn default_context_budget_tokens() -> usize {
61 0
62}
63
64fn default_soft_compaction_threshold() -> f32 {
65 0.60
66}
67
68fn default_hard_compaction_threshold() -> f32 {
69 0.90
70}
71
72fn default_compaction_preserve_tail() -> usize {
73 6
74}
75
76fn default_compaction_cooldown_turns() -> u8 {
77 2
78}
79
80fn default_auto_budget() -> bool {
81 true
82}
83
84fn default_prune_protect_tokens() -> usize {
85 40_000
86}
87
88fn default_cross_session_score_threshold() -> f32 {
89 0.35
90}
91
92fn default_temporal_decay_half_life_days() -> u32 {
93 30
94}
95
96fn default_mmr_lambda() -> f32 {
97 0.7
98}
99
100fn default_semantic_enabled() -> bool {
101 true
102}
103
104fn default_recall_limit() -> usize {
105 5
106}
107
108fn default_vector_weight() -> f64 {
109 0.7
110}
111
112fn default_keyword_weight() -> f64 {
113 0.3
114}
115
116fn default_graph_max_entities_per_message() -> usize {
117 10
118}
119
120fn default_graph_max_edges_per_message() -> usize {
121 15
122}
123
124fn default_graph_community_refresh_interval() -> usize {
125 100
126}
127
128fn default_graph_community_summary_max_prompt_bytes() -> usize {
129 8192
130}
131
132fn default_graph_community_summary_concurrency() -> usize {
133 4
134}
135
136fn default_lpa_edge_chunk_size() -> usize {
137 10_000
138}
139
140fn default_graph_entity_similarity_threshold() -> f32 {
141 0.85
142}
143
144fn default_graph_entity_ambiguous_threshold() -> f32 {
145 0.70
146}
147
148fn default_graph_extraction_timeout_secs() -> u64 {
149 15
150}
151
152fn default_graph_max_hops() -> u32 {
153 2
154}
155
156fn default_graph_recall_limit() -> usize {
157 10
158}
159
160fn default_graph_expired_edge_retention_days() -> u32 {
161 90
162}
163
164fn default_graph_temporal_decay_rate() -> f64 {
165 0.0
166}
167
168fn default_graph_edge_history_limit() -> usize {
169 100
170}
171
172fn default_spreading_activation_decay_lambda() -> f32 {
173 0.85
174}
175
176fn default_spreading_activation_max_hops() -> u32 {
177 3
178}
179
180fn default_spreading_activation_activation_threshold() -> f32 {
181 0.1
182}
183
184fn default_spreading_activation_inhibition_threshold() -> f32 {
185 0.8
186}
187
188fn default_spreading_activation_max_activated_nodes() -> usize {
189 50
190}
191
192fn default_spreading_activation_recall_timeout_ms() -> u64 {
193 1000
194}
195
196fn default_note_linking_similarity_threshold() -> f32 {
197 0.85
198}
199
200fn default_note_linking_top_k() -> usize {
201 10
202}
203
204fn default_note_linking_timeout_secs() -> u64 {
205 5
206}
207
208fn default_shutdown_summary() -> bool {
209 true
210}
211
212fn default_shutdown_summary_min_messages() -> usize {
213 4
214}
215
216fn default_shutdown_summary_max_messages() -> usize {
217 20
218}
219
220fn default_shutdown_summary_timeout_secs() -> u64 {
221 10
222}
223
224fn validate_tier_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
225where
226 D: serde::Deserializer<'de>,
227{
228 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
229 if value.is_nan() || value.is_infinite() {
230 return Err(serde::de::Error::custom(
231 "similarity_threshold must be a finite number",
232 ));
233 }
234 if !(0.5..=1.0).contains(&value) {
235 return Err(serde::de::Error::custom(
236 "similarity_threshold must be in [0.5, 1.0]",
237 ));
238 }
239 Ok(value)
240}
241
242fn validate_tier_promotion_min_sessions<'de, D>(deserializer: D) -> Result<u32, D::Error>
243where
244 D: serde::Deserializer<'de>,
245{
246 let value = <u32 as serde::Deserialize>::deserialize(deserializer)?;
247 if value < 2 {
248 return Err(serde::de::Error::custom(
249 "promotion_min_sessions must be >= 2",
250 ));
251 }
252 Ok(value)
253}
254
255fn validate_tier_sweep_batch_size<'de, D>(deserializer: D) -> Result<usize, D::Error>
256where
257 D: serde::Deserializer<'de>,
258{
259 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
260 if value == 0 {
261 return Err(serde::de::Error::custom("sweep_batch_size must be >= 1"));
262 }
263 Ok(value)
264}
265
266fn default_tier_promotion_min_sessions() -> u32 {
267 3
268}
269
270fn default_tier_similarity_threshold() -> f32 {
271 0.92
272}
273
274fn default_tier_sweep_interval_secs() -> u64 {
275 3600
276}
277
278fn default_tier_sweep_batch_size() -> usize {
279 100
280}
281
282fn default_scene_similarity_threshold() -> f32 {
283 0.80
284}
285
286fn default_scene_batch_size() -> usize {
287 50
288}
289
290fn validate_scene_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
291where
292 D: serde::Deserializer<'de>,
293{
294 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
295 if value.is_nan() || value.is_infinite() {
296 return Err(serde::de::Error::custom(
297 "scene_similarity_threshold must be a finite number",
298 ));
299 }
300 if !(0.5..=1.0).contains(&value) {
301 return Err(serde::de::Error::custom(
302 "scene_similarity_threshold must be in [0.5, 1.0]",
303 ));
304 }
305 Ok(value)
306}
307
308fn validate_scene_batch_size<'de, D>(deserializer: D) -> Result<usize, D::Error>
309where
310 D: serde::Deserializer<'de>,
311{
312 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
313 if value == 0 {
314 return Err(serde::de::Error::custom("scene_batch_size must be >= 1"));
315 }
316 Ok(value)
317}
318
319#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
333#[serde(default)]
334pub struct TierConfig {
335 pub enabled: bool,
338 #[serde(deserialize_with = "validate_tier_promotion_min_sessions")]
341 pub promotion_min_sessions: u32,
342 #[serde(deserialize_with = "validate_tier_similarity_threshold")]
345 pub similarity_threshold: f32,
346 pub sweep_interval_secs: u64,
348 #[serde(deserialize_with = "validate_tier_sweep_batch_size")]
350 pub sweep_batch_size: usize,
351 pub scene_enabled: bool,
353 #[serde(deserialize_with = "validate_scene_similarity_threshold")]
355 pub scene_similarity_threshold: f32,
356 #[serde(deserialize_with = "validate_scene_batch_size")]
358 pub scene_batch_size: usize,
359 pub scene_provider: String,
362 pub scene_sweep_interval_secs: u64,
364}
365
366fn default_scene_sweep_interval_secs() -> u64 {
367 7200
368}
369
370impl Default for TierConfig {
371 fn default() -> Self {
372 Self {
373 enabled: false,
374 promotion_min_sessions: default_tier_promotion_min_sessions(),
375 similarity_threshold: default_tier_similarity_threshold(),
376 sweep_interval_secs: default_tier_sweep_interval_secs(),
377 sweep_batch_size: default_tier_sweep_batch_size(),
378 scene_enabled: false,
379 scene_similarity_threshold: default_scene_similarity_threshold(),
380 scene_batch_size: default_scene_batch_size(),
381 scene_provider: String::new(),
382 scene_sweep_interval_secs: default_scene_sweep_interval_secs(),
383 }
384 }
385}
386
387fn validate_temporal_decay_rate<'de, D>(deserializer: D) -> Result<f64, D::Error>
388where
389 D: serde::Deserializer<'de>,
390{
391 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
392 if value.is_nan() || value.is_infinite() {
393 return Err(serde::de::Error::custom(
394 "temporal_decay_rate must be a finite number",
395 ));
396 }
397 if !(0.0..=10.0).contains(&value) {
398 return Err(serde::de::Error::custom(
399 "temporal_decay_rate must be in [0.0, 10.0]",
400 ));
401 }
402 Ok(value)
403}
404
405fn validate_similarity_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
406where
407 D: serde::Deserializer<'de>,
408{
409 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
410 if value.is_nan() || value.is_infinite() {
411 return Err(serde::de::Error::custom(
412 "similarity_threshold must be a finite number",
413 ));
414 }
415 if !(0.0..=1.0).contains(&value) {
416 return Err(serde::de::Error::custom(
417 "similarity_threshold must be in [0.0, 1.0]",
418 ));
419 }
420 Ok(value)
421}
422
423fn validate_importance_weight<'de, D>(deserializer: D) -> Result<f64, D::Error>
424where
425 D: serde::Deserializer<'de>,
426{
427 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
428 if value.is_nan() || value.is_infinite() {
429 return Err(serde::de::Error::custom(
430 "importance_weight must be a finite number",
431 ));
432 }
433 if value < 0.0 {
434 return Err(serde::de::Error::custom(
435 "importance_weight must be non-negative",
436 ));
437 }
438 if value > 1.0 {
439 return Err(serde::de::Error::custom("importance_weight must be <= 1.0"));
440 }
441 Ok(value)
442}
443
444fn default_importance_weight() -> f64 {
445 0.15
446}
447
448#[derive(Debug, Clone, Deserialize, Serialize)]
462#[serde(default)]
463pub struct SpreadingActivationConfig {
464 pub enabled: bool,
466 #[serde(deserialize_with = "validate_decay_lambda")]
468 pub decay_lambda: f32,
469 #[serde(deserialize_with = "validate_max_hops")]
471 pub max_hops: u32,
472 pub activation_threshold: f32,
474 pub inhibition_threshold: f32,
476 pub max_activated_nodes: usize,
478 #[serde(default = "default_seed_structural_weight")]
480 pub seed_structural_weight: f32,
481 #[serde(default = "default_seed_community_cap")]
483 pub seed_community_cap: usize,
484 #[serde(default = "default_spreading_activation_recall_timeout_ms")]
488 pub recall_timeout_ms: u64,
489}
490
491fn validate_decay_lambda<'de, D>(deserializer: D) -> Result<f32, D::Error>
492where
493 D: serde::Deserializer<'de>,
494{
495 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
496 if value.is_nan() || value.is_infinite() {
497 return Err(serde::de::Error::custom(
498 "decay_lambda must be a finite number",
499 ));
500 }
501 if !(value > 0.0 && value <= 1.0) {
502 return Err(serde::de::Error::custom(
503 "decay_lambda must be in (0.0, 1.0]",
504 ));
505 }
506 Ok(value)
507}
508
509fn validate_max_hops<'de, D>(deserializer: D) -> Result<u32, D::Error>
510where
511 D: serde::Deserializer<'de>,
512{
513 let value = <u32 as serde::Deserialize>::deserialize(deserializer)?;
514 if value == 0 {
515 return Err(serde::de::Error::custom("max_hops must be >= 1"));
516 }
517 Ok(value)
518}
519
520impl SpreadingActivationConfig {
521 pub fn validate(&self) -> Result<(), String> {
527 if self.activation_threshold >= self.inhibition_threshold {
528 return Err(format!(
529 "activation_threshold ({}) must be < inhibition_threshold ({})",
530 self.activation_threshold, self.inhibition_threshold
531 ));
532 }
533 Ok(())
534 }
535}
536
537fn default_seed_structural_weight() -> f32 {
538 0.4
539}
540
541fn default_seed_community_cap() -> usize {
542 3
543}
544
545impl Default for SpreadingActivationConfig {
546 fn default() -> Self {
547 Self {
548 enabled: false,
549 decay_lambda: default_spreading_activation_decay_lambda(),
550 max_hops: default_spreading_activation_max_hops(),
551 activation_threshold: default_spreading_activation_activation_threshold(),
552 inhibition_threshold: default_spreading_activation_inhibition_threshold(),
553 max_activated_nodes: default_spreading_activation_max_activated_nodes(),
554 seed_structural_weight: default_seed_structural_weight(),
555 seed_community_cap: default_seed_community_cap(),
556 recall_timeout_ms: default_spreading_activation_recall_timeout_ms(),
557 }
558 }
559}
560
561#[derive(Debug, Clone, Deserialize, Serialize)]
563#[serde(default)]
564pub struct BeliefRevisionConfig {
565 pub enabled: bool,
567 #[serde(deserialize_with = "validate_similarity_threshold")]
570 pub similarity_threshold: f32,
571}
572
573fn default_belief_revision_similarity_threshold() -> f32 {
574 0.85
575}
576
577impl Default for BeliefRevisionConfig {
578 fn default() -> Self {
579 Self {
580 enabled: false,
581 similarity_threshold: default_belief_revision_similarity_threshold(),
582 }
583 }
584}
585
586#[derive(Debug, Clone, Deserialize, Serialize)]
588#[serde(default)]
589pub struct RpeConfig {
590 pub enabled: bool,
592 #[serde(deserialize_with = "validate_similarity_threshold")]
595 pub threshold: f32,
596 pub max_skip_turns: u32,
598}
599
600fn default_rpe_threshold() -> f32 {
601 0.3
602}
603
604fn default_rpe_max_skip_turns() -> u32 {
605 5
606}
607
608impl Default for RpeConfig {
609 fn default() -> Self {
610 Self {
611 enabled: false,
612 threshold: default_rpe_threshold(),
613 max_skip_turns: default_rpe_max_skip_turns(),
614 }
615 }
616}
617
618#[derive(Debug, Clone, Deserialize, Serialize)]
624#[serde(default)]
625pub struct NoteLinkingConfig {
626 pub enabled: bool,
628 #[serde(deserialize_with = "validate_similarity_threshold")]
630 pub similarity_threshold: f32,
631 pub top_k: usize,
633 pub timeout_secs: u64,
635}
636
637impl Default for NoteLinkingConfig {
638 fn default() -> Self {
639 Self {
640 enabled: false,
641 similarity_threshold: default_note_linking_similarity_threshold(),
642 top_k: default_note_linking_top_k(),
643 timeout_secs: default_note_linking_timeout_secs(),
644 }
645 }
646}
647
648#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
650#[serde(rename_all = "lowercase")]
651pub enum VectorBackend {
652 Qdrant,
653 #[default]
654 Sqlite,
655}
656
657impl VectorBackend {
658 #[must_use]
659 pub fn as_str(&self) -> &'static str {
660 match self {
661 Self::Qdrant => "qdrant",
662 Self::Sqlite => "sqlite",
663 }
664 }
665}
666
667#[derive(Debug, Deserialize, Serialize)]
668#[allow(clippy::struct_excessive_bools)]
669pub struct MemoryConfig {
670 #[serde(default)]
671 pub compression_guidelines: zeph_memory::CompressionGuidelinesConfig,
672 #[serde(default = "default_sqlite_path_field")]
673 pub sqlite_path: String,
674 pub history_limit: u32,
675 #[serde(default = "default_qdrant_url")]
676 pub qdrant_url: String,
677 #[serde(default)]
678 pub semantic: SemanticConfig,
679 #[serde(default = "default_summarization_threshold")]
680 pub summarization_threshold: usize,
681 #[serde(default = "default_context_budget_tokens")]
682 pub context_budget_tokens: usize,
683 #[serde(default = "default_soft_compaction_threshold")]
684 pub soft_compaction_threshold: f32,
685 #[serde(
686 default = "default_hard_compaction_threshold",
687 alias = "compaction_threshold"
688 )]
689 pub hard_compaction_threshold: f32,
690 #[serde(default = "default_compaction_preserve_tail")]
691 pub compaction_preserve_tail: usize,
692 #[serde(default = "default_compaction_cooldown_turns")]
693 pub compaction_cooldown_turns: u8,
694 #[serde(default = "default_auto_budget")]
695 pub auto_budget: bool,
696 #[serde(default = "default_prune_protect_tokens")]
697 pub prune_protect_tokens: usize,
698 #[serde(default = "default_cross_session_score_threshold")]
699 pub cross_session_score_threshold: f32,
700 #[serde(default)]
701 pub vector_backend: VectorBackend,
702 #[serde(default = "default_token_safety_margin")]
703 pub token_safety_margin: f32,
704 #[serde(default = "default_redact_credentials")]
705 pub redact_credentials: bool,
706 #[serde(default = "default_true")]
707 pub autosave_assistant: bool,
708 #[serde(default = "default_autosave_min_length")]
709 pub autosave_min_length: usize,
710 #[serde(default = "default_tool_call_cutoff")]
711 pub tool_call_cutoff: usize,
712 #[serde(default = "default_sqlite_pool_size")]
713 pub sqlite_pool_size: u32,
714 #[serde(default)]
715 pub sessions: SessionsConfig,
716 #[serde(default)]
717 pub documents: DocumentConfig,
718 #[serde(default)]
719 pub eviction: zeph_memory::EvictionConfig,
720 #[serde(default)]
721 pub compression: CompressionConfig,
722 #[serde(default)]
723 pub sidequest: SidequestConfig,
724 #[serde(default)]
725 pub graph: GraphConfig,
726 #[serde(default = "default_shutdown_summary")]
730 pub shutdown_summary: bool,
731 #[serde(default = "default_shutdown_summary_min_messages")]
734 pub shutdown_summary_min_messages: usize,
735 #[serde(default = "default_shutdown_summary_max_messages")]
739 pub shutdown_summary_max_messages: usize,
740 #[serde(default = "default_shutdown_summary_timeout_secs")]
744 pub shutdown_summary_timeout_secs: u64,
745 #[serde(default)]
751 pub structured_summaries: bool,
752 #[serde(default)]
757 pub tiers: TierConfig,
758 #[serde(default)]
763 pub admission: AdmissionConfig,
764 #[serde(default)]
766 pub digest: DigestConfig,
767 #[serde(default)]
769 pub context_strategy: ContextStrategy,
770 #[serde(default = "default_crossover_turn_threshold")]
772 pub crossover_turn_threshold: u32,
773 #[serde(default)]
778 pub consolidation: ConsolidationConfig,
779 #[serde(default)]
786 pub database_url: Option<String>,
787 #[serde(default)]
792 pub store_routing: StoreRoutingConfig,
793}
794
795fn default_crossover_turn_threshold() -> u32 {
796 20
797}
798
799#[derive(Debug, Clone, Deserialize, Serialize)]
801#[serde(default)]
802pub struct DigestConfig {
803 pub enabled: bool,
805 pub provider: String,
808 pub max_tokens: usize,
810 pub max_input_messages: usize,
812}
813
814impl Default for DigestConfig {
815 fn default() -> Self {
816 Self {
817 enabled: false,
818 provider: String::new(),
819 max_tokens: 500,
820 max_input_messages: 50,
821 }
822 }
823}
824
825#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
827#[serde(rename_all = "snake_case")]
828pub enum ContextStrategy {
829 #[default]
832 FullHistory,
833 MemoryFirst,
836 Adaptive,
839}
840
841#[derive(Debug, Clone, Deserialize, Serialize)]
842#[serde(default)]
843pub struct SessionsConfig {
844 #[serde(default = "default_max_history")]
846 pub max_history: usize,
847 #[serde(default = "default_title_max_chars")]
849 pub title_max_chars: usize,
850}
851
852impl Default for SessionsConfig {
853 fn default() -> Self {
854 Self {
855 max_history: default_max_history(),
856 title_max_chars: default_title_max_chars(),
857 }
858 }
859}
860
861#[derive(Debug, Clone, Deserialize, Serialize)]
863pub struct DocumentConfig {
864 #[serde(default = "default_document_collection")]
865 pub collection: String,
866 #[serde(default = "default_document_chunk_size")]
867 pub chunk_size: usize,
868 #[serde(default = "default_document_chunk_overlap")]
869 pub chunk_overlap: usize,
870 #[serde(default = "default_document_top_k")]
872 pub top_k: usize,
873 #[serde(default)]
875 pub rag_enabled: bool,
876}
877
878impl Default for DocumentConfig {
879 fn default() -> Self {
880 Self {
881 collection: default_document_collection(),
882 chunk_size: default_document_chunk_size(),
883 chunk_overlap: default_document_chunk_overlap(),
884 top_k: default_document_top_k(),
885 rag_enabled: false,
886 }
887 }
888}
889
890#[derive(Debug, Deserialize, Serialize)]
891#[allow(clippy::struct_excessive_bools)]
892pub struct SemanticConfig {
893 #[serde(default = "default_semantic_enabled")]
894 pub enabled: bool,
895 #[serde(default = "default_recall_limit")]
896 pub recall_limit: usize,
897 #[serde(default = "default_vector_weight")]
898 pub vector_weight: f64,
899 #[serde(default = "default_keyword_weight")]
900 pub keyword_weight: f64,
901 #[serde(default = "default_true")]
902 pub temporal_decay_enabled: bool,
903 #[serde(default = "default_temporal_decay_half_life_days")]
904 pub temporal_decay_half_life_days: u32,
905 #[serde(default = "default_true")]
906 pub mmr_enabled: bool,
907 #[serde(default = "default_mmr_lambda")]
908 pub mmr_lambda: f32,
909 #[serde(default = "default_true")]
910 pub importance_enabled: bool,
911 #[serde(
912 default = "default_importance_weight",
913 deserialize_with = "validate_importance_weight"
914 )]
915 pub importance_weight: f64,
916}
917
918impl Default for SemanticConfig {
919 fn default() -> Self {
920 Self {
921 enabled: default_semantic_enabled(),
922 recall_limit: default_recall_limit(),
923 vector_weight: default_vector_weight(),
924 keyword_weight: default_keyword_weight(),
925 temporal_decay_enabled: true,
926 temporal_decay_half_life_days: default_temporal_decay_half_life_days(),
927 mmr_enabled: true,
928 mmr_lambda: default_mmr_lambda(),
929 importance_enabled: true,
930 importance_weight: default_importance_weight(),
931 }
932 }
933}
934
935#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
937#[serde(tag = "strategy", rename_all = "snake_case")]
938pub enum CompressionStrategy {
939 #[default]
941 Reactive,
942 Proactive {
944 threshold_tokens: usize,
946 max_summary_tokens: usize,
948 },
949 Autonomous,
952}
953
954#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
959#[serde(rename_all = "snake_case")]
960pub enum PruningStrategy {
961 #[default]
963 Reactive,
964 TaskAware,
967 Mig,
970 Subgoal,
974 SubgoalMig,
977}
978
979impl PruningStrategy {
980 #[must_use]
982 pub fn is_subgoal(self) -> bool {
983 matches!(self, Self::Subgoal | Self::SubgoalMig)
984 }
985}
986
987impl<'de> serde::Deserialize<'de> for PruningStrategy {
990 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
991 let s = String::deserialize(deserializer)?;
992 s.parse().map_err(serde::de::Error::custom)
993 }
994}
995
996impl std::str::FromStr for PruningStrategy {
997 type Err = String;
998
999 fn from_str(s: &str) -> Result<Self, Self::Err> {
1000 match s {
1001 "reactive" => Ok(Self::Reactive),
1002 "task_aware" | "task-aware" => Ok(Self::TaskAware),
1003 "mig" => Ok(Self::Mig),
1004 "task_aware_mig" | "task-aware-mig" => {
1007 tracing::warn!(
1008 "pruning strategy `task_aware_mig` has been removed; \
1009 falling back to `reactive`. Use `task_aware` or `mig` instead."
1010 );
1011 Ok(Self::Reactive)
1012 }
1013 "subgoal" => Ok(Self::Subgoal),
1014 "subgoal_mig" | "subgoal-mig" => Ok(Self::SubgoalMig),
1015 other => Err(format!(
1016 "unknown pruning strategy `{other}`, expected \
1017 reactive|task_aware|mig|subgoal|subgoal_mig"
1018 )),
1019 }
1020 }
1021}
1022
1023#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1025#[serde(default)]
1026pub struct CompressionConfig {
1027 #[serde(flatten)]
1029 pub strategy: CompressionStrategy,
1030 pub pruning_strategy: PruningStrategy,
1032 pub model: String,
1037 pub compress_provider: String,
1040 #[serde(default)]
1042 pub probe: zeph_memory::CompactionProbeConfig,
1043 #[serde(default)]
1051 pub archive_tool_outputs: bool,
1052}
1053
1054fn default_sidequest_interval_turns() -> u32 {
1055 4
1056}
1057
1058fn default_sidequest_max_eviction_ratio() -> f32 {
1059 0.5
1060}
1061
1062fn default_sidequest_max_cursors() -> usize {
1063 30
1064}
1065
1066fn default_sidequest_min_cursor_tokens() -> usize {
1067 100
1068}
1069
1070#[derive(Debug, Clone, Deserialize, Serialize)]
1072#[serde(default)]
1073pub struct SidequestConfig {
1074 pub enabled: bool,
1076 #[serde(default = "default_sidequest_interval_turns")]
1078 pub interval_turns: u32,
1079 #[serde(default = "default_sidequest_max_eviction_ratio")]
1081 pub max_eviction_ratio: f32,
1082 #[serde(default = "default_sidequest_max_cursors")]
1084 pub max_cursors: usize,
1085 #[serde(default = "default_sidequest_min_cursor_tokens")]
1088 pub min_cursor_tokens: usize,
1089}
1090
1091impl Default for SidequestConfig {
1092 fn default() -> Self {
1093 Self {
1094 enabled: false,
1095 interval_turns: default_sidequest_interval_turns(),
1096 max_eviction_ratio: default_sidequest_max_eviction_ratio(),
1097 max_cursors: default_sidequest_max_cursors(),
1098 min_cursor_tokens: default_sidequest_min_cursor_tokens(),
1099 }
1100 }
1101}
1102
1103#[derive(Debug, Clone, Deserialize, Serialize)]
1112#[serde(default)]
1113pub struct GraphConfig {
1114 pub enabled: bool,
1115 pub extract_model: String,
1116 #[serde(default = "default_graph_max_entities_per_message")]
1117 pub max_entities_per_message: usize,
1118 #[serde(default = "default_graph_max_edges_per_message")]
1119 pub max_edges_per_message: usize,
1120 #[serde(default = "default_graph_community_refresh_interval")]
1121 pub community_refresh_interval: usize,
1122 #[serde(default = "default_graph_entity_similarity_threshold")]
1123 pub entity_similarity_threshold: f32,
1124 #[serde(default = "default_graph_extraction_timeout_secs")]
1125 pub extraction_timeout_secs: u64,
1126 #[serde(default)]
1127 pub use_embedding_resolution: bool,
1128 #[serde(default = "default_graph_entity_ambiguous_threshold")]
1129 pub entity_ambiguous_threshold: f32,
1130 #[serde(default = "default_graph_max_hops")]
1131 pub max_hops: u32,
1132 #[serde(default = "default_graph_recall_limit")]
1133 pub recall_limit: usize,
1134 #[serde(default = "default_graph_expired_edge_retention_days")]
1136 pub expired_edge_retention_days: u32,
1137 #[serde(default)]
1139 pub max_entities: usize,
1140 #[serde(default = "default_graph_community_summary_max_prompt_bytes")]
1142 pub community_summary_max_prompt_bytes: usize,
1143 #[serde(default = "default_graph_community_summary_concurrency")]
1145 pub community_summary_concurrency: usize,
1146 #[serde(default = "default_lpa_edge_chunk_size")]
1149 pub lpa_edge_chunk_size: usize,
1150 #[serde(
1156 default = "default_graph_temporal_decay_rate",
1157 deserialize_with = "validate_temporal_decay_rate"
1158 )]
1159 pub temporal_decay_rate: f64,
1160 #[serde(default = "default_graph_edge_history_limit")]
1166 pub edge_history_limit: usize,
1167 #[serde(default)]
1173 pub note_linking: NoteLinkingConfig,
1174 #[serde(default)]
1179 pub spreading_activation: SpreadingActivationConfig,
1180 #[serde(
1183 default = "default_link_weight_decay_lambda",
1184 deserialize_with = "validate_link_weight_decay_lambda"
1185 )]
1186 pub link_weight_decay_lambda: f64,
1187 #[serde(default = "default_link_weight_decay_interval_secs")]
1189 pub link_weight_decay_interval_secs: u64,
1190 #[serde(default)]
1196 pub belief_revision: BeliefRevisionConfig,
1197 #[serde(default)]
1202 pub rpe: RpeConfig,
1203}
1204
1205impl Default for GraphConfig {
1206 fn default() -> Self {
1207 Self {
1208 enabled: false,
1209 extract_model: String::new(),
1210 max_entities_per_message: default_graph_max_entities_per_message(),
1211 max_edges_per_message: default_graph_max_edges_per_message(),
1212 community_refresh_interval: default_graph_community_refresh_interval(),
1213 entity_similarity_threshold: default_graph_entity_similarity_threshold(),
1214 extraction_timeout_secs: default_graph_extraction_timeout_secs(),
1215 use_embedding_resolution: false,
1216 entity_ambiguous_threshold: default_graph_entity_ambiguous_threshold(),
1217 max_hops: default_graph_max_hops(),
1218 recall_limit: default_graph_recall_limit(),
1219 expired_edge_retention_days: default_graph_expired_edge_retention_days(),
1220 max_entities: 0,
1221 community_summary_max_prompt_bytes: default_graph_community_summary_max_prompt_bytes(),
1222 community_summary_concurrency: default_graph_community_summary_concurrency(),
1223 lpa_edge_chunk_size: default_lpa_edge_chunk_size(),
1224 temporal_decay_rate: default_graph_temporal_decay_rate(),
1225 edge_history_limit: default_graph_edge_history_limit(),
1226 note_linking: NoteLinkingConfig::default(),
1227 spreading_activation: SpreadingActivationConfig::default(),
1228 link_weight_decay_lambda: default_link_weight_decay_lambda(),
1229 link_weight_decay_interval_secs: default_link_weight_decay_interval_secs(),
1230 belief_revision: BeliefRevisionConfig::default(),
1231 rpe: RpeConfig::default(),
1232 }
1233 }
1234}
1235
1236fn default_consolidation_confidence_threshold() -> f32 {
1237 0.7
1238}
1239
1240fn default_consolidation_sweep_interval_secs() -> u64 {
1241 3600
1242}
1243
1244fn default_consolidation_sweep_batch_size() -> usize {
1245 50
1246}
1247
1248fn default_consolidation_similarity_threshold() -> f32 {
1249 0.85
1250}
1251
1252#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1258#[serde(default)]
1259pub struct ConsolidationConfig {
1260 pub enabled: bool,
1262 #[serde(default)]
1265 pub consolidation_provider: String,
1266 #[serde(default = "default_consolidation_confidence_threshold")]
1268 pub confidence_threshold: f32,
1269 #[serde(default = "default_consolidation_sweep_interval_secs")]
1271 pub sweep_interval_secs: u64,
1272 #[serde(default = "default_consolidation_sweep_batch_size")]
1274 pub sweep_batch_size: usize,
1275 #[serde(default = "default_consolidation_similarity_threshold")]
1278 pub similarity_threshold: f32,
1279}
1280
1281impl Default for ConsolidationConfig {
1282 fn default() -> Self {
1283 Self {
1284 enabled: false,
1285 consolidation_provider: String::new(),
1286 confidence_threshold: default_consolidation_confidence_threshold(),
1287 sweep_interval_secs: default_consolidation_sweep_interval_secs(),
1288 sweep_batch_size: default_consolidation_sweep_batch_size(),
1289 similarity_threshold: default_consolidation_similarity_threshold(),
1290 }
1291 }
1292}
1293
1294fn default_link_weight_decay_lambda() -> f64 {
1295 0.95
1296}
1297
1298fn default_link_weight_decay_interval_secs() -> u64 {
1299 86400
1300}
1301
1302fn validate_link_weight_decay_lambda<'de, D>(deserializer: D) -> Result<f64, D::Error>
1303where
1304 D: serde::Deserializer<'de>,
1305{
1306 let value = <f64 as serde::Deserialize>::deserialize(deserializer)?;
1307 if value.is_nan() || value.is_infinite() {
1308 return Err(serde::de::Error::custom(
1309 "link_weight_decay_lambda must be a finite number",
1310 ));
1311 }
1312 if !(value > 0.0 && value <= 1.0) {
1313 return Err(serde::de::Error::custom(
1314 "link_weight_decay_lambda must be in (0.0, 1.0]",
1315 ));
1316 }
1317 Ok(value)
1318}
1319
1320fn validate_admission_threshold<'de, D>(deserializer: D) -> Result<f32, D::Error>
1321where
1322 D: serde::Deserializer<'de>,
1323{
1324 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1325 if value.is_nan() || value.is_infinite() {
1326 return Err(serde::de::Error::custom(
1327 "threshold must be a finite number",
1328 ));
1329 }
1330 if !(0.0..=1.0).contains(&value) {
1331 return Err(serde::de::Error::custom("threshold must be in [0.0, 1.0]"));
1332 }
1333 Ok(value)
1334}
1335
1336fn validate_admission_fast_path_margin<'de, D>(deserializer: D) -> Result<f32, D::Error>
1337where
1338 D: serde::Deserializer<'de>,
1339{
1340 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1341 if value.is_nan() || value.is_infinite() {
1342 return Err(serde::de::Error::custom(
1343 "fast_path_margin must be a finite number",
1344 ));
1345 }
1346 if !(0.0..=1.0).contains(&value) {
1347 return Err(serde::de::Error::custom(
1348 "fast_path_margin must be in [0.0, 1.0]",
1349 ));
1350 }
1351 Ok(value)
1352}
1353
1354fn default_admission_threshold() -> f32 {
1355 0.40
1356}
1357
1358fn default_admission_fast_path_margin() -> f32 {
1359 0.15
1360}
1361
1362fn default_rl_min_samples() -> u32 {
1363 500
1364}
1365
1366fn default_rl_retrain_interval_secs() -> u64 {
1367 3600
1368}
1369
1370#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
1375#[serde(rename_all = "snake_case")]
1376pub enum AdmissionStrategy {
1377 #[default]
1379 Heuristic,
1380 Rl,
1383}
1384
1385fn validate_admission_weight<'de, D>(deserializer: D) -> Result<f32, D::Error>
1386where
1387 D: serde::Deserializer<'de>,
1388{
1389 let value = <f32 as serde::Deserialize>::deserialize(deserializer)?;
1390 if value < 0.0 {
1391 return Err(serde::de::Error::custom(
1392 "admission weight must be non-negative (>= 0.0)",
1393 ));
1394 }
1395 Ok(value)
1396}
1397
1398#[derive(Debug, Clone, Deserialize, Serialize)]
1403#[serde(default)]
1404pub struct AdmissionWeights {
1405 #[serde(deserialize_with = "validate_admission_weight")]
1407 pub future_utility: f32,
1408 #[serde(deserialize_with = "validate_admission_weight")]
1410 pub factual_confidence: f32,
1411 #[serde(deserialize_with = "validate_admission_weight")]
1413 pub semantic_novelty: f32,
1414 #[serde(deserialize_with = "validate_admission_weight")]
1416 pub temporal_recency: f32,
1417 #[serde(deserialize_with = "validate_admission_weight")]
1419 pub content_type_prior: f32,
1420 #[serde(deserialize_with = "validate_admission_weight")]
1424 pub goal_utility: f32,
1425}
1426
1427impl Default for AdmissionWeights {
1428 fn default() -> Self {
1429 Self {
1430 future_utility: 0.30,
1431 factual_confidence: 0.15,
1432 semantic_novelty: 0.30,
1433 temporal_recency: 0.10,
1434 content_type_prior: 0.15,
1435 goal_utility: 0.0,
1436 }
1437 }
1438}
1439
1440impl AdmissionWeights {
1441 #[must_use]
1445 pub fn normalized(&self) -> Self {
1446 let sum = self.future_utility
1447 + self.factual_confidence
1448 + self.semantic_novelty
1449 + self.temporal_recency
1450 + self.content_type_prior
1451 + self.goal_utility;
1452 if sum <= f32::EPSILON {
1453 return Self::default();
1454 }
1455 Self {
1456 future_utility: self.future_utility / sum,
1457 factual_confidence: self.factual_confidence / sum,
1458 semantic_novelty: self.semantic_novelty / sum,
1459 temporal_recency: self.temporal_recency / sum,
1460 content_type_prior: self.content_type_prior / sum,
1461 goal_utility: self.goal_utility / sum,
1462 }
1463 }
1464}
1465
1466#[derive(Debug, Clone, Deserialize, Serialize)]
1471#[serde(default)]
1472pub struct AdmissionConfig {
1473 pub enabled: bool,
1475 #[serde(deserialize_with = "validate_admission_threshold")]
1478 pub threshold: f32,
1479 #[serde(deserialize_with = "validate_admission_fast_path_margin")]
1482 pub fast_path_margin: f32,
1483 pub admission_provider: String,
1486 pub weights: AdmissionWeights,
1488 #[serde(default)]
1490 pub admission_strategy: AdmissionStrategy,
1491 #[serde(default = "default_rl_min_samples")]
1494 pub rl_min_samples: u32,
1495 #[serde(default = "default_rl_retrain_interval_secs")]
1497 pub rl_retrain_interval_secs: u64,
1498 #[serde(default)]
1502 pub goal_conditioned_write: bool,
1503 #[serde(default)]
1507 pub goal_utility_provider: String,
1508 #[serde(default = "default_goal_utility_threshold")]
1511 pub goal_utility_threshold: f32,
1512 #[serde(default = "default_goal_utility_weight")]
1515 pub goal_utility_weight: f32,
1516}
1517
1518fn default_goal_utility_threshold() -> f32 {
1519 0.4
1520}
1521
1522fn default_goal_utility_weight() -> f32 {
1523 0.25
1524}
1525
1526impl Default for AdmissionConfig {
1527 fn default() -> Self {
1528 Self {
1529 enabled: false,
1530 threshold: default_admission_threshold(),
1531 fast_path_margin: default_admission_fast_path_margin(),
1532 admission_provider: String::new(),
1533 weights: AdmissionWeights::default(),
1534 admission_strategy: AdmissionStrategy::default(),
1535 rl_min_samples: default_rl_min_samples(),
1536 rl_retrain_interval_secs: default_rl_retrain_interval_secs(),
1537 goal_conditioned_write: false,
1538 goal_utility_provider: String::new(),
1539 goal_utility_threshold: default_goal_utility_threshold(),
1540 goal_utility_weight: default_goal_utility_weight(),
1541 }
1542 }
1543}
1544
1545#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
1547#[serde(rename_all = "snake_case")]
1548pub enum StoreRoutingStrategy {
1549 #[default]
1551 Heuristic,
1552 Llm,
1554 Hybrid,
1556}
1557
1558#[derive(Debug, Clone, Deserialize, Serialize)]
1563#[serde(default)]
1564pub struct StoreRoutingConfig {
1565 pub enabled: bool,
1568 pub strategy: StoreRoutingStrategy,
1570 pub routing_classifier_provider: String,
1573 pub fallback_route: String,
1576 pub confidence_threshold: f32,
1579}
1580
1581impl Default for StoreRoutingConfig {
1582 fn default() -> Self {
1583 Self {
1584 enabled: false,
1585 strategy: StoreRoutingStrategy::Heuristic,
1586 routing_classifier_provider: String::new(),
1587 fallback_route: "hybrid".into(),
1588 confidence_threshold: 0.7,
1589 }
1590 }
1591}
1592
1593#[cfg(test)]
1594mod tests {
1595 use super::*;
1596
1597 #[test]
1600 fn pruning_strategy_toml_task_aware_mig_falls_back_to_reactive() {
1601 #[derive(serde::Deserialize)]
1602 struct Wrapper {
1603 #[allow(dead_code)]
1604 pruning_strategy: PruningStrategy,
1605 }
1606 let toml = r#"pruning_strategy = "task_aware_mig""#;
1607 let w: Wrapper = toml::from_str(toml).expect("should deserialize without error");
1608 assert_eq!(
1609 w.pruning_strategy,
1610 PruningStrategy::Reactive,
1611 "task_aware_mig must fall back to Reactive"
1612 );
1613 }
1614
1615 #[test]
1616 fn pruning_strategy_toml_round_trip() {
1617 #[derive(serde::Deserialize)]
1618 struct Wrapper {
1619 #[allow(dead_code)]
1620 pruning_strategy: PruningStrategy,
1621 }
1622 for (input, expected) in [
1623 ("reactive", PruningStrategy::Reactive),
1624 ("task_aware", PruningStrategy::TaskAware),
1625 ("mig", PruningStrategy::Mig),
1626 ] {
1627 let toml = format!(r#"pruning_strategy = "{input}""#);
1628 let w: Wrapper = toml::from_str(&toml)
1629 .unwrap_or_else(|e| panic!("failed to deserialize `{input}`: {e}"));
1630 assert_eq!(w.pruning_strategy, expected, "mismatch for `{input}`");
1631 }
1632 }
1633
1634 #[test]
1635 fn pruning_strategy_toml_unknown_value_errors() {
1636 #[derive(serde::Deserialize)]
1637 #[allow(dead_code)]
1638 struct Wrapper {
1639 pruning_strategy: PruningStrategy,
1640 }
1641 let toml = r#"pruning_strategy = "nonexistent_strategy""#;
1642 assert!(
1643 toml::from_str::<Wrapper>(toml).is_err(),
1644 "unknown strategy must produce an error"
1645 );
1646 }
1647
1648 #[test]
1649 fn tier_config_defaults_are_correct() {
1650 let cfg = TierConfig::default();
1651 assert!(!cfg.enabled);
1652 assert_eq!(cfg.promotion_min_sessions, 3);
1653 assert!((cfg.similarity_threshold - 0.92).abs() < f32::EPSILON);
1654 assert_eq!(cfg.sweep_interval_secs, 3600);
1655 assert_eq!(cfg.sweep_batch_size, 100);
1656 }
1657
1658 #[test]
1659 fn tier_config_rejects_min_sessions_below_2() {
1660 let toml = "promotion_min_sessions = 1";
1661 assert!(toml::from_str::<TierConfig>(toml).is_err());
1662 }
1663
1664 #[test]
1665 fn tier_config_rejects_similarity_threshold_below_0_5() {
1666 let toml = "similarity_threshold = 0.4";
1667 assert!(toml::from_str::<TierConfig>(toml).is_err());
1668 }
1669
1670 #[test]
1671 fn tier_config_rejects_zero_sweep_batch_size() {
1672 let toml = "sweep_batch_size = 0";
1673 assert!(toml::from_str::<TierConfig>(toml).is_err());
1674 }
1675
1676 fn deserialize_importance_weight(toml_val: &str) -> Result<SemanticConfig, toml::de::Error> {
1677 let input = format!("importance_weight = {toml_val}");
1678 toml::from_str::<SemanticConfig>(&input)
1679 }
1680
1681 #[test]
1682 fn importance_weight_default_is_0_15() {
1683 let cfg = SemanticConfig::default();
1684 assert!((cfg.importance_weight - 0.15).abs() < f64::EPSILON);
1685 }
1686
1687 #[test]
1688 fn importance_weight_valid_zero() {
1689 let cfg = deserialize_importance_weight("0.0").unwrap();
1690 assert!((cfg.importance_weight - 0.0_f64).abs() < f64::EPSILON);
1691 }
1692
1693 #[test]
1694 fn importance_weight_valid_one() {
1695 let cfg = deserialize_importance_weight("1.0").unwrap();
1696 assert!((cfg.importance_weight - 1.0_f64).abs() < f64::EPSILON);
1697 }
1698
1699 #[test]
1700 fn importance_weight_rejects_near_zero_negative() {
1701 let result = deserialize_importance_weight("-0.01");
1706 assert!(
1707 result.is_err(),
1708 "negative importance_weight must be rejected"
1709 );
1710 }
1711
1712 #[test]
1713 fn importance_weight_rejects_negative() {
1714 let result = deserialize_importance_weight("-1.0");
1715 assert!(result.is_err(), "negative value must be rejected");
1716 }
1717
1718 #[test]
1719 fn importance_weight_rejects_greater_than_one() {
1720 let result = deserialize_importance_weight("1.01");
1721 assert!(result.is_err(), "value > 1.0 must be rejected");
1722 }
1723
1724 #[test]
1728 fn admission_weights_normalized_sums_to_one() {
1729 let w = AdmissionWeights {
1730 future_utility: 2.0,
1731 factual_confidence: 1.0,
1732 semantic_novelty: 3.0,
1733 temporal_recency: 1.0,
1734 content_type_prior: 3.0,
1735 goal_utility: 0.0,
1736 };
1737 let n = w.normalized();
1738 let sum = n.future_utility
1739 + n.factual_confidence
1740 + n.semantic_novelty
1741 + n.temporal_recency
1742 + n.content_type_prior;
1743 assert!(
1744 (sum - 1.0).abs() < 0.001,
1745 "normalized weights must sum to 1.0, got {sum}"
1746 );
1747 }
1748
1749 #[test]
1751 fn admission_weights_normalized_preserves_already_unit_sum() {
1752 let w = AdmissionWeights::default();
1753 let n = w.normalized();
1754 let sum = n.future_utility
1755 + n.factual_confidence
1756 + n.semantic_novelty
1757 + n.temporal_recency
1758 + n.content_type_prior;
1759 assert!(
1760 (sum - 1.0).abs() < 0.001,
1761 "default weights sum to ~1.0 after normalization"
1762 );
1763 }
1764
1765 #[test]
1767 fn admission_weights_normalized_zero_sum_falls_back_to_default() {
1768 let w = AdmissionWeights {
1769 future_utility: 0.0,
1770 factual_confidence: 0.0,
1771 semantic_novelty: 0.0,
1772 temporal_recency: 0.0,
1773 content_type_prior: 0.0,
1774 goal_utility: 0.0,
1775 };
1776 let n = w.normalized();
1777 let default = AdmissionWeights::default();
1778 assert!(
1779 (n.future_utility - default.future_utility).abs() < 0.001,
1780 "zero-sum weights must fall back to defaults"
1781 );
1782 }
1783
1784 #[test]
1786 fn admission_config_defaults() {
1787 let cfg = AdmissionConfig::default();
1788 assert!(!cfg.enabled);
1789 assert!((cfg.threshold - 0.40).abs() < 0.001);
1790 assert!((cfg.fast_path_margin - 0.15).abs() < 0.001);
1791 assert!(cfg.admission_provider.is_empty());
1792 }
1793
1794 #[test]
1797 fn spreading_activation_default_recall_timeout_ms_is_1000() {
1798 let cfg = SpreadingActivationConfig::default();
1799 assert_eq!(
1800 cfg.recall_timeout_ms, 1000,
1801 "default recall_timeout_ms must be 1000ms"
1802 );
1803 }
1804
1805 #[test]
1806 fn spreading_activation_toml_recall_timeout_ms_round_trip() {
1807 #[derive(serde::Deserialize)]
1808 struct Wrapper {
1809 recall_timeout_ms: u64,
1810 }
1811 let toml = "recall_timeout_ms = 500";
1812 let w: Wrapper = toml::from_str(toml).unwrap();
1813 assert_eq!(w.recall_timeout_ms, 500);
1814 }
1815
1816 #[test]
1817 fn spreading_activation_validate_cross_field_constraints() {
1818 let mut cfg = SpreadingActivationConfig::default();
1819 assert!(cfg.validate().is_ok());
1821
1822 cfg.activation_threshold = 0.5;
1824 cfg.inhibition_threshold = 0.5;
1825 assert!(cfg.validate().is_err());
1826 }
1827}