A drafting loop can spend its entire allowance on a failure the model was never able to fix. Consider a cheap tier that keeps emitting the same unknown column name because the catalog never contained it. Four repairs later the loop has burned its budget, returned nothing usable, and taught you nothing about the schema. The question is not whether to retry, but which failures deserve another attempt on the same tier at all.
My earlier post about an agent-written query that locked a production table ended with a gate, not a model choice. This post stays inside the loop and argues the retry policy itself, because that policy decides how much free capacity either position consumes. Two credible positions follow, then a failure taxonomy and a small harness that turns the argument into a test you can run against your own recorded drafts.
Position A: repair in place, keep the cheap tier
The first position holds that most failed drafts are phrasing failures, and phrasing is exactly what a repair prompt fixes cheaply. Give the same tier the parser error, the offending line, and the target dialect, then ask for a minimal diff instead of a full rewrite. Proponents point out that switching tiers on the first error throws away valid reasoning attached to a single bad identifier. They also argue that cheap tiers improve faster than escalation ladders, so a policy tuned to today's weak tier ages badly.
The strongest version of this position is narrow: repair only when the validator produced a specific, localizable error. A parser that says syntax error at or near "GROUPS" on line 7 gives the model something to act on. A zero-row result gives it almost nothing, and a repair prompt against nothing tends to produce confident guesses. Position A is defensible exactly to the extent that your validator emits precise diagnostics.
Position B: escalate on failure class, never on count
The second position says the retry count is the wrong variable and the failure class is the right one. A missing column is a retrieval problem: the model did not see the catalog, and asking again with the same context cannot help. A dialect break is a translation problem with a known mapping table. Answering both with the same "try again" is how loops burn budget while producing identical failures.
Advocates of class-based escalation add an uncomfortable observation about empty results. A query that runs cleanly and returns nothing usually means the request was underspecified or the filters are wrong, so escalation to a stronger tier tends to produce a more articulate wrong answer. This position would rather stop and surface the ambiguity than pay for better-sounding noise. That argument is worth taking seriously, because it is the failure mode that reviews rarely catch.
Where both positions break
Both positions assume your validator can name the failure, and most harnesses cannot. If the only signal is "the query failed", you cannot classify anything, and escalation collapses into expensive guessing. Instrument the validator first: distinguish parser rejection, catalog miss, dialect refusal, empty result, and timeout as separate codes. Until those five codes exist, neither position is testable.
Both positions also ignore transport failures, which deserve their own rule. A 429 is not a model problem, and retrying around a rate limit is how agents get blocked instead of helped. Treat throttling as a stop-and-back-off event with no escalation and no repair, and record it in the ledger so the budget report stays honest. Escalating after a rate limit is the one behaviour that reliably makes things worse.
The artifact: a failure taxonomy and a policy harness
The code below is a policy skeleton, not a benchmark. Model tiers are abstract labels you supply, token weights are arbitrary units you define, and nothing here assumes a vendor, a price, or a quota. Run it against recorded failures before you attach it to live drafts.
from dataclasses import dataclass, field
from enum import Enum
class FailClass(str, Enum):
SYNTAX = "syntax" # parser rejected the text
DIALECT = "dialect" # parses, target engine refuses it
UNKNOWN_OBJ = "unknown_obj" # table or column absent from catalog
EMPTY_RES = "empty_result" # executed, returned zero rows
TIMEOUT = "timeout" # statement or scan budget exceeded
TRANSPORT = "transport" # 429, 5xx, dropped connection
@dataclass
class Attempt:
tier: str
fail: FailClass | None
tokens: int
@dataclass
class Ledger:
attempts: list[Attempt] = field(default_factory=list)
def charge(self, tier, fail, tokens):
self.attempts.append(Attempt(tier, fail, tokens))
@property
def spent(self):
return sum(a.tokens for a in self.attempts)
def failures_at(self, tier):
return sum(1 for a in self.attempts if a.tier == tier and a.fail is not None)
def decide(fail, prior_failures_at_tier, max_same_tier=1):
if fail is FailClass.TRANSPORT:
return "stop_backoff" # never retry around a rate limit
if fail is FailClass.TIMEOUT:
return "stop" # a stronger tier will not make it faster
if fail is FailClass.EMPTY_RES:
return "escalate" if prior_failures_at_tier == 0 else "stop"
if fail is FailClass.UNKNOWN_OBJ:
return "escalate" # retrieval gap, not phrasing
if prior_failures_at_tier < max_same_tier:
return "repair" # syntax and dialect get one local retry
return "escalate"
def run(draft, token_cost, max_same_tier=1):
ledger, tier = Ledger(), "cheap"
while True:
fail = draft(tier)
ledger.charge(tier, fail, token_cost(tier))
if fail is None:
return "accepted", tier, ledger
prior = ledger.failures_at(tier) - 1
action = decide(fail, prior, max_same_tier)
if action == "repair":
continue
if action == "escalate" and tier == "cheap":
tier = "strong"
continue
return action, tier, ledger
Running the fixture test
- Record your last fifty failed drafts with the validator code that rejected each one, and store them as a replay file.
- Replace
draftwith a replay function that returns the recorded failure for each step, so the run is deterministic and free of new spend. - Set
token_costto your own unit weights, for example1for the cheap tier and8for the strong tier, to model relative spend without quoting prices. - Run the fixture matrix below and check that outcomes and ledger totals match the expected column.
- Only then wire the policy to live drafting, and log every decision next to the validator code that caused it.
CASES = [
# name, cheap failures, strong failures, expected
("repair once", [FailClass.SYNTAX, None], [], "accepted/cheap"),
("repair then climb", [FailClass.SYNTAX] * 2, [None], "accepted/strong"),
("catalog miss", [FailClass.UNKNOWN_OBJ], [None], "accepted/strong"),
("throttled", [FailClass.TRANSPORT], [], "stop_backoff/cheap"),
("slow query", [FailClass.TIMEOUT], [], "stop/cheap"),
("empty result", [FailClass.EMPTY_RES], [None], "accepted/strong"),
]
With cheap attempts weighted at one unit and strong attempts at eight, the fixture separates the two positions by spend rather than by opinion. Repair-once succeeds for two units, one catalog miss escalated early costs nine, and the same shape escalated late costs ten. Those numbers are placeholders for your weights, but the shape of the comparison is the useful part.
The decision table
| Failure class | Same-tier repair | Escalate now | Stop | Rule of thumb |
|---|---|---|---|---|
syntax |
yes, once | after one failed repair | — | localizable error, cheap fix |
dialect |
yes, once | if the mapping is unknown | — | pair with a translation table |
unknown_obj |
no | yes | — | retrieval gap, retry cannot help |
empty_result |
no | once, then stop | second time | ambiguity, not phrasing |
timeout |
no | no | yes | cost problem, not capability |
transport |
no | no | yes | respect the limit |
The decision rule that survives both positions is this: repair only what the validator localized, escalate only what retrieval or capability caused, and stop on anything that is really about cost or rate limits. Everything else is a rounding error in your budget report.
Where free model access and a free server fit
This policy is cheap to try because the cheap rung does not need a paid key. Free model access covers the repair loops and the fixture replays, while a free server gives the validator somewhere isolated to parse, replay, and log decisions without touching your laptop or your production network. Disclose plainly: Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Availability claims age, so verify the current terms on the project page rather than trusting a number in a blog post, including this one. I deliberately quote no token allowance, no hardware, and no model name here, because free-tier terms move faster than articles. If the escalation experiment appeals to you, start with the fixture file and the project's own documentation, then compare spend per accepted draft against your current loop.
Limitations and who should skip this
The harness proves nothing without validator codes, so teams whose only signal is a boolean failure should invest there first. Teams whose failures are mostly empty results will find escalation expensive and unhelpful, since the missing input is specification quality rather than model strength. Teams on metered paid tiers should re-weight the table, because their escalation cost may dwarf any retry savings and invert the conclusion.
Finally, this is a policy layer, not a safety layer. Read-only credentials, statement timeouts, and a parser gate still carry the responsibility for what the query does to your database. A clean escalation ladder only decides who drafts next, and it should never be the reason a query reaches production unreviewed.
Top comments (0)