The Agent Loop, From Scratch
0/7 lessons completed
Overview

Chapter 1: How the loop works · Lesson 2 of 7 · 7 min read

Tools: how the model acts

The model never runs code. It asks, you run, you report back. Get that contract right and most bugs disappear.

The model can't run code, open files or call APIs. When we say an agent "uses a tool", what actually happens is a three-step handshake:

  1. You describe each tool to the model: a name, what it does, and its parameters.
  2. The model asks for a tool by name, with arguments.
  3. Your code runs it and reports back the result as a new message.
A sequence between your code and the model: 1, you describe get_order to the model. 2, the model asks for get_order(id=4411). 3, you run it and report back {"price": 250}.
Describe, ask, run and report. The model never touches your systems.

Describing a tool

Most model APIs take a JSON Schema per tool. The description is the most important field: it's the only thing the model reads when deciding whether to call it.

get_order = {
    "name": "get_order",
    "description": (
        "Look up one order by its numeric ID. "
        "Returns price (USD), status and created_at. "
        "Use this before answering any question about a specific order."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "id": {"type": "integer", "description": "Order ID, e.g. 4411"}
        },
        "required": ["id"],
    },
}

Write descriptions for a new teammate who has never seen your system: what it returns, when to use it, and when not to.

Errors are results too

Here's the most common beginner bug. A tool throws, the exception kills the loop, and the agent dies mid-task:

result = tools[call.name](**call.arguments)   # raises KeyError, loop is gone

Instead, catch the error and send it back to the model as the tool's result. Models are good at recovering when they can read what went wrong:

try:
    result = tools[call.name](**call.arguments)
except Exception as e:
    result = f"ERROR: {type(e).__name__}: {e}"

Now "order 4411 not found" becomes something the model can react to ("let me check the ID with the user") instead of a crash.

Two runs with the same bad order ID. Left: the KeyError escapes and the loop is gone. Right: the error goes back as the tool result and the model asks the user to double-check the ID.

Three rules that prevent most tool bugs

1. Validate arguments before running. The model can produce arguments that match the schema's shape but not your reality: an ID that doesn't exist, a date in the past. Check them and return a clear error.

2. Keep results small. Everything you return goes into messages and is resent on every later step. A tool that dumps a 2,000-row table will slow the agent down and push older context out. Return what the model needs, plus a hint if there's more ("showing 20 of 2,000 rows; filter by status to narrow down").

3. Separate reading from doing. get_order is safe to call ten times. refund_order is not. Mark tools that change the world, because lesson 3 puts a brake on exactly those.

Recap

  • The model asks; your code runs. The model only knows what you send back.
  • Tool descriptions are prompts. Write them carefully.
  • Turn exceptions into results so the loop survives.

Next: our loop still has while True in it. Let's give it brakes.