use anyhow::{Context, Result};
use async_trait::async_trait;
use std::path::Path;
use std::time::Instant;
use tokio::process::Command;
use super::{
AiderConfig, ProviderCapabilities, ProviderConfig, ProviderExecutor, ProviderHealthStatus,
};
use crate::agent::{Task, TaskResult, TaskType};
use crate::identity::AgentIdentity;
pub struct AiderExecutor {
config: AiderConfig,
}
impl AiderExecutor {
pub fn new(config: AiderConfig) -> Self {
Self { config }
}
fn generate_task_prompt(&self, identity: &AgentIdentity, task: &Task) -> String {
let context_header = format!(
"# ccswarm Agent Context\n\
Agent: {} ({})\n\
Task: {}\n\
Priority: {:?}\n\
Type: {:?}\n\n",
identity.agent_id,
identity.specialization.name(),
task.id,
task.priority,
task.task_type
);
let boundaries = self.generate_agent_boundaries(&identity.specialization);
let task_description = format!("## Task Description\n{}\n\n", task.description);
let task_details = if let Some(details) = &task.details {
format!("## Additional Details\n{}\n\n", details)
} else {
String::new()
};
let aider_instructions = self.generate_aider_instructions(task);
format!(
"{}{}{}{}\n{}",
context_header, boundaries, task_description, task_details, aider_instructions
)
}
fn generate_agent_boundaries(&self, specialization: &crate::identity::AgentRole) -> String {
match specialization {
crate::identity::AgentRole::Frontend {
technologies,
responsibilities,
..
} => {
format!(
"## Agent Specialization: Frontend\n\
Technologies: {}\n\
Responsibilities: {}\n\
\n\
**IMPORTANT**: Focus only on frontend concerns. Do not modify:\n\
- Backend API endpoints or server code\n\
- Database schemas or migrations\n\
- Infrastructure or deployment configurations\n\
\n",
technologies.join(", "),
responsibilities.join(", ")
)
}
crate::identity::AgentRole::Backend {
technologies,
responsibilities,
..
} => {
format!(
"## Agent Specialization: Backend\n\
Technologies: {}\n\
Responsibilities: {}\n\
\n\
**IMPORTANT**: Focus only on backend concerns. Do not modify:\n\
- Frontend components or UI code\n\
- Client-side styling or layouts\n\
- Frontend build configurations\n\
\n",
technologies.join(", "),
responsibilities.join(", ")
)
}
crate::identity::AgentRole::DevOps {
technologies,
responsibilities,
..
} => {
format!(
"## Agent Specialization: DevOps\n\
Technologies: {}\n\
Responsibilities: {}\n\
\n\
**IMPORTANT**: Focus only on infrastructure and deployment. Do not modify:\n\
- Application business logic\n\
- Frontend or backend feature code\n\
- Database application schemas\n\
\n",
technologies.join(", "),
responsibilities.join(", ")
)
}
crate::identity::AgentRole::QA {
responsibilities, ..
} => {
format!(
"## Agent Specialization: QA\n\
Responsibilities: {}\n\
\n\
**IMPORTANT**: Focus only on testing and quality assurance. Do not modify:\n\
- Production application code\n\
- Core business logic\n\
- Infrastructure configurations\n\
\n",
responsibilities.join(", ")
)
}
crate::identity::AgentRole::Master { .. } => {
"## Agent Specialization: Master Orchestrator\n\
\n\
**IMPORTANT**: You are the orchestrator. Do not modify code directly.\n\
Instead, coordinate between other agents and provide guidance.\n\
\n"
.to_string()
}
crate::identity::AgentRole::Search {
technologies,
responsibilities,
..
} => {
format!(
"## Agent Specialization: Search\n\
Technologies: {}\n\
Responsibilities: {}\n\
\n\
**IMPORTANT**: Focus only on information retrieval. Do not modify:\n\
- Any source code files\n\
- Configuration files\n\
- Infrastructure settings\n\
\n",
technologies.join(", "),
responsibilities.join(", ")
)
}
}
}
fn generate_aider_instructions(&self, task: &Task) -> String {
let base_instructions = "## Aider Instructions\n\
Please analyze the codebase and implement the requested changes.\n";
let task_specific = match task.task_type {
TaskType::Development => {
"- Focus on implementing clean, maintainable code\n\
- Follow existing code patterns and conventions\n\
- Add appropriate error handling\n\
- Include relevant tests if applicable\n"
}
TaskType::Testing => {
"- Write comprehensive tests for the feature/bug\n\
- Ensure good test coverage\n\
- Follow testing best practices\n\
- Use appropriate testing frameworks\n"
}
TaskType::Documentation => {
"- Create clear, comprehensive documentation\n\
- Include code examples where appropriate\n\
- Follow documentation standards\n\
- Update related documentation files\n"
}
TaskType::Bugfix => {
"- Identify and fix the root cause\n\
- Add regression tests\n\
- Ensure the fix doesn't break existing functionality\n\
- Document the fix if necessary\n"
}
TaskType::Infrastructure => {
"- Focus on infrastructure and deployment concerns\n\
- Ensure configurations are secure and scalable\n\
- Follow infrastructure best practices\n\
- Test configuration changes thoroughly\n"
}
TaskType::Coordination => {
"- This is a coordination task\n\
- Focus on planning and organizing\n\
- Do not implement code directly\n\
- Provide clear guidance for other agents\n"
}
TaskType::Review => {
"- Review code and documentation quality\n\
- Check for security issues and best practices\n\
- Provide constructive feedback\n\
- Ensure compliance with coding standards\n"
}
TaskType::Feature => {
"- Implement new feature functionality\n\
- Ensure proper integration with existing code\n\
- Follow feature specifications carefully\n\
- Add appropriate tests and documentation\n"
}
TaskType::Remediation => {
"- Fix the quality issues identified in the review\n\
- Follow the specific instructions provided\n\
- Ensure all issues are resolved completely\n\
- Add tests to prevent regression\n\
- Improve code quality as needed\n"
}
TaskType::Bug => {
"- Identify and fix the bug\n\
- Add regression tests\n\
- Ensure the fix doesn't break existing functionality\n\
- Document the fix if necessary\n"
}
TaskType::Assistance => {
"- Provide help and support for the requested task\n\
- Collaborate with the requesting agent\n\
- Share expertise and knowledge\n\
- Help unblock the original task\n"
}
TaskType::Research => {
"- Research and gather information\n\
- Analyze search results and findings\n\
- Apply insights to improve implementation\n\
- Document key findings and recommendations\n"
}
};
let git_instructions = if self.config.auto_commit {
"\n**Git Behavior**: Auto-commit is enabled. Changes will be committed automatically."
} else {
"\n**Git Behavior**: Auto-commit is disabled. Review changes before committing."
};
format!("{}{}{}", base_instructions, task_specific, git_instructions)
}
fn build_command_args(&self, prompt: &str, _working_dir: &Path) -> Vec<String> {
let mut args = Vec::new();
args.push("--model".to_string());
args.push(self.config.model.clone());
if self.config.auto_commit {
args.push("--auto-commits".to_string());
} else {
args.push("--no-auto-commits".to_string());
}
if !self.config.git {
args.push("--no-git".to_string());
}
args.push("--message".to_string());
args.push(prompt.to_string());
args.extend(self.config.additional_args.clone());
args.push("--yes".to_string());
args
}
async fn execute_aider_command(
&self,
args: Vec<String>,
working_dir: &Path,
identity: &AgentIdentity,
) -> Result<String> {
let executable = if let Some(path) = &self.config.executable_path {
path.to_string_lossy().to_string()
} else {
"aider".to_string()
};
let mut cmd = Command::new(&executable);
cmd.current_dir(working_dir);
for (key, value) in &identity.env_vars {
cmd.env(key, value);
}
for (key, value) in self.config.get_env_vars() {
cmd.env(key, value);
}
cmd.args(&args);
let start = Instant::now();
let output = tokio::time::timeout(
std::time::Duration::from_secs(600), cmd.output(),
)
.await
.context("Aider command timed out")?
.context("Failed to execute Aider")?;
let duration = start.elapsed();
tracing::debug!(
"Aider execution completed in {:?} for agent {}",
duration,
identity.agent_id
);
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if !stderr.trim().is_empty() {
Ok(format!("{}\n\nAider Output:\n{}", stdout, stderr))
} else {
Ok(stdout)
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
Err(anyhow::anyhow!(
"Aider execution failed (exit code: {:?})\nStderr: {}\nStdout: {}",
output.status.code(),
stderr,
stdout
))
}
}
fn parse_aider_output(
&self,
output: String,
task: &Task,
duration: std::time::Duration,
) -> TaskResult {
let mut files_changed = Vec::new();
let mut commits_made = Vec::new();
let mut errors = Vec::new();
for line in output.lines() {
if line.contains("Modified ") || line.contains("Created ") {
if let Some(file) = self.extract_filename_from_line(line) {
files_changed.push(file);
}
} else if line.contains("Commit ") && line.contains("hash:") {
if let Some(commit) = self.extract_commit_from_line(line) {
commits_made.push(commit);
}
} else if line.contains("Error:") || line.contains("WARNING:") {
errors.push(line.to_string());
}
}
let success = errors.is_empty() || errors.iter().all(|e| !e.contains("Error:"));
TaskResult {
success,
output: serde_json::json!({
"aider_output": output,
"files_changed": files_changed,
"commits_made": commits_made,
"warnings": errors.iter().filter(|e| e.contains("WARNING")).collect::<Vec<_>>(),
"task_id": task.id,
"provider": "aider",
"model": self.config.model,
"auto_commit": self.config.auto_commit,
}),
error: if success {
None
} else {
Some(errors.join("; "))
},
duration,
}
}
fn extract_filename_from_line(&self, line: &str) -> Option<String> {
if let Some(start) = line.find("Modified ").or_else(|| line.find("Created ")) {
let start_pos = if line[start..].starts_with("Modified ") {
start + 9
} else {
start + 8
};
if let Some(end) = line[start_pos..].find(' ') {
Some(line[start_pos..start_pos + end].to_string())
} else {
Some(line[start_pos..].to_string())
}
} else {
None
}
}
fn extract_commit_from_line(&self, line: &str) -> Option<String> {
if let Some(hash_pos) = line.find("hash: ") {
let start = hash_pos + 6;
if let Some(end) = line[start..].find(' ') {
Some(line[start..start + end].to_string())
} else {
Some(line[start..].to_string())
}
} else {
None
}
}
}
#[async_trait]
impl ProviderExecutor for AiderExecutor {
async fn execute_prompt(
&self,
prompt: &str,
identity: &AgentIdentity,
working_dir: &Path,
) -> Result<String> {
let args = self.build_command_args(prompt, working_dir);
self.execute_aider_command(args, working_dir, identity)
.await
}
async fn execute_task(
&self,
task: &Task,
identity: &AgentIdentity,
working_dir: &Path,
) -> Result<TaskResult> {
let start = Instant::now();
let prompt = self.generate_task_prompt(identity, task);
tracing::info!(
"Executing task '{}' with Aider (model: {}) for agent {}",
task.description,
self.config.model,
identity.agent_id
);
match self.execute_prompt(&prompt, identity, working_dir).await {
Ok(output) => {
let duration = start.elapsed();
let result = self.parse_aider_output(output, task, duration);
tracing::info!(
"Aider task completed in {:?} for agent {} (files changed: {})",
duration,
identity.agent_id,
result.output["files_changed"]
.as_array()
.map(|a| a.len())
.unwrap_or(0)
);
Ok(result)
}
Err(e) => {
let duration = start.elapsed();
tracing::error!(
"Aider task failed after {:?} for agent {}: {}",
duration,
identity.agent_id,
e
);
Ok(TaskResult {
success: false,
output: serde_json::json!({
"provider": "aider",
"model": self.config.model,
}),
error: Some(e.to_string()),
duration,
})
}
}
}
async fn health_check(&self, working_dir: &Path) -> Result<ProviderHealthStatus> {
let start = Instant::now();
let executable = if let Some(path) = &self.config.executable_path {
path.to_string_lossy().to_string()
} else {
"aider".to_string()
};
let result = Command::new(&executable)
.arg("--version")
.current_dir(working_dir)
.output()
.await;
let duration = start.elapsed();
let response_time_ms = duration.as_millis() as u64;
match result {
Ok(output) if output.status.success() => {
let version_output = String::from_utf8_lossy(&output.stdout);
let version = version_output
.lines()
.find(|line| line.contains("aider"))
.unwrap_or(version_output.trim())
.to_string();
Ok(ProviderHealthStatus {
is_healthy: true,
version: Some(version),
last_check: chrono::Utc::now(),
error_message: None,
response_time_ms: Some(response_time_ms),
})
}
Ok(output) => {
let error = String::from_utf8_lossy(&output.stderr).to_string();
Ok(ProviderHealthStatus {
is_healthy: false,
version: None,
last_check: chrono::Utc::now(),
error_message: Some(format!("Command failed: {}", error)),
response_time_ms: Some(response_time_ms),
})
}
Err(e) => Ok(ProviderHealthStatus {
is_healthy: false,
version: None,
last_check: chrono::Utc::now(),
error_message: Some(format!("Failed to execute Aider: {}", e)),
response_time_ms: Some(response_time_ms),
}),
}
}
fn get_capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities {
supports_json_output: false, supports_streaming: false,
supports_file_operations: true,
supports_git_operations: true,
supports_code_execution: false, max_context_length: Some(128_000), supported_languages: vec![
"python".to_string(),
"javascript".to_string(),
"typescript".to_string(),
"java".to_string(),
"c++".to_string(),
"c".to_string(),
"c#".to_string(),
"go".to_string(),
"rust".to_string(),
"php".to_string(),
"ruby".to_string(),
"html".to_string(),
"css".to_string(),
"sql".to_string(),
"bash".to_string(),
"yaml".to_string(),
"json".to_string(),
"markdown".to_string(),
],
}
}
}