Harness Engineering · Module 1

The Anatomy of a Harness

Module 1 of 5

Jul 4, 2026 · ml · 24 min read · 4800 words intermediate

The anatomy of a harness.

ml agents harness-engineering claude-code module-1

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 inferenceA real agent
Lifetimeone request/responsea long-running session of many turns
Statestateless — forgets instantlyaccumulating message history + persistent memory
Actionsnone — it only emits textreads files, runs commands, calls APIs
Control flowyou drive every step by handthe model decides its own next step in a loop
Failureyou get a bad string, retry manuallythe loop catches errors and self-corrects
Ends whenthe response is returneda 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.

The model is a brain in a jar. A helpful mental image: the model is a disembodied reasoner with no hands, no eyes, no memory of yesterday. It can think brilliantly about what to do — but it can't do anything, and it wakes up with amnesia every request. The harness is the body: it gives the brain senses (tool results as observations), hands (tools that act), and a memory (persistent state). Harness engineering is building a good body for a very smart, very forgetful brain.

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.

The harness stack — every layer sits between you and the model UI / entry points — terminal, messaging, cron (what the user talks to) Orchestration — the agent loop, sub-agents, supervision Context engine — budget, compaction, memory, system prompt Tools + execution — read/write/edit/bash, permissions, sandbox Durability — checkpointing, replay, retries, sessions Model client — the API call (the ONLY part that is "the model") the model (weights) Everything above the red box is the harness. That's what this series builds.

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 toolsread, 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.

Same anatomy, different dials. pi minimizes the core and pushes everything to extensions. Hermes maximizes orchestration and durability. Claude Code ships a rich default. But all three have a model client, a tool layer, a context engine, a loop, and some durability story. Once you can name the layers, every harness — including ones that don't exist yet — becomes legible. That's the goal of this module.

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.

DisciplineThe question it answersOperates onExample artifact
Prompt engineeringWhat do I say to the model this turn?one messagean instruction / template
Context engineeringWhat information is in the window right now?one turn's contextretrieval + assembly logic
Harness engineeringWhat environment surrounds the model — tools, loop, memory, recovery?the whole running systemthe 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:

The minimal agent loop — four moving parts messages[](the state) call model(a turn) tool calls?execute + append no calls= stop loop back: results become new state for the next turn

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.
The stop condition is not "the model says done." A subtle but crucial point for the bare loop: you don't parse the model's prose for "I'm finished." You check a structural signal — did this turn contain tool calls or not? A turn with tool calls means "I want to act, then continue"; a turn with none means "I'm done acting, here's my answer." Structure, not sentiment. This is the seed of the whole termination discipline we'll harden in Module 4.

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.

You build: a bare harness — model client + message array + a loop that runs turns until the model emits no tool calls. Wire in one toy tool (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.

Try it, then break it. Once it runs: (1) add a second tool and watch the model chain them; (2) remove the 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_time with real read/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

← Loop engineering Module 2 — Tools & execution →
© cvam — written in plaintext, served warm