deep dive · claude code · part 2 — the loop

2026-04-21 · 8 min read · #claude-code #source-dive #agents

Part 1 drew Claude Code's shape: one envelope, one loop. The loop was eight lines of pseudocode. This piece opens them up — it's actually two loops nested together: an outer while(true) that iterates once per model call (rebuild, sample, maybe run tools, decide), and an inner for await that consumes SSE events inside each API call (reassembling tokens and tool calls on the wire). The plan: list the outer calls, walk an example, zoom into the wire.

the outer loop

The outer loop lives in query.ts. It's an async function* — a generator that yields messages upward as they stream. Its body is six calls in a while(true). Click any call to expand.

async function* query(state) {
  while (true) {
    // 1. prepare — rebuild envelope, run pre-query surgeries
    const request  = await prepareRequest(state)

    // 2. sample — stream the API call, drain SSE events
    const response = yield* callModel(request)

    // 3. post-sample — fire hooks, non-blocking
    runPostSamplingHooks(response)

    // 4. decide — keep going or return?
    if (shouldExit(response, state)) return

    // 5. act — execute tool calls
    const results  = yield* runTools(response.toolUses)

    // 6. commit — append + advance turn
    commit(state, response, results)
  }
}

The next section walks the loop through a real example.

a turn, then two more

Say you type add error handling to this function. What appears in your terminal:

$ add error handling to this function

 Read(errors.ts)
  ⎿ Read 42 lines

 Edit(errors.ts)
  ⎿ Added try/catch to parseConfig

The function now handles missing config files and malformed JSON
gracefully, returning a default config instead of crashing.

From the outside: a read, an edit, a paragraph. Underneath, the outer loop ran three times — three envelope rebuilds, two tool executions, one streamed text block. The animation walks it.

you keystroke envelope · rebuilt every turn system prompts · ctx · mcp · skills tools ~160 declarations messages history · grows each turn Claude API local system files · shell · git · net loop turn input tokens 0 output tokens 0
(empty — press play)
press play to walk three outer-loop iterations
The prompt produces a Read, an Edit, and a paragraph. Underneath: three envelope rebuilds, two tool executions, one streamed text block.
step 0/12
system empty
(not built yet)
tools 0 / 160
(not built yet)
messages 0 items
(empty)

inside callModel · what's on the wire

The inner loop is a for await over SSE events. Each event updates one content block in the blocks[] array. Click any event type to expand.

what is SSE?

SSE (Server-Sent Events) is plain HTTP streaming — the server keeps the socket open and writes events as they happen. Each event is a text block, separated from the next by a blank line:

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\":"}}

The client reads event: <type> + data: <json>, parses the JSON, and dispatches on event.type.

Compared to a normal HTTP request: one request, one complete response, connection closes. SSE keeps the socket open — one request, then a stream of events flows back until the server closes it.

async function* callModel(request) {
  const stream = await openStream(request)       // POST + SSE
  const blocks = []                               // accumulator

  for await (const event of stream) {
    switch (event.type) {
      case 'message_start':        initMessage(event)
      case 'content_block_start':  openBlock(event, blocks)
      case 'content_block_delta':  applyDelta(event, blocks)
      case 'content_block_stop':   closeBlock(event, blocks)
      case 'message_stop':         return finalMessage(blocks)
    }
    yield partialMessage(blocks)
  }
}
incoming event
(press play)
blocks[] state
(not yet created)
press play to watch a tool_use block assemble
Six wire events build one tool_use block. input_json_delta fragments accumulate in _buf. JSON.parse fires only at content_block_stop.
step 0/6

What looked like "the API returned a tool_use object" is really a handful of wire events glued together on the client side.

wrap-up · the agentic loop

We dived into the main agentic loop — the while(true) in query.ts that drives every interaction with the model. Each turn rebuilds the envelope, calls the model, runs tool calls, and decides whether to keep going. Plus a small detour into the inner loop — the for await that consumes SSE events and assembles each response from deltas.

What we skipped and will return to in later pieces: reactive compaction when the envelope overflows, the abort tree that unwinds on ctrl-c, stop hooks that can veto end_turn, the pre-query surgeries that trim messages before each call.

references