LevelUpWith

Explore / Agent-Ready Products: CLI, MCP Server or Skill / Lesson 1

Lesson 1: Inside the Agent Loop: What Happens on a Tool Call

Learn how a coding agent turns a user request into tool calls: it reads the tool definitions in its context, picks one, fills in its arguments, reads what comes back and decides what to do next.

Free to read, no account needed. Part of Agent-Ready Products: CLI, MCP Server or Skill.

By the end of this level you'll be able to

  • Trace one agent turn from the user's request to the tool definitions, the model's chosen call, the tool result and the next step
  • Explain why the model only ever sees text (names, descriptions, schemas and output) and never your code
  • Spot the points in a real transcript where a tool's design helped or blocked the agent

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_call item with a call_id and arguments as a JSON-encoded string, and you reply with a function_call_output. An MCP server never talks to the model at all. The host finds its tools with tools/list, sends tools/call when 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.

A 6-question knowledge check follows this lesson in the app, free and without an account.

More like this

  • Google Cloud Generative AI Leader Certification Exam Prep

    Master all four sections of the current Generative AI Leader exam guide at business level, with no coding, from first principles to confident scenario "best an…

    Exam prep · Google · AI & Agents · Expert · 42 levels · 4 free · Professionally curated by levelupwith.com

  • AI Coder: Claude Code & Coding Agents

    Master vibe coding and autonomous workflows using Claude Code, Cursor, MCP servers, and modern AI coding agent frameworks.

    Beyond the Obvious · AI & Agents · Capable · 18 levels · 4 free · Professionally curated by levelupwith.com

  • Introduction to Generative AI

    Learn who invented generative AI, how it works, where the technology stands today, and where it's headed — all in a quick, beginner-friendly course.

    Course · AI & Agents · AI & Agents · Curious · 7 levels · 4 free · Professionally curated by levelupwith.com

  • Architecting for Coding Agents: Prescribe vs Delegate

    Decide how much architecture to prescribe to a coding agent and how much to leave to it, working from a catalogue of named patterns and anti-patterns that each…

    Beyond the Obvious · AI & Agents · Expert · 38 levels · 4 free · Professionally curated by levelupwith.com

  • AI Agents for Non-Developers: Understand, Try, Oversee

    Learn how AI agents differ from the chat assistants you already use, try research and browser agents safely without writing any code, and delegate tasks with c…

    Course · AI & Agents · AI & Agents · Capable · 18 levels · 4 free · Professionally curated by levelupwith.com

  • AI Proficiency: From Occasional User to Power User

    Get dependable, high-quality results from AI assistants such as ChatGPT, Gemini, Claude and Copilot. You will learn to give rich context, run multistep workflo…

    Course · AI & Agents · AI & Agents · Capable · 18 levels · 4 free · Professionally curated by levelupwith.com