use crate::outcome::Interrupt;
use crate::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
pub type RunId = String;
pub type CheckpointAttemptId = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AttemptStatus {
Running,
Completed,
Failed,
Interrupted,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttemptRecord {
pub attempt_id: CheckpointAttemptId,
pub run_id: RunId,
pub node_id: String,
pub attempt: u32,
pub input: Value,
pub output: Option<Value>,
pub status: AttemptStatus,
pub error: Option<String>,
pub meta: HashMap<String, Value>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub trace_ctx: Option<stack_ids::TraceCtx>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub finished_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunState {
pub run_id: RunId,
pub graph_name: String,
pub status: RunStatus,
pub attempts: Vec<AttemptRecord>,
pub state_snapshot: HashMap<String, Value>,
pub interrupted: Option<Interrupt>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RunStatus {
Running,
Completed,
Failed,
Interrupted,
Cancelled,
}
pub trait CheckpointStore: Send + Sync {
fn create_run(
&self,
graph_name: &str,
) -> Pin<Box<dyn Future<Output = Result<RunId>> + Send + '_>>;
fn record_attempt(
&self,
run_id: &str,
node_id: &str,
attempt: u32,
input: &Value,
) -> Pin<Box<dyn Future<Output = Result<CheckpointAttemptId>> + Send + '_>>;
fn complete_attempt(
&self,
attempt_id: &str,
output: &Value,
meta: &HashMap<String, Value>,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
fn fail_attempt(
&self,
attempt_id: &str,
error: &str,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
fn record_interrupt(
&self,
attempt_id: &str,
interrupt: &Interrupt,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
fn save_state_snapshot(
&self,
run_id: &str,
state: &HashMap<String, Value>,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
fn load_run(
&self,
run_id: &str,
) -> Pin<Box<dyn Future<Output = Result<Option<RunState>>> + Send + '_>>;
fn complete_run(&self, run_id: &str) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
fn fail_run(
&self,
run_id: &str,
error: &str,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointMetadata {
pub graph_hash: String,
pub run_id: String,
pub node_id: String,
pub step: usize,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunSummary {
pub run_id: String,
pub graph_name: String,
pub status: RunStatus,
pub total_nodes_executed: usize,
pub total_attempts: usize,
pub failed_attempts: usize,
pub trace_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub trace_ctx: Option<stack_ids::TraceCtx>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub finished_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl InMemoryCheckpointStore {
pub async fn summarize_run(&self, run_id: &str) -> Option<RunSummary> {
let runs = self.runs.read().await;
let run = runs.get(run_id)?;
let total_attempts = run.attempts.len();
let failed_attempts = run
.attempts
.iter()
.filter(|a| a.status == AttemptStatus::Failed)
.count();
let trace_id = run.attempts.iter().find_map(|attempt| {
attempt
.meta
.get("trace_id")
.and_then(|value| value.as_str())
.map(str::to_owned)
});
let trace_ctx = run
.attempts
.iter()
.find_map(|attempt| attempt.trace_ctx.clone());
let unique_nodes: std::collections::HashSet<&str> =
run.attempts.iter().map(|a| a.node_id.as_str()).collect();
Some(RunSummary {
run_id: run.run_id.clone(),
graph_name: run.graph_name.clone(),
status: run.status.clone(),
total_nodes_executed: unique_nodes.len(),
total_attempts,
failed_attempts,
trace_id,
trace_ctx,
started_at: run.created_at,
finished_at: if run.status == RunStatus::Running {
None
} else {
Some(run.updated_at)
},
})
}
}
pub struct InMemoryCheckpointStore {
runs: Arc<RwLock<HashMap<RunId, RunState>>>,
attempts: Arc<RwLock<HashMap<CheckpointAttemptId, AttemptRecord>>>,
}
impl InMemoryCheckpointStore {
pub fn new() -> Self {
Self {
runs: Arc::new(RwLock::new(HashMap::new())),
attempts: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn list_runs(&self) -> Vec<RunState> {
self.runs.read().await.values().cloned().collect()
}
}
impl Default for InMemoryCheckpointStore {
fn default() -> Self {
Self::new()
}
}
impl CheckpointStore for InMemoryCheckpointStore {
fn create_run(
&self,
graph_name: &str,
) -> Pin<Box<dyn Future<Output = Result<RunId>> + Send + '_>> {
let graph_name = graph_name.to_string();
Box::pin(async move {
let run_id = stack_ids::GraphRunId::random("agent-graph").to_string();
let now = chrono::Utc::now();
let run = RunState {
run_id: run_id.clone(),
graph_name,
status: RunStatus::Running,
attempts: Vec::new(),
state_snapshot: HashMap::new(),
interrupted: None,
created_at: now,
updated_at: now,
};
self.runs.write().await.insert(run_id.clone(), run);
Ok(run_id)
})
}
fn record_attempt(
&self,
run_id: &str,
node_id: &str,
attempt: u32,
input: &Value,
) -> Pin<Box<dyn Future<Output = Result<CheckpointAttemptId>> + Send + '_>> {
let run_id = run_id.to_string();
let node_id = node_id.to_string();
let input = input.clone();
Box::pin(async move {
let attempt_id =
stack_ids::GraphCheckpointAttemptId::random("agent-graph-checkpoint").to_string();
let now = chrono::Utc::now();
let record = AttemptRecord {
attempt_id: attempt_id.clone(),
run_id: run_id.clone(),
node_id: node_id.clone(),
attempt,
input,
output: None,
status: AttemptStatus::Running,
error: None,
meta: HashMap::new(),
trace_ctx: None,
started_at: now,
finished_at: None,
};
self.attempts
.write()
.await
.insert(attempt_id.clone(), record.clone());
if let Some(run) = self.runs.write().await.get_mut(&run_id) {
run.attempts.push(record);
run.updated_at = now;
}
Ok(attempt_id)
})
}
fn complete_attempt(
&self,
attempt_id: &str,
output: &Value,
meta: &HashMap<String, Value>,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
let attempt_id = attempt_id.to_string();
let output = output.clone();
let meta = meta.clone();
Box::pin(async move {
let now = chrono::Utc::now();
let mut attempts = self.attempts.write().await;
if let Some(record) = attempts.get_mut(&attempt_id) {
record.status = AttemptStatus::Completed;
record.output = Some(output.clone());
record.meta = meta.clone();
record.finished_at = Some(now);
let run_id = record.run_id.clone();
drop(attempts);
if let Some(run) = self.runs.write().await.get_mut(&run_id) {
if let Some(a) = run.attempts.iter_mut().find(|a| a.attempt_id == attempt_id) {
a.status = AttemptStatus::Completed;
a.output = Some(output);
a.meta = meta;
a.finished_at = Some(now);
}
run.updated_at = now;
}
}
Ok(())
})
}
fn fail_attempt(
&self,
attempt_id: &str,
error: &str,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
let attempt_id = attempt_id.to_string();
let error = error.to_string();
Box::pin(async move {
let now = chrono::Utc::now();
let mut attempts = self.attempts.write().await;
if let Some(record) = attempts.get_mut(&attempt_id) {
record.status = AttemptStatus::Failed;
record.error = Some(error.clone());
record.finished_at = Some(now);
let run_id = record.run_id.clone();
drop(attempts);
if let Some(run) = self.runs.write().await.get_mut(&run_id) {
if let Some(a) = run.attempts.iter_mut().find(|a| a.attempt_id == attempt_id) {
a.status = AttemptStatus::Failed;
a.error = Some(error);
a.finished_at = Some(now);
}
run.updated_at = now;
}
}
Ok(())
})
}
fn record_interrupt(
&self,
attempt_id: &str,
interrupt: &Interrupt,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
let attempt_id = attempt_id.to_string();
let interrupt = interrupt.clone();
Box::pin(async move {
let now = chrono::Utc::now();
let mut attempts = self.attempts.write().await;
if let Some(record) = attempts.get_mut(&attempt_id) {
record.status = AttemptStatus::Interrupted;
record.finished_at = Some(now);
let run_id = record.run_id.clone();
drop(attempts);
if let Some(run) = self.runs.write().await.get_mut(&run_id) {
run.interrupted = Some(interrupt);
run.status = RunStatus::Interrupted;
if let Some(a) = run.attempts.iter_mut().find(|a| a.attempt_id == attempt_id) {
a.status = AttemptStatus::Interrupted;
a.finished_at = Some(now);
}
run.updated_at = now;
}
}
Ok(())
})
}
fn save_state_snapshot(
&self,
run_id: &str,
state: &HashMap<String, Value>,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
let run_id = run_id.to_string();
let state = state.clone();
Box::pin(async move {
if let Some(run) = self.runs.write().await.get_mut(&run_id) {
run.state_snapshot = state;
run.updated_at = chrono::Utc::now();
}
Ok(())
})
}
fn load_run(
&self,
run_id: &str,
) -> Pin<Box<dyn Future<Output = Result<Option<RunState>>> + Send + '_>> {
let run_id = run_id.to_string();
Box::pin(async move { Ok(self.runs.read().await.get(&run_id).cloned()) })
}
fn complete_run(&self, run_id: &str) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
let run_id = run_id.to_string();
Box::pin(async move {
let mut runs = self.runs.write().await;
let run = runs
.get_mut(&run_id)
.ok_or_else(|| crate::AgentGraphError::RunNotFound(run_id.clone()))?;
if run.status != RunStatus::Running {
return Err(crate::AgentGraphError::TerminalStateConflict(run_id));
}
run.status = RunStatus::Completed;
run.updated_at = chrono::Utc::now();
Ok(())
})
}
fn fail_run(
&self,
run_id: &str,
_error: &str,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
let run_id = run_id.to_string();
Box::pin(async move {
let mut runs = self.runs.write().await;
let run = runs
.get_mut(&run_id)
.ok_or_else(|| crate::AgentGraphError::RunNotFound(run_id.clone()))?;
if run.status != RunStatus::Running {
return Err(crate::AgentGraphError::TerminalStateConflict(run_id));
}
run.status = RunStatus::Failed;
run.updated_at = chrono::Utc::now();
Ok(())
})
}
}