Skip to main content

adk_memory/
service.rs

1use adk_core::{Content, Result};
2use async_trait::async_trait;
3use chrono::{DateTime, Utc};
4
5#[derive(Debug, Clone)]
6pub struct MemoryEntry {
7    pub content: Content,
8    pub author: String,
9    pub timestamp: DateTime<Utc>,
10}
11
12#[derive(Debug, Clone, serde::Deserialize)]
13pub struct SearchRequest {
14    pub query: String,
15    pub user_id: String,
16    pub app_name: String,
17    /// Maximum number of results to return. `None` defaults to 10.
18    pub limit: Option<usize>,
19    /// Minimum similarity score threshold (0.0–1.0). Results below this
20    /// score are excluded. `None` means no threshold.
21    pub min_score: Option<f32>,
22    /// Optional project scope. `None` returns only global entries.
23    /// `Some(id)` returns global entries + entries for that project.
24    #[serde(default)]
25    pub project_id: Option<String>,
26}
27
28#[derive(Debug, Clone)]
29pub struct SearchResponse {
30    pub memories: Vec<MemoryEntry>,
31}
32
33/// Validate a project identifier.
34///
35/// Returns `Ok(())` if the project_id is non-empty and at most 256 characters.
36/// Returns a descriptive error otherwise.
37pub fn validate_project_id(project_id: &str) -> Result<()> {
38    if project_id.is_empty() {
39        return Err(adk_core::AdkError::memory("project_id must not be empty"));
40    }
41    if project_id.len() > 256 {
42        return Err(adk_core::AdkError::memory(format!(
43            "project_id exceeds maximum length of 256 characters (got {})",
44            project_id.len()
45        )));
46    }
47    Ok(())
48}
49
50#[async_trait]
51pub trait MemoryService: Send + Sync {
52    async fn add_session(
53        &self,
54        app_name: &str,
55        user_id: &str,
56        session_id: &str,
57        entries: Vec<MemoryEntry>,
58    ) -> Result<()>;
59    async fn search(&self, req: SearchRequest) -> Result<SearchResponse>;
60
61    /// Delete all memory entries for a specific user.
62    ///
63    /// Required for GDPR right-to-erasure compliance. Removes all stored
64    /// memories (including embeddings) for the given app and user.
65    async fn delete_user(&self, app_name: &str, user_id: &str) -> Result<()> {
66        let _ = (app_name, user_id);
67        Err(adk_core::AdkError::memory("delete_user not implemented"))
68    }
69
70    /// Delete all memory entries for a specific session.
71    async fn delete_session(&self, app_name: &str, user_id: &str, session_id: &str) -> Result<()> {
72        let _ = (app_name, user_id, session_id);
73        Err(adk_core::AdkError::memory("delete_session not implemented"))
74    }
75
76    /// Add a single memory entry directly (not tied to a session).
77    async fn add_entry(&self, app_name: &str, user_id: &str, entry: MemoryEntry) -> Result<()> {
78        let _ = (app_name, user_id, entry);
79        Err(adk_core::AdkError::memory("add_entry not implemented"))
80    }
81
82    /// Delete entries matching a query. Returns count of deleted entries.
83    async fn delete_entries(&self, app_name: &str, user_id: &str, query: &str) -> Result<u64> {
84        let _ = (app_name, user_id, query);
85        Err(adk_core::AdkError::memory("delete_entries not implemented"))
86    }
87
88    /// List the most recent entries for a user (global + project-scoped),
89    /// newest first. Unlike `search`, this is a pure recency listing — no
90    /// query, no similarity ranking — suitable for "what does the agent
91    /// remember?" UIs and latest-item lookups.
92    async fn list_recent(
93        &self,
94        app_name: &str,
95        user_id: &str,
96        limit: usize,
97    ) -> Result<Vec<MemoryEntry>> {
98        let _ = (app_name, user_id, limit);
99        Err(adk_core::AdkError::memory("list_recent not implemented"))
100    }
101
102    /// Verify backend connectivity.
103    ///
104    /// Returns `Ok(())` if the backend is reachable and responsive.
105    /// The default implementation always succeeds (suitable for in-memory).
106    async fn health_check(&self) -> Result<()> {
107        Ok(())
108    }
109
110    /// Whether this backend keeps project-scoped entries isolated.
111    ///
112    /// Returns `false` by default. A backend that implements the project methods
113    /// below overrides this to `true`, so a caller can tell isolation apart from a
114    /// backend that has no project support rather than discovering it from data.
115    fn supports_project_scoping(&self) -> bool {
116        false
117    }
118
119    /// Adds session entries scoped to a project.
120    ///
121    /// # Errors
122    ///
123    /// The default implementation returns an error. Discarding `project_id` and
124    /// writing globally would make entries intended for one project visible to
125    /// everything else under the same app and user, with nothing in the return value
126    /// to say so, so a backend without project support refuses the write instead.
127    async fn add_session_to_project(
128        &self,
129        app_name: &str,
130        user_id: &str,
131        session_id: &str,
132        project_id: &str,
133        entries: Vec<MemoryEntry>,
134    ) -> Result<()> {
135        let _ = (app_name, user_id, session_id, project_id, entries);
136        Err(project_scoping_unsupported("add_session_to_project"))
137    }
138
139    /// Adds a single entry scoped to a project.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error by default, for the reason given on
144    /// [`MemoryService::add_session_to_project`].
145    async fn add_entry_to_project(
146        &self,
147        app_name: &str,
148        user_id: &str,
149        project_id: &str,
150        entry: MemoryEntry,
151    ) -> Result<()> {
152        let _ = (app_name, user_id, project_id, entry);
153        Err(project_scoping_unsupported("add_entry_to_project"))
154    }
155
156    /// Deletes entries matching a query within a specific project.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error by default. Falling back to a global delete would remove
161    /// entries outside the named project, which is worse than refusing.
162    async fn delete_entries_in_project(
163        &self,
164        app_name: &str,
165        user_id: &str,
166        project_id: &str,
167        query: &str,
168    ) -> Result<u64> {
169        let _ = (app_name, user_id, project_id, query);
170        Err(project_scoping_unsupported("delete_entries_in_project"))
171    }
172
173    /// Delete all entries for a specific project.
174    /// Default returns "not implemented" error.
175    async fn delete_project(&self, app_name: &str, user_id: &str, project_id: &str) -> Result<u64> {
176        let _ = (app_name, user_id, project_id);
177        Err(adk_core::AdkError::memory("delete_project not implemented"))
178    }
179}
180
181/// The error a backend without project support returns from a project method.
182fn project_scoping_unsupported(method: &str) -> adk_core::AdkError {
183    adk_core::AdkError::memory(format!(
184        "this memory backend does not implement project scoping, so `{method}` cannot honour \
185         the project boundary; use a backend whose `supports_project_scoping` returns true, or \
186         call the global method explicitly if global scope is intended"
187    ))
188}