Ask someone what their agent does and they’ll describe the model. GPT-4o, Claude, whatever’s behind the endpoint. But the model isn’t the agent. Drop a model into your architecture and it does exactly one thing: turn tokens into more tokens. It cannot read a file, call your API, or notice it has been looping on the same question for forty minutes.
Everything that makes it an agent lives in the code wrapped around it. That code has a name, and once you have the name you start seeing it everywhere: the harness. LangChain’s Harrison Chase put it as a formula worth remembering: “if you’re not the model, you’re the harness.” Agent equals model plus harness.
Every coding assistant you’ve used is one. Every agent framework ships one, whether or not it calls it that. And most teams building agents write one by accident, badly, one while loop at a time. This post is the anatomy, and the argument that when you adopt a harness you are really adopting its defaults, so you had better know how to read them.
What’s covered#
- What a harness actually is, and why the loop is the agent
- The naive loop, and the four ways it fails in production
- The anatomy: a loop with a budget, a brake, and a witness
- Why “is it done?” is the hardest box in the diagram
- The part that matters most in practice: the defaults are the product
Where the word came from#
Worth saying up front, because the vocabulary is younger than it looks. “Harness” is a practitioner’s word, not an academic one. The research papers that defined this space, ReAct and the ones after it, don’t use it. Anthropic’s widely-read Building Effective Agents doesn’t either; it says “workflows” and “orchestrator.”
The term spread later, through coding tools and evals work, and it still competes with “runtime,” “agent loop,” and “scaffolding” for the same idea. So if the naming feels inconsistent across frameworks, that’s because it genuinely is. The concept underneath is stable; the label is still settling.
The definition to keep. Agent = an LLM with tools running in a loop to accomplish a goal. Harness = everything after the LLM: the tools, the loop, and the “is it done?” check. And notice what the harness is not. It isn’t the IDE or the chat window; those are interfaces. Claude Code, Codex, and Antigravity are three different front-ends onto the same kind of harness.
The loop is the agent#
Strip away the vocabulary and a harness is a loop. You send the model a task; it answers with either text or a request to call a tool; if it’s a tool call, you execute it and hand the result back; repeat until it stops asking.
or answer?"}:::gw EXEC["Execute tool"]:::gw DONE["Answer"]:::ok TASK --> MODEL MODEL --> WANT WANT -->|answer| DONE WANT -->|tool call| EXEC EXEC -->|result| MODEL
That is a working agent. It is about forty lines of code, and you should write it once by hand because it demystifies the whole field. It’s also the same shape whether the concrete pattern is ReAct, plan-then-execute, or generate-test-repair. Real harnesses layer several of these, but they’re all this loop wearing different clothes.
The forty-line version is also unshippable, and the reasons why are the reason harnesses exist.
Four ways the naive loop fails#
Run that loop against real work and it breaks in four predictable places.
- It never stops. The model asks for one more tool call, forever. This isn’t hypothetical: a static scan of 6,549 real agent repositories found 68 confirmed infinite-loop failures across 47 distinct projects. A confused agent with no cap bills you until someone notices.
- It does things you’d never have approved. The loop executes whatever the model asks for. Delete the file, post the payment, run the shell command. Nothing sits between intent and action.
- It forgets, then overflows. Long tasks accumulate history until they hit the context window, and then the run either errors or silently drops the early turns where the actual instructions were.
- It’s invisible. Something took nine minutes and £4. Which step? Which tool? You have a final answer and no idea how it got there.
Notice these aren’t model problems. A better model fails the same four ways. They’re runtime problems, and a harness is what you call the runtime once it solves them.
The anatomy: a budget, a brake, and a witness#
Every production harness I’ve looked at converges on the same three concerns, whatever the vocabulary.
- Budget: the ceilings. Maximum iterations, a context allowance, a spend limit. Answers how much may this cost me?
- Brake: the stops. Approval gates, guardrails, stop conditions. Answers what may it do without me?
- Witness: the record. Traces, spans, persisted history. Answers what did it actually do?
Here is the whole thing assembled. This is what a real harness runs on every turn:
draft todos · agree scope"]:::gw CTX["Assemble context
history · memory · todos"]:::gw BUDGET{"Over token
budget?"}:::gw COMPACT["Compact history"]:::gw MODEL["Model call"]:::core WANT{"Tool call
or answer?"}:::gw GATE{"Approval gate"}:::hero EXEC["Execute tool"]:::gw TRACK["Update todos"]:::gw PERSIST[("Persist history
every model call")]:::infra ITER{"Iteration
limit hit?"}:::gw REVIEW{"Goal met?
verify, don't assume"}:::sec end HUMAN["Human decides"]:::sec OTEL["Traces · spans"]:::infra DONE["Final answer"]:::ok TASK --> PLAN PLAN --> CTX CTX --> BUDGET BUDGET -->|yes| COMPACT COMPACT --> MODEL BUDGET -->|no| MODEL MODEL --> PERSIST MODEL --> WANT WANT -->|answer| REVIEW WANT -->|tool call| GATE GATE -->|needs a human| HUMAN GATE -->|policy allows| EXEC HUMAN -->|approved| EXEC HUMAN -->|rejected| MODEL EXEC --> TRACK TRACK --> ITER ITER -->|no| MODEL ITER -->|yes| REVIEW REVIEW -->|more work| CTX REVIEW -->|done| DONE HARNESS -.->|emits| OTEL
Two steps bookend the loop, and they’re the ones people leave out.
Planning comes first. The agent drafts a todo list, agrees the scope, and only then starts working. That plan isn’t a document it writes and forgets: it’s state the harness carries, re-injects into context every turn, and updates as work completes. It’s how a long task survives a hundred model calls without drifting off the brief.
Review closes it, and it’s the box coloured like a warning for a reason. The loop doesn’t end when the model stops asking for tools; it ends when something decides the work is done. That decision is the hardest box in the diagram, and it gets its own section below.
Between those bookends sit the three rings. The budget decides whether the turn happens at all. The brake decides whether the tool call happens. The witness records both, whatever they decided.
Budget: the ceilings#
Two ceilings matter, and they fail differently.
The iteration cap is the crude one, and you need it anyway. Somewhere in the loop a counter says stop after N model calls. Without it a single confused run spends real money, which is exactly what those 68 infinite loops are made of.
The context budget is subtler, and here’s the part most people get backwards. Long tool-calling loops accumulate history until they approach the window, and harnesses handle this with compaction: before the model call, if history exceeds a token allowance, summarise or evict the middle and keep the ends. It sits before the model call, on every turn. It isn’t a cleanup job; it’s part of the turn.
The flip side is the trap. Compaction is lossy by definition, so you are deciding what the agent forgets. Summarise away the constraint it was given in turn one and it will confidently do the wrong thing with a clean context window.
The durable fix is to keep persistent rules out of the summarisable history entirely. Anthropic’s harness re-injects standing instructions on every request rather than letting compaction eat them. The same discipline (externalise the plan and the rules to a file, don’t trust the window to hold them) is what separates a long-running harness that works from one that drifts.
Brake: what it may do without you#
This is the interesting one, and it’s where most of the engineering judgment lives.
The naive loop executes every tool call. A harness puts a gate in front and asks a question first: does this specific call need a human? The answer is a policy, and it’s yours to write.
a tool call"] STANDING{"Decided this
before?"}:::gw POLICY{"Auto-approval
policy matches?"}:::hero ASK["Ask the human"]:::sec RUN["Execute"]:::ok NO["Refuse · tell the model"]:::sec CALL --> STANDING STANDING -->|"yes, remembered"| RUN STANDING -->|no| POLICY POLICY -->|"safe by policy"| RUN POLICY -->|"no rule matches"| ASK ASK -->|approved| RUN ASK -->|rejected| NO
Three layers, each a different trade. Standing approvals remember prior decisions, so the human isn’t asked twice for the same thing. The auto-approval policy is a set of predicates you write: reads are fine, writes are not; this directory yes, that one no. Anything unmatched falls through to a human.
That middle layer is the whole ballgame. Set it to approve nothing and your “autonomous” agent is a very expensive way to click yes four hundred times, until approval fatigue sets in and someone starts rubber-stamping. Set it to approve everything and you’ve deleted the brake while keeping the word “gate” in your architecture diagram.
Autonomy isn’t a setting you switch on. It’s the quality of the predicates you write in that middle layer. Everything else in the harness is plumbing; this is the design work.
The useful default in almost every system I’ve seen: auto-approve reads, always ask on writes and shell, and scope file access to a working directory. It’s boring, it survives contact with production, and it makes the human prompts rare enough that people actually read them.
One warning that generalises. If your brake is a regex denylist of dangerous commands, it isn’t a brake. A blocklist stops the command you thought of; the approval gate and a real sandbox stop the one you didn’t. And keep the prompt-injection surface in view here, because a tool result is untrusted input: the text a tool returns can carry instructions, and your gate is what stands between “the model read something” and “the model acted on it.”
Witness: what it actually did#
An agent you can’t see is an agent you can’t operate. Two mechanisms do the work.
Tracing gives you a span tree per run: the agent invocation, each model call, each tool execution, nested and timed. When someone asks why a run took nine minutes, the answer is a shape you can look at rather than a log you have to reconstruct. This is a solved problem with a standard, OpenTelemetry’s GenAI conventions; bespoke telemetry is a migration you’ll pay for later.
History persistence is the one people skip. Persist the conversation after every model call, not at the end of the run. The diagram puts it immediately after the model call for a reason: a crash mid-run leaves you a resumable state instead of a mystery, and you can inspect a stuck agent while it’s still stuck.
Review: knowing it actually did the job#
This is the box people treat as trivial, and it’s the one that decides whether the agent shipped or just stopped. The loop ends when something decides the work is done. If that “something” is the model saying “done,” you have a problem, because the model is a poor judge of its own work. Left to self-assess, it marks a feature complete after editing the code without ever checking that the code runs.
The fix is to make completion a verification step, not a self-declaration. The model’s “done” is a claim; the harness checks the claim against the goal before it believes it.
the task is done"]:::core VERIFY["Check against the goal
run the tests · acceptance criteria · a judge model"]:::gw MET{"Goal actually
met?"}:::sec BACK["Back to work
with the gap named"]:::sec FIN["Finish"]:::ok CLAIM --> VERIFY VERIFY --> MET MET -->|no| BACK MET -->|yes| FIN
What “check against the goal” means depends on the task, and picking it is the design work. For code, it’s running the tests or the build, the one check the agent can’t argue with. For open-ended work, it’s a set of acceptance criteria the plan committed to up front, or a second model scoring the output against them.
Using a model to judge isn’t hopeless: a judge model with clear criteria agrees with human reviewers more often than the alternatives. It just isn’t free, and it is never the same model rubber-stamping itself.
The defaults are the product#
Here’s the part that matters most in practice, and the reason the anatomy is worth carrying in your head.
Every harness makes all of the choices above for you, as defaults, and the defaults are where two harnesses that look identical on paper turn out to be opposites. The clearest example sits between the two most-used SDKs.
- The Claude Agent SDK ships with no iteration cap and no cost cap. Both
max_turnsandmax_budget_usddefault to no limit. Out of the box the loop runs until the model decides it’s done, which the docs themselves flag as a runaway risk on open-ended prompts, with the advice to set a budget for production. - The OpenAI Agents SDK ships the opposite default: exceed
max_turnsand it raises a typedMaxTurnsExceeded. The cap is on unless you turn it off withmax_turns=None.
Same concept, opposite posture. One hands you an unbounded loop and trusts you to fence it; the other fences it and lets you remove the fence. Neither is wrong. But if you adopted one because a benchmark said it was good and never read this line, you took on a production risk you didn’t know about. Read the defaults, not the marketing page. The defaults are the product.
This is also the answer to the tempting objection that a harness is just a wrapper with a new name, that all the intelligence is in the model and the surrounding code is 10,000 lines of ceremony. The evidence doesn’t support it. In a controlled comparison that held the model and the task fixed and varied only the scaffold, the harness alone moved measured accuracy by as much as 28 points.
More striking, the effect didn’t shrink for stronger models. The most capable model tested gained the most from a good harness. The scaffold is load-bearing. It is the difference between the same model shipping and spinning.
None of which makes a framework the automatic answer. Teams have run a framework in production for a year and then torn it out for framework-free building blocks, and they’re not wrong to.
The honest position is narrower than either camp. The loop is trivial to write and the plumbing around it is not, so the question is never “framework or not” in the abstract. It’s whether this harness’s defaults and this harness’s escape hatches fit what you’re building, and that’s a question you can only answer once you can read the anatomy.
The bottom line#
- The model is not the agent. The harness is: the loop that calls tools, manages context, gates actions, and decides when to stop. That’s where your engineering lives.
- Every naive loop fails the same four ways: it never stops, it acts without asking, it overflows its context, and it’s invisible. A harness is just the name for the code that fixes those.
- A budget, a brake, and a witness is the whole anatomy, and “is it done?” is the hardest box in it. The model is a poor judge of its own work; make completion a step, not an assumption.
- Autonomy is engineered, not enabled. It’s the quality of the predicates in your auto-approval policy. Approve-everything is not autonomy; it’s an outage with good intentions.
- The defaults are the product. Two harnesses with the same anatomy can ship opposite postures. Read them before you adopt.
Where I’d start#
Write the forty-line loop by hand, once, against a real task. Give it two tools and no ceilings and watch it fail in at least two of the four ways above. Then add the iteration cap, the approval gate, and a trace, in that order, and notice that you’ve just rebuilt the harness every framework ships.
Do that and framework choice stops being a matter of taste. You’ll open a harness’s defaults table and know within a minute which of the three concerns it takes seriously, which it quietly leaves to you, and whether that gap is one you’re willing to fill yourself. That minute is the whole return on understanding the anatomy.
