↻ The Agent Loop
Runnpm run lesson3View the code →

Lesson 3 · Keep going until it's done

Lesson 2 gave the model one round of tools. Some questions need one. Some need five. You can't know which in advance, but the model can tell when it has seen enough. So we let it decide.

The shape of an agent

ask the model ──► wants tools? ──no──► that's the answer
      ▲               │ yes
      │               ▼
      └──── run them, send the results back

In code:

let reply = await model.ask({ messages, tools });

while (reply.stop_reason === "tool_use") {
  messages.push({ role: "assistant", content: reply.content });
  messages.push({ role: "user", content: runAll(toolCallsOf(reply)) }); // every call, one message (lesson 2)
  reply = await model.ask({ messages, tools });
}

// the model replied without asking for a tool: that's the answer

Each pass is a turn: the model asks, we run, we report back, it looks again. Nothing in this code knows what the task is or how many steps it takes. The model decides when the job is done.

That's the difference from a normal program. You don't write "read the tests, then the code, then answer". You hand over the tools and the question, and the path through the task comes from the model.

Run it

npm run lesson3
── turn 1 ──────────────────────────
model  Let me see what's in the project first.
tool → list_files({})

── turn 2 ──────────────────────────
model  I'll read the cart module and its tests side by side.
tool → read_file({"path":"src/cart.js"})
tool → read_file({"path":"test/cart.test.js"})

── turn 3 ──────────────────────────
model  The test "shipping is free at exactly 50.00" fails. …

Three turns. Nobody told it to list the files first. It chose to, because it didn't know the test file's name. That's the "agent" part: the path through the task comes from the model, not from your code.

The messages array is the memory

After the run, messages holds everything: the question, each model turn, each batch of tool results. The model has no memory between requests. You send the entire conversation every time. That's why the model in turn 3 "remembers" the file it read in turn 2: it's right there in the request.

Two practical consequences:

A definition you can use

Simon Willison puts it well: an agent is a model that runs tools in a loop to achieve a goal. You've now written every piece of that sentence:

Frameworks give you this loop with extras: retries, streaming, tracing. Worth using once you know what they're doing. Now you do.

Try this