Skip to main content

claude_api/managed_agents/
memory_stores.rs

1//! Memory stores: persistent text documents that survive across sessions.
2//!
3//! A memory store is a workspace-scoped collection of small text files
4//! (memories) addressed by path. Sessions mount stores under
5//! `/mnt/memory/` and read/write them with the standard agent toolset.
6//! Every mutation creates an immutable [`MemoryVersion`] for audit and
7//! point-in-time recovery.
8//!
9//! Limits: max 8 stores per session, individual memories capped at
10//! 100KB (~25K tokens). Structure as many small focused files.
11
12use std::collections::HashMap;
13
14use serde::{Deserialize, Serialize};
15
16use crate::client::Client;
17use crate::error::Result;
18use crate::pagination::Paginated;
19
20use super::MANAGED_AGENTS_BETA;
21
22// =====================================================================
23// Memory store types
24// =====================================================================
25
26/// A workspace-scoped collection of memories.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[non_exhaustive]
29pub struct MemoryStore {
30    /// Stable identifier (`memstore_...`).
31    pub id: String,
32    /// Wire type tag (`"memory_store"`).
33    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
34    pub ty: Option<String>,
35    /// Human-readable name.
36    pub name: String,
37    /// Description shown to the agent.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub description: Option<String>,
40    /// Free-form key-value metadata attached at create time.
41    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
42    pub metadata: HashMap<String, String>,
43    /// Creation timestamp.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub created_at: Option<String>,
46    /// Last-modified timestamp.
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub updated_at: Option<String>,
49    /// Set when the store has been archived.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub archived_at: Option<String>,
52}
53
54/// Request body for `POST /v1/memory_stores`.
55#[derive(Debug, Clone, Serialize)]
56#[non_exhaustive]
57pub struct CreateMemoryStoreRequest {
58    /// Human-readable name. Required.
59    pub name: String,
60    /// Description shown to the agent describing what's in the store.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub description: Option<String>,
63}
64
65impl CreateMemoryStoreRequest {
66    /// Build a request with the given name.
67    #[must_use]
68    pub fn new(name: impl Into<String>) -> Self {
69        Self {
70            name: name.into(),
71            description: None,
72        }
73    }
74
75    /// Attach a description.
76    #[must_use]
77    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
78        self.description = Some(desc.into());
79        self
80    }
81}
82
83/// Request body for `POST /v1/memory_stores/{id}` (update).
84#[derive(Debug, Clone, Default, Serialize)]
85#[non_exhaustive]
86pub struct UpdateMemoryStoreRequest {
87    /// New name.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub name: Option<String>,
90    /// New description.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub description: Option<String>,
93}
94
95/// Optional knobs for [`MemoryStores::list`].
96#[derive(Debug, Clone, Default)]
97#[non_exhaustive]
98pub struct ListMemoryStoresParams {
99    /// Pagination cursor.
100    pub after: Option<String>,
101    /// Pagination cursor.
102    pub before: Option<String>,
103    /// Page size.
104    pub limit: Option<u32>,
105    /// Whether to include archived stores.
106    pub include_archived: Option<bool>,
107}
108
109impl ListMemoryStoresParams {
110    fn to_query(&self) -> Vec<(&'static str, String)> {
111        let mut q = Vec::new();
112        if let Some(a) = &self.after {
113            q.push(("after", a.clone()));
114        }
115        if let Some(b) = &self.before {
116            q.push(("before", b.clone()));
117        }
118        if let Some(l) = self.limit {
119            q.push(("limit", l.to_string()));
120        }
121        if let Some(ia) = self.include_archived {
122            q.push(("include_archived", ia.to_string()));
123        }
124        q
125    }
126}
127
128// =====================================================================
129// Memory types
130// =====================================================================
131
132/// One memory inside a store.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[non_exhaustive]
135pub struct Memory {
136    /// Stable identifier (`mem_...`).
137    pub id: String,
138    /// Wire `type` tag: `"file"` or `"directory"` for directory-style
139    /// listings; preserved as-is.
140    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
141    pub ty: Option<String>,
142    /// Parent store ID.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub memory_store_id: Option<String>,
145    /// Current version ID for this memory. Updates on every mutation.
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub memory_version_id: Option<String>,
148    /// Path within the store (e.g. `/preferences/formatting.md`).
149    pub path: String,
150    /// Memory content. Absent on list responses.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub content: Option<String>,
153    /// SHA-256 of the current content. Used for optimistic-concurrency
154    /// preconditions on update.
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub content_sha256: Option<String>,
157    /// Size of the content in bytes.
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub content_size_bytes: Option<u64>,
160    /// Creation timestamp.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub created_at: Option<String>,
163    /// Last-modified timestamp.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub updated_at: Option<String>,
166}
167
168/// Request body for `POST /v1/memory_stores/{id}/memories`.
169#[derive(Debug, Clone, Serialize)]
170#[non_exhaustive]
171pub struct CreateMemoryRequest {
172    /// Path within the store.
173    pub path: String,
174    /// Initial content.
175    pub content: String,
176}
177
178impl CreateMemoryRequest {
179    /// Build with a path and content.
180    #[must_use]
181    pub fn new(path: impl Into<String>, content: impl Into<String>) -> Self {
182        Self {
183            path: path.into(),
184            content: content.into(),
185        }
186    }
187}
188
189/// Optimistic-concurrency precondition for [`UpdateMemoryRequest`].
190#[derive(Debug, Clone, Serialize)]
191#[serde(tag = "type", rename_all = "snake_case")]
192#[non_exhaustive]
193pub enum MemoryPrecondition {
194    /// Apply only if the stored content hash matches.
195    ContentSha256 {
196        /// Expected hash.
197        content_sha256: String,
198    },
199}
200
201/// Request body for `POST /v1/memory_stores/{id}/memories/{mem}` (update).
202#[derive(Debug, Clone, Default, Serialize)]
203#[non_exhaustive]
204pub struct UpdateMemoryRequest {
205    /// New content. Pass `None` to keep the current content.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub content: Option<String>,
208    /// New path (rename).
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub path: Option<String>,
211    /// Optimistic-concurrency precondition. The update is rejected if
212    /// the stored hash no longer matches.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub precondition: Option<MemoryPrecondition>,
215}
216
217/// Optional knobs for [`Memories::list`].
218#[derive(Debug, Clone, Default)]
219#[non_exhaustive]
220pub struct ListMemoriesParams {
221    /// Browse by path prefix.
222    pub path_prefix: Option<String>,
223    /// Sort key (`"path"`, etc.).
224    pub order_by: Option<String>,
225    /// Maximum recursion depth when browsing.
226    pub depth: Option<u32>,
227    /// Pagination cursor.
228    pub after: Option<String>,
229    /// Page size.
230    pub limit: Option<u32>,
231}
232
233impl ListMemoriesParams {
234    fn to_query(&self) -> Vec<(&'static str, String)> {
235        let mut q = Vec::new();
236        if let Some(p) = &self.path_prefix {
237            q.push(("path_prefix", p.clone()));
238        }
239        if let Some(o) = &self.order_by {
240            q.push(("order_by", o.clone()));
241        }
242        if let Some(d) = self.depth {
243            q.push(("depth", d.to_string()));
244        }
245        if let Some(a) = &self.after {
246            q.push(("after", a.clone()));
247        }
248        if let Some(l) = self.limit {
249            q.push(("limit", l.to_string()));
250        }
251        q
252    }
253}
254
255// =====================================================================
256// Memory version types
257// =====================================================================
258
259/// What mutation produced a [`MemoryVersion`]. Closed enum.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
261#[serde(rename_all = "lowercase")]
262#[non_exhaustive]
263pub enum MemoryVersionOperation {
264    /// Initial creation.
265    Created,
266    /// Content was modified.
267    Modified,
268    /// Memory was deleted.
269    Deleted,
270}
271
272/// Who or what produced a [`MemoryVersion`]. Tagged-union of
273/// session, API, or user actor.
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
275#[serde(tag = "type", rename_all = "snake_case")]
276#[non_exhaustive]
277pub enum MemoryActor {
278    /// Mutation made by a managed-agents session.
279    SessionActor {
280        /// Session ID.
281        session_id: String,
282    },
283    /// Mutation made directly via the API.
284    ApiActor {
285        /// API key ID that produced the mutation.
286        api_key_id: String,
287    },
288    /// Mutation made by a console user.
289    UserActor {
290        /// User ID.
291        user_id: String,
292    },
293}
294
295/// An immutable historical version of a memory.
296#[derive(Debug, Clone, Serialize, Deserialize)]
297#[non_exhaustive]
298pub struct MemoryVersion {
299    /// Stable identifier (`memver_...`).
300    pub id: String,
301    /// Wire `type`; always `"memory_version"`.
302    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
303    pub ty: Option<String>,
304    /// Parent store ID.
305    #[serde(default, skip_serializing_if = "Option::is_none")]
306    pub memory_store_id: Option<String>,
307    /// ID of the memory this version belongs to.
308    #[serde(default, skip_serializing_if = "Option::is_none")]
309    pub memory_id: Option<String>,
310    /// What kind of mutation produced this version.
311    #[serde(default, skip_serializing_if = "Option::is_none")]
312    pub operation: Option<MemoryVersionOperation>,
313    /// Path at the time of this version.
314    #[serde(default, skip_serializing_if = "Option::is_none")]
315    pub path: Option<String>,
316    /// Content at the time of this version. Absent on list responses;
317    /// the retrieve endpoint includes it. `None` for redacted versions.
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub content: Option<String>,
320    /// Size of the content in bytes.
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub content_size_bytes: Option<u64>,
323    /// SHA-256 of the content.
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub content_sha256: Option<String>,
326    /// Who created this version.
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub created_by: Option<MemoryActor>,
329    /// Creation timestamp.
330    #[serde(default, skip_serializing_if = "Option::is_none")]
331    pub created_at: Option<String>,
332    /// Set when the version has been redacted (RFC3339).
333    #[serde(default, skip_serializing_if = "Option::is_none")]
334    pub redacted_at: Option<String>,
335    /// Who redacted the version, when applicable.
336    #[serde(default, skip_serializing_if = "Option::is_none")]
337    pub redacted_by: Option<MemoryActor>,
338}
339
340/// Optional knobs for [`MemoryVersions::list`].
341#[derive(Debug, Clone, Default)]
342#[non_exhaustive]
343pub struct ListMemoryVersionsParams {
344    /// Filter to a single memory's history.
345    pub memory_id: Option<String>,
346    /// Pagination cursor.
347    pub after: Option<String>,
348    /// Page size.
349    pub limit: Option<u32>,
350}
351
352impl ListMemoryVersionsParams {
353    fn to_query(&self) -> Vec<(&'static str, String)> {
354        let mut q = Vec::new();
355        if let Some(m) = &self.memory_id {
356            q.push(("memory_id", m.clone()));
357        }
358        if let Some(a) = &self.after {
359            q.push(("after", a.clone()));
360        }
361        if let Some(l) = self.limit {
362            q.push(("limit", l.to_string()));
363        }
364        q
365    }
366}
367
368// =====================================================================
369// Namespace handles
370// =====================================================================
371
372/// Namespace handle for the memory-stores API.
373pub struct MemoryStores<'a> {
374    client: &'a Client,
375}
376
377impl<'a> MemoryStores<'a> {
378    pub(crate) fn new(client: &'a Client) -> Self {
379        Self { client }
380    }
381
382    /// `POST /v1/memory_stores`.
383    pub async fn create(&self, request: CreateMemoryStoreRequest) -> Result<MemoryStore> {
384        let body = &request;
385        self.client
386            .execute_with_retry(
387                || {
388                    self.client
389                        .request_builder(reqwest::Method::POST, "/v1/memory_stores")
390                        .json(body)
391                },
392                &[MANAGED_AGENTS_BETA],
393            )
394            .await
395    }
396
397    /// `GET /v1/memory_stores/{id}`.
398    pub async fn retrieve(&self, store_id: &str) -> Result<MemoryStore> {
399        let path = format!("/v1/memory_stores/{store_id}");
400        self.client
401            .execute_with_retry(
402                || self.client.request_builder(reqwest::Method::GET, &path),
403                &[MANAGED_AGENTS_BETA],
404            )
405            .await
406    }
407
408    /// `POST /v1/memory_stores/{id}` (update name / description).
409    pub async fn update(
410        &self,
411        store_id: &str,
412        request: UpdateMemoryStoreRequest,
413    ) -> Result<MemoryStore> {
414        let path = format!("/v1/memory_stores/{store_id}");
415        let body = &request;
416        self.client
417            .execute_with_retry(
418                || {
419                    self.client
420                        .request_builder(reqwest::Method::POST, &path)
421                        .json(body)
422                },
423                &[MANAGED_AGENTS_BETA],
424            )
425            .await
426    }
427
428    /// `GET /v1/memory_stores`.
429    pub async fn list(&self, params: ListMemoryStoresParams) -> Result<Paginated<MemoryStore>> {
430        let query = params.to_query();
431        self.client
432            .execute_with_retry(
433                || {
434                    let mut req = self
435                        .client
436                        .request_builder(reqwest::Method::GET, "/v1/memory_stores");
437                    for (k, v) in &query {
438                        req = req.query(&[(k, v)]);
439                    }
440                    req
441                },
442                &[MANAGED_AGENTS_BETA],
443            )
444            .await
445    }
446
447    /// `POST /v1/memory_stores/{id}/archive`. One-way; there is no
448    /// unarchive.
449    pub async fn archive(&self, store_id: &str) -> Result<MemoryStore> {
450        let path = format!("/v1/memory_stores/{store_id}/archive");
451        self.client
452            .execute_with_retry(
453                || self.client.request_builder(reqwest::Method::POST, &path),
454                &[MANAGED_AGENTS_BETA],
455            )
456            .await
457    }
458
459    /// `DELETE /v1/memory_stores/{id}`. Permanently removes the store
460    /// and all of its memories and versions. Use archive if you need
461    /// an audit trail.
462    pub async fn delete(&self, store_id: &str) -> Result<()> {
463        let path = format!("/v1/memory_stores/{store_id}");
464        let _: serde_json::Value = self
465            .client
466            .execute_with_retry(
467                || self.client.request_builder(reqwest::Method::DELETE, &path),
468                &[MANAGED_AGENTS_BETA],
469            )
470            .await?;
471        Ok(())
472    }
473
474    /// Sub-namespace for memory operations on a single store.
475    #[must_use]
476    pub fn memories(&self, store_id: impl Into<String>) -> Memories<'_> {
477        Memories {
478            client: self.client,
479            store_id: store_id.into(),
480        }
481    }
482
483    /// Sub-namespace for version-history operations on a single store.
484    #[must_use]
485    pub fn memory_versions(&self, store_id: impl Into<String>) -> MemoryVersions<'_> {
486        MemoryVersions {
487            client: self.client,
488            store_id: store_id.into(),
489        }
490    }
491}
492
493/// Namespace handle for memory operations on a single store.
494pub struct Memories<'a> {
495    client: &'a Client,
496    store_id: String,
497}
498
499impl Memories<'_> {
500    /// `POST /v1/memory_stores/{store_id}/memories`.
501    pub async fn create(&self, request: CreateMemoryRequest) -> Result<Memory> {
502        let path = format!("/v1/memory_stores/{}/memories", self.store_id);
503        let body = &request;
504        self.client
505            .execute_with_retry(
506                || {
507                    self.client
508                        .request_builder(reqwest::Method::POST, &path)
509                        .json(body)
510                },
511                &[MANAGED_AGENTS_BETA],
512            )
513            .await
514    }
515
516    /// `GET /v1/memory_stores/{store_id}/memories/{memory_id}`.
517    pub async fn retrieve(&self, memory_id: &str) -> Result<Memory> {
518        let path = format!("/v1/memory_stores/{}/memories/{memory_id}", self.store_id);
519        self.client
520            .execute_with_retry(
521                || self.client.request_builder(reqwest::Method::GET, &path),
522                &[MANAGED_AGENTS_BETA],
523            )
524            .await
525    }
526
527    /// `POST /v1/memory_stores/{store_id}/memories/{memory_id}` (update).
528    /// Pass an [`UpdateMemoryRequest::precondition`] for safe concurrent
529    /// edits.
530    pub async fn update(&self, memory_id: &str, request: UpdateMemoryRequest) -> Result<Memory> {
531        let path = format!("/v1/memory_stores/{}/memories/{memory_id}", self.store_id);
532        let body = &request;
533        self.client
534            .execute_with_retry(
535                || {
536                    self.client
537                        .request_builder(reqwest::Method::POST, &path)
538                        .json(body)
539                },
540                &[MANAGED_AGENTS_BETA],
541            )
542            .await
543    }
544
545    /// `GET /v1/memory_stores/{store_id}/memories`.
546    pub async fn list(&self, params: ListMemoriesParams) -> Result<Paginated<Memory>> {
547        let path = format!("/v1/memory_stores/{}/memories", self.store_id);
548        let query = params.to_query();
549        self.client
550            .execute_with_retry(
551                || {
552                    let mut req = self.client.request_builder(reqwest::Method::GET, &path);
553                    for (k, v) in &query {
554                        req = req.query(&[(k, v)]);
555                    }
556                    req
557                },
558                &[MANAGED_AGENTS_BETA],
559            )
560            .await
561    }
562
563    /// `DELETE /v1/memory_stores/{store_id}/memories/{memory_id}`.
564    pub async fn delete(&self, memory_id: &str) -> Result<()> {
565        let path = format!("/v1/memory_stores/{}/memories/{memory_id}", self.store_id);
566        let _: serde_json::Value = self
567            .client
568            .execute_with_retry(
569                || self.client.request_builder(reqwest::Method::DELETE, &path),
570                &[MANAGED_AGENTS_BETA],
571            )
572            .await?;
573        Ok(())
574    }
575}
576
577/// Namespace handle for memory-version (history) operations on a store.
578pub struct MemoryVersions<'a> {
579    client: &'a Client,
580    store_id: String,
581}
582
583impl MemoryVersions<'_> {
584    /// `GET /v1/memory_stores/{store_id}/memory_versions`.
585    pub async fn list(&self, params: ListMemoryVersionsParams) -> Result<Paginated<MemoryVersion>> {
586        let path = format!("/v1/memory_stores/{}/memory_versions", self.store_id);
587        let query = params.to_query();
588        self.client
589            .execute_with_retry(
590                || {
591                    let mut req = self.client.request_builder(reqwest::Method::GET, &path);
592                    for (k, v) in &query {
593                        req = req.query(&[(k, v)]);
594                    }
595                    req
596                },
597                &[MANAGED_AGENTS_BETA],
598            )
599            .await
600    }
601
602    /// `GET /v1/memory_stores/{store_id}/memory_versions/{version_id}`.
603    pub async fn retrieve(&self, version_id: &str) -> Result<MemoryVersion> {
604        let path = format!(
605            "/v1/memory_stores/{}/memory_versions/{version_id}",
606            self.store_id
607        );
608        self.client
609            .execute_with_retry(
610                || self.client.request_builder(reqwest::Method::GET, &path),
611                &[MANAGED_AGENTS_BETA],
612            )
613            .await
614    }
615
616    /// `POST /v1/memory_stores/{store_id}/memory_versions/{version_id}/redact`.
617    /// Scrubs content while preserving the audit trail. Cannot redact
618    /// the head version of a live memory; write a new version or
619    /// delete the memory first.
620    pub async fn redact(&self, version_id: &str) -> Result<MemoryVersion> {
621        let path = format!(
622            "/v1/memory_stores/{}/memory_versions/{version_id}/redact",
623            self.store_id
624        );
625        self.client
626            .execute_with_retry(
627                || {
628                    self.client
629                        .request_builder(reqwest::Method::POST, &path)
630                        .json(&serde_json::json!({}))
631                },
632                &[MANAGED_AGENTS_BETA],
633            )
634            .await
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641    use pretty_assertions::assert_eq;
642    use serde_json::json;
643    use wiremock::matchers::{body_partial_json, method, path};
644    use wiremock::{Mock, MockServer, ResponseTemplate};
645
646    fn client_for(mock: &MockServer) -> Client {
647        Client::builder()
648            .api_key("sk-ant-test")
649            .base_url(mock.uri())
650            .build()
651            .unwrap()
652    }
653
654    #[tokio::test]
655    async fn create_memory_store_round_trips() {
656        let mock = MockServer::start().await;
657        Mock::given(method("POST"))
658            .and(path("/v1/memory_stores"))
659            .and(body_partial_json(json!({
660                "name": "User Preferences",
661                "description": "Per-user preferences."
662            })))
663            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
664                "id": "memstore_01",
665                "type": "memory_store",
666                "name": "User Preferences",
667                "description": "Per-user preferences."
668            })))
669            .mount(&mock)
670            .await;
671
672        let client = client_for(&mock);
673        let req = CreateMemoryStoreRequest::new("User Preferences")
674            .with_description("Per-user preferences.");
675        let s = client
676            .managed_agents()
677            .memory_stores()
678            .create(req)
679            .await
680            .unwrap();
681        assert_eq!(s.id, "memstore_01");
682    }
683
684    #[tokio::test]
685    async fn create_memory_under_store() {
686        let mock = MockServer::start().await;
687        Mock::given(method("POST"))
688            .and(path("/v1/memory_stores/memstore_01/memories"))
689            .and(body_partial_json(json!({
690                "path": "/preferences/formatting.md",
691                "content": "Always use tabs."
692            })))
693            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
694                "id": "mem_01",
695                "type": "file",
696                "path": "/preferences/formatting.md",
697                "content": "Always use tabs.",
698                "content_sha256": "abc123"
699            })))
700            .mount(&mock)
701            .await;
702
703        let client = client_for(&mock);
704        let req = CreateMemoryRequest::new("/preferences/formatting.md", "Always use tabs.");
705        let m = client
706            .managed_agents()
707            .memory_stores()
708            .memories("memstore_01")
709            .create(req)
710            .await
711            .unwrap();
712        assert_eq!(m.id, "mem_01");
713        assert_eq!(m.content_sha256.as_deref(), Some("abc123"));
714    }
715
716    #[tokio::test]
717    async fn update_memory_with_content_sha256_precondition() {
718        let mock = MockServer::start().await;
719        Mock::given(method("POST"))
720            .and(path("/v1/memory_stores/memstore_01/memories/mem_01"))
721            .and(body_partial_json(json!({
722                "content": "CORRECTED",
723                "precondition": {"type": "content_sha256", "content_sha256": "abc123"}
724            })))
725            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
726                "id": "mem_01",
727                "path": "/preferences/formatting.md",
728                "content": "CORRECTED",
729                "content_sha256": "def456"
730            })))
731            .mount(&mock)
732            .await;
733
734        let client = client_for(&mock);
735        let req = UpdateMemoryRequest {
736            content: Some("CORRECTED".into()),
737            path: None,
738            precondition: Some(MemoryPrecondition::ContentSha256 {
739                content_sha256: "abc123".into(),
740            }),
741        };
742        let m = client
743            .managed_agents()
744            .memory_stores()
745            .memories("memstore_01")
746            .update("mem_01", req)
747            .await
748            .unwrap();
749        assert_eq!(m.content_sha256.as_deref(), Some("def456"));
750    }
751
752    #[tokio::test]
753    async fn list_memories_passes_path_prefix_query() {
754        let mock = MockServer::start().await;
755        Mock::given(method("GET"))
756            .and(path("/v1/memory_stores/memstore_01/memories"))
757            .and(wiremock::matchers::query_param(
758                "path_prefix",
759                "/preferences/",
760            ))
761            .and(wiremock::matchers::query_param("depth", "2"))
762            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
763                "data": [
764                    {"id": "mem_01", "type": "file", "path": "/preferences/formatting.md"}
765                ],
766                "has_more": false
767            })))
768            .mount(&mock)
769            .await;
770
771        let client = client_for(&mock);
772        let page = client
773            .managed_agents()
774            .memory_stores()
775            .memories("memstore_01")
776            .list(ListMemoriesParams {
777                path_prefix: Some("/preferences/".into()),
778                depth: Some(2),
779                ..Default::default()
780            })
781            .await
782            .unwrap();
783        assert_eq!(page.data.len(), 1);
784    }
785
786    #[tokio::test]
787    async fn retrieve_memory_store_returns_typed_record() {
788        let mock = MockServer::start().await;
789        Mock::given(method("GET"))
790            .and(path("/v1/memory_stores/memstore_01"))
791            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
792                "id": "memstore_01",
793                "type": "memory_store",
794                "name": "Prefs"
795            })))
796            .mount(&mock)
797            .await;
798        let client = client_for(&mock);
799        let s = client
800            .managed_agents()
801            .memory_stores()
802            .retrieve("memstore_01")
803            .await
804            .unwrap();
805        assert_eq!(s.id, "memstore_01");
806    }
807
808    #[tokio::test]
809    async fn update_memory_store_patches_name_and_description() {
810        let mock = MockServer::start().await;
811        Mock::given(method("POST"))
812            .and(path("/v1/memory_stores/memstore_01"))
813            .and(wiremock::matchers::body_partial_json(json!({
814                "name": "Renamed",
815                "description": "New desc."
816            })))
817            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
818                "id": "memstore_01",
819                "name": "Renamed",
820                "description": "New desc."
821            })))
822            .mount(&mock)
823            .await;
824        let client = client_for(&mock);
825        let s = client
826            .managed_agents()
827            .memory_stores()
828            .update(
829                "memstore_01",
830                UpdateMemoryStoreRequest {
831                    name: Some("Renamed".into()),
832                    description: Some("New desc.".into()),
833                },
834            )
835            .await
836            .unwrap();
837        assert_eq!(s.name, "Renamed");
838    }
839
840    #[tokio::test]
841    async fn list_memory_stores_passes_include_archived_query() {
842        let mock = MockServer::start().await;
843        Mock::given(method("GET"))
844            .and(path("/v1/memory_stores"))
845            .and(wiremock::matchers::query_param("include_archived", "true"))
846            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
847                "data": [{"id": "memstore_01", "name": "Prefs", "archived_at": "2026-04-30T12:00:00Z"}],
848                "has_more": false
849            })))
850            .mount(&mock)
851            .await;
852        let client = client_for(&mock);
853        let page = client
854            .managed_agents()
855            .memory_stores()
856            .list(ListMemoryStoresParams {
857                include_archived: Some(true),
858                ..Default::default()
859            })
860            .await
861            .unwrap();
862        assert_eq!(page.data.len(), 1);
863        assert!(page.data[0].archived_at.is_some());
864    }
865
866    #[tokio::test]
867    async fn archive_memory_store_posts_to_archive_subpath() {
868        let mock = MockServer::start().await;
869        Mock::given(method("POST"))
870            .and(path("/v1/memory_stores/memstore_01/archive"))
871            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
872                "id": "memstore_01",
873                "name": "Prefs",
874                "archived_at": "2026-04-30T12:00:00Z"
875            })))
876            .mount(&mock)
877            .await;
878        let client = client_for(&mock);
879        let s = client
880            .managed_agents()
881            .memory_stores()
882            .archive("memstore_01")
883            .await
884            .unwrap();
885        assert!(s.archived_at.is_some());
886    }
887
888    #[tokio::test]
889    async fn delete_memory_store_returns_unit() {
890        let mock = MockServer::start().await;
891        Mock::given(method("DELETE"))
892            .and(path("/v1/memory_stores/memstore_01"))
893            .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
894            .mount(&mock)
895            .await;
896        let client = client_for(&mock);
897        client
898            .managed_agents()
899            .memory_stores()
900            .delete("memstore_01")
901            .await
902            .unwrap();
903    }
904
905    #[tokio::test]
906    async fn retrieve_memory_returns_full_content() {
907        let mock = MockServer::start().await;
908        Mock::given(method("GET"))
909            .and(path("/v1/memory_stores/memstore_01/memories/mem_01"))
910            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
911                "id": "mem_01",
912                "type": "file",
913                "path": "/notes.md",
914                "content": "Hello.",
915                "content_sha256": "abc"
916            })))
917            .mount(&mock)
918            .await;
919        let client = client_for(&mock);
920        let m = client
921            .managed_agents()
922            .memory_stores()
923            .memories("memstore_01")
924            .retrieve("mem_01")
925            .await
926            .unwrap();
927        assert_eq!(m.content.as_deref(), Some("Hello."));
928    }
929
930    #[tokio::test]
931    async fn delete_memory_returns_unit() {
932        let mock = MockServer::start().await;
933        Mock::given(method("DELETE"))
934            .and(path("/v1/memory_stores/memstore_01/memories/mem_01"))
935            .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
936            .mount(&mock)
937            .await;
938        let client = client_for(&mock);
939        client
940            .managed_agents()
941            .memory_stores()
942            .memories("memstore_01")
943            .delete("mem_01")
944            .await
945            .unwrap();
946    }
947
948    #[tokio::test]
949    async fn retrieve_memory_version_includes_content() {
950        let mock = MockServer::start().await;
951        Mock::given(method("GET"))
952            .and(path(
953                "/v1/memory_stores/memstore_01/memory_versions/memver_01",
954            ))
955            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
956                "id": "memver_01",
957                "memory_id": "mem_01",
958                "operation": "created",
959                "path": "/notes.md",
960                "content": "Original."
961            })))
962            .mount(&mock)
963            .await;
964        let client = client_for(&mock);
965        let v = client
966            .managed_agents()
967            .memory_stores()
968            .memory_versions("memstore_01")
969            .retrieve("memver_01")
970            .await
971            .unwrap();
972        assert_eq!(v.content.as_deref(), Some("Original."));
973        assert_eq!(v.operation, Some(MemoryVersionOperation::Created));
974    }
975
976    #[tokio::test]
977    async fn redact_memory_version_posts_to_redact_subpath() {
978        let mock = MockServer::start().await;
979        Mock::given(method("POST"))
980            .and(path(
981                "/v1/memory_stores/memstore_01/memory_versions/memver_01/redact",
982            ))
983            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
984                "id": "memver_01",
985                "operation": "deleted",
986                "redacted_at": "2026-04-30T12:00:00Z",
987                "redacted_by": {"type": "api_actor", "api_key_id": "ak_01"}
988            })))
989            .mount(&mock)
990            .await;
991
992        let client = client_for(&mock);
993        let v = client
994            .managed_agents()
995            .memory_stores()
996            .memory_versions("memstore_01")
997            .redact("memver_01")
998            .await
999            .unwrap();
1000        assert_eq!(v.redacted_at.as_deref(), Some("2026-04-30T12:00:00Z"));
1001        match v.redacted_by.unwrap() {
1002            MemoryActor::ApiActor { api_key_id } => assert_eq!(api_key_id, "ak_01"),
1003            _ => panic!("expected ApiActor"),
1004        }
1005    }
1006
1007    #[tokio::test]
1008    async fn list_memory_versions_filters_by_memory_id() {
1009        let mock = MockServer::start().await;
1010        Mock::given(method("GET"))
1011            .and(path("/v1/memory_stores/memstore_01/memory_versions"))
1012            .and(wiremock::matchers::query_param("memory_id", "mem_01"))
1013            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1014                "data": [
1015                    {"id": "memver_02", "operation": "modified"},
1016                    {"id": "memver_01", "operation": "created"}
1017                ],
1018                "has_more": false
1019            })))
1020            .mount(&mock)
1021            .await;
1022
1023        let client = client_for(&mock);
1024        let page = client
1025            .managed_agents()
1026            .memory_stores()
1027            .memory_versions("memstore_01")
1028            .list(ListMemoryVersionsParams {
1029                memory_id: Some("mem_01".into()),
1030                ..Default::default()
1031            })
1032            .await
1033            .unwrap();
1034        assert_eq!(page.data.len(), 2);
1035    }
1036
1037    #[test]
1038    fn memory_actor_round_trips_all_three_variants() {
1039        for (actor, expected) in [
1040            (
1041                MemoryActor::SessionActor {
1042                    session_id: "sesn_x".into(),
1043                },
1044                json!({"type": "session_actor", "session_id": "sesn_x"}),
1045            ),
1046            (
1047                MemoryActor::ApiActor {
1048                    api_key_id: "ak_x".into(),
1049                },
1050                json!({"type": "api_actor", "api_key_id": "ak_x"}),
1051            ),
1052            (
1053                MemoryActor::UserActor {
1054                    user_id: "user_x".into(),
1055                },
1056                json!({"type": "user_actor", "user_id": "user_x"}),
1057            ),
1058        ] {
1059            let v = serde_json::to_value(&actor).unwrap();
1060            assert_eq!(v, expected);
1061            let parsed: MemoryActor = serde_json::from_value(v).unwrap();
1062            assert_eq!(parsed, actor);
1063        }
1064    }
1065
1066    #[test]
1067    fn memory_version_operation_round_trips_lowercase() {
1068        for (op, wire) in [
1069            (MemoryVersionOperation::Created, "created"),
1070            (MemoryVersionOperation::Modified, "modified"),
1071            (MemoryVersionOperation::Deleted, "deleted"),
1072        ] {
1073            let v = serde_json::to_value(op).unwrap();
1074            assert_eq!(v, json!(wire));
1075            let parsed: MemoryVersionOperation = serde_json::from_value(v).unwrap();
1076            assert_eq!(parsed, op);
1077        }
1078    }
1079}