Index Ai Academy
Phase 10 · Agent harnesses · Harness foundations

What an agent harness is

The software around a model that turns it into an agent.

About 30 minutes · Free lesson

Start this lesson

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

  1. Engine and car
  2. What a model does alone
  3. Three meanings of "harness"
  4. Build it, or use one
  5. Testing what you can't easily trigger
  6. A minimal harness with a scripted model
  7. More than a loop
  8. 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 partits joblessons
the loopcall the model, run its tools, feed results back, know when to stopcore loop
toolsthe actions on offer, their schemas and how they runregistry, files, execution
permissionswhich actions need a person's approvalpermissions
hooks and skillslet users and plugins change behavior; load know-how on demandhooks and skills
context managementkeep the conversation inside the window and cheapcontext
memory and planningcarry work across long tasks and new sessionsmemory and plans
subagentshand off focused sub-tasks to fresh contextssubagents
sessions and UIstream output, allow interrupts, save and resumesessions
observability and limitslogs, traces, costs, loop detectionproduction

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.

Worked example

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"])
It runs right here in your browser, with nothing to install.

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.py runs 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.

Quick check
A coding agent keeps forgetting what it did an hour ago in a long task. Is that the model's problem or the harness's?
  1. Neither; agents can't do long tasks
  2. Mostly the harness's: carrying work across a long task (memory, notes, context management) is its job
  3. Only the model's: it needs a bigger brain
  4. The operating system's
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:

approachyou writeyou get
a manual loopthe whole loop over stop_reasonfull control, no extra dependencies
the SDK's tool runner (client.beta.messages.tool_runner)your tool functionsthe loop for tools you define, with per-turn hooks; you host it
the Claude Agent SDKa prompt and optionsClaude 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 resultsAnthropic 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.

Worked example

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")
In the lesson you can change this and run it.

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.

Quick check
What does this print?
def fake_model(prompt):
    return "done"
def agent(model):
    return model("go")
print(agent(fake_model))
  1. done
  2. go
  3. fake_model
  4. None
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.

Quick check
You want Anthropic to run the agent loop and host a sandbox container for each session. Which option fits?
  1. The Claude Agent SDK
  2. The SDK's tool runner
  3. A hand-written loop
  4. Managed agents
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.

Worked example

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}))
In the lesson you can change this and run it.

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.

Try it now

Each reply is one model call. Two ask for tools, so the loop runs them and goes round again; the third ends the turn.

Quick check
Why build harnesses with the model passed in as a function?
  1. Because the API requires it
  2. It makes the model smarter
  3. So the model can call itself
  4. Tests can use a scripted model, so every part of the harness is fast, free and repeatable to test
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

Start this lesson

Try a quiz question

Which of these is the model's job rather than the harness's?

  1. Choosing which tool to call next, given the conversation
  2. Running the tool's code
  3. Asking the user to approve an action
  4. Keeping the conversation within the context window

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson