What an agent harness is
The software around a model that turns it into an agent.
Why it matters
A model on its own only turns text into text. Everything that makes it an agent, the loop, the tools, the permissions, the memory, is software around it called a harness. Two products using the same model can differ enormously because of their harnesses. Claude Code is a model plus a very good harness. This phase teaches you to build one: to customize agents, debug them, and judge the ones you're offered.
What you'll be able to do
- Tell the model apart from the harness, and list what a harness is responsible for
- Recognize the three meanings of "harness": agent, test and evaluation harnesses
- Choose between a hand-built loop, the SDK's tool runner, the Claude Agent SDK and managed agents
- Build a minimal harness with a scripted model, pluggable tools and an event log
What it covers
- Engine and car
- What a model does alone
- Three meanings of "harness"
- Build it, or use one
- Testing what you can't easily trigger
- A minimal harness with a scripted model
- More than a loop
- Test without a model
Engine and car
Think of the model as an engine. It's the source of power, but an engine on a workbench goes nowhere. The harness is the rest of the car: wheels, brakes, fuel system, dashboard. A great engine in a bad car is a bad car.
A model call does exactly one thing: it reads a request (system prompt, messages, tool definitions) and returns a response (text, maybe tool_use blocks, and a stop_reason). Everything else is the harness:
| harness part | its job | lessons |
|---|---|---|
| the loop | call the model, run its tools, feed results back, know when to stop | core loop |
| tools | the actions on offer, their schemas and how they run | registry, files, execution |
| permissions | which actions need a person's approval | permissions |
| hooks and skills | let users and plugins change behavior; load know-how on demand | hooks and skills |
| context management | keep the conversation inside the window and cheap | context |
| memory and planning | carry work across long tasks and new sessions | memory and plans |
| subagents | hand off focused sub-tasks to fresh contexts | subagents |
| sessions and UI | stream output, allow interrupts, save and resume | sessions |
| observability and limits | logs, traces, costs, loop detection | production |
In plain words: the model only ever writes a reply. The harness is everything that acts on that reply.
You built the smallest possible harness in Phase 1's agent loop lesson, and a host in Phase 9. This phase builds the full set.
What a model does alone
A model only returns text and requests. Without a harness, a request to read a file is nothing but data. Here's the same scripted reply with and without one.
def model(messages): if len(messages) == 1: return {"stop_reason": "tool_use", "tool": "read_file", "input": "notes.txt"} return {"stop_reason": "end_turn", "text": "The notes say: ship on Friday."} FILES = {"notes.txt": "ship on Friday"} # No harness: one call, and the request goes nowhere. print("alone:", model(["What do my notes say?"])) # A harness runs the tool and goes round again. messages = ["What do my notes say?"] while True: reply = model(messages) if reply["stop_reason"] == "end_turn": break messages.append(FILES[reply["input"]]) print("with a harness:", reply["text"])
The engine is the same in both. All the doing, like reading files, keeping the conversation and deciding when it's over, comes from the code around it.
Three meanings of "harness"
A horse's harness and a climber's harness are both gear around something, keeping it under control. The word turns up in software in three related senses:
- A test harness runs code under controlled conditions and checks the results. This academy has one:
harness.pyruns your exercises and their checks. - An evaluation harness runs a model or agent across a benchmark of tasks and scores it, such as EleutherAI's
lm-evaluation-harness. The eval harness lesson builds one for agents. - An agent harness is the subject of this phase: the runtime that turns a model into an agent.
They share an idea: a harness is the controlled environment around the thing being run.
Show the answer
Mostly the harness's: carrying work across a long task (memory, notes, context management) is its job
A model only sees what the harness puts in its context. Keeping notes, summaries and plans there is harness work.
Build it, or use one
You can cook dinner from scratch, use a meal kit, heat up a ready meal or go to a restaurant. There are four common ways to get an agent running on Claude, depending on who writes the loop and who runs the machines:
| approach | you write | you get |
|---|---|---|
| a manual loop | the whole loop over stop_reason | full control, no extra dependencies |
the SDK's tool runner (client.beta.messages.tool_runner) | your tool functions | the loop for tools you define, with per-turn hooks; you host it |
| the Claude Agent SDK | a prompt and options | Claude Code's harness as a library: built-in file, bash, grep and web tools, permissions, hooks, subagents, sessions; you host it |
| managed agents (a hosted API) | an agent config and your custom tool results | Anthropic runs the loop and a sandbox container per session |
The tool runner and the Agent SDK are easy to mix up. The tool runner is a thin helper in the normal API SDK that loops over your tools. The Agent SDK is a separate package with a full, opinionated harness and built-in tools.
So why build one yourself? Because every one of these is made of the parts in this phase. To configure them well, debug them when they misbehave, add what's missing, or build a harness for a domain no product covers, you need to know what's inside.
A design rule: inject the model
Pilots train in flight simulators, because they can practice an engine failure there at no risk or cost. Make the model a parameter of the harness: a function that takes messages and tools and returns a response. Production passes a function that calls the API. Tests pass a scripted model that returns prepared responses in order. Then every part of the harness can be tested quickly, cheaply and deterministically, which is how the rest of this phase works.
Testing what you can't easily trigger
How do you test what your harness does when the model asks for a file that doesn't exist? With a real model, you'd have to coax it. With a scripted one, you write the reply yourself.
def run(model, tools): messages = [{"role": "user", "content": "go"}] while True: reply = model(messages) if reply["stop_reason"] == "end_turn": return messages try: result = {"content": tools[reply["tool"]](reply["input"])} except FileNotFoundError as err: result = {"content": str(err), "is_error": True} messages.append({"role": "user", "content": result}) script = iter([{"stop_reason": "tool_use", "tool": "read", "input": "missing.txt"}, {"stop_reason": "end_turn"}]) def scripted(messages): return next(script) def read(path): raise FileNotFoundError(f"No file {path}") last = run(scripted, {"read": read})[-1]["content"] print(last) assert last["is_error"] is True print("test passed, with no API call")
That test costs nothing, takes milliseconds and gives the same result every time. It's why this phase passes the model in as an argument.
def fake_model(prompt): return "done" def agent(model): return model("go") print(agent(fake_model))
Show the answer
done
agent calls whatever model it's given, and fake_model returns "done", so it prints done. Pass the model in as an argument and your tests can use a scripted fake.
Show the answer
Managed agents
The tool runner and the Agent SDK run on your own machines. Managed agents run the loop and the sandbox for you.
A minimal harness with a scripted model
The model is a list of prepared replies. The harness runs tools and records an event for everything that happens.
def scripted(replies): replies = list(replies) def model(messages, tools): return replies.pop(0) return model def run(task, model, tools): messages = [{"role": "user", "content": task}] events = [] while True: reply = model(messages, list(tools)) messages.append({"role": "assistant", "content": reply["content"]}) events.append(("model", reply["stop_reason"])) if reply["stop_reason"] != "tool_use": return "".join(b["text"] for b in reply["content"] if b["type"] == "text"), events results = [] for block in reply["content"]: if block["type"] == "tool_use": output = tools[block["name"]](**block["input"]) events.append(("tool", block["name"])) results.append({"type": "tool_result", "tool_use_id": block["id"], "content": str(output)}) messages.append({"role": "user", "content": results}) model = scripted([ {"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "t1", "name": "add", "input": {"a": 2, "b": 3}}]}, {"stop_reason": "end_turn", "content": [{"type": "text", "text": "2 + 3 = 5"}]}, ]) print(run("What is 2 + 3?", model, {"add": lambda a, b: a + b}))
Two model calls, one tool run, and an answer. Nothing here talks to a network. Swap scripted(...) for a function that POSTs to the Messages API and the same harness runs a real model. The event list is the seed of the tracing you'll add later.
Each reply is one model call. Two ask for tools, so the loop runs them and goes round again; the third ends the turn.
Show the answer
Tests can use a scripted model, so every part of the harness is fast, free and repeatable to test
It's dependency injection again. A scripted model makes behavior deterministic, so tests can check exact sequences.
More than a loop
The same engine feels very different in a truck and a race car. An agent's quality isn't only the model. The same model behaves very differently depending on its tools, its context and its checks. And a harness is more than its while loop, which is the easy part.
Test without a model
Most harness bugs can be found with no API call at all.
Practice what you just read
- 3 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
Which of these is the model's job rather than the harness's?