Articles

Build Your Own AI Agent in 100 Lines of Python

Build a real AI agent from scratch — no framework. Just the Anthropic API, a tool-use loop, and two tools the model can call to explore your files.

Chisato Chisato · · 4 min read
Abstract network of connected AI agents

“Agent” sounds like it needs a heavyweight framework. It doesn’t. Strip away the abstractions and an AI agent is one small idea: a language model in a loop, calling tools you give it, until the task is done. In this guide we’ll build a working agent in about a hundred lines of Python — no LangChain, no orchestration library — using Anthropic’s API and its SDK. Our agent will be able to explore a directory and answer questions about your code.

The whole idea, in one diagram

The loop is the agent:

  1. Send the conversation (plus a list of tools) to the model.
  2. The model either answers, or asks to call a tool.
  3. If it called a tool, you run it and feed the result back.
  4. Repeat until the model stops asking for tools.

That’s it. Everything else — memory, planning, multi-step reasoning — emerges from running this loop with good tools.

Setup

Install the SDK and set your API key:

pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..."
import os
from anthropic import Anthropic

client = Anthropic()                 # reads ANTHROPIC_API_KEY from the environment
MODEL = "claude-opus-4-8"

Defining tools

A tool is a name, a description, and a JSON Schema for its inputs. The description matters more than you’d think — it’s how the model decides when to reach for the tool. We’ll give our agent two: one to list a directory, one to read a file.

TOOLS = [
    {
        "name": "list_files",
        "description": "List files and folders in a directory. Use this to "
                       "explore the project before reading individual files.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Directory to list. Use '.' for the current folder."}
            },
            "required": ["path"],
        },
    },
    {
        "name": "read_file",
        "description": "Read and return the contents of a text file.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Path to the file to read."}
            },
            "required": ["path"],
        },
    },
]

Implementing the tools

The model never runs code — you do. Each tool is an ordinary Python function. Keep them confined to a safe directory; the model’s inputs are untrusted, so never feed a raw path straight into the filesystem in production.

def list_files(path="."):
    return "\n".join(sorted(os.listdir(path)))

def read_file(path):
    with open(path, "r", encoding="utf-8") as f:
        return f.read()

TOOL_FUNCTIONS = {"list_files": list_files, "read_file": read_file}

def run_tool(name, tool_input):
    try:
        return TOOL_FUNCTIONS[name](**tool_input)
    except Exception as exc:                       # surface errors back to the model
        return f"Error: {exc}"

Notice we return errors as strings instead of crashing. The model reads the error and adapts — tries a different path, asks a clarifying question — which is half of what makes an agent feel capable.

The loop

Here’s the engine. We call the API; if the model’s stop_reason is tool_use, we execute every tool it requested, append the results, and go around again. Crucially, we append the model’s entire response (response.content) back into the history — that preserves the tool_use blocks the API needs to match against our results.

def run_agent(user_message):
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            system="You are a coding assistant. Use the tools to explore the "
                   "project, then answer the user's question concisely.",
            tools=TOOLS,
            messages=messages,
        )

        for block in response.content:               # show the model's narration
            if block.type == "text":
                print(block.text)

        if response.stop_reason != "tool_use":
            return                                   # end_turn — the agent is done

        messages.append({"role": "assistant", "content": response.content})

        results = []
        for block in response.content:
            if block.type == "tool_use":
                print(f"  → {block.name}({block.input})")
                output = run_tool(block.name, block.input)
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,         # must match the request
                    "content": output,
                })
        messages.append({"role": "user", "content": results})


if __name__ == "__main__":
    run_agent("What does this project do? Look around and summarize it.")

Watch it think

Run it inside any project directory and you’ll see the agent work the loop on its own:

  → list_files({'path': '.'})
  → read_file({'path': 'package.json'})
  → read_file({'path': 'src/index.ts'})
This is a small Astro blog. It builds static pages from Markdown in
src/content/blog, deploys to Cloudflare Pages, and includes an RSS feed...

Nobody told it to read package.json first — it decided that listing the directory, then reading the manifest, then the entry point was the way to understand an unfamiliar project. That planning is the model’s; the loop just gave it room to act.

Where to go next

This 100-line core is genuinely the same shape the big frameworks wrap. To grow it:

  • More tools. Add write_file, run_command, or a web search. Capability scales with the tools you expose.
  • Standardize them with MCP. The Model Context Protocol lets your agent plug into tools other people built, instead of hand-writing each one. The layer above — how agents discover which tools exist — is being standardized too, under ARD.
  • Run it cheaper or locally. Swap in a smaller model, or run a model locally with Ollama for offline experiments. For production, prompt caching cuts the cost of resending the conversation each turn.
  • Let it think. Enabling extended thinking gives the model room to reason between tool calls on harder tasks.

The takeaway

An agent isn’t a framework — it’s a loop. Once you’ve written while True: call the model, run its tools, feed back the results, every agent product you’ve read about becomes legible. Start here, add one tool at a time, and you’ll understand exactly where the intelligence lives and where your code does.

Chisato Chisato · · 4 min read

What Is Prompt Chaining? Multi-Step LLM Pipelines

Prompt chaining splits a task into a sequence of smaller LLM calls, each one feeding the next, instead of asking one giant prompt to do everything.

#AI #LLMs #Developer Tools