Lesson 188 of 2116
Build It: A Minimal AI Agent Loop From Scratch
An agent is a loop: model decides, tool runs, model reads result, decides again. You'll build one in 100 lines without a framework.
Lesson map
What this lesson covers
Learning path
The main moves in order
- 1No framework, just the loop
- 2agent loop
- 3tool use
- 4function calling
Concept cluster
Terms to connect while reading
Section 1
No framework, just the loop
Every 'agent' library — LangChain, CrewAI, LangGraph — is built on the same 20-line core. Writing it by hand once makes every library make sense forever. We'll build an agent that can search the web, read files, and do math.
Two tools: a sandboxed calculator and a URL fetcher. Real tools would be broader.
# pyproject.toml: anthropic, httpx
import json
import httpx
from anthropic import Anthropic
client = Anthropic()
MODEL = "claude-opus-4-7"
# --- Tools the agent can use ---
def tool_calculate(expression: str) -> str:
# Pretend-safe eval for a DEMO — never use eval on untrusted input in production.
allowed = set("0123456789+-*/(). ")
if not set(expression).issubset(allowed):
return "Error: expression has disallowed characters"
try:
return str(eval(expression))
except Exception as e:
return f"Error: {e}"
def tool_fetch(url: str) -> str:
try:
r = httpx.get(url, timeout=10, headers={"User-Agent": "tendril-agent/1.0"})
r.raise_for_status()
return r.text[:4000]
except Exception as e:
return f"Error fetching: {e}"
TOOLS = {
"calculate": tool_calculate,
"fetch_url": tool_fetch,
}Tool specs — what the model sees when deciding which tool to call.
TOOL_SPECS = [
{
"name": "calculate",
"description": "Evaluate a basic arithmetic expression. Only digits, +,-,*,/,()",
"input_schema": {
"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"],
},
},
{
"name": "fetch_url",
"description": "Fetch the first 4KB of text from a URL.",
"input_schema": {
"type": "object",
"properties": {"url": {"type": "string"}},
"required": ["url"],
},
},
]The loop: call model → if tools, run them and feed back → if done, return. That's the entire agent.
def run_agent(task: str, max_steps: int = 8) -> str:
messages = [{"role": "user", "content": task}]
for step in range(max_steps):
response = client.messages.create(
model=MODEL,
max_tokens=1500,
tools=TOOL_SPECS,
messages=messages,
)
# Save assistant turn
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
# Agent is done — return final text
for block in response.content:
if block.type == "text":
return block.text
return "(no text)"
# Otherwise gather tool calls and run them
tool_results = []
for block in response.content:
if block.type == "tool_use":
func = TOOLS.get(block.name)
result = func(**block.input) if func else f"No such tool: {block.name}"
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result),
})
messages.append({"role": "user", "content": tool_results})
return "Agent exceeded max steps without finishing."
if __name__ == "__main__":
print(run_agent("What is 25 * 34, and what's on https://example.com — give one sentence about each."))Advanced: persistent memory
Turning agent state into JSON lets you pause and resume — the foundation of every production agent.
# Save the messages list to disk so the agent can resume a conversation
import json
from pathlib import Path
def save_state(messages, path=Path("agent_state.json")):
# Serialize tool_use/tool_result blocks into dicts
serialized = []
for m in messages:
if isinstance(m["content"], str):
serialized.append(m)
else:
serialized.append({
"role": m["role"],
"content": [b.model_dump() if hasattr(b, "model_dump") else b for b in m["content"]]
})
path.write_text(json.dumps(serialized, indent=2))Mini-exercise
- 1Add a tool read_file(path: str) that returns the first 2KB of a local file
- 2Add a max-cost guard: estimate tokens per step and stop if over budget
- 3Ask the agent to 'read my notes.txt and summarize'
- 4Print the step-by-step trace so you can see what tools it chose
Compare the options
| Chatbot | Agent |
|---|---|
| One response per turn | Many steps per turn |
| No side effects | Calls tools, touches the world |
| Linear | Branches until goal met |
| Predictable | Needs guardrails |
Key terms in this lesson
Big idea: an agent is shockingly simple code. Frameworks add conveniences (retries, logging, streaming), but the core is a for-loop with two cases: 'call a tool' or 'stop.'
End-of-lesson quiz
Check what stuck
15 questions · Score saves to your progress.
Tutor
Curious about “Build It: A Minimal AI Agent Loop From Scratch”?
Ask anything about this lesson. I’ll answer using just what you’re reading — short, friendly, grounded.
Progress saved locally in this browser. Sign in to sync across devices.
Related lessons
Keep going
Creators · 35 min
Cursor Agent — autonomous coding in your editor
Cursor Agent is the editor equivalent of Claude Code — give it a goal, it reads, writes, tests, and commits across files.
Creators · 50 min
Tool-Use Patterns
The model calls a function you defined, you run it, you return the result. Learn the loop and the common pitfalls.
Creators · 60 min
Capstone — Python CLI That Summarizes With Claude
Tie it all together. A command-line tool that reads a file, calls Claude, and prints a summary. Real code, real errors, real polish.
