Skip to main content

adk_session/
service.rs

1use crate::{Event, Session};
2use adk_core::identity::{AdkIdentity, AppName, SessionId, UserId};
3use adk_core::{AdkError, ErrorComponent, Result};
4use async_trait::async_trait;
5use chrono::{DateTime, Utc};
6use serde_json::Value;
7use std::collections::HashMap;
8
9/// Request to create a new session.
10#[derive(Debug, Clone)]
11pub struct CreateRequest {
12    /// Application name that owns the session.
13    pub app_name: String,
14    /// User identifier for the session owner.
15    pub user_id: String,
16    /// Optional session ID; generated if not provided.
17    pub session_id: Option<String>,
18    /// Initial state key-value pairs for the session.
19    pub state: HashMap<String, Value>,
20}
21
22impl CreateRequest {
23    /// Returns the application name as a typed [`AppName`].
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if the raw string fails identity validation.
28    pub fn try_app_name(&self) -> Result<AppName> {
29        Ok(AppName::try_from(self.app_name.as_str())?)
30    }
31
32    /// Returns the user identifier as a typed [`UserId`].
33    ///
34    /// # Errors
35    ///
36    /// Returns an error if the raw string fails identity validation.
37    pub fn try_user_id(&self) -> Result<UserId> {
38        Ok(UserId::try_from(self.user_id.as_str())?)
39    }
40
41    /// Returns the session identifier as a typed [`SessionId`], if one was
42    /// provided.
43    ///
44    /// Returns `Ok(None)` when `session_id` is `None` (the service will
45    /// generate one). Returns an error only when a non-`None` value fails
46    /// identity validation.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if the provided session ID string fails validation.
51    pub fn try_session_id(&self) -> Result<Option<SessionId>> {
52        self.session_id.as_deref().map(SessionId::try_from).transpose().map_err(Into::into)
53    }
54
55    /// Returns the stable session-scoped [`AdkIdentity`] triple, if a session
56    /// ID was provided.
57    ///
58    /// Because `CreateRequest` allows `session_id` to be `None` (the backend
59    /// generates one), this returns `Ok(None)` when no session ID is present.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if any of the constituent identifiers fail validation.
64    pub fn try_identity(&self) -> Result<Option<AdkIdentity>> {
65        let Some(sid) = self.try_session_id()? else {
66            return Ok(None);
67        };
68        Ok(Some(AdkIdentity {
69            app_name: self.try_app_name()?,
70            user_id: self.try_user_id()?,
71            session_id: sid,
72        }))
73    }
74}
75
76/// Request to retrieve an existing session.
77#[derive(Debug, Clone)]
78pub struct GetRequest {
79    /// Application name that owns the session.
80    pub app_name: String,
81    /// User identifier for the session owner.
82    pub user_id: String,
83    /// Session identifier to retrieve.
84    pub session_id: String,
85    /// If set, only return the N most recent events.
86    pub num_recent_events: Option<usize>,
87    /// If set, only return events after this timestamp.
88    pub after: Option<DateTime<Utc>>,
89}
90
91impl GetRequest {
92    /// Returns the stable session-scoped [`AdkIdentity`] triple.
93    ///
94    /// Parses `app_name`, `user_id`, and `session_id` into their typed
95    /// equivalents and combines them into an [`AdkIdentity`].
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if any of the three identifiers fail validation.
100    pub fn try_identity(&self) -> Result<AdkIdentity> {
101        Ok(AdkIdentity {
102            app_name: AppName::try_from(self.app_name.as_str())?,
103            user_id: UserId::try_from(self.user_id.as_str())?,
104            session_id: SessionId::try_from(self.session_id.as_str())?,
105        })
106    }
107}
108
109/// Builds the canonical error returned when a session identity has no matching record.
110pub(crate) fn session_not_found(req: &GetRequest) -> AdkError {
111    session_not_found_for(&req.app_name, &req.user_id, &req.session_id)
112}
113
114/// Builds the canonical not-found error for a raw `(app_name, user_id, session_id)` triple.
115pub(crate) fn session_not_found_for(app_name: &str, user_id: &str, session_id: &str) -> AdkError {
116    AdkError::not_found(
117        ErrorComponent::Session,
118        "session.not_found",
119        format!("session '{session_id}' was not found for app '{app_name}' and user '{user_id}'"),
120    )
121}
122
123/// Request to list sessions for a given app and user.
124#[derive(Debug, Clone)]
125pub struct ListRequest {
126    /// Application name to filter sessions by.
127    pub app_name: String,
128    /// User identifier to filter sessions by.
129    pub user_id: String,
130    /// Maximum number of sessions to return. `None` means no limit.
131    pub limit: Option<usize>,
132    /// Number of sessions to skip for pagination. `None` means start from the beginning.
133    pub offset: Option<usize>,
134}
135
136impl ListRequest {
137    /// Returns the application name as a typed [`AppName`].
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if the raw string fails identity validation.
142    pub fn try_app_name(&self) -> Result<AppName> {
143        Ok(AppName::try_from(self.app_name.as_str())?)
144    }
145
146    /// Returns the user identifier as a typed [`UserId`].
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if the raw string fails identity validation.
151    pub fn try_user_id(&self) -> Result<UserId> {
152        Ok(UserId::try_from(self.user_id.as_str())?)
153    }
154}
155
156/// Request to append an event to a session using typed [`AdkIdentity`] addressing.
157///
158/// This is the preferred way to append events in new code because it uses the
159/// full `(app_name, user_id, session_id)` triple, eliminating ambiguity that
160/// can arise when a bare `session_id` string is not globally unique.
161///
162/// # Example
163///
164/// ```rust
165/// use adk_core::identity::{AdkIdentity, AppName, SessionId, UserId};
166/// use adk_session::AppendEventRequest;
167/// use adk_session::Event;
168///
169/// let identity = AdkIdentity::new(
170///     AppName::try_from("weather-app").unwrap(),
171///     UserId::try_from("user-123").unwrap(),
172///     SessionId::try_from("session-456").unwrap(),
173/// );
174///
175/// let event = Event::new("inv-001");
176/// let req = AppendEventRequest { identity, event };
177/// ```
178#[derive(Debug, Clone)]
179pub struct AppendEventRequest {
180    /// The typed session-scoped identity triple.
181    pub identity: AdkIdentity,
182    /// The event to append.
183    pub event: Event,
184}
185
186/// Request to delete a session.
187#[derive(Debug, Clone)]
188pub struct DeleteRequest {
189    /// Application name that owns the session.
190    pub app_name: String,
191    /// User identifier for the session owner.
192    pub user_id: String,
193    /// Session identifier to delete.
194    pub session_id: String,
195}
196
197impl DeleteRequest {
198    /// Returns the stable session-scoped [`AdkIdentity`] triple.
199    ///
200    /// Parses `app_name`, `user_id`, and `session_id` into their typed
201    /// equivalents and combines them into an [`AdkIdentity`].
202    ///
203    /// # Errors
204    ///
205    /// Returns an error if any of the three identifiers fail validation.
206    pub fn try_identity(&self) -> Result<AdkIdentity> {
207        Ok(AdkIdentity {
208            app_name: AppName::try_from(self.app_name.as_str())?,
209            user_id: UserId::try_from(self.user_id.as_str())?,
210            session_id: SessionId::try_from(self.session_id.as_str())?,
211        })
212    }
213}
214
215/// Trait for session persistence backends.
216///
217/// Implementations manage the full lifecycle of sessions: creation, retrieval,
218/// listing, deletion, and event appending.
219#[async_trait]
220pub trait SessionService: Send + Sync {
221    /// Create a new session and return it.
222    async fn create(&self, req: CreateRequest) -> Result<Box<dyn Session>>;
223    /// Retrieves an existing session by its complete identity.
224    ///
225    /// Implementations address sessions by the full `(app_name, user_id, session_id)` tuple.
226    /// When that valid identity has no matching record, implementations return an
227    /// [`AdkError`] with code `session.not_found` and a
228    /// [`NotFound`](adk_core::ErrorCategory::NotFound) category. Backend and transport failures
229    /// must not be reported as not-found errors.
230    ///
231    /// # Errors
232    ///
233    /// Returns an invalid-input error when an identifier fails validation, `session.not_found`
234    /// when no matching session exists, or a backend-specific error when retrieval fails.
235    async fn get(&self, req: GetRequest) -> Result<Box<dyn Session>>;
236    /// List sessions for a given app and user.
237    async fn list(&self, req: ListRequest) -> Result<Vec<Box<dyn Session>>>;
238    /// Delete a session by its identifiers.
239    async fn delete(&self, req: DeleteRequest) -> Result<()>;
240    /// Append an event to a session identified by its session ID string.
241    async fn append_event(&self, session_id: &str, event: Event) -> Result<()>;
242
243    /// Get a session using typed [`AdkIdentity`] addressing.
244    ///
245    /// This is the preferred path for new code. It constructs a [`GetRequest`]
246    /// from the full `(app_name, user_id, session_id)` triple so that session
247    /// lookup is unambiguous.
248    ///
249    /// The default implementation delegates to
250    /// [`get`](SessionService::get) with a freshly built [`GetRequest`].
251    ///
252    /// # Errors
253    ///
254    /// Returns an error if the session cannot be retrieved.
255    async fn get_for_identity(&self, identity: &AdkIdentity) -> Result<Box<dyn Session>> {
256        self.get(GetRequest {
257            app_name: identity.app_name.as_ref().to_string(),
258            user_id: identity.user_id.as_ref().to_string(),
259            session_id: identity.session_id.as_ref().to_string(),
260            num_recent_events: None,
261            after: None,
262        })
263        .await
264    }
265
266    /// Delete a session using typed [`AdkIdentity`] addressing.
267    ///
268    /// This is the preferred path for new code. It constructs a
269    /// [`DeleteRequest`] from the full `(app_name, user_id, session_id)` triple
270    /// so that session deletion is unambiguous.
271    ///
272    /// The default implementation delegates to
273    /// [`delete`](SessionService::delete) with a freshly built
274    /// [`DeleteRequest`].
275    ///
276    /// # Errors
277    ///
278    /// Returns an error if the session cannot be deleted.
279    async fn delete_for_identity(&self, identity: &AdkIdentity) -> Result<()> {
280        self.delete(DeleteRequest {
281            app_name: identity.app_name.as_ref().to_string(),
282            user_id: identity.user_id.as_ref().to_string(),
283            session_id: identity.session_id.as_ref().to_string(),
284        })
285        .await
286    }
287
288    /// Append an event to a session using typed [`AdkIdentity`] addressing.
289    ///
290    /// This is the preferred path for new code. It uses the full
291    /// `(app_name, user_id, session_id)` triple so that session lookup is
292    /// unambiguous even when the same `session_id` string appears under
293    /// different apps or users.
294    ///
295    /// The default implementation delegates to the legacy
296    /// [`append_event`](SessionService::append_event) method using only the
297    /// `session_id` component. Backends that support composite-key addressing
298    /// should override this method to use all three identity fields.
299    ///
300    /// # Errors
301    ///
302    /// Returns an error if the event cannot be appended.
303    async fn append_event_for_identity(&self, req: AppendEventRequest) -> Result<()> {
304        self.append_event(req.identity.session_id.as_ref(), req.event).await
305    }
306
307    /// Delete all sessions for a given app and user.
308    ///
309    /// Removes all sessions and their associated events. Useful for
310    /// bulk cleanup and GDPR right-to-erasure compliance.
311    /// The default implementation returns an error.
312    async fn delete_all_sessions(&self, app_name: &str, user_id: &str) -> Result<()> {
313        let _ = (app_name, user_id);
314        Err(adk_core::AdkError::session("delete_all_sessions not implemented"))
315    }
316
317    /// Rewind a session to the specified event, removing all subsequent events
318    /// and rebuilding state from remaining events' state deltas.
319    ///
320    /// After rewinding, the session will contain only events up to and including
321    /// the target event, and the session state will reflect the cumulative
322    /// application of those events' state deltas.
323    ///
324    /// # Errors
325    ///
326    /// Returns an error if the backend does not support rewind, the session is
327    /// not found, or the target event ID does not exist in the session.
328    async fn rewind(&self, _session_id: &str, _target_event_id: &str) -> Result<Box<dyn Session>> {
329        Err(adk_core::AdkError::session("rewind not supported by this backend"))
330    }
331
332    /// Rewind a session by N steps from the end.
333    ///
334    /// If `steps` is 0, returns the session unchanged. If `steps` exceeds the
335    /// number of events, returns an error.
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if the backend does not support rewind, the session is
340    /// not found, or `steps` exceeds the event count.
341    async fn rewind_steps(&self, _session_id: &str, _steps: usize) -> Result<Box<dyn Session>> {
342        Err(adk_core::AdkError::session("rewind_steps not supported by this backend"))
343    }
344
345    /// Verify backend connectivity.
346    ///
347    /// Returns `Ok(())` if the backend is reachable and responsive.
348    /// Use this for Kubernetes readiness probes and `/healthz` endpoints.
349    /// The default implementation always succeeds (suitable for in-memory).
350    async fn health_check(&self) -> Result<()> {
351        Ok(())
352    }
353}