Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/A1.7.mp4An unbounded loop against an impossible task spends until somebody notices, and a gate nobody can approve fast enough is a gate that gets removed.
High-risk actions gated on a named human; budgets that bind and are recorded when they bind.
Approve one action, refuse another, then exhaust the budget on purpose.
The agent is asked to reconcile a booking that cannot be reconciled. It tries, rephrases, tries again. Six hours later it has spent four hundred thousand tokens and exhausted a vendor's rate limit for everybody else on that integration. Nothing failed. There was simply no number at which it was supposed to stop.
cybertravels/config.py::TOOL_POLICY. The budgets are two lines above them.The two ceilings on an agent loop — a human gate on irreversible actions, and budgets on steps and tool calls — including what a run returns when it hits one, and why a present-but-saturated gate is not a control.
An unbounded loop against an impossible task spends until somebody notices, and a gate nobody can approve fast enough gets removed.
Gate the irreversible actions on a named human, and bound both steps and tool calls.
Approvals per reviewer per hour — the number that says whether the gate is still a control — and the share of runs that hit a ceiling.
Measure this
| what to measure | denominator or target |
|---|---|
| approvals per reviewer per hour | rate against what one reviewer can actually consider |
| runs that hit a ceiling | share of runs |
TWO CEILINGS, OPPOSITE FAILURES human gate present but saturated ─► coverage 100%, review 0% budget absent ─► spend until somebody notices at the ceiling: return INCOMPLETE, and say so
Two ceilings on the same loop, and they fail in opposite directions.
Some actions are worth pausing for. In CyberTravels those are cancel_booking and issue_refund — the ones that are hard to reverse. The runtime pauses, names the action and the scope it is about to request, and waits for a person.
The failure mode is not that the gate is missing. It is that the gate stops working while still being present: at ten approvals a day a person reads each one, at four hundred they approve the queue. Coverage stays at 100% and actual review collapses, which is why the control is measured by approvals per reviewer per hour rather than by whether it exists. B3.9 is the lesson that models it.
A loop against an impossible task does not stop. It retries, rephrases, and spends — tokens, money, and somebody else's rate limit. CyberTravels bounds two things:
MAX_STEPS 8 model turns
MAX_TOOL_CALLS 12 tool invocations
The important property is what happens at the ceiling: the run returns an incomplete result that says so, not a confident summary of what it managed. A budget that silently truncates is worse than none, because the output looks finished.
And hitting a ceiling is recorded as a span. A loop that quietly hit its limit on 40% of runs last week is a thing you want to know.
1. Start the application:
./cybertravels/run.sh # then open http://127.0.0.1:8000
2. Sign in as Dana and ask it to cancel booking CT-4417.
3. Watch it stop. It names the action it wants to take and the permission it is about to ask for, and then it waits for you. Only two tools do this — cancel_booking and issue_refund — because they are the two that are hard to undo.
4. Approve it, and watch the rest of the run continue.
5. Now ask again, and refuse this time. Both of your answers get written down. That is the part that matters: a gate that only records the times somebody said yes cannot answer the question an investigation asks first, which is what did it try to do.
Two limits, and you can watch one of them bite in four lines. Start Python and type:
from cybertravels import runtime, config
print(config.MAX_STEPS, config.MAX_TOOL_CALLS) # 8 12
b, n = runtime.Budget(), 0
while b.call():
n += 1
print(n, b.exhausted()) # 12 tool_calls
It counts to twelve and stops, then prints the word tool_calls. That word is the point. The run does not just end — it ends saying which limit it hit, so what comes back is an unfinished answer that admits it is unfinished, rather than a confident summary of however far it got.
A note on the skill below, because the numbers will not match and the mismatch is the point rather than a mistake. CyberTravels bounds two things — model turns and tool calls. The skill audits four kinds of ceiling, including per-target and wall-clock, and on its fixture the per-target ceiling fires first at six calls. Your loop does not have a per-target ceiling. That is the finding the skill is for: an agent that may make twelve calls total can still make all twelve against one traveller's booking.
The skill — skills/runtime/budget-and-stop-condition-audit/SKILL.md
name: budget-and-stop-condition-audit
description: >-
Check that an agent loop has ceilings that actually bind — per-target, token
and action — and that hitting one returns an incomplete result rather than a
summary of what it managed. Use when reviewing loop termination, retries, or
an agent that runs unattended.
allowed-tools: Read, Grep, GlobA loop usually has several budgets and only one of them ever fires. Which one fires, and how early, is the design; the rest are decoration. The second half matters more: what the loop returns when it stops. A run that halts and reports its partial work as an answer has converted a budget into a quality problem.
Any agentic loop, particularly one that retries, and any agent that runs on a schedule with nobody watching.
1 — Enumerate every ceiling. Steps, tokens, wall clock, cost, actions, per-target attempts. For each, where it is checked and what it does on breach.
2 — Order them by when they bind. Run a task that consumes all resources and record which fires first. A per-target ceiling usually binds long before a token budget, which means the token budget was never the control.
3 — Test the breach path. The result must carry an explicit incomplete flag. Check what a caller does with it: a loop that returns complete: False into a pipeline that ignores the field has the same outcome as no budget.
4 — Check the ceiling is not resettable by the agent. A budget the loop can extend on its own behalf — by starting a sub-task, spawning a child, or retrying at a new target — is advisory.
5 — Derive the numbers from observation. State the p95 of a legitimate run and set each ceiling above it. A round number either strangles real work or never fires.
Input — the fixture committed at the top of scripts/budget_and_stop_condition_audit.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.
Output — the opening lines of a real run:
steps taken : 6
stopped by : per_target (reports-db)
complete : False <- visible in the output, not silent
ceiling limit used
tokens 50000 10800
seconds 60 2.4
actions 20 6
The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.
{
"ceilings": [{"name": "str", "value": 0, "checked_at": "str", "binds_at_step": 0}],
"first_to_bind": "str",
"breach": {"returns_incomplete": true, "caller_respects_flag": false},
"agent_resettable": ["str"],
"basis": {"p95_legitimate_run": {"steps": 0, "tokens": 0}}
}
Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.
The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.
Install it, once. From the folder you cloned. --all does every agent it finds on your machine:
python3 scripts/install_skills.py --all --lessons A1.7Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick a1-7-the-human-gate:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons A1.7 | type /a1-7-the-human-gate |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons A1.7 | type /a1-7-the-human-gate in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons A1.7 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons A1.7 | type $a1-7-the-human-gate, or run /skills |
No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.
python3 scripts/lesson.py A1.7Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.
python3 scripts/checkpoint.py --at A1.7 --out work/cybertravels
python3 scripts/checkpoint.py --at A1.7 --diffOn Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.
Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.
Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.
If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.
A high-risk action pausing and naming its scope, an approval and a refusal both recorded as audit rows, twelve tool calls before the ceiling binds with exhausted() naming tool_calls, and a budget ceiling returning an incomplete result rather than a summary. From the skill: a per-target ceiling reported as missing, which it is.
Raise MAX_TOOL_CALLS in cybertravels/config.py to 500 and give the agent a task it cannot finish. Watch the cost, and then answer the question the skill raised: twelve calls is a ceiling on the run, and nothing yet stops all twelve landing on one traveller's booking. Write down the number you think that should be. B3.4 builds exactly this ceiling, and it is worth having guessed first — the number in the code is four.
What you can do now. A running agentic platform you built yourself: a reasoning loop with an independent verifier, two MCP resource servers behind a process boundary, a workload identity per agent, per-action delegation that a resource server actually enforces, memory that records where its contents came from, signed agent-to-agent envelopes, a human gate and a budget that binds.
What you still cannot do. It runs, and you cannot yet tell anybody what it did. There is no trace, the audit rows cannot say what motivated an action, and the only evidence it works is that you watched it work once.
A2.0 — why a demo is not a system, and the three things that separate them.
Tools used
Skill source: skills/runtime/budget-and-stop-condition-audit