1use crate::errors::{GraphError, GraphResult};
5use crate::state::StateSchema;
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use tokio::sync::Mutex;
11use uuid::Uuid;
12
13#[async_trait]
15pub trait Checkpointer<S: StateSchema>: Send + Sync {
16 async fn save(&self, state: &S, recursion_count: usize) -> GraphResult<String>;
19 async fn load(&self, checkpoint_id: &str) -> GraphResult<S>;
21 async fn list(&self) -> GraphResult<Vec<String>>;
23 async fn delete(&self, checkpoint_id: &str) -> GraphResult<()>;
25 async fn last(&self) -> GraphResult<Option<(S, usize)>>;
27
28 async fn snapshots(&self) -> GraphResult<Vec<CheckpointInfo<S>>> {
38 let mut snaps = Vec::new();
39 for id in self.list().await? {
40 let state = self.load(&id).await?;
41 snaps.push(CheckpointInfo {
42 id,
43 timestamp: 0,
44 seq: 0,
45 recursion_count: 0,
46 state,
47 });
48 }
49 Ok(snaps)
50 }
51
52 async fn update_state(
65 &self,
66 checkpoint_id: &str,
67 state: &S,
68 expected_version: u64,
69 ) -> GraphResult<u64> {
70 let _ = (state, expected_version);
71 Err(GraphError::CheckpointError(format!(
72 "update_state is not supported on checkpoint '{checkpoint_id}' by this checkpointer",
73 )))
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(bound = "S: StateSchema")]
80pub struct CheckpointData<S: StateSchema> {
81 pub id: String,
83 pub state: S,
85 pub timestamp: i64,
87 pub metadata: HashMap<String, serde_json::Value>,
89 #[serde(default)]
93 pub seq: u64,
94 #[serde(default)]
98 pub recursion_count: usize,
99 #[serde(default = "initial_version")]
103 pub version: u64,
104}
105
106fn initial_version() -> u64 {
108 1
109}
110
111#[derive(Debug, Clone)]
115pub struct CheckpointInfo<S: StateSchema> {
116 pub id: String,
118 pub timestamp: i64,
120 pub seq: u64,
122 pub recursion_count: usize,
125 pub state: S,
127}
128
129impl<S: StateSchema> CheckpointData<S> {
130 pub fn new(state: S) -> Self {
132 Self {
133 id: Uuid::new_v4().to_string(),
134 state,
135 timestamp: chrono::Utc::now().timestamp(),
136 metadata: HashMap::new(),
137 seq: 0,
138 recursion_count: 0,
139 version: 1,
140 }
141 }
142
143 pub fn with_progress(state: S, seq: u64, recursion_count: usize) -> Self {
146 let mut data = Self::new(state);
147 data.seq = seq;
148 data.recursion_count = recursion_count;
149 data
150 }
151}
152
153pub struct MemoryCheckpointer<S: StateSchema> {
155 checkpoints: Mutex<HashMap<String, CheckpointData<S>>>,
156 next_seq: AtomicU64,
157}
158
159impl<S: StateSchema> MemoryCheckpointer<S> {
160 pub fn new() -> Self {
162 Self {
163 checkpoints: Mutex::new(HashMap::new()),
164 next_seq: AtomicU64::new(0),
165 }
166 }
167}
168
169impl<S: StateSchema> Default for MemoryCheckpointer<S> {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175#[async_trait]
176impl<S: StateSchema> Checkpointer<S> for MemoryCheckpointer<S> {
177 async fn save(&self, state: &S, recursion_count: usize) -> GraphResult<String> {
178 let seq = self.next_seq.fetch_add(1, Ordering::SeqCst);
179 let data = CheckpointData::with_progress(state.clone(), seq, recursion_count);
180 let id = data.id.clone();
181 self.checkpoints.lock().await.insert(id.clone(), data);
182 Ok(id)
183 }
184
185 async fn load(&self, checkpoint_id: &str) -> GraphResult<S> {
186 self.checkpoints
187 .lock()
188 .await
189 .get(checkpoint_id)
190 .map(|d| d.state.clone())
191 .ok_or_else(|| {
192 GraphError::CheckpointError(format!("Checkpoint '{}' not found", checkpoint_id))
193 })
194 }
195
196 async fn list(&self) -> GraphResult<Vec<String>> {
197 let guard = self.checkpoints.lock().await;
198 let mut items: Vec<(i64, u64, String)> = guard
199 .values()
200 .map(|d| (d.timestamp, d.seq, d.id.clone()))
201 .collect();
202 items.sort();
205 Ok(items.into_iter().map(|(_, _, id)| id).collect())
206 }
207
208 async fn last(&self) -> GraphResult<Option<(S, usize)>> {
209 let guard = self.checkpoints.lock().await;
210 Ok(guard
211 .values()
212 .max_by_key(|d| (d.timestamp, d.seq))
213 .map(|d| (d.state.clone(), d.recursion_count)))
214 }
215
216 async fn snapshots(&self) -> GraphResult<Vec<CheckpointInfo<S>>> {
217 let guard = self.checkpoints.lock().await;
218 let mut items: Vec<&CheckpointData<S>> = guard.values().collect();
219 items.sort_by_key(|d| (d.timestamp, d.seq));
220 Ok(items.into_iter().map(checkpoint_info_from_data).collect())
221 }
222
223 async fn update_state(
224 &self,
225 checkpoint_id: &str,
226 state: &S,
227 expected_version: u64,
228 ) -> GraphResult<u64> {
229 update_locked(&self.checkpoints, checkpoint_id, state, expected_version).await
230 }
231
232 async fn delete(&self, checkpoint_id: &str) -> GraphResult<()> {
233 self.checkpoints.lock().await.remove(checkpoint_id);
234 Ok(())
235 }
236}
237
238fn checkpoint_info_from_data<S: StateSchema>(d: &CheckpointData<S>) -> CheckpointInfo<S> {
241 CheckpointInfo {
242 id: d.id.clone(),
243 timestamp: d.timestamp,
244 seq: d.seq,
245 recursion_count: d.recursion_count,
246 state: d.state.clone(),
247 }
248}
249
250async fn update_locked<S: StateSchema>(
255 checkpoints: &Mutex<HashMap<String, CheckpointData<S>>>,
256 checkpoint_id: &str,
257 state: &S,
258 expected_version: u64,
259) -> GraphResult<u64> {
260 let mut guard = checkpoints.lock().await;
261 let data = guard.get_mut(checkpoint_id).ok_or_else(|| {
262 GraphError::CheckpointError(format!("Checkpoint '{checkpoint_id}' not found"))
263 })?;
264 if data.version != expected_version {
265 return Err(GraphError::CheckpointVersionConflict {
266 checkpoint_id: checkpoint_id.to_string(),
267 expected: expected_version,
268 actual: data.version,
269 });
270 }
271 data.state = state.clone();
272 data.version += 1;
273 Ok(data.version)
274}
275
276pub struct ThreadSafeMemoryCheckpointer<S: StateSchema> {
278 checkpoints: Mutex<HashMap<String, CheckpointData<S>>>,
279 next_seq: AtomicU64,
280}
281
282impl<S: StateSchema> ThreadSafeMemoryCheckpointer<S> {
283 pub fn new() -> Self {
285 Self {
286 checkpoints: Mutex::new(HashMap::new()),
287 next_seq: AtomicU64::new(0),
288 }
289 }
290}
291
292impl<S: StateSchema> Default for ThreadSafeMemoryCheckpointer<S> {
293 fn default() -> Self {
294 Self::new()
295 }
296}
297
298#[async_trait]
299impl<S: StateSchema> Checkpointer<S> for ThreadSafeMemoryCheckpointer<S> {
300 async fn save(&self, state: &S, recursion_count: usize) -> GraphResult<String> {
301 let seq = self.next_seq.fetch_add(1, Ordering::SeqCst);
302 let data = CheckpointData::with_progress(state.clone(), seq, recursion_count);
303 let id = data.id.clone();
304 self.checkpoints.lock().await.insert(id.clone(), data);
305 Ok(id)
306 }
307
308 async fn load(&self, checkpoint_id: &str) -> GraphResult<S> {
309 let checkpoints = self.checkpoints.lock().await;
310 checkpoints
311 .get(checkpoint_id)
312 .map(|d| d.state.clone())
313 .ok_or_else(|| {
314 GraphError::CheckpointError(format!("Checkpoint '{}' not found", checkpoint_id))
315 })
316 }
317
318 async fn list(&self) -> GraphResult<Vec<String>> {
319 let guard = self.checkpoints.lock().await;
320 let mut items: Vec<(i64, u64, String)> = guard
321 .values()
322 .map(|d| (d.timestamp, d.seq, d.id.clone()))
323 .collect();
324 items.sort();
326 Ok(items.into_iter().map(|(_, _, id)| id).collect())
327 }
328
329 async fn last(&self) -> GraphResult<Option<(S, usize)>> {
330 let guard = self.checkpoints.lock().await;
331 Ok(guard
332 .values()
333 .max_by_key(|d| (d.timestamp, d.seq))
334 .map(|d| (d.state.clone(), d.recursion_count)))
335 }
336
337 async fn snapshots(&self) -> GraphResult<Vec<CheckpointInfo<S>>> {
338 let guard = self.checkpoints.lock().await;
339 let mut items: Vec<&CheckpointData<S>> = guard.values().collect();
340 items.sort_by_key(|d| (d.timestamp, d.seq));
341 Ok(items.into_iter().map(checkpoint_info_from_data).collect())
342 }
343
344 async fn update_state(
345 &self,
346 checkpoint_id: &str,
347 state: &S,
348 expected_version: u64,
349 ) -> GraphResult<u64> {
350 update_locked(&self.checkpoints, checkpoint_id, state, expected_version).await
351 }
352
353 async fn delete(&self, checkpoint_id: &str) -> GraphResult<()> {
354 self.checkpoints.lock().await.remove(checkpoint_id);
355 Ok(())
356 }
357}
358
359pub struct FileCheckpointer<S: StateSchema> {
361 directory: std::path::PathBuf,
362 next_seq: AtomicU64,
363 update_lock: Mutex<()>,
367 _phantom: std::marker::PhantomData<S>,
368}
369
370impl<S: StateSchema> FileCheckpointer<S> {
371 pub fn new(directory: impl Into<std::path::PathBuf>) -> GraphResult<Self> {
373 let dir = directory.into();
374 if !dir.exists() {
375 std::fs::create_dir_all(&dir).map_err(|e| {
376 GraphError::CheckpointError(format!(
377 "Failed to create directory '{}': {}",
378 dir.display(),
379 e
380 ))
381 })?;
382 }
383 Ok(Self {
384 directory: dir,
385 next_seq: AtomicU64::new(0),
386 update_lock: Mutex::new(()),
387 _phantom: std::marker::PhantomData,
388 })
389 }
390
391 fn checkpoint_path(&self, id: &str) -> GraphResult<std::path::PathBuf> {
392 if id.contains("..") || id.contains('/') || id.contains('\\') {
394 return Err(GraphError::CheckpointError(format!(
395 "Invalid checkpoint id '{}': path traversal detected",
396 id
397 )));
398 }
399 if std::path::Path::new(id).is_absolute() {
400 return Err(GraphError::CheckpointError(format!(
401 "Invalid checkpoint id '{}': absolute path not allowed",
402 id
403 )));
404 }
405 Ok(self.directory.join(format!("{}.json", id)))
406 }
407
408 async fn sorted_ids(&self) -> GraphResult<Vec<(i64, u64, String)>> {
410 let mut items: Vec<(i64, u64, String)> = Vec::new();
411 let mut entries = tokio::fs::read_dir(&self.directory)
412 .await
413 .map_err(|e| GraphError::CheckpointError(format!("Read dir error: {}", e)))?;
414 while let Some(entry) = entries
415 .next_entry()
416 .await
417 .map_err(|e| GraphError::CheckpointError(format!("Read dir entry error: {}", e)))?
418 {
419 let path = entry.path();
420 if path.extension().is_some_and(|ext| ext == "json") {
421 let Some(id) = path.file_stem().and_then(|s| s.to_str()).map(String::from) else {
422 continue;
423 };
424 let json = tokio::fs::read_to_string(&path)
425 .await
426 .map_err(|e| GraphError::CheckpointError(format!("Read error: {}", e)))?;
427 let data: CheckpointData<S> = serde_json::from_str(&json).map_err(|e| {
428 GraphError::CheckpointError(format!("Deserialize error: {}", e))
429 })?;
430 items.push((data.timestamp, data.seq, id));
431 }
432 }
433 items.sort();
435 Ok(items)
436 }
437}
438
439#[async_trait]
446impl<S: StateSchema> Checkpointer<S> for FileCheckpointer<S> {
447 async fn save(&self, state: &S, recursion_count: usize) -> GraphResult<String> {
448 let seq = self.next_seq.fetch_add(1, Ordering::SeqCst);
449 let data = CheckpointData::with_progress(state.clone(), seq, recursion_count);
450 let id = data.id.clone();
451 let path = self.checkpoint_path(&id)?;
452
453 let json = serde_json::to_string_pretty(&data)
454 .map_err(|e| GraphError::CheckpointError(format!("Serialize error: {}", e)))?;
455
456 let tmp_path = self.directory.join(format!("{id}.json.tmp"));
460 tokio::fs::write(&tmp_path, &json)
461 .await
462 .map_err(|e| GraphError::CheckpointError(format!("Write error: {}", e)))?;
463 tokio::fs::rename(&tmp_path, &path)
464 .await
465 .map_err(|e| GraphError::CheckpointError(format!("Atomic rename error: {}", e)))?;
466
467 Ok(id)
468 }
469
470 async fn load(&self, checkpoint_id: &str) -> GraphResult<S> {
471 let path = self.checkpoint_path(checkpoint_id)?;
472
473 if !path.exists() {
474 return Err(GraphError::CheckpointError(format!(
475 "Checkpoint '{}' not found",
476 checkpoint_id
477 )));
478 }
479
480 let json = tokio::fs::read_to_string(&path)
481 .await
482 .map_err(|e| GraphError::CheckpointError(format!("Read error: {}", e)))?;
483
484 let data: CheckpointData<S> = serde_json::from_str(&json)
485 .map_err(|e| GraphError::CheckpointError(format!("Deserialize error: {}", e)))?;
486
487 Ok(data.state)
488 }
489
490 async fn list(&self) -> GraphResult<Vec<String>> {
491 Ok(self
492 .sorted_ids()
493 .await?
494 .into_iter()
495 .map(|(_, _, id)| id)
496 .collect())
497 }
498
499 async fn last(&self) -> GraphResult<Option<(S, usize)>> {
500 let Some((_, _, last_id)) = self.sorted_ids().await?.into_iter().last() else {
501 return Ok(None);
502 };
503 let json = tokio::fs::read_to_string(&self.checkpoint_path(&last_id)?)
504 .await
505 .map_err(|e| GraphError::CheckpointError(format!("Read error: {}", e)))?;
506 let data: CheckpointData<S> = serde_json::from_str(&json)
507 .map_err(|e| GraphError::CheckpointError(format!("Deserialize error: {}", e)))?;
508 Ok(Some((data.state, data.recursion_count)))
509 }
510
511 async fn snapshots(&self) -> GraphResult<Vec<CheckpointInfo<S>>> {
512 let ids = self.sorted_ids().await?;
513 let mut snaps = Vec::with_capacity(ids.len());
514 for (_, _, id) in ids {
515 let json = tokio::fs::read_to_string(&self.checkpoint_path(&id)?)
516 .await
517 .map_err(|e| GraphError::CheckpointError(format!("Read error: {}", e)))?;
518 let data: CheckpointData<S> = serde_json::from_str(&json)
519 .map_err(|e| GraphError::CheckpointError(format!("Deserialize error: {}", e)))?;
520 snaps.push(checkpoint_info_from_data(&data));
521 }
522 Ok(snaps)
523 }
524
525 async fn update_state(
526 &self,
527 checkpoint_id: &str,
528 state: &S,
529 expected_version: u64,
530 ) -> GraphResult<u64> {
531 let _guard = self.update_lock.lock().await;
532 let path = self.checkpoint_path(checkpoint_id)?;
533 let json = tokio::fs::read_to_string(&path)
534 .await
535 .map_err(|e| GraphError::CheckpointError(format!("Read error: {}", e)))?;
536 let mut data: CheckpointData<S> = serde_json::from_str(&json)
537 .map_err(|e| GraphError::CheckpointError(format!("Deserialize error: {}", e)))?;
538 if data.version != expected_version {
539 return Err(GraphError::CheckpointVersionConflict {
540 checkpoint_id: checkpoint_id.to_string(),
541 expected: expected_version,
542 actual: data.version,
543 });
544 }
545 data.state = state.clone();
546 data.version += 1;
547
548 let json = serde_json::to_string_pretty(&data)
549 .map_err(|e| GraphError::CheckpointError(format!("Serialize error: {}", e)))?;
550 let tmp_path = self.directory.join(format!("{checkpoint_id}.json.tmp"));
551 tokio::fs::write(&tmp_path, &json)
552 .await
553 .map_err(|e| GraphError::CheckpointError(format!("Write error: {}", e)))?;
554 tokio::fs::rename(&tmp_path, &path)
555 .await
556 .map_err(|e| GraphError::CheckpointError(format!("Atomic rename error: {}", e)))?;
557 Ok(data.version)
558 }
559
560 async fn delete(&self, checkpoint_id: &str) -> GraphResult<()> {
561 let path = self.checkpoint_path(checkpoint_id)?;
562
563 if path.exists() {
564 tokio::fs::remove_file(&path)
565 .await
566 .map_err(|e| GraphError::CheckpointError(format!("Delete error: {}", e)))?;
567 }
568
569 Ok(())
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use crate::state::AgentState;
577
578 #[tokio::test]
579 async fn test_thread_safe_checkpointer() {
580 let checkpointer = ThreadSafeMemoryCheckpointer::<AgentState>::new();
581
582 let state = AgentState::new("test".to_string());
583 let id = checkpointer.save(&state, 0).await.unwrap();
584
585 let loaded = checkpointer.load(&id).await.unwrap();
586 assert_eq!(loaded.input, "test");
587
588 let list = checkpointer.list().await.unwrap();
589 assert_eq!(list.len(), 1);
590
591 checkpointer.delete(&id).await.unwrap();
592 let list = checkpointer.list().await.unwrap();
593 assert!(list.is_empty());
594 }
595
596 #[tokio::test]
597 async fn test_file_checkpointer() {
598 let temp_dir = tempfile::tempdir().unwrap();
599 let checkpointer = FileCheckpointer::<AgentState>::new(temp_dir.path()).unwrap();
600
601 let state = AgentState::new("file_test".to_string());
602 let id = checkpointer.save(&state, 0).await.unwrap();
603
604 let loaded = checkpointer.load(&id).await.unwrap();
605 assert_eq!(loaded.input, "file_test");
606
607 let list = checkpointer.list().await.unwrap();
608 assert_eq!(list.len(), 1);
609
610 checkpointer.delete(&id).await.unwrap();
611 let list = checkpointer.list().await.unwrap();
612 assert!(list.is_empty());
613 }
614
615 #[tokio::test]
616 async fn test_file_checkpointer_atomic_write() {
617 let temp_dir = tempfile::tempdir().unwrap();
618 let checkpointer = FileCheckpointer::<AgentState>::new(temp_dir.path()).unwrap();
619
620 let id = checkpointer
621 .save(&AgentState::new("atomic".to_string()), 0)
622 .await
623 .unwrap();
624
625 let main = temp_dir.path().join(format!("{id}.json"));
627 assert!(main.exists(), "checkpoint file must exist");
628 let json = tokio::fs::read_to_string(&main).await.unwrap();
629 assert!(
630 serde_json::from_str::<CheckpointData<AgentState>>(&json).is_ok(),
631 "checkpoint file must be complete JSON after atomic write"
632 );
633 assert!(
634 !temp_dir.path().join(format!("{id}.json.tmp")).exists(),
635 "tmp file must be renamed away, not left behind"
636 );
637
638 std::fs::write(temp_dir.path().join("stale.json.tmp"), b"{}").unwrap();
640 let list = checkpointer.list().await.unwrap();
641 assert_eq!(list, vec![id]);
642 }
643
644 #[tokio::test]
645 async fn test_file_checkpointer_multiple() {
646 let temp_dir = tempfile::tempdir().unwrap();
647 let checkpointer = FileCheckpointer::<AgentState>::new(temp_dir.path()).unwrap();
648
649 let id1 = checkpointer
650 .save(&AgentState::new("state1".to_string()), 0)
651 .await
652 .unwrap();
653 let id2 = checkpointer
654 .save(&AgentState::new("state2".to_string()), 0)
655 .await
656 .unwrap();
657 let _id3 = checkpointer
658 .save(&AgentState::new("state3".to_string()), 0)
659 .await
660 .unwrap();
661
662 let list = checkpointer.list().await.unwrap();
663 assert_eq!(list.len(), 3);
664
665 let loaded = checkpointer.load(&id2).await.unwrap();
666 assert_eq!(loaded.input, "state2");
667
668 checkpointer.delete(&id1).await.unwrap();
669 let list = checkpointer.list().await.unwrap();
670 assert_eq!(list.len(), 2);
671 }
672
673 #[tokio::test]
674 async fn test_file_checkpointer_path_traversal() {
675 let temp_dir = tempfile::tempdir().unwrap();
676 let checkpointer = FileCheckpointer::<AgentState>::new(temp_dir.path()).unwrap();
677
678 let result = checkpointer.load("..").await;
680 assert!(result.is_err());
681
682 let result = checkpointer.load("../etc/passwd").await;
683 assert!(result.is_err());
684 }
685
686 #[tokio::test]
687 async fn test_list_orders_oldest_to_newest() {
688 let checkpointer = ThreadSafeMemoryCheckpointer::<AgentState>::new();
689 checkpointer
690 .save(&AgentState::new("first".to_string()), 0)
691 .await
692 .unwrap();
693 checkpointer
694 .save(&AgentState::new("second".to_string()), 1)
695 .await
696 .unwrap();
697 checkpointer
698 .save(&AgentState::new("third".to_string()), 2)
699 .await
700 .unwrap();
701
702 let list = checkpointer.list().await.unwrap();
703 assert_eq!(list.len(), 3);
704 let (state, _) = checkpointer.last().await.unwrap().unwrap();
706 assert_eq!(state.input, "third");
707 }
708
709 #[tokio::test]
710 async fn test_last_returns_recursion_count() {
711 let checkpointer = ThreadSafeMemoryCheckpointer::<AgentState>::new();
712 checkpointer
713 .save(&AgentState::new("a".to_string()), 7)
714 .await
715 .unwrap();
716 checkpointer
717 .save(&AgentState::new("b".to_string()), 12)
718 .await
719 .unwrap();
720
721 let (state, recursion_count) = checkpointer.last().await.unwrap().unwrap();
723 assert_eq!(state.input, "b");
724 assert_eq!(recursion_count, 12);
725 }
726
727 #[tokio::test]
728 async fn test_update_state_occ_memory() {
729 let checkpointer = ThreadSafeMemoryCheckpointer::<AgentState>::new();
730 let id = checkpointer
731 .save(&AgentState::new("v1".to_string()), 0)
732 .await
733 .unwrap();
734
735 let version = checkpointer
737 .update_state(&id, &AgentState::new("v2".to_string()), 1)
738 .await
739 .unwrap();
740 assert_eq!(version, 2);
741 assert_eq!(checkpointer.load(&id).await.unwrap().input, "v2");
742
743 let conflict = checkpointer
745 .update_state(&id, &AgentState::new("v3-stale".to_string()), 1)
746 .await
747 .unwrap_err();
748 match conflict {
749 GraphError::CheckpointVersionConflict {
750 expected, actual, ..
751 } => {
752 assert_eq!(expected, 1);
753 assert_eq!(actual, 2);
754 }
755 other => panic!("expected CheckpointVersionConflict, got {other:?}"),
756 }
757 assert_eq!(checkpointer.load(&id).await.unwrap().input, "v2");
758
759 checkpointer
761 .update_state(&id, &AgentState::new("v3".to_string()), 2)
762 .await
763 .unwrap();
764 assert_eq!(checkpointer.load(&id).await.unwrap().input, "v3");
765 }
766
767 #[tokio::test]
768 async fn test_update_state_missing_and_file() {
769 let temp_dir = tempfile::tempdir().unwrap();
770 let checkpointer = FileCheckpointer::<AgentState>::new(temp_dir.path()).unwrap();
771 let id = checkpointer
772 .save(&AgentState::new("disk-v1".to_string()), 0)
773 .await
774 .unwrap();
775 checkpointer
776 .update_state(&id, &AgentState::new("disk-v2".to_string()), 1)
777 .await
778 .unwrap();
779 assert_eq!(checkpointer.load(&id).await.unwrap().input, "disk-v2");
780 assert!(checkpointer
782 .update_state(&id, &AgentState::new("stale".to_string()), 1)
783 .await
784 .is_err());
785 assert!(checkpointer
787 .update_state("missing", &AgentState::new("x".to_string()), 1)
788 .await
789 .is_err());
790 }
791
792 #[tokio::test]
793 async fn test_file_checkpointer_last_orders_by_save() {
794 let temp_dir = tempfile::tempdir().unwrap();
795 let checkpointer = FileCheckpointer::<AgentState>::new(temp_dir.path()).unwrap();
796 checkpointer
797 .save(&AgentState::new("one".to_string()), 1)
798 .await
799 .unwrap();
800 checkpointer
801 .save(&AgentState::new("two".to_string()), 2)
802 .await
803 .unwrap();
804
805 let list = checkpointer.list().await.unwrap();
806 assert_eq!(list.len(), 2);
807 let (state, recursion_count) = checkpointer.last().await.unwrap().unwrap();
808 assert_eq!(state.input, "two");
809 assert_eq!(recursion_count, 2);
810 }
811}