# Build an AI agent with the Claude API from scratch

> An AI agent on the Claude API is a loop. You send the conversation and your tools, run whatever tools Claude asks for, append the results and call again until stop_reason is no longer tool_use. A turn limit stops runaway loops, and compaction keeps the history inside the context window. This lesson writes that loop in Python.

Mindket editors · Published 2026-11-04 · Updated 2026-11-05 · https://mindket.com/learn/claude-api/build-ai-agent-claude-api

## In short

- Loop while stop_reason is tool_use, and append the whole response each turn.
- Cap the turns yourself, because the client loop has no built-in limit.
- Return tool failures as is_error results and guard every path the model picks.
- Plan for context growth with caching and compaction, then pick the right tool for the loop.

## Before you start

This lesson uses everything so far: the Messages call from lesson 1, the tool round trip from lesson 2 and the cache from lesson 3. If the word agent is new, [what is an AI agent](https://mindket.com/learn/prompt-engineering/what-is-an-ai-agent) comes first.

[The Agent SDK overview](https://code.claude.com/docs/en/agent-sdk/overview) defines an agent as an application that completes a task by planning its own steps and calling tools. On the Claude API that is a loop around one call: Claude picks the next step, your code runs it and the result goes back. The sample answers questions about a folder of Markdown files with two tools, `list_files` and `read_file`.

## Write the agent loop

The loop follows the canonical shape on the [how tool use works page](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works): keep calling while `stop_reason` is `tool_use`. It adds three things the basic loop in the docs' [tool-using agent tutorial](https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent) leaves to you: a turn limit, a path guard and `is_error` results.

The code is a sample written for this lesson. Put a few Markdown files in a `docs` folder, export your key and run it.

````python
from pathlib import Path

import anthropic

client = anthropic.Anthropic()

MODEL = "claude-opus-5-5"
MAX_TURNS = 12
ROOT = Path("docs").resolve()
SYSTEM = "Answer only from the files in the docs folder. If the answer is not there, say so."

TOOLS = [
    {
        "name": "list_files",
        "description": (
            "List the Markdown files in the docs folder. Use it first to see what "
            "exists. Pass a glob such as 'guides/*.md' to narrow the list. It returns "
            "paths relative to the docs folder, one per line."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {"type": "string", "description": "Optional glob, default **/*.md"},
            },
        },
    },
    {
        "name": "read_file",
        "description": (
            "Read one Markdown file from the docs folder and return its text. Use a "
            "path from list_files. It fails for any path outside the folder."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Path relative to the docs folder"},
            },
            "required": ["path"],
        },
    },
]


def safe_path(relative: str) -> Path:
    path = (ROOT / relative).resolve()
    if not path.is_relative_to(ROOT):
        raise ValueError("That path is outside the docs folder.")
    return path


def run_tool(name: str, args: dict) -> str:
    if name == "list_files":
        pattern = args.get("pattern", "**/*.md")
        files = sorted(p.relative_to(ROOT).as_posix() for p in ROOT.glob(pattern) if p.is_file())
        return "\n".join(files) or "No files match."
    if name == "read_file":
        return safe_path(args["path"]).read_text(encoding="utf-8")[:20000]
    raise ValueError(f"Unknown tool: {name}")


def run_agent(task: str) -> str:
    messages = [{"role": "user", "content": task}]
    for turn in range(1, MAX_TURNS + 1):
        response = client.messages.create(
            model=MODEL,
            max_tokens=2048,
            system=SYSTEM,
            tools=TOOLS,
            cache_control={"type": "ephemeral"},
            messages=messages,
        )
        print(f"turn {turn}: {response.stop_reason}, cache read {response.usage.cache_read_input_tokens}")
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            break
        results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            try:
                content, is_error = run_tool(block.name, block.input), False
            except Exception as error:
                content, is_error = str(error), True
            results.append(
                {"type": "tool_result", "tool_use_id": block.id, "content": content, "is_error": is_error}
            )
        messages.append({"role": "user", "content": results})
    else:
        return f"Stopped after {MAX_TURNS} turns."

    if response.stop_reason != "end_turn":
        return f"Stopped early: {response.stop_reason}"
    return "".join(block.text for block in response.content if block.type == "text")


print(run_agent("Which file explains how to reset a password, and what is its first heading?"))
````

1. Define the two tools with long descriptions, as lesson 2 explains.
2. Guard every path. `safe_path` resolves the path and refuses anything outside `docs`, because the model chooses the argument and your code is the only check.
3. Keep one `messages` list and append to it each turn: the assistant's `response.content` whole, then one user message that holds every `tool_result`.
4. Leave the loop on any stop reason except `tool_use`, and return the text only for `end_turn`.
5. Count turns. If the `for` loop runs out, the `else` branch reports the limit instead of looping on.
6. Run it and watch the cache-read number. It starts at 0 and climbs once the history passes the model minimum, 512 tokens on Opus 5.5.

## What happens on each turn

The walk-through sketches the message flow for the sample's question. It illustrates the protocol and is not a recording of a real run.

**One question, three calls**

1. Claude asks for the file list. The first response ends with stop_reason tool_use and a list_files call. Your code runs it and the output goes back as a tool_result in a user message.
2. Claude opens one file. The second response calls read_file with a path from that list. After its result goes back, the history is five messages: the task, two assistant turns and two result turns.
3. Claude answers. The third response has stop_reason end_turn and a text block. The loop leaves and returns the text.
4. Every call resends the history. The third request is larger than the first. With the top-level cache_control, the part already seen is read from the cache once the prefix reaches the model minimum.

## Stop conditions

The API decides when Claude is done. You decide when to give up. The docs' basic loop is a `while` on `stop_reason`, and the tool runner's `max_iterations` is optional, so a loop with no counter of its own can run until the context fills.

Count turns and cap them. The [stop reasons page](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons) lists no signal for a turn count or a budget, so both limits belong in your code. If you carry a budget, add up the `usage` of every response as well.

| Stop signal | Where it comes from | What the loop does |
| --- | --- | --- |
| `end_turn` | Claude finished | Return the text |
| `tool_use` | Claude wants a tool | Run it, append the results, call again |
| `max_tokens` | The reply hit your limit | Stop and report, or raise the limit |
| `refusal` | Claude declined | Read `stop_details`, then retry on a fallback model |
| `model_context_window_exceeded` | The context window filled | Treat the reply as truncated, compact earlier |
| `pause_turn` | A server-tool loop reached its limit | Send the response back as it came, then continue |
| Turn count, token budget, clock | Your own code | Stop and say why |

## Context growth

Every turn resends the whole history, so each call is larger than the one before. Tool results do most of the growing: a file that read_file returns stays in every later request. The sample cuts a file at 20,000 characters for that reason. Prompt caching makes the repeated part cheaper but does not make it smaller.

Size matters beyond the window too. The [compaction overview](https://platform.claude.com/docs/en/build-with-claude/compaction) says response quality degrades as a conversation grows, which is why it offers to replace older turns with a summary Claude writes on the server. Both kinds are in beta.

Threshold compaction is the smaller change to a loop like this one: one parameter on ordinary requests, and the response with its compaction block goes into the history whole. On-demand compaction suits an application that must pick the moment, and the docs say to use it wherever it is available.

**Numbers that shape the loop**

- **1M** tokens of context on each current model (Source: [Anthropic Docs: Models overview](https://platform.claude.com/docs/en/models/overview), read 2026-11-05)
- **150,000** input tokens before threshold compaction starts, by default (Source: [Anthropic Docs: Compaction at a token threshold](https://platform.claude.com/docs/en/build-with-claude/compaction-threshold), read 2026-11-04)
- **50,000** the lowest trigger value you can set (Source: [Anthropic Docs: Compaction at a token threshold](https://platform.claude.com/docs/en/build-with-claude/compaction-threshold), read 2026-11-04)

````python
response = client.beta.messages.create(
    betas=["compact-2026-01-12"],
    model="claude-opus-5-5",
    max_tokens=4096,
    messages=messages,
    context_management={"edits": [{"type": "compact_20260112"}]},
)
messages.append({"role": "assistant", "content": response.content})  # the compaction block stays in
````

| Option | Who decides when | What happens |
| --- | --- | --- |
| Compaction at a token threshold | The API, at your trigger | It summarizes inside an ordinary request. Beta header `compact-2026-01-12`, edit type `compact_20260112` |
| Compaction on demand | Your application | A separate request with `compaction` set to summarize returns one summary block to put first in `messages`. Beta header `compact-2026-09-04` |
| Context editing | Rules you set | It clears old tool results or old thinking blocks instead of summarizing them |

## When to stop hand-writing the loop

Write the loop by hand once, as above, so you know what a library does for you. The tool runner, in beta in the SDKs, runs the same loop from `client.beta.messages.tool_runner(...)`. It turns a tool exception into an `is_error` result and stops at `max_iterations` when you set it. The docs still send you back to the manual loop for human approval, custom logging or conditional execution ([tool runner page](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner)).

The Agent SDK is a different kind of tool. It is Claude Code as a library, with its built-in file, shell and web tools, hooks, subagents, MCP, permissions and sessions, in Python and TypeScript ([overview](https://code.claude.com/docs/en/agent-sdk/overview)). Use it when you want all of that built in. The [Claude Agent SDK lesson](https://mindket.com/learn/claude-code-advanced/claude-agent-sdk) shows how.

**Three ways to run an agent on Claude**

| | Manual loop | Tool runner (Our pick) | Agent SDK |
| --- | --- | --- | --- |
| Runs the loop for you | ✗ No | ✓ Yes | ✓ Yes |
| Approval before a tool runs | ✓ Yes | ✗ No | ✓ Yes |
| Built-in file, shell and web tools | ✗ No | ✗ No | ✓ Yes |
| Hooks, subagents and sessions | ✗ No | ✗ No | ✓ Yes |
| Works over plain HTTP | ✓ Yes | ✗ No | ✗ No |

**Our pick: Tool runner.** For your own tools with no approval step, it removes the loop code and keeps your requests. Take the manual loop when a person approves each call, and the Agent SDK when you want Claude Code's own tools.

Sources: [Anthropic Docs: Tool runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner) (read 2026-11-04), [Claude Code Docs: Agent SDK overview](https://code.claude.com/docs/en/agent-sdk/overview) (read 2026-11-04)

## Simon Willison and Anthropic on what an agent loop is

Simon Willison, creator of Datasette and co-creator of Django, [posted on X](https://x.com/simonw/status/1968755421201711588) on 18 September 2025 that he was ready to accept one definition of an agent. He had dismissed the word as hopelessly ambiguous for years. The definition is the line quoted below, and the loop in this lesson fits it: the tools are `list_files` and `read_file`, the goal is the user's question.

Anthropic [posted on 30 September 2025](https://x.com/AnthropicAI/status/1973098580060631341) to share an engineering blog piece on [context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents). It names the problem behind context growth: as the window fills, the model recalls less of what is in it, which the authors call context rot. Among the safest and lightest kinds of compaction it lists clearing old tool results, the job of the context editing row in the compaction table. Try that before you summarize.

> An LLM agent runs tools in a loop to achieve a goal
>
> Simon Willison, Creator of Datasette and co-creator of Django, [X](https://x.com/simonw/status/1968755421201711588) (read 2026-11-05)

[![Simon Willison (@simonw) on X, 18 September 2025: I'm ready to accept a definition of "agent" that I think is widely-enough agreed upon to be useful. An LLM agent runs tools in a loop to achieve a goal. He adds that he had dismissed the term as hopelessly ambiguous for years.](https://mindket.com/blog/shots/x-simonw-tools-in-a-loop.webp?v=fb0c262da094)](https://x.com/simonw/status/1968755421201711588)

Simon Willison on X, 18 September 2025

[![Anthropic (@AnthropicAI) on X, 30 September 2025: New on the Anthropic Engineering Blog. Most developers have heard of prompt engineering. But to get the most out of AI agents, you need context engineering. We explain how it works.](https://mindket.com/blog/shots/x-anthropicai-context-engineering.webp?v=89912d75f228)](https://x.com/AnthropicAI/status/1973098580060631341)

Anthropic on X, 30 September 2025

[How We Build Effective Agents: Barry Zhang, Anthropic](https://www.youtube.com/watch?v=D7_ipDqhtwk)

Barry Zhang of Anthropic on the AI Engineer YouTube channel, in a talk titled How We Build Effective Agents.

> Screenshots of X's own embeds of the posts, taken on 5 November 2026, with the time in UTC. Select one to open the post on X. The Anthropic embed leaves out the link that ends the post.

## Common mistakes

- Running the loop with no turn cap, and finding out about it on the invoice.
- Rebuilding the assistant message from text blocks and dropping the thinking blocks the API wants back.
- Sending each tool_result in its own user message instead of one message per turn.
- Trusting a path, a command or a URL the model chose without checking it in code.
- Returning a whole file or page as one result and letting it ride along in every later request.
- Treating text inside a tool result as instructions. It is untrusted data.
- Reading stop_reason max_tokens as a finished answer.

## Try it yourself

- Set `MAX_TURNS` to 2 and ask a question that needs three tool calls. What does the function return?
- Ask the agent to read `../secrets.txt` and watch the error result. Does Claude recover or explain?
- Run the same question twice in a row and compare the cache-read numbers per turn.
- Rewrite `run_agent` with the tool runner and compare how much code is left.

## Next

This agent finds answers by listing and opening files, which stops working once the folder holds thousands of pages. The next course builds agents on OpenAI's API, starting with [the OpenAI API quickstart](https://mindket.com/learn/openai-api/openai-api-quickstart). The [RAG tutorial](https://mindket.com/learn/rag/rag-tutorial) after it covers retrieval over your own documents. If you want the tools reachable from other clients too, [build an MCP server](https://mindket.com/learn/mcp/build-mcp-server-python) around them.

## Check yourself

1. What keeps an agent loop from running forever when Claude keeps calling tools?

   A. The API stops it after ten turns
   B. A turn limit in your own code
   C. The tool_choice parameter
   D. The cache lifetime

   Answer: B. The client loop exits only on a stop reason other than tool_use, so you count the turns yourself.

2. Where do all the tool_result blocks for one assistant turn go?

   A. Each in its own user message
   B. In the system prompt
   C. In one user message, each with its tool_use id
   D. In an assistant message

   Answer: C. One user message per turn keeps the ids matched and keeps Claude making parallel calls.

3. Why append response.content whole instead of only the text blocks?

   A. The tool_use and thinking blocks have to go back unchanged
   B. Text blocks are always empty after a tool call
   C. It shrinks the history
   D. Python lists cannot be filtered

   Answer: A. The API needs the tool_use blocks to match the results and requires thinking blocks back unchanged.

4. You need a person to approve each tool call in an agent on the Client SDK. What do the docs recommend?

   A. The tool runner
   B. Compaction on demand
   C. Prompt caching
   D. A manual loop

   Answer: D. The tool runner page sends you to the manual loop for human approval, custom logging or conditional execution.

## Frequently asked questions

### How many turns should the loop allow?

The docs give no number. Their tool runner examples use 10 for max_iterations, and this lesson's sample uses 12. Set yours a little above the longest legitimate run in your logs.

### Where do I add human approval?

In a manual loop, between reading the tool_use blocks and running them. If the person says no, send a tool_result with is_error true and the reason, because every tool_use id still needs an answer.

### Can the same loop use Anthropic's server tools such as web search?

Yes, but handle pause_turn. A server-tool loop stops after its iteration limit, which the docs put at 10 by default, and you continue by sending the response back as it came.

### Can I sign users in with their claude.ai account?

No. Unless Anthropic has approved it beforehand, third-party products, agents built on the Agent SDK included, cannot offer claude.ai login or its rate limits. Use API key authentication.

## Sources

- [Anthropic Docs: How tool use works](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works) (read 2026-11-04)
- [Anthropic Docs: Tutorial, build a tool-using agent](https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent) (read 2026-11-04)
- [Anthropic Docs: Tool runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner) (read 2026-11-04)
- [Anthropic Docs: Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons) (read 2026-11-04)
- [Anthropic Docs: Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) (read 2026-11-04)
- [Anthropic Docs: Compaction overview](https://platform.claude.com/docs/en/build-with-claude/compaction) (read 2026-11-04)
- [Anthropic Docs: Compaction at a token threshold](https://platform.claude.com/docs/en/build-with-claude/compaction-threshold) (read 2026-11-04)
- [Anthropic Docs: Compaction on demand](https://platform.claude.com/docs/en/build-with-claude/compaction-on-demand) (read 2026-11-04)
- [Anthropic Engineering: Effective context engineering for AI agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents) (read 2026-11-05)
- [X: Simon Willison on the definition of an LLM agent](https://x.com/simonw/status/1968755421201711588) (read 2026-11-05)
- [X: Anthropic shares its context engineering article](https://x.com/AnthropicAI/status/1973098580060631341) (read 2026-11-05)
- [YouTube (AI Engineer): How We Build Effective Agents, Barry Zhang, Anthropic](https://www.youtube.com/watch?v=D7_ipDqhtwk) (read 2026-11-05)
- [Anthropic Docs: Models overview](https://platform.claude.com/docs/en/models/overview) (read 2026-11-05)
- [Claude Code Docs: Agent SDK overview](https://code.claude.com/docs/en/agent-sdk/overview) (read 2026-11-04)

[Browse AI agents](https://mindket.com/agents)
