# Building an AI Agent

> What an agent actually is under the hood: the model plus tools plus a loop. Function-calling, the reasoning-acting cycle, and where agents go wrong.


---

# Building an AI Agent

You've seen the demos where an "AI agent" books a flight, fixes a bug, or files your taxes, and it feels like there's some new kind of intelligence behind the curtain. There isn't. An agent is the same language model you already know, handed two things: a set of tools it can call, and a loop that keeps running until the job is done. Once you see those three parts - model, tools, loop - the magic turns into machinery you can build, debug, and trust.

This guide takes the curtain down: the mental model first (it's a loop, and you write most of it), then the reasoning-acting cycle step by step with real function-calls, and finally the plain-spoken part - the ways agents spiral, hallucinate tools, and burn money, and the guardrails that keep them on a leash.

## How to read this

- **Want the whole idea in one sitting?** Read [Phase 1: An Agent Is a Loop](01-an-agent-is-a-loop.md). It installs the model-plus-tools-plus-loop picture, which is most of the battle.
- **Want it to actually click?** Read in order. Phase 1 is the *what*, Phase 2 is the *how* (function-calling and the real cycle), and Phase 3 is the *where it bites* - the failure modes and the guardrails that separate a toy from something you'd let near production.

## The phases

1. **[An Agent Is a Loop](01-an-agent-is-a-loop.md)** - the mental model: a model that reasons, decides to call a tool, reads the result, and repeats until done. The control loop *you* write versus the choices the *model* makes.
2. **[The Reasoning-Acting Cycle](02-the-reasoning-acting-cycle.md)** - function-calling with a schema, the turn-by-turn message exchange, how tool results feed back in, and what "memory" really means here.
3. **[Where Agents Go Wrong](03-where-agents-go-wrong.md)** - infinite loops, hallucinated tool calls, runaway cost, and the guardrails - step budgets, validation, approval gates - that keep an agent on a leash.

> This guide assumes you're comfortable calling a model programmatically. If "send a request, get text back" isn't second nature yet, read [Using an LLM API](/guides/using-an-llm-api) first - an agent is that same call, wrapped in a loop.


---

# An Agent Is a Loop

You ask a plain chatbot "what's the weather in Oslo right now?" and it tells you, confidently, something it cannot possibly know. It has no window, no thermometer, no live feed - it's a text predictor frozen at training time. That gap is the whole reason agents exist. An agent is what you get when you stop expecting the model to *know* everything and start letting it *go find out*.

Here's the picture to carry through this entire guide: an agent is a language model given **tools** and a **loop**. The model thinks, picks a tool, you run it, you feed the result back, and the model thinks again - around and around until the task is done. That's it. There's no second brain. The same model that writes your emails is the one "driving," except now it can press buttons in the real world.

## The three parts, and who owns each

Strip an agent down and exactly three pieces remain. Knowing which ones *you* control and which one the *model* controls is the single most clarifying thing in this whole topic.

```text
  ┌──────────────────────────────────────────────┐
  │  1. THE MODEL - reasons, decides what to do  │  ← the model owns this
  │  2. THE TOOLS - functions it's allowed to call│  ← you define these
  │  3. THE LOOP - runs tools, feeds results back│  ← you write this
  └──────────────────────────────────────────────┘
```

*What just happened:* We split the agent into the part that decides (the model) and the parts that act and orchestrate (your code). The model never actually *runs* anything - it can only ask. Your loop is the hands; the model is the head. Keep that division in your head and agents stop being mysterious.

The most common beginner misread is thinking the model "does things." It doesn't - it emits a request ("please call `get_weather` with city = Oslo") and waits. Your code reaches out to the weather service, gets the number, and hands it back. The model proposes; your loop disposes.

## One call versus a loop of calls

A normal LLM feature is a single round trip: you send a prompt, you get text, you're done. An agent is that same call placed inside a `while` loop that keeps going until the model says "I'm finished."

```text
  PLAIN LLM CALL              AGENT
  ─────────────              ─────────────────────────────
  prompt ─► model ─► text     loop:
                               prompt ─► model ─► "call tool X"
                               run tool X ─► result
                               feed result back ─► model ─► "call tool Y"
                               run tool Y ─► result
                               feed result back ─► model ─► "done: <answer>"
                              end loop
```

*What just happened:* The only structural difference between a chatbot and an agent is the loop. One call answers from memory; a loop lets the model gather facts it didn't have, one tool call at a time, and revise its plan as results come in. That repetition is the entire upgrade.

## The cycle, step by step

Every trip around the loop follows the same rhythm. People call it the **reason-act cycle** (or "ReAct"), but you can read it plainly: think, do, look, repeat.

```mermaid
flowchart LR
  G[goal] --> R[model reasons]
  R --> D{needs a tool?}
  D -->|yes| T[model requests tool call]
  T --> X[your loop runs the tool]
  X --> O[result fed back in]
  O --> R
  D -->|no| A[final answer]
```

*What just happened:* The loop only ever exits through one door - the model deciding it has enough to answer and producing a final response instead of another tool request. Until then it keeps circling: reason, request, run, read. Notice the tool result flows *back into* the model on the next turn; that feedback is what lets the agent course-correct instead of committing to a blind plan up front.

## A concrete walk-through

Say the goal is: "Is the office in Oslo open right now?" Answering needs two facts the model doesn't have - the current time in Oslo, and the office hours. Watch the loop earn the answer.

```text
TURN 1  model: "I need the current time in Oslo."
        → requests: get_time(city="Oslo")
        your loop runs it → "2026-06-30 19:40"
TURN 2  model: "Now I need the office hours."
        → requests: lookup_hours(office="Oslo")
        your loop runs it → "Mon–Fri 09:00–18:00"
TURN 3  model: "19:40 is past 18:00 on a weekday."
        → no tool needed → final answer: "No, it closed at 18:00."
```

*What just happened:* The model decomposed one question into two lookups, ran them in sequence, then reasoned over the combined results to answer. No single tool gave the answer - the *loop* did, by letting the model gather and then conclude. This is the difference between an agent and a glorified search box.

> 💡 **Key point.** An agent's intelligence isn't only in the model - it's in the loop closing. The model is smart at deciding *what to fetch* and *what the results mean*; your loop is what makes those fetches actually happen and keeps the conversation moving until there's an answer.

## For builders

When you build one, the loop is yours and it's small - often a few dozen lines. You hold a running list of messages, call the model, check whether the reply is a tool request or a final answer, run the tool if asked, append the result to the message list, and call again. The model supplies the brains; you supply the plumbing, and the plumbing is where bugs and runaway costs live (that's all of Phase 3). The takeaway for now: the loop is *ordinary code*, and you are fully in charge of it.

> ⚠️ **Gotcha - the model can't stop itself.** Nothing in the model guarantees it ever decides "done." If your loop has no limit, a confused agent will circle forever, billing you every turn. The stop conditions are *your* job, not the model's. We make this a hard rule in [Phase 3](03-where-agents-go-wrong.md).

```quiz
[
  {
    "q": "What are the three parts of an AI agent?",
    "choices": [
      "A model, a database, and a user interface",
      "A model, a set of tools, and a loop",
      "Two models checking each other and a referee",
      "A prompt, a fine-tuned model, and a GPU"
    ],
    "answer": 1,
    "explain": "An agent is a language model given tools it can call and a loop that runs those tools and feeds results back until the task is done."
  },
  {
    "q": "When the model 'calls a tool,' what actually happens?",
    "choices": [
      "The model executes the function itself, internally",
      "The model requests the call, and your loop's code runs the tool and returns the result",
      "The tool runs inside the training data",
      "Nothing - tool calls are just suggestions the user runs by hand"
    ],
    "answer": 1,
    "explain": "The model only proposes a call. Your code is what actually runs the tool and hands the result back on the next turn. The model is the head; your loop is the hands."
  },
  {
    "q": "What is the one structural difference between a plain LLM feature and an agent?",
    "choices": [
      "Agents use a bigger, more expensive model",
      "Agents wrap the model call in a loop that repeats until the model signals it's done",
      "Agents never hallucinate",
      "Agents run entirely on the user's device"
    ],
    "answer": 1,
    "explain": "A plain feature is a single request-response. An agent puts that same call inside a loop, letting the model gather facts and revise its plan across multiple turns."
  }
]
```


---

# The Reasoning-Acting Cycle

[Phase 1](01-an-agent-is-a-loop.md) landed the shape: reason, act, look, repeat. That leaves the question every builder hits next - *how does the model actually "call" a function?* It only emits text. So how does fuzzy English ("I should check the weather") turn into a clean, runnable call your code can trust?

The answer is **function-calling**: you describe your tools to the model in a strict format, and it replies with a structured request instead of prose. This phase walks the real exchange, message by message, so you can see exactly what crosses the wire each turn.

## Step 1 - You describe the tools with a schema

Before the loop starts, you tell the model what tools exist. Not the code - a *description*: each tool's name, what it does, and what arguments it takes, written as a small JSON schema. Think of it as a menu the model orders from.

```text
tool: get_weather
  description: "Get the current weather for a city."
  parameters:
    city:  string  (required) - "the city name, e.g. Oslo"
    units: string  (optional) - "celsius or fahrenheit"
```

*What just happened:* You handed the model a contract. The `description` lines aren't decoration - the model reads them to decide *when* a tool fits a task and *how* to fill the arguments. Vague descriptions get vague tool calls; this is the cheapest, highest-leverage quality knob you have.

> 📝 **Schema** - a machine-readable description of a tool's name, purpose, and arguments (usually JSON Schema). The model uses it to format a valid call; your code uses it to validate what comes back. Same contract, both sides.

## Step 2 - The model replies with a structured call, not prose

When the model decides a tool fits, it doesn't write "you should check the weather." It returns a structured object naming the tool and its arguments - already parsed, ready to run.

```text
model returns:
{
  "tool_call": {
    "name": "get_weather",
    "arguments": { "city": "Oslo", "units": "celsius" }
  }
}
```

*What just happened:* The model translated its intent into the exact shape your code expects. No regex, no scraping English for a city name - you get a name and a clean argument bag. This structured handoff is the engineering breakthrough that made agents practical; before function-calling, you were parsing freeform text and praying.

## Step 3 - Your loop runs the tool and feeds the result back

Your code takes that object, calls the real `get_weather("Oslo")`, gets a number, and appends the result to the conversation as a new message - tagged as coming from the tool. Then you call the model again.

```text
your loop appends:
{
  "role": "tool",
  "name": "get_weather",
  "content": "12°C, light rain"
}
→ call model again with the updated message list
```

*What just happened:* The tool's output re-entered the conversation as just another message. From the model's point of view, the next turn simply has more information than the last. This is the feedback edge from the Phase 1 diagram, made concrete - and notice nothing is hidden: the whole exchange is plain messages.

## The full exchange in one view

Here's a complete two-tool task as the message list grows. Each block is one message; the loop calls the model once per "→".

```text
[user]   "Should I bring an umbrella to the Oslo office today?"
   → model
[model]  tool_call: get_weather(city="Oslo")
   (your loop runs it)
[tool]   get_weather → "12°C, light rain expected this afternoon"
   → model
[model]  tool_call: get_hours(office="Oslo")
   (your loop runs it)
[tool]   get_hours → "open until 18:00"
   → model
[model]  "Yes - rain's expected this afternoon and the office is
          open until 18:00, so you'll likely be out in it."
```

*What just happened:* The agent ran twice through reason→act→observe, then exited the loop with a final answer because no further tool was needed. The entire "memory" of the task is right there: it's the growing message list. That's the next idea, and it's smaller than it sounds.

## What "memory" actually means here

People imagine agent memory as something exotic. For a single task, it isn't - it's the **conversation history you keep resending**. The model is stateless between calls; it remembers nothing on its own. Every turn, you send the *entire* message list back, and that list *is* the memory.

```text
TURN 3 request to model =  [user msg]
                           + [model: tool_call get_weather]
                           + [tool: 12°C, light rain]
                           + [model: tool_call get_hours]
                           + [tool: open until 18:00]
```

*What just happened:* By turn 3 you're resending everything from turns 1 and 2. The model "remembers" the weather only because you handed it back the message that contained it. Drop a message and the agent forgets that fact instantly - there's no hidden store keeping it.

> 💡 **Key point.** Short-term memory = the message list you resend each turn. Long-term memory (facts that outlive one task) is a separate thing you build - usually by writing notes to a store and *retrieving* the relevant ones into the prompt later. That retrieval-into-the-prompt pattern is exactly what [RAG](/guides/rag-explained) describes; an agent with long-term memory is, under the hood, an agent that does RAG over its own past.

## For builders

The whole cycle is a tidy loop you can write today: keep a `messages` list, call the model, branch on the reply. If it's a tool call, run the named function with the given arguments, append a tool message with the output, and loop. If it's a final answer, return it. The two failure points to anticipate - the model naming a tool that doesn't exist, and the loop never reaching a final answer - are exactly what [Phase 3](03-where-agents-go-wrong.md) is about.

> ⚠️ **Gotcha - never trust the arguments blindly.** The model can return arguments that are malformed, out of range, or pointed somewhere dangerous (a path traversal, an oversized query). Validate every tool call against its schema *before* you execute it. The model is suggesting; your code is responsible for what actually runs.

```quiz
[
  {
    "q": "In function-calling, how does the model 'call' a tool?",
    "choices": [
      "It writes a sentence and your code searches it for keywords",
      "It returns a structured object naming the tool and its arguments",
      "It runs the function directly inside the model",
      "It opens a network socket to the tool's server"
    ],
    "answer": 1,
    "explain": "The model returns a structured call (name + arguments) that your code can run directly - no parsing of freeform English required."
  },
  {
    "q": "After your loop runs a tool, how does the result reach the model?",
    "choices": [
      "It's stored in the model's weights for next time",
      "You append it to the message list as a tool message and call the model again",
      "The model polls your server for it",
      "It's discarded - the model re-derives it"
    ],
    "answer": 1,
    "explain": "The tool output re-enters the conversation as a new message. On the next call the model simply sees more information than before."
  },
  {
    "q": "What is an agent's short-term 'memory' during a single task?",
    "choices": [
      "A vector database the model writes to automatically",
      "The conversation message list you resend on every turn",
      "Hidden internal state the model keeps between calls",
      "The system prompt only"
    ],
    "answer": 1,
    "explain": "The model is stateless between calls. It remembers a fact only because you keep resending the message that contains it. The growing message list IS the memory."
  }
]
```


---

# Where Agents Go Wrong

The loop from the last two phases is elegant on paper. In production it's where the bills, the 2am pages, and the "why did it delete that?" incidents come from. None of this is a reason to avoid agents - it's the reason to build them with a leash. Every failure traces back to the same root: the model decides, but has no built-in sense of *when to stop*, *what's real*, or *what's safe*. Those judgments are your code's job, and skipping them is how a demo becomes a disaster.

This phase is the plain-spoken one. Four ways agents spiral, and the guardrail for each. Build the guardrails first; the agent is the easy part.

## Failure 1 - The infinite loop

The model never decides "done," so your loop never exits. Sometimes it repeats the same tool call forever; sometimes it oscillates between two; sometimes it keeps "almost" finishing. Every turn is a paid model call.

```text
TURN 14  search("invoice total")  → "no results"
TURN 15  search("invoice amount")  → "no results"
TURN 16  search("invoice total")  → "no results"   ← already tried this
TURN 17  search("invoice amount")  → "no results"   ← and this. forever.
```

*What just happened:* The agent got stuck, re-trying near-identical calls because nothing told it to give up. With no limit, this runs until you notice the bill. The fix is a hard ceiling you own - a **step budget**.

```text
MAX_STEPS = 12
for step in range(MAX_STEPS):
    reply = call_model(messages)
    if reply.is_final: return reply
    run_tool_and_append(reply)
else:
    return "Stopped: hit the step budget without finishing."
```

*What just happened:* The loop now *cannot* run more than 12 turns, full stop. A step budget is the single most important guardrail an agent has - it converts "potentially infinite cost" into "bounded cost." Make it the first line you write, before the agent does anything clever.

> ⚠️ **Gotcha - the budget must be a hard stop, not a hint.** Don't put "please finish within 12 steps" in the prompt and call it done. The model may ignore it. The ceiling has to live in *your loop's* control flow, where the model can't talk its way past it.

## Failure 2 - Hallucinated tool calls

The model is a text predictor, and sometimes it predicts a tool that doesn't exist, or arguments that don't match the schema. It'll confidently "call" `send_invoice` when your only tools are `get_invoice` and `list_invoices`.

```text
model returns:  tool_call: send_invoice(id="INV-200")
your tools:     get_invoice, list_invoices        ← no send_invoice here
```

*What just happened:* The model invented a capability it wished it had. If your loop runs tool calls without checking, this throws - or worse, silently does nothing while the loop keeps spinning. The guardrail is plain validation: confirm the tool exists and the arguments fit its schema *before* running, and if they don't, feed the error back as a tool message so the model can correct.

```text
if reply.tool_name not in TOOLS:
    append_tool_error(f"No such tool '{reply.tool_name}'. Available: {list(TOOLS)}")
    continue   # let the model try again, within the step budget
```

*What just happened:* Instead of crashing, you handed the model a correction and let it retry - bounded by the step budget so a stubborn hallucination can't loop forever. Treat every tool call as untrusted input, because that's exactly what it is.

## Failure 3 - Runaway cost

Even a *correct* agent can be expensive, and the reason is sneaky: remember from [Phase 2](02-the-reasoning-acting-cycle.md) that you resend the whole message list every turn. So the cost of each turn *grows* as the conversation does. A 20-turn task isn't 20 cheap calls - the later calls are large.

```text
TURN 1   send  1 message      → small
TURN 5   send  9 messages     → bigger
TURN 12  send 23 messages     → big; you pay for all of it, again
```

*What just happened:* Because the history compounds, total cost climbs faster than the turn count. Two levers tame it: the step budget (caps how many turns happen) and trimming the history (drop or summarize old, no-longer-needed messages so each turn doesn't carry the full pile). The instinct "I'll let it run as long as it needs" is precisely the expensive one.

> 💡 **Key point.** Agent cost is roughly *turns × growing-context*, not *turns × fixed-price*. Budget the turns and prune the context, and you turn an open-ended bill into a predictable one.

## Failure 4 - Unsafe actions

This is the one that keeps people up at night. A tool that only *reads* is low-stakes; a tool that *writes, deletes, sends, or spends* can do real damage from a single confident-but-wrong call. The model doesn't know it's about to email the wrong customer or drop the wrong table.

```text
model: tool_call: delete_records(table="customers", where="status='trial'")
                  ↑ confidently wrong filter - about to delete real accounts
```

*What just happened:* One hallucinated argument on a destructive tool is an incident. The guardrail is an **approval gate**: high-impact tools pause and require a human (or a stricter rule) to confirm before they run. Read-only tools run freely; anything that changes the world stops for a check.

```text
if TOOLS[name].is_destructive:
    if not human_approves(name, arguments):
        append_tool_error("Action declined by approver.")
        continue
run_tool(name, arguments)
```

*What just happened:* You drew a line between "let it explore" and "let it act." The agent can plan all it wants; the moment it reaches for something irreversible, a human is in the loop. The lazy-but-correct default: every new tool is read-only until you've earned the trust to let it write.

## The guardrail checklist

Before any agent touches production, walk this list. It's the difference between a tool you trust and one you fear.

```text
☐ STEP BUDGET     hard turn limit in the loop (not the prompt)
☐ VALIDATE        check tool name + arguments against the schema before running
☐ PRUNE CONTEXT   trim/summarize history so cost stays bounded
☐ APPROVAL GATE   human confirm for write/delete/send/spend tools
☐ OBSERVE         log every turn - reasoning, tool, args, result, cost
```

*What just happened:* You turned four failure modes into five concrete defenses. The last one - observability - is the quiet hero: when an agent misbehaves (and it will), a per-turn log of what it decided and why is the only way to debug something whose "code path" was chosen by a model at runtime.

## For builders

Build the cage before the animal. Write the loop with the step budget and validation in place *first*, give it one read-only tool, log every turn, and watch it run end to end. Only then add a second tool. Add anything destructive last, behind an approval gate, and never on the same day you ship. An agent is genuinely a small amount of code - the engineering is almost entirely in the guardrails, and that's where your attention belongs.

```quiz
[
  {
    "q": "What is the single most important guardrail against an agent looping forever?",
    "choices": [
      "A bigger model that's less likely to get confused",
      "A hard step budget enforced in your loop's control flow",
      "A polite instruction in the prompt to finish quickly",
      "Lowering the temperature setting"
    ],
    "answer": 1,
    "explain": "A step budget enforced in code caps the number of turns no matter what the model does, converting potentially infinite cost into bounded cost. A prompt hint can be ignored."
  },
  {
    "q": "Why does a longer agent task cost more than its turn count alone suggests?",
    "choices": [
      "Later turns use a more expensive model automatically",
      "You resend the whole message history each turn, so each turn's context grows",
      "The model charges a penalty for slow tools",
      "It doesn't - cost is flat per turn"
    ],
    "answer": 1,
    "explain": "Because the full conversation is resent every turn, the per-turn context (and cost) grows as the task goes on. Cost is roughly turns times growing-context."
  },
  {
    "q": "What's the right guardrail for a tool that deletes or sends things?",
    "choices": [
      "Run it freely - the model is usually right",
      "Put it behind an approval gate so a human confirms before it runs",
      "Remove it from the schema so the model can't see it",
      "Retry it three times to be safe"
    ],
    "answer": 1,
    "explain": "Destructive tools should pause for human (or stricter-rule) approval before executing. Read-only tools can run freely; anything that changes the world stops for a check."
  }
]
```
