pub mod local;
pub use local::LocalStorageBackend;
#[cfg(feature = "cloud-runtime")]
pub mod redis;
#[cfg(feature = "cloud-runtime")]
pub use redis::RedisSessionStore;
#[cfg(feature = "cloud-runtime")]
pub mod s3;
#[cfg(feature = "cloud-runtime")]
pub use s3::S3StorageBackend;
use crate::error::Result;
use crate::message::Message;
use async_trait::async_trait;
#[async_trait]
pub trait StorageBackend: Send + Sync + 'static {
async fn load_transcript(&self, session_id: &str) -> Result<Vec<Message>>;
async fn save_transcript(&self, session_id: &str, messages: &[Message]) -> Result<()>;
async fn load_memory(&self, key: &str) -> Result<Option<String>>;
async fn save_memory(&self, key: &str, value: &str) -> Result<()>;
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct AgentCheckpointState {
pub step: usize,
pub transcript_len: usize,
}
#[async_trait]
pub trait SessionStore: Send + Sync + 'static {
async fn save_state(&self, session_id: &str, state: &AgentCheckpointState) -> Result<()>;
async fn load_state(&self, session_id: &str) -> Result<Option<AgentCheckpointState>>;
async fn delete_state(&self, session_id: &str) -> Result<()>;
}
pub struct NoopSessionStore;
#[async_trait]
impl SessionStore for NoopSessionStore {
async fn save_state(&self, _session_id: &str, _state: &AgentCheckpointState) -> Result<()> {
Ok(())
}
async fn load_state(&self, _session_id: &str) -> Result<Option<AgentCheckpointState>> {
Ok(None)
}
async fn delete_state(&self, _session_id: &str) -> Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn noop_session_store_is_always_empty() {
let store = NoopSessionStore;
let state = store.load_state("test-session").await.unwrap();
assert!(state.is_none());
}
#[tokio::test]
async fn noop_session_store_save_and_delete_are_noop() {
let store = NoopSessionStore;
let checkpoint = AgentCheckpointState {
step: 3,
transcript_len: 7,
};
store.save_state("session-1", &checkpoint).await.unwrap();
store.delete_state("session-1").await.unwrap();
let loaded = store.load_state("session-1").await.unwrap();
assert!(loaded.is_none());
}
#[test]
fn agent_checkpoint_state_serializes() {
let state = AgentCheckpointState {
step: 5,
transcript_len: 12,
};
let json = serde_json::to_string(&state).unwrap();
let roundtripped: AgentCheckpointState = serde_json::from_str(&json).unwrap();
assert_eq!(state, roundtripped);
}
#[test]
fn agent_checkpoint_state_zero_is_valid() {
let state = AgentCheckpointState {
step: 0,
transcript_len: 0,
};
let json = serde_json::to_string(&state).unwrap();
let rt: AgentCheckpointState = serde_json::from_str(&json).unwrap();
assert_eq!(rt.step, 0);
assert_eq!(rt.transcript_len, 0);
}
}