Skip to main content

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}