Skip to main content

synaptic_graph/
checkpoint.rs

1use std::collections::HashMap;
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use synaptic_core::SynapticError;
6
7/// Configuration identifying a checkpoint (thread/conversation).
8#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
9pub struct CheckpointConfig {
10    pub thread_id: String,
11    /// Optional: target a specific checkpoint for time-travel.
12    /// When `None`, operations target the latest checkpoint.
13    pub checkpoint_id: Option<String>,
14}
15
16impl CheckpointConfig {
17    pub fn new(thread_id: impl Into<String>) -> Self {
18        Self {
19            thread_id: thread_id.into(),
20            checkpoint_id: None,
21        }
22    }
23
24    /// Create a config targeting a specific checkpoint (for time-travel).
25    pub fn with_checkpoint_id(
26        thread_id: impl Into<String>,
27        checkpoint_id: impl Into<String>,
28    ) -> Self {
29        Self {
30            thread_id: thread_id.into(),
31            checkpoint_id: Some(checkpoint_id.into()),
32        }
33    }
34}
35
36/// A snapshot of graph state at a point in execution.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Checkpoint {
39    /// Unique identifier for this checkpoint.
40    pub id: String,
41    /// Serialized graph state.
42    pub state: serde_json::Value,
43    /// The next node to execute (or None if graph completed).
44    pub next_node: Option<String>,
45    /// ID of the previous checkpoint (for traversing history).
46    pub parent_id: Option<String>,
47    /// Metadata about this checkpoint (node name, timestamp, etc.).
48    pub metadata: HashMap<String, serde_json::Value>,
49}
50
51impl Checkpoint {
52    /// Create a new checkpoint with auto-generated ID.
53    pub fn new(state: serde_json::Value, next_node: Option<String>) -> Self {
54        Self {
55            id: generate_checkpoint_id(),
56            state,
57            next_node,
58            parent_id: None,
59            metadata: HashMap::new(),
60        }
61    }
62
63    /// Set the parent checkpoint ID.
64    pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
65        self.parent_id = Some(parent_id.into());
66        self
67    }
68
69    /// Add metadata to the checkpoint.
70    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
71        self.metadata.insert(key.into(), value);
72        self
73    }
74}
75
76fn generate_checkpoint_id() -> String {
77    use std::sync::atomic::{AtomicU64, Ordering};
78    use std::time::{SystemTime, UNIX_EPOCH};
79
80    static COUNTER: AtomicU64 = AtomicU64::new(0);
81
82    let ts = SystemTime::now()
83        .duration_since(UNIX_EPOCH)
84        .unwrap_or_default()
85        .as_nanos();
86    let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
87    format!("{ts:x}-{seq:04x}")
88}
89
90/// Trait for persisting graph state checkpoints.
91#[async_trait]
92pub trait Checkpointer: Send + Sync {
93    /// Save a checkpoint for the given thread.
94    async fn put(
95        &self,
96        config: &CheckpointConfig,
97        checkpoint: &Checkpoint,
98    ) -> Result<(), SynapticError>;
99
100    /// Get a checkpoint. If `config.checkpoint_id` is set, returns that specific
101    /// checkpoint; otherwise returns the latest checkpoint for the thread.
102    async fn get(&self, config: &CheckpointConfig) -> Result<Option<Checkpoint>, SynapticError>;
103
104    /// List all checkpoints for a thread, ordered oldest to newest.
105    async fn list(&self, config: &CheckpointConfig) -> Result<Vec<Checkpoint>, SynapticError>;
106}