Build your own Claude Code
The agent loop·10 min

Bounding the loop

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage the-step-cap --watch

Your 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.

the continue rule
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

Your task

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.

src/loop.ts
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
}
Fail bounded, never spin

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.

Mind the boundary

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.

How it's graded

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.