Chapter 1: The Zero Magic Manifesto
If you have tried to build an AI application in the last two years, you have likely felt Framework Fatigue.
You install a popular library. You import a ReasoningEngine. You call .run(). It works like magic for the “Hello World” example. But the moment you try to do something real—like editing a specific line in a Python file without deleting the imports—it breaks.
And because you used a framework, you can’t fix it. You are stuck digging through layers of abstract classes, factory patterns, and “Chains” trying to find the one prompt that is causing the hallucination.
We are not going to do that here.
This book is a rebellion against “Magic.” We are going to build a production-grade coding agent called Nanocode. We will build it in pure Python. We will not use LangChain, AutoGPT, or Pydantic.
Why? Because an autonomous agent is not magic. It is just a while loop.
What is an Agent, Really?
Strip away the venture capital marketing, and an “Agent” is just a thermostat.
A thermostat reads the temperature (input), compares it to the target (decision), and turns on the heater (action). Then it waits and repeats. That’s it. An AI agent does the same thing, just with text instead of temperature.

More specifically, an agent is composed of four parts:
- The Brain: The LLM (Claude, DeepSeek). A stateless function. You send text; it returns text.
- The Tools: Functions the Brain can “call” (Read File, Run Command).
- The Memory: The conversation history. A Python list.
- The Loop: A
while Truethat cycles through the above until the task is done.
Note: This list exists only in memory—it dies when the program ends. We’ll add persistent storage in Chapter 6.
If you can write a while loop, you can build an agent.
By building it from scratch, you will have something the framework users don’t: Control. When our agent gets stuck in a loop, you will know exactly which line of code caused it. When the API bill gets too high, you will see exactly where the tokens are leaking.
What We Are Building
Nanocode is a CLI tool that runs in your terminal. You talk to it like a colleague. It reads your files. It runs your commands. It edits your code.
By the end of this book, you will have built:
- The Brain: Connects to Claude Sonnet 4.5 (or DeepSeek, or a local model via Ollama).
- The Hands: Tools to read files, write files, and run shell commands.
- The Eyes: A search tool that finds code using
git grep. - The Safety Harness: A “Planner Mode” that prevents accidental
rm -rf /.
But first, we set up the workshop.
Project Setup
1. Initialize the Project
1 mkdir nanocode
2 cd nanocode
3 git init
2. Create a Virtual Environment
Never install AI tools globally. They conflict with system packages.
1 # Mac/Linux
2 python3 -m venv venv
3 source venv/bin/activate
4
5 # Windows
6 python -m venv venv
7 venv\Scripts\activate
3. Install Dependencies
We only need three libraries:
requests— To talk to the LLM APIs.python-dotenv— To load API keys from a.envfile.pytest— To test our code without making API calls.
Create requirements.txt:
1 requests
2 python-dotenv
3 pytest
Install:
1 pip install -r requirements.txt
4. Secure Your Keys
![]() |
Warning: If you push your API key to GitHub, bots will scrape it and drain your account within minutes. |
Create .gitignore:
1 .env
2 __pycache__/
3 venv/
4 .DS_Store
5 .nanocode/
The AgentStop Exception
Before we write the event loop, we need a clean way to exit. Instead of using break statements scattered throughout the code, we’ll define an exception that signals “the agent should stop.”
The Context: Exceptions are not just for errors; they are also a control flow mechanism. When the user types /q, we raise AgentStop. The main loop catches it and exits cleanly.
The Code:
1 # --- Exceptions ---
2
3 class AgentStop(Exception):
4 """Raised when the agent should stop processing."""
5 pass
This goes at the top of nanocode.py, right after the imports. It’s a marker exception—no logic, just a signal.
The Agent Class
Now the core abstraction: the Agent class. This encapsulates the agent’s state and behavior in a testable unit.
The Context: We could put all the logic in main(). But then we’d have to mock input() and print() to test it. By extracting the logic into Agent.handle_input(), we can test it directly.
The Code:
10 class Agent:
11 """A coding agent that processes user input."""
12
13 def __init__(self):
14 pass
15
16 def handle_input(self, user_input):
17 """Handle user input. Returns output string, raises AgentStop to quit."""
18 if user_input.strip() == "/q":
19 raise AgentStop()
20
21 if not user_input.strip():
22 return ""
23
24 return f"You said: {user_input}\n(Agent not yet connected)"
The Walkthrough:
- Lines 13-14: Empty constructor for now. We’ll add
brainandtoolsin later chapters. - Lines 18-19: Check for the
/qquit command. RaiseAgentStopinstead of returning a special value. - Lines 21-22: Skip empty input. Return empty string (no output to display).
- Line 24: Echo the input back. This is a placeholder—later, we’ll send this to the Brain.
Defining Success with Tests
Before we write the main loop, let’s define what success looks like. Tests serve as executable documentation.
Create test_nanocode.py:
1 import pytest
2 from nanocode import Agent, AgentStop
3
4
5 def test_handle_input_returns_string():
6 """Verify handle_input returns a string for normal input."""
7 agent = Agent()
8 result = agent.handle_input("hello")
9 assert isinstance(result, str)
10 assert "hello" in result
11
12
13 def test_empty_input_returns_empty_string():
14 """Verify empty/whitespace input returns empty string."""
15 agent = Agent()
16 assert agent.handle_input("") == ""
17 assert agent.handle_input(" ") == ""
18 assert agent.handle_input("\n") == ""
19
20
21 def test_quit_command_raises_agent_stop():
22 """Verify /q raises AgentStop exception."""
23 agent = Agent()
24 with pytest.raises(AgentStop):
25 agent.handle_input("/q")
26
27
28 def test_quit_command_with_whitespace():
29 """Verify /q works with surrounding whitespace."""
30 agent = Agent()
31 with pytest.raises(AgentStop):
32 agent.handle_input(" /q ")
Run the tests:
1 pytest test_nanocode.py -v
1 test_nanocode.py::test_handle_input_returns_string PASSED
2 test_nanocode.py::test_empty_input_returns_empty_string PASSED
3 test_nanocode.py::test_quit_command_raises_agent_stop PASSED
4 test_nanocode.py::test_quit_command_with_whitespace PASSED
All green. Our agent handles the basic cases correctly.
![]() |
Aside: Why pytest? It discovers functions starting with |
The Main Loop
Now the thin I/O wrapper that connects the agent to the terminal:
29 def main():
30 agent = Agent()
31 print("⚡ Nanocode v0.1 initialized.")
32 print("Type '/q' to quit.")
33
34 while True:
35 try:
36 user_input = input("\n❯ ")
37 output = agent.handle_input(user_input)
38 if output:
39 print(output)
40
41 except (AgentStop, KeyboardInterrupt):
42 print("\nExiting...")
43 break
44
45
46 if __name__ == "__main__":
47 main()
The Walkthrough:
- Lines 30-32: Create the agent and print startup messages.
- Line 36:
input()blocks and waits for the user to type something. - Lines 37-39: Call
handle_input()and print any output. - Lines 41-43: Catch
AgentStop(from/q) orKeyboardInterrupt(from Ctrl+C) and exit cleanly.
![]() |
Aside: Python’s |
Notice the separation: Agent.handle_input() contains all the logic. main() is just I/O glue. This makes the agent testable without mocking stdin/stdout.
Run It
1 python nanocode.py
You should see:
1 ⚡ Nanocode v0.1 initialized.
2 Type '/q' to quit.
3
4 ❯ hello
5 You said: hello
6 (Agent not yet connected)
7
8 ❯ /q
9
10 Exiting...
This is the chassis. The engine comes next.
Wrapping Up
In this chapter, you built the foundation of an AI agent: an Agent class with a handle_input() method, controlled by a simple event loop. More importantly, you embraced the Zero Magic philosophy—no frameworks, no abstractions, just raw Python you can understand and control.
You learned that an agent is composed of four parts: the Brain (LLM), the Tools (functions), the Memory (conversation history), and the Loop (the while True that ties it all together). If you can write a while loop, you can build an agent.
You also wrote tests before verifying the behavior manually. This test-first approach will pay dividends as the codebase grows—each chapter builds on the last, and tests ensure we don’t break what already works.
In the next chapter, we’ll wake up the brain by connecting to the Claude API using nothing but the requests library. No SDK magic—just raw HTTP.

