How an agent works
An agent is a model in a loop - and the loop is your code, not the model. Walk through the message flow, see what a tool actually is, see what the model receives on each turn, and watch what goes wrong when the loop misbehaves.
The whole lesson in one sentence
An agent is a model in a loop - and the loop is your code, not the model.
Most people imagine the model "calling tools" the way a function calls another function. It doesn't. The model is just a function over text: messages in, one message out. The orchestration code that wraps it - the harness - is what calls tools, manages state, decides when to stop. That harness is the agent. The model is one of its dependencies.
This lesson walks through the actual message flow, then drills into three pieces the picture leaves abstract: what a tool really is, what the model literally receives on each turn, and what goes wrong when the loop is the bug.
The message flow
Four actors: user, harness, model, tool. Watch a single cycle of "What's the weather in Tokyo?" play out as a sequence of messages. Every arrow either starts or ends at the harness - the model and the tool never speak to each other directly. That fact is the whole architecture.
Press step → to walk one full cycle. Notice every arrow either starts or ends at the harness - model and tool never talk directly.
The aha: there is no "agent API". The model is just POST /v1/messages in a while loop. The harness assembles the request, parses
the response, runs any tool calls, and decides whether to call the model
again. The model has no idea a "loop" exists.
What's a tool, really?
A "tool" isn't a runtime connection between the model and some external
system. It's a JSON schema - name, description, parameter types -
included in the prompt sent to the model. The model reads those
descriptions and may emit a tool_call matching one of them.
The harness reads the tool_call (it's just text!) and runs the actual function
in your code.
-
-
Why this matters: the model "knows about" a tool only because the harness told it (in the prompt) that the tool exists. Add a new tool by adding a new entry to the tool list - that's it. Remove a tool by removing the entry. There's no plugin system; there's just text.
What does the model actually see?
Models are stateless. There is no "memory" - only replayed history. Every API call sends the entire conversation so far, plus the system prompt, plus the tool definitions. Five turns in, the model receives all five turns, every time. Use the inspector below to scrub through one conversation and see what the harness actually sends on each call.
-
Why this matters: "context window" is a count of tokens in this single request. Long conversations don't accumulate in the model - they accumulate in the messages array, which the harness re-sends in full every time. Compaction, summarization, sliding-window memory - those are all harness-side strategies for keeping that array small.
When the loop is the bug
Three canonical ways the loop misbehaves: a tool keeps erroring and the model can't fix it, the user's goal is too vague and the model wanders, or the loop has no max iteration count and runs forever. The harness's policy - retry caps, token budgets, halt conditions - is what protects you. Pick a mode and step through.
Press step → to watch the loop misbehave. Switch modes above to see different failure patterns.
Why this matters: stop conditions are non-negotiable. A model that's been trained to be "helpful and thorough" will not stop on its own. Every production agent has a max-iter cap, a token budget, a per-tool retry limit, or all three. The model is the engine; the harness is the brakes.
What you just saw
while loop, the
tool dispatcher, the stop conditions. tool_call as text; your harness parses it and runs the function. No magic, no
protocol. The whole orchestration in 15 lines
Strip away the framework, and this is what every agent reduces to. The harness assembles the request, parses the response, runs tool_calls, and decides whether to loop. The model is one line of that code.
const messages = [system, userMessage]
while (iter < MAX_ITER) { // ← stop condition
const resp = await model(messages, tools) // ← the only place the model is called
messages.push(resp)
if (!resp.tool_calls) break // ← model said it's done
for (const call of resp.tool_calls) {
const result = await runTool(call) // ← harness runs the function
messages.push({ role: "tool", content: result })
}
iter++
}
return last(messages).content // ← the final answer The closing aha: the orchestration is trivial. The hard parts live in the prompt, the tool design, and what the harness does around the loop - compaction, permissions, sub-agents, parallel tool execution. That's the next lesson.