An AI agent in ~100 lines of Python — no LangChain, no CrewAI, just the four primitives.
Visit Product ↗Everyone wants to "build an AI agent," and almost everyone starts by pip install-ing a framework. LangChain, CrewAI, AutoGen — thousands of lines of abstraction that promise to do the hard part for you. Then the demo breaks, the abstractions leak, and you're debugging someone else's control flow with no idea what's actually happening underneath.
I wanted to solve the opposite problem. Not "how do I orchestrate the most complex agent?" but "what is the smallest thing that is genuinely an agent, and what are its irreducible parts?" The answer turned out to be about a hundred lines of Python and exactly four building blocks. This is the full journey — how I framed the problem, did discovery, scoped the MVP, and built it — treated like a product, not a tutorial.
"Agents" feel like magic because frameworks hide the mechanics. The opportunity isn't a bigger agent — it's the smallest one that makes the mechanics obvious.
Frameworks are too abstract to learn from; blog tutorials are too shallow to trust. There's a gap for a runnable reference that teaches the primitives.
Scope to brain, memory, tools, and a loop — plus three test cases that prove each path. Everything else is a non-goal.
Implement each primitive directly on the Anthropic Python SDK so nothing is hidden — the model, the message list, the tool schema, the loop.
Three test cases exercise the no-tool path, the single-tool path, and multi-step reasoning — so "it works" is demonstrable, not asserted.
Ship to GitHub with a README, a one-command run, and an interactive infographic so the architecture is legible at a glance.
Start with the discovery question every PM should ask: what are people actually trying to do, and why does the current solution fail them?
What people want is to understand and control how an agent reasons, remembers, and acts. What they reach for is a framework — and frameworks optimise for the opposite: they hide reasoning, remembering, and acting behind decorators and config so you can ship fast without understanding. That's a fine trade when you already know the primitives. It's a terrible one when you're trying to learn them, because the first time something breaks you have no mental model to debug against.
| Approach | Great at | Fails the learner because… |
|---|---|---|
| LangChain / CrewAI | Composing complex, multi-agent systems fast | The agent loop, memory, and tool dispatch are all abstracted away — you can't see the mechanics |
| Blog tutorials | A quick copy-paste win | Usually a single prompt call — no real loop, no tool use, nothing to generalise from |
| Reading the docs | Reference accuracy | Shows the API surface, not how the pieces compose into an agent |
| Raw SDK, from scratch | Seeing every moving part | Nothing — this is the gap. Slightly more code, total clarity |
The gap: there was no minimal, runnable reference that shows the four primitives of an agent with nothing hidden. That gap is the product.
I wrote down three people who feel this pain differently — and one shared need.
The shared need: a mental model you can hold in your head. That became the design constraint — if it doesn't fit on one screen, it's too big for v1.
The unlock was realising that every LLM agent, no matter how elaborate, reduces to four parts. Strip away orchestration and you're left with a brain, a memory, some tools, and a loop that ties them together.
Claude as the LLM, with a system prompt that sets personality and reasoning style.
A growing messages list, sent in full on every call — context across turns.
A CalculatorTool exposing a JSON schema the model reads to decide when to call it.
Iterates until stop_reason == "end_turn"; each tool call feeds back as input.
Takeaway: Naming the irreducible parts is the highest-leverage design decision. Once "agent = brain + memory + tools + loop" was on paper, the scope wrote itself.
I scoped v1 against measurable goals, so "done" wasn't a feeling.
| Goal | Success looks like |
|---|---|
| Teach the four primitives | Each block is a named, readable unit of code |
| Fit in one mental model | The whole agent is ~100 lines, one file |
| Zero framework dependency | Only anthropic + python-dotenv |
| Real tool use, not a demo | Model autonomously decides when to call the calculator |
| Prove it works | Three test cases covering distinct execution paths |
And the non-goals — the "no"s that kept v1 shippable:
Takeaway: A learning product earns its value by what it leaves out. Every omission keeps the mental model small.
The brain is the model plus a system prompt. No magic: instantiate the client, pick a model, set the reasoning style.
self.client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
self.model = "claude-sonnet-4-6"
self.system_message = (
"You are a helpful assistant that breaks down "
"problems into steps and solves them systematically."
)
Memory is a Python list. Every turn appends to it, and the entire list is sent on every call — that's what gives the model continuity.
self.messages = [] # grows with every turn
self.messages.append({"role": "user", "content": message})
self.messages.append({"role": "assistant", "content": response.content})
What actually flows through messages[] over a tool-using exchange is the part most tutorials skip:
"What is 157.09 * 493.89?" — a plain string on the first turnresponse.content → a list of blocks: a TextBlock ("I'll calculate…") plus a ToolUseBlock with id, name, and input[][TextBlock("157.09 × 493.89 = 77,592.38")] — final answer, stop_reason: "end_turn"A tool is any object that can describe itself to the model and execute when called. Two methods do it: get_schema() tells Claude when and how to call it; execute() runs when it does.
class CalculatorTool:
def get_schema(self):
return {
"name": "calculator",
"description": "Performs mathematical calculations. Use for any arithmetic.",
"input_schema": {
"type": "object",
"properties": {
"expression": {"type": "string",
"description": "Math expression e.g. '157.09 * 493.89'"}
},
"required": ["expression"],
},
}
def execute(self, expression):
try:
return {"result": eval(expression)}
except Exception:
return {"error": "Invalid mathematical expression"}
The schema is the contract. Claude never sees the Python — it sees this JSON, and from it decides whether the user's request needs arithmetic and what expression to pass. (And yes, eval is a deliberate v1 shortcut — flagged as a non-goal to harden, not a thing to ship to production.)
The loop is the agent. It calls the model, inspects stop_reason, and branches: if the model wants a tool, run it and feed the result back; if the model is done, return the answer.
for i in range(1, max_turns + 1):
response = agent.chat(user_input)
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool = agent.tool_map[block.name]
result = tool.execute(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
user_input = tool_results # feed back, loop again
else: # stop_reason == "end_turn"
return final_text(response)
Visualised, the whole control flow is a single loop with one decision point:
🔧 "tool_use"
Extract tool name + input → run CalculatorTool.execute() → build a tool_result block → feed it back as the next user message → loop again ↺
✅ "end_turn" Extract the text from the response blocks → return the final answer string to the caller → done ✓
Takeaway: The "intelligence" of an agent isn't in any one block — it's in the loop that lets the model act, observe the result, and decide again. That feedback cycle is the whole game.
An MVP isn't done when it runs; it's done when it's demonstrably right across the paths that matter. Three test cases, each exercising a different route through the loop:
"I have 4 apples…" → Claude answers directly → 1 iteration, end_turn immediately.
"What is 157.09 * 493.89?" → Claude calls the calculator → 2 iterations: tool_use → end_turn.
A brother-age word problem → Claude chains reasoning, may call the tool mid-chain → 2–3 iterations.
Running python agent.py makes the loop observable in the terminal — exactly the legibility the product is for:
=== Test 2: Math (Tool Required) ===
🔄 Iteration 1
📥 Input: What is 157.09 * 493.89?
🤖 Agent: I'll calculate that for you.
🔧 Using tool: calculator | Input: {'expression': '157.09 * 493.89'}
✅ Tool result: {'result': 77592.3801}
🔄 Iteration 2
🤖 Agent: 157.09 × 493.89 = 77,592.38
Shipping a learning product means two artefacts, not one: the code that runs, and the explanation that makes it legible.
pip install -r requirements.txt, drop an API key in .env, then python agent.py. No build, no services.infographic.html visualises the four blocks, the loop, the message format, and the test paths — the same diagrams recreated in this article.Agent(tools=[CalculatorTool(), WebSearchTool()]). The loop doesn't change.The roadmap follows the non-goals — each one is a deliberate v2+ candidate, not an oversight:
eval with a sandboxed expression parser (the obvious first hardening).messages[] across runs to demonstrate long-term context.The hard part of building an AI agent isn't the code — it's resisting the urge to reach for abstraction before you understand the thing you're abstracting. Framed as a product, the goal was never "the most powerful agent." It was the most legible one: brain, memory, tools, loop — nothing hidden.
Master those four primitives on the raw SDK and every framework afterwards becomes a convenience you can evaluate on its merits, instead of a black box you have to trust. That clarity — not the hundred lines of Python — is the actual product.