Caps are two lines at the top of the loop body
Run the code. That's the whole pattern — a metered loop in under twenty lines, and the two lines that matter are the first two inside the for.
Three details are load-bearing, and all three are the kind of thing that looks pedantic until it pages you:
The cost check runs before the spend. spent + COST[tool] > cap asks "would this call bust the budget?" — not "did the last one?" Check after the spend and your cap is a tripwire you discover you crossed one rerun_job too late. With real money, "detected the overage" and "prevented the overage" are different features. Build the second one.
The checks precede the work in every iteration. Not once before the loop, not in a wrapper around it — at the top of every pass. The plan here is fixed, but in the real loop the model picks the next tool each iteration based on the last result, so cost accrues one decision at a time. The gate has to sit where the decisions happen.
for/else gives plan-complete for free. The else arm runs only when the loop exhausts the plan without a break — which is precisely the definition of "finished on its own." The three exits produce three distinct stop reasons, each printed from exactly one place. When a run ends, there is no ambiguity about which door it left through.
Scripted plans, real logic
One honest note on the drills in this lesson: the plan list stands in for the model. In production, each iteration calls messages.create, the model looks at the tool results so far and proposes the next call, and that proposal is what streams through your validator and your caps. Scripting the plan removes the nondeterminism so the thing under test is your metering logic — the part that doesn't change when you swap models.
That's not a toy simplification; it's the actual boundary. The caps don't care whether the next tool call came from Opus, from a cheaper model, or from a list. Neither does the validator from lesson 1. Everything you're building in this studio lives on your side of the API call, which is why all of it survives your next model migration untouched.