behest_runtime/
snapshot.rs1use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7use tokio::fs;
8use uuid::Uuid;
9
10use super::error::{RuntimeError, RuntimeResult};
11use super::run::{RunId, RunRequest, RunStatus};
12use super::turn::TurnState;
13use behest_provider::{FinishReason, Message, TokenUsage};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Snapshot {
18 pub run_id: RunId,
20 pub session_id: Uuid,
22 pub status: RunStatus,
24 pub iteration: usize,
26 pub current_state: TurnState,
28 pub total_usage: TokenUsage,
30 pub last_finish: Option<FinishReason>,
32 pub assistant_message: Option<Message>,
34 pub assistant_msg_id: Option<Uuid>,
36 pub request: RunRequest,
38 #[serde(default)]
40 pub output_recovery_count: u32,
41 pub timestamp: DateTime<Utc>,
43}
44
45#[async_trait]
47pub trait SnapshotStore: Send + Sync + 'static {
48 async fn save(&self, snapshot: &Snapshot) -> RuntimeResult<()>;
54
55 async fn load(&self, run_id: RunId) -> RuntimeResult<Option<Snapshot>>;
61
62 async fn delete(&self, run_id: RunId) -> RuntimeResult<()>;
68
69 async fn list(&self) -> RuntimeResult<Vec<Snapshot>>;
75}
76
77pub struct FileSnapshotStore {
83 base_dir: PathBuf,
84}
85
86impl FileSnapshotStore {
87 #[must_use]
89 pub fn new(base_dir: PathBuf) -> Self {
90 Self { base_dir }
91 }
92
93 fn path_for(&self, run_id: RunId) -> PathBuf {
94 self.base_dir.join(format!("snapshot_{run_id}.json"))
95 }
96}
97
98#[async_trait]
99impl SnapshotStore for FileSnapshotStore {
100 async fn save(&self, snapshot: &Snapshot) -> RuntimeResult<()> {
101 if let Err(e) = fs::create_dir_all(&self.base_dir).await {
102 return Err(RuntimeError::RecoveryFailed(format!(
103 "failed to create snapshot dir: {e}"
104 )));
105 }
106
107 let serialized = serde_json::to_string_pretty(snapshot).map_err(|e| {
108 RuntimeError::RecoveryFailed(format!("failed to serialize snapshot: {e}"))
109 })?;
110
111 let temp_path = self
112 .base_dir
113 .join(format!("snapshot_{}.json.tmp", snapshot.run_id));
114 if let Err(e) = fs::write(&temp_path, serialized).await {
115 return Err(RuntimeError::RecoveryFailed(format!(
116 "failed to write snapshot: {e}"
117 )));
118 }
119
120 let final_path = self.path_for(snapshot.run_id);
121 fs::rename(temp_path, final_path).await.map_err(|e| {
122 RuntimeError::RecoveryFailed(format!("failed to finalize snapshot: {e}"))
123 })?;
124
125 Ok(())
126 }
127
128 async fn load(&self, run_id: RunId) -> RuntimeResult<Option<Snapshot>> {
129 let path = self.path_for(run_id);
130 if !path.exists() {
131 return Ok(None);
132 }
133
134 let content = fs::read_to_string(&path).await.map_err(|e| {
135 RuntimeError::RecoveryFailed(format!("failed to read snapshot file: {e}"))
136 })?;
137
138 let snapshot: Snapshot = serde_json::from_str(&content).map_err(|e| {
139 RuntimeError::RecoveryFailed(format!("failed to deserialize snapshot file: {e}"))
140 })?;
141
142 Ok(Some(snapshot))
143 }
144
145 async fn delete(&self, run_id: RunId) -> RuntimeResult<()> {
146 let path = self.path_for(run_id);
147 if path.exists() {
148 fs::remove_file(path).await.map_err(|e| {
149 RuntimeError::RecoveryFailed(format!("failed to delete snapshot file: {e}"))
150 })?;
151 }
152 Ok(())
153 }
154
155 async fn list(&self) -> RuntimeResult<Vec<Snapshot>> {
156 if !self.base_dir.exists() {
157 return Ok(Vec::new());
158 }
159
160 let mut snapshots = Vec::new();
161 let mut entries = fs::read_dir(&self.base_dir).await.map_err(|e| {
162 RuntimeError::RecoveryFailed(format!("failed to read snapshot dir: {e}"))
163 })?;
164
165 while let Some(entry) = entries.next_entry().await.map_err(|e| {
166 RuntimeError::RecoveryFailed(format!("failed to read snapshot directory entry: {e}"))
167 })? {
168 let path = entry.path();
169 if path.is_file()
170 && let Some(ext) = path.extension()
171 && ext == "json"
172 && let Ok(content) = fs::read_to_string(&path).await
173 && let Ok(snapshot) = serde_json::from_str::<Snapshot>(&content)
174 {
175 snapshots.push(snapshot);
176 }
177 }
178
179 Ok(snapshots)
180 }
181}