zeph_config/memory/hebbian.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Hebbian / APEX synaptic-learning configuration.
5//!
6//! Edge-weight plasticity ([`HebbianConfig`]), reward-prediction-error gating
7//! ([`RpeConfig`]), belief revision, write gating, and conflict-recency tuning.
8
9use crate::providers::ProviderName;
10use serde::{Deserialize, Serialize};
11use zeph_common::memory::EdgeType;
12
13fn default_write_gate_min_edge_relevance() -> f32 {
14 0.3
15}
16
17fn default_conflict_recency_slow_threshold() -> f32 {
18 0.2
19}
20
21/// `MemORAI` write-gate prefilter configuration (#3709).
22///
23/// When `enabled = true`, low-signal edges (confidence below threshold + generic relation type)
24/// are silently dropped before write, reducing noise in the knowledge graph.
25///
26/// TOML path: `[memory.graph.write_gate]`
27#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
28#[serde(default)]
29pub struct WriteGateConfig {
30 /// Enable write-gate prefilter. Default: `false` (opt-in).
31 pub enabled: bool,
32 /// Minimum edge confidence to pass the gate when the relation is low-signal. Default: `0.3`.
33 ///
34 /// Range: `[0.0, 1.0]`.
35 #[serde(
36 default = "default_write_gate_min_edge_relevance",
37 deserialize_with = "crate::de_helpers::de_unit_closed"
38 )]
39 pub min_edge_relevance: f32,
40}
41
42impl Default for WriteGateConfig {
43 fn default() -> Self {
44 Self {
45 enabled: false,
46 min_edge_relevance: default_write_gate_min_edge_relevance(),
47 }
48 }
49}
50
51/// Recency fallback threshold for the conflict resolver (#3709).
52///
53/// TOML path: `[memory.graph.conflict]`
54#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
55#[serde(default)]
56pub struct ConflictRecencyConfig {
57 /// Minimum `confidence_slow` for the recency strategy to prefer an edge. Default: `0.2`.
58 ///
59 /// When two cardinality-1 heads conflict and recency is the resolution strategy,
60 /// only edges with `confidence_slow >= recency_slow_threshold` are preferred by recency;
61 /// edges below the threshold fall back to `valid_from` comparison. Range: `[0.0, 1.0]`.
62 #[serde(
63 default = "default_conflict_recency_slow_threshold",
64 deserialize_with = "crate::de_helpers::de_unit_closed"
65 )]
66 pub recency_slow_threshold: f32,
67}
68
69impl Default for ConflictRecencyConfig {
70 fn default() -> Self {
71 Self {
72 recency_slow_threshold: default_conflict_recency_slow_threshold(),
73 }
74 }
75}
76
77/// Kumiho belief revision configuration.
78#[derive(Debug, Clone, Deserialize, Serialize)]
79#[serde(default)]
80pub struct BeliefRevisionConfig {
81 /// Enable semantic contradiction detection for graph edges. Default: `false`.
82 pub enabled: bool,
83 /// Cosine similarity threshold for considering two facts as contradictory.
84 /// Only edges with similarity >= this value are candidates for revision. Default: `0.85`.
85 #[serde(deserialize_with = "crate::de_helpers::de_unit_closed")]
86 pub similarity_threshold: f32,
87}
88
89fn default_belief_revision_similarity_threshold() -> f32 {
90 0.85
91}
92
93impl Default for BeliefRevisionConfig {
94 fn default() -> Self {
95 Self {
96 enabled: false,
97 similarity_threshold: default_belief_revision_similarity_threshold(),
98 }
99 }
100}
101
102/// D-MEM RPE-based tiered graph extraction routing configuration.
103#[derive(Debug, Clone, Deserialize, Serialize)]
104#[serde(default)]
105pub struct RpeConfig {
106 /// Enable RPE-based routing to skip extraction on low-surprise turns. Default: `false`.
107 pub enabled: bool,
108 /// RPE threshold. Turns with RPE < this value skip graph extraction. Range: `[0.0, 1.0]`.
109 /// Default: `0.3`.
110 #[serde(deserialize_with = "crate::de_helpers::de_unit_closed")]
111 pub threshold: f32,
112 /// Maximum consecutive turns to skip before forcing extraction (safety valve). Default: `5`.
113 pub max_skip_turns: u32,
114}
115
116fn default_rpe_threshold() -> f32 {
117 0.3
118}
119
120fn default_rpe_max_skip_turns() -> u32 {
121 5
122}
123
124impl Default for RpeConfig {
125 fn default() -> Self {
126 Self {
127 enabled: false,
128 threshold: default_rpe_threshold(),
129 max_skip_turns: default_rpe_max_skip_turns(),
130 }
131 }
132}
133
134/// Hebbian edge-weight reinforcement and consolidation configuration (HL-F1/F2/F3/F4, #3344/#3345).
135///
136/// Controls opt-in Hebbian learning on knowledge-graph edges. When enabled, every
137/// recall traversal increments the `weight` column of the traversed edges, building
138/// a usage-frequency signal into the graph. The consolidation sub-feature (HL-F3/F4)
139/// runs a background sweep that identifies high-traffic entity clusters and distills
140/// them into `graph_rules` entries via an LLM.
141#[derive(Debug, Clone, Deserialize, Serialize)]
142#[serde(default)]
143pub struct HebbianConfig {
144 /// Master switch. When `false`, no `weight` updates are written to the database
145 /// and the consolidation loop does not start. Default: `false`.
146 pub enabled: bool,
147 /// Weight increment per co-activation (HL-F2, #3344).
148 ///
149 /// Typical range: `0.01`–`0.5`. A value of `0.0` is accepted but logs a `WARN` at
150 /// startup when `enabled = true`. Default: `0.1`.
151 pub hebbian_lr: f32,
152 /// How often the consolidation sweep runs, in seconds (HL-F3, #3345).
153 ///
154 /// Set to `0` to disable the consolidation loop while keeping Hebbian updates active.
155 /// Default: `3600` (one hour).
156 pub consolidation_interval_secs: u64,
157 /// Minimum `degree × avg_weight` score for an entity to qualify as a consolidation
158 /// candidate (HL-F3, #3345). Default: `5.0`.
159 pub consolidation_threshold: f64,
160 /// Provider name (from `[[llm.providers]]`) used for cluster distillation (HL-F4, #3345).
161 ///
162 /// Falls back to the main provider when `None` or unresolvable.
163 #[serde(default)]
164 pub consolidate_provider: Option<ProviderName>,
165 /// Maximum number of candidates processed per sweep (HL-F3, #3345). Default: `10`.
166 pub max_candidates_per_sweep: usize,
167 /// Minimum seconds between consecutive consolidations of the same entity (HL-F3, #3345).
168 ///
169 /// An entity is skipped if its `consolidated_at` timestamp is within this window.
170 /// Default: `86400` (24 hours).
171 pub consolidation_cooldown_secs: u64,
172 /// LLM prompt timeout for a single distillation call, in seconds (HL-F4, #3345).
173 /// Default: `30`.
174 pub consolidation_prompt_timeout_secs: u64,
175 /// Maximum number of neighbouring entity summaries passed to the LLM per candidate
176 /// (HL-F4, #3345). Default: `20`.
177 pub consolidation_max_neighbors: usize,
178 /// Enable HL-F5 spreading activation from the top-1 ANN anchor (HL-F5, #3346).
179 ///
180 /// When `true` and `enabled = true`, `recall_graph_hela` performs BFS from the
181 /// nearest entity anchor, scoring nodes by `path_weight × cosine`. Default: `false`.
182 pub spreading_activation: bool,
183 /// BFS depth for HL-F5 spreading activation. Clamped to `[1, 6]`. Default: `2`.
184 pub spread_depth: u32,
185 /// MAGMA edge-type filter for HL-F5 spreading activation.
186 ///
187 /// Accepted values: `"semantic"`, `"temporal"`, `"causal"`, `"entity"`.
188 /// Empty = traverse all edge types. Default: `[]`.
189 pub spread_edge_types: Vec<EdgeType>,
190 /// Per-step circuit-breaker timeout for HL-F5 in milliseconds.
191 ///
192 /// Any internal step (anchor ANN, edges batch, vectors batch) that exceeds this
193 /// duration triggers an `Ok(Vec::new())` fallback with a `WARN`. Default: `80`
194 /// (headroom over realistic local-Qdrant round-trip latency; the previous `8`
195 /// default aborted almost every call even when Qdrant was healthy, #5785).
196 pub step_budget_ms: u64,
197 /// Timeout for the initial query embedding call in HL-F5, in seconds.
198 ///
199 /// `0` disables the timeout. Default: `5`.
200 pub embed_timeout_secs: u64,
201}
202
203impl Default for HebbianConfig {
204 fn default() -> Self {
205 Self {
206 enabled: false,
207 hebbian_lr: 0.1,
208 consolidation_interval_secs: 3600,
209 consolidation_threshold: 5.0,
210 consolidate_provider: None,
211 max_candidates_per_sweep: 10,
212 consolidation_cooldown_secs: 86_400,
213 consolidation_prompt_timeout_secs: 30,
214 consolidation_max_neighbors: 20,
215 spreading_activation: false,
216 spread_depth: 2,
217 spread_edge_types: Vec::new(),
218 step_budget_ms: 80,
219 embed_timeout_secs: 5,
220 }
221 }
222}