An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
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}