use super::commit_validator::{CommitValidationResult, CommitValidator};
use super::state_machine::{apply_transition, state_to_result};
use super::types::{AgentHandle, AgentLifecycleState, AgentResult, AgentTransition};
use crate::abstractions::git::GitOperations;
use crate::commands::attributes::AttributeValue;
use crate::commands::{CommandRegistry, ExecutionContext as CommandExecutionContext};
use crate::cook::error::ResultExt;
use crate::cook::execution::dlq::DeadLetterQueue;
use crate::cook::execution::interpolation::InterpolationContext;
use crate::cook::execution::progress::{AgentProgress, EnhancedProgressTracker};
use crate::cook::orchestrator::ExecutionEnvironment;
use crate::cook::workflow::{StepResult, WorkflowStep};
use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::{debug, error, warn};
#[derive(Debug, thiserror::Error)]
pub enum ExecutionError {
#[error("Command execution failed: {0}")]
CommandFailed(String),
#[error("Timeout occurred after {0} seconds")]
Timeout(u64),
#[error("Interpolation failed: {0}")]
InterpolationError(String),
#[error("Worktree operation failed: {0}")]
WorktreeError(String),
#[error("Agent execution failed: {0}")]
AgentError(String),
#[error("Commit validation failed for agent {0}: {0}")]
CommitValidationFailed(Box<CommitValidationError>),
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("Command '{command}' (step {step_index}) did not create required commits. Branch still at {base_commit}. Worktree: {worktree_path}")]
pub struct CommitValidationError {
pub agent_id: String,
pub item_id: String,
pub step_index: usize,
pub command: String,
pub base_commit: String,
pub worktree_path: String,
}
pub type ExecutionResult<T> = Result<T, ExecutionError>;
#[derive(Debug, Clone, Copy)]
pub enum ExecutionStrategy {
Standard,
Enhanced,
}
#[async_trait]
pub trait AgentExecutor: Send + Sync {
async fn execute(
&self,
handle: &AgentHandle,
item: &Value,
env: &ExecutionEnvironment,
context: ExecutionContext,
) -> ExecutionResult<AgentResult>;
async fn execute_with_retry(
&self,
handle: &AgentHandle,
item: &Value,
env: &ExecutionEnvironment,
context: ExecutionContext,
max_retries: u32,
) -> ExecutionResult<AgentResult>;
}
#[derive(Clone)]
pub struct ExecutionContext {
pub agent_index: usize,
pub progress_tracker: Option<Arc<AgentProgress>>,
pub event_logger: Option<Arc<crate::cook::execution::events::EventLogger>>,
pub dlq: Option<Arc<DeadLetterQueue>>,
pub attempt: u32,
pub previous_error: Option<String>,
pub strategy: ExecutionStrategy,
pub command_registry: Arc<CommandRegistry>,
pub enhanced_progress: Option<Arc<EnhancedProgressTracker>>,
pub git_operations: Arc<dyn GitOperations>,
}
pub struct StandardExecutor {
interpolation_engine: Arc<RwLock<crate::cook::execution::interpolation::InterpolationEngine>>,
}
impl StandardExecutor {
pub fn new() -> Self {
Self {
interpolation_engine: Arc::new(RwLock::new(
crate::cook::execution::interpolation::InterpolationEngine::new(false),
)),
}
}
fn get_step_display_name(step: &WorkflowStep) -> String {
if let Some(name) = &step.name {
name.clone()
} else if let Some(claude) = &step.claude {
format!("claude: {}", claude)
} else if let Some(shell) = &step.shell {
format!("shell: {}", shell)
} else {
"unknown command".to_string()
}
}
async fn execute_commands(
&self,
handle: &AgentHandle,
item: &Value,
env: &ExecutionEnvironment,
context: &ExecutionContext,
) -> ExecutionResult<(String, Vec<String>, Vec<String>, Option<String>)> {
let mut total_output = String::new();
let mut all_commits = Vec::new();
let all_files = Vec::new();
let mut json_log_location: Option<String> = None;
let commit_validator = CommitValidator::new(Arc::clone(&context.git_operations));
let interp_context = self.build_interpolation_context(item, &handle.config.item_id);
for (idx, step) in handle.commands.iter().enumerate() {
{
let mut state = handle.state.write().await;
state.update_progress(idx + 1, handle.commands.len());
state.set_operation(format!(
"Executing command {}/{}",
idx + 1,
handle.commands.len()
));
}
let head_before = if step.commit_required {
Some(
commit_validator
.get_head(handle.worktree_path())
.await
.with_context(|| {
format!(
"Getting HEAD before command execution in worktree {}",
handle.worktree_path().display()
)
})
.map_err(|e| ExecutionError::AgentError(e.to_string()))?,
)
} else {
None
};
let interpolated_step = self
.interpolate_workflow_step(step, &interp_context)
.await?;
let (result, log_location) = self
.execute_single_command(&interpolated_step, handle.worktree_path(), env, context)
.await?;
if log_location.is_some() {
json_log_location = log_location;
}
total_output.push_str(&result.stdout);
if !result.stderr.is_empty() {
total_output.push_str("\n[STDERR]: ");
total_output.push_str(&result.stderr);
}
if !result.success {
return Err(ExecutionError::CommandFailed(format!(
"Command {} failed with exit code {}",
idx + 1,
result.exit_code.unwrap_or(-1)
)));
}
if let Some(before_sha) = head_before {
let head_after = commit_validator
.get_head(handle.worktree_path())
.await
.with_context(|| {
format!(
"Getting HEAD after command execution in worktree {}",
handle.worktree_path().display()
)
})
.map_err(|e| ExecutionError::AgentError(e.to_string()))?;
let validation_result = commit_validator
.verify_commits_created(handle.worktree_path(), &before_sha, &head_after)
.await
.with_context(|| {
format!(
"Verifying commits created for step {} in worktree {}",
idx + 1,
handle.worktree_path().display()
)
})
.map_err(|e| ExecutionError::AgentError(e.to_string()))?;
match validation_result {
CommitValidationResult::NoCommits => {
return Err(ExecutionError::CommitValidationFailed(Box::new(
CommitValidationError {
agent_id: handle.config.id.clone(),
item_id: handle.config.item_id.clone(),
step_index: idx,
command: Self::get_step_display_name(step),
base_commit: before_sha,
worktree_path: handle.worktree_path().to_string_lossy().to_string(),
},
)));
}
CommitValidationResult::Valid { commits } => {
for commit in commits {
all_commits.push(commit.sha.clone());
}
debug!(
agent_id = %handle.config.id,
commits = ?all_commits,
"Commit validation passed"
);
}
}
}
}
Ok((total_output, all_commits, all_files, json_log_location))
}
fn build_interpolation_context(&self, item: &Value, item_id: &str) -> InterpolationContext {
let mut context = InterpolationContext::new();
context.variables.insert("item".to_string(), item.clone());
context
.variables
.insert("item_id".to_string(), Value::String(item_id.to_string()));
if let Some(obj) = item.as_object() {
for (key, value) in obj {
let key_path = format!("item.{}", key);
context.variables.insert(key_path, value.clone());
}
}
context
}
async fn interpolate_workflow_step(
&self,
step: &WorkflowStep,
context: &InterpolationContext,
) -> ExecutionResult<WorkflowStep> {
let mut engine = self.interpolation_engine.write().await;
let mut interpolated = step.clone();
if let Some(name) = &step.name {
interpolated.name = Some(
engine
.interpolate(name, context)
.with_context(|| format!("Interpolating step name '{}'", name))
.map_err(|e| ExecutionError::InterpolationError(e.to_string()))?,
);
}
if let Some(claude) = &step.claude {
interpolated.claude = Some(
engine
.interpolate(claude, context)
.with_context(|| format!("Interpolating claude command '{}'", claude))
.map_err(|e| ExecutionError::InterpolationError(e.to_string()))?,
);
}
if let Some(shell) = &step.shell {
interpolated.shell = Some(
engine
.interpolate(shell, context)
.with_context(|| format!("Interpolating shell command '{}'", shell))
.map_err(|e| ExecutionError::InterpolationError(e.to_string()))?,
);
}
Ok(interpolated)
}
fn create_initial_state(agent_id: String, work_item: Value) -> AgentLifecycleState {
AgentLifecycleState::Created {
agent_id,
work_item,
}
}
fn transition_to_running(
state: AgentLifecycleState,
worktree_path: PathBuf,
) -> Result<AgentLifecycleState, ExecutionError> {
let transition = AgentTransition::Start {
worktree_path: worktree_path.clone(),
};
apply_transition(state, transition)
.with_context(|| {
format!(
"Transitioning agent to running state in worktree {}",
worktree_path.display()
)
})
.map_err(|e| ExecutionError::AgentError(e.to_string()))
}
fn transition_to_completed(
state: AgentLifecycleState,
output: Option<String>,
commits: Vec<String>,
) -> Result<AgentLifecycleState, ExecutionError> {
let transition = AgentTransition::Complete {
output,
commits: commits.clone(),
};
apply_transition(state, transition)
.with_context(|| {
format!(
"Transitioning agent to completed state with {} commit(s)",
commits.len()
)
})
.map_err(|e| ExecutionError::AgentError(e.to_string()))
}
fn transition_to_failed(
state: AgentLifecycleState,
error: String,
json_log_location: Option<String>,
) -> Result<AgentLifecycleState, ExecutionError> {
let transition = AgentTransition::Fail {
error: error.clone(),
json_log_location: json_log_location.clone(),
};
apply_transition(state, transition)
.with_context(|| {
let log_info = json_log_location
.map(|l| format!(" (log: {})", l))
.unwrap_or_default();
format!("Transitioning agent to failed state: {}{}", error, log_info)
})
.map_err(|e| ExecutionError::AgentError(e.to_string()))
}
async fn execute_single_command(
&self,
step: &WorkflowStep,
worktree_path: &Path,
_env: &ExecutionEnvironment,
context: &ExecutionContext,
) -> ExecutionResult<(StepResult, Option<String>)> {
let mut exec_context = CommandExecutionContext::new(worktree_path.to_path_buf());
exec_context.env_vars = step.env.clone();
let result = if let Some(command) = &step.claude {
let mut attributes = HashMap::new();
attributes.insert(
"command".to_string(),
AttributeValue::String(command.clone()),
);
let cmd_result = context
.command_registry
.execute("claude", &exec_context, attributes)
.await;
if !cmd_result.success {
return Err(ExecutionError::CommandFailed(
cmd_result
.stderr
.unwrap_or_else(|| "Command failed".to_string()),
));
}
cmd_result
} else if let Some(command) = &step.shell {
let mut attributes = HashMap::new();
attributes.insert(
"command".to_string(),
AttributeValue::String(command.clone()),
);
let cmd_result = context
.command_registry
.execute("shell", &exec_context, attributes)
.await;
if !cmd_result.success {
return Err(ExecutionError::CommandFailed(
cmd_result
.stderr
.unwrap_or_else(|| "Command failed".to_string()),
));
}
cmd_result
} else {
return Err(ExecutionError::CommandFailed(
"No command specified in step".to_string(),
));
};
let json_log_location = result.json_log_location.clone();
Ok((
StepResult {
success: result.exit_code == Some(0),
stdout: result.stdout.unwrap_or_default(),
stderr: result.stderr.unwrap_or_default(),
exit_code: result.exit_code,
json_log_location: None,
},
json_log_location,
))
}
}
impl Default for StandardExecutor {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl AgentExecutor for StandardExecutor {
async fn execute(
&self,
handle: &AgentHandle,
item: &Value,
env: &ExecutionEnvironment,
context: ExecutionContext,
) -> ExecutionResult<AgentResult> {
let lifecycle_state =
Self::create_initial_state(handle.item_id().to_string(), item.clone());
let lifecycle_state =
Self::transition_to_running(lifecycle_state, handle.worktree_path().to_path_buf())?;
{
let mut state = handle.state.write().await;
state.status = super::types::AgentStateStatus::Executing;
}
let result = self.execute_commands(handle, item, env, &context).await;
let lifecycle_state = match result {
Ok((output, commits, files, json_log_location)) => {
let final_state = Self::transition_to_completed(
lifecycle_state,
Some(output.clone()),
commits.clone(),
)?;
{
let mut state = handle.state.write().await;
state.mark_completed();
}
let mut agent_result = state_to_result(&final_state).ok_or_else(|| {
ExecutionError::AgentError("State conversion failed".to_string())
})?;
agent_result.files_modified = files;
agent_result.worktree_path = Some(handle.worktree_path().to_path_buf());
agent_result.branch_name = Some(handle.config.branch_name.clone());
agent_result.worktree_session_id = Some(handle.worktree_session.name.clone());
agent_result.json_log_location = json_log_location;
agent_result
}
Err(e) => {
let final_state = Self::transition_to_failed(
lifecycle_state,
e.to_string(),
None, )?;
{
let mut state = handle.state.write().await;
state.mark_failed(e.to_string());
}
let mut agent_result = state_to_result(&final_state).ok_or_else(|| {
ExecutionError::AgentError("State conversion failed".to_string())
})?;
agent_result.worktree_path = Some(handle.worktree_path().to_path_buf());
agent_result.branch_name = Some(handle.config.branch_name.clone());
agent_result.worktree_session_id = Some(handle.worktree_session.name.clone());
agent_result
}
};
Ok(lifecycle_state)
}
async fn execute_with_retry(
&self,
handle: &AgentHandle,
item: &Value,
env: &ExecutionEnvironment,
mut context: ExecutionContext,
max_retries: u32,
) -> ExecutionResult<AgentResult> {
let mut attempt = 0;
let mut last_error = None;
loop {
attempt += 1;
context.attempt = attempt;
context.previous_error = last_error.clone();
if attempt > 1 {
let mut state = handle.state.write().await;
state.mark_retrying(attempt);
}
match self.execute(handle, item, env, context.clone()).await {
Ok(result) => return Ok(result),
Err(e) if attempt <= max_retries => {
last_error = Some(e.to_string());
warn!(
"Agent {} attempt {} failed: {}, retrying...",
handle.id(),
attempt,
e
);
tokio::time::sleep(Duration::from_secs(2)).await;
continue;
}
Err(e) => {
error!(
"Agent {} failed after {} attempts: {}",
handle.id(),
attempt,
e
);
return Err(e);
}
}
}
}
}
pub struct EnhancedProgressExecutor {
standard_executor: StandardExecutor,
}
impl EnhancedProgressExecutor {
pub fn new() -> Self {
Self {
standard_executor: StandardExecutor::new(),
}
}
}
impl Default for EnhancedProgressExecutor {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl AgentExecutor for EnhancedProgressExecutor {
async fn execute(
&self,
handle: &AgentHandle,
item: &Value,
env: &ExecutionEnvironment,
context: ExecutionContext,
) -> ExecutionResult<AgentResult> {
if let Some(progress) = &context.enhanced_progress {
progress
.update_agent_state(
&format!("agent-{}", context.agent_index),
crate::cook::execution::progress::AgentState::Running {
step: "Executing".to_string(),
progress: 0.0,
},
)
.await
.ok();
}
let result = self
.standard_executor
.execute(handle, item, env, context.clone())
.await;
if let Some(progress) = &context.enhanced_progress {
let state = if result.is_ok() {
crate::cook::execution::progress::AgentState::Completed
} else {
crate::cook::execution::progress::AgentState::Failed {
error: "Execution failed".to_string(),
}
};
progress
.update_agent_state(&format!("agent-{}", context.agent_index), state)
.await
.ok();
}
result
}
async fn execute_with_retry(
&self,
handle: &AgentHandle,
item: &Value,
env: &ExecutionEnvironment,
context: ExecutionContext,
max_retries: u32,
) -> ExecutionResult<AgentResult> {
self.standard_executor
.execute_with_retry(handle, item, env, context, max_retries)
.await
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
use serde_json::json;
use std::path::PathBuf;
#[test]
fn test_state_machine_integration_create_to_running() {
let agent_id = "test-agent-1".to_string();
let work_item = json!({"id": 1, "name": "test"});
let state = StandardExecutor::create_initial_state(agent_id.clone(), work_item.clone());
assert!(matches!(state, AgentLifecycleState::Created { .. }));
let worktree_path = PathBuf::from("/tmp/test-worktree");
let running_state = StandardExecutor::transition_to_running(state, worktree_path.clone());
assert!(running_state.is_ok());
let running_state = running_state.unwrap();
assert!(matches!(running_state, AgentLifecycleState::Running { .. }));
}
#[test]
fn test_state_machine_integration_running_to_completed() {
let agent_id = "test-agent-2".to_string();
let work_item = json!({"id": 2});
let state = StandardExecutor::create_initial_state(agent_id, work_item);
let running_state =
StandardExecutor::transition_to_running(state, PathBuf::from("/tmp/test")).unwrap();
let output = Some("Command executed successfully".to_string());
let commits = vec!["abc123".to_string(), "def456".to_string()];
let completed_state = StandardExecutor::transition_to_completed(
running_state,
output.clone(),
commits.clone(),
);
assert!(completed_state.is_ok());
let completed_state = completed_state.unwrap();
assert!(matches!(
completed_state,
AgentLifecycleState::Completed { .. }
));
let result = state_to_result(&completed_state);
assert!(result.is_some());
let result = result.unwrap();
assert!(result.is_success());
assert_eq!(result.output, output);
assert_eq!(result.commits, commits);
}
#[test]
fn test_state_machine_integration_running_to_failed() {
let agent_id = "test-agent-3".to_string();
let work_item = json!({"id": 3});
let state = StandardExecutor::create_initial_state(agent_id, work_item);
let running_state =
StandardExecutor::transition_to_running(state, PathBuf::from("/tmp/test")).unwrap();
let error_msg = "Command execution failed".to_string();
let json_log = Some("/tmp/logs/session-123.json".to_string());
let failed_state = StandardExecutor::transition_to_failed(
running_state,
error_msg.clone(),
json_log.clone(),
);
assert!(failed_state.is_ok());
let failed_state = failed_state.unwrap();
assert!(matches!(failed_state, AgentLifecycleState::Failed { .. }));
let result = state_to_result(&failed_state);
assert!(result.is_some());
let result = result.unwrap();
assert!(!result.is_success());
assert_eq!(result.error, Some(error_msg));
assert_eq!(result.json_log_location, json_log);
}
#[test]
fn test_state_machine_integration_invalid_transition() {
let agent_id = "test-agent-4".to_string();
let work_item = json!({"id": 4});
let state = StandardExecutor::create_initial_state(agent_id, work_item);
let invalid_result =
StandardExecutor::transition_to_completed(state, Some("output".to_string()), vec![]);
assert!(invalid_result.is_err());
assert!(matches!(
invalid_result.unwrap_err(),
ExecutionError::AgentError(_)
));
}
#[test]
fn test_state_machine_integration_full_lifecycle_success() {
let agent_id = "test-agent-5".to_string();
let work_item = json!({"id": 5, "file": "test.rs"});
let state = StandardExecutor::create_initial_state(agent_id.clone(), work_item);
assert!(matches!(state, AgentLifecycleState::Created { .. }));
let state =
StandardExecutor::transition_to_running(state, PathBuf::from("/tmp/worktree-5"))
.unwrap();
assert!(matches!(state, AgentLifecycleState::Running { .. }));
let state = StandardExecutor::transition_to_completed(
state,
Some("All tests passed".to_string()),
vec!["commit-1".to_string()],
)
.unwrap();
assert!(matches!(state, AgentLifecycleState::Completed { .. }));
let result = state_to_result(&state).unwrap();
assert!(result.is_success());
assert_eq!(result.item_id, agent_id);
}
#[test]
fn test_state_machine_integration_full_lifecycle_failure() {
let agent_id = "test-agent-6".to_string();
let work_item = json!({"id": 6});
let state = StandardExecutor::create_initial_state(agent_id.clone(), work_item);
let state =
StandardExecutor::transition_to_running(state, PathBuf::from("/tmp/worktree-6"))
.unwrap();
let state = StandardExecutor::transition_to_failed(
state,
"Test execution failed".to_string(),
Some("/logs/test.json".to_string()),
)
.unwrap();
assert!(matches!(state, AgentLifecycleState::Failed { .. }));
let result = state_to_result(&state).unwrap();
assert!(!result.is_success());
assert_eq!(result.item_id, agent_id);
assert!(result.error.is_some());
}
}