A language model can only do one thing: take text in, produce text out. Everything that makes it feel like an agent — editing your files, running commands, remembering across sessions, recovering from crashes — lives outside the model, in a layer called the harness. Agent = Model + Harness. This series builds that layer from scratch. Module 1 draws the map: why "just call the API" gives you a chatbot and not an agent, what every layer of Claude Code / pi / Hermes actually does, the precise borders between prompt, context, and harness engineering, and the agent loop from first principles. You finish by building a bare harness — a model client, a message array, and a hand-rolled loop that runs until the model stops asking for work.
There's a moment every builder hits: you call the chat API, get a great answer, and think "this is easy." Then you try to make it do something — edit a file, run a test, fix the failure, try again — and the whole thing falls apart. The model doesn't have your files. It can't run anything. It forgets everything the moment the response ends. You realize the intelligence was never the hard part. The hard part is the machinery around the intelligence that turns a paragraph of text into an action in the world, and back again, over and over.
That machinery is the harness, and building it well is harness engineering. As the saying in the field goes: "if you're not the model, you're the harness." Model labs make the intelligence; everyone else builds the layer that makes it useful. This series is a hands-on course in that layer. By the end you'll have assembled your own pi-style harness — loop, tools, context engine, recovery, orchestration — and understand every production harness by having built a small one.
Why "just call the API" fails
Start with the failure, because it defines everything that follows. A raw API call is transactional inference: one request, one response, no memory, no side effects. It's a pure function from text to text. That's genuinely useful for a lot of things — classification, drafting, extraction — but it is categorically not an agent. Compare the two:
| Transactional inference | A real agent | |
|---|---|---|
| Lifetime | one request/response | a long-running session of many turns |
| State | stateless — forgets instantly | accumulating message history + persistent memory |
| Actions | none — it only emits text | reads files, runs commands, calls APIs |
| Control flow | you drive every step by hand | the model decides its own next step in a loop |
| Failure | you get a bad string, retry manually | the loop catches errors and self-corrects |
| Ends when | the response is returned | a goal is met or a stop condition fires |
The gap between those columns is entirely harness. The model is the same in both cases — the same weights, the same API. What changes an inference endpoint into an agent is the code that: keeps a running conversation, exposes tools the model can invoke, executes those tools and feeds results back, loops until the work is done, and survives errors and restarts. "Just call the API" gives you the first column. This series builds the second.
Dissecting real harnesses, layer by layer
The fastest way to understand a harness is to take three real ones apart and see that they're built from the same parts. We'll reference these throughout the series: Claude Code (Anthropic's coding agent), pi (a deliberately minimal harness by Mario Zechner), and Hermes (Nous Research's open harness). They make very different choices, but every one of them is a stack of the same layers.
Fig 1 — The harness is a stack of layers wrapping a single API call. Different harnesses emphasize different layers, but the anatomy is shared.
pi — the minimalist
pi is the proof that a harness doesn't need to be enormous to be real. Its entire thesis: a coding agent needs exactly four tools — read, write, edit, bash — plus a couple of helpers (grep, find, ls), and a system prompt under 1,000 tokens. Everything else is an extension. pi deliberately ships without MCP, without sub-agents, without permission popups, and without a plan mode — not because those are bad, but because pi's philosophy is that the core should be tiny and you should add exactly what you need. Models and providers are configured in a models.json file; anything more exotic is a ~20-line TypeScript extension. pi is the whole series in miniature, and your capstone is built in its spirit.
Hermes — the orchestrator
Hermes (Nous Research, MIT-licensed) makes the opposite bet: it's an active orchestration layer, not a thin wrapper. Its distinguishing choice is treating sessions as infrastructure — session state lives in a SQLite database (with full-text search), so a CLI, a Telegram bot, and a scheduled cron job can all attach to the same session. It separates tool registration (the whole installed library) from tool exposure (what any single run shows the model), and compresses context by lineage — closing a session and spawning a child seeded with a summary rather than rewriting history. Hermes is what a harness looks like when the goal is durable, always-on, externally-steerable agents.
Claude Code — the batteries-included harness
Claude Code sits between the two: a full-featured harness with skills (progressive-disclosure capabilities), hooks (deterministic code that fires on agent events), MCP (a protocol for plugging in external tools), and typed sub-agents. It bakes in permission gates (it asks before destructive commands), context compaction, and durable sessions. It's the harness most people meet first, and dissecting it in Module 5 will make sense once you've built each layer yourself.
Prompt vs context vs harness engineering — precise boundaries
These three terms get used interchangeably and shouldn't be. They're nested layers, each a strict superset of the one inside it. Getting the boundaries precise is the difference between knowing which knob to turn when your agent misbehaves.
| Discipline | The question it answers | Operates on | Example artifact |
|---|---|---|---|
| Prompt engineering | What do I say to the model this turn? | one message | an instruction / template |
| Context engineering | What information is in the window right now? | one turn's context | retrieval + assembly logic |
| Harness engineering | What environment surrounds the model — tools, loop, memory, recovery? | the whole running system | the tool layer, loop, sandbox, checkpoints |
Read it as a containment hierarchy. Harness engineering builds the environment — the tools the model can call, the loop that drives it, the sandbox it runs in, the memory it persists to. Context engineering fills each turn's window — deciding what documents, tool results, and memory go into the prompt on this specific turn. Prompt engineering words the individual message. A great prompt inside a broken harness (no tools, no loop, no recovery) still produces a chatbot. A mediocre prompt inside a well-built harness often produces a working agent. That inversion — the outer layers dominating — is why the field's attention has moved outward, and why this series is about the outermost layer.
If you've read the loop engineering article, note the relationship: loop engineering is one slice of harness engineering — specifically the orchestration layer (how the model is invoked repeatedly and when it stops). Harness engineering is the whole stack: the loop plus the tools, context engine, sandbox, and durability that the loop depends on.
The agent loop from first principles
At the center of every harness is a loop. Strip away the features and it's astonishingly small — but each piece is load-bearing. Let's derive it from scratch.
A transactional call is: response = model(messages). To make it an agent, we need the model to be able to act, and we need to feed the result of that action back so it can decide what to do next. That single feedback requirement forces a loop:
Fig 2 — The loop: the message array is the state; each turn is one model call; tool results are appended and fed back; the loop stops when the model asks for no more work.
The four first-principles pieces:
- Messages — the state. The entire memory of the agent within a session is an ordered array of messages: the system prompt, the user's goal, the model's turns (which may contain tool calls), and the tool results. Each loop iteration appends to it. The array is the agent's working memory.
- Turns — the unit of progress. One turn is one model call over the current messages. The model reads the whole history and produces its next move: either a final answer, or a request to call one or more tools.
- Stop conditions — when to end. The simplest, most natural stop: the model returns a turn with no tool calls. That means it has nothing more it wants to do — it's answering, not acting. (Real harnesses add safety stops — iteration caps, budgets — which Module 4 covers. The natural stop is "the model stopped asking for work.")
- Streaming — the UX layer. Models emit tokens incrementally. A harness streams them to the terminal as they arrive so the user sees thinking and output live, and — importantly — so tool calls can be surfaced the instant they're complete rather than after the whole turn. Streaming is optional for correctness but essential for a usable agent.
You build — a bare harness
Time to make it real. The deliverable for Module 1 is the smallest thing that is genuinely an agent and not a chatbot: a model client, a message array, and a hand-rolled loop that runs until the model stops asking for work. No real tools yet (that's Module 2) — but the full loop skeleton, with one trivial tool to prove the cycle works.
get_time) so you can watch the model call it, see the result fed back, and then stop on its own. This is the spine every later module hangs features onto.import json, time
from anthropic import Anthropic # any model client works
client = Anthropic()
# 1. TOOLS — a schema (the contract) + an implementation
TOOLS = [{
"name": "get_time",
"description": "Return the current UTC time as a string.",
"input_schema": {"type": "object", "properties": {}},
}]
def run_tool(name, args):
if name == "get_time":
return time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())
return f"ERROR: unknown tool {name}"
# 2. THE LOOP — the whole harness, in ~15 lines
def run(goal, max_turns=10):
messages = [{"role": "user", "content": goal}] # the STATE
for turn in range(max_turns): # a safety cap (Module 4)
resp = client.messages.create( # a TURN
model="claude-sonnet-5", max_tokens=1024,
tools=TOOLS, messages=messages,
)
messages.append({"role": "assistant", "content": resp.content})
tool_calls = [b for b in resp.content if b.type == "tool_use"]
if not tool_calls: # STOP CONDITION
return resp # no tools = the model is done
results = [] # EXECUTE + feed back
for call in tool_calls:
out = run_tool(call.name, call.input)
results.append({
"type": "tool_result",
"tool_use_id": call.id, # must reference the call id
"content": str(out),
})
messages.append({"role": "user", "content": results})
raise RuntimeError("hit max_turns without finishing")
print(run("What time is it? Use your tool, then tell me in words.").content)
Read what just happened against the first-principles list. messages is the state. Each pass through the for loop is a turn. The model either asks for the get_time tool (we run it, append the result, loop) or answers with no tool call (we return — the natural stop). The max_turns cap is our only safety exit for now; Module 4 makes recovery serious. That's it. That is an agent — a real one, in fifteen lines — because it has the loop. Everything else in this series is making each layer of that loop production-grade.
max_turns cap and give it a task it can't finish — feel the loop run away (that's why caps exist); (3) print messages after each turn and watch the state grow. Feeling the loop's mechanics by hand is worth more than any diagram — you now know what every harness is doing underneath its features.What each module builds from here
You have the spine. The rest of the series thickens each layer:
- Module 2 — Tools & execution: replace
get_timewith realread/write/edit/bash, with permission gates and a sandbox so the agent edits real code safely. - Module 3 — Context engineering: keep the loop alive across 200 turns with compaction, memory files, and a system prompt treated as infrastructure.
- Module 4 — Durability & orchestration: checkpoint every turn, replay on crash, classify failures, and spawn sub-agents with approval gates.
- Module 5 — Production & capstone: dissect pi, Hermes, and Claude Code internals, learn to evaluate a harness, and assemble everything into your own pi-style harness.
FAQ
Isn't the harness just "glue code"? Why call it engineering?
Because the hard problems live there: how the loop terminates, how tools fail safely, how context survives long sessions, how the agent recovers from a crash mid-task. Those are real engineering problems with real design trade-offs — and they, not the prompt, decide whether your agent works. The model is a dependency you import; the harness is the system you build.
Do I need a huge framework to build a harness?
No — pi proves a real harness can be small (four tools, a sub-1k-token prompt, a tiny core). Frameworks (LangChain, the Agents SDK) give you batteries, but building a bare harness by hand first is the point of this series: you'll understand any framework because you'll know what it's abstracting.
Where does the model end and the harness begin, exactly?
The model is the single API call: text in, text (and structured tool-call requests) out. Everything else — deciding what text to send, executing the tool requests, looping, remembering, recovering — is harness. The model never touches your filesystem or runs a command; the harness does, on the model's instruction.
Is a chatbot a harness?
A minimal one — it keeps message history and streams output, which are harness features. But it has no tools, no loop that acts, and no durability, so it stays transactional in spirit. The moment you add "the model can call a tool and I feed the result back and let it continue," you've crossed from chatbot to agent harness.
Takeaways
Agent = Model + Harness. The model is intelligence; the harness is everything that makes it useful — and it's where the engineering is.- "Just call the API" is transactional inference: stateless, actionless, one-shot. An agent adds session state, tools, a loop, and recovery — all harness.
- pi (minimal core + extensions), Hermes (session-as-infrastructure orchestration), and Claude Code (batteries-included) share one anatomy: model client, tools, context engine, loop, durability.
- Prompt ⊂ context ⊂ harness engineering — nested layers; the outer ones increasingly dominate outcomes.
- The agent loop from first principles: messages are the state, a turn is one model call, the natural stop is "no tool calls," streaming is the UX.
- You built a real agent in ~15 lines — because it has the loop. The rest of the series makes each layer production-grade.
References & further reading
- LangChain — The Anatomy of an Agent Harness — Agent = Model + Harness, the five primitives.
- pi (pi.dev) — the minimal harness: four tools, sub-1k-token prompt, extensions.
- pi coding-agent README — the extension model and models.json in detail.
- Hermes Agent (Nous Research) — sessions as infrastructure, lineage compression.
- Arize — How Hermes implements an open agent-harness architecture — tool registration vs exposure, SQLite sessions.
- Firecrawl — What Is an Agent Harness? — the infrastructure that makes agents work.