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