Mindket

Build an AI agent with the Claude API from scratch

A black banner titled "Claude API" over the line "Calls, tools, caching and an agent loop".

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.

In this lesson

  • 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.
All courses
On this page
  1. Before you start
  2. Write the agent loop
  3. What happens on each turn
  4. Stop conditions
  5. Context growth
  6. When to stop hand-writing the loop
  7. Simon Willison and Anthropic on what an agent loop is
  8. Common mistakes
  9. Try it yourself
  10. Next
  11. Check yourself
  12. Frequently asked questions
  13. Sources

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 comes first.

The 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: keep calling while stop_reason is tool_use. It adds three things the basic loop in the docs' tool-using agent tutorial 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.

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 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 reasons from the stop reasons page, read on 4 November 2026.
Stop signalWhere it comes fromWhat the loop does
end_turnClaude finishedReturn the text
tool_useClaude wants a toolRun it, append the results, call again
max_tokensThe reply hit your limitStop and report, or raise the limit
refusalClaude declinedRead stop_details, then retry on a fallback model
model_context_window_exceededThe context window filledTreat the reply as truncated, compact earlier
pause_turnA server-tool loop reached its limitSend the response back as it came, then continue
Turn count, token budget, clockYour own codeStop 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 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 (read November 5, 2026)

150,000

input tokens before threshold compaction starts, by default

Source: Anthropic Docs: Compaction at a token threshold (read November 4, 2026)

50,000

the lowest trigger value you can set

Source: Anthropic Docs: Compaction at a token threshold (read November 4, 2026)

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
From the compaction overview, the threshold page and the on-demand page, read on 4 November 2026.
OptionWho decides whenWhat happens
Compaction at a token thresholdThe API, at your triggerIt summarizes inside an ordinary request. Beta header compact-2026-01-12, edit type compact_20260112
Compaction on demandYour applicationA separate request with compaction set to summarize returns one summary block to put first in messages. Beta header compact-2026-09-04
Context editingRules you setIt 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).

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). Use it when you want all of that built in. The Claude Agent SDK lesson shows how.

Three ways to run an agent on Claude
Manual loopTool runner Our pickAgent SDK
Runs the loop for youNoYesYes
Approval before a tool runsYesNoYes
Built-in file, shell and web toolsNoNoYes
Hooks, subagents and sessionsNoNoYes
Works over plain HTTPYesNoNo

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 (read November 4, 2026), Claude Code Docs: Agent SDK overview (read November 4, 2026)

Simon Willison and Anthropic on what an agent loop is

Simon Willison, creator of Datasette and co-creator of Django, posted on X 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 to share an engineering blog piece on context engineering. 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 (read November 5, 2026)
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.
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.
Anthropic on X, 30 September 2025
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. The RAG tutorial after it covers retrieval over your own documents. If you want the tools reachable from other clients too, build an MCP server around them.

Check yourself

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

    Show answer

    B. A turn limit in your own code 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?

    Show answer

    C. In one user message, each with its tool_use id 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?

    Show answer

    A. The tool_use and thinking blocks have to go back unchanged 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?

    Show answer

    D. A manual loop 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

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

Share this lesson

See how finished agents are listed

Browse AI agents on Mindket, with their setup notes, then list the agent you build here.

Browse AI agents

This lesson as Markdown · Claude API course: build an AI agent from scratch · All courses