adk_managed/
state_store.rs1use serde::{Deserialize, Serialize};
28
29use crate::checkpoint::RunState;
30use crate::types::{RuntimeError, SessionEvent};
31use async_trait::async_trait;
32use std::collections::HashMap;
33use std::sync::Arc;
34use tokio::sync::RwLock;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Durability {
39 ProcessLocal,
42 CrashDurable,
45}
46
47impl Durability {
48 pub fn survives_process_loss(&self) -> bool {
53 matches!(self, Durability::CrashDurable)
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ManagedSessionState {
63 pub events: Vec<SessionEvent>,
65 pub run_state: RunState,
67}
68
69#[async_trait]
75pub trait ManagedStateStore: Send + Sync + std::fmt::Debug {
76 fn durability(&self) -> Durability;
78
79 async fn save(&self, session_id: &str, state: ManagedSessionState) -> Result<(), RuntimeError>;
87
88 async fn load(&self, session_id: &str) -> Result<Option<ManagedSessionState>, RuntimeError>;
94
95 async fn delete(&self, session_id: &str) -> Result<(), RuntimeError>;
103
104 async fn session_ids(&self) -> Result<Vec<String>, RuntimeError>;
113}
114
115#[derive(Debug, Default)]
120pub struct InMemoryManagedStateStore {
121 sessions: Arc<RwLock<HashMap<String, ManagedSessionState>>>,
122}
123
124impl InMemoryManagedStateStore {
125 pub fn new() -> Self {
127 Self::default()
128 }
129}
130
131#[async_trait]
132impl ManagedStateStore for InMemoryManagedStateStore {
133 fn durability(&self) -> Durability {
134 Durability::ProcessLocal
135 }
136
137 async fn save(&self, session_id: &str, state: ManagedSessionState) -> Result<(), RuntimeError> {
138 self.sessions.write().await.insert(session_id.to_string(), state);
139 Ok(())
140 }
141
142 async fn load(&self, session_id: &str) -> Result<Option<ManagedSessionState>, RuntimeError> {
143 Ok(self.sessions.read().await.get(session_id).cloned())
144 }
145
146 async fn delete(&self, session_id: &str) -> Result<(), RuntimeError> {
147 self.sessions.write().await.remove(session_id);
148 Ok(())
149 }
150
151 async fn session_ids(&self) -> Result<Vec<String>, RuntimeError> {
152 Ok(self.sessions.read().await.keys().cloned().collect())
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::checkpoint::RunState;
160 use crate::types::SessionStatus;
161
162 fn state() -> ManagedSessionState {
163 ManagedSessionState {
164 events: Vec::new(),
165 run_state: RunState {
166 seq: 7,
167 pending_tool_ids: vec!["call-1".to_string()],
168 status: SessionStatus::Running,
169 },
170 }
171 }
172
173 #[tokio::test]
174 async fn the_in_memory_store_reports_its_own_guarantee() {
175 let store = InMemoryManagedStateStore::new();
176 assert_eq!(store.durability(), Durability::ProcessLocal);
177 assert!(
178 !store.durability().survives_process_loss(),
179 "a caller requiring resume-after-restart must be able to detect that it is absent"
180 );
181 }
182
183 #[tokio::test]
184 async fn a_saved_snapshot_round_trips() {
185 let store = InMemoryManagedStateStore::new();
186 store.save("session-1", state()).await.unwrap();
187
188 let loaded = store.load("session-1").await.unwrap().expect("saved state must load");
189 assert_eq!(loaded.run_state, state().run_state);
190 assert_eq!(loaded.events.len(), state().events.len());
191 assert_eq!(store.session_ids().await.unwrap(), vec!["session-1".to_string()]);
192 }
193
194 #[tokio::test]
195 async fn an_unknown_session_loads_as_none_and_deletes_without_error() {
196 let store = InMemoryManagedStateStore::new();
197 assert!(store.load("missing").await.unwrap().is_none());
198 assert!(store.delete("missing").await.is_ok(), "deletion is idempotent");
199 }
200
201 #[tokio::test]
202 async fn saving_twice_replaces_the_snapshot() {
203 let store = InMemoryManagedStateStore::new();
204 store.save("session-1", state()).await.unwrap();
205
206 let mut later = state();
207 later.run_state.seq = 9;
208 store.save("session-1", later.clone()).await.unwrap();
209
210 let loaded = store.load("session-1").await.unwrap().expect("state must load");
211 assert_eq!(loaded.run_state.seq, later.run_state.seq);
212 assert_eq!(store.session_ids().await.unwrap().len(), 1, "not appended twice");
213 }
214
215 #[tokio::test]
216 async fn a_new_store_shares_nothing_with_the_old_one() {
217 let first = InMemoryManagedStateStore::new();
221 first.save("session-1", state()).await.unwrap();
222
223 let second = InMemoryManagedStateStore::new();
224 assert!(
225 second.load("session-1").await.unwrap().is_none(),
226 "process-local state does not cross process boundaries"
227 );
228 assert!(second.session_ids().await.unwrap().is_empty());
229 }
230}
231
232#[derive(Debug)]
251pub struct FileManagedStateStore {
252 root: std::path::PathBuf,
253}
254
255impl FileManagedStateStore {
256 pub fn new(root: impl Into<std::path::PathBuf>) -> Self {
258 Self { root: root.into() }
259 }
260
261 fn path_for(&self, session_id: &str) -> std::path::PathBuf {
266 let safe: String = session_id
267 .chars()
268 .map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' })
269 .collect();
270 self.root.join(format!("{safe}.json"))
271 }
272
273 fn failed(action: &str, error: impl std::fmt::Display) -> RuntimeError {
274 RuntimeError::CheckpointFailed {
275 message: format!("could not {action} session state: {error}"),
276 }
277 }
278}
279
280#[async_trait::async_trait]
281impl ManagedStateStore for FileManagedStateStore {
282 fn durability(&self) -> Durability {
283 Durability::CrashDurable
284 }
285
286 async fn save(&self, session_id: &str, state: ManagedSessionState) -> Result<(), RuntimeError> {
287 use std::io::Write;
291
292 let path = self.path_for(session_id);
293 let text = serde_json::to_vec_pretty(&state).map_err(|e| Self::failed("encode", e))?;
294 std::fs::create_dir_all(&self.root)
295 .map_err(|e| Self::failed("create the directory for", e))?;
296
297 let temporary = path.with_extension("json.tmp");
301 let mut file = std::fs::File::create(&temporary)
302 .map_err(|e| Self::failed("open a temporary file for", e))?;
303 file.write_all(&text).map_err(|e| Self::failed("write", e))?;
304 file.sync_all().map_err(|e| Self::failed("sync", e))?;
305 drop(file);
306 std::fs::rename(&temporary, &path).map_err(|e| Self::failed("commit", e))
307 }
308
309 async fn load(&self, session_id: &str) -> Result<Option<ManagedSessionState>, RuntimeError> {
310 match std::fs::read(self.path_for(session_id)) {
311 Ok(bytes) => {
312 serde_json::from_slice(&bytes).map(Some).map_err(|e| Self::failed("decode", e))
313 }
314 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
315 Err(error) => Err(Self::failed("read", error)),
316 }
317 }
318
319 async fn delete(&self, session_id: &str) -> Result<(), RuntimeError> {
320 match std::fs::remove_file(self.path_for(session_id)) {
321 Ok(()) => Ok(()),
322 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
323 Err(error) => Err(Self::failed("delete", error)),
324 }
325 }
326
327 async fn session_ids(&self) -> Result<Vec<String>, RuntimeError> {
328 let entries = match std::fs::read_dir(&self.root) {
329 Ok(entries) => entries,
330 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
331 Err(error) => return Err(Self::failed("list", error)),
332 };
333 let mut ids = Vec::new();
334 for entry in entries {
335 let entry = entry.map_err(|e| Self::failed("list", e))?;
336 let name = entry.file_name().to_string_lossy().to_string();
337 if let Some(id) = name.strip_suffix(".json") {
338 ids.push(id.to_string());
339 }
340 }
341 ids.sort();
342 Ok(ids)
343 }
344}