1use crate::cook::workflow::checkpoint_errors::CheckpointError;
6use crate::cook::workflow::checkpoint_path::CheckpointStorage;
7use crate::cook::workflow::executor::WorkflowContext;
8use crate::cook::workflow::normalized::NormalizedWorkflow;
9use crate::cook::workflow::variable_checkpoint::VariableCheckpointState;
10use anyhow::{Context, Result};
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::{HashMap, HashSet};
15use std::path::PathBuf;
16use std::time::Duration;
17use tokio::fs;
18use tracing::{debug, info, warn};
19
20const DEFAULT_CHECKPOINT_INTERVAL: Duration = Duration::from_secs(60);
22
23pub const CHECKPOINT_VERSION: u32 = 1;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct WorkflowCheckpoint {
29 pub workflow_id: String,
31 pub execution_state: ExecutionState,
33 pub completed_steps: Vec<CompletedStep>,
35 pub variable_state: HashMap<String, Value>,
37 pub mapreduce_state: Option<MapReduceCheckpoint>,
39 pub timestamp: DateTime<Utc>,
41 pub version: u32,
43 pub workflow_hash: String,
45 pub total_steps: usize,
47 pub workflow_name: Option<String>,
49 pub workflow_path: Option<PathBuf>,
51 #[serde(skip)]
53 pub error_recovery_state: Option<crate::cook::workflow::error_recovery::ErrorRecoveryState>,
54 pub retry_checkpoint_state: Option<crate::cook::retry_state::RetryCheckpointState>,
56 pub variable_checkpoint_state: Option<VariableCheckpointState>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ExecutionState {
63 pub current_step_index: usize,
65 pub total_steps: usize,
67 pub status: WorkflowStatus,
69 pub start_time: DateTime<Utc>,
71 pub last_checkpoint: DateTime<Utc>,
73 pub current_iteration: Option<usize>,
75 pub total_iterations: Option<usize>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub enum WorkflowStatus {
82 Running,
84 Paused,
86 Completed,
88 Failed,
90 Interrupted,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct CompletedStep {
97 pub step_index: usize,
99 pub command: String,
101 pub success: bool,
103 pub output: Option<String>,
105 pub captured_variables: HashMap<String, String>,
107 pub duration: Duration,
109 pub completed_at: DateTime<Utc>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub retry_state: Option<RetryState>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct RetryState {
119 pub current_attempt: usize,
121 pub max_attempts: usize,
123 pub failure_history: Vec<String>,
125 pub in_retry_loop: bool,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct MapReduceCheckpoint {
132 pub completed_items: HashSet<String>,
134 pub failed_items: Vec<String>,
136 pub in_progress_items: HashMap<String, AgentState>,
138 pub reduce_completed: bool,
140 pub agent_results: HashMap<String, Value>,
142 pub total_items: usize,
144 pub aggregate_variables: HashMap<String, String>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct AgentState {
151 pub agent_id: String,
153 pub item_id: String,
155 pub started_at: DateTime<Utc>,
157 pub last_update: DateTime<Utc>,
159}
160
161#[derive(Debug, Clone)]
163pub struct ResumeContext {
164 pub skip_steps: Vec<CompletedStep>,
166 pub variable_state: HashMap<String, Value>,
168 pub mapreduce_state: Option<MapReduceCheckpoint>,
170 pub start_from_step: usize,
172 pub resume_iteration: Option<usize>,
174 pub checkpoint: Option<Box<WorkflowCheckpoint>>,
176}
177
178#[derive(Debug, Clone, Default)]
180pub struct ResumeOptions {
181 pub force: bool,
183 pub from_step: Option<usize>,
185 pub reset_failures: bool,
187 pub skip_validation: bool,
189}
190
191pub struct CheckpointManager {
211 storage: CheckpointStorage,
213 checkpoint_interval: Duration,
215 enabled: bool,
217}
218
219impl CheckpointManager {
220 pub fn with_storage(storage: CheckpointStorage) -> Self {
228 Self {
229 storage,
230 checkpoint_interval: DEFAULT_CHECKPOINT_INTERVAL,
231 enabled: true,
232 }
233 }
234
235 pub fn with_interval(mut self, interval: Duration) -> Self {
247 self.checkpoint_interval = interval;
248 self
249 }
250
251 pub fn with_enabled(mut self, enabled: bool) -> Self {
262 self.enabled = enabled;
263 self
264 }
265
266 #[deprecated(
291 since = "0.10.0",
292 note = "Use CheckpointManager::with_storage() with explicit storage strategy instead"
293 )]
294 pub fn new(storage_path: PathBuf) -> Self {
295 Self {
296 storage: CheckpointStorage::Local(storage_path),
297 checkpoint_interval: DEFAULT_CHECKPOINT_INTERVAL,
298 enabled: true,
299 }
300 }
301
302 #[deprecated(
327 since = "0.10.0",
328 note = "Use .with_interval().with_enabled() builder pattern instead"
329 )]
330 pub fn configure(&mut self, interval: Duration, enabled: bool) {
331 self.checkpoint_interval = interval;
332 self.enabled = enabled;
333 }
334
335 pub async fn save_checkpoint(&self, checkpoint: &WorkflowCheckpoint) -> Result<()> {
337 if !self.enabled {
338 return Ok(());
339 }
340
341 let checkpoint_path = self
343 .storage
344 .checkpoint_file_path(&checkpoint.workflow_id)
345 .context("Failed to resolve checkpoint path")?;
346 let temp_path = checkpoint_path.with_extension("tmp");
347
348 ensure_checkpoint_dir_exists(&checkpoint_path).await?;
350
351 write_checkpoint_atomically(&checkpoint_path, &temp_path, checkpoint).await?;
353
354 info!(
355 "Saved checkpoint for workflow {} at step {}",
356 checkpoint.workflow_id, checkpoint.execution_state.current_step_index
357 );
358
359 Ok(())
360 }
361
362 pub async fn save_intervention_request(&self, workflow_id: &str, message: &str) -> Result<()> {
364 let mut checkpoint = self.load_checkpoint(workflow_id).await?;
366
367 checkpoint.variable_state.insert(
369 "__intervention_required".to_string(),
370 serde_json::Value::String(message.to_string()),
371 );
372 checkpoint.variable_state.insert(
373 "__intervention_timestamp".to_string(),
374 serde_json::Value::String(chrono::Utc::now().to_rfc3339()),
375 );
376
377 self.save_checkpoint(&checkpoint).await?;
379
380 info!(
381 "Saved intervention request for workflow {}: {}",
382 workflow_id, message
383 );
384
385 Ok(())
386 }
387
388 pub async fn load_checkpoint(&self, workflow_id: &str) -> Result<WorkflowCheckpoint> {
390 let checkpoint_path = self
391 .storage
392 .checkpoint_file_path(workflow_id)
393 .context("Failed to resolve checkpoint path")?;
394
395 if !checkpoint_path.exists() {
397 let checkpoint_dir = self
398 .storage
399 .resolve_base_dir()
400 .context("Failed to resolve checkpoint directory")?;
401
402 return Err(CheckpointError::not_found(workflow_id.to_string(), checkpoint_dir).into());
403 }
404
405 let content =
406 fs::read_to_string(&checkpoint_path)
407 .await
408 .map_err(|e| CheckpointError::IoError {
409 operation: "read checkpoint file".to_string(),
410 path: Some(checkpoint_path.clone()),
411 source: e,
412 })?;
413
414 let checkpoint: WorkflowCheckpoint =
415 serde_json::from_str(&content).map_err(|e| -> anyhow::Error {
416 CheckpointError::InvalidCheckpoint {
417 reason: format!("Failed to parse checkpoint: {}", e),
418 session_id: workflow_id.to_string(),
419 }
420 .into()
421 })?;
422
423 if checkpoint.version > CHECKPOINT_VERSION {
425 return Err(CheckpointError::version_mismatch(
426 checkpoint.version,
427 CHECKPOINT_VERSION,
428 checkpoint_path,
429 Some(checkpoint.timestamp),
430 )
431 .into());
432 }
433
434 Ok(checkpoint)
435 }
436
437 pub async fn should_checkpoint(&self, last_checkpoint: DateTime<Utc>) -> bool {
439 if !self.enabled {
440 return false;
441 }
442
443 let elapsed = Utc::now().signed_duration_since(last_checkpoint);
444 elapsed.num_seconds() as u64 >= self.checkpoint_interval.as_secs()
445 }
446
447 pub async fn delete_checkpoint(&self, workflow_id: &str) -> Result<()> {
449 let checkpoint_path = self
450 .storage
451 .checkpoint_file_path(workflow_id)
452 .context("Failed to resolve checkpoint path")?;
453 if checkpoint_path.exists() {
454 fs::remove_file(checkpoint_path)
455 .await
456 .context("Failed to delete checkpoint")?;
457 debug!("Deleted checkpoint for completed workflow {}", workflow_id);
458 }
459 Ok(())
460 }
461
462 pub async fn list_checkpoints(&self) -> Result<Vec<String>> {
464 let mut checkpoints = Vec::new();
465
466 let base_dir = self
467 .storage
468 .resolve_base_dir()
469 .context("Failed to resolve checkpoint base directory")?;
470
471 if !base_dir.exists() {
472 return Ok(checkpoints);
473 }
474
475 let mut entries = fs::read_dir(&base_dir).await?;
476 while let Some(entry) = entries.next_entry().await? {
477 if let Some(name) = entry.file_name().to_str() {
478 if name.ends_with(".checkpoint.json") {
479 if let Some(workflow_id) = name.strip_suffix(".checkpoint.json") {
480 checkpoints.push(workflow_id.to_string());
481 }
482 }
483 }
484 }
485
486 Ok(checkpoints)
487 }
488
489 pub fn validate_checkpoint(
491 checkpoint: &WorkflowCheckpoint,
492 workflow_hash: &str,
493 workflow_path: Option<&PathBuf>,
494 current_steps: usize,
495 ) -> Result<()> {
496 if checkpoint.workflow_hash != workflow_hash {
498 warn!("Workflow has changed since checkpoint was created");
499
500 if let Some(path) = workflow_path {
502 return Err(CheckpointError::workflow_hash_mismatch(
503 checkpoint.workflow_hash.clone(),
504 workflow_hash.to_string(),
505 checkpoint.total_steps,
506 current_steps,
507 checkpoint.workflow_id.clone(),
508 path.clone(),
509 Some(checkpoint.timestamp),
510 )
511 .into());
512 }
513 }
514
515 if checkpoint.execution_state.current_step_index > checkpoint.execution_state.total_steps {
517 return Err(CheckpointError::InvalidCheckpoint {
518 reason: format!(
519 "Step index {} exceeds total steps {}",
520 checkpoint.execution_state.current_step_index,
521 checkpoint.execution_state.total_steps
522 ),
523 session_id: checkpoint.workflow_id.clone(),
524 }
525 .into());
526 }
527
528 Ok(())
529 }
530}
531
532pub fn create_checkpoint(
534 workflow_id: String,
535 workflow: &NormalizedWorkflow,
536 context: &WorkflowContext,
537 completed_steps: Vec<CompletedStep>,
538 current_step: usize,
539 workflow_hash: String,
540) -> WorkflowCheckpoint {
541 create_checkpoint_with_total_steps(
542 workflow_id,
543 workflow,
544 context,
545 completed_steps,
546 current_step,
547 workflow_hash,
548 workflow.steps.len(),
549 )
550}
551
552pub fn create_checkpoint_with_total_steps(
554 workflow_id: String,
555 workflow: &NormalizedWorkflow,
556 context: &WorkflowContext,
557 completed_steps: Vec<CompletedStep>,
558 current_step: usize,
559 workflow_hash: String,
560 total_steps: usize,
561) -> WorkflowCheckpoint {
562 let mut variable_state = HashMap::new();
564 for (key, value) in &context.variables {
565 variable_state.insert(key.clone(), Value::String(value.clone()));
566 }
567 for (key, value) in &context.captured_outputs {
568 variable_state.insert(key.clone(), Value::String(value.clone()));
569 }
570
571 let variable_checkpoint_state = {
573 use crate::cook::workflow::variable_checkpoint::VariableResumeManager;
574 let manager = VariableResumeManager::new();
575 manager
576 .create_checkpoint(
577 &context.variables,
578 &context.captured_outputs,
579 &context.iteration_vars,
580 &context.variable_store,
581 )
582 .ok()
583 };
584
585 WorkflowCheckpoint {
586 workflow_id,
587 execution_state: ExecutionState {
588 current_step_index: current_step,
589 total_steps,
590 status: WorkflowStatus::Running,
591 start_time: Utc::now(),
592 last_checkpoint: Utc::now(),
593 current_iteration: None,
594 total_iterations: None,
595 },
596 completed_steps,
597 variable_state,
598 mapreduce_state: None,
599 timestamp: Utc::now(),
600 version: CHECKPOINT_VERSION,
601 workflow_hash,
602 total_steps,
603 workflow_name: Some(workflow.name.to_string()),
604 workflow_path: None, error_recovery_state: None, retry_checkpoint_state: None, variable_checkpoint_state,
608 }
609}
610
611pub fn create_completion_checkpoint(
616 workflow_id: String,
617 workflow: &NormalizedWorkflow,
618 context: &WorkflowContext,
619 completed_steps: Vec<CompletedStep>,
620 current_step_index: usize,
621 workflow_hash: String,
622) -> Result<WorkflowCheckpoint> {
623 let mut checkpoint = create_checkpoint(
624 workflow_id,
625 workflow,
626 context,
627 completed_steps,
628 current_step_index,
629 workflow_hash,
630 );
631
632 checkpoint.execution_state.status = WorkflowStatus::Completed;
633 Ok(checkpoint)
634}
635
636pub fn create_error_checkpoint(
646 workflow_id: String,
647 workflow: &NormalizedWorkflow,
648 context: &WorkflowContext,
649 completed_steps: Vec<CompletedStep>,
650 workflow_hash: String,
651 error: &anyhow::Error,
652 failed_step_index: usize,
653) -> Result<WorkflowCheckpoint> {
654 let mut checkpoint = create_checkpoint(
655 workflow_id,
656 workflow,
657 context,
658 completed_steps,
659 failed_step_index,
660 workflow_hash,
661 );
662
663 checkpoint.execution_state.status = WorkflowStatus::Failed;
665
666 checkpoint.variable_state.insert(
668 "__error_message".to_string(),
669 Value::String(error.to_string()),
670 );
671 checkpoint.variable_state.insert(
672 "__failed_step_index".to_string(),
673 Value::Number(failed_step_index.into()),
674 );
675 checkpoint.variable_state.insert(
676 "__error_timestamp".to_string(),
677 Value::String(Utc::now().to_rfc3339()),
678 );
679
680 Ok(checkpoint)
681}
682
683pub fn build_resume_context(checkpoint: WorkflowCheckpoint) -> ResumeContext {
685 let completed_steps = checkpoint.completed_steps.clone();
686 let variable_state = checkpoint.variable_state.clone();
687 let mapreduce_state = checkpoint.mapreduce_state.clone();
688 let start_from_step = checkpoint.execution_state.current_step_index;
689 let resume_iteration = checkpoint.execution_state.current_iteration;
690
691 ResumeContext {
692 skip_steps: completed_steps,
693 variable_state,
694 mapreduce_state,
695 start_from_step,
696 resume_iteration,
697 checkpoint: Some(Box::new(checkpoint)),
698 }
699}
700
701fn serialize_checkpoint(checkpoint: &WorkflowCheckpoint) -> Result<String> {
710 serde_json::to_string_pretty(checkpoint).context("Failed to serialize checkpoint to JSON")
711}
712
713async fn ensure_checkpoint_dir_exists(checkpoint_path: &std::path::Path) -> Result<()> {
721 if let Some(parent) = checkpoint_path.parent() {
722 fs::create_dir_all(parent)
723 .await
724 .context("Failed to create checkpoint directory")?;
725 }
726 Ok(())
727}
728
729async fn write_checkpoint_atomically(
734 final_path: &std::path::Path,
735 temp_path: &std::path::Path,
736 checkpoint: &WorkflowCheckpoint,
737) -> Result<()> {
738 let json = serialize_checkpoint(checkpoint)?;
740
741 fs::write(temp_path, json)
743 .await
744 .context("Failed to write checkpoint to temp file")?;
745
746 fs::rename(temp_path, final_path)
748 .await
749 .context("Failed to move checkpoint to final location")
750}