Mayank Agarwal · Products

Simple AI Agent

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.

💡
Phase 1 · Ideation
Name the real problem

"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.

🔍
Phase 2 · Discovery
Why the alternatives fail

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.

🎯
Phase 3 · Define MVP
Four primitives, three tests

Scope to brain, memory, tools, and a loop — plus three test cases that prove each path. Everything else is a non-goal.

🔨
Phase 4 · Build
Raw SDK, from scratch

Implement each primitive directly on the Anthropic Python SDK so nothing is hidden — the model, the message list, the tool schema, the loop.

Phase 5 · Validate
Prove every path

Three test cases exercise the no-tool path, the single-tool path, and multi-step reasoning — so "it works" is demonstrable, not asserted.

🚀
Phase 6 · Ship & Teach
Runnable + a visual explainer

Ship to GitHub with a README, a one-command run, and an interactive infographic so the architecture is legible at a glance.


The Problem: Frameworks Hide the Fundamentals

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.

ApproachGreat atFails the learner because…
LangChain / CrewAIComposing complex, multi-agent systems fastThe agent loop, memory, and tool dispatch are all abstracted away — you can't see the mechanics
Blog tutorialsA quick copy-paste winUsually a single prompt call — no real loop, no tool use, nothing to generalise from
Reading the docsReference accuracyShows the API surface, not how the pieces compose into an agent
Raw SDK, from scratchSeeing every moving partNothing — 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.

Who It's For

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 Insight: An Agent Is Just Four Primitives

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.

🧠
Block 1

Brain

Claude as the LLM, with a system prompt that sets personality and reasoning style.

💾
Block 2

Memory

A growing messages list, sent in full on every call — context across turns.

🔧
Block 3

Tools

A CalculatorTool exposing a JSON schema the model reads to decide when to call it.

🔄
Block 4

Agent Loop

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.

Defining the MVP

I scoped v1 against measurable goals, so "done" wasn't a feeling.

GoalSuccess looks like
Teach the four primitivesEach block is a named, readable unit of code
Fit in one mental modelThe whole agent is ~100 lines, one file
Zero framework dependencyOnly anthropic + python-dotenv
Real tool use, not a demoModel autonomously decides when to call the calculator
Prove it worksThree 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 Build

Block 1 — The Brain

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."
)

Block 2 — Memory

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:

User
"What is 157.09 * 493.89?" — a plain string on the first turn
Asst
response.content → a list of blocks: a TextBlock ("I'll calculate…") plus a ToolUseBlock with id, name, and input
User
The tool result, fed back as the next user turn: []
Asst
[TextBlock("157.09 × 493.89 = 77,592.38")] — final answer, stop_reason: "end_turn"

Block 3 — Tools

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.)

Block 4 — The Agent Loop

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:

👤 User Inputstring or tool results
agent.chat()appends to messages[]
messages[]full history
Anthropic APIclaude-sonnet-4-6
stop_reason? — the 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 ✓

↺ Max 10 iterations by default — a safety ceiling that prevents runaway loops

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.

Proving It Works

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:

💬 Test 1 — No Tool

"I have 4 apples…" → Claude answers directly → 1 iteration, end_turn immediately.

🔧 Test 2 — Tool Required

"What is 157.09 * 493.89?" → Claude calls the calculator → 2 iterations: tool_useend_turn.

🧩 Test 3 — Multi-Step

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

Ship & Teach

Shipping a learning product means two artefacts, not one: the code that runs, and the explanation that makes it legible.

What I'd Build Next

The roadmap follows the non-goals — each one is a deliberate v2+ candidate, not an oversight:

The takeaway

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.

← Back to all products