Skip to content
MCP Five

The HostProject #213 / 51

An agent is a while loop

Send, reply, run, paste, repeat. There was never anything else in the box.

Think back to what happened when you said "roll me three twenty-sided dice".

Your server rolled the dice. That is all it did. Something else read your sentence, realised a tool was needed, picked the right one out of four, invented the arguments, made the call, read the result, and turned it into a friendly sentence.

That something else is called a host, and it did all the interesting work. Project #2 builds one.

Part 1 Β· Four steps, and that is the whole thing

"AI agent" sounds like a thing you buy. It isn't. It is four steps you write yourself.

#What happens
1Send the conversation to the model, along with the list of tools.
2The model replies. Its reply either is the answer, or it is a request: "please run roll_dice with these arguments."
3If it's a request: run the tool, paste the result into the conversation as if the user had said it, and go back to step 1.
4Otherwise, you're done.

There was never anything else in the box.

In actual code

Stripped of bookkeeping, the loop is about twenty lines:

lib/agent-loop.ts β€” the shape
ts
let messages = [{ role: "user", content: userText }];
 
for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
  // 1 Β· ask the model, giving it the conversation AND the tool list
  const response = await anthropic.messages.create({
    model: "claude-sonnet-5",
    max_tokens: 4096,
    tools,                       // built once, outside the loop
    messages,
  });
 
  // 2 Β· append the WHOLE reply β€” see rule 1 on the next page
  messages.push({ role: "assistant", content: response.content });
 
  // 3 Β· if it didn't ask for a tool, it answered. Stop.
  if (response.stop_reason !== "tool_use") break;
 
  // 4 Β· run every tool it asked for, in parallel
  const toolUses = response.content.filter((b) => b.type === "tool_use");
  const results = await Promise.all(
    toolUses.map(async (use) => ({
      type: "tool_result",
      tool_use_id: use.id,
      content: await toolbox.dispatch(use.name, use.input),
    })),
  );
 
  // 5 Β· paste them ALL back as ONE user message, then loop
  messages.push({ role: "user", content: results });
}

Read step 5 again. The tool results go back into the conversation in the user role β€” as though you had typed them. The model has no separate channel for tool output. It is all just conversation.

The agent loop
The amber box is where the magic lives: pasting results back into the conversation and asking again. Everything else is bookkeeping, and the edge from PASTE back up to CALL is the only thing that makes this an agent rather than a function call.

What stop_reason is telling you

The model tells you why it stopped talking, and branching on it properly is the difference between a loop and a spin.

stop_reasonMeansWhat the loop does
end_turnIt finished its answer.Stop. This is success.
tool_useIt wants a tool run.Run it, paste, loop.
max_tokensIt was cut off mid-sentence.Stop and say so β€” the answer is truncated, not wrong.
refusalIt declined.Stop and surface it.

Part 2 Β· The demo that proves it

One sentence, chosen because it cannot be faked:

Roll 3d20, then put that many cookies in the jar.

Why this sentence? Because the second half depends on the first half. The model cannot fill in count until it has seen the dice total β€” and the dice total does not exist until a real server on the real internet has rolled real random numbers.

If you see the right number in the second call, the chaining is genuine. There is no other explanation available.

What actually happened

npm run agent
--- iteration 1 --------------------------------------------------
-> CALL cookiejar__roll_dice  {"sides":20,"times":3}
<- RESULT Rolled 3d20 -> [10, 4, 16]  Total: 30  (265ms)

--- iteration 2 --------------------------------------------------
The rolls were 10, 4, and 16 β€” a total of 30. Now I'll add 30 cookies to the jar.
-> CALL cookiejar__cookie_jar  {"action":"add","count":30}
<- RESULT Added 30. The jar now has 42 cookie(s).  (275ms)

--- iteration 3 --------------------------------------------------
Done! 🎲 I rolled 3d20 and got 10, 4, 16 for a total of 30.
I added 30 cookies to the jar, which now has 42 cookies in it.

done β€” end_turn
3 iteration(s), 2 tool call(s), 697 uncached in / 287 out
cache: 2281 written, 4562 read at ~10% price

Walking the three iterations

Iteration 1. messages contains one thing: your sentence. The model sees it plus eight tool descriptions, and returns stop_reason: "tool_use" asking for roll_dice. The loop runs it and gets back the string "Rolled 3d20 -> [10, 4, 16] Total: 30".

Now messages has three entries: your sentence, the assistant's request, and a user message carrying the result.

Iteration 2. The model is called again with all three. It reads its own previous request and the result underneath it β€” and asks for cookie_jar { action: "add", count: 30 }.

Iteration 3. Called again with all five messages. Nothing left to do, so stop_reason: "end_turn" and a summary.

The thing to actually notice

Look at count: 30.

Part 3 Β· Server, client, host

Worth nailing down now, because the rest of the course leans on it.

Talks toOwns
serverwhoever calls itits own tools and data
clientexactly one serverthe transport
hostthe model, and several clientsthe conversation, the loop, and the money

Every AI product you admire β€” Claude Code, Cursor, ChatGPT with plugins β€” is a variation on that third row.

Project #1 built the green box and let Claude Desktop be the red one. Project #2 builds the red box, which is where all the decisions live.

Part 4 Β· The state is an array, and that turns out to matter

One more observation, easy to skip, that two later projects are built on.

Look again at what the loop actually holds between iterations. It is messages β€” an array. That is the whole of the agent's state. There is no hidden memory, no session on Anthropic's side, no server-side conversation id.

What you now know

  • An agent is four steps: send, read the reply, run any tools, paste the results back, repeat.
  • Tool results go back in the user role. There is no separate channel.
  • Branch on every stop_reason, not just tool_use.
  • Chaining works because the loop pastes output into the conversation β€” no code connects one tool to the next.
  • A host owns the model conversation, the loop and the key; servers only propose.
  • The agent's entire state is the messages array, which is why it can be frozen.

Next: the three rules that will save you, each of which cost real time to learn.