1use crate::config::AgentConfig;
4use crate::errors::{AgentError, ToolError};
5use crate::models::LanguageModel;
6use crate::tools::ToolRegistry;
7use crate::types::{ExecutionResult, Task, TaskComplexity, TaskPlan, TaskResult, TaskStatus};
8use std::sync::Arc;
9use tokio::sync::Mutex;
10
11pub struct CodeAgent {
13 model: Box<dyn LanguageModel>,
14 tools: Arc<Mutex<ToolRegistry>>,
15 config: AgentConfig,
16 _error_handler: crate::errors::ErrorHandler,
17}
18
19impl CodeAgent {
20 pub fn new(model: Box<dyn LanguageModel>, config: AgentConfig) -> Self {
22 let _error_handler = crate::errors::ErrorHandler::new(
23 config.execution.max_retries,
24 config.execution.retry_delay_seconds,
25 );
26 Self {
27 model,
28 tools: Arc::new(Mutex::new(ToolRegistry::new())),
29 config,
30 _error_handler,
31 }
32 }
33
34 pub async fn process_task(&mut self, request: &str) -> Result<TaskResult, AgentError> {
36 let task_id = uuid::Uuid::new_v4().to_string();
37 let task = Task {
38 id: task_id.clone(),
39 request: request.to_string(),
40 status: TaskStatus::Pending,
41 created_at: chrono::Utc::now(),
42 updated_at: chrono::Utc::now(),
43 result: None,
44 };
45
46 self.execute_task_internal(task).await
47 }
48
49 async fn execute_task_internal(&mut self, mut task: Task) -> Result<TaskResult, AgentError> {
51 task.status = TaskStatus::InProgress;
52 task.updated_at = chrono::Utc::now();
53
54 let plan = self.understand_task(&task.request).await?;
56
57 tracing::info!(
58 "Task plan created: {} steps estimated",
59 plan.estimated_steps.unwrap_or(0)
60 );
61
62 let execution_result = self.execute_task_real(&task.id, plan.clone()).await?;
64
65 let result = TaskResult {
67 success: execution_result.success,
68 summary: execution_result.summary,
69 details: Some(execution_result.details),
70 execution_time: Some(execution_result.execution_time),
71 task_plan: Some(plan),
72 };
73
74 task.result = Some(result.clone());
75 task.status = if result.success {
76 TaskStatus::Completed
77 } else {
78 TaskStatus::Failed
79 };
80 task.updated_at = chrono::Utc::now();
81
82 Ok(result)
83 }
84
85 pub async fn register_tool<T: crate::tools::Tool + 'static>(&mut self, tool: T) {
87 let mut tools = self.tools.lock().await;
88 tools.register(tool);
89 }
90
91 pub async fn get_tools(&self) -> Arc<Mutex<ToolRegistry>> {
93 self.tools.clone()
94 }
95
96 pub fn get_config(&self) -> &AgentConfig {
98 &self.config
99 }
100
101 pub fn get_model(&self) -> &Box<dyn LanguageModel> {
103 &self.model
104 }
105
106 async fn understand_task(&self, request: &str) -> Result<TaskPlan, AgentError> {
108 tracing::info!("🧠 Starting task understanding for: {}", request);
109
110 let prompt = format!(
111 "You are an intelligent coding assistant with full autonomy.
112
113TASK TO ANALYZE: {request}
114
115Please analyze this task and provide:
1161. Your understanding of what the user wants
1172. Your approach to solving it
1183. Assessment of complexity (Simple/Moderate/Complex)
1194. Any requirements or dependencies you identify
120
121You have complete freedom in how to structure your response. Be thorough but concise.
122
123Respond in this format:
124UNDERSTANDING: [your understanding]
125APPROACH: [your approach]
126COMPLEXITY: [Simple/Moderate/Complex]
127REQUIREMENTS: [any requirements or dependencies, or \"None\"]"
128 );
129
130 tracing::debug!("📝 Sending prompt to AI model");
131
132 let response = self
133 .model
134 .complete(&prompt)
135 .await
136 .map_err(|e| AgentError::ModelError(e))?;
137
138 tracing::debug!("🤖 AI model response: {}", response.content);
139
140 let plan = self.parse_task_plan(&response.content)?;
141
142 tracing::info!("📋 Task plan created - Complexity: {:?}, Steps: {}",
143 plan.complexity, plan.estimated_steps.unwrap_or(0));
144
145 Ok(plan)
146 }
147
148 fn parse_task_plan(&self, response: &str) -> Result<TaskPlan, AgentError> {
149 let mut understanding = String::new();
150 let mut approach = String::new();
151 let mut complexity = TaskComplexity::Moderate;
152 let mut requirements = Vec::new();
153
154 for line in response.lines() {
155 let line = line.trim();
156 if line.to_uppercase().starts_with("UNDERSTANDING:") {
157 understanding = line[13..].trim().to_string();
158 } else if line.to_uppercase().starts_with("APPROACH:") {
159 approach = line[9..].trim().to_string();
160 } else if line.to_uppercase().starts_with("COMPLEXITY:") {
161 match line[11..].trim().to_uppercase().as_str() {
162 "SIMPLE" => complexity = TaskComplexity::Simple,
163 "COMPLEX" => complexity = TaskComplexity::Complex,
164 _ => complexity = TaskComplexity::Moderate,
165 }
166 } else if line.to_uppercase().starts_with("REQUIREMENTS:") {
167 let req_text = line[13..].trim();
168 if req_text != "None" {
169 requirements = req_text.split(',').map(|s| s.trim().to_string()).collect();
170 }
171 }
172 }
173
174 let estimated_steps = match complexity {
175 TaskComplexity::Simple => 1,
176 TaskComplexity::Moderate => 5,
177 TaskComplexity::Complex => 10,
178 };
179
180 Ok(TaskPlan {
181 understanding,
182 approach,
183 complexity,
184 estimated_steps: Some(estimated_steps),
185 requirements,
186 })
187 }
188
189 async fn execute_task_real(
191 &mut self,
192 task_id: &str,
193 plan: TaskPlan,
194 ) -> Result<ExecutionResult, AgentError> {
195 tracing::info!("Starting real execution for task: {}", task_id);
196
197 self.execute_simple_task(&plan.understanding).await
203 }
204
205 async fn execute_simple_task(
207 &self,
208 task_understanding: &str,
209 ) -> Result<ExecutionResult, AgentError> {
210 tracing::info!("Executing simple task based on understanding: {}", task_understanding);
211
212 let lower_understanding = task_understanding.to_lowercase();
214
215 if lower_understanding.contains("read") && lower_understanding.contains("file") {
216 if let Some(file_path) = self.extract_file_path(task_understanding) {
218 match self.read_file(&file_path).await {
219 Ok(content) => {
220 return Ok(ExecutionResult {
221 success: true,
222 summary: format!("Successfully read file: {}", file_path),
223 details: content,
224 execution_time: 2,
225 });
226 }
227 Err(e) => {
228 return Ok(ExecutionResult {
229 success: false,
230 summary: format!("Failed to read file: {}", file_path),
231 details: format!("Error: {}", e),
232 execution_time: 1,
233 });
234 }
235 }
236 }
237 }
238
239 if lower_understanding.contains("list") && lower_understanding.contains("file") {
240 match self.list_files(".").await {
242 Ok(files) => {
243 return Ok(ExecutionResult {
244 success: true,
245 summary: "Successfully listed files".to_string(),
246 details: files,
247 execution_time: 1,
248 });
249 }
250 Err(e) => {
251 return Ok(ExecutionResult {
252 success: false,
253 summary: "Failed to list files".to_string(),
254 details: format!("Error: {}", e),
255 execution_time: 1,
256 });
257 }
258 }
259 }
260
261 if lower_understanding.contains("run") && lower_understanding.contains("command") {
262 if let Some(command) = self.extract_command(task_understanding) {
264 match self.run_command(&command).await {
265 Ok(output) => {
266 return Ok(ExecutionResult {
267 success: true,
268 summary: format!("Successfully ran command: {}", command),
269 details: output,
270 execution_time: 3,
271 });
272 }
273 Err(e) => {
274 return Ok(ExecutionResult {
275 success: false,
276 summary: format!("Failed to run command: {}", command),
277 details: format!("Error: {}", e),
278 execution_time: 1,
279 });
280 }
281 }
282 }
283 }
284
285 Ok(ExecutionResult {
287 success: true,
288 summary: "Task completed".to_string(),
289 details: format!("AI Analysis: {}", task_understanding),
290 execution_time: 1,
291 })
292 }
293
294 fn extract_file_path(&self, text: &str) -> Option<String> {
296 let words: Vec<&str> = text.split_whitespace().collect();
298 for (i, word) in words.iter().enumerate() {
299 if *word == "file" && i + 1 < words.len() {
300 let next_word = words[i + 1];
301 if next_word.ends_with(".txt") || next_word.ends_with(".md") ||
302 next_word.ends_with(".rs") || next_word.ends_with(".toml") {
303 return Some(next_word.trim_matches('"').trim_matches('\'').to_string());
304 }
305 }
306 }
307 None
308 }
309
310 fn extract_command(&self, text: &str) -> Option<String> {
312 let lower = text.to_lowercase();
313 if lower.contains("echo") {
314 if let Some(start) = lower.find("echo") {
315 let command_part = &text[start..];
316 if let Some(end) = command_part.find(['\'', '"']) {
317 return Some(command_part[..end].trim().to_string());
318 }
319 return Some(command_part.trim().to_string());
320 }
321 }
322 None
323 }
324
325 async fn read_file(&self, path: &str) -> Result<String, AgentError> {
327 let content = tokio::fs::read_to_string(path)
328 .await
329 .map_err(|e| AgentError::ToolError(ToolError::ExecutionError(e.to_string())))?;
330 Ok(content)
331 }
332
333 async fn list_files(&self, path: &str) -> Result<String, AgentError> {
335 let mut entries = tokio::fs::read_dir(path)
336 .await
337 .map_err(|e| AgentError::ToolError(ToolError::ExecutionError(e.to_string())))?;
338
339 let mut files = Vec::new();
340 while let Some(entry) = entries.next_entry().await
341 .map_err(|e| AgentError::ToolError(ToolError::ExecutionError(e.to_string())))? {
342 let name = entry.file_name().to_string_lossy().to_string();
343 let metadata = entry.metadata().await
344 .map_err(|e| AgentError::ToolError(ToolError::ExecutionError(e.to_string())))?;
345 let file_type = if metadata.is_dir() { "DIR" } else { "FILE" };
346 files.push(format!("{}: {}", file_type, name));
347 }
348
349 files.sort();
350 Ok(files.join("\n"))
351 }
352
353 async fn run_command(&self, command: &str) -> Result<String, AgentError> {
355 let output = tokio::process::Command::new("sh")
356 .arg("-c")
357 .arg(command)
358 .output()
359 .await
360 .map_err(|e| AgentError::ToolError(ToolError::ExecutionError(e.to_string())))?;
361
362 if output.status.success() {
363 Ok(String::from_utf8_lossy(&output.stdout).to_string())
364 } else {
365 Ok(String::from_utf8_lossy(&output.stderr).to_string())
366 }
367 }
368}
369
370pub fn create_agent_with_default_tools(
372 model: Box<dyn LanguageModel>,
373 config: AgentConfig,
374) -> CodeAgent {
375 CodeAgent::new(model, config)
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::models::MockModel;
382 use crate::tools::ReadFileTool;
383
384 #[tokio::test]
385 async fn test_agent_creation() {
386 let model = Box::new(MockModel::new("test".to_string()));
387 let config = AgentConfig::default();
388 let agent = CodeAgent::new(model, config);
389
390 assert_eq!(agent.get_model().model_name(), "test");
391 }
392
393 #[tokio::test]
394 async fn test_tool_registration() {
395 let model = Box::new(MockModel::new("test".to_string()));
396 let config = AgentConfig::default();
397 let mut agent = CodeAgent::new(model, config);
398
399 agent.register_tool(ReadFileTool).await;
400
401 let tools = agent.get_tools().await;
402 let tool_names = tools.lock().await.get_tool_names();
403 assert!(tool_names.contains(&"read_file".to_string()));
404 }
405}