From ab82accc085952e64d961b2b37b7936d62437a0a Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Wed, 11 Feb 2026 06:35:14 -0800 Subject: [PATCH] Address review feedback: fix event handler leak, error handling, model alignment - Move session.On handler outside loop to prevent handler accumulation (C#) - Use TrySetResult instead of SetResult to avoid duplicate-set exceptions (C#) - Wrap CreateSessionAsync in broader try/finally so client always stops (C#) - Fix PersistentRalphLoop to use maxIterations parameter instead of hardcoded 10 - Align model name to gpt-5.1-codex-mini across all doc snippets - Fix completion promise DONE -> COMPLETE in usage snippet - Replace Claude references with generic model terminology --- cookbook/copilot-sdk/dotnet/ralph-loop.md | 138 ++++++++++-------- .../copilot-sdk/dotnet/recipe/ralph-loop.cs | 65 +++++---- cookbook/copilot-sdk/nodejs/ralph-loop.md | 4 +- 3 files changed, 119 insertions(+), 88 deletions(-) diff --git a/cookbook/copilot-sdk/dotnet/ralph-loop.md b/cookbook/copilot-sdk/dotnet/ralph-loop.md index 5580e0f4..12aa0138 100644 --- a/cookbook/copilot-sdk/dotnet/ralph-loop.md +++ b/cookbook/copilot-sdk/dotnet/ralph-loop.md @@ -20,13 +20,13 @@ RALPH-loop is a development methodology for iterative AI-powered task completion ## Example Scenario -You need to iteratively improve code until all tests pass. Instead of asking Claude to "write perfect code," you use RALPH-loop to: +You need to iteratively improve code until all tests pass. Instead of asking the model to "write perfect code," you use RALPH-loop to: 1. Send the initial prompt with clear success criteria -2. Claude writes code and tests -3. Claude runs tests and sees failures +2. The model writes code and tests +3. The model runs tests and sees failures 4. Loop automatically re-sends the prompt -5. Claude reads test output and previous code, fixes issues +5. The model reads test output and previous code, fixes issues 6. Repeat until all tests pass and completion promise is output ## Basic Implementation @@ -52,56 +52,66 @@ public class RalphLoop public async Task RunAsync(string prompt) { await _client.StartAsync(); - var session = await _client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" }); try { - while (_iteration < _maxIterations) - { - _iteration++; - Console.WriteLine($"\n--- Iteration {_iteration} ---"); + var session = await _client.CreateSessionAsync( + new SessionConfig { Model = "gpt-5.1-codex-mini" }); + try + { var done = new TaskCompletionSource(); session.On(evt => { if (evt is AssistantMessageEvent msg) { _lastResponse = msg.Data.Content; - done.SetResult(msg.Data.Content); + done.TrySetResult(msg.Data.Content); } }); - // 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)) + while (_iteration < _maxIterations) { - Console.WriteLine($"āœ“ Completion promise detected: {_completionPromise}"); - return response; + _iteration++; + Console.WriteLine($"\n--- Iteration {_iteration} ---"); + + done = new TaskCompletionSource(); + + // 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..."); } - Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); + throw new InvalidOperationException( + $"Max iterations ({_maxIterations}) reached without completion promise"); + } + finally + { + await session.DisposeAsync(); } - - throw new InvalidOperationException( - $"Max iterations ({_maxIterations}) reached without completion promise"); } finally { - await session.DisposeAsync(); await _client.StopAsync(); } } } // Usage -var loop = new RalphLoop(maxIterations: 5, completionPromise: "DONE"); +var loop = new RalphLoop(maxIterations: 5, completionPromise: "COMPLETE"); var result = await loop.RunAsync("Your task here"); Console.WriteLine(result); ``` @@ -115,11 +125,13 @@ public class PersistentRalphLoop { private readonly string _workDir; private readonly CopilotClient _client; + private readonly int _maxIterations; private int _iteration = 0; public PersistentRalphLoop(string workDir, int maxIterations = 10) { _workDir = workDir; + _maxIterations = maxIterations; Directory.CreateDirectory(_workDir); _client = new CopilotClient(); } @@ -127,26 +139,17 @@ public class PersistentRalphLoop public async Task RunAsync(string prompt) { await _client.StartAsync(); - var session = await _client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" }); try { - // Store initial prompt - var promptFile = Path.Combine(_workDir, "prompt.md"); - await File.WriteAllTextAsync(promptFile, prompt); + var session = await _client.CreateSessionAsync( + new SessionConfig { Model = "gpt-5.1-codex-mini" }); - while (_iteration < 10) + try { - _iteration++; - Console.WriteLine($"\n--- Iteration {_iteration} ---"); - - // 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)}"); - } + // Store initial prompt + var promptFile = Path.Combine(_workDir, "prompt.md"); + await File.WriteAllTextAsync(promptFile, prompt); var done = new TaskCompletionSource(); string response = ""; @@ -155,29 +158,48 @@ public class PersistentRalphLoop if (evt is AssistantMessageEvent msg) { response = msg.Data.Content; - done.SetResult(msg.Data.Content); + done.TrySetResult(msg.Data.Content); } }); - 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")) + while (_iteration < _maxIterations) { - return response; - } - } + _iteration++; + Console.WriteLine($"\n--- Iteration {_iteration} ---"); - throw new InvalidOperationException("Max iterations reached"); + done = new TaskCompletionSource(); + + // 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(); + } } finally { - await session.DisposeAsync(); await _client.StopAsync(); } } diff --git a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs index a1297656..0e81b5f8 100644 --- a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs +++ b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs @@ -62,54 +62,63 @@ public class RalphLoop public async Task RunAsync(string initialPrompt) { await _client.StartAsync(); - var session = await _client.CreateSessionAsync(new SessionConfig - { - Model = "gpt-5.1-codex-mini" - }); try { - while (_iteration < _maxIterations) - { - _iteration++; - Console.WriteLine($"\n=== Iteration {_iteration}/{_maxIterations} ==="); + var session = await _client.CreateSessionAsync(new SessionConfig + { + Model = "gpt-5.1-codex-mini" + }); + try + { var done = new TaskCompletionSource(); session.On(evt => { if (evt is AssistantMessageEvent msg) { _lastResponse = msg.Data.Content; - done.SetResult(msg.Data.Content); + done.TrySetResult(msg.Data.Content); } }); - 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)) + while (_iteration < _maxIterations) { - Console.WriteLine($"\nāœ“ Completion promise detected: '{_completionPromise}'"); - return response; + _iteration++; + Console.WriteLine($"\n=== Iteration {_iteration}/{_maxIterations} ==="); + + done = new TaskCompletionSource(); + + 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..."); } - Console.WriteLine($"Iteration {_iteration} complete. Continuing..."); + throw new InvalidOperationException( + $"Max iterations ({_maxIterations}) reached without completion promise: '{_completionPromise}'"); + } + finally + { + await session.DisposeAsync(); } - - throw new InvalidOperationException( - $"Max iterations ({_maxIterations}) reached without completion promise: '{_completionPromise}'"); } finally { - await session.DisposeAsync(); await _client.StopAsync(); } } diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md index bc4d1998..deafa628 100644 --- a/cookbook/copilot-sdk/nodejs/ralph-loop.md +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -49,7 +49,7 @@ class RalphLoop { async run(initialPrompt: string): Promise { await this.client.start(); - const session = await this.client.createSession({ model: "gpt-5" }); + const session = await this.client.createSession({ model: "gpt-5.1-codex-mini" }); try { while (this.iteration < this.maxIterations) { @@ -115,7 +115,7 @@ class PersistentRalphLoop { async run(initialPrompt: string): Promise { await fs.mkdir(this.workDir, { recursive: true }); await this.client.start(); - const session = await this.client.createSession({ model: "gpt-5" }); + const session = await this.client.createSession({ model: "gpt-5.1-codex-mini" }); try { // Store initial prompt