Lesson 164 of 1596
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.
Creators · AI-Assisted Coding · ~45 min read
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
8 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.
