use super::{AgentContext, AgentResult, AgentStatus, MapReduceError, MapReduceResult};
use crate::commands::CommandRegistry;
use crate::cook::execution::interpolation::{InterpolationContext, InterpolationEngine};
use crate::cook::execution::variables::VariableContext;
use crate::cook::execution::ClaudeExecutor;
use crate::cook::orchestrator::ExecutionEnvironment;
use crate::cook::session::SessionManager;
use crate::cook::workflow::WorkflowStep;
use crate::subprocess::SubprocessManager;
use serde_json;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::{debug, error, info, warn};
#[derive(Debug, Clone)]
pub struct StepResult {
pub success: bool,
pub stdout: Option<String>,
pub stderr: Option<String>,
pub commits: Option<Vec<String>>,
pub files_changed: Option<Vec<String>>,
}
pub struct AgentCommandExecutor {
command_executor: Arc<dyn ClaudeExecutor>,
subprocess_manager: Arc<SubprocessManager>,
_session_manager: Arc<dyn SessionManager>,
_command_registry: Arc<CommandRegistry>,
}
impl AgentCommandExecutor {
pub fn new(
command_executor: Arc<dyn ClaudeExecutor>,
subprocess_manager: Arc<SubprocessManager>,
session_manager: Arc<dyn SessionManager>,
command_registry: Arc<CommandRegistry>,
) -> Self {
Self {
command_executor,
subprocess_manager,
_session_manager: session_manager,
_command_registry: command_registry,
}
}
pub async fn execute_agent_commands_with_retry_info(
&self,
agent_context: &AgentContext,
commands: &[WorkflowStep],
env: &ExecutionEnvironment,
job_id: &str,
retry_attempt: u32,
max_retries: u32,
) -> MapReduceResult<AgentResult> {
let start_time = Instant::now();
let agent_id = &agent_context.item_id;
info!(
"Agent {} executing {} commands (attempt {}/{})",
agent_id,
commands.len(),
retry_attempt + 1,
max_retries + 1
);
let mut result = AgentResult {
item_id: agent_context.item_id.clone(),
status: AgentStatus::Running,
output: None,
commits: Vec::new(),
files_modified: Vec::new(),
duration: Duration::from_secs(0),
error: None,
worktree_path: Some(env.working_dir.to_path_buf()),
branch_name: Some(agent_context.worktree_name.clone()),
worktree_session_id: None,
json_log_location: None,
cleanup_status: None,
};
match self
.execute_all_steps(commands, agent_context, env, job_id)
.await
{
Ok(step_results) => {
for step_result in step_results {
if let Some(commits) = step_result.commits {
result.commits.extend(commits);
}
if let Some(files) = step_result.files_changed {
result.files_modified.extend(files);
}
}
result.status = AgentStatus::Success;
result.duration = start_time.elapsed();
info!(
"Agent {} completed successfully in {:?}",
agent_id, result.duration
);
}
Err(e) => {
let error_msg = format!("Agent {} failed: {}", agent_id, e);
error!("{}", error_msg);
result.status = AgentStatus::Failed(error_msg.clone());
result.error = Some(error_msg);
result.duration = start_time.elapsed();
if retry_attempt < max_retries {
warn!(
"Agent {} will retry (attempt {}/{})",
agent_id,
retry_attempt + 2,
max_retries + 1
);
result.status = AgentStatus::Retrying(retry_attempt + 1);
}
}
}
Ok(result)
}
pub async fn execute_agent_commands(
&self,
agent_context: &AgentContext,
commands: &[WorkflowStep],
env: &ExecutionEnvironment,
job_id: &str,
) -> MapReduceResult<AgentResult> {
self.execute_agent_commands_with_retry_info(agent_context, commands, env, job_id, 0, 0)
.await
}
async fn execute_all_steps(
&self,
steps: &[WorkflowStep],
agent_context: &AgentContext,
env: &ExecutionEnvironment,
job_id: &str,
) -> MapReduceResult<Vec<StepResult>> {
let mut results = Vec::new();
let mut variable_context = VariableContext::new();
for (key, value) in &agent_context.variables {
variable_context.set_global(
key.clone(),
crate::cook::execution::variables::Variable::Static(serde_json::Value::String(
value.clone(),
)),
);
}
for (index, step) in steps.iter().enumerate() {
debug!(
"Agent {} executing step {}/{}",
agent_context.item_id,
index + 1,
steps.len()
);
let mut vars_map = HashMap::new();
for (key, value) in &agent_context.variables {
vars_map.insert(key.clone(), serde_json::Value::String(value.clone()));
}
let interp_context = InterpolationContext {
variables: vars_map,
parent: None,
};
let step_result = self
.execute_single_step(step, &interp_context, env, job_id)
.await?;
if let Some(output) = &step_result.stdout {
variable_context.set_global(
"output",
crate::cook::execution::variables::Variable::Static(serde_json::Value::String(
output.clone(),
)),
);
}
results.push(step_result);
}
Ok(results)
}
async fn execute_single_step(
&self,
step: &WorkflowStep,
context: &InterpolationContext,
env: &ExecutionEnvironment,
_job_id: &str,
) -> MapReduceResult<StepResult> {
let mut interpolation_engine = InterpolationEngine::new(false);
let command_str = step.command.as_deref().unwrap_or("");
let interpolated_command = interpolation_engine
.interpolate(command_str, context)
.map_err(|e| MapReduceError::General {
message: format!("Failed to interpolate command: {}", e),
source: None,
})?;
let result = if interpolated_command.starts_with("claude:") {
self.execute_claude_command(&interpolated_command, env)
.await?
} else if interpolated_command.starts_with("shell:") {
self.execute_shell_command(&interpolated_command, env)
.await?
} else {
return Err(MapReduceError::General {
message: format!("Unknown command type: {}", interpolated_command),
source: None,
});
};
Ok(result)
}
async fn execute_claude_command(
&self,
command: &str,
env: &ExecutionEnvironment,
) -> MapReduceResult<StepResult> {
let claude_cmd = command
.strip_prefix("claude:")
.ok_or_else(|| MapReduceError::General {
message: format!("Invalid Claude command format: {}", command),
source: None,
})?
.trim();
let result = self
.command_executor
.execute_claude_command(claude_cmd, &env.working_dir, HashMap::new())
.await
.map_err(|e| MapReduceError::General {
message: format!("Claude command failed: {}", e),
source: None,
})?;
Ok(StepResult {
success: result.success,
stdout: Some(result.stdout),
stderr: if result.stderr.is_empty() {
None
} else {
Some(result.stderr)
},
commits: None,
files_changed: None,
})
}
async fn execute_shell_command(
&self,
command: &str,
env: &ExecutionEnvironment,
) -> MapReduceResult<StepResult> {
let shell_cmd = command
.strip_prefix("shell:")
.ok_or_else(|| MapReduceError::General {
message: format!("Invalid shell command format: {}", command),
source: None,
})?
.trim();
use crate::subprocess::ProcessCommandBuilder;
let command = ProcessCommandBuilder::new("sh")
.args(["-c", shell_cmd])
.current_dir(&env.working_dir)
.build();
let result = self
.subprocess_manager
.runner()
.run(command)
.await
.map_err(|e| MapReduceError::General {
message: format!("Shell command failed: {}", e),
source: None,
})?;
Ok(StepResult {
success: result.status.success(),
stdout: Some(result.stdout),
stderr: if result.status.success() {
None
} else {
Some(result.stderr)
},
commits: None,
files_changed: None,
})
}
}