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 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.
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.
(not built yet)
(not built yet)
(empty)
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.
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)
}
}
(press play)
blocks[] state(not yet created)
tool_use block assemble
tool_use block. input_json_delta fragments accumulate in _buf. JSON.parse fires only at content_block_stop.What looked like "the API returned a tool_use object" is really a handful of wire events glued together on the client side.
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.