1use mentedb_core::MemoryNode;
2use mentedb_core::edge::EdgeType;
3use mentedb_core::memory::MemoryType;
4use mentedb_core::types::MemoryId;
5
6use crate::llm::{
7 CognitiveLlmService, ContradictionVerdict, InvalidationVerdict, LlmJudge, MemorySummary,
8};
9
10#[derive(Debug, Clone)]
11pub enum InferredAction {
12 FlagContradiction {
13 existing: MemoryId,
14 new: MemoryId,
15 reason: String,
16 },
17 MarkObsolete {
18 memory: MemoryId,
19 superseded_by: MemoryId,
20 },
21 InvalidateMemory {
23 memory: MemoryId,
24 superseded_by: MemoryId,
25 valid_until: u64,
26 },
27 UpdateContent {
29 memory: MemoryId,
30 new_content: String,
31 reason: String,
32 },
33 CreateEdge {
34 source: MemoryId,
35 target: MemoryId,
36 edge_type: EdgeType,
37 weight: f32,
38 },
39 UpdateConfidence {
40 memory: MemoryId,
41 new_confidence: f32,
42 },
43 PropagateBeliefChange {
44 root: MemoryId,
45 delta: f32,
46 },
47}
48
49fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
50 if a.len() != b.len() || a.is_empty() {
51 return 0.0;
52 }
53 let mut dot = 0.0f32;
54 let mut norm_a = 0.0f32;
55 let mut norm_b = 0.0f32;
56 for i in 0..a.len() {
57 dot += a[i] * b[i];
58 norm_a += a[i] * a[i];
59 norm_b += b[i] * b[i];
60 }
61 let denom = norm_a.sqrt() * norm_b.sqrt();
62 if denom == 0.0 { 0.0 } else { dot / denom }
63}
64
65#[derive(Debug, Clone)]
67pub struct WriteInferenceConfig {
68 pub contradiction_threshold: f32,
70 pub obsolete_threshold: f32,
72 pub related_min: f32,
74 pub related_max: f32,
76 pub correction_threshold: f32,
78 pub confidence_decay_factor: f32,
80 pub confidence_floor: f32,
82}
83
84impl Default for WriteInferenceConfig {
85 fn default() -> Self {
86 Self {
87 contradiction_threshold: 0.95,
88 obsolete_threshold: 0.85,
89 related_min: 0.6,
90 related_max: 0.85,
91 correction_threshold: 0.5,
92 confidence_decay_factor: 0.5,
93 confidence_floor: 0.1,
94 }
95 }
96}
97
98pub struct WriteInferenceEngine {
99 config: WriteInferenceConfig,
100}
101
102impl WriteInferenceEngine {
103 pub fn new() -> Self {
104 Self {
105 config: WriteInferenceConfig::default(),
106 }
107 }
108
109 pub fn with_config(config: WriteInferenceConfig) -> Self {
110 Self { config }
111 }
112
113 pub fn infer_on_write(
114 &self,
115 new_memory: &MemoryNode,
116 existing_memories: &[MemoryNode],
117 existing_edges: &[(MemoryId, MemoryId, EdgeType)],
118 ) -> Vec<InferredAction> {
119 let _ = existing_edges; let mut actions = Vec::new();
121
122 for existing in existing_memories {
123 if existing.id == new_memory.id {
124 continue;
125 }
126
127 let sim = cosine_similarity(&new_memory.embedding, &existing.embedding);
128
129 if sim > self.config.contradiction_threshold {
133 if existing.agent_id == new_memory.agent_id
134 && existing.content != new_memory.content
135 {
136 actions.push(InferredAction::FlagContradiction {
137 existing: existing.id,
138 new: new_memory.id,
139 reason: format!(
140 "High embedding similarity ({:.3}) with different content from same agent",
141 sim
142 ),
143 });
144 } else if new_memory.created_at > existing.created_at {
145 actions.push(InferredAction::InvalidateMemory {
147 memory: existing.id,
148 superseded_by: new_memory.id,
149 valid_until: new_memory.created_at,
150 });
151 }
152 } else if sim > self.config.obsolete_threshold
153 && new_memory.created_at > existing.created_at
154 {
155 actions.push(InferredAction::InvalidateMemory {
159 memory: existing.id,
160 superseded_by: new_memory.id,
161 valid_until: new_memory.created_at,
162 });
163 } else if sim > self.config.related_min && sim <= self.config.related_max {
164 actions.push(InferredAction::CreateEdge {
165 source: new_memory.id,
166 target: existing.id,
167 edge_type: EdgeType::Related,
168 weight: sim,
169 });
170 }
171 }
172
173 if new_memory.memory_type == MemoryType::Correction
175 && let Some(original) = existing_memories
176 .iter()
177 .filter(|m| m.id != new_memory.id)
178 .max_by(|a, b| {
179 cosine_similarity(&new_memory.embedding, &a.embedding)
180 .partial_cmp(&cosine_similarity(&new_memory.embedding, &b.embedding))
181 .unwrap_or(std::cmp::Ordering::Equal)
182 })
183 {
184 let sim = cosine_similarity(&new_memory.embedding, &original.embedding);
185 if sim > self.config.correction_threshold {
186 actions.push(InferredAction::CreateEdge {
187 source: new_memory.id,
188 target: original.id,
189 edge_type: EdgeType::Supersedes,
190 weight: 1.0,
191 });
192 actions.push(InferredAction::UpdateConfidence {
193 memory: original.id,
194 new_confidence: (original.confidence * self.config.confidence_decay_factor)
195 .max(self.config.confidence_floor),
196 });
197 }
198 }
199
200 actions
201 }
202
203 pub async fn infer_on_write_with_llm<J: LlmJudge>(
208 &self,
209 new_memory: &MemoryNode,
210 existing_memories: &[MemoryNode],
211 existing_edges: &[(MemoryId, MemoryId, EdgeType)],
212 llm: &CognitiveLlmService<J>,
213 ) -> Vec<InferredAction> {
214 let _ = existing_edges;
215 let mut actions = Vec::new();
216
217 let new_summary = memory_to_summary(new_memory);
218
219 for existing in existing_memories {
220 if existing.id == new_memory.id {
221 continue;
222 }
223
224 let sim = cosine_similarity(&new_memory.embedding, &existing.embedding);
225
226 if sim > 0.5 && existing.agent_id == new_memory.agent_id {
229 let old_summary = memory_to_summary(existing);
230
231 if let Ok(verdict) = llm.judge_invalidation(&old_summary, &new_summary).await {
233 match verdict {
234 InvalidationVerdict::Invalidate { reason: _ } => {
235 actions.push(InferredAction::InvalidateMemory {
238 memory: existing.id,
239 superseded_by: new_memory.id,
240 valid_until: new_memory.created_at,
241 });
242 actions.push(InferredAction::UpdateConfidence {
243 memory: existing.id,
244 new_confidence: (existing.confidence
245 * self.config.confidence_decay_factor)
246 .max(self.config.confidence_floor),
247 });
248 continue;
250 }
251 InvalidationVerdict::Update {
252 merged_content,
253 reason,
254 } => {
255 actions.push(InferredAction::UpdateContent {
256 memory: existing.id,
257 new_content: merged_content,
258 reason,
259 });
260 continue;
261 }
262 InvalidationVerdict::Keep { .. } => {
263 }
265 }
266 }
267
268 if sim > 0.7
270 && existing.content != new_memory.content
271 && let Ok(verdict) = llm.detect_contradiction(&old_summary, &new_summary).await
272 {
273 match verdict {
274 ContradictionVerdict::Contradicts { reason } => {
275 actions.push(InferredAction::FlagContradiction {
276 existing: existing.id,
277 new: new_memory.id,
278 reason,
279 });
280 }
281 ContradictionVerdict::Supersedes { winner, reason: _ } => {
282 let winner_is_new = winner == new_memory.id.to_string();
283 let (obsolete, superseder) = if winner_is_new {
284 (existing.id, new_memory.id)
285 } else {
286 (new_memory.id, existing.id)
287 };
288 actions.push(InferredAction::InvalidateMemory {
289 memory: obsolete,
290 superseded_by: superseder,
291 valid_until: new_memory.created_at,
292 });
293 }
294 ContradictionVerdict::Compatible { .. } => {}
295 }
296 }
297 }
298
299 if sim > self.config.related_min && sim <= self.config.related_max {
301 actions.push(InferredAction::CreateEdge {
302 source: new_memory.id,
303 target: existing.id,
304 edge_type: EdgeType::Related,
305 weight: sim,
306 });
307 }
308 }
309
310 if new_memory.memory_type == MemoryType::Correction
312 && let Some(original) = existing_memories
313 .iter()
314 .filter(|m| m.id != new_memory.id)
315 .max_by(|a, b| {
316 cosine_similarity(&new_memory.embedding, &a.embedding)
317 .partial_cmp(&cosine_similarity(&new_memory.embedding, &b.embedding))
318 .unwrap_or(std::cmp::Ordering::Equal)
319 })
320 {
321 let sim = cosine_similarity(&new_memory.embedding, &original.embedding);
322 if sim > self.config.correction_threshold {
323 actions.push(InferredAction::InvalidateMemory {
324 memory: original.id,
325 superseded_by: new_memory.id,
326 valid_until: new_memory.created_at,
327 });
328 actions.push(InferredAction::UpdateConfidence {
329 memory: original.id,
330 new_confidence: (original.confidence * self.config.confidence_decay_factor)
331 .max(self.config.confidence_floor),
332 });
333 }
334 }
335
336 actions
337 }
338}
339
340impl Default for WriteInferenceEngine {
341 fn default() -> Self {
342 Self::new()
343 }
344}
345
346fn memory_to_summary(m: &MemoryNode) -> MemorySummary {
347 MemorySummary {
348 id: m.id,
349 content: m.content.clone(),
350 memory_type: m.memory_type,
351 confidence: m.confidence,
352 created_at: m.created_at,
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use mentedb_core::memory::MemoryType;
360 use mentedb_core::types::AgentId;
361
362 use crate::llm::MockLlmJudge;
363
364 fn make_memory(content: &str, embedding: Vec<f32>, mem_type: MemoryType) -> MemoryNode {
365 let mut m = MemoryNode::new(AgentId::new(), mem_type, content.to_string(), embedding);
366 m.created_at = 1000;
367 m
368 }
369
370 #[test]
371 fn test_flag_contradiction() {
372 let agent = AgentId::new();
373 let mut existing =
374 make_memory("uses PostgreSQL", vec![1.0, 0.0, 0.0], MemoryType::Semantic);
375 existing.agent_id = agent;
376
377 let mut new_mem = make_memory("uses MySQL", vec![0.99, 0.01, 0.0], MemoryType::Semantic);
378 new_mem.agent_id = agent;
379 new_mem.created_at = 2000;
380
381 let engine = WriteInferenceEngine::new();
382 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
383 assert!(
384 actions
385 .iter()
386 .any(|a| matches!(a, InferredAction::FlagContradiction { .. })),
387 "Expected FlagContradiction, got: {:?}",
388 actions
389 );
390 }
391
392 #[test]
393 fn test_moderate_similarity_creates_edge() {
394 let existing = make_memory("topic A", vec![1.0, 0.0, 0.0], MemoryType::Semantic);
395 let new_mem = make_memory("topic B", vec![0.7, 0.714, 0.0], MemoryType::Semantic);
397
398 let engine = WriteInferenceEngine::new();
399 let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
400 assert!(
401 actions.iter().any(|a| matches!(
402 a,
403 InferredAction::CreateEdge {
404 edge_type: EdgeType::Related,
405 ..
406 }
407 )),
408 "Expected CreateEdge Related, got: {:?}",
409 actions
410 );
411 }
412
413 #[tokio::test]
414 async fn test_llm_invalidation_emits_temporal_actions() {
415 let agent = AgentId::new();
416 let mut existing = make_memory(
417 "Alice works at Acme",
418 vec![0.8, 0.6, 0.0],
419 MemoryType::Semantic,
420 );
421 existing.agent_id = agent;
422
423 let mut new_mem = make_memory(
424 "Alice joined Google last week",
425 vec![0.75, 0.65, 0.1],
426 MemoryType::Semantic,
427 );
428 new_mem.agent_id = agent;
429 new_mem.created_at = 2000;
430
431 let judge =
432 MockLlmJudge::new(r#"{"verdict": "invalidate", "reason": "Alice changed jobs"}"#);
433 let llm = CognitiveLlmService::new(judge);
434 let engine = WriteInferenceEngine::new();
435 let actions = engine
436 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
437 .await;
438
439 assert!(
440 actions
441 .iter()
442 .any(|a| matches!(a, InferredAction::InvalidateMemory { .. })),
443 "Expected InvalidateMemory from LLM verdict, got: {:?}",
444 actions
445 );
446 let invalidation_count = actions
450 .iter()
451 .filter(|a| {
452 matches!(
453 a,
454 InferredAction::InvalidateMemory { .. } | InferredAction::MarkObsolete { .. }
455 )
456 })
457 .count();
458 assert_eq!(
459 invalidation_count, 1,
460 "Expected a single invalidation action, got: {:?}",
461 actions
462 );
463 }
464
465 #[tokio::test]
466 async fn test_llm_update_emits_update_content() {
467 let agent = AgentId::new();
468 let mut existing = make_memory(
469 "Project uses React",
470 vec![0.8, 0.6, 0.0],
471 MemoryType::Semantic,
472 );
473 existing.agent_id = agent;
474
475 let mut new_mem = make_memory(
476 "Project migrated from React to Vue",
477 vec![0.75, 0.65, 0.1],
478 MemoryType::Semantic,
479 );
480 new_mem.agent_id = agent;
481 new_mem.created_at = 2000;
482
483 let judge = MockLlmJudge::new(
484 r#"{"verdict": "update", "merged_content": "Project migrated from React to Vue in Q2", "reason": "adds temporal context"}"#,
485 );
486 let llm = CognitiveLlmService::new(judge);
487 let engine = WriteInferenceEngine::new();
488 let actions = engine
489 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
490 .await;
491
492 assert!(
493 actions
494 .iter()
495 .any(|a| matches!(a, InferredAction::UpdateContent { .. })),
496 "Expected UpdateContent from LLM update verdict, got: {:?}",
497 actions
498 );
499 }
500
501 #[tokio::test]
502 async fn test_llm_keep_falls_through_to_contradiction_check() {
503 let agent = AgentId::new();
504 let mut existing = make_memory("Prefers tabs", vec![0.9, 0.44, 0.0], MemoryType::Semantic);
506 existing.agent_id = agent;
507
508 let mut new_mem = make_memory(
509 "Prefers spaces",
510 vec![0.88, 0.47, 0.0],
511 MemoryType::Semantic,
512 );
513 new_mem.agent_id = agent;
514 new_mem.created_at = 2000;
515
516 let judge = MockLlmJudge::new(
517 r#"{"verdict": "compatible", "reason": "different formatting preferences"}"#,
518 );
519 let llm = CognitiveLlmService::new(judge);
520 let engine = WriteInferenceEngine::new();
521 let actions = engine
522 .infer_on_write_with_llm(&new_mem, &[existing.clone()], &[], &llm)
523 .await;
524
525 assert!(
528 !actions
529 .iter()
530 .any(|a| matches!(a, InferredAction::FlagContradiction { .. })),
531 "Should not flag contradiction when LLM says compatible",
532 );
533 }
534}