Skip to main content

meerkat_core/
memory.rs

1//! MemoryStore trait — semantic memory indexing for discarded conversation history.
2//!
3//! Implementations live in `meerkat-memory` crate.
4
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8/// Canonical semantic-memory owner.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct MemoryOwner {
11    /// Session that owns the indexed memory shard.
12    session_id: crate::types::SessionId,
13}
14
15impl MemoryOwner {
16    pub fn canonical_session(session_id: crate::types::SessionId) -> Self {
17        Self { session_id }
18    }
19
20    pub fn session_id(&self) -> &crate::types::SessionId {
21        &self.session_id
22    }
23
24    fn includes(&self, metadata: &MemoryMetadata) -> bool {
25        metadata.session_id == self.session_id
26    }
27}
28
29/// Metadata associated with an indexed memory entry.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct MemoryMetadata {
32    /// The session ID this memory originated from.
33    pub session_id: crate::types::SessionId,
34    /// Turn number within the session.
35    pub turn: Option<u32>,
36    /// When the memory was indexed.
37    pub indexed_at: crate::time_compat::SystemTime,
38}
39
40/// A memory search result.
41#[derive(Debug, Clone)]
42pub struct MemoryResult {
43    /// The text content of the memory.
44    pub content: String,
45    /// Metadata about the source.
46    pub metadata: MemoryMetadata,
47    /// Relevance score (0.0 = no match, 1.0 = perfect match).
48    pub score: f32,
49}
50
51/// Typed owner/scope for semantic memory retrieval.
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53pub struct MemorySearchScope {
54    /// Canonical owner whose indexed memory is visible to this search.
55    pub owner: MemoryOwner,
56}
57
58impl MemorySearchScope {
59    pub fn for_session(session_id: crate::types::SessionId) -> Self {
60        Self {
61            owner: MemoryOwner::canonical_session(session_id),
62        }
63    }
64
65    pub fn for_owner(owner: MemoryOwner) -> Self {
66        Self { owner }
67    }
68
69    pub fn session_id(&self) -> &crate::types::SessionId {
70        self.owner.session_id()
71    }
72
73    pub fn includes(&self, metadata: &MemoryMetadata) -> bool {
74        self.owner.includes(metadata)
75    }
76}
77
78/// Typed owner/scope for semantic memory indexing.
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
80pub struct MemoryIndexScope {
81    /// Canonical owner receiving the indexed memory projection.
82    pub owner: MemoryOwner,
83}
84
85impl MemoryIndexScope {
86    pub fn for_session(session_id: crate::types::SessionId) -> Self {
87        Self {
88            owner: MemoryOwner::canonical_session(session_id),
89        }
90    }
91
92    pub fn for_owner(owner: MemoryOwner) -> Self {
93        Self { owner }
94    }
95
96    pub fn session_id(&self) -> &crate::types::SessionId {
97        self.owner.session_id()
98    }
99
100    pub fn includes(&self, metadata: &MemoryMetadata) -> bool {
101        self.owner.includes(metadata)
102    }
103}
104
105/// One scoped semantic-memory indexing request.
106#[derive(Debug, Clone)]
107pub struct MemoryIndexRequest {
108    scope: MemoryIndexScope,
109    content: String,
110    metadata: MemoryMetadata,
111}
112
113impl MemoryIndexRequest {
114    pub fn new(
115        scope: MemoryIndexScope,
116        content: String,
117        metadata: MemoryMetadata,
118    ) -> Result<Self, MemoryStoreError> {
119        if !scope.includes(&metadata) {
120            return Err(MemoryStoreError::Scope(format!(
121                "memory metadata session {} is outside indexing scope {}",
122                metadata.session_id,
123                scope.session_id()
124            )));
125        }
126        Ok(Self {
127            scope,
128            content,
129            metadata,
130        })
131    }
132
133    pub fn scope(&self) -> &MemoryIndexScope {
134        &self.scope
135    }
136
137    pub fn content(&self) -> &str {
138        &self.content
139    }
140
141    pub fn metadata(&self) -> &MemoryMetadata {
142        &self.metadata
143    }
144
145    pub fn into_parts(self) -> (MemoryIndexScope, String, MemoryMetadata) {
146        (self.scope, self.content, self.metadata)
147    }
148}
149
150/// Atomic scoped semantic-memory indexing batch.
151#[derive(Debug, Clone)]
152pub struct MemoryIndexBatch {
153    scope: MemoryIndexScope,
154    requests: Vec<MemoryIndexRequest>,
155}
156
157impl MemoryIndexBatch {
158    pub fn new(
159        scope: MemoryIndexScope,
160        requests: Vec<MemoryIndexRequest>,
161    ) -> Result<Self, MemoryStoreError> {
162        for request in &requests {
163            if request.scope() != &scope {
164                return Err(MemoryStoreError::Scope(format!(
165                    "memory index request scope {} is outside batch scope {}",
166                    request.scope().session_id(),
167                    scope.session_id()
168                )));
169            }
170        }
171        Ok(Self { scope, requests })
172    }
173
174    pub fn single(request: MemoryIndexRequest) -> Self {
175        Self {
176            scope: request.scope.clone(),
177            requests: vec![request],
178        }
179    }
180
181    pub fn scope(&self) -> &MemoryIndexScope {
182        &self.scope
183    }
184
185    pub fn len(&self) -> usize {
186        self.requests.len()
187    }
188
189    pub fn is_empty(&self) -> bool {
190        self.requests.is_empty()
191    }
192
193    pub fn into_parts(self) -> (MemoryIndexScope, Vec<MemoryIndexRequest>) {
194        (self.scope, self.requests)
195    }
196}
197
198/// Successful delivery receipt for a scoped memory index request.
199#[derive(Debug, Clone)]
200pub struct MemoryIndexReceipt {
201    pub scope: MemoryIndexScope,
202    pub indexed_entries: usize,
203}
204
205/// Typed compaction-to-memory delivery outcome.
206#[derive(Debug)]
207pub enum MemoryIndexDelivery {
208    NoStore {
209        scope: MemoryIndexScope,
210    },
211    Delivered(MemoryIndexReceipt),
212    Rejected {
213        scope: MemoryIndexScope,
214        attempted_entries: usize,
215        error: MemoryStoreError,
216    },
217}
218
219/// Semantic memory store for indexing and searching conversation history.
220#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
221#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
222pub trait MemoryStore: Send + Sync {
223    /// Index a typed, owner-scoped memory request.
224    async fn index_scoped(
225        &self,
226        request: MemoryIndexRequest,
227    ) -> Result<MemoryIndexReceipt, MemoryStoreError> {
228        self.index_scoped_batch(MemoryIndexBatch::single(request))
229            .await
230    }
231
232    /// Atomically index a typed, owner-scoped memory batch.
233    ///
234    /// Implementations must either make every request in the batch visible or
235    /// make none of them visible.
236    async fn index_scoped_batch(
237        &self,
238        batch: MemoryIndexBatch,
239    ) -> Result<MemoryIndexReceipt, MemoryStoreError>;
240
241    /// Semantic search: return up to `limit` results ordered by relevance.
242    async fn search(
243        &self,
244        scope: &MemorySearchScope,
245        query: &str,
246        limit: usize,
247    ) -> Result<Vec<MemoryResult>, MemoryStoreError>;
248}
249
250/// Errors from memory store operations.
251#[derive(Debug, thiserror::Error)]
252pub enum MemoryStoreError {
253    #[error("Scope error: {0}")]
254    Scope(String),
255
256    #[error("Embedding error: {0}")]
257    Embedding(String),
258
259    #[error("Index error: {0}")]
260    Index(String),
261
262    #[error("IO error: {0}")]
263    Io(#[from] std::io::Error),
264}