Skip to main content

bamboo_server/app_state/
session_loader.rs

1//! Unified session loading helpers on AppState.
2//!
3//! Consolidates the three session loading patterns previously duplicated across handlers:
4//!
5//! - **`load_session`** (strict): memory → storage, returns `Option`
6//! - **`load_or_create_session`**: memory → storage → create new
7//! - **`load_session_merged`**: merges memory + storage with `should_prefer_storage` heuristic
8//! - **`save_and_cache_session`**: dual write (persist + memory cache)
9//!
10//! Also provides the `SessionAccess` trait implementation for `AppState`,
11//! bridging the application-layer use cases to the server infrastructure.
12
13use super::*;
14
15use bamboo_agent_core::Session;
16use bamboo_engine::session_app::errors::{SessionLoadError, SessionSaveError};
17use bamboo_engine::session_app::repository::SessionAccess;
18
19// `SessionAccess` for `AppState` is a pure forward to the framework-owned
20// `session_repo` (the canonical `SessionAccess` impl lives on
21// `bamboo_engine::SessionRepository`). The coordination + error mapping is
22// defined once there, not duplicated here.
23#[async_trait::async_trait]
24impl SessionAccess for AppState {
25    async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
26        SessionAccess::load_session(&self.session_repo, id).await
27    }
28
29    async fn load_or_create(&self, id: &str, model: &str) -> Result<Session, SessionLoadError> {
30        SessionAccess::load_or_create(&self.session_repo, id, model).await
31    }
32
33    async fn load_merged(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
34        SessionAccess::load_merged(&self.session_repo, id).await
35    }
36
37    async fn save_session(&self, session: &mut Session) -> Result<(), SessionSaveError> {
38        SessionAccess::save_session(&self.session_repo, session).await
39    }
40
41    async fn save_and_cache(&self, session: &mut Session) -> Result<(), SessionSaveError> {
42        SessionAccess::save_and_cache(&self.session_repo, session).await
43    }
44}
45
46// The canonical load/save coordination now lives in
47// `bamboo_engine::SessionRepository`. These inherent methods are kept as thin
48// delegations so the ~hundreds of `state.load_session(...)` call sites stay
49// unchanged; new code can take a `&SessionRepository` directly.
50impl AppState {
51    /// Load a session from memory cache, falling back to persistent storage.
52    pub async fn load_session(&self, session_id: &str) -> Option<bamboo_agent_core::Session> {
53        self.session_repo.load(session_id).await
54    }
55
56    /// Load a session, creating a new one if it doesn't exist.
57    pub async fn load_or_create_session(
58        &self,
59        session_id: &str,
60        model: &str,
61    ) -> bamboo_agent_core::Session {
62        self.session_repo.load_or_create(session_id, model).await
63    }
64
65    /// Load a session, merging memory and storage using a preference heuristic.
66    pub async fn load_session_merged(
67        &self,
68        session_id: &str,
69    ) -> Option<bamboo_agent_core::Session> {
70        self.session_repo.load_merged(session_id).await
71    }
72
73    /// Persist session to storage and update the in-memory cache.
74    pub async fn save_and_cache_session(&self, session: &mut bamboo_agent_core::Session) {
75        self.session_repo.save_and_cache(session).await
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[tokio::test]
84    async fn load_session_returns_from_memory_first() {
85        let temp_dir = tempfile::tempdir().expect("temp dir");
86        let state = AppState::new(temp_dir.path().to_path_buf())
87            .await
88            .expect("app state");
89
90        let session_id = "session-memory-first";
91        let session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
92
93        // Seed memory cache.
94        state.sessions.insert(
95            session_id.to_string(),
96            Arc::new(parking_lot::RwLock::new(session.clone())),
97        );
98
99        let loaded = state.load_session(session_id).await;
100        assert!(loaded.is_some());
101        assert_eq!(loaded.unwrap().id, session_id);
102    }
103
104    #[tokio::test]
105    async fn load_session_falls_back_to_storage() {
106        let temp_dir = tempfile::tempdir().expect("temp dir");
107        let state = AppState::new(temp_dir.path().to_path_buf())
108            .await
109            .expect("app state");
110
111        let session_id = "session-storage-fallback";
112        let session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
113
114        // Seed storage only.
115        state
116            .storage
117            .save_session(&session)
118            .await
119            .expect("save session");
120
121        let loaded = state.load_session(session_id).await;
122        assert!(loaded.is_some());
123        assert_eq!(loaded.unwrap().id, session_id);
124    }
125
126    #[tokio::test]
127    async fn load_session_returns_none_when_missing() {
128        let temp_dir = tempfile::tempdir().expect("temp dir");
129        let state = AppState::new(temp_dir.path().to_path_buf())
130            .await
131            .expect("app state");
132
133        let loaded = state.load_session("nonexistent").await;
134        assert!(loaded.is_none());
135    }
136
137    #[tokio::test]
138    async fn load_or_create_creates_new_when_missing() {
139        let temp_dir = tempfile::tempdir().expect("temp dir");
140        let state = AppState::new(temp_dir.path().to_path_buf())
141            .await
142            .expect("app state");
143
144        let session = state.load_or_create_session("new-session", "gpt-4").await;
145        assert_eq!(session.id, "new-session");
146        assert_eq!(session.model, "gpt-4");
147    }
148
149    #[tokio::test]
150    async fn load_session_merged_prefers_storage_with_pending_question() {
151        let temp_dir = tempfile::tempdir().expect("temp dir");
152        let state = AppState::new(temp_dir.path().to_path_buf())
153            .await
154            .expect("app state");
155
156        let session_id = "session-merge-pending";
157        let memory_session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
158        let mut storage_session = memory_session.clone();
159        storage_session.set_pending_question(
160            "tool-call-1".to_string(),
161            "ConclusionWithOptions".to_string(),
162            "Need confirmation?".to_string(),
163            vec!["OK".to_string()],
164            true,
165        );
166
167        state.sessions.insert(
168            session_id.to_string(),
169            Arc::new(parking_lot::RwLock::new(memory_session)),
170        );
171        state
172            .storage
173            .save_session(&storage_session)
174            .await
175            .expect("save session");
176
177        let loaded = state.load_session_merged(session_id).await;
178        assert!(loaded.is_some());
179        assert!(loaded.unwrap().pending_question.is_some());
180    }
181
182    #[tokio::test]
183    async fn save_and_cache_session_writes_both() {
184        let temp_dir = tempfile::tempdir().expect("temp dir");
185        let state = AppState::new(temp_dir.path().to_path_buf())
186            .await
187            .expect("app state");
188
189        let session_id = "session-save-cache";
190        let mut session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
191        session.title = "test-title".to_string();
192
193        state.save_and_cache_session(&mut session).await;
194
195        // Verify memory cache.
196        let cached = { bamboo_engine::read_cached_session(&state.sessions, session_id) };
197        assert!(cached.is_some());
198        assert_eq!(cached.unwrap().title, "test-title");
199
200        // Verify storage.
201        let loaded = state.storage.load_session(session_id).await;
202        assert!(loaded.is_ok());
203        assert_eq!(loaded.unwrap().unwrap().title, "test-title");
204    }
205}