Curious Pandas

A model, a loop and some tools

An agent is a language model that can ask your code to do things, in a loop, until it has an answer. Step through one call at a time, watch the context grow, and find out who actually decides when it stops.

17 min+35 XP for finishing

This track is about building an agent: the moving parts, which part does what, and which of those parts you control.

Start with the uncomfortable fact. A language model can’t do anything. It reads text and writes text. It can’t open a file, look up an invoice or send an email. What it can write is a request: “please call find_invoice with 1042”. Your program reads that request, runs the real function, and pastes the result back into the conversation. Then it asks the model again.

Anthropic’s tool-use docs describe exactly that split. The model “determines when to call a tool based on the user’s request and the tool’s description”, and returns “a structured call that your application runs”. When the model wants a tool, the response says so with a stop reason of tool_use; your code runs the operation “and sends back a tool_result”.

So an agent is three things:

  1. A model, which only ever decides: call this tool with these arguments, or answer.
  2. Tools, which are ordinary functions you wrote, with a name and a description the model can read.
  3. A loop, also code you wrote, that runs the model, runs the tools it asks for, and decides when enough is enough.
Visual guideWho does what in the loop
Who does what in the loopThe task goes to the model. The model either answers or writes a tool call. Your code runs the tool and adds the result to the conversation, which the model reads again in full on its next call. The loop, which is also your code, can stop it at any point.taskfrom a personmodeldecides onlyanswerthe model is doneconversationgrows every turnyour coderuns the toolthe looplimits: steps, moneyanswerstool callresultread againcan stopWho does what in the loopThe task goes to the model. The model either answers or writes a tool call. Your code runs the tool and adds the result to the conversation, which the model reads again in full on its next call. The loop, which is also your code, can stop it at any point.taskfrom a personmodeldecides onlyanswerthe model is doneconversationgrows every turnyour coderuns the toolthe looplimits: steps, moneyanswerstool callresultread againcan stop
  1. A person gives the model a task. The model reads it - plus its instructions and the descriptions of every tool it has.
  2. The model writes a tool call. That is all it can do: describe what it wants done.
  3. Your code runs the real function and adds the result to the conversation. The model keeps no memory, so its next call reads the whole conversation again.
  4. Eventually the model writes an answer instead of a call. Or the loop - your code again - stops it first.

Run one

The agent below has three tools and one question about an overdue invoice. Its “model” is a simulation: a short set of rules (listed in the next lesson’s widget, under “How this simulated model decides”), that picks tools the way a real model does, by reading their descriptions. No AI is called and nothing leaves your browser. What you see is the real shape of the loop, one model call at a time.

An agent, one call at a timesimulated model - no AI is called
Tools:
    What each model call had to read
      model calls0
      tokens read in total0

      The pattern has a name

      Write a run out as plain text and you get the shape the 2022 ReAct paper made famous. It had models write “reasoning traces and task-specific actions in an interleaved manner”, and letting them look things up (through a simple Wikipedia API) made them invent fewer facts. Most agents today still run some version of it:

      1. Think. The model writes what it’s trying to do next.
      2. Act. It writes a tool call in a fixed format.
      3. Observe. Your code runs the tool and pastes the result into the conversation.
      4. Repeat until the model answers, or the loop stops it.

      An illustrative trace (the search result is written for the example, not real output):

      GOAL: How tall can a bamboo plant get in a week?
      
      THINK   I need a growth rate. Let me search.
      ACT     search("fastest growing bamboo rate per day")
      OBSERVE "...some species have been measured growing close to a metre a day..."
      THINK   Roughly a metre a day, times 7 days. I should say it's an upper bound.
      ACT     calculator("0.9 * 7")
      OBSERVE 6.3
      ANSWER  Under ideal conditions, a fast species could grow around 6 m in a week -
              an upper bound; most bamboo grows far more slowly.

      Every THINK, ACT and ANSWER line is the model writing tokens. Every OBSERVE line is the outside world being pasted in - which is exactly where the tools lesson finds its biggest danger.

      The context only grows

      Look at the bars beside the trace. Every model call reads everything so far: the instructions, the tool definitions, the task, every tool call and every result. The model doesn’t remember the last call. It is handed the whole transcript again.

      Two costs follow from that:

      • Money. You pay for every token every call reads. Tool definitions count too: the docs list “tool names, descriptions, and schemas” as part of the input, plus a fixed tool-use instruction the API adds (286 tokens on Claude Opus 5.5). A ten-step run pays for its early steps ten times. The “tokens read in total” counter shows the sum.
      • Attention. A long, cluttered context is harder for the model to use well, which is one more reason to give an agent fewer, clearer tools and shorter results. Text becomes tokens covers what a token is.

      Who decides when it stops

      The model ends a run only one way: by answering. Nothing in the model says “this is taking too long” or “I’ve spent enough”. A confused model can call tools forever.

      So the loop needs its own limits, written in plain code the model can’t talk its way around: a maximum number of steps, a time limit, a spending cap. Anthropic’s guidance on building agents recommends “extensive testing in sandboxed environments, along with the appropriate guardrails”, and a step limit is the first of them.

      Take away its tool for counting days, too. A good agent says what it couldn’t do instead of guessing. Real models don’t always manage that, which is why the next lessons put checks outside the model rather than trusting its final answer.

      Check yourself+10 XP

      Your agent's model replies with stop_reason tool_use and a call to find_invoice. What happens next?

      Check yourself+10 XP

      A run takes 10 model calls. Roughly how many times do you pay to send the original task?

      Check yourself+10 XP

      Which of these actually guarantees an agent stops?

      Do it for real · your machine+50 XP bonus

      Everything above ran in your browser. Now try it in a real terminal: any Linux machine, a Mac's Terminal app, or Windows with WSL. Nothing to install beyond that.

      Build the same loop for real in a few lines. You need your own API key for a hosted model, or a local model through Ollama. With Anthropic’s Python SDK (pip install anthropic, key in ANTHROPIC_API_KEY), following the tool-use docs:

      import json, anthropic
      
      client = anthropic.Anthropic()
      tools = [{
          "name": "find_invoice",
          "description": "Look up an invoice by its number: amount, customer and due date.",
          "input_schema": {"type": "object", "properties": {"number": {"type": "string"}}, "required": ["number"]},
      }]
      
      def find_invoice(number):                # your code, your data
          return {"number": number, "customer": "Bamboo Bakery", "due": "2026-10-12", "status": "unpaid"}
      
      messages = [{"role": "user", "content": "Is invoice 1042 overdue? Today is 2026-10-30."}]
      for step in range(5):                    # the step limit lives here, in your code
          reply = client.messages.create(model="claude-sonnet-5-5", max_tokens=1024, tools=tools, messages=messages)
          messages.append({"role": "assistant", "content": reply.content})
          if reply.stop_reason != "tool_use":
              print([b.text for b in reply.content if b.type == "text"], reply.stop_reason)
              break
          results = [{"type": "tool_result", "tool_use_id": b.id, "content": json.dumps(find_invoice(**b.input))}
                     for b in reply.content if b.type == "tool_use"]
          messages.append({"role": "user", "content": results})
      else:                                    # runs only if the loop never hit break
          print("stopped by the step limit, not the model")

      Print reply.usage inside the loop and watch the input tokens grow each step. This costs money on your own account - a run like this is a fraction of a cent, but check current prices first.

      Self-reported. We can't see your machine, and we don't want to. This is a bonus, never required.

      Sources

      Finished reading?

      Missions and quizzes above earn their own XP. This marks the lesson done.