bamboo_server/app_state/
session_loader.rs1use super::*;
14
15use bamboo_agent_core::Session;
16use bamboo_engine::session_app::errors::{RespondError, SessionLoadError, SessionSaveError};
17use bamboo_engine::session_app::repository::SessionAccess;
18
19#[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 async fn inspect_for_response(&self, id: &str) -> Result<Option<Session>, SessionLoadError> {
46 SessionAccess::inspect_for_response(&self.session_repo, id).await
47 }
48
49 async fn mutate_for_response(
50 &self,
51 id: &str,
52 mutate: Box<
53 dyn for<'session> FnOnce(&'session mut Session) -> Result<(), RespondError>
54 + Send
55 + 'static,
56 >,
57 ) -> Result<Option<Session>, RespondError> {
58 SessionAccess::mutate_for_response(&self.session_repo, id, mutate).await
59 }
60}
61
62impl AppState {
67 pub async fn load_session(&self, session_id: &str) -> Option<bamboo_agent_core::Session> {
69 self.session_repo.load(session_id).await
70 }
71
72 pub async fn load_or_create_session(
74 &self,
75 session_id: &str,
76 model: &str,
77 ) -> bamboo_agent_core::Session {
78 self.session_repo.load_or_create(session_id, model).await
79 }
80
81 pub async fn load_session_merged(
83 &self,
84 session_id: &str,
85 ) -> Option<bamboo_agent_core::Session> {
86 self.session_repo.load_merged(session_id).await
87 }
88
89 pub async fn save_and_cache_session(&self, session: &mut bamboo_agent_core::Session) {
91 self.session_repo.save_and_cache(session).await
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[tokio::test]
100 async fn load_session_returns_from_memory_first() {
101 let temp_dir = tempfile::tempdir().expect("temp dir");
102 let state = AppState::new(temp_dir.path().to_path_buf())
103 .await
104 .expect("app state");
105
106 let session_id = "session-memory-first";
107 let session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
108
109 state.sessions.insert(
111 session_id.to_string(),
112 Arc::new(bamboo_engine::SessionSnapshot::new(session.clone())),
113 );
114
115 let loaded = state.load_session(session_id).await;
116 assert!(loaded.is_some());
117 assert_eq!(loaded.unwrap().id, session_id);
118 }
119
120 #[tokio::test]
121 async fn load_session_falls_back_to_storage() {
122 let temp_dir = tempfile::tempdir().expect("temp dir");
123 let state = AppState::new(temp_dir.path().to_path_buf())
124 .await
125 .expect("app state");
126
127 let session_id = "session-storage-fallback";
128 let session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
129
130 state
132 .storage
133 .save_session(&session)
134 .await
135 .expect("save session");
136
137 let loaded = state.load_session(session_id).await;
138 assert!(loaded.is_some());
139 assert_eq!(loaded.unwrap().id, session_id);
140 }
141
142 #[tokio::test]
143 async fn load_session_returns_none_when_missing() {
144 let temp_dir = tempfile::tempdir().expect("temp dir");
145 let state = AppState::new(temp_dir.path().to_path_buf())
146 .await
147 .expect("app state");
148
149 let loaded = state.load_session("nonexistent").await;
150 assert!(loaded.is_none());
151 }
152
153 #[tokio::test]
154 async fn load_or_create_creates_new_when_missing() {
155 let temp_dir = tempfile::tempdir().expect("temp dir");
156 let state = AppState::new(temp_dir.path().to_path_buf())
157 .await
158 .expect("app state");
159
160 let session = state.load_or_create_session("new-session", "gpt-4").await;
161 assert_eq!(session.id, "new-session");
162 assert_eq!(session.model, "gpt-4");
163 }
164
165 #[tokio::test]
166 async fn load_session_merged_prefers_storage_with_pending_question() {
167 let temp_dir = tempfile::tempdir().expect("temp dir");
168 let state = AppState::new(temp_dir.path().to_path_buf())
169 .await
170 .expect("app state");
171
172 let session_id = "session-merge-pending";
173 let memory_session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
174 let mut storage_session = memory_session.clone();
175 storage_session.set_pending_question(
176 "tool-call-1".to_string(),
177 "ConclusionWithOptions".to_string(),
178 "Need confirmation?".to_string(),
179 vec!["OK".to_string()],
180 true,
181 );
182
183 state.sessions.insert(
184 session_id.to_string(),
185 Arc::new(bamboo_engine::SessionSnapshot::new(memory_session)),
186 );
187 state
188 .storage
189 .save_session(&storage_session)
190 .await
191 .expect("save session");
192
193 let loaded = state.load_session_merged(session_id).await;
194 assert!(loaded.is_some());
195 assert!(loaded.unwrap().pending_question.is_some());
196 }
197
198 #[tokio::test]
199 async fn save_and_cache_session_writes_both() {
200 let temp_dir = tempfile::tempdir().expect("temp dir");
201 let state = AppState::new(temp_dir.path().to_path_buf())
202 .await
203 .expect("app state");
204
205 let session_id = "session-save-cache";
206 let mut session = bamboo_agent_core::Session::new(session_id.to_string(), "test-model");
207 session.title = "test-title".to_string();
208
209 state.save_and_cache_session(&mut session).await;
210
211 let cached = { bamboo_engine::read_cached_session(&state.sessions, session_id) };
213 assert!(cached.is_some());
214 assert_eq!(cached.unwrap().title, "test-title");
215
216 let loaded = state.storage.load_session(session_id).await;
218 assert!(loaded.is_ok());
219 assert_eq!(loaded.unwrap().unwrap().title, "test-title");
220 }
221}