An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
at main 471 lines 19 kB view raw
1use crate::config::Config; 2use crate::llm::factory::create_client; 3use crate::llm::{LlmClient, Message, ResponseContent}; 4use crate::security::{SecurityValidator, permission::CliPermissionHandler}; 5use crate::spec::{Spec, TaskStatus}; 6use crate::tools::ToolRegistry; 7use crate::tools::factory::create_default_registry; 8use crate::tui::messages::{AgentMessage, AgentSender}; 9use anyhow::{Context, Result}; 10use chrono::Utc; 11use std::sync::Arc; 12 13const DEFAULT_MAX_ITERATIONS: usize = 100; 14 15pub struct RalphLoop { 16 client: Arc<dyn LlmClient>, 17 tools: ToolRegistry, 18 pub spec_path: String, 19 max_iterations: usize, 20} 21 22impl RalphLoop { 23 pub fn new(config: Config, spec_path: String, max_iterations: Option<usize>) -> Result<Self> { 24 // Get ralph-specific LLM config 25 let llm_config = config.ralph_llm().clone(); 26 27 // Create LLM client using factory 28 let client = create_client(&config, &llm_config)?; 29 30 // Create security validator and permission handler 31 let validator = Arc::new(SecurityValidator::new(config.security.clone())?); 32 let permission_handler = Arc::new(CliPermissionHandler); 33 34 // Register tools 35 let tools = create_default_registry(validator, permission_handler); 36 37 let max_iterations = max_iterations 38 .or(config.rustagent.max_iterations) 39 .unwrap_or(DEFAULT_MAX_ITERATIONS); 40 41 Ok(Self { 42 client, 43 tools, 44 spec_path, 45 max_iterations, 46 }) 47 } 48 49 pub async fn run(&self) -> Result<()> { 50 println!("Starting Ralph Loop"); 51 println!("Max iterations: {}", self.max_iterations); 52 println!(); 53 54 let mut iteration = 0; 55 56 loop { 57 iteration += 1; 58 if iteration > self.max_iterations { 59 println!("Reached max iterations ({})", self.max_iterations); 60 break; 61 } 62 63 println!("=== Iteration {} ===", iteration); 64 65 // Load spec fresh each iteration 66 let mut spec = Spec::load(&self.spec_path).context("Failed to load spec")?; 67 68 // Find next pending task 69 let task = match spec.find_next_task() { 70 Some(t) => t.clone(), 71 None => { 72 println!("No more pending tasks. All tasks complete!"); 73 break; 74 } 75 }; 76 77 println!("Executing task: {} - {}", task.id, task.title); 78 79 // Mark task as in progress 80 { 81 let task_mut = spec 82 .find_task_mut(&task.id) 83 .context("Task not found in spec")?; 84 task_mut.status = TaskStatus::InProgress; 85 } 86 spec.save(&self.spec_path).context("Failed to save spec")?; 87 88 // Execute the task 89 match self.execute_task(&task.id).await { 90 Ok(signal) => { 91 // Reload spec to get any changes made during execution 92 let mut spec = Spec::load(&self.spec_path)?; 93 94 match signal.as_str() { 95 "TASK_COMPLETE" => { 96 println!("Task completed successfully"); 97 let task_mut = spec 98 .find_task_mut(&task.id) 99 .context("Task not found in spec")?; 100 task_mut.status = TaskStatus::Complete; 101 task_mut.completed_at = Some(Utc::now()); 102 spec.save(&self.spec_path)?; 103 } 104 "TASK_BLOCKED" => { 105 println!("Task is blocked"); 106 let task_mut = spec 107 .find_task_mut(&task.id) 108 .context("Task not found in spec")?; 109 task_mut.status = TaskStatus::Blocked; 110 spec.save(&self.spec_path)?; 111 } 112 _ => { 113 println!("Unknown signal: {}", signal); 114 // Reset to pending to retry 115 let task_mut = spec 116 .find_task_mut(&task.id) 117 .context("Task not found in spec")?; 118 task_mut.status = TaskStatus::Pending; 119 spec.save(&self.spec_path)?; 120 } 121 } 122 } 123 Err(e) => { 124 println!("Error executing task: {}", e); 125 // Reset to pending to retry 126 let mut spec = Spec::load(&self.spec_path)?; 127 let task_mut = spec 128 .find_task_mut(&task.id) 129 .context("Task not found in spec")?; 130 task_mut.status = TaskStatus::Pending; 131 spec.save(&self.spec_path)?; 132 break; 133 } 134 } 135 136 println!(); 137 } 138 139 println!("Ralph Loop finished"); 140 Ok(()) 141 } 142 143 /// Run execution with a message sender for TUI integration 144 pub async fn run_with_sender(&self, tx: AgentSender) -> Result<()> { 145 tx.send(AgentMessage::ExecutionStarted { 146 spec_path: self.spec_path.clone(), 147 }) 148 .await?; 149 150 let mut iteration = 0; 151 152 loop { 153 iteration += 1; 154 if iteration > self.max_iterations { 155 tx.send(AgentMessage::ExecutionError(format!( 156 "Reached max iterations ({})", 157 self.max_iterations 158 ))) 159 .await?; 160 break; 161 } 162 163 let mut spec = Spec::load(&self.spec_path).context("Failed to load spec")?; 164 165 let task = match spec.find_next_task() { 166 Some(t) => t.clone(), 167 None => { 168 tx.send(AgentMessage::ExecutionComplete).await?; 169 break; 170 } 171 }; 172 173 tx.send(AgentMessage::TaskStarted { 174 task_id: task.id.clone(), 175 title: task.title.clone(), 176 }) 177 .await?; 178 179 // Mark task as in progress 180 { 181 let task_mut = spec 182 .find_task_mut(&task.id) 183 .context("Task not found in spec")?; 184 task_mut.status = TaskStatus::InProgress; 185 } 186 spec.save(&self.spec_path).context("Failed to save spec")?; 187 188 match self.execute_task_with_sender(&task.id, &tx).await { 189 Ok((signal, reason)) => { 190 let mut spec = Spec::load(&self.spec_path)?; 191 192 match signal.as_str() { 193 "TASK_COMPLETE" => { 194 let task_mut = 195 spec.find_task_mut(&task.id).context("Task not found")?; 196 task_mut.status = TaskStatus::Complete; 197 task_mut.completed_at = Some(Utc::now()); 198 spec.save(&self.spec_path)?; 199 tx.send(AgentMessage::TaskComplete { task_id: task.id }) 200 .await?; 201 } 202 "TASK_BLOCKED" => { 203 let task_mut = 204 spec.find_task_mut(&task.id).context("Task not found")?; 205 task_mut.status = TaskStatus::Blocked; 206 spec.save(&self.spec_path)?; 207 tx.send(AgentMessage::TaskBlocked { 208 task_id: task.id, 209 reason: reason 210 .unwrap_or_else(|| "Task reported blocked".to_string()), 211 }) 212 .await?; 213 } 214 _ => { 215 let task_mut = 216 spec.find_task_mut(&task.id).context("Task not found")?; 217 task_mut.status = TaskStatus::Pending; 218 spec.save(&self.spec_path)?; 219 tx.send(AgentMessage::TaskResponse(format!( 220 "Unknown signal '{}', resetting task to pending", 221 signal 222 ))) 223 .await?; 224 } 225 } 226 } 227 Err(e) => { 228 tx.send(AgentMessage::ExecutionError(e.to_string())).await?; 229 break; 230 } 231 } 232 } 233 234 Ok(()) 235 } 236 237 async fn execute_task_with_sender( 238 &self, 239 task_id: &str, 240 tx: &AgentSender, 241 ) -> Result<(String, Option<String>)> { 242 let context = self.build_context(task_id)?; 243 let tool_definitions = self.tools.definitions(); 244 245 let mut messages = vec![Message::user(context)]; 246 247 let max_turns = 50; 248 for _turn in 0..max_turns { 249 let response = self 250 .client 251 .chat(messages.clone(), &tool_definitions) 252 .await?; 253 254 match response.content { 255 ResponseContent::Text(text) => { 256 tx.send(AgentMessage::TaskResponse(text.clone())).await?; 257 258 if text.contains("TASK_COMPLETE") { 259 return Ok(("TASK_COMPLETE".to_string(), None)); 260 } 261 if text.contains("TASK_BLOCKED") { 262 return Ok(("TASK_BLOCKED".to_string(), Some(text))); 263 } 264 265 messages.push(Message::assistant(text)); 266 } 267 ResponseContent::ToolCalls(tool_calls) => { 268 for tool_call in &tool_calls { 269 if tool_call.name == "signal_completion" { 270 let tool = self 271 .tools 272 .get(&tool_call.name) 273 .context("signal_completion tool not found")?; 274 let result = tool.execute(tool_call.parameters.clone()).await?; 275 276 if result.starts_with("SIGNAL:complete:") { 277 return Ok(("TASK_COMPLETE".to_string(), None)); 278 } else if result.starts_with("SIGNAL:blocked:") { 279 let reason = result 280 .strip_prefix("SIGNAL:blocked:") 281 .map(|s| s.to_string()); 282 return Ok(("TASK_BLOCKED".to_string(), reason)); 283 } 284 } 285 } 286 287 let mut results = Vec::new(); 288 for tool_call in tool_calls { 289 if tool_call.name == "signal_completion" { 290 continue; 291 } 292 293 tx.send(AgentMessage::TaskToolCall { 294 name: tool_call.name.clone(), 295 args: tool_call.parameters.to_string(), 296 }) 297 .await?; 298 299 let tool = self.tools.get(&tool_call.name).context("Tool not found")?; 300 301 match tool.execute(tool_call.parameters).await { 302 Ok(output) => { 303 tx.send(AgentMessage::TaskToolResult { 304 name: tool_call.name.clone(), 305 output: output.clone(), 306 }) 307 .await?; 308 results 309 .push(format!("Tool: {}\nResult: {}", tool_call.name, output)); 310 } 311 Err(e) => { 312 tx.send(AgentMessage::TaskToolResult { 313 name: tool_call.name.clone(), 314 output: format!("Error: {}", e), 315 }) 316 .await?; 317 results.push(format!("Tool: {}\nError: {}", tool_call.name, e)); 318 } 319 } 320 } 321 322 let results_text = results.join("\n\n"); 323 if !results_text.is_empty() { 324 messages.push(Message::user(results_text)); 325 } 326 } 327 } 328 } 329 330 Err(anyhow::anyhow!("Reached max turns without completion")) 331 } 332 333 async fn execute_task(&self, task_id: &str) -> Result<String> { 334 let context = self.build_context(task_id)?; 335 let tool_definitions = self.tools.definitions(); 336 337 let mut messages = vec![Message::user(context)]; 338 339 // Agentic loop - continue until we get a completion signal 340 let max_turns = 50; 341 for turn in 0..max_turns { 342 println!(" Turn {}", turn + 1); 343 344 let response = self 345 .client 346 .chat(messages.clone(), &tool_definitions) 347 .await?; 348 349 match response.content { 350 ResponseContent::Text(text) => { 351 println!(" Response: {}", text); 352 353 // Check for completion signals 354 if text.contains("TASK_COMPLETE") { 355 return Ok("TASK_COMPLETE".to_string()); 356 } 357 if text.contains("TASK_BLOCKED") { 358 return Ok("TASK_BLOCKED".to_string()); 359 } 360 361 // Add assistant response to conversation 362 messages.push(Message::assistant(text)); 363 } 364 ResponseContent::ToolCalls(tool_calls) => { 365 println!(" Executing {} tool calls", tool_calls.len()); 366 367 // Check for signal_completion tool call first 368 for tool_call in &tool_calls { 369 if tool_call.name == "signal_completion" { 370 let tool = self 371 .tools 372 .get(&tool_call.name) 373 .context("signal_completion tool not found")?; 374 let result = tool.execute(tool_call.parameters.clone()).await?; 375 376 if result.starts_with("SIGNAL:complete:") { 377 return Ok("TASK_COMPLETE".to_string()); 378 } else if result.starts_with("SIGNAL:blocked:") { 379 return Ok("TASK_BLOCKED".to_string()); 380 } 381 } 382 } 383 384 // Execute all other tool calls 385 let mut results = Vec::new(); 386 for tool_call in tool_calls { 387 if tool_call.name == "signal_completion" { 388 continue; 389 } 390 println!(" Tool: {}", tool_call.name); 391 392 let tool = self.tools.get(&tool_call.name).context("Tool not found")?; 393 394 match tool.execute(tool_call.parameters).await { 395 Ok(output) => { 396 results 397 .push(format!("Tool: {}\nResult: {}", tool_call.name, output)); 398 } 399 Err(e) => { 400 results.push(format!("Tool: {}\nError: {}", tool_call.name, e)); 401 } 402 } 403 } 404 405 // Add tool results as user message 406 // Note: Using User role for now as Anthropic expects tool results 407 // in user messages. Future OpenAI provider will use Message::tool_result() 408 let results_text = results.join("\n\n"); 409 if !results_text.is_empty() { 410 messages.push(Message::user(results_text)); 411 } 412 } 413 } 414 } 415 416 Err(anyhow::anyhow!("Reached max turns without completion")) 417 } 418 419 fn build_context(&self, task_id: &str) -> Result<String> { 420 let spec = Spec::load(&self.spec_path)?; 421 422 let task = spec 423 .tasks 424 .iter() 425 .find(|t| t.id == task_id) 426 .context("Task not found")?; 427 428 let mut context = String::new(); 429 430 context.push_str("You are Ralph, an autonomous coding agent.\n\n"); 431 context.push_str("PROJECT CONTEXT:\n"); 432 context.push_str(&format!("Spec: {}\n", spec.name)); 433 context.push_str(&format!("Description: {}\n", spec.description)); 434 context.push_str(&format!("Branch: {}\n\n", spec.branch_name)); 435 436 context.push_str("YOUR TASK:\n"); 437 context.push_str(&format!("ID: {}\n", task.id)); 438 context.push_str(&format!("Title: {}\n", task.title)); 439 context.push_str(&format!("Description: {}\n\n", task.description)); 440 441 if !task.acceptance_criteria.is_empty() { 442 context.push_str("ACCEPTANCE CRITERIA:\n"); 443 for criterion in &task.acceptance_criteria { 444 context.push_str(&format!("- {}\n", criterion)); 445 } 446 context.push('\n'); 447 } 448 449 if !spec.learnings.is_empty() { 450 context.push_str("LEARNINGS FROM PREVIOUS TASKS:\n"); 451 for learning in &spec.learnings { 452 context.push_str(&format!("- {}\n", learning)); 453 } 454 context.push('\n'); 455 } 456 457 context.push_str("INSTRUCTIONS:\n"); 458 context.push_str("1. Execute the task using available tools\n"); 459 context.push_str("2. Use read_file to examine code\n"); 460 context.push_str("3. Use write_file to create/modify files\n"); 461 context.push_str("4. Use run_command to run tests, builds, git commands\n"); 462 context.push_str("5. When complete, call signal_completion with signal='complete'\n"); 463 context.push_str( 464 "6. If blocked, call signal_completion with signal='blocked' and explain why\n", 465 ); 466 context.push('\n'); 467 context.push_str("Begin executing the task now.\n"); 468 469 Ok(context) 470 } 471}