Skip to main content

adk_session/
service.rs

1use crate::{Event, Session};
2use adk_core::Result;
3use adk_core::identity::{AdkIdentity, AppName, SessionId, UserId};
4use async_trait::async_trait;
5use chrono::{DateTime, Utc};
6use serde_json::Value;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
10pub struct CreateRequest {
11    pub app_name: String,
12    pub user_id: String,
13    pub session_id: Option<String>,
14    pub state: HashMap<String, Value>,
15}
16
17impl CreateRequest {
18    /// Returns the application name as a typed [`AppName`].
19    ///
20    /// # Errors
21    ///
22    /// Returns an error if the raw string fails identity validation.
23    pub fn try_app_name(&self) -> Result<AppName> {
24        Ok(AppName::try_from(self.app_name.as_str())?)
25    }
26
27    /// Returns the user identifier as a typed [`UserId`].
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if the raw string fails identity validation.
32    pub fn try_user_id(&self) -> Result<UserId> {
33        Ok(UserId::try_from(self.user_id.as_str())?)
34    }
35
36    /// Returns the session identifier as a typed [`SessionId`], if one was
37    /// provided.
38    ///
39    /// Returns `Ok(None)` when `session_id` is `None` (the service will
40    /// generate one). Returns an error only when a non-`None` value fails
41    /// identity validation.
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if the provided session ID string fails validation.
46    pub fn try_session_id(&self) -> Result<Option<SessionId>> {
47        self.session_id.as_deref().map(SessionId::try_from).transpose().map_err(Into::into)
48    }
49
50    /// Returns the stable session-scoped [`AdkIdentity`] triple, if a session
51    /// ID was provided.
52    ///
53    /// Because `CreateRequest` allows `session_id` to be `None` (the backend
54    /// generates one), this returns `Ok(None)` when no session ID is present.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if any of the constituent identifiers fail validation.
59    pub fn try_identity(&self) -> Result<Option<AdkIdentity>> {
60        let Some(sid) = self.try_session_id()? else {
61            return Ok(None);
62        };
63        Ok(Some(AdkIdentity {
64            app_name: self.try_app_name()?,
65            user_id: self.try_user_id()?,
66            session_id: sid,
67        }))
68    }
69}
70
71#[derive(Debug, Clone)]
72pub struct GetRequest {
73    pub app_name: String,
74    pub user_id: String,
75    pub session_id: String,
76    pub num_recent_events: Option<usize>,
77    pub after: Option<DateTime<Utc>>,
78}
79
80impl GetRequest {
81    /// Returns the stable session-scoped [`AdkIdentity`] triple.
82    ///
83    /// Parses `app_name`, `user_id`, and `session_id` into their typed
84    /// equivalents and combines them into an [`AdkIdentity`].
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if any of the three identifiers fail validation.
89    pub fn try_identity(&self) -> Result<AdkIdentity> {
90        Ok(AdkIdentity {
91            app_name: AppName::try_from(self.app_name.as_str())?,
92            user_id: UserId::try_from(self.user_id.as_str())?,
93            session_id: SessionId::try_from(self.session_id.as_str())?,
94        })
95    }
96}
97
98#[derive(Debug, Clone)]
99pub struct ListRequest {
100    pub app_name: String,
101    pub user_id: String,
102    /// Maximum number of sessions to return. `None` means no limit.
103    pub limit: Option<usize>,
104    /// Number of sessions to skip for pagination. `None` means start from the beginning.
105    pub offset: Option<usize>,
106}
107
108impl ListRequest {
109    /// Returns the application name as a typed [`AppName`].
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the raw string fails identity validation.
114    pub fn try_app_name(&self) -> Result<AppName> {
115        Ok(AppName::try_from(self.app_name.as_str())?)
116    }
117
118    /// Returns the user identifier as a typed [`UserId`].
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if the raw string fails identity validation.
123    pub fn try_user_id(&self) -> Result<UserId> {
124        Ok(UserId::try_from(self.user_id.as_str())?)
125    }
126}
127
128/// Request to append an event to a session using typed [`AdkIdentity`] addressing.
129///
130/// This is the preferred way to append events in new code because it uses the
131/// full `(app_name, user_id, session_id)` triple, eliminating ambiguity that
132/// can arise when a bare `session_id` string is not globally unique.
133///
134/// # Example
135///
136/// ```rust
137/// use adk_core::identity::{AdkIdentity, AppName, SessionId, UserId};
138/// use adk_session::AppendEventRequest;
139/// use adk_session::Event;
140///
141/// let identity = AdkIdentity::new(
142///     AppName::try_from("weather-app").unwrap(),
143///     UserId::try_from("user-123").unwrap(),
144///     SessionId::try_from("session-456").unwrap(),
145/// );
146///
147/// let event = Event::new("inv-001");
148/// let req = AppendEventRequest { identity, event };
149/// ```
150#[derive(Debug, Clone)]
151pub struct AppendEventRequest {
152    /// The typed session-scoped identity triple.
153    pub identity: AdkIdentity,
154    /// The event to append.
155    pub event: Event,
156}
157
158#[derive(Debug, Clone)]
159pub struct DeleteRequest {
160    pub app_name: String,
161    pub user_id: String,
162    pub session_id: String,
163}
164
165impl DeleteRequest {
166    /// Returns the stable session-scoped [`AdkIdentity`] triple.
167    ///
168    /// Parses `app_name`, `user_id`, and `session_id` into their typed
169    /// equivalents and combines them into an [`AdkIdentity`].
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if any of the three identifiers fail validation.
174    pub fn try_identity(&self) -> Result<AdkIdentity> {
175        Ok(AdkIdentity {
176            app_name: AppName::try_from(self.app_name.as_str())?,
177            user_id: UserId::try_from(self.user_id.as_str())?,
178            session_id: SessionId::try_from(self.session_id.as_str())?,
179        })
180    }
181}
182
183#[async_trait]
184pub trait SessionService: Send + Sync {
185    async fn create(&self, req: CreateRequest) -> Result<Box<dyn Session>>;
186    async fn get(&self, req: GetRequest) -> Result<Box<dyn Session>>;
187    async fn list(&self, req: ListRequest) -> Result<Vec<Box<dyn Session>>>;
188    async fn delete(&self, req: DeleteRequest) -> Result<()>;
189    async fn append_event(&self, session_id: &str, event: Event) -> Result<()>;
190
191    /// Get a session using typed [`AdkIdentity`] addressing.
192    ///
193    /// This is the preferred path for new code. It constructs a [`GetRequest`]
194    /// from the full `(app_name, user_id, session_id)` triple so that session
195    /// lookup is unambiguous.
196    ///
197    /// The default implementation delegates to
198    /// [`get`](SessionService::get) with a freshly built [`GetRequest`].
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if the session cannot be retrieved.
203    async fn get_for_identity(&self, identity: &AdkIdentity) -> Result<Box<dyn Session>> {
204        self.get(GetRequest {
205            app_name: identity.app_name.as_ref().to_string(),
206            user_id: identity.user_id.as_ref().to_string(),
207            session_id: identity.session_id.as_ref().to_string(),
208            num_recent_events: None,
209            after: None,
210        })
211        .await
212    }
213
214    /// Delete a session using typed [`AdkIdentity`] addressing.
215    ///
216    /// This is the preferred path for new code. It constructs a
217    /// [`DeleteRequest`] from the full `(app_name, user_id, session_id)` triple
218    /// so that session deletion is unambiguous.
219    ///
220    /// The default implementation delegates to
221    /// [`delete`](SessionService::delete) with a freshly built
222    /// [`DeleteRequest`].
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if the session cannot be deleted.
227    async fn delete_for_identity(&self, identity: &AdkIdentity) -> Result<()> {
228        self.delete(DeleteRequest {
229            app_name: identity.app_name.as_ref().to_string(),
230            user_id: identity.user_id.as_ref().to_string(),
231            session_id: identity.session_id.as_ref().to_string(),
232        })
233        .await
234    }
235
236    /// Append an event to a session using typed [`AdkIdentity`] addressing.
237    ///
238    /// This is the preferred path for new code. It uses the full
239    /// `(app_name, user_id, session_id)` triple so that session lookup is
240    /// unambiguous even when the same `session_id` string appears under
241    /// different apps or users.
242    ///
243    /// The default implementation delegates to the legacy
244    /// [`append_event`](SessionService::append_event) method using only the
245    /// `session_id` component. Backends that support composite-key addressing
246    /// should override this method to use all three identity fields.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the event cannot be appended.
251    async fn append_event_for_identity(&self, req: AppendEventRequest) -> Result<()> {
252        self.append_event(req.identity.session_id.as_ref(), req.event).await
253    }
254
255    /// Delete all sessions for a given app and user.
256    ///
257    /// Removes all sessions and their associated events. Useful for
258    /// bulk cleanup and GDPR right-to-erasure compliance.
259    /// The default implementation returns an error.
260    async fn delete_all_sessions(&self, app_name: &str, user_id: &str) -> Result<()> {
261        let _ = (app_name, user_id);
262        Err(adk_core::AdkError::Session("delete_all_sessions not implemented".into()))
263    }
264
265    /// Verify backend connectivity.
266    ///
267    /// Returns `Ok(())` if the backend is reachable and responsive.
268    /// Use this for Kubernetes readiness probes and `/healthz` endpoints.
269    /// The default implementation always succeeds (suitable for in-memory).
270    async fn health_check(&self) -> Result<()> {
271        Ok(())
272    }
273}