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.
“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:
- Send the conversation (plus a list of tools) to the model.
- The model either answers, or asks to call a tool.
- If it called a tool, you run it and feed the result back.
- 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.
Tagged
Keep reading
Chisato · · 5 min read LLM Observability: Tracing AI Agents in Production
LLM observability traces every prompt, tool call, and token spent across an agent's run, turning an opaque chain of model calls into something debuggable.
Chisato · · 4 min read What Is Context Rot in LLMs?
Context rot is the drop in an LLM's accuracy and reliability as the amount of text in its context window grows, even when the window technically fits it.
Chisato · · 4 min read RAG vs Long-Context LLMs: Do You Still Need It?
Retrieval-augmented generation and long context windows both feed an LLM more information — but they solve different problems and cost differently.