This example demonstrates the Ralph Loop pattern using the ADK's Runner API, wrapped in a reusable RalphLoop abstraction.
The Ralph Loop (originated by Geoffrey Huntley) is an autonomous agent iteration pattern:
- A single task prompt is fed to an AI agent repeatedly
- Each turn, the agent gets a fresh context window but discovers prior work via the filesystem
- The agent's output is inspected for a completion promise (
<COMPLETE/>) - A verification gate rejects premature completion — the caller decides what "done" means
- If rejected or no promise → the prompt is re-injected for another turn
- A max turns limit provides a safety bound
The example provides a RalphLoop type that encapsulates the pattern. The turn loop is driven externally via a simple for loop — each turn uses Runner to execute the agent once:
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "ralph",
Instruction: "You are an autonomous AI developer...",
Model: chatModel,
Handlers: []adk.ChatModelAgentMiddleware{fsMw},
})
rl := NewRalphLoop(RalphLoopConfig{
Agent: agent,
Prompt: taskPrompt,
MaxTurns: 10,
CompletionPromise: "<COMPLETE/>",
VerifyCompletion: func(ctx context.Context) error {
// Return nil to accept, error to reject and continue.
bugs, _ := backend.GrepRaw(ctx, &filesystem.GrepRequest{
Path: "/project", Pattern: "BUG:",
})
if len(bugs) > 0 {
return fmt.Errorf("%d BUG markers remaining", len(bugs))
}
return nil
},
OnEvent: func(event *adk.AgentEvent) {
// Observability hook — print events, log metrics, etc.
prints.Event(event)
},
})
result := rl.Run(ctx) // blocking
fmt.Printf("Complete: %v, Turns: %d/%d\n", result.Complete, result.Turns, result.MaxTurns)| Field | Description |
|---|---|
Agent |
The adk.Agent to run each turn. Built by the caller with desired tools, middleware, retry config. |
Prompt |
Task description re-injected every turn. |
MaxTurns |
Safety bound — forced stop when reached. |
CompletionPromise |
String the agent must output to signal completion. Defaults to <COMPLETE/>. |
VerifyCompletion |
Called when the promise is detected. Return nil to accept, error to reject and continue. Optional. |
OnEvent |
Called for each agent event during a turn (observability). Optional. |
| Field | Description |
|---|---|
Complete |
true if the agent's completion promise was accepted. |
Turns |
Number of turns executed. |
MaxTurns |
Configured maximum. |
StopCause |
Reason the loop stopped (e.g. "completion promise accepted", "max turns reached"). |
Err |
Non-nil if the loop exited due to an error. |
┌──────────────────────────────────────────────────────┐
│ RalphLoop.Run() │
│ │
│ for turn := 1; turn <= MaxTurns; turn++ { │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Runner.Run(prompt) │ │
│ │ → Agent executes one turn using fs tools │ │
│ │ → Collects text output │ │
│ └──────────────────────┬───────────────────────┘ │
│ │ │
│ CompletionPromise in output? │
│ YES → VerifyCompletion() │
│ ├─ error → REJECT, continue loop │
│ └─ nil → ACCEPT, return result │
│ NO → continue loop │
│ │
│ } │
│ → max turns reached, return result │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ InMemoryBackend (shared) │ │
│ │ Files persist across all turns │ │
│ │ Pre-seeded with buggy starter project │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
The agent is given a buggy URL shortener project pre-seeded in the in-memory filesystem.
The project has 15 intentional bugs across 5 files (marked with // BUG: comments).
The agent must iteratively:
- Read files and find remaining
BUG:markers - Fix 2-3 bugs per turn using
edit_file - Remove the corresponding
BUG:comments - Verify fixes by re-reading the edited files
- Only declare
<COMPLETE/>when allBUG:markers are gone
The VerifyCompletion gate ensures the agent can't declare victory prematurely.
Configure your LLM provider:
OpenAI (default):
export OPENAI_API_KEY="sk-..."
export OPENAI_MODEL="gpt-4o"
export OPENAI_BASE_URL="https://api.openai.com/v1"Ark (Volcengine):
export MODEL_TYPE="ark"
export ARK_API_KEY="..."
export ARK_MODEL="..."
export ARK_BASE_URL="..."cd examples
go run ./adk/agent/ralph-loop/- Turn 1: Agent reads all files, finds 15
BUG:markers, fixes 2-3 in store.go - Turn 2-4: Agent continues fixing handler.go, handler_test.go, main.go, README.md
- When agent outputs
<COMPLETE/>, the verification gate greps for remaining BUG markers - If markers remain → promise rejected, loop continues with another turn
- When all markers are resolved → promise accepted, loop exits
- Final summary shows stop cause, turn count, and all files in the filesystem