Bounding the loop
# grade this stage (from your course directory)
npx theinference test --stage the-step-cap --watchYour loop runs until the model says end_turn. But what if it never does? A confused model can ask for tool after tool after tool — and your while will happily oblige, forever, burning tokens and your patience. Every real agent has a hard cap on turns. This step adds it.
You'll factor the loop's continue/stop decision into one tiny predicate: keep going only while the model is still calling tools and you're under the cap. Pull that condition out where you can see it, test it, and trust it.
continue ⇔ stop_reason == "tool_use" AND turn < cap
tool_use, under cap → true (keep looping)
tool_use, at cap → false (stop — bounded)
end_turn, any turn → false (stop — done)
What to build
Bound the loop
In src/loop.ts, implement shouldContinue(turn, stopReason, cap). Return true only when the model wants another tool turn (stopReason === 'tool_use') and the current turn is still below cap. Everything else — a non-tool_use stop, or hitting the cap — returns false. Wire it into runLoop so the loop fails bounded on overrun instead of spinning.
Graded by program.ts step-cap, which calls shouldContinue on a table of (turn, stopReason, cap) cases and prints the boolean for each.
import type { StopReason } from './types.ts'
// Continue only while the model is still calling tools and we're under the cap.
export function shouldContinue(turn: number, stopReason: StopReason, cap: number): boolean {
// return stopReason === 'tool_use' && turn < cap
}
A confused loop must fail bounded, not run forever. The cap is the safety rail every production agent ships with — without it, one bad model turn becomes an unbounded bill and a hung process. When the cap is hit, the loop should stop with a clear bounded outcome (max_turns), not crash and not continue.
The exact comparison matters: turn < cap, not turn <= cap. With cap = 5, turn 0 continues but turn 5 stops — that off-by-one is precisely what the fixture checks. Decide what turn counts (zero-based here) and keep it consistent with how runLoop increments it.
Process mode: your_program.sh step-cap runs program.ts step-cap on the case fixture, which maps a list of (turn, stopReason, cap) checks through your shouldContinue and prints { results: [...] }. The grader compares that JSON to fixtures/expected.json — {"results":[true,false,false]} — as an exact match: continue under the cap, stop at the cap, stop on end_turn.