1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Option / option-builder structs for the public API.
use std::collections::BTreeMap;
use crate::attribute::AttributeValue;
use crate::link::LinkKind;
use crate::memory::MemoryRef;
/// Options for `Memory::append`.
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct AppendOpts {
/// Explicit links to record alongside the new memory.
pub links: Vec<MemoryRef>,
/// Caller-provided embedding. When set, the engine skips its `Embedder` and uses
/// this vector. Length must match `schema_meta.embedder_dims`.
pub embedding: Option<Vec<f32>>,
/// Plan 11: caller-supplied typed metadata. Multiple
/// [`AppendOpts::with_attribute`] calls accumulate; the same key
/// overwrites the prior value before the SQL transaction runs.
pub attributes: BTreeMap<String, AttributeValue>,
/// Plan 15: when the FACT became operationally true. `None` means
/// "no lower validity bound".
pub valid_from_ms: Option<i64>,
/// Plan 15: when the FACT stopped being true. `None` means "still
/// valid at read time".
pub valid_until_ms: Option<i64>,
/// Plan 15: typed memory discriminator. `None` defaults to
/// [`crate::memory::MemoryKind::Episodic`] at append time (legacy
/// rows on disk stay NULL).
pub kind: Option<crate::memory::MemoryKind>,
}
impl AppendOpts {
/// Builder helper.
#[must_use]
pub fn with_links<I: IntoIterator<Item = MemoryRef>>(mut self, links: I) -> Self {
self.links = links.into_iter().collect();
self
}
/// Caller hands the engine a pre-computed embedding. Used by Apple Foundation
/// Models, OpenAI proxies, Swift FFI consumers, etc. — anywhere the model
/// runs outside the library.
#[must_use]
pub fn with_embedding(mut self, vector: Vec<f32>) -> Self {
self.embedding = Some(vector);
self
}
/// Plan 11: attach one typed metadata attribute (e.g. transcript-style
/// `speaker`, `ts_start_ms`, `meeting_id`). The attribute is persisted
/// in the same SQL transaction as the new memory row; an `attribute_set`
/// audit entry is recorded per attribute.
///
/// Multiple calls accumulate; later calls overwrite the same key.
#[must_use]
pub fn with_attribute(
mut self,
key: impl Into<String>,
value: impl Into<AttributeValue>,
) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
/// Plan 15: record both validity bounds at once.
#[must_use]
pub fn with_validity(mut self, from_ms: i64, until_ms: Option<i64>) -> Self {
self.valid_from_ms = Some(from_ms);
self.valid_until_ms = until_ms;
self
}
/// Plan 15: record the lower validity bound only.
#[must_use]
pub fn valid_from(mut self, from_ms: i64) -> Self {
self.valid_from_ms = Some(from_ms);
self
}
/// Plan 15: record the upper validity bound only.
#[must_use]
pub fn valid_until(mut self, until_ms: i64) -> Self {
self.valid_until_ms = Some(until_ms);
self
}
/// Plan 15: tag the memory with a typed kind (episodic / semantic /
/// procedural / archival / working).
#[must_use]
pub fn with_kind(mut self, kind: crate::memory::MemoryKind) -> Self {
self.kind = Some(kind);
self
}
/// Plan 16: attach an `Array<String>` keyword list at the canonical
/// [`crate::canonical_keys::KEYWORDS`] key. Convenience for callers
/// that just want to tag-as-they-go.
#[must_use]
pub fn with_keywords<I, S>(mut self, keywords: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let arr = AttributeValue::Array(
keywords
.into_iter()
.map(|s| AttributeValue::String(s.into()))
.collect(),
);
self.attributes
.insert(crate::canonical_keys::KEYWORDS.to_string(), arr);
self
}
/// Plan 16: attach an `Array<String>` tag list at the canonical
/// [`crate::canonical_keys::TAGS`] key.
#[must_use]
pub fn with_tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let arr = AttributeValue::Array(
tags.into_iter()
.map(|s| AttributeValue::String(s.into()))
.collect(),
);
self.attributes
.insert(crate::canonical_keys::TAGS.to_string(), arr);
self
}
/// Plan 16: attach a one-liner headline at the canonical
/// [`crate::canonical_keys::HEADLINE`] key.
#[must_use]
pub fn with_headline(mut self, headline: impl Into<String>) -> Self {
self.attributes.insert(
crate::canonical_keys::HEADLINE.to_string(),
AttributeValue::String(headline.into()),
);
self
}
}
/// Plan 16: options for attaching a summary that pre-collect typed
/// attributes the caller wants set in the same write window.
///
/// The engine only consumes the attributes after the summary row commits;
/// the writes happen in distinct SQL transactions (one per attribute) so
/// callers don't pay a transactional penalty per call. For atomic
/// multi-attribute writes, use [`crate::Memory::evolve`] after attach.
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct AttachSummaryOpts {
/// Caller-supplied attributes to set on the new summary row.
pub attributes: BTreeMap<String, AttributeValue>,
}
impl AttachSummaryOpts {
/// Insert one typed attribute. Multiple calls accumulate; the same
/// key overwrites the prior value.
#[must_use]
pub fn with_attribute(
mut self,
key: impl Into<String>,
value: impl Into<AttributeValue>,
) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
/// Convenience: attach an `Array<String>` keyword list at
/// [`crate::canonical_keys::KEYWORDS`].
#[must_use]
pub fn with_keywords<I, S>(mut self, keywords: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let arr = AttributeValue::Array(
keywords
.into_iter()
.map(|s| AttributeValue::String(s.into()))
.collect(),
);
self.attributes
.insert(crate::canonical_keys::KEYWORDS.to_string(), arr);
self
}
/// Convenience: attach an `Array<String>` tag list at
/// [`crate::canonical_keys::TAGS`].
#[must_use]
pub fn with_tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let arr = AttributeValue::Array(
tags.into_iter()
.map(|s| AttributeValue::String(s.into()))
.collect(),
);
self.attributes
.insert(crate::canonical_keys::TAGS.to_string(), arr);
self
}
/// Convenience: attach a one-liner headline at
/// [`crate::canonical_keys::HEADLINE`].
#[must_use]
pub fn with_headline(mut self, headline: impl Into<String>) -> Self {
self.attributes.insert(
crate::canonical_keys::HEADLINE.to_string(),
AttributeValue::String(headline.into()),
);
self
}
}
/// Options for `Memory::list`.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ListOpts {
/// Hard cap on returned items (paginated). Defaults to 100. `0` = unlimited.
pub limit: u32,
/// Pagination cursor — opaque, returned by previous `Page`.
pub cursor: Option<String>,
/// Include soft-tombstoned rows.
pub include_tombstoned: bool,
/// Plan 15: filter to rows whose validity range covers this instant.
/// A row matches when `(valid_from_ms IS NULL OR valid_from_ms <= t)
/// AND (valid_until_ms IS NULL OR valid_until_ms > t)`.
pub valid_at_ms: Option<i64>,
}
impl Default for ListOpts {
fn default() -> Self {
ListOpts {
limit: 100,
cursor: None,
include_tombstoned: false,
valid_at_ms: None,
}
}
}
impl ListOpts {
/// Plan 15: keep only rows whose validity covers the supplied instant.
#[must_use]
pub fn with_valid_at(mut self, ts_ms: i64) -> Self {
self.valid_at_ms = Some(ts_ms);
self
}
}
/// Plan 15: knobs on [`crate::Memory::links_of_with_opts`].
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct LinksOpts {
/// When `Some(k)`, only links whose `kind == k` are returned. `None`
/// returns links of every kind.
pub kind_filter: Option<LinkKind>,
}
impl LinksOpts {
/// Constrain to one link kind.
#[must_use]
pub fn with_kind(mut self, kind: LinkKind) -> Self {
self.kind_filter = Some(kind);
self
}
}
/// Options for `Memory::delete_partition`.
#[derive(Debug, Clone, Default)]
pub struct DeleteOpts {
hard: bool,
}
impl DeleteOpts {
/// Soft delete (default): tombstone-only.
#[must_use]
pub fn soft() -> Self {
DeleteOpts { hard: false }
}
/// Hard delete: physical removal on next compaction.
#[must_use]
pub fn hard() -> Self {
DeleteOpts { hard: true }
}
/// Whether the caller asked for hard delete.
#[must_use]
pub fn is_hard(&self) -> bool {
self.hard
}
}
/// One page of results.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Page<T> {
/// Items in this page.
pub items: Vec<T>,
/// Cursor for the next page; `None` means no more pages.
pub next_cursor: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_opts_defaults() {
let l = ListOpts::default();
assert_eq!(l.limit, 100);
assert!(l.cursor.is_none());
assert!(!l.include_tombstoned);
}
#[test]
fn delete_opts_soft_default() {
assert!(!DeleteOpts::soft().is_hard());
assert!(DeleteOpts::hard().is_hard());
}
#[test]
fn with_embedding_sets_field() {
let o = AppendOpts::default().with_embedding(vec![0.0; 384]);
assert_eq!(o.embedding.as_ref().map(Vec::len), Some(384));
}
}