Skip to main content

boxxkite_client/
memory.rs

1//! MemoryBase: durable, account-scoped agent memory owned end-to-end by the
2//! control plane (no third-party memory API) -- `/v1/memory*`. Mirrors
3//! `sdk-python`'s `remember`/`recall`/`ingest_memory`/`memory_profile`/
4//! `forget_memory` plus the rest of the REST surface.
5
6use std::collections::HashMap;
7
8use reqwest::Method;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::client::Client;
13use crate::error::BoxxkiteError;
14
15/// One durable, account-scoped memory (`POST/GET /v1/memory`).
16#[derive(Debug, Clone, Deserialize)]
17pub struct Memory {
18    pub id: String,
19    pub scope: String,
20    pub kind: String,
21    pub content: String,
22    #[serde(default)]
23    pub metadata: HashMap<String, Value>,
24    pub source_session_id: Option<String>,
25    pub created_at: String,
26    pub updated_at: String,
27    pub expires_at: Option<String>,
28    pub source_content: Option<String>,
29    pub document_date: Option<String>,
30    #[serde(default)]
31    pub event_dates: Vec<String>,
32    pub importance: f64,
33    pub access_count: i64,
34    pub last_accessed_at: Option<String>,
35    pub superseded_at: Option<String>,
36    pub superseded_by_id: Option<String>,
37}
38
39/// A memory returned from [`Client::recall`], with its retrieval score.
40#[derive(Debug, Clone, Deserialize)]
41pub struct MemorySearchHit {
42    #[serde(flatten)]
43    pub memory: Memory,
44    pub score: f64,
45}
46
47/// Response from [`Client::recall`] (`GET /v1/memory/search`).
48#[derive(Debug, Clone, Deserialize)]
49pub struct MemorySearchResponse {
50    pub query: String,
51    pub memories: Vec<MemorySearchHit>,
52}
53
54/// Response from [`Client::ingest_memory`] and [`Client::import_memories`].
55#[derive(Debug, Clone, Deserialize)]
56pub struct MemoryIngestResponse {
57    pub source_session_id: Option<String>,
58    pub memories: Vec<Memory>,
59}
60
61/// Response from [`Client::memory_profile`] -- stable, long-lived memories
62/// separated from recently-touched ones.
63#[derive(Debug, Clone, Deserialize)]
64pub struct MemoryProfileResponse {
65    pub r#static: Vec<Memory>,
66    pub dynamic: Vec<Memory>,
67}
68
69/// One edge from [`Client::memory_relations`].
70#[derive(Debug, Clone, Deserialize)]
71pub struct MemoryRelation {
72    pub source_memory_id: String,
73    pub target_memory_id: String,
74    pub relation_type: String,
75    pub confidence: f64,
76    pub created_at: String,
77}
78
79/// Response from [`Client::memory_relations`] (`GET /v1/memory/{id}/relations`).
80#[derive(Debug, Clone, Deserialize)]
81pub struct MemoryRelationsResponse {
82    pub memory_id: String,
83    pub relations: Vec<MemoryRelation>,
84}
85
86/// Builder for [`Client::remember`]'s optional fields.
87#[derive(Debug, Clone, Default, Serialize)]
88pub struct RememberOptions {
89    #[serde(skip_serializing_if = "Option::is_none")]
90    scope: Option<String>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    kind: Option<String>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    metadata: Option<HashMap<String, Value>>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    source_session_id: Option<String>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    importance: Option<f64>,
99}
100
101impl RememberOptions {
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Partitions memories within an account (e.g. per project). Not an
107    /// authorization boundary by itself -- every read/search/mutation is
108    /// still filtered by the authenticated account. Defaults to
109    /// `"default"` server-side when omitted.
110    pub fn scope(mut self, scope: impl Into<String>) -> Self {
111        self.scope = Some(scope.into());
112        self
113    }
114
115    /// One of `fact`/`preference`/`goal`/`instruction`/`note`/`summary`.
116    /// Defaults to `"fact"` server-side when omitted.
117    pub fn kind(mut self, kind: impl Into<String>) -> Self {
118        self.kind = Some(kind.into());
119        self
120    }
121
122    pub fn metadata(mut self, metadata: HashMap<String, Value>) -> Self {
123        self.metadata = Some(metadata);
124        self
125    }
126
127    pub fn source_session_id(mut self, source_session_id: impl Into<String>) -> Self {
128        self.source_session_id = Some(source_session_id.into());
129        self
130    }
131
132    /// `0.0..=1.0`. Defaults to `0.5` server-side when omitted.
133    pub fn importance(mut self, importance: f64) -> Self {
134        self.importance = Some(importance);
135        self
136    }
137}
138
139/// Builder for [`Client::ingest_memory`]'s optional fields.
140#[derive(Debug, Clone, Default, Serialize)]
141pub struct IngestMemoryOptions {
142    #[serde(skip_serializing_if = "Option::is_none")]
143    scope: Option<String>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    source_session_id: Option<String>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    metadata: Option<HashMap<String, Value>>,
148}
149
150impl IngestMemoryOptions {
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    pub fn scope(mut self, scope: impl Into<String>) -> Self {
156        self.scope = Some(scope.into());
157        self
158    }
159
160    pub fn source_session_id(mut self, source_session_id: impl Into<String>) -> Self {
161        self.source_session_id = Some(source_session_id.into());
162        self
163    }
164
165    pub fn metadata(mut self, metadata: HashMap<String, Value>) -> Self {
166        self.metadata = Some(metadata);
167        self
168    }
169}
170
171/// Options for [`Client::list_memories`].
172#[derive(Debug, Clone, Default)]
173pub struct ListMemoriesOptions {
174    pub scope: Option<String>,
175    pub include_superseded: bool,
176    pub limit: Option<u32>,
177    pub offset: Option<u32>,
178}
179
180impl Client {
181    /// `POST /v1/memory` -- store one durable, account-scoped memory
182    /// directly. See the MemoryBase developer guide for the full model
183    /// (opt-in, self-hosted retrieval by default, no third-party memory
184    /// API).
185    pub async fn remember(
186        &self,
187        content: &str,
188        options: RememberOptions,
189    ) -> Result<Memory, BoxxkiteError> {
190        #[derive(Serialize)]
191        struct Body<'a> {
192            content: &'a str,
193            #[serde(flatten)]
194            options: &'a RememberOptions,
195        }
196        let builder = self.request(Method::POST, "/v1/memory").json(&Body {
197            content,
198            options: &options,
199        });
200        self.send(builder).await
201    }
202
203    /// `POST /v1/memory/ingest` -- split a bounded document or conversation
204    /// into atomic memories. Works with no model provider configured (the
205    /// default extractor is deterministic).
206    pub async fn ingest_memory(
207        &self,
208        content: &str,
209        options: IngestMemoryOptions,
210    ) -> Result<MemoryIngestResponse, BoxxkiteError> {
211        #[derive(Serialize)]
212        struct Body<'a> {
213            content: &'a str,
214            #[serde(flatten)]
215            options: &'a IngestMemoryOptions,
216        }
217        let builder = self.request(Method::POST, "/v1/memory/ingest").json(&Body {
218            content,
219            options: &options,
220        });
221        self.send(builder).await
222    }
223
224    /// `GET /v1/memory/search` -- rank live account memories against a
225    /// query using lexical evidence, phrase match, recency, and importance.
226    pub async fn recall(
227        &self,
228        query: &str,
229        scope: Option<&str>,
230        limit: Option<u32>,
231    ) -> Result<MemorySearchResponse, BoxxkiteError> {
232        let mut params = vec![("q".to_string(), query.to_string())];
233        params.push(("limit".to_string(), limit.unwrap_or(10).to_string()));
234        if let Some(scope) = scope {
235            params.push(("scope".to_string(), scope.to_string()));
236        }
237        let builder = self
238            .request(Method::GET, "/v1/memory/search")
239            .query(&params);
240        self.send(builder).await
241    }
242
243    /// `GET /v1/memory/profile` -- stable, long-lived memories separated
244    /// from recently-touched ones.
245    pub async fn memory_profile(
246        &self,
247        scope: Option<&str>,
248        limit: Option<u32>,
249    ) -> Result<MemoryProfileResponse, BoxxkiteError> {
250        let mut params = vec![("limit".to_string(), limit.unwrap_or(20).to_string())];
251        if let Some(scope) = scope {
252            params.push(("scope".to_string(), scope.to_string()));
253        }
254        let builder = self
255            .request(Method::GET, "/v1/memory/profile")
256            .query(&params);
257        self.send(builder).await
258    }
259
260    /// `GET /v1/memory` -- live memories for this account.
261    pub async fn list_memories(
262        &self,
263        options: ListMemoriesOptions,
264    ) -> Result<Vec<Memory>, BoxxkiteError> {
265        let mut params = vec![
266            (
267                "include_superseded".to_string(),
268                options.include_superseded.to_string(),
269            ),
270            ("limit".to_string(), options.limit.unwrap_or(50).to_string()),
271            (
272                "offset".to_string(),
273                options.offset.unwrap_or(0).to_string(),
274            ),
275        ];
276        if let Some(scope) = options.scope {
277            params.push(("scope".to_string(), scope));
278        }
279        let builder = self.request(Method::GET, "/v1/memory").query(&params);
280        self.send_or_default(builder).await
281    }
282
283    /// `GET /v1/memory/{id}` -- one live memory owned by this account.
284    pub async fn get_memory(&self, memory_id: &str) -> Result<Memory, BoxxkiteError> {
285        let builder = self.request(Method::GET, &format!("/v1/memory/{memory_id}"));
286        self.send(builder).await
287    }
288
289    /// `GET /v1/memory/{id}/relations` -- other memories related to this
290    /// one (updates/extends/related/derives edges).
291    pub async fn memory_relations(
292        &self,
293        memory_id: &str,
294        limit: Option<u32>,
295    ) -> Result<MemoryRelationsResponse, BoxxkiteError> {
296        let builder = self
297            .request(Method::GET, &format!("/v1/memory/{memory_id}/relations"))
298            .query(&[("limit", limit.unwrap_or(20).to_string())]);
299        self.send(builder).await
300    }
301
302    /// `DELETE /v1/memory/{id}` -- permanently delete one memory. Not a
303    /// soft-delete; a forgotten memory cannot be recalled again.
304    pub async fn forget_memory(&self, memory_id: &str) -> Result<(), BoxxkiteError> {
305        let builder = self.request(Method::DELETE, &format!("/v1/memory/{memory_id}"));
306        self.send_no_content(builder).await
307    }
308
309    /// `GET /v1/memory/export` -- export live memories (and their
310    /// relations) for this account, for backup or migration.
311    pub async fn export_memories(&self, scope: Option<&str>) -> Result<Value, BoxxkiteError> {
312        let mut builder = self.request(Method::GET, "/v1/memory/export");
313        if let Some(scope) = scope {
314            builder = builder.query(&[("scope", scope)]);
315        }
316        self.send(builder).await
317    }
318
319    /// `POST /v1/memory/import` -- import memories (and optionally their
320    /// relations) previously produced by [`Client::export_memories`].
321    pub async fn import_memories(
322        &self,
323        memories: Vec<Value>,
324        relations: Vec<Value>,
325    ) -> Result<MemoryIngestResponse, BoxxkiteError> {
326        #[derive(Serialize)]
327        struct Body {
328            memories: Vec<Value>,
329            relations: Vec<Value>,
330        }
331        let builder = self.request(Method::POST, "/v1/memory/import").json(&Body {
332            memories,
333            relations,
334        });
335        self.send(builder).await
336    }
337}