Rewrite Ralph loop recipes: split into simple vs ideal versions

Align all 4 language recipes (Node.js, Python, .NET, Go) with the
Ralph Playbook architecture:

- Simple version: minimal outer loop with fresh session per iteration
- Ideal version: planning/building modes, backpressure, git integration
- Fresh context isolation instead of in-session context accumulation
- Disk-based shared state via IMPLEMENTATION_PLAN.md
- Example prompt templates (PROMPT_plan.md, PROMPT_build.md, AGENTS.md)
- Updated cookbook README descriptions
This commit is contained in:
Anthony Shaw
2026-02-11 11:28:41 -08:00
parent ab82accc08
commit 952372c1ec
9 changed files with 1052 additions and 1122 deletions

View File

@@ -1,6 +1,6 @@
# RALPH-loop: Iterative Self-Referential AI Loops
# Ralph Loop: Autonomous AI Task Loops
Implement self-referential feedback loops where an AI agent iteratively improves work by reading its own previous output.
Build autonomous coding loops where an AI agent picks tasks, implements them, validates against backpressure (tests, builds), commits, and repeats — each iteration in a fresh context window.
> **Runnable example:** [recipe/ralph-loop.cs](recipe/ralph-loop.cs)
>
@@ -9,252 +9,250 @@ Implement self-referential feedback loops where an AI agent iteratively improves
> dotnet run recipe/ralph-loop.cs
> ```
## What is RALPH-loop?
## What is a Ralph Loop?
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:
A [Ralph loop](https://ghuntley.com/ralph/) is an autonomous development workflow where an AI agent iterates through tasks in isolated context windows. The key insight: **state lives on disk, not in the model's context**. Each iteration starts fresh, reads the current state from files, does one task, writes results back to disk, and exits.
- **One prompt, multiple iterations**: The same prompt is processed repeatedly
- **Self-referential feedback**: The AI reads its own previous work (file changes, git history)
- **Completion detection**: Loop exits when a completion promise is detected in output
- **Safety limits**: Always include a maximum iteration count to prevent infinite loops
```
┌─────────────────────────────────────────────────┐
│ loop.sh │
│ while true: │
│ ┌─────────────────────────────────────────┐ │
│ │ Fresh session (isolated context) │ │
│ │ │ │
│ │ 1. Read PROMPT.md + AGENTS.md │ │
│ │ 2. Study specs/* and code │ │
│ │ 3. Pick next task from plan │ │
│ │ 4. Implement + run tests │ │
│ │ 5. Update plan, commit, exit │ │
│ └─────────────────────────────────────────┘ │
│ ↻ next iteration (fresh context) │
└─────────────────────────────────────────────────┘
```
## Example Scenario
**Core principles:**
You need to iteratively improve code until all tests pass. Instead of asking the model to "write perfect code," you use RALPH-loop to:
- **Fresh context per iteration**: Each loop creates a new session — no context accumulation, always in the "smart zone"
- **Disk as shared state**: `IMPLEMENTATION_PLAN.md` persists between iterations and acts as the coordination mechanism
- **Backpressure steers quality**: Tests, builds, and lints reject bad work — the agent must fix issues before committing
- **Two modes**: PLANNING (gap analysis → generate plan) and BUILDING (implement from plan)
1. Send the initial prompt with clear success criteria
2. The model writes code and tests
3. The model runs tests and sees failures
4. Loop automatically re-sends the prompt
5. The model reads test output and previous code, fixes issues
6. Repeat until all tests pass and completion promise is output
## Simple Version
## Basic Implementation
The minimal Ralph loop — the SDK equivalent of `while :; do cat PROMPT.md | claude ; done`:
```csharp
using GitHub.Copilot.SDK;
public class RalphLoop
var client = new CopilotClient();
await client.StartAsync();
try
{
private readonly CopilotClient _client;
private int _iteration = 0;
private readonly int _maxIterations;
private readonly string _completionPromise;
private string? _lastResponse;
var prompt = await File.ReadAllTextAsync("PROMPT.md");
var maxIterations = 50;
public RalphLoop(int maxIterations = 10, string completionPromise = "COMPLETE")
for (var i = 1; i <= maxIterations; i++)
{
_client = new CopilotClient();
_maxIterations = maxIterations;
_completionPromise = completionPromise;
}
public async Task<string> RunAsync(string prompt)
{
await _client.StartAsync();
Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ===");
// Fresh session each iteration — context isolation is the point
var session = await client.CreateSessionAsync(
new SessionConfig { Model = "claude-sonnet-4.5" });
try
{
var session = await _client.CreateSessionAsync(
new SessionConfig { Model = "gpt-5.1-codex-mini" });
try
var done = new TaskCompletionSource<string>();
session.On(evt =>
{
var done = new TaskCompletionSource<string>();
session.On(evt =>
{
if (evt is AssistantMessageEvent msg)
{
_lastResponse = msg.Data.Content;
done.TrySetResult(msg.Data.Content);
}
});
if (evt is AssistantMessageEvent msg)
done.TrySetResult(msg.Data.Content);
});
while (_iteration < _maxIterations)
{
_iteration++;
Console.WriteLine($"\n--- Iteration {_iteration} ---");
done = new TaskCompletionSource<string>();
// Send prompt (on first iteration) or continuation
var messagePrompt = _iteration == 1
? prompt
: $"{prompt}\n\nPrevious attempt:\n{_lastResponse}\n\nContinue iterating...";
await session.SendAsync(new MessageOptions { Prompt = messagePrompt });
var response = await done.Task;
// Check for completion promise
if (response.Contains(_completionPromise))
{
Console.WriteLine($"✓ Completion promise detected: {_completionPromise}");
return response;
}
Console.WriteLine($"Iteration {_iteration} complete. Continuing...");
}
throw new InvalidOperationException(
$"Max iterations ({_maxIterations}) reached without completion promise");
}
finally
{
await session.DisposeAsync();
}
await session.SendAsync(new MessageOptions { Prompt = prompt });
await done.Task;
}
finally
{
await _client.StopAsync();
await session.DisposeAsync();
}
Console.WriteLine($"Iteration {i} complete.");
}
}
// Usage
var loop = new RalphLoop(maxIterations: 5, completionPromise: "COMPLETE");
var result = await loop.RunAsync("Your task here");
Console.WriteLine(result);
finally
{
await client.StopAsync();
}
```
## With File Persistence
This is all you need to get started. The prompt file tells the agent what to do; the agent reads project files, does work, commits, and exits. The loop restarts with a clean slate.
For tasks involving code generation, persist state to files so the AI can see changes:
## Ideal Version
The full Ralph pattern with planning and building modes, matching the [Ralph Playbook](https://github.com/ClaytonFarr/ralph-playbook) architecture:
```csharp
public class PersistentRalphLoop
using System.Diagnostics;
using GitHub.Copilot.SDK;
// Parse args: dotnet run [plan] [max_iterations]
var mode = args.Contains("plan") ? "plan" : "build";
var maxArg = args.FirstOrDefault(a => int.TryParse(a, out _));
var maxIterations = maxArg != null ? int.Parse(maxArg) : 50;
var promptFile = mode == "plan" ? "PROMPT_plan.md" : "PROMPT_build.md";
var client = new CopilotClient();
await client.StartAsync();
var branchInfo = new ProcessStartInfo("git", "branch --show-current")
{ RedirectStandardOutput = true };
var branch = Process.Start(branchInfo)!;
var branchName = (await branch.StandardOutput.ReadToEndAsync()).Trim();
await branch.WaitForExitAsync();
Console.WriteLine(new string('━', 40));
Console.WriteLine($"Mode: {mode}");
Console.WriteLine($"Prompt: {promptFile}");
Console.WriteLine($"Branch: {branchName}");
Console.WriteLine($"Max: {maxIterations} iterations");
Console.WriteLine(new string('━', 40));
try
{
private readonly string _workDir;
private readonly CopilotClient _client;
private readonly int _maxIterations;
private int _iteration = 0;
var prompt = await File.ReadAllTextAsync(promptFile);
public PersistentRalphLoop(string workDir, int maxIterations = 10)
for (var i = 1; i <= maxIterations; i++)
{
_workDir = workDir;
_maxIterations = maxIterations;
Directory.CreateDirectory(_workDir);
_client = new CopilotClient();
}
public async Task<string> RunAsync(string prompt)
{
await _client.StartAsync();
Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ===");
// Fresh session — each task gets full context budget
var session = await client.CreateSessionAsync(
new SessionConfig { Model = "claude-sonnet-4.5" });
try
{
var session = await _client.CreateSessionAsync(
new SessionConfig { Model = "gpt-5.1-codex-mini" });
try
var done = new TaskCompletionSource<string>();
session.On(evt =>
{
// Store initial prompt
var promptFile = Path.Combine(_workDir, "prompt.md");
await File.WriteAllTextAsync(promptFile, prompt);
if (evt is AssistantMessageEvent msg)
done.TrySetResult(msg.Data.Content);
});
var done = new TaskCompletionSource<string>();
string response = "";
session.On(evt =>
{
if (evt is AssistantMessageEvent msg)
{
response = msg.Data.Content;
done.TrySetResult(msg.Data.Content);
}
});
while (_iteration < _maxIterations)
{
_iteration++;
Console.WriteLine($"\n--- Iteration {_iteration} ---");
done = new TaskCompletionSource<string>();
// Build context including previous work
var contextBuilder = new StringBuilder(prompt);
var previousOutput = Path.Combine(_workDir, $"output-{_iteration - 1}.txt");
if (File.Exists(previousOutput))
{
contextBuilder.AppendLine($"\nPrevious iteration output:\n{await File.ReadAllTextAsync(previousOutput)}");
}
await session.SendAsync(new MessageOptions { Prompt = contextBuilder.ToString() });
await done.Task;
// Persist output
await File.WriteAllTextAsync(
Path.Combine(_workDir, $"output-{_iteration}.txt"),
response);
if (response.Contains("COMPLETE"))
{
return response;
}
}
throw new InvalidOperationException("Max iterations reached");
}
finally
{
await session.DisposeAsync();
}
await session.SendAsync(new MessageOptions { Prompt = prompt });
await done.Task;
}
finally
{
await _client.StopAsync();
await session.DisposeAsync();
}
// Push changes after each iteration
try
{
Process.Start("git", $"push origin {branchName}")!.WaitForExit();
}
catch
{
Process.Start("git", $"push -u origin {branchName}")!.WaitForExit();
}
Console.WriteLine($"\nIteration {i} complete.");
}
Console.WriteLine($"\nReached max iterations: {maxIterations}");
}
finally
{
await client.StopAsync();
}
```
### Required Project Files
The ideal version expects this file structure in your project:
```
project-root/
├── PROMPT_plan.md # Planning mode instructions
├── PROMPT_build.md # Building mode instructions
├── AGENTS.md # Operational guide (build/test commands)
├── IMPLEMENTATION_PLAN.md # Task list (generated by planning mode)
├── specs/ # Requirement specs (one per topic)
│ ├── auth.md
│ └── data-pipeline.md
└── src/ # Your source code
```
### Example `PROMPT_plan.md`
```markdown
0a. Study `specs/*` to learn the application specifications.
0b. Study IMPLEMENTATION_PLAN.md (if present) to understand the plan so far.
0c. Study `src/` to understand existing code and shared utilities.
1. Compare specs against code (gap analysis). Create or update
IMPLEMENTATION_PLAN.md as a prioritized bullet-point list of tasks
yet to be implemented. Do NOT implement anything.
IMPORTANT: Do NOT assume functionality is missing — search the
codebase first to confirm. Prefer updating existing utilities over
creating ad-hoc copies.
```
### Example `PROMPT_build.md`
```markdown
0a. Study `specs/*` to learn the application specifications.
0b. Study IMPLEMENTATION_PLAN.md.
0c. Study `src/` for reference.
1. Choose the most important item from IMPLEMENTATION_PLAN.md. Before
making changes, search the codebase (don't assume not implemented).
2. After implementing, run the tests. If functionality is missing, add it.
3. When you discover issues, update IMPLEMENTATION_PLAN.md immediately.
4. When tests pass, update IMPLEMENTATION_PLAN.md, then `git add -A`
then `git commit` with a descriptive message.
99999. When authoring documentation, capture the why.
999999. Implement completely. No placeholders or stubs.
9999999. Keep IMPLEMENTATION_PLAN.md current — future iterations depend on it.
```
### Example `AGENTS.md`
Keep this brief (~60 lines). It's loaded every iteration, so bloat wastes context.
```markdown
## Build & Run
dotnet build
## Validation
- Tests: `dotnet test`
- Build: `dotnet build --no-restore`
```
## Best Practices
1. **Write clear completion criteria**: Include exactly what "done" looks like
2. **Use output markers**: Include `<promise>COMPLETE</promise>` or similar in completion condition
3. **Always set max iterations**: Prevents infinite loops on impossible tasks
4. **Persist state**: Save files so AI can see what changed between iterations
5. **Include context**: Feed previous iteration output back as context
6. **Monitor progress**: Log each iteration to track what's happening
1. **Fresh context per iteration**: Never accumulate context across iterations — that's the whole point
2. **Disk is your database**: `IMPLEMENTATION_PLAN.md` is shared state between isolated sessions
3. **Backpressure is essential**: Tests, builds, lints in `AGENTS.md` — the agent must pass them before committing
4. **Start with PLANNING mode**: Generate the plan first, then switch to BUILDING
5. **Observe and tune**: Watch early iterations, add guardrails to prompts when the agent fails in specific ways
6. **The plan is disposable**: If the agent goes off track, delete `IMPLEMENTATION_PLAN.md` and re-plan
7. **Keep `AGENTS.md` brief**: It's loaded every iteration — operational info only, no progress notes
8. **Use a sandbox**: The agent runs autonomously with full tool access — isolate it
## Example: Iterative Code Generation
```csharp
var prompt = @"Write a function that:
1. Parses CSV data
2. Validates required fields
3. Returns parsed records or error
4. Has unit tests
5. Output <promise>COMPLETE</promise> when done";
var loop = new RalphLoop(maxIterations: 10, completionPromise: "COMPLETE");
var result = await loop.RunAsync(prompt);
```
## Handling Failures
```csharp
try
{
var result = await loop.RunAsync(prompt);
Console.WriteLine("Task completed successfully!");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("Max iterations"))
{
Console.WriteLine("Task did not complete within iteration limit.");
Console.WriteLine($"Last response: {loop.LastResponse}");
// Document what was attempted and suggest alternatives
}
```
## When to Use RALPH-loop
## When to Use a Ralph Loop
**Good for:**
- Code generation with automatic verification (tests, linters)
- Tasks with clear success criteria
- Iterative refinement where each attempt learns from previous failures
- Unattended long-running improvements
- Implementing features from specs with test-driven validation
- Large refactors broken into many small tasks
- Unattended, long-running development with clear requirements
- Any work where backpressure (tests/builds) can verify correctness
**Not good for:**
- Tasks requiring human judgment or design input
- One-shot operations
- Tasks with vague success criteria
- Real-time interactive debugging
- Tasks requiring human judgment mid-loop
- One-shot operations that don't benefit from iteration
- Vague requirements without testable acceptance criteria
- Exploratory prototyping where direction isn't clear

View File

@@ -1,141 +1,90 @@
#:package GitHub.Copilot.SDK@*
#:property PublishAot=false
using System.Diagnostics;
using GitHub.Copilot.SDK;
using System.Text;
// RALPH-loop: Iterative self-referential AI loops.
// The same prompt is sent repeatedly, with AI reading its own previous output.
// Loop continues until completion promise is detected in the response.
// Ralph loop: autonomous AI task loop with fresh context per iteration.
//
// Two modes:
// - "plan": reads PROMPT_plan.md, generates/updates IMPLEMENTATION_PLAN.md
// - "build": reads PROMPT_build.md, implements tasks, runs tests, commits
//
// Each iteration creates a fresh session so the agent always operates in
// the "smart zone" of its context window. State is shared between
// iterations via files on disk (IMPLEMENTATION_PLAN.md, AGENTS.md, specs/*).
//
// Usage:
// dotnet run # build mode, 50 iterations
// dotnet run plan # planning mode
// dotnet run 20 # build mode, 20 iterations
// dotnet run plan 5 # planning mode, 5 iterations
var prompt = @"You are iteratively building a small library. Follow these phases IN ORDER.
Do NOT skip ahead — only do the current phase, then stop and wait for the next iteration.
var mode = args.Contains("plan") ? "plan" : "build";
var maxArg = args.FirstOrDefault(a => int.TryParse(a, out _));
var maxIterations = maxArg != null ? int.Parse(maxArg) : 50;
var promptFile = mode == "plan" ? "PROMPT_plan.md" : "PROMPT_build.md";
Phase 1: Design a DataValidator class that validates records against a schema.
- Schema defines field names, types (string, int, float, bool), and whether required.
- Return a list of validation errors per record.
- Show the class code only. Do NOT output COMPLETE.
var client = new CopilotClient();
await client.StartAsync();
Phase 2: Write at least 4 unit tests covering: missing required field, wrong type,
valid record, and empty input. Show test code only. Do NOT output COMPLETE.
var branchProc = Process.Start(new ProcessStartInfo("git", "branch --show-current")
{ RedirectStandardOutput = true })!;
var branch = (await branchProc.StandardOutput.ReadToEndAsync()).Trim();
await branchProc.WaitForExitAsync();
Phase 3: Review the code from phases 1 and 2. Fix any bugs, add docstrings, and add
an extra edge-case test. Show the final consolidated code with all fixes.
When this phase is fully done, output the exact text: COMPLETE";
var loop = new RalphLoop(maxIterations: 5, completionPromise: "COMPLETE");
Console.WriteLine(new string('━', 40));
Console.WriteLine($"Mode: {mode}");
Console.WriteLine($"Prompt: {promptFile}");
Console.WriteLine($"Branch: {branch}");
Console.WriteLine($"Max: {maxIterations} iterations");
Console.WriteLine(new string('━', 40));
try
{
var result = await loop.RunAsync(prompt);
Console.WriteLine("\n=== FINAL RESULT ===");
Console.WriteLine(result);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"\nTask did not complete: {ex.Message}");
if (loop.LastResponse != null)
var prompt = await File.ReadAllTextAsync(promptFile);
for (var i = 1; i <= maxIterations; i++)
{
Console.WriteLine($"\nLast attempt:\n{loop.LastResponse}");
}
}
Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ===");
// --- RalphLoop class definition ---
public class RalphLoop
{
private readonly CopilotClient _client;
private int _iteration = 0;
private readonly int _maxIterations;
private readonly string _completionPromise;
private string? _lastResponse;
public RalphLoop(int maxIterations = 10, string completionPromise = "COMPLETE")
{
_client = new CopilotClient();
_maxIterations = maxIterations;
_completionPromise = completionPromise;
}
public string? LastResponse => _lastResponse;
public async Task<string> RunAsync(string initialPrompt)
{
await _client.StartAsync();
// Fresh session — each task gets full context budget
var session = await client.CreateSessionAsync(
new SessionConfig { Model = "claude-sonnet-4.5" });
try
{
var session = await _client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5.1-codex-mini"
var done = new TaskCompletionSource<string>();
session.On(evt =>
{
if (evt is AssistantMessageEvent msg)
done.TrySetResult(msg.Data.Content);
});
try
{
var done = new TaskCompletionSource<string>();
session.On(evt =>
{
if (evt is AssistantMessageEvent msg)
{
_lastResponse = msg.Data.Content;
done.TrySetResult(msg.Data.Content);
}
});
while (_iteration < _maxIterations)
{
_iteration++;
Console.WriteLine($"\n=== Iteration {_iteration}/{_maxIterations} ===");
done = new TaskCompletionSource<string>();
var currentPrompt = BuildIterationPrompt(initialPrompt);
Console.WriteLine($"Sending prompt (length: {currentPrompt.Length})...");
await session.SendAsync(new MessageOptions { Prompt = currentPrompt });
var response = await done.Task;
var summary = response.Length > 200
? response.Substring(0, 200) + "..."
: response;
Console.WriteLine($"Response: {summary}");
if (response.Contains(_completionPromise))
{
Console.WriteLine($"\n✓ Completion promise detected: '{_completionPromise}'");
return response;
}
Console.WriteLine($"Iteration {_iteration} complete. Continuing...");
}
throw new InvalidOperationException(
$"Max iterations ({_maxIterations}) reached without completion promise: '{_completionPromise}'");
}
finally
{
await session.DisposeAsync();
}
await session.SendAsync(new MessageOptions { Prompt = prompt });
await done.Task;
}
finally
{
await _client.StopAsync();
await session.DisposeAsync();
}
// Push changes after each iteration
try
{
Process.Start("git", $"push origin {branch}")!.WaitForExit();
}
catch
{
Process.Start("git", $"push -u origin {branch}")!.WaitForExit();
}
Console.WriteLine($"\nIteration {i} complete.");
}
private string BuildIterationPrompt(string initialPrompt)
{
if (_iteration == 1)
return initialPrompt;
var sb = new StringBuilder();
sb.AppendLine(initialPrompt);
sb.AppendLine();
sb.AppendLine("=== CONTEXT FROM PREVIOUS ITERATION ===");
sb.AppendLine(_lastResponse);
sb.AppendLine("=== END CONTEXT ===");
sb.AppendLine();
sb.AppendLine("Continue working on this task. Review the previous attempt and improve upon it.");
return sb.ToString();
}
Console.WriteLine($"\nReached max iterations: {maxIterations}");
}
finally
{
await client.StopAsync();
}