Part II Understand It The Theory Behind Reliability
15 min read

Chapter 5 – The Ouroboros Protocol (Why Loops Converge)

Start with a convergence trace:

attempt 1 -> FAIL schema.required_field_missing(path=items[0].country)
attempt 2 -> FAIL scope.out_of_allowlist(path=docs/runbook.md)
attempt 3 -> FAIL unit_test.tax_rounding
attempt 4 -> PASS

That is the whole chapter in four lines. The loop is not just “trying again.” It is tightening against specific deterministic findings until it reaches a pass state or a circuit breaker says stop.

In Part I, you built the loop: propose a change, validate it, and feed deterministic failures into the next attempt.

Chapter 5 explains why that loop can settle instead of wandering. We call that pattern the Ouroboros Protocol, with a deliberate nod to the myth of the ouroboros, the serpent that eats its own tail: the loop feeds its own output back into the next turn and evolves as it goes. It is not a static circle, but a self-correcting one. Each attempt uses the last candidate and the last validator findings as input to the next one, and each Mission still ends in a small set of deterministic outcomes: PASS and commit, FAIL and revert, or escalate with evidence.

That is the difference between a useful retry loop and blind sampling. The system is not just “trying again.” It is retrying against a shrinking set of explicit constraints.

Ouroboros Protocol (Write → Judge → Refine)

This is the retry loop behind reliable agent work: produce one bounded candidate, run deterministic checks, then retry only against the exact failure signal. Repeat until the checks pass or the circuit breaker stops the run.

Clear stop conditions
Write → Judge → Refine Stop on PASS (checks pass) or when the loop budget runs out (escalate). The Judge can send the next step to refine, rewrite, or re-scope.
Interactive loop model
Target quality 86/100
Circuit breaker 8 loops
Refinement strength 55/100
Stochasticity 45/100
Scenario presets
Manual controls. Choose a preset to model a specific loop regime.
WRITE
JUDGE
REFINE
Loop count
0
Current quality
52/100
Open errors
6
Status
READY
Minimum progress window (W=4)
Collecting baseline.
Run the loop to inspect error signatures.
Structured error object

Ready. Run one bounded cycle.
Loop outcomes
Cyan = PASS, red = refine, violet = escalate

Two properties make this loop more than “retry until it passes”:

  1. Self-reference is a feature. The loop feeds its own artifacts back in: the last diff, the last validator failures, the last scope decision, and the last trace. It is not “try again.” It is “try again with the exact evidence of what failed.”

  2. Maintenance is drift repair. Over time, the Terrain changes and the Map falls behind: docs, inventories, indexes, and rules drift. Ouroboros is the small repair loop that turns that drift into a bounded fix under deterministic checks (Physics), whether the diff touches code (Terrain) or a Map surface.

The core insight: you do not need a deterministic large language model (LLM). You need a deterministic process around a stochastic engine.

Traceability Needs Convergence

The classical V-model gives you traceability between declared intent and the checks that prove correspondence. Ouroboros adds the missing control law: deterministic findings, bounded retries, and explicit stop conditions that turn a stochastic implementation step into a converging process.

That distinction matters because a traceable loop can still thrash. Ouroboros is the mechanism that shrinks failure against exact evidence until the candidate reaches an admissible state or stops with a deterministic reason.

The Core Loop: Write → Judge → Refine

At its heart, Ouroboros is a feedback loop with three steps:

  1. Write: The model gets the mission, scope, and prior findings, then produces one candidate output. Since LLMs are stochastic engines (Chapter 4), this step is inherently probabilistic. Even with identical inputs, the output might vary between runs.

  2. Judge: Run deterministic validators. In this book, the checker stack is Physics and this decision stage is the Judge. It produces structured findings and the next move (commit, refine, revert, or defer).

  3. Refine: Feed the exact findings back into the next attempt, with scope and output constraints unchanged.

This cycle repeats until the Judge reports success, or a circuit breaker is tripped.

The Loop as a State Machine (what persists, what resets)

Ouroboros sounds mystical in prose. In implementation, it is a small state machine with a short memory.

If you’re implementing this in a real repository, build it in layers:

  1. One-shot candidate + Physics: generate once, validate once, stop.
  2. Strict parsing first: fail fast on invalid diff/JSON before expensive checks.
  3. Structured findings feedback: retry only with specific, machine-readable failures.
  4. Budgets + escalation: stop deterministically when progress stalls; route hard cases to humans.
flowchart TD
  Start([START]) --> Prep["PREP<br/>- build slice<br/>- compile authority<br/>- select validators<br/>- set budgets"]
  Prep --> Write["WRITE (Effector)<br/>- propose candidate<br/>(diff / JSON / etc.)"]
  Write --> Judge["JUDGE (Physics)<br/>- parse + scope + policy<br/>- run validators"]
  Judge --> Pass{PASS?}

  Pass -- yes --> Commit["COMMIT + TRACE"] --> End([END])
  Pass -- no --> Budgets{"BUDGETS / PROGRESS OK?"}
  Budgets -- yes --> Refine["REFINE FEEDBACK<br/>+ UPDATE STATE"] --> Write
  Budgets -- no --> Escalate["ESCALATE / DEFER + TRACE"] --> End

Same loop, rendered as implementation pseudocode:

state = init_state(mission, scope, budgets)

while True:
    candidate = write(state.slice, state.mission, state.findings)
    parsed = parse_candidate(candidate)
    if parsed == invalid:
        return escalate("parse_shape_failure", trace=state.trace + [candidate])

    findings = judge(parsed, state.mission, state.scope)
    if findings == []:
        return commit(candidate, trace=state.trace + [candidate])

    if should_stop(state, findings):
        return escalate("non_converging_or_budget_exhausted", findings=findings, trace=state.trace + [candidate])

    state = refine(state, candidate, findings)

State (what the loop remembers)

There are two kinds of memory in Ouroboros:

Keep that working memory small and deterministic. Do not feed the entire run history back into the model request.

Practical state bundle per attempt:

Feedback is constraint (the model isn’t “learning”)

Inside one Ouroboros run, the model is not getting smarter. Its weights do not change. The loop gets better only by adding explicit, deterministic constraints.

The difference between a good loop and a bad one is the feedback contract:

Illustrative feedback template (diff-shaped work):

ROLE: Effector (component, not chat)
OUTPUT: Unified diff only. No prose.

## Mission (authority)
{mission_summary}

## Scope (policy)
- write_allowlist: {allowlist}
- denylist: {denylist}
- allowed_edit_regions: {regions}

## Previous candidate (artifact)
{previous_diff}

## Deterministic findings (Physics)
{findings_json}

## Instructions
- Fix ONLY the recorded findings.
- Do not change files outside the allowlist.
- Keep the diff minimal and localized.

If your work is JSON-only, swap “Unified diff only” for “Valid JSON only” and make strict parsing the first gate.

Convergence vs. Thrashing

Convergence means the failing set shrinks to zero and the loop exits with PASS. Thrashing means the loop keeps moving without getting closer: signatures repeat or scope expands while the same failures remain.

Convergence signals in practice

In real traces, convergence usually has three visible signals:

You do not need perfectly monotonic progress every attempt, but you should see net progress inside a bounded window.

A common starting point is a 3-attempt progress window: require either a smaller failing-code set or a smaller authorized diff surface inside that window. If neither changes, classify the run as non-converging and escalate.

Attractors: the region that counts as “done”

Convergence is not luck. It comes from defining what a successful candidate looks like.

An attractor is the set of candidates your checks would accept. In this book’s terms, it is the region where the candidate parses cleanly, stays in scope, respects budgets, and passes every validator.

You can picture it as a valley in the space of possible diffs: once a candidate lands inside it, the validators stop pushing it back out.

If you prefer a less poetic definition:

admissible(candidate) =
  parse_ok
  AND in_scope
  AND budgets_ok
  AND all_validators_pass

That boolean predicate defines the attractor. The loop settles when successive candidates land in the same admissible region and stop moving.

This framing is useful because it turns “the model is being weird” into a design diagnosis:

When you debug thrash, ask one question first: did I define an attractor that actually exists? In plain language: is there a reachable PASS state inside the declared scope and budgets?

Thrash debug checklist (Attractor):

Convergence criteria (heuristics you can implement)

PASS is the termination condition, but it is not enough to steer a loop. For guidance you need heuristics: small, deterministic checks that answer a simpler question, “are we getting closer?”

This is the Ratchet Principle inside Ouroboros: once you can measure progress or safety, you stop letting it slip. (Chapter 11 applies the same idea to repo-wide quality metrics.)

1) Reachability: prove PASS is possible

Before you spend retries, ask if the attractor exists at all. In plain language: can any candidate actually reach PASS under the declared scope and budgets?

If the answer is “no,” retries are just burning budget. Split the Mission, fix the slice, or fix the conflicting rules.

2) Minimum progress: require monotonic improvement (or stop)

Progress doesn’t have to be strictly monotonic every iteration, but it should be monotonic within a window.

A practical “distance to attractor” signature:

Then enforce a simple ratchet:

3) Oscillation detection: catch A↔︎B ping-pong

Thrash is often a two-state loop: fix A, break B, fix B, break A.

Detect it mechanically by keeping a tiny ring buffer of recent signatures:

signature = (diff_hash, sorted(failing_validator_codes))
if signature repeats within last K attempts:
  abort("oscillation detected; split mission or tighten slice")

This turns “it feels stuck” into a deterministic stop condition.

4) Failure classes: don’t treat all failures as equal

Not all FAIL signals deserve the same response.

The goal is a loop that is strict about boundaries, cheap about early failure, and honest about when a human needs to intervene.

Failure routing matrix (fast policy)

Codify the first response per failure class so retries stay deterministic:

Failure class First response Retry policy
Parse/shape (invalid diff, invalid JSON) tighten output contract, fail fast retry after contract tightening
Scope/policy (out_of_allowlist) reject candidate, keep mission scope fixed retry only with stricter scope
Semantic validator failures feed exact findings back to Refine retry within progress window
Infra/transient (timeout, rate limit) backoff, preserve last good state retry with bounded attempts

This prevents a common anti-pattern: treating every FAIL as equivalent and blindly retrying.

Practical stop defaults

If you do not have better repo-specific numbers yet, start here:

Control Default Why
max_iterations 3 Enough to use structured feedback once or twice without hiding thrash
progress window 3 attempts Long enough to see net improvement, short enough to stop budget burn
max_files_changed 3 for bounded missions Keeps blast radius small while the loop is immature
max_lines_changed 120 Forces splitting before changes become hard to review
repeated failure signature 2 identical signatures Signals the loop is stuck, not learning
infra retry budget 2 retries with backoff Enough for transient noise without masking system issues

Loop latency: fast local Judges vs. slow remote Judges

This chapter can sound unrealistically snappy if your full integration suite takes 20-30 minutes. In many real systems, that is normal. If every retry waits on the slowest gate, the loop dies of latency.

The fix is not to drop the deep checks. The fix is to tier the Judge:

This is also one reason the book keeps pushing bounded units. Breaking a monolith into smaller, explicit surfaces is not only a context optimization. It is also a testing and verification optimization: smaller units give you smaller validator surfaces, faster local checks, and a much better chance that the inner loop can run at human-useful speed.

One practical pattern:

  1. Retry only against the fast local Judge.
  2. Run the slow remote Judge only after the candidate is locally admissible.
  3. If the slow Judge fails, feed back one structured summary, then either retry with a narrower slice or escalate.
  4. If the same slow gate keeps failing, stop pretending it is a cheap retry loop and change the slice, the Mission, or the validator layout.

In other words: fast local validation keeps the loop alive; slow remote validation keeps the system honest. Later chapters split this out into governance and CI policy. The key point here is simpler: Ouroboros survives enterprise latency only when the Judge is staged, not monolithic.

Thrashing: deterministic stop signals

Treat thrash as a state classification, not a vibe:

When these signals appear, abort and escalate. Re-running unchanged inputs is budget burn, not progress. Typical fixes are: tighten the slice, tighten the acceptance criteria, or split the Mission into smaller units.

Escalate immediately when

Circuit Breakers: Guardrails for Stochasticity

A loop that can retry can also thrash. Circuit breakers make failure cheap and deterministic.

Dry Runs (Plan Mode)

Before you call the model, run Prep only. A dry run should print:

The companion repo (github.com/kjwise/aoi_code) includes a mission-dry-run target that demonstrates this. It reads a Mission Object and prints the slice, validators, and budgets without actually calling a model.

Precedence Order (Cheap Checks First)

A practical check order:

  1. Parse/structure: if you can’t parse the candidate, you can’t judge it.
  2. Policy: scope allowlists, protected paths, diff budgets, review limits.
  3. Validation: run the Judge and collect signal-rich failures.
  4. Stop conditions: max iterations, time/cost limits, minimum-progress windows.

The Economics of Determinism (Why Loop Budgets Matter)

The book has talked about ROI for building loops. You also need ROI for running them.

The right comparison is not “a loop costs tokens.” It is “a bounded retry loop costs tokens” versus “a human debugging session costs time” versus “a production mistake costs money and trust.”

Illustrative cost comparison (order of magnitude; plug in your numbers):

Event Typical cost
N retries inside a constrained loop ~$0.50–$2.00
One human debugging session ~$100–$300
One production incident ~$5,000+

This is why circuit breakers matter. You want to spend a small, bounded budget before merge so you do not spend an unbounded budget after deploy.

Now widen the lens: your inference cost model is a strategic choice. It determines whether you can afford retries, strict gating, and background maintenance at scale.

Inference model Cost structure Moat durability
Pay-per-token API Variable, provider-controlled Fragile
Fixed-cost SLA Predictable, negotiated Moderate
Self-hosted / owned inference CapEx + energy, you control Strong

State the thesis explicitly:

A constrained loop with a weaker model can beat an unconstrained flagship model. Teams that control inference costs, including teams running open-weight models behind validated loops, can outcompete teams renting flagship models without constraints. The model is the engine; the loop is the vehicle. The loop is also where the value compounds: once it exists, new tasks can reuse the same scope controls, validators, and audit trail.

“Vehicle” is not poetry here. It cashes out into governance: your loop defines scope, budgets, validation, and audit evidence. That is what lets you scale autonomy without scaling incidents.

Compressed Trace Readout

You can classify loop behavior quickly from a short trace:

attempt failing codes diff scope classification
1 5 4 files initial
2 3 2 files converging
3 1 1 file converging
4 0 1 file PASS

A thrashing trace tends to alternate signatures (A -> B -> A) or keep the same failing set while scope expands. That is your signal to stop retries and change inputs, not keep sampling.

Actionable: What you can do this week

  1. Pick a small, schema-driven generation task in your current work (e.g., generating a JSON configuration file, a SQL query based on a schema, or a simple code snippet that must pass a linter).

  2. Define your deterministic checker: Write a simple tool or use an existing validator (like a JSON schema validator, a Kubernetes schema validator such as kubeconform, or a linter/type checker) that can deterministically validate the output of your chosen task. In this chapter, that checker is the Judge, and it should return clear, structured errors.

  3. Build a manual Ouroboros loop:

    • Write a Mission Object: an explicit task contract for the model.
    • Run your Judge against the output.
    • If it fails, paste the Judge output into the next request and ask for a fix that addresses only the recorded failures.
    • Repeat until it converges or you hit a manual max_iterations limit.
  4. Debug thrashing as a reachability problem: If you cannot converge, do not “try harder.” Check reachability:

    • Can a human produce a PASS artifact under the same scope and budgets?
    • Do your Validators conflict (two rules that cannot both be true)?
    • Are you missing the contract in your slice (schema/interface/policy) so the loop is guessing?

This exercise makes the core point tangible: deterministic checks drive a stochastic model toward something reviewable and safe.

Share