Skip to main content

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}