use crate::claude::{ClaudeCodeInterface, ClaudeConfig};
use crate::session::{SessionInitOptions, SessionManager, SessionManagerConfig};
use crate::task::{
ErrorHandler, ErrorStrategy, OutputCondition, SetupCommand, SetupResult, TaskManager,
TaskManagerConfig, TaskSpec, TaskStatus,
};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::process::Stdio;
use std::sync::Arc;
use std::time::Instant;
use tokio::process::Command;
use tracing::{info, warn};
use uuid::Uuid;
pub struct AgentSystem {
task_manager: Arc<TaskManager>,
session_manager: Arc<SessionManager>,
claude_interface: Arc<ClaudeCodeInterface>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub workspace_path: std::path::PathBuf,
pub session_config: SessionManagerConfig,
pub task_config: TaskManagerConfig,
pub claude_config: ClaudeConfig,
pub setup_commands: Vec<SetupCommand>,
}
impl AgentConfig {
pub fn with_subprocess_output(mut self, show_output: bool) -> Self {
tracing::info!("Setting show_subprocess_output to: {}", show_output);
self.claude_config.show_subprocess_output = show_output;
self
}
}
impl AgentSystem {
pub async fn new(config: AgentConfig) -> Result<Self> {
if !config.setup_commands.is_empty() {
info!("Executing setup commands before system initialization...");
Self::execute_setup_commands(&config.setup_commands).await?;
}
let workspace_path = config.workspace_path.clone();
let session_manager = Arc::new(
SessionManager::new(
config.workspace_path,
config.session_config,
SessionInitOptions::default(),
)
.await?,
);
let task_manager = Arc::new(TaskManager::new(config.task_config));
let claude_interface = Arc::new(
ClaudeCodeInterface::new(config.claude_config, workspace_path)
.await
.map_err(|e| anyhow::anyhow!("Failed to initialize Claude interface: {}", e))?,
);
Ok(Self {
task_manager,
session_manager,
claude_interface,
})
}
pub async fn process_task(&self, task_id: Uuid) -> Result<()> {
let task = self.task_manager.get_task(task_id).await?;
tracing::info!("Processing task: {} - {}", task.id, task.title);
self.task_manager
.update_task_status(
task_id,
TaskStatus::InProgress {
started_at: chrono::Utc::now(),
estimated_completion: None,
},
)
.await?;
self.save_session_state().await?;
let result = self.claude_interface.process_task(&task, None).await;
match result {
Ok(completed_task) => {
self.task_manager
.update_task_status(task_id, completed_task.status)
.await?;
self.save_session_state().await?;
tracing::info!("Task completed successfully: {}", task_id);
Ok(())
}
Err(e) => {
self.task_manager
.update_task_status(
task_id,
TaskStatus::Failed {
failed_at: chrono::Utc::now(),
error: crate::task::types::TaskError::ClaudeError {
message: format!("Failed: {}", e),
error_code: None,
retry_possible: true,
},
retry_count: 0,
},
)
.await?;
self.save_session_state().await?;
tracing::error!("Task failed: {} - {}", task_id, e);
Err(anyhow::anyhow!("Task processing failed: {}", e))
}
}
}
pub async fn create_and_process_task(&self, title: &str, description: &str) -> Result<Uuid> {
let task_spec = TaskSpec {
title: title.to_string(),
description: description.to_string(),
dependencies: Vec::new(),
metadata: crate::task::types::TaskMetadata {
priority: crate::task::types::TaskPriority::Normal,
estimated_complexity: Some(crate::task::types::ComplexityLevel::Moderate),
estimated_duration: Some(
chrono::Duration::from_std(std::time::Duration::from_secs(300)).unwrap(),
),
repository_refs: Vec::new(),
file_refs: Vec::new(),
tags: vec!["auto-generated".to_string()],
context_requirements: crate::task::types::ContextRequirements {
required_files: Vec::new(),
required_repositories: Vec::new(),
build_dependencies: Vec::new(),
environment_vars: std::collections::HashMap::new(),
claude_context_keys: Vec::new(),
},
},
};
let task_id = self.task_manager.create_task(task_spec, None).await?;
self.save_session_state().await?;
self.process_task(task_id).await?;
Ok(task_id)
}
pub async fn execute_plan(&self, plan: crate::task::ExecutionPlan) -> Result<Vec<uuid::Uuid>> {
use tracing::{error, info, warn};
info!("Executing plan: {}", plan.summary());
if let Err(validation_error) = plan.validate() {
return Err(anyhow::anyhow!(
"Invalid execution plan: {}",
validation_error
));
}
let mut task_ids = Vec::new();
if plan.has_setup_commands() {
info!("Executing {} setup commands...", plan.setup_command_count());
Self::execute_setup_commands(&plan.setup_commands).await?;
info!("Setup commands completed successfully");
}
if plan.has_tasks() {
info!(
"Processing {} tasks with mode: {:?}",
plan.task_count(),
plan.execution_mode
);
let total_tasks = plan.task_count();
match plan.execution_mode {
crate::task::ExecutionMode::Sequential => {
for (index, task_spec) in plan.task_specs.into_iter().enumerate() {
let task_num = index + 1;
info!(
"Processing task {}/{}: {}",
task_num, total_tasks, task_spec.title
);
match self.create_and_process_task_spec(task_spec).await {
Ok(task_id) => {
info!(
"Task {}/{} completed successfully: {}",
task_num, total_tasks, task_id
);
task_ids.push(task_id);
}
Err(e) => {
error!("Task {}/{} failed: {}", task_num, total_tasks, e);
warn!("Continuing with remaining tasks despite failure");
}
}
}
}
crate::task::ExecutionMode::Parallel { max_concurrent: _ } => {
warn!(
"Parallel execution mode not yet fully implemented, falling back to sequential"
);
for task_spec in plan.task_specs {
match self.create_and_process_task_spec(task_spec).await {
Ok(task_id) => task_ids.push(task_id),
Err(e) => warn!("Task failed in parallel mode: {}", e),
}
}
}
crate::task::ExecutionMode::Intelligent => {
warn!(
"Intelligent execution mode not yet implemented, falling back to sequential"
);
for task_spec in plan.task_specs {
match self.create_and_process_task_spec(task_spec).await {
Ok(task_id) => task_ids.push(task_id),
Err(e) => warn!("Task failed in intelligent mode: {}", e),
}
}
}
}
}
self.save_session_checkpoint("plan_execution_complete")
.await?;
info!(
"Execution plan completed: {} tasks processed",
task_ids.len()
);
Ok(task_ids)
}
async fn create_and_process_task_spec(
&self,
task_spec: crate::task::TaskSpec,
) -> Result<uuid::Uuid> {
let task_id = self.task_manager.create_task(task_spec, None).await?;
self.process_task(task_id).await?;
Ok(task_id)
}
pub fn agent_config_to_execution_plan(config: &AgentConfig) -> crate::task::ExecutionPlan {
use tracing::info;
info!(
"Converting AgentConfig to ExecutionPlan with {} setup commands",
config.setup_commands.len()
);
let plan_name = format!(
"Structured Config: {}",
config
.workspace_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("workspace")
);
let plan = crate::task::ExecutionPlan::new()
.with_setup_commands(config.setup_commands.clone())
.with_metadata(
plan_name,
"Execution plan generated from structured TOML configuration",
)
.with_tags(vec![
"structured-config".to_string(),
"auto-generated".to_string(),
])
.with_sequential_execution();
info!("Created execution plan: {}", plan.summary());
plan
}
pub async fn get_system_status(&self) -> Result<SystemStatus> {
let task_stats = self.task_manager.get_statistics().await?;
let claude_status = self.claude_interface.get_interface_status().await;
let session_stats = self.session_manager.get_session_statistics().await?;
Ok(SystemStatus {
task_stats: task_stats.clone(),
claude_status: claude_status.clone(),
session_stats,
is_healthy: claude_status.is_healthy && task_stats.total_tasks > 0,
})
}
async fn save_session_state(&self) -> Result<()> {
self.session_manager.save_session().await?;
Ok(())
}
async fn save_session_checkpoint(&self, description: &str) -> Result<()> {
self.session_manager.save_session().await?;
self.session_manager
.create_checkpoint(description.to_string())
.await?;
Ok(())
}
pub async fn shutdown(&self) -> Result<()> {
tracing::info!("Shutting down agent system...");
self.save_session_checkpoint("agent_shutdown").await?;
self.session_manager.shutdown().await?;
tracing::info!("Agent system shutdown complete");
Ok(())
}
async fn execute_setup_commands(commands: &[SetupCommand]) -> Result<()> {
if commands.is_empty() {
return Ok(());
}
info!("Running {} setup commands...", commands.len());
for (index, cmd) in commands.iter().enumerate() {
info!(
"Executing setup command {}/{}: {}",
index + 1,
commands.len(),
cmd.name
);
let result = Self::execute_shell_command(cmd).await?;
if !result.success {
info!(
"Setup command '{}' failed with exit code: {}",
cmd.name, result.exit_code
);
if let Some(handler) = &cmd.error_handler {
if let Err(e) = Self::handle_command_error(cmd, &result, handler).await {
if cmd.required {
return Err(anyhow::anyhow!(
"Required setup command '{}' failed: {}",
cmd.name,
e
));
} else {
warn!("Optional setup command '{}' failed: {}", cmd.name, e);
}
}
} else if cmd.required {
return Err(anyhow::anyhow!(
"Required setup command '{}' failed: {}",
cmd.name,
result.stderr
));
} else {
warn!(
"Optional setup command '{}' failed: {}",
cmd.name, result.stderr
);
}
} else {
info!("Setup command '{}' completed successfully", cmd.name);
}
}
info!("All setup commands completed successfully");
Ok(())
}
async fn execute_shell_command(cmd: &SetupCommand) -> Result<SetupResult> {
let start_time = Instant::now();
let mut command = Command::new(&cmd.command);
command.args(&cmd.args);
if let Some(working_dir) = &cmd.working_dir {
command.current_dir(working_dir);
}
command.stdout(Stdio::piped()).stderr(Stdio::piped());
let result = if let Some(timeout) = cmd.timeout {
let timeout_std = timeout
.to_std()
.map_err(|_| anyhow::anyhow!("Invalid timeout duration: {:?}", timeout))?;
tokio::time::timeout(timeout_std, command.output())
.await
.map_err(|_| anyhow::anyhow!("Command timed out after {:?}", timeout))?
} else {
command.output().await
};
let duration = start_time.elapsed();
match result {
Ok(output) => {
let success = output.status.success();
let exit_code = output.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok(SetupResult {
command_id: cmd.id,
success,
exit_code,
stdout,
stderr,
duration: chrono::Duration::from_std(duration).unwrap_or_default(),
})
}
Err(e) => Err(anyhow::anyhow!("Failed to execute command: {}", e)),
}
}
async fn handle_command_error(
cmd: &SetupCommand,
result: &SetupResult,
handler: &ErrorHandler,
) -> Result<()> {
match &handler.strategy {
ErrorStrategy::Skip => {
warn!("Skipping failed command: {}", cmd.name);
Ok(())
}
ErrorStrategy::Retry {
max_attempts,
delay,
} => {
info!(
"Retrying command '{}' (max {} attempts)",
cmd.name, max_attempts
);
Self::retry_command(cmd, *max_attempts, *delay).await
}
ErrorStrategy::Backup {
condition,
backup_command,
backup_args,
} => {
if Self::should_run_backup(result, condition) {
info!("Running backup command for: {}", cmd.name);
Self::execute_backup_command(backup_command, backup_args, &cmd.working_dir)
.await
} else {
Err(anyhow::anyhow!(
"Backup condition not met for: {}",
cmd.name
))
}
}
}
}
async fn retry_command(
cmd: &SetupCommand,
max_attempts: u32,
delay: chrono::Duration,
) -> Result<()> {
for attempt in 1..=max_attempts {
info!(
"Retry attempt {}/{} for command: {}",
attempt, max_attempts, cmd.name
);
if let Ok(delay_std) = delay.to_std() {
tokio::time::sleep(delay_std).await;
}
let result = Self::execute_shell_command(cmd).await?;
if result.success {
info!(
"Command '{}' succeeded on retry attempt {}",
cmd.name, attempt
);
return Ok(());
}
if attempt == max_attempts {
return Err(anyhow::anyhow!(
"Command '{}' failed after {} attempts. Last error: {}",
cmd.name,
max_attempts,
result.stderr
));
}
}
Ok(())
}
async fn execute_backup_command(
backup_command: &str,
backup_args: &[String],
working_dir: &Option<std::path::PathBuf>,
) -> Result<()> {
let mut command = Command::new(backup_command);
command.args(backup_args);
if let Some(working_dir) = working_dir {
command.current_dir(working_dir);
}
command.stdout(Stdio::piped()).stderr(Stdio::piped());
let output = command.output().await?;
if output.status.success() {
info!("Backup command executed successfully");
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(anyhow::anyhow!("Backup command failed: {}", stderr))
}
}
fn should_run_backup(result: &SetupResult, condition: &OutputCondition) -> bool {
if let Some((min, max)) = condition.exit_code_range
&& (result.exit_code < min || result.exit_code > max)
{
return false;
}
if let Some(must_contain) = &condition.contains {
let output = if condition.check_stdout {
&result.stdout
} else {
&result.stderr
};
if !output.contains(must_contain) {
return false;
}
}
if let Some(must_not_contain) = &condition.not_contains {
let output = if condition.check_stdout {
&result.stdout
} else {
&result.stderr
};
if output.contains(must_not_contain) {
return false;
}
}
true
}
pub fn task_manager(&self) -> Arc<TaskManager> {
self.task_manager.clone()
}
pub fn session_manager(&self) -> Arc<SessionManager> {
self.session_manager.clone()
}
pub fn claude_interface(&self) -> Arc<ClaudeCodeInterface> {
self.claude_interface.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::task::{ErrorHandler, OutputCondition};
use chrono::Duration;
use std::path::PathBuf;
#[tokio::test]
async fn test_setup_command_execution_success() {
let setup_command = SetupCommand::new("test_echo", "echo")
.with_args(vec!["hello".to_string(), "world".to_string()]);
let result = AgentSystem::execute_shell_command(&setup_command).await;
assert!(result.is_ok());
let setup_result = result.unwrap();
assert!(setup_result.success);
assert_eq!(setup_result.exit_code, 0);
assert!(setup_result.stdout.contains("hello world"));
}
#[tokio::test]
async fn test_setup_command_execution_failure() {
let setup_command = SetupCommand::new("test_fail", "false");
let result = AgentSystem::execute_shell_command(&setup_command).await;
assert!(result.is_ok());
let setup_result = result.unwrap();
assert!(!setup_result.success);
assert_eq!(setup_result.exit_code, 1);
}
#[tokio::test]
async fn test_setup_command_with_working_directory() {
let setup_command =
SetupCommand::new("test_pwd", "pwd").with_working_dir(PathBuf::from("/tmp"));
let result = AgentSystem::execute_shell_command(&setup_command).await;
assert!(result.is_ok());
let setup_result = result.unwrap();
assert!(setup_result.success);
assert!(setup_result.stdout.trim().ends_with("tmp"));
}
#[tokio::test]
async fn test_setup_command_timeout() {
let setup_command = SetupCommand::new("test_timeout", "sleep")
.with_args(vec!["2".to_string()])
.with_timeout(Duration::milliseconds(100));
let result = AgentSystem::execute_shell_command(&setup_command).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("timed out"));
}
#[tokio::test]
async fn test_should_run_backup_with_stderr_contains() {
let result = SetupResult {
command_id: uuid::Uuid::new_v4(),
success: false,
exit_code: 1,
stdout: String::new(),
stderr: "command not found".to_string(),
duration: Duration::milliseconds(100),
};
let condition = OutputCondition::stderr_contains("command not found");
assert!(AgentSystem::should_run_backup(&result, &condition));
let condition = OutputCondition::stderr_contains("different error");
assert!(!AgentSystem::should_run_backup(&result, &condition));
}
#[tokio::test]
async fn test_should_run_backup_with_exit_code_range() {
let result = SetupResult {
command_id: uuid::Uuid::new_v4(),
success: false,
exit_code: 5,
stdout: String::new(),
stderr: String::new(),
duration: Duration::milliseconds(100),
};
let condition = OutputCondition {
exit_code_range: Some((1, 10)),
..Default::default()
};
assert!(AgentSystem::should_run_backup(&result, &condition));
let condition = OutputCondition {
exit_code_range: Some((1, 3)),
..Default::default()
};
assert!(!AgentSystem::should_run_backup(&result, &condition));
}
#[tokio::test]
async fn test_setup_commands_execution_with_optional_failure() {
let setup_commands = vec![
SetupCommand::new("success_command", "echo").with_args(vec!["success".to_string()]),
SetupCommand::new("optional_fail", "false")
.optional()
.with_error_handler(ErrorHandler::skip("skip_false")),
SetupCommand::new("final_success", "echo").with_args(vec!["final".to_string()]),
];
let result = AgentSystem::execute_setup_commands(&setup_commands).await;
assert!(
result.is_ok(),
"Setup commands should succeed even with optional failures"
);
}
#[tokio::test]
async fn test_setup_commands_required_failure() {
let setup_commands = vec![
SetupCommand::new("required_fail", "false"), ];
let result = AgentSystem::execute_setup_commands(&setup_commands).await;
assert!(
result.is_err(),
"Required command failure should cause setup to fail"
);
assert!(
result
.unwrap_err()
.to_string()
.contains("Required setup command")
);
}
#[tokio::test]
async fn test_backup_command_execution() {
let result =
AgentSystem::execute_backup_command("echo", &["backup executed".to_string()], &None)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_retry_command_eventual_success() {
let setup_command = SetupCommand::new("test_retry", "true");
let result =
AgentSystem::retry_command(&setup_command, 2, Duration::milliseconds(10)).await;
assert!(result.is_ok());
}
#[test]
fn test_setup_command_builder_pattern() {
let cmd = SetupCommand::new("test", "echo")
.with_args(vec!["hello".to_string()])
.with_working_dir(PathBuf::from("/tmp"))
.with_timeout(Duration::seconds(30))
.optional()
.with_error_handler(ErrorHandler::skip("test_skip"));
assert_eq!(cmd.name, "test");
assert_eq!(cmd.command, "echo");
assert_eq!(cmd.args, vec!["hello".to_string()]);
assert_eq!(cmd.working_dir, Some(PathBuf::from("/tmp")));
assert_eq!(cmd.timeout, Some(Duration::seconds(30)));
assert!(!cmd.required); assert!(cmd.error_handler.is_some());
}
#[test]
fn test_output_condition_helpers() {
let condition = OutputCondition::stderr_contains("error text");
assert!(!condition.check_stdout);
assert!(condition.check_stderr);
assert_eq!(condition.contains, Some("error text".to_string()));
let condition = OutputCondition::stdout_contains("success text");
assert!(condition.check_stdout);
assert!(!condition.check_stderr);
assert_eq!(condition.contains, Some("success text".to_string()));
let condition = OutputCondition::exit_code_range(1, 5);
assert_eq!(condition.exit_code_range, Some((1, 5)));
}
#[test]
fn test_error_handler_constructors() {
let handler = ErrorHandler::skip("test_skip");
assert_eq!(handler.name, "test_skip");
assert!(matches!(handler.strategy, ErrorStrategy::Skip));
let handler = ErrorHandler::retry("test_retry", 3, Duration::seconds(1));
assert_eq!(handler.name, "test_retry");
assert!(matches!(
handler.strategy,
ErrorStrategy::Retry {
max_attempts: 3,
..
}
));
let handler = ErrorHandler::backup(
"test_backup",
OutputCondition::default(),
"echo",
vec!["backup".to_string()],
);
assert_eq!(handler.name, "test_backup");
assert!(matches!(handler.strategy, ErrorStrategy::Backup { .. }));
}
#[test]
fn test_agent_config_with_setup_commands() {
let setup_commands = vec![
SetupCommand::new("test1", "echo"),
SetupCommand::new("test2", "ls"),
];
let config = AgentConfig {
setup_commands: setup_commands.clone(),
..Default::default()
};
assert_eq!(config.setup_commands.len(), 2);
assert_eq!(config.setup_commands[0].name, "test1");
assert_eq!(config.setup_commands[1].name, "test2");
}
#[test]
fn test_agent_config_toml_serialization() {
let config = AgentConfig::default();
let toml_str = config
.to_toml_string()
.expect("Failed to serialize to TOML");
assert!(!toml_str.is_empty());
assert!(toml_str.contains("workspace_path"));
let deserialized =
AgentConfig::from_toml_str(&toml_str).expect("Failed to deserialize from TOML");
assert_eq!(config.workspace_path, deserialized.workspace_path);
assert_eq!(
config.setup_commands.len(),
deserialized.setup_commands.len()
);
}
#[test]
fn test_agent_config_toml_file_operations() {
use tempfile::NamedTempFile;
let config = AgentConfig::default();
let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let temp_path = temp_file.path();
config
.to_toml_file(temp_path)
.expect("Failed to save config to file");
let loaded_config =
AgentConfig::from_toml_file(temp_path).expect("Failed to load config from file");
assert_eq!(config.workspace_path, loaded_config.workspace_path);
assert_eq!(
config.setup_commands.len(),
loaded_config.setup_commands.len()
);
}
}
#[derive(Debug, Clone)]
pub struct SystemStatus {
pub task_stats: crate::task::TaskTreeStatistics,
pub claude_status: crate::claude::interface::ClaudeInterfaceStatus,
pub session_stats: crate::session::SessionStatistics,
pub is_healthy: bool,
}
impl AgentConfig {
pub fn from_toml_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path).context("Failed to read config file")?;
Self::from_toml_str(&content)
}
pub fn from_toml_str(content: &str) -> Result<Self> {
toml::from_str(content).context("Failed to parse TOML configuration")
}
pub fn to_toml_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
let content = self.to_toml_string()?;
std::fs::write(path, content).context("Failed to write config file")
}
pub fn to_toml_string(&self) -> Result<String> {
toml::to_string_pretty(self).context("Failed to serialize configuration to TOML")
}
}
impl Default for AgentConfig {
fn default() -> Self {
let workspace_path = std::env::temp_dir().join("aca");
Self {
workspace_path,
session_config: SessionManagerConfig::default(),
task_config: TaskManagerConfig::default(),
claude_config: ClaudeConfig::default(),
setup_commands: Vec::new(), }
}
}