recursive/memory/mod.rs
1//! Vector memory layer — semantic storage and retrieval of agent memories.
2//!
3//! This module provides two traits:
4//!
5//! - [`EmbeddingProvider`] — converts text into a dense float vector.
6//! - [`VectorStore`] — stores and retrieves [`MemoryEntry`] items by semantic
7//! similarity (cosine) or by fallback linear text scan.
8//!
9//! ## Default (no extra features)
10//!
11//! [`NoopEmbedding`] and [`NoopVectorStore`] are always available and
12//! provide backward-compatible keyword search without any new dependencies.
13//!
14//! ## OpenAI embeddings (`openai-embedding` feature)
15//!
16//! [`OpenAiEmbedding`] calls the OpenAI `text-embedding-3-small` endpoint.
17//! Reuses the existing `RECURSIVE_API_KEY` / `RECURSIVE_API_BASE` env vars.
18//!
19//! ## SQLite vector store (`vector-memory` feature)
20//!
21//! [`SqliteVecStore`] persists vectors in a per-workspace SQLite database.
22//! Cosine similarity is computed in Rust (linear scan over stored BLOBs),
23//! requiring no native extension and no C compiler beyond the bundled SQLite.
24
25use async_trait::async_trait;
26use serde::{Deserialize, Serialize};
27
28pub mod noop;
29
30#[cfg(feature = "openai-embedding")]
31pub mod openai_embedding;
32
33#[cfg(feature = "vector-memory")]
34pub mod sqlite_vec;
35
36pub use noop::{NoopEmbedding, NoopVectorStore};
37
38#[cfg(feature = "openai-embedding")]
39pub use openai_embedding::OpenAiEmbedding;
40
41#[cfg(feature = "vector-memory")]
42pub use sqlite_vec::SqliteVecStore;
43
44// ──────────────────────────────────────────────────────────────────────────────
45// MemoryEntry
46// ──────────────────────────────────────────────────────────────────────────────
47
48/// A single memory fragment that can be stored and retrieved.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct MemoryEntry {
51 /// Unique, stable identifier (e.g. `"N1"`, a UUID, or a content hash).
52 pub id: String,
53 /// Free-form text content.
54 pub text: String,
55 /// Optional semantic tags for filtering.
56 #[serde(default)]
57 pub tags: Vec<String>,
58 /// ISO-8601 creation timestamp.
59 pub ts: String,
60}
61
62// ──────────────────────────────────────────────────────────────────────────────
63// EmbeddingProvider
64// ──────────────────────────────────────────────────────────────────────────────
65
66/// Converts text into a dense embedding vector.
67///
68/// Implementations must be [`Send`] + [`Sync`] so they can be shared across
69/// async tasks. Return an empty `Vec` to signal "no embedding available"
70/// (the store will fall back to linear text search in that case).
71#[async_trait]
72pub trait EmbeddingProvider: Send + Sync + 'static {
73 /// Embed `text` and return a float vector. May return an empty vec on
74 /// error or when embedding is intentionally disabled.
75 async fn embed(&self, text: &str) -> Vec<f32>;
76}
77
78// ──────────────────────────────────────────────────────────────────────────────
79// VectorStore
80// ──────────────────────────────────────────────────────────────────────────────
81
82/// Persistent store for [`MemoryEntry`] items with optional semantic search.
83///
84/// All methods are async and must not panic; they return `Result` so the
85/// caller can log warnings and continue rather than crashing the agent loop.
86#[async_trait]
87pub trait VectorStore: Send + Sync + 'static {
88 /// Persist a memory entry. If an entry with the same `id` already exists
89 /// it should be overwritten.
90 async fn upsert(&self, entry: &MemoryEntry, vector: Vec<f32>) -> crate::error::Result<()>;
91
92 /// Retrieve up to `limit` entries whose vector is closest to `query_vec`
93 /// (cosine similarity). If `query_vec` is empty, fall back to returning
94 /// recent entries in insertion order.
95 async fn search(
96 &self,
97 query_vec: Vec<f32>,
98 query_text: &str,
99 limit: usize,
100 ) -> crate::error::Result<Vec<MemoryEntry>>;
101
102 /// Remove the entry with the given `id`. No-op if not found.
103 async fn remove(&self, id: &str) -> crate::error::Result<()>;
104
105 /// Return all entries in insertion order.
106 async fn list_all(&self) -> crate::error::Result<Vec<MemoryEntry>>;
107}