use crate::contract::event::AgentEvent;
use crate::contract::finding::Finding;
use crate::contract::ids::{AgentId, PhaseId, RunId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunCheckpoint {
pub run_id: RunId,
pub task: String,
pub status: CheckpointStatus,
pub current_phase: u32,
pub completed_phases: Vec<PhaseSummary>,
pub agent_results: HashMap<AgentId, AgentResultCache>,
#[serde(default)]
pub agent_sessions: HashMap<AgentId, AgentSessionCheckpoint>,
pub findings: Vec<Finding>,
pub total_tokens: u64,
pub created_at: u64,
pub updated_at: u64,
#[serde(default)]
pub workflow_meta: Option<serde_json::Value>,
#[serde(default)]
pub started_agent_ids: Vec<AgentId>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CheckpointStatus {
Running,
Completed,
Failed,
Cancelled,
}
impl std::fmt::Display for CheckpointStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
CheckpointStatus::Running => "Running",
CheckpointStatus::Completed => "Completed",
CheckpointStatus::Failed => "Failed",
CheckpointStatus::Cancelled => "Cancelled",
};
f.write_str(s)
}
}
impl CheckpointStatus {
pub fn as_str(&self) -> &'static str {
match self {
CheckpointStatus::Running => "running",
CheckpointStatus::Completed => "completed",
CheckpointStatus::Failed => "failed",
CheckpointStatus::Cancelled => "cancelled",
}
}
pub fn parse_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"completed" => CheckpointStatus::Completed,
"failed" => CheckpointStatus::Failed,
"cancelled" => CheckpointStatus::Cancelled,
_ => CheckpointStatus::Running,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseSummary {
pub phase_id: PhaseId,
pub label: String,
pub planned: usize,
pub ok: usize,
pub failed: usize,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub role: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentResultCache {
pub agent_id: AgentId,
pub phase_id: PhaseId,
pub status: String,
pub output: serde_json::Value,
pub findings: Vec<Finding>,
pub tokens: u64,
pub completed_at: u64,
#[serde(default)]
pub cache_key_hash: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub role: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSessionCheckpoint {
pub agent_id: AgentId,
#[serde(default)]
pub backend_id: Option<String>,
#[serde(default)]
pub protocol_session_id: Option<String>,
pub session_id: String,
pub status: String,
pub updated_at: u64,
#[serde(default)]
pub resumable: bool,
}
pub trait CheckpointBackend: Send + Sync + std::fmt::Debug {
fn init_run(&self, run_id: RunId, task: &str, run_dir: &str) -> anyhow::Result<()>;
fn init_run_with_meta(
&self,
run_id: RunId,
task: &str,
run_dir: &str,
workflow_meta: serde_json::Value,
) -> anyhow::Result<()>;
fn open_run(&self, run_id: RunId) -> anyhow::Result<Option<RunCheckpoint>>;
fn append_event(&self, event: &AgentEvent) -> anyhow::Result<()>;
fn upsert_agent_result(&self, cache: &AgentResultCache) -> anyhow::Result<()>;
fn upsert_agent_session(&self, session: &AgentSessionCheckpoint) -> anyhow::Result<()>;
fn get_checkpoint(&self) -> Option<RunCheckpoint>;
fn get_findings(&self) -> Vec<Finding>;
fn get_event_log(&self) -> anyhow::Result<Vec<AgentEvent>>;
fn can_resume(&self) -> bool;
fn reset_status_to_running(&self) -> anyhow::Result<()>;
fn cancel(&self) -> anyhow::Result<()>;
fn save_checkpoint(&self, checkpoint: &RunCheckpoint) -> anyhow::Result<()>;
}
pub fn current_timestamp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub fn list_run_dirs(base_dir: &Path) -> anyhow::Result<Vec<String>> {
if !base_dir.exists() {
return Ok(vec![]);
}
let mut run_dirs = Vec::new();
for entry in std::fs::read_dir(base_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
run_dirs.push(name.to_string());
}
}
}
run_dirs.sort();
Ok(run_dirs)
}