Skip to main content

hirn_engine/consolidation/
evolution.rs

1use super::*;
2
3// ═══════════════════════════════════════════════════════════════════════════
4// Memory Evolution (A-MEM inspired, arXiv:2502.12110, NeurIPS 2025)
5// ═══════════════════════════════════════════════════════════════════════════
6
7/// Result from a memory evolution pass.
8#[derive(Debug, Clone)]
9pub struct EvolutionResult {
10    /// Number of existing semantic records whose context was updated.
11    pub records_evolved: usize,
12    /// Number of new links created between the new memory and existing records.
13    pub links_created: usize,
14}
15
16/// Evolve existing semantic memories in response to a newly stored episodic record.
17///
18/// When a new memory is stored, scan for semantically related existing records
19/// and update their descriptions and evidence counts to reflect the new
20/// information. This implements the A-MEM "memory evolution" pattern where
21/// storing new memories refines existing knowledge rather than leaving it
22/// immutable.
23///
24/// Reference: A-MEM (Zou et al., NeurIPS 2025, arXiv:2502.12110).
25/// Ablation shows ~25% improvement from evolution alone vs static storage.
26pub async fn evolve_on_new_memory(
27    db: &HirnDB,
28    new_record: &EpisodicRecord,
29    config: &EvolutionConfig,
30) -> HirnResult<EvolutionResult> {
31    let embedding = match &new_record.embedding {
32        Some(emb) => emb,
33        None => {
34            return Ok(EvolutionResult {
35                records_evolved: 0,
36                links_created: 0,
37            });
38        }
39    };
40
41    // Find top-k semantically similar existing records via LanceDB vector search.
42    let metric = db.distance_metric();
43    let candidates = match db
44        .vector_search_all(embedding, config.evolution_top_k, metric)
45        .await
46    {
47        Ok(c) => c,
48        Err(e) => {
49            tracing::warn!(error = %e, "evolve_on_new_memory: vector search failed, skipping evolution");
50            return Ok(EvolutionResult {
51                records_evolved: 0,
52                links_created: 0,
53            });
54        }
55    };
56
57    let mut records_evolved = 0;
58    let mut links_created = 0;
59
60    for &(uid, sim) in &candidates {
61        let candidate_id = MemoryId::from_ulid(ulid::Ulid(uid));
62
63        // Only evolve semantic records.
64        let record = match db.get_memory(candidate_id).await {
65            Ok(hirn_core::record::MemoryRecord::Semantic(s)) => s,
66            _ => continue,
67        };
68
69        // Skip if similarity is below threshold.
70        if sim < config.evolution_similarity_threshold {
71            continue;
72        }
73
74        // Evolve: bump evidence count and update description with new context.
75        let new_evidence = format!(
76            "{}. [Corroborated by episode at {}]",
77            record.description,
78            new_record.timestamp.as_datetime().format("%Y-%m-%d %H:%M")
79        );
80
81        // Boost confidence based on additional evidence.
82        let new_evidence_count = record.evidence_count + 1;
83        let base_confidence: f32 = match new_evidence_count {
84            1 => 0.3,
85            2..=3 => 0.5,
86            4..=7 => 0.7,
87            _ => 0.85,
88        };
89        let contradiction_penalty: f32 = if record.contradiction_ids.is_empty() {
90            0.0
91        } else {
92            0.15_f32 * record.contradiction_ids.len() as f32
93        };
94        let new_confidence = (base_confidence - contradiction_penalty).clamp(0.1, 1.0);
95
96        db.correct_semantic(
97            candidate_id,
98            crate::db::SemanticUpdate {
99                description: Some(new_evidence),
100                confidence: Some(new_confidence),
101                evidence_count: Some(new_evidence_count),
102                reason: Some(format!(
103                    "Evolution: corroborated by episode {}",
104                    new_record.id
105                )),
106                ..crate::db::SemanticUpdate::with_metadata(
107                    AgentId::well_known("memory_evolution"),
108                    new_record.id,
109                )
110            },
111        )
112        .await?;
113
114        records_evolved += 1;
115
116        // Create a DerivedFrom edge from the evolved record to the new episode.
117        if db
118            .connect_with(
119                candidate_id,
120                new_record.id,
121                EdgeRelation::DerivedFrom,
122                sim,
123                Metadata::default(),
124            )
125            .await
126            .is_ok()
127        {
128            links_created += 1;
129        }
130    }
131
132    Ok(EvolutionResult {
133        records_evolved,
134        links_created,
135    })
136}
137
138/// Configuration for memory evolution.
139#[derive(Debug, Clone)]
140pub struct EvolutionConfig {
141    /// Number of nearest neighbors to check for evolution. Default: 5.
142    pub evolution_top_k: usize,
143    /// Minimum similarity threshold for evolution to trigger. Default: 0.75.
144    pub evolution_similarity_threshold: f32,
145}
146
147impl Default for EvolutionConfig {
148    fn default() -> Self {
149        Self {
150            evolution_top_k: 5,
151            evolution_similarity_threshold: 0.75,
152        }
153    }
154}