flashmind_types/memory.rs
1//! Memory abstraction — [`MemoryProvider`] trait and entry types.
2//!
3//! The [`MemoryProvider`] trait defines a simple three-operation interface for long-term memory:
4//! store, search (semantic), and forget. Implementations are backed by vector stores that
5//! generate embeddings on write and perform similarity search on read.
6//!
7//! # Key types
8//!
9//! | Type | Role |
10//! |------|------|
11//! | [`MemoryProvider`] | Trait: `store`, `search`, `forget` |
12//! | [`MemoryEntry`] | A single stored memory with relevance score and metadata |
13//! | [`MemoryMetadata`] | Key-value metadata attached to a memory (tags, TTL, context) |
14//!
15//! # Implemented by
16//!
17//! `VectorMemory` in the `flashmind-memory` crate
18//! provides the primary implementation using SQLite + sqlite-vec + FTS5 hybrid search.
19
20use async_trait::async_trait;
21use chrono::{DateTime, Utc};
22use serde::{Deserialize, Serialize};
23
24/// Arbitrary key-value metadata attached to a memory entry.
25///
26/// Use `tags` for categorization (e.g. `"fact"`, `"preference"`, `"project:foo"`).
27/// Set `expires_at` to enable automatic TTL-based eviction.
28#[derive(Debug, Clone, Serialize, Deserialize, Default)]
29pub struct MemoryMetadata {
30 /// Free-form human-readable context (e.g. summarises the surrounding conversation).
31 pub context: Option<String>,
32 /// Flat list of string tags used for filtering and discovery.
33 pub tags: Vec<String>,
34 /// When set, the entry is considered expired after this instant.
35 pub expires_at: Option<DateTime<Utc>>,
36}
37
38/// A single stored memory with its relevance score and provenance.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct MemoryEntry {
41 /// Unique identifier assigned at store time.
42 pub id: String,
43 /// The stored text content.
44 pub content: String,
45 /// Relevance score from the last search (1.0 = perfect match).
46 pub score: f64,
47 /// Attached metadata (tags, expiry, context).
48 pub metadata: MemoryMetadata,
49 /// UTC timestamp when this entry was first stored.
50 pub created_at: DateTime<Utc>,
51}
52
53/// Abstraction over long-term memory storage and retrieval.
54///
55/// Implementations back the `memory_store` / `memory_recall` tools.
56/// Returned IDs are opaque strings; callers must store them if they need to call `forget`.
57#[async_trait]
58pub trait MemoryProvider: Send + Sync {
59 /// Persist `content` with `metadata`. Returns the new entry's ID.
60 async fn store(&self, content: &str, metadata: MemoryMetadata) -> anyhow::Result<String>;
61
62 /// Semantic search. Returns up to `limit` entries ordered by descending relevance.
63 async fn search(&self, query: &str, limit: usize) -> anyhow::Result<Vec<MemoryEntry>>;
64
65 /// Remove a specific entry by ID. No-op if the ID does not exist.
66 async fn forget(&self, id: &str) -> anyhow::Result<()>;
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn memory_metadata_default() {
75 let meta = MemoryMetadata::default();
76 assert!(meta.context.is_none());
77 assert!(meta.tags.is_empty());
78 assert!(meta.expires_at.is_none());
79 }
80
81 #[test]
82 fn memory_entry_serde_round_trip() {
83 let entry = MemoryEntry {
84 id: "mem-123".into(),
85 content: "the user prefers dark mode".into(),
86 score: 0.95,
87 metadata: MemoryMetadata {
88 context: Some("preferences".into()),
89 tags: vec!["preference".into(), "ui".into()],
90 expires_at: None,
91 },
92 created_at: Utc::now(),
93 };
94 let json = serde_json::to_string(&entry).unwrap();
95 let deserialized: MemoryEntry = serde_json::from_str(&json).unwrap();
96 assert_eq!(deserialized.id, "mem-123");
97 assert_eq!(deserialized.content, "the user prefers dark mode");
98 assert!((deserialized.score - 0.95).abs() < f64::EPSILON);
99 assert_eq!(deserialized.metadata.tags.len(), 2);
100 }
101
102 #[test]
103 fn memory_metadata_with_expiry() {
104 let expiry = Utc::now() + chrono::Duration::hours(24);
105 let meta = MemoryMetadata {
106 context: None,
107 tags: vec!["ephemeral".into()],
108 expires_at: Some(expiry),
109 };
110 assert!(meta.expires_at.unwrap() > Utc::now());
111 }
112}