Skip to main content

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}