|
| 1 | +# RALPH-loop: Iterative Self-Referential AI Loops |
| 2 | + |
| 3 | +Implement self-referential feedback loops where an AI agent iteratively improves work by reading its own previous output. |
| 4 | + |
| 5 | +> **Runnable example:** [recipe/ralph-loop.cs](recipe/ralph-loop.cs) |
| 6 | +> |
| 7 | +> ```bash |
| 8 | +> cd dotnet/recipe |
| 9 | +> dotnet run ralph-loop.cs |
| 10 | +> ``` |
| 11 | +
|
| 12 | +## What is RALPH-loop? |
| 13 | +
|
| 14 | +RALPH-loop is a development methodology for iterative AI-powered task completion. Named after the Ralph Wiggum technique, it embodies the philosophy of persistent iteration: |
| 15 | +
|
| 16 | +- **One prompt, multiple iterations**: The same prompt is processed repeatedly |
| 17 | +- **Self-referential feedback**: The AI reads its own previous work (file changes, git history) |
| 18 | +- **Completion detection**: Loop exits when a completion promise is detected in output |
| 19 | +- **Safety limits**: Always include a maximum iteration count to prevent infinite loops |
| 20 | +
|
| 21 | +## Example Scenario |
| 22 | +
|
| 23 | +You need to iteratively improve code until all tests pass. Instead of asking Claude to "write perfect code," you use RALPH-loop to: |
| 24 | +
|
| 25 | +1. Send the initial prompt with clear success criteria |
| 26 | +2. Claude writes code and tests |
| 27 | +3. Claude runs tests and sees failures |
| 28 | +4. Loop automatically re-sends the prompt |
| 29 | +5. Claude reads test output and previous code, fixes issues |
| 30 | +6. Repeat until all tests pass and completion promise is output |
| 31 | +
|
| 32 | +## Basic Implementation |
| 33 | +
|
| 34 | +```csharp |
| 35 | +using GitHub.Copilot.SDK; |
| 36 | +
|
| 37 | +public class RalphLoop |
| 38 | +{ |
| 39 | + private readonly CopilotClient _client; |
| 40 | + private int _iteration = 0; |
| 41 | + private readonly int _maxIterations; |
| 42 | + private readonly string _completionPromise; |
| 43 | + private string? _lastResponse; |
| 44 | +
|
| 45 | + public RalphLoop(int maxIterations = 10, string completionPromise = "COMPLETE") |
| 46 | + { |
| 47 | + _client = new CopilotClient(); |
| 48 | + _maxIterations = maxIterations; |
| 49 | + _completionPromise = completionPromise; |
| 50 | + } |
| 51 | +
|
| 52 | + public async Task<string> RunAsync(string prompt) |
| 53 | + { |
| 54 | + await _client.StartAsync(); |
| 55 | + var session = await _client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" }); |
| 56 | +
|
| 57 | + try |
| 58 | + { |
| 59 | + while (_iteration < _maxIterations) |
| 60 | + { |
| 61 | + _iteration++; |
| 62 | + Console.WriteLine($"\n--- Iteration {_iteration} ---"); |
| 63 | +
|
| 64 | + var done = new TaskCompletionSource<string>(); |
| 65 | + session.On(evt => |
| 66 | + { |
| 67 | + if (evt is AssistantMessageEvent msg) |
| 68 | + { |
| 69 | + _lastResponse = msg.Data.Content; |
| 70 | + done.SetResult(msg.Data.Content); |
| 71 | + } |
| 72 | + }); |
| 73 | +
|
| 74 | + // Send prompt (on first iteration) or continuation |
| 75 | + var messagePrompt = _iteration == 1 |
| 76 | + ? prompt |
| 77 | + : $"{prompt}\n\nPrevious attempt:\n{_lastResponse}\n\nContinue iterating..."; |
| 78 | +
|
| 79 | + await session.SendAsync(new MessageOptions { Prompt = messagePrompt }); |
| 80 | + var response = await done.Task; |
| 81 | +
|
| 82 | + // Check for completion promise |
| 83 | + if (response.Contains(_completionPromise)) |
| 84 | + { |
| 85 | + Console.WriteLine($"✓ Completion promise detected: {_completionPromise}"); |
| 86 | + return response; |
| 87 | + } |
| 88 | +
|
| 89 | + Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); |
| 90 | + } |
| 91 | +
|
| 92 | + throw new InvalidOperationException( |
| 93 | + $"Max iterations ({_maxIterations}) reached without completion promise"); |
| 94 | + } |
| 95 | + finally |
| 96 | + { |
| 97 | + await session.DisposeAsync(); |
| 98 | + await _client.StopAsync(); |
| 99 | + } |
| 100 | + } |
| 101 | +} |
| 102 | +
|
| 103 | +// Usage |
| 104 | +var loop = new RalphLoop(maxIterations: 5, completionPromise: "DONE"); |
| 105 | +var result = await loop.RunAsync("Your task here"); |
| 106 | +Console.WriteLine(result); |
| 107 | +``` |
| 108 | +
|
| 109 | +## With File Persistence |
| 110 | +
|
| 111 | +For tasks involving code generation, persist state to files so the AI can see changes: |
| 112 | +
|
| 113 | +```csharp |
| 114 | +public class PersistentRalphLoop |
| 115 | +{ |
| 116 | + private readonly string _workDir; |
| 117 | + private readonly CopilotClient _client; |
| 118 | + private int _iteration = 0; |
| 119 | +
|
| 120 | + public PersistentRalphLoop(string workDir, int maxIterations = 10) |
| 121 | + { |
| 122 | + _workDir = workDir; |
| 123 | + Directory.CreateDirectory(_workDir); |
| 124 | + _client = new CopilotClient(); |
| 125 | + } |
| 126 | +
|
| 127 | + public async Task<string> RunAsync(string prompt) |
| 128 | + { |
| 129 | + await _client.StartAsync(); |
| 130 | + var session = await _client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" }); |
| 131 | +
|
| 132 | + try |
| 133 | + { |
| 134 | + // Store initial prompt |
| 135 | + var promptFile = Path.Combine(_workDir, "prompt.md"); |
| 136 | + await File.WriteAllTextAsync(promptFile, prompt); |
| 137 | +
|
| 138 | + while (_iteration < 10) |
| 139 | + { |
| 140 | + _iteration++; |
| 141 | + Console.WriteLine($"\n--- Iteration {_iteration} ---"); |
| 142 | +
|
| 143 | + // Build context including previous work |
| 144 | + var contextBuilder = new StringBuilder(prompt); |
| 145 | + var previousOutput = Path.Combine(_workDir, $"output-{_iteration - 1}.txt"); |
| 146 | + if (File.Exists(previousOutput)) |
| 147 | + { |
| 148 | + contextBuilder.AppendLine($"\nPrevious iteration output:\n{await File.ReadAllTextAsync(previousOutput)}"); |
| 149 | + } |
| 150 | +
|
| 151 | + var done = new TaskCompletionSource<string>(); |
| 152 | + string response = ""; |
| 153 | + session.On(evt => |
| 154 | + { |
| 155 | + if (evt is AssistantMessageEvent msg) |
| 156 | + { |
| 157 | + response = msg.Data.Content; |
| 158 | + done.SetResult(msg.Data.Content); |
| 159 | + } |
| 160 | + }); |
| 161 | +
|
| 162 | + await session.SendAsync(new MessageOptions { Prompt = contextBuilder.ToString() }); |
| 163 | + await done.Task; |
| 164 | +
|
| 165 | + // Persist output |
| 166 | + await File.WriteAllTextAsync( |
| 167 | + Path.Combine(_workDir, $"output-{_iteration}.txt"), |
| 168 | + response); |
| 169 | +
|
| 170 | + if (response.Contains("COMPLETE")) |
| 171 | + { |
| 172 | + return response; |
| 173 | + } |
| 174 | + } |
| 175 | +
|
| 176 | + throw new InvalidOperationException("Max iterations reached"); |
| 177 | + } |
| 178 | + finally |
| 179 | + { |
| 180 | + await session.DisposeAsync(); |
| 181 | + await _client.StopAsync(); |
| 182 | + } |
| 183 | + } |
| 184 | +} |
| 185 | +``` |
| 186 | +
|
| 187 | +## Best Practices |
| 188 | +
|
| 189 | +1. **Write clear completion criteria**: Include exactly what "done" looks like |
| 190 | +2. **Use output markers**: Include `<promise>COMPLETE</promise>` or similar in completion condition |
| 191 | +3. **Always set max iterations**: Prevents infinite loops on impossible tasks |
| 192 | +4. **Persist state**: Save files so AI can see what changed between iterations |
| 193 | +5. **Include context**: Feed previous iteration output back as context |
| 194 | +6. **Monitor progress**: Log each iteration to track what's happening |
| 195 | +
|
| 196 | +## Example: Iterative Code Generation |
| 197 | +
|
| 198 | +```csharp |
| 199 | +var prompt = @"Write a function that: |
| 200 | +1. Parses CSV data |
| 201 | +2. Validates required fields |
| 202 | +3. Returns parsed records or error |
| 203 | +4. Has unit tests |
| 204 | +5. Output <promise>COMPLETE</promise> when done"; |
| 205 | +
|
| 206 | +var loop = new RalphLoop(maxIterations: 10, completionPromise: "COMPLETE"); |
| 207 | +var result = await loop.RunAsync(prompt); |
| 208 | +``` |
| 209 | +
|
| 210 | +## Handling Failures |
| 211 | +
|
| 212 | +```csharp |
| 213 | +try |
| 214 | +{ |
| 215 | + var result = await loop.RunAsync(prompt); |
| 216 | + Console.WriteLine("Task completed successfully!"); |
| 217 | +} |
| 218 | +catch (InvalidOperationException ex) when (ex.Message.Contains("Max iterations")) |
| 219 | +{ |
| 220 | + Console.WriteLine("Task did not complete within iteration limit."); |
| 221 | + Console.WriteLine($"Last response: {loop.LastResponse}"); |
| 222 | + // Document what was attempted and suggest alternatives |
| 223 | +} |
| 224 | +``` |
| 225 | +
|
| 226 | +## When to Use RALPH-loop |
| 227 | +
|
| 228 | +**Good for:** |
| 229 | +- Code generation with automatic verification (tests, linters) |
| 230 | +- Tasks with clear success criteria |
| 231 | +- Iterative refinement where each attempt learns from previous failures |
| 232 | +- Unattended long-running improvements |
| 233 | +
|
| 234 | +**Not good for:** |
| 235 | +- Tasks requiring human judgment or design input |
| 236 | +- One-shot operations |
| 237 | +- Tasks with vague success criteria |
| 238 | +- Real-time interactive debugging |
0 commit comments