use super::checkpoint::{RetryState as CheckpointRetryState, WorkflowCheckpoint};
use super::executor::WorkflowContext;
use super::on_failure::{HandlerCommand, HandlerStrategy, OnFailureConfig};
use crate::cook::execution::{CommandExecutor, ExecutionContext};
use crate::cook::expression::{ExpressionEvaluator, VariableContext};
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, error, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorRecoveryState {
pub active_handlers: Vec<ErrorHandler>,
pub error_context: HashMap<String, Value>,
pub handler_execution_history: Vec<HandlerExecution>,
pub retry_state: Option<CheckpointRetryState>,
pub correlation_id: String,
pub recovery_attempts: usize,
pub max_recovery_attempts: usize,
}
impl Default for ErrorRecoveryState {
fn default() -> Self {
Self {
active_handlers: Vec::new(),
error_context: HashMap::new(),
handler_execution_history: Vec::new(),
retry_state: None,
correlation_id: uuid::Uuid::new_v4().to_string(),
recovery_attempts: 0,
max_recovery_attempts: 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorHandler {
pub id: String,
pub condition: Option<String>,
pub commands: Vec<HandlerCommand>,
pub retry_config: Option<RetryConfig>,
pub timeout: Option<Duration>,
pub scope: ErrorHandlerScope,
pub strategy: HandlerStrategy,
#[serde(default)]
pub capture: HashMap<String, String>,
#[serde(default)]
pub handler_failure_fatal: bool,
#[serde(default)]
pub fail_workflow: bool,
#[serde(default)]
pub retry_original: bool,
#[serde(default)]
pub max_retries: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryConfig {
pub max_attempts: usize,
pub initial_delay: Duration,
pub max_delay: Duration,
pub backoff_multiplier: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ErrorHandlerScope {
Command,
Step,
Phase,
Workflow,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandlerExecution {
pub handler_id: String,
pub executed_at: DateTime<Utc>,
pub success: bool,
pub error: Option<String>,
pub retry_attempt: usize,
pub duration: Duration,
}
#[derive(Debug, Clone)]
pub enum RecoveryAction {
Retry {
delay: Duration,
max_attempts: usize,
},
Fallback {
alternative_path: String,
},
PartialResume {
from_step: usize,
},
RequestIntervention {
message: String,
},
SafeAbort {
cleanup_actions: Vec<HandlerCommand>,
},
Continue,
}
#[derive(Debug)]
pub enum ResumeError {
CorruptedCheckpoint(String),
MissingDependency(String),
EnvironmentMismatch(String),
HandlerExecutionFailed(String),
RecoveryLimitExceeded,
Other(anyhow::Error),
}
impl std::fmt::Display for ResumeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResumeError::CorruptedCheckpoint(msg) => write!(f, "Corrupted checkpoint: {}", msg),
ResumeError::MissingDependency(dep) => write!(f, "Missing dependency: {}", dep),
ResumeError::EnvironmentMismatch(msg) => write!(f, "Environment mismatch: {}", msg),
ResumeError::HandlerExecutionFailed(msg) => {
write!(f, "Handler execution failed: {}", msg)
}
ResumeError::RecoveryLimitExceeded => write!(f, "Recovery limit exceeded"),
ResumeError::Other(err) => write!(f, "Other error: {}", err),
}
}
}
impl std::error::Error for ResumeError {}
pub struct ResumeErrorRecovery {
pub(crate) recovery_state: ErrorRecoveryState,
command_executor: Option<Arc<dyn CommandExecutor>>,
}
impl Default for ResumeErrorRecovery {
fn default() -> Self {
Self::new()
}
}
impl ResumeErrorRecovery {
pub fn new() -> Self {
Self {
recovery_state: ErrorRecoveryState::default(),
command_executor: None,
}
}
pub fn with_executor(mut self, executor: Arc<dyn CommandExecutor>) -> Self {
self.command_executor = Some(executor);
self
}
pub async fn restore_error_handlers(
&mut self,
checkpoint: &WorkflowCheckpoint,
) -> Result<Vec<ErrorHandler>> {
info!("Restoring error handlers from checkpoint");
let handlers = if let Some(recovery_state) = self.extract_recovery_state(checkpoint)? {
self.recovery_state = recovery_state;
self.recovery_state.active_handlers.clone()
} else {
Vec::new()
};
self.validate_error_handlers(&handlers).await?;
self.restore_error_context(checkpoint).await?;
info!("Restored {} error handlers", handlers.len());
Ok(handlers)
}
pub async fn handle_resume_error(
&mut self,
error: &ResumeError,
checkpoint: &WorkflowCheckpoint,
) -> Result<RecoveryAction> {
warn!("Handling resume error: {}", error);
if self.recovery_state.recovery_attempts >= self.recovery_state.max_recovery_attempts {
error!("Recovery attempt limit exceeded");
return Ok(RecoveryAction::SafeAbort {
cleanup_actions: Vec::new(),
});
}
self.recovery_state.recovery_attempts += 1;
match error {
ResumeError::CorruptedCheckpoint(msg) => {
warn!("Attempting checkpoint repair: {}", msg);
self.attempt_checkpoint_repair(checkpoint).await
}
ResumeError::MissingDependency(dep) => {
warn!("Resolving missing dependency: {}", dep);
self.resolve_missing_dependencies(dep).await
}
ResumeError::EnvironmentMismatch(msg) => {
warn!("Adapting to environment changes: {}", msg);
self.adapt_to_environment_changes(msg).await
}
ResumeError::HandlerExecutionFailed(msg) => {
warn!("Handler execution failed: {}", msg);
self.handle_handler_failure(msg).await
}
ResumeError::RecoveryLimitExceeded => {
error!("Recovery limit already exceeded");
Ok(RecoveryAction::SafeAbort {
cleanup_actions: Vec::new(),
})
}
ResumeError::Other(e) => {
warn!("Applying default recovery for: {}", e);
self.default_error_recovery(e).await
}
}
}
pub async fn execute_error_handler_with_resume_context(
&mut self,
handler: &ErrorHandler,
error_msg: &str,
workflow_context: &mut WorkflowContext,
) -> Result<bool> {
info!(
"Executing error handler {} with strategy {:?}",
handler.id, handler.strategy
);
self.recovery_state.error_context.insert(
"error.message".to_string(),
Value::String(error_msg.to_string()),
);
self.recovery_state.error_context.insert(
"error.correlation_id".to_string(),
Value::String(self.recovery_state.correlation_id.clone()),
);
self.recovery_state.error_context.insert(
"error.handler".to_string(),
Value::String(handler.id.clone()),
);
self.recovery_state.error_context.insert(
"error.attempt".to_string(),
Value::Number(serde_json::Number::from(
self.recovery_state.recovery_attempts,
)),
);
if let Some(ref condition) = handler.condition {
debug!("Evaluating handler condition: {}", condition);
if !self.evaluate_handler_condition(condition, workflow_context)? {
info!("Handler condition not met, skipping handler");
return Ok(false);
}
}
let start_time = std::time::Instant::now();
let mut success = true;
let handler_future = async {
for command in &handler.commands {
match self
.execute_handler_command(command, workflow_context)
.await
{
Ok(_) => {
debug!("Handler command executed successfully");
}
Err(e) => {
warn!("Handler command failed: {}", e);
if !command.continue_on_error {
success = false;
break;
}
}
}
}
success
};
success = if let Some(timeout) = handler.timeout {
match tokio::time::timeout(timeout, handler_future).await {
Ok(result) => result,
Err(_) => {
error!("Handler {} timed out after {:?}", handler.id, timeout);
false
}
}
} else {
handler_future.await
};
let execution = HandlerExecution {
handler_id: handler.id.clone(),
executed_at: Utc::now(),
success,
error: if success {
None
} else {
Some(error_msg.to_string())
},
retry_attempt: self.recovery_state.recovery_attempts,
duration: start_time.elapsed(),
};
self.recovery_state
.handler_execution_history
.push(execution);
Ok(success)
}
fn extract_recovery_state(
&self,
checkpoint: &WorkflowCheckpoint,
) -> Result<Option<ErrorRecoveryState>> {
if let Some(value) = checkpoint.variable_state.get("__error_recovery_state") {
match serde_json::from_value::<ErrorRecoveryState>(value.clone()) {
Ok(state) => Ok(Some(state)),
Err(e) => {
warn!("Failed to deserialize recovery state: {}", e);
Ok(None)
}
}
} else {
Ok(None)
}
}
async fn validate_error_handlers(&self, handlers: &[ErrorHandler]) -> Result<()> {
for handler in handlers {
if let Some(ref condition) = handler.condition {
debug!("Validating handler condition: {}", condition);
let evaluator = ExpressionEvaluator::new();
let context = VariableContext::new();
if let Err(e) = evaluator.evaluate(condition, &context) {
warn!("Handler condition '{}' has syntax error: {}", condition, e);
}
}
for command in &handler.commands {
if command.shell.is_none() && command.claude.is_none() {
return Err(anyhow!(
"Invalid handler command: no shell or claude command specified"
));
}
}
}
Ok(())
}
async fn restore_error_context(&mut self, checkpoint: &WorkflowCheckpoint) -> Result<()> {
if let Some(retry_state) = checkpoint
.completed_steps
.last()
.and_then(|step| step.retry_state.clone())
{
self.recovery_state.retry_state = Some(retry_state);
}
Ok(())
}
async fn attempt_checkpoint_repair(
&self,
checkpoint: &WorkflowCheckpoint,
) -> Result<RecoveryAction> {
info!("Attempting checkpoint repair");
if checkpoint.completed_steps.is_empty() {
warn!("No completed steps in checkpoint, starting from beginning");
return Ok(RecoveryAction::PartialResume { from_step: 0 });
}
let last_good_step = checkpoint
.completed_steps
.iter()
.rposition(|step| step.success)
.unwrap_or(0);
info!("Resuming from last known good step: {}", last_good_step);
Ok(RecoveryAction::PartialResume {
from_step: last_good_step,
})
}
async fn resolve_missing_dependencies(&self, dependency: &str) -> Result<RecoveryAction> {
info!("Attempting to resolve missing dependency: {}", dependency);
if dependency.starts_with("claude:") || dependency.starts_with("shell:") {
return Ok(RecoveryAction::RequestIntervention {
message: format!("Missing command dependency: {}. Please ensure the command is available and retry.", dependency),
});
}
Ok(RecoveryAction::RequestIntervention {
message: format!(
"Missing dependency: {}. Please install or configure the dependency and retry.",
dependency
),
})
}
async fn adapt_to_environment_changes(&self, issue: &str) -> Result<RecoveryAction> {
info!("Adapting to environment changes: {}", issue);
warn!("Environment has changed since checkpoint. Attempting to continue with current environment.");
Ok(RecoveryAction::Continue)
}
async fn handle_handler_failure(&mut self, msg: &str) -> Result<RecoveryAction> {
warn!("Handler execution failed: {}", msg);
if let Some(ref mut retry_state) = self.recovery_state.retry_state {
if retry_state.current_attempt < retry_state.max_attempts {
retry_state.current_attempt += 1;
let delay = Duration::from_secs(2_u64.pow(retry_state.current_attempt as u32));
return Ok(RecoveryAction::Retry {
delay,
max_attempts: retry_state.max_attempts,
});
}
}
Ok(RecoveryAction::RequestIntervention {
message: format!(
"Error handler failed: {}. Manual intervention required.",
msg
),
})
}
async fn default_error_recovery(&self, error: &anyhow::Error) -> Result<RecoveryAction> {
warn!("Applying default error recovery: {}", error);
Ok(RecoveryAction::Retry {
delay: Duration::from_secs(5),
max_attempts: 3,
})
}
fn evaluate_handler_condition(
&self,
condition: &str,
workflow_context: &WorkflowContext,
) -> Result<bool> {
let evaluator = ExpressionEvaluator::new();
let mut var_context = VariableContext::new();
for (key, value) in &workflow_context.variables {
var_context.set_string(key.clone(), value.clone());
}
for (key, value) in &self.recovery_state.error_context {
match value {
Value::String(s) => var_context.set_string(key.clone(), s.clone()),
Value::Number(n) => {
if let Some(f) = n.as_f64() {
var_context.set_number(key.clone(), f);
}
}
Value::Bool(b) => var_context.set_bool(key.clone(), *b),
_ => {} }
}
var_context.set_number(
"recovery.attempts".to_string(),
self.recovery_state.recovery_attempts as f64,
);
var_context.set_number(
"recovery.max_attempts".to_string(),
self.recovery_state.max_recovery_attempts as f64,
);
evaluator.evaluate(condition, &var_context)
}
async fn execute_handler_command(
&self,
command: &HandlerCommand,
workflow_context: &mut WorkflowContext,
) -> Result<()> {
let executor = self
.command_executor
.as_ref()
.ok_or_else(|| anyhow!("Command executor not configured for error recovery"))?;
let mut context = ExecutionContext {
working_directory: std::env::current_dir()?,
capture_output: true,
..ExecutionContext::default()
};
for (key, value) in &self.recovery_state.error_context {
if let Value::String(s) = value {
context.env_vars.insert(key.clone(), s.clone());
}
}
if let Some(ref shell_cmd) = command.shell {
info!("Executing shell handler: {}", shell_cmd);
let interpolated_cmd = workflow_context.interpolate(shell_cmd);
let result = executor
.execute(
"sh",
&["-c".to_string(), interpolated_cmd.clone()],
context.clone(),
)
.await?;
if !result.success {
let error_msg = format!(
"Shell handler command failed: {} (exit code: {:?})",
interpolated_cmd, result.exit_code
);
if !command.continue_on_error {
return Err(anyhow!(error_msg));
}
warn!("{}", error_msg);
} else {
debug!("Shell handler command succeeded");
if !result.stdout.is_empty() {
workflow_context.variables.insert(
"handler.output".to_string(),
result.stdout.trim().to_string(),
);
}
}
}
if let Some(ref claude_cmd) = command.claude {
info!("Executing claude handler: {}", claude_cmd);
let interpolated_cmd = workflow_context.interpolate(claude_cmd);
let result = executor
.execute("claude", std::slice::from_ref(&interpolated_cmd), context)
.await?;
if !result.success {
let error_msg = format!(
"Claude handler command failed: {} (exit code: {:?})",
interpolated_cmd, result.exit_code
);
if !command.continue_on_error {
return Err(anyhow!(error_msg));
}
warn!("{}", error_msg);
} else {
debug!("Claude handler command succeeded");
if !result.stdout.is_empty() {
workflow_context.variables.insert(
"handler.output".to_string(),
result.stdout.trim().to_string(),
);
}
}
}
Ok(())
}
}
pub fn on_failure_to_error_handler(
on_failure: &OnFailureConfig,
step_index: usize,
) -> Option<ErrorHandler> {
let commands = on_failure.handler_commands();
if commands.is_empty() {
return None;
}
let strategy = on_failure.strategy();
let handler_failure_fatal = on_failure.handler_failure_fatal();
let fail_workflow = on_failure.should_fail_workflow();
let max_retries = on_failure.max_retries();
let retry_original = on_failure.should_retry();
let timeout = on_failure.handler_timeout().map(Duration::from_secs);
let capture = match on_failure {
OnFailureConfig::Detailed(config) => config.capture.clone(),
_ => HashMap::new(),
};
let retry_config = if max_retries > 0 {
Some(RetryConfig {
max_attempts: max_retries as usize,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
})
} else {
None
};
Some(ErrorHandler {
id: format!("step_{}_handler", step_index),
condition: None,
commands,
retry_config,
timeout,
scope: ErrorHandlerScope::Step,
strategy,
capture,
handler_failure_fatal,
fail_workflow,
retry_original,
max_retries,
})
}
pub fn save_recovery_state_to_checkpoint(
checkpoint: &mut WorkflowCheckpoint,
recovery_state: &ErrorRecoveryState,
) {
checkpoint.variable_state.insert(
"__error_recovery_state".to_string(),
serde_json::to_value(recovery_state).unwrap_or(Value::Null),
);
}
pub fn load_recovery_state_from_checkpoint(
checkpoint: &WorkflowCheckpoint,
) -> Option<ErrorRecoveryState> {
checkpoint
.variable_state
.get("__error_recovery_state")
.and_then(|v| serde_json::from_value(v.clone()).ok())
}