An unbounded agent is not a better programmer than you. It is a retry engine with a polite tone. You should refuse to ship any loop that cannot fail cheap.
The public scoreboard is lying
Public threads keep declaring models better than most developers. That claim measures draft speed, not ownership cost. You do not ship drafts into a shared branch.
Faster diffs look like competence during a recorded demo. They hide tool retries, schema thrash, and duplicated calls. You pay for every polite recovery the model invents.
You should score the abort path before the feature path. A loop that cannot stop is not intelligent work. It is a budget leak with syntax highlighting attached.
Cheap failure is a merge rule
Cheap failure means three concrete checks you can automate. The loop names a spend cap before the first tool. The loop dies when that named cap is actually hit.
The log must show the reason for that death. Missing reasons make the next incident undebuggable.
You already demand tests for parsers and database migrations. Demand the same discipline for agent control flow today. A green demo is not a control-flow test at all.
If the agent cannot fail cheap, do not merge it. That sentence is the whole opinion of this piece. Everything below is just an envelope you can copy.
Write the envelope before any prompt
Do not start the week with another prompt rewrite. Start with a budget contract the orchestrator must honor. Keep that contract in the repo beside agent config.
{
"max_rounds": 4,
"max_tool_calls": 6,
"max_input_tokens": 8000,
"max_output_tokens": 2000,
"on_breach": "abort",
"allowed_tools": ["repo.search", "repo.read"],
"forbidden_tools": ["shell.exec", "net.fetch"]
}
That JSON file is not documentation for future readers. It is a gate your runner must load first.
Treat the snippet as a proposed local contract only. Tune the numbers for your repository and your tools. Do not copy these caps as universal production truth.
What each field actually buys you
-
max_roundskills conversational thrash before the bill compounds. -
max_tool_callsstops search loops that never actually converge. -
max_input_tokensblocks context stuffing copied from earlier failures. -
max_output_tokenscaps rambling patches and unearned certainty. -
on_breach: abortrefuses one more try as a product feature. -
allowed_toolskeeps the model inside a named surface. -
forbidden_toolsis the real boundary, not the system prompt.
You put this file in version control on purpose. Reviewers then argue numbers instead of demo vibes.
The runner is the test, not the model
You do not trust the model to honor the envelope. You enforce the envelope inside your own process. The model is a guest, and guests do not hold keys.
Here is a small Python gate you can drop beside the agent. Label it as example code, not a vendor SDK.
# example: budget_gate.py — proposed local gate
from dataclasses import dataclass
@dataclass
class Envelope:
max_rounds: int
max_tool_calls: int
max_input_tokens: int
max_output_tokens: int
allowed_tools: set
forbidden_tools: set
@dataclass
class Spend:
rounds: int = 0
tool_calls: int = 0
input_tokens: int = 0
output_tokens: int = 0
class BudgetBreach(Exception):
pass
def assert_tool_legal(name: str, env: Envelope) -> None:
if name in env.forbidden_tools:
raise BudgetBreach(f"forbidden tool: {name}")
if name not in env.allowed_tools:
raise BudgetBreach(f"unnamed tool: {name}")
def assert_spend(spend: Spend, env: Envelope, next_in: int, next_out: int) -> None:
if spend.rounds > env.max_rounds:
raise BudgetBreach("round cap")
if spend.tool_calls > env.max_tool_calls:
raise BudgetBreach("tool cap")
if spend.input_tokens + next_in > env.max_input_tokens:
raise BudgetBreach("input cap")
if spend.output_tokens + next_out > env.max_output_tokens:
raise BudgetBreach("output cap")
You call assert_tool_legal before any tool dispatch. You call assert_spend before every model completion. You never ask the model whether the cap still matters.
Wire the gate at the process boundary, not in the prompt. A prompt can be ignored under pressure. A raised exception cannot be ignored without logs.
# example: one round of a guarded loop
def run_round(env, spend, tool_name, prompt_tokens, completion_tokens, dispatch):
assert_tool_legal(tool_name, env)
spend.rounds += 1
spend.tool_calls += 1
assert_spend(spend, env, prompt_tokens, completion_tokens)
result = dispatch(tool_name)
spend.input_tokens += prompt_tokens
spend.output_tokens += completion_tokens
return result
If dispatch never runs after a breach, you already won. The cheapest token is the one you did not send.
Prove the abort with a hostile fixture
A happy-path demo is worthless for this merge rule. You need a fixture that wants to run forever. The fixture is the test, and the abort is the assertion.
# example: test_budget_abort.py
import pytest
def test_search_loop_aborts_on_tool_cap(gate):
env = gate.load("envelope.json")
spend = gate.Spend()
with pytest.raises(gate.BudgetBreach, match="tool cap"):
for _ in range(20):
gate.assert_tool_legal("repo.search", env)
spend.tool_calls += 1
gate.assert_spend(spend, env, next_in=200, next_out=80)
def test_forbidden_shell_never_reaches_dispatch(gate):
env = gate.load("envelope.json")
with pytest.raises(gate.BudgetBreach, match="forbidden tool"):
gate.assert_tool_legal("shell.exec", env)
def test_round_cap_beats_polite_retry(gate):
env = gate.load("envelope.json")
spend = gate.Spend()
with pytest.raises(gate.BudgetBreach, match="round cap"):
for _ in range(env.max_rounds + 2):
spend.rounds += 1
gate.assert_spend(spend, env, next_in=100, next_out=50)
def test_unnamed_tool_is_a_hard_miss(gate):
env = gate.load("envelope.json")
with pytest.raises(gate.BudgetBreach, match="unnamed tool"):
gate.assert_tool_legal("repo.write", env)
Run it like any other unit suite today.
python -m pytest test_budget_abort.py -q
If that suite is red, the agent is not almost ready. It is unshippable, even if the sample task looks clean. You do not negotiate with a missing abort.
Add the suite to required checks on the agent branch. A required check beats a wiki page every time. People skip wikis when a demo is due Friday.
Decision table you can paste into the PR
| Signal | Demo culture says | Merge rule says |
|---|---|---|
| Green sample task | Ship it | Ignore it |
| Tool retry on timeout | Try again | Count it as spend |
| Model asks for shell | Trust the rationale | Forbidden tool, abort |
| Output looks confident | Accept the patch | Check token caps first |
| Cap hit mid-task | Raise the cap | Fail the job |
| Human override just once | Allow it | Treat it as an incident |
| Unnamed tool appears | Add it quickly | Reject the run |
| Logs omit the breach reason | Ship anyway | Block merge |
You paste the table into the pull request body. Reviewers then argue the numbers, not the vibes. That is the point of an opinionated gate.
Keep a short comment template next to the table. Ask one question only: did the abort fire on purpose? If nobody can answer, the run did not happen.
Rehearse on throwaway capacity
You still need a live loop to find abort bugs. Unit tests cannot see a model that rewrites the tool name. You need a rehearsal host that you can burn without drama.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Rehearse this envelope using MonkeyCode's free model access and free server option. Keep production keys off that throwaway box entirely.
Run only abort fixtures on that isolated host. Do not treat free capacity as proof of quality. Treat it as a place where a runaway loop is allowed to die.
If the abort never fires on the rehearsal host, it will not fire in CI either. Log every breach with the envelope identifier. Keep those logs beside the failing fixture, not in chat.
Do not tune the prompt until the gate is green. Prompt churn hides a missing cap. Caps belong in code, not in vibes.
# example rehearsal loop — proposed workflow, not a vendor CLI
export ENVELOPE_PATH=./envelope.json
python -m pytest test_budget_abort.py -q
python run_hostile_fixture.py --envelope "$ENVELOPE_PATH" --max-seconds 30
Stop the host after the fixture. A rehearsal box that stays up becomes production by accident. Accidental production is how polite retries become invoices.
Limitations you must not hand-wave
This envelope does not measure correctness of the patch. It only measures whether the loop can stop. A cheap abort can still merge a wrong diff if review is skipped.
Token counts are estimates unless your provider returns them. Prefer provider usage fields over local token guesses. Local guesses drift, and drift is a breach, not noise.
The allowed-tool list is not a sandbox by itself. A tool with broad power is still broad. repo.read can leak secrets if your tree is dirty.
Pair this gate with least-privilege credentials on every host. Pair it with replayable logs for every tool call. Pair it with a human review on anything that touches shared branches.
This will annoy agents that need one more search. That annoyance is the feature, not a bug. If your task cannot fit the cap, split the task.
Do not inflate the cap to protect the demo recording. Demo protection is how envelopes become fiction. Fiction is worse than a hard fail in CI.
Who should not use this
Do not use this as a substitute for code review. Do not use this on unsupervised production writes. Do not use this if you cannot replay the run from logs.
Skip it if you only generate one-shot snippets in an editor. Skip it if you have no tool-calling loop at all. Skip it if a hard budget proxy already sits in front of every model call.
This rule is for teams merging agent output into shared branches. If that is not you, the envelope is ceremony. Ceremony without a merge queue is just YAML.
Also skip it if your tools cannot be named in advance. Unnamed tool surfaces cannot be gated honestly. Honesty is the whole point of the abort.
The opinion, again
AI is not better at coding than you. It is better at emitting unfinished work at high speed. You remain the owner of spend, tools, and merge.
Build the abort before you chase the benchmark thread. Make cheap failure a required check. If the agent cannot fail cheap, you do not ship.
Top comments (0)