1use crate::outcome::Interrupt;
8use crate::Result;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12use std::future::Future;
13use std::pin::Pin;
14use std::sync::Arc;
15use tokio::sync::RwLock;
16
17pub type RunId = String;
19pub type CheckpointAttemptId = String;
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28pub enum AttemptStatus {
29 Running,
30 Completed,
31 Failed,
32 Interrupted,
33 Cancelled,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct AttemptRecord {
39 pub attempt_id: CheckpointAttemptId,
40 pub run_id: RunId,
41 pub node_id: String,
42 pub attempt: u32,
43 pub input: Value,
44 pub output: Option<Value>,
45 pub status: AttemptStatus,
46 pub error: Option<String>,
47 pub meta: HashMap<String, Value>,
48 #[serde(skip_serializing_if = "Option::is_none", default)]
50 pub trace_ctx: Option<stack_ids::TraceCtx>,
51 pub started_at: chrono::DateTime<chrono::Utc>,
52 pub finished_at: Option<chrono::DateTime<chrono::Utc>>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct RunState {
58 pub run_id: RunId,
59 pub graph_name: String,
60 pub status: RunStatus,
61 pub attempts: Vec<AttemptRecord>,
62 pub state_snapshot: HashMap<String, Value>,
63 pub interrupted: Option<Interrupt>,
64 pub created_at: chrono::DateTime<chrono::Utc>,
65 pub updated_at: chrono::DateTime<chrono::Utc>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70pub enum RunStatus {
71 Running,
72 Completed,
73 Failed,
74 Interrupted,
75 Cancelled,
76}
77
78pub trait CheckpointStore: Send + Sync {
82 fn create_run(
84 &self,
85 graph_name: &str,
86 ) -> Pin<Box<dyn Future<Output = Result<RunId>> + Send + '_>>;
87
88 fn record_attempt(
90 &self,
91 run_id: &str,
92 node_id: &str,
93 attempt: u32,
94 input: &Value,
95 ) -> Pin<Box<dyn Future<Output = Result<CheckpointAttemptId>> + Send + '_>>;
96
97 fn complete_attempt(
99 &self,
100 attempt_id: &str,
101 output: &Value,
102 meta: &HashMap<String, Value>,
103 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
104
105 fn fail_attempt(
107 &self,
108 attempt_id: &str,
109 error: &str,
110 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
111
112 fn record_interrupt(
114 &self,
115 attempt_id: &str,
116 interrupt: &Interrupt,
117 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
118
119 fn save_state_snapshot(
121 &self,
122 run_id: &str,
123 state: &HashMap<String, Value>,
124 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
125
126 fn load_run(
128 &self,
129 run_id: &str,
130 ) -> Pin<Box<dyn Future<Output = Result<Option<RunState>>> + Send + '_>>;
131
132 fn complete_run(&self, run_id: &str) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
134
135 fn fail_run(
137 &self,
138 run_id: &str,
139 error: &str,
140 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct CheckpointMetadata {
146 pub graph_hash: String,
149 pub run_id: String,
151 pub node_id: String,
153 pub step: usize,
155 pub created_at: chrono::DateTime<chrono::Utc>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct RunSummary {
162 pub run_id: String,
163 pub graph_name: String,
164 pub status: RunStatus,
165 pub total_nodes_executed: usize,
166 pub total_attempts: usize,
167 pub failed_attempts: usize,
168 pub trace_id: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none", default)]
172 pub trace_ctx: Option<stack_ids::TraceCtx>,
173 pub started_at: chrono::DateTime<chrono::Utc>,
174 pub finished_at: Option<chrono::DateTime<chrono::Utc>>,
175}
176
177impl InMemoryCheckpointStore {
178 pub async fn summarize_run(&self, run_id: &str) -> Option<RunSummary> {
180 let runs = self.runs.read().await;
181 let run = runs.get(run_id)?;
182 let total_attempts = run.attempts.len();
183 let failed_attempts = run
184 .attempts
185 .iter()
186 .filter(|a| a.status == AttemptStatus::Failed)
187 .count();
188 let trace_id = run.attempts.iter().find_map(|attempt| {
189 attempt
190 .meta
191 .get("trace_id")
192 .and_then(|value| value.as_str())
193 .map(str::to_owned)
194 });
195 let trace_ctx = run
196 .attempts
197 .iter()
198 .find_map(|attempt| attempt.trace_ctx.clone());
199 let unique_nodes: std::collections::HashSet<&str> =
200 run.attempts.iter().map(|a| a.node_id.as_str()).collect();
201 Some(RunSummary {
202 run_id: run.run_id.clone(),
203 graph_name: run.graph_name.clone(),
204 status: run.status.clone(),
205 total_nodes_executed: unique_nodes.len(),
206 total_attempts,
207 failed_attempts,
208 trace_id,
209 trace_ctx,
210 started_at: run.created_at,
211 finished_at: if run.status == RunStatus::Running {
212 None
213 } else {
214 Some(run.updated_at)
215 },
216 })
217 }
218}
219
220pub struct InMemoryCheckpointStore {
222 runs: Arc<RwLock<HashMap<RunId, RunState>>>,
223 attempts: Arc<RwLock<HashMap<CheckpointAttemptId, AttemptRecord>>>,
224}
225
226impl InMemoryCheckpointStore {
227 pub fn new() -> Self {
228 Self {
229 runs: Arc::new(RwLock::new(HashMap::new())),
230 attempts: Arc::new(RwLock::new(HashMap::new())),
231 }
232 }
233
234 pub async fn list_runs(&self) -> Vec<RunState> {
236 self.runs.read().await.values().cloned().collect()
237 }
238}
239
240impl Default for InMemoryCheckpointStore {
241 fn default() -> Self {
242 Self::new()
243 }
244}
245
246impl CheckpointStore for InMemoryCheckpointStore {
247 fn create_run(
248 &self,
249 graph_name: &str,
250 ) -> Pin<Box<dyn Future<Output = Result<RunId>> + Send + '_>> {
251 let graph_name = graph_name.to_string();
252 Box::pin(async move {
253 let run_id = stack_ids::GraphRunId::random("agent-graph").to_string();
254 let now = chrono::Utc::now();
255 let run = RunState {
256 run_id: run_id.clone(),
257 graph_name,
258 status: RunStatus::Running,
259 attempts: Vec::new(),
260 state_snapshot: HashMap::new(),
261 interrupted: None,
262 created_at: now,
263 updated_at: now,
264 };
265 self.runs.write().await.insert(run_id.clone(), run);
266 Ok(run_id)
267 })
268 }
269
270 fn record_attempt(
271 &self,
272 run_id: &str,
273 node_id: &str,
274 attempt: u32,
275 input: &Value,
276 ) -> Pin<Box<dyn Future<Output = Result<CheckpointAttemptId>> + Send + '_>> {
277 let run_id = run_id.to_string();
278 let node_id = node_id.to_string();
279 let input = input.clone();
280 Box::pin(async move {
281 let attempt_id =
282 stack_ids::GraphCheckpointAttemptId::random("agent-graph-checkpoint").to_string();
283 let now = chrono::Utc::now();
284 let record = AttemptRecord {
285 attempt_id: attempt_id.clone(),
286 run_id: run_id.clone(),
287 node_id: node_id.clone(),
288 attempt,
289 input,
290 output: None,
291 status: AttemptStatus::Running,
292 error: None,
293 meta: HashMap::new(),
294 trace_ctx: None,
295 started_at: now,
296 finished_at: None,
297 };
298 self.attempts
299 .write()
300 .await
301 .insert(attempt_id.clone(), record.clone());
302 if let Some(run) = self.runs.write().await.get_mut(&run_id) {
303 run.attempts.push(record);
304 run.updated_at = now;
305 }
306 Ok(attempt_id)
307 })
308 }
309
310 fn complete_attempt(
311 &self,
312 attempt_id: &str,
313 output: &Value,
314 meta: &HashMap<String, Value>,
315 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
316 let attempt_id = attempt_id.to_string();
317 let output = output.clone();
318 let meta = meta.clone();
319 Box::pin(async move {
320 let now = chrono::Utc::now();
321 let mut attempts = self.attempts.write().await;
322 if let Some(record) = attempts.get_mut(&attempt_id) {
323 record.status = AttemptStatus::Completed;
324 record.output = Some(output.clone());
325 record.meta = meta.clone();
326 record.finished_at = Some(now);
327 let run_id = record.run_id.clone();
329 drop(attempts);
330 if let Some(run) = self.runs.write().await.get_mut(&run_id) {
331 if let Some(a) = run.attempts.iter_mut().find(|a| a.attempt_id == attempt_id) {
332 a.status = AttemptStatus::Completed;
333 a.output = Some(output);
334 a.meta = meta;
335 a.finished_at = Some(now);
336 }
337 run.updated_at = now;
338 }
339 }
340 Ok(())
341 })
342 }
343
344 fn fail_attempt(
345 &self,
346 attempt_id: &str,
347 error: &str,
348 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
349 let attempt_id = attempt_id.to_string();
350 let error = error.to_string();
351 Box::pin(async move {
352 let now = chrono::Utc::now();
353 let mut attempts = self.attempts.write().await;
354 if let Some(record) = attempts.get_mut(&attempt_id) {
355 record.status = AttemptStatus::Failed;
356 record.error = Some(error.clone());
357 record.finished_at = Some(now);
358 let run_id = record.run_id.clone();
359 drop(attempts);
360 if let Some(run) = self.runs.write().await.get_mut(&run_id) {
361 if let Some(a) = run.attempts.iter_mut().find(|a| a.attempt_id == attempt_id) {
362 a.status = AttemptStatus::Failed;
363 a.error = Some(error);
364 a.finished_at = Some(now);
365 }
366 run.updated_at = now;
367 }
368 }
369 Ok(())
370 })
371 }
372
373 fn record_interrupt(
374 &self,
375 attempt_id: &str,
376 interrupt: &Interrupt,
377 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
378 let attempt_id = attempt_id.to_string();
379 let interrupt = interrupt.clone();
380 Box::pin(async move {
381 let now = chrono::Utc::now();
382 let mut attempts = self.attempts.write().await;
383 if let Some(record) = attempts.get_mut(&attempt_id) {
384 record.status = AttemptStatus::Interrupted;
385 record.finished_at = Some(now);
386 let run_id = record.run_id.clone();
387 drop(attempts);
388 if let Some(run) = self.runs.write().await.get_mut(&run_id) {
389 run.interrupted = Some(interrupt);
390 run.status = RunStatus::Interrupted;
391 if let Some(a) = run.attempts.iter_mut().find(|a| a.attempt_id == attempt_id) {
392 a.status = AttemptStatus::Interrupted;
393 a.finished_at = Some(now);
394 }
395 run.updated_at = now;
396 }
397 }
398 Ok(())
399 })
400 }
401
402 fn save_state_snapshot(
403 &self,
404 run_id: &str,
405 state: &HashMap<String, Value>,
406 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
407 let run_id = run_id.to_string();
408 let state = state.clone();
409 Box::pin(async move {
410 if let Some(run) = self.runs.write().await.get_mut(&run_id) {
411 run.state_snapshot = state;
412 run.updated_at = chrono::Utc::now();
413 }
414 Ok(())
415 })
416 }
417
418 fn load_run(
419 &self,
420 run_id: &str,
421 ) -> Pin<Box<dyn Future<Output = Result<Option<RunState>>> + Send + '_>> {
422 let run_id = run_id.to_string();
423 Box::pin(async move { Ok(self.runs.read().await.get(&run_id).cloned()) })
424 }
425
426 fn complete_run(&self, run_id: &str) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
427 let run_id = run_id.to_string();
428 Box::pin(async move {
429 let mut runs = self.runs.write().await;
430 let run = runs
431 .get_mut(&run_id)
432 .ok_or_else(|| crate::AgentGraphError::RunNotFound(run_id.clone()))?;
433 if run.status != RunStatus::Running {
434 return Err(crate::AgentGraphError::TerminalStateConflict(run_id));
435 }
436 run.status = RunStatus::Completed;
437 run.updated_at = chrono::Utc::now();
438 Ok(())
439 })
440 }
441
442 fn fail_run(
443 &self,
444 run_id: &str,
445 _error: &str,
446 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
447 let run_id = run_id.to_string();
448 Box::pin(async move {
449 let mut runs = self.runs.write().await;
450 let run = runs
451 .get_mut(&run_id)
452 .ok_or_else(|| crate::AgentGraphError::RunNotFound(run_id.clone()))?;
453 if run.status != RunStatus::Running {
454 return Err(crate::AgentGraphError::TerminalStateConflict(run_id));
455 }
456 run.status = RunStatus::Failed;
457 run.updated_at = chrono::Utc::now();
458 Ok(())
459 })
460 }
461}