Skip to main content

memrust/
types.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4/// Agent-native memory categories, mirroring how agent frameworks reason
5/// about memory rather than how databases store rows.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
7#[serde(rename_all = "snake_case")]
8pub enum MemoryKind {
9    /// Things that happened: events, conversation turns, actions taken.
10    #[default]
11    Episodic,
12    /// Distilled facts and knowledge, independent of when they were learned.
13    Semantic,
14    /// Short-lived scratch state for the current task/session.
15    Working,
16    /// The agent's own conclusions about its behavior or the task.
17    Reflection,
18    /// Indexed history of tool invocations and their results.
19    ToolCall,
20    /// How-to knowledge: workflows, tool usage patterns, few-shot examples.
21    /// Agents that remember *how* they did something are the ones that improve.
22    Procedural,
23}
24
25/// Who can recall a memory in a multi-agent deployment.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
27#[serde(rename_all = "snake_case")]
28pub enum Visibility {
29    /// Recallable by every agent (and by unscoped recalls).
30    #[default]
31    Shared,
32    /// Recallable only by the owning `agent_id` (and unscoped recalls).
33    Private,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct MemoryRecord {
38    pub id: Uuid,
39    pub kind: MemoryKind,
40    pub text: String,
41    /// Unix milliseconds.
42    pub created_at: i64,
43    /// 0.0..=1.0, used as a retrieval boost.
44    pub importance: f32,
45    /// Unix milliseconds; the memory is invisible to recall after this and
46    /// durably removed by the next lifecycle sweep.
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub expires_at: Option<i64>,
49    /// For consolidated summaries: ids of the memories this was distilled
50    /// from (which were forgotten in the same lifecycle pass).
51    #[serde(default, skip_serializing_if = "Vec::is_empty")]
52    pub sources: Vec<Uuid>,
53    /// Entities extracted at ingestion (names, identifiers, tags); the
54    /// graph index links memories that share or co-mention them.
55    #[serde(default, skip_serializing_if = "Vec::is_empty")]
56    pub entities: Vec<String>,
57    #[serde(default, skip_serializing_if = "Vec::is_empty")]
58    pub tags: Vec<String>,
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub session_id: Option<String>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub agent_id: Option<String>,
63    /// Defaults at ingest: `private` when `agent_id` is set, else `shared`.
64    /// (Pre-v0.5 records deserialize as `shared`, preserving old behavior.)
65    #[serde(default)]
66    pub visibility: Visibility,
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub metadata: Option<serde_json::Value>,
69    /// L2-normalized embedding. Present after ingestion (auto-embedded when
70    /// the caller does not supply one).
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub embedding: Option<Vec<f32>>,
73}
74
75#[derive(Debug, Clone, Default, Deserialize)]
76pub struct RememberRequest {
77    pub text: String,
78    #[serde(default)]
79    pub kind: MemoryKind,
80    #[serde(default)]
81    pub tags: Vec<String>,
82    #[serde(default)]
83    pub session_id: Option<String>,
84    #[serde(default)]
85    pub agent_id: Option<String>,
86    #[serde(default)]
87    pub metadata: Option<serde_json::Value>,
88    /// Defaults to 0.5 when unset.
89    #[serde(default)]
90    pub importance: Option<f32>,
91    /// Time-to-live in seconds. Unset: `working` memories get the engine's
92    /// default working TTL; other kinds never expire.
93    #[serde(default)]
94    pub ttl_seconds: Option<u64>,
95    /// Unset: `private` when `agent_id` is set, else `shared`.
96    #[serde(default)]
97    pub visibility: Option<Visibility>,
98    /// Bring-your-own embedding; auto-embedded otherwise.
99    #[serde(default)]
100    pub embedding: Option<Vec<f32>>,
101}
102
103/// How recall should weight its retrieval signals.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
105#[serde(rename_all = "snake_case")]
106pub enum RecallStrategy {
107    /// Even blend of semantic and lexical signals with mild recency.
108    #[default]
109    Balanced,
110    /// Meaning-first: lean on the vector index.
111    Semantic,
112    /// Exact-term-first: lean on BM25.
113    Lexical,
114    /// Recency-first: what happened lately matters most.
115    Recent,
116    /// Relation-first: follow entity links — "things connected to X",
117    /// not just "things similar to X".
118    Relational,
119}
120
121impl RecallStrategy {
122    /// (vector_weight, lexical_weight, graph_weight, recency_weight)
123    pub fn weights(self) -> (f32, f32, f32, f32) {
124        match self {
125            RecallStrategy::Balanced => (1.0, 1.0, 0.7, 0.3),
126            RecallStrategy::Semantic => (1.0, 0.3, 0.5, 0.1),
127            RecallStrategy::Lexical => (0.3, 1.0, 0.5, 0.1),
128            RecallStrategy::Recent => (0.5, 0.5, 0.3, 1.2),
129            RecallStrategy::Relational => (0.4, 0.4, 1.2, 0.2),
130        }
131    }
132}
133
134#[derive(Debug, Clone, Default, Deserialize)]
135pub struct MemoryFilter {
136    #[serde(default)]
137    pub kinds: Vec<MemoryKind>,
138    #[serde(default)]
139    pub tags: Vec<String>,
140    #[serde(default)]
141    pub session_id: Option<String>,
142    #[serde(default)]
143    pub agent_id: Option<String>,
144    /// Unix milliseconds, inclusive bounds.
145    #[serde(default)]
146    pub since: Option<i64>,
147    #[serde(default)]
148    pub until: Option<i64>,
149}
150
151impl MemoryFilter {
152    /// True when the filter constrains nothing, letting recall skip the
153    /// per-candidate predicate entirely.
154    pub fn is_empty(&self) -> bool {
155        self.kinds.is_empty()
156            && self.tags.is_empty()
157            && self.session_id.is_none()
158            && self.agent_id.is_none()
159            && self.since.is_none()
160            && self.until.is_none()
161    }
162
163    pub fn matches(&self, rec: &MemoryRecord) -> bool {
164        if !self.kinds.is_empty() && !self.kinds.contains(&rec.kind) {
165            return false;
166        }
167        if let Some(s) = &self.session_id {
168            if rec.session_id.as_deref() != Some(s.as_str()) {
169                return false;
170            }
171        }
172        if let Some(a) = &self.agent_id {
173            if rec.agent_id.as_deref() != Some(a.as_str()) {
174                return false;
175            }
176        }
177        if !self.tags.is_empty() && !self.tags.iter().all(|t| rec.tags.contains(t)) {
178            return false;
179        }
180        if let Some(since) = self.since {
181            if rec.created_at < since {
182                return false;
183            }
184        }
185        if let Some(until) = self.until {
186            if rec.created_at > until {
187                return false;
188            }
189        }
190        true
191    }
192}
193
194#[derive(Debug, Clone, Default, Deserialize)]
195pub struct RecallRequest {
196    pub query: String,
197    #[serde(default)]
198    pub top_k: Option<usize>,
199    #[serde(default)]
200    pub strategy: RecallStrategy,
201    #[serde(default)]
202    pub filter: MemoryFilter,
203    /// Bring-your-own query vector (must match the collection's embedding
204    /// model); when unset, the engine embeds `query` itself.
205    #[serde(default)]
206    pub query_embedding: Option<Vec<f32>>,
207    /// Set false to skip the configured reranker for this request.
208    #[serde(default)]
209    pub rerank: Option<bool>,
210    /// Recall *on behalf of* this agent: sees shared memories, unowned
211    /// memories, and its own private ones. Unset = unscoped (sees all —
212    /// single-agent mode or an operator view). Distinct from
213    /// `filter.agent_id`, which narrows to exactly one agent's records
214    /// within the visible set.
215    #[serde(default)]
216    pub as_agent: Option<String>,
217    /// Per-query HNSW beam width. Higher searches harder: more accurate,
218    /// slower. Unset uses the index default (100).
219    #[serde(default)]
220    pub ef_search: Option<usize>,
221}
222
223/// Per-signal contributions, so agents (and humans) can see *why* a memory
224/// was recalled instead of getting an opaque score.
225#[derive(Debug, Clone, Copy, Serialize)]
226pub struct RecallSignals {
227    /// Reciprocal-rank contribution from the vector index (0 if not matched).
228    pub vector: f32,
229    /// Reciprocal-rank contribution from BM25 (0 if not matched).
230    pub lexical: f32,
231    /// Reciprocal-rank contribution from entity-graph traversal (0 if not
232    /// matched via shared or co-occurring entities).
233    pub graph: f32,
234    /// Exponential time-decay factor in 0..=1.
235    pub recency: f32,
236    /// Importance boost contribution.
237    pub importance: f32,
238    /// Reranker relevance in 0..=1 (0 when no reranker ran). When set, hits
239    /// are ordered by this; `score` still reports the fused retrieval score.
240    pub rerank: f32,
241}
242
243#[derive(Debug, Clone, Serialize)]
244pub struct RecallHit {
245    pub record: MemoryRecord,
246    pub score: f32,
247    pub signals: RecallSignals,
248}
249
250#[derive(Debug, Clone, Serialize)]
251pub struct EngineStats {
252    pub total_memories: usize,
253    pub vector_indexed: usize,
254    pub lexical_indexed: usize,
255    /// Dimension of the engine's own embedder (unused when callers supply
256    /// their own vectors).
257    pub embedding_dim: usize,
258    /// Dimension of the vector index itself — set by the first vector stored,
259    /// so it reflects bring-your-own embeddings. None until one is stored.
260    pub vector_dim: Option<usize>,
261    /// Distinct entities tracked by the graph index.
262    pub entities: usize,
263    /// Whether the vector index stores SQ8 codes instead of f32.
264    pub quantized: bool,
265    /// WAL ops a restart would replay (resets to 0 at each checkpoint).
266    pub wal_tail_ops: usize,
267}
268
269/// Policy knobs for the memory lifecycle pass.
270#[derive(Debug, Clone)]
271pub struct LifecycleConfig {
272    /// Default TTL applied to `working` memories that don't set one. 1 day.
273    pub working_ttl_secs: u64,
274    /// Episodic memories older than this become consolidation candidates. 7 days.
275    pub consolidate_after_secs: u64,
276    /// Don't summarize groups smaller than this; they wait for more context.
277    pub min_batch: usize,
278    /// Cap on how many memories fold into one summary.
279    pub max_batch: usize,
280}
281
282impl Default for LifecycleConfig {
283    fn default() -> Self {
284        Self {
285            working_ttl_secs: 24 * 3600,
286            consolidate_after_secs: 7 * 24 * 3600,
287            min_batch: 4,
288            max_batch: 12,
289        }
290    }
291}
292
293#[derive(Debug, Clone, Default, Serialize)]
294pub struct LifecycleReport {
295    /// Expired memories durably forgotten this pass.
296    pub expired_swept: usize,
297    /// Episodic batches folded into semantic summaries.
298    pub batches_consolidated: usize,
299    /// Ids of the summary memories created.
300    pub summaries: Vec<Uuid>,
301    /// Whether this pass also checkpointed state and truncated the WAL.
302    pub checkpointed: bool,
303}
304
305/// A portable export of (a session's) memory, restorable into any engine.
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct Snapshot {
308    pub created_at: i64,
309    #[serde(default, skip_serializing_if = "Option::is_none")]
310    pub session_id: Option<String>,
311    pub records: Vec<MemoryRecord>,
312}
313
314pub fn now_ms() -> i64 {
315    std::time::SystemTime::now()
316        .duration_since(std::time::UNIX_EPOCH)
317        .map(|d| d.as_millis() as i64)
318        .unwrap_or(0)
319}