The model is not the agent
When you watch a coding agent such as Claude Code, OpenAI's Codex or Cursor fix a bug, it looks like one smart program at work. It is really two parts. The model takes text in and gives text out, and it can run nothing. The host, also called the harness or client, runs a loop around it. The host sends text to the model, reads back a request to call a tool, runs that tool, and sends the result back as more text.
That split is the most important idea in this course. A good comparison is a remote pair programmer you only reach by chat. They can't see your screen or your source code. They know only what you paste into the chat: a list of commands they may ask you to run, and whatever those commands print. Whether they do well depends on how clear that list and that output are.
The running example for this course is Ticketly, an invented support-ticket product with a REST API. Here is one turn of the loop around it.
Step 1: Tool definitions go into the context
The host sends the model a list of tool definitions with every request. Each one has a name, a description and a JSON Schema for its arguments. This is the Anthropic Messages API format:
{
"name": "search_tickets",
"description": "Search Ticketly support tickets. Returns id, subject, status and resolved_at for each match.",
"input_schema": {
"type": "object",
"properties": {
"customer": { "type": "string", "description": "Customer email domain, e.g. acme.com" },
"status": { "type": "string", "enum": ["open", "pending", "resolved", "closed"] }
},
"required": ["customer"]
}
}
That JSON is the whole of what the model knows about search_tickets. Your handler might be 300 lines of careful validation, but the model never reads it. When the API receives tools, it also adds its own system prompt so the model knows how to call them. The definitions are billed as input tokens on every request.
Step 2: The model picks a tool and fills in arguments
The user types: "Close Acme's resolved tickets older than a week." The model compares the request with the descriptions and decides search_tickets fits. It then writes a structured call:
{
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "I'll find Acme's resolved tickets first." },
{ "type": "tool_use", "id": "toolu_01A9", "name": "search_tickets",
"input": { "customer": "acme.com", "status": "resolved" } }
]
}
The model never asked the user for the domain. It got acme.com from the example in the parameter description. Hints in your descriptions turn directly into arguments.
Step 3: The host runs your code
A stop_reason of tool_use means "I want something run." The model has run nothing. The host matches the name to your handler and calls it. Here is a minimal loop in Python:
while True:
resp = client.messages.create(model=MODEL, max_tokens=2048,
tools=TOOLS, # the definitions go in every time
messages=messages)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use": # the model chose to answer instead
break
results = []
for block in resp.content:
if block.type == "tool_use":
text, failed = run_tool(block.name, block.input) # YOUR code runs here
results.append({"type": "tool_result",
"tool_use_id": block.id, # ties the result to its call
"content": text, # everything the model will see
"is_error": failed})
messages.append({"role": "user", "content": results})
Step 4: The result becomes more text, and the loop repeats
The tool_result is added to the conversation. On the next request the model reads it, together with everything before it, and decides what to do next: call another tool, retry, or answer the user. This repeats until the model stops asking for tools.
Different clients, same loop (as of October 2026). OpenAI's Responses API returns a
function_callitem with acall_idandargumentsas a JSON-encoded string, and you reply with afunction_call_output. An MCP server never talks to the model at all. The host finds its tools withtools/list, sendstools/callwhen the model picks one, and converts both into its own model's format. The details of the wire format differ, but the loop is the same.
Reading a transcript: where design helps or blocks
Suppose an earlier version of Ticketly described customer only as "Customer". Here is what the agent did:
→ search_tickets {"customer": "Acme"}
← is_error: true "Error 400"
→ search_tickets {"customer": "ACME Corp"} # guessing
← is_error: true "Error 400"
→ search_tickets {"customer": "acme"} # guessing again
← is_error: true "Error 400"
Assistant: "I couldn't access Acme's tickets. Please check the customer name."
The agent failed for three reasons:
- The description was vague, so the model had to guess the argument format.
- The error was opaque. "Error 400" carries the same information as no answer at all.
- The agent gave up after a few tries because nothing in the context pointed it to the fix.
Here is the same failure with one change, an error message the model can act on:
← is_error: true "No customer 'Acme'. `customer` must be an email domain. Closest match: acme.com"
→ search_tickets {"customer": "acme.com", "status": "resolved"}
← [{"id":"T-812","resolved_at":"2026-10-12"}, ...]
The agent recovered in one step. The validation code in your handler stayed the same. All that changed was the text that reached the model.
Human-friendly output can cause the same kind of trouble. A coloured table that ends in ...and 37 more reads fine in a terminal. An agent, though, may close only the rows it can see and report that the job is done.
Gotcha: if your tool throws and the host passes back only a stack trace, the model gets a pile of internal frames and no instruction about what to do. The MCP specification says execution errors (bad input, API failures) belong in the tool result with
isError: true, so the model can correct itself. Protocol errors such as an unknown tool are reported separately, and models are less able to recover from them.
Key takeaways
- An agent is a loop. The host sends tool definitions, the model asks for a call, the host runs it, and the result goes back as text. This repeats until the model answers the user.
- The model sees only text: names, descriptions, schemas and output. It never sees your code.
- The model does not execute anything. Your code, run by the host, does.
- An ID (
tool_use_id,call_id) links each result to its call. - Error messages are part of the interface. One that says what to do next lets the agent recover. A bare status code leaves it guessing.
- Anthropic, OpenAI and MCP use different formats, but the loop is the same in all of them.