Skip to main content

rs_adk/session/
mod.rs

1//! Session persistence — multi-session, multi-turn CRUD.
2//!
3//! Mirrors ADK-JS's `BaseSessionService`. Provides a trait for session
4//! persistence with an in-memory default implementation.
5
6mod memory;
7#[cfg(feature = "postgres-sessions")]
8mod postgres;
9mod sqlite;
10mod types;
11mod vertex_ai;
12
13#[cfg(feature = "database-sessions")]
14mod database;
15#[cfg(feature = "database-sessions")]
16pub use database::DatabaseSessionService;
17
18pub mod db_schema;
19
20pub use memory::InMemorySessionService;
21#[cfg(feature = "postgres-sessions")]
22pub use postgres::{PostgresSessionConfig, PostgresSessionService};
23pub use sqlite::{SqliteSessionConfig, SqliteSessionService};
24pub use types::{Session, SessionId};
25pub use vertex_ai::{VertexAiSessionConfig, VertexAiSessionService};
26
27use async_trait::async_trait;
28
29use crate::events::Event;
30
31/// Errors from session service operations.
32#[derive(Debug, thiserror::Error)]
33pub enum SessionError {
34    /// The session with the given ID was not found.
35    #[error("Session not found: {0}")]
36    NotFound(SessionId),
37    /// A storage backend error.
38    #[error("Storage error: {0}")]
39    Storage(String),
40}
41
42/// Trait for session persistence — CRUD operations + event append.
43///
44/// Implementations must be `Send + Sync` for use across async tasks.
45#[async_trait]
46pub trait SessionService: Send + Sync {
47    /// Create a new session.
48    async fn create_session(&self, app_name: &str, user_id: &str) -> Result<Session, SessionError>;
49
50    /// Get a session by ID.
51    async fn get_session(&self, id: &SessionId) -> Result<Option<Session>, SessionError>;
52
53    /// List sessions for an app + user.
54    async fn list_sessions(
55        &self,
56        app_name: &str,
57        user_id: &str,
58    ) -> Result<Vec<Session>, SessionError>;
59
60    /// Delete a session.
61    async fn delete_session(&self, id: &SessionId) -> Result<(), SessionError>;
62
63    /// Append an event to a session's history.
64    async fn append_event(&self, id: &SessionId, event: Event) -> Result<(), SessionError>;
65
66    /// Get all events for a session.
67    async fn get_events(&self, id: &SessionId) -> Result<Vec<Event>, SessionError>;
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[tokio::test]
75    async fn create_and_get_session() {
76        let svc = InMemorySessionService::new();
77        let session = svc.create_session("my-app", "user-1").await.unwrap();
78        assert_eq!(session.app_name, "my-app");
79        assert_eq!(session.user_id, "user-1");
80
81        let fetched = svc.get_session(&session.id).await.unwrap();
82        assert!(fetched.is_some());
83        assert_eq!(fetched.unwrap().id, session.id);
84    }
85
86    #[tokio::test]
87    async fn list_sessions_filters_by_app_and_user() {
88        let svc = InMemorySessionService::new();
89        svc.create_session("app-a", "user-1").await.unwrap();
90        svc.create_session("app-a", "user-1").await.unwrap();
91        svc.create_session("app-a", "user-2").await.unwrap();
92        svc.create_session("app-b", "user-1").await.unwrap();
93
94        let list = svc.list_sessions("app-a", "user-1").await.unwrap();
95        assert_eq!(list.len(), 2);
96    }
97
98    #[tokio::test]
99    async fn delete_session_removes_it() {
100        let svc = InMemorySessionService::new();
101        let session = svc.create_session("app", "user").await.unwrap();
102        svc.delete_session(&session.id).await.unwrap();
103        let fetched = svc.get_session(&session.id).await.unwrap();
104        assert!(fetched.is_none());
105    }
106
107    #[tokio::test]
108    async fn append_and_get_events() {
109        let svc = InMemorySessionService::new();
110        let session = svc.create_session("app", "user").await.unwrap();
111
112        let event = Event::new("user", Some("Hello!".to_string()));
113        svc.append_event(&session.id, event).await.unwrap();
114
115        let events = svc.get_events(&session.id).await.unwrap();
116        assert_eq!(events.len(), 1);
117        assert_eq!(events[0].author, "user");
118    }
119
120    #[tokio::test]
121    async fn append_event_to_nonexistent_session() {
122        let svc = InMemorySessionService::new();
123        let id = SessionId::new();
124        let event = Event::new("user", Some("Hello".to_string()));
125        let result = svc.append_event(&id, event).await;
126        assert!(result.is_err());
127    }
128
129    #[tokio::test]
130    async fn session_service_is_object_safe() {
131        fn _assert(_: &dyn SessionService) {}
132    }
133}
134
135#[cfg(test)]
136mod schema_tests {
137    use super::db_schema;
138
139    #[test]
140    fn postgres_schema_has_tables() {
141        assert!(db_schema::POSTGRES_SCHEMA.contains("CREATE TABLE IF NOT EXISTS sessions"));
142        assert!(db_schema::POSTGRES_SCHEMA.contains("CREATE TABLE IF NOT EXISTS events"));
143    }
144
145    #[test]
146    fn sqlite_schema_has_tables() {
147        assert!(db_schema::SQLITE_SCHEMA.contains("CREATE TABLE IF NOT EXISTS sessions"));
148        assert!(db_schema::SQLITE_SCHEMA.contains("CREATE TABLE IF NOT EXISTS events"));
149    }
150
151    #[test]
152    fn postgres_schema_has_indexes() {
153        assert!(db_schema::POSTGRES_SCHEMA.contains("idx_events_session"));
154        assert!(db_schema::POSTGRES_SCHEMA.contains("idx_sessions_app_user"));
155    }
156
157    #[test]
158    fn sqlite_schema_has_indexes() {
159        assert!(db_schema::SQLITE_SCHEMA.contains("idx_events_session"));
160        assert!(db_schema::SQLITE_SCHEMA.contains("idx_sessions_app_user"));
161    }
162
163    #[test]
164    fn postgres_schema_uses_jsonb() {
165        assert!(db_schema::POSTGRES_SCHEMA.contains("JSONB"));
166    }
167
168    #[test]
169    fn sqlite_schema_uses_text_for_json() {
170        // SQLite doesn't have JSONB, so JSON columns use TEXT
171        assert!(!db_schema::SQLITE_SCHEMA.contains("JSONB"));
172    }
173}