AI Engineering Playbook
Agents

The Agent Harness

The runtime around the model — loop, tool dispatch, context, sandbox, permissions — and how to build one.

Prerequisites

  • Agent Foundations — the loop this page builds a runtime around, and why the transcript is the only state.
  • Tool Calling — schemas and tool_usetool_result mechanics; the harness is what actually dispatches them.
  • Tokens & Context Windows — the finite budget that forces compaction on every long-running agent.

The intuition

Picture a brilliant contractor with one strange set of limitations. They can reason about anything, but they have no hands, no memory between sentences, and no way to see the room — they can only read a note you hand them and write a note back. Worse, they will follow instructions in any note that reaches them, including one they find taped inside a cupboard.

Everything you build so they can actually renovate a house is the harness: the workbench of tools they can request, the assistant who fetches things and reads results back, the notebook that survives the shift change, the site foreman who refuses to let them knock out a load-bearing wall, and the fence around the property.

The model is the contractor. The harness is the entire job site. This section has used the word fifteen times already — caps enforced by the harness, gates at the tool boundary in the harness — without ever defining it. This page is that definition.

Key insight

Agent = model + harness. The model supplies judgement; the harness supplies everything else — memory, hands, eyes, and rails. When you evaluate "an agent," you are always evaluating a pairing, never a model on its own.

Why it exists

Agent foundations leaves you with a working while-loop. Everything between that loop and something you would put in front of a customer is harness, and each piece exists because of a specific property of the model.

  1. The model is stateless and blind. It sees exactly one thing: the request you assemble. Someone must decide, every single turn, which instructions, which tools, which files, and how much history go into that request. That assembly step is not plumbing — it is the highest-leverage code in the system.
  2. Tool calls are intents, not actions. The model emits {name, arguments}. Something has to validate it, execute it against the real world, catch the failure, and turn the result back into tokens. That something is code you own, and it is where every side effect in your product lives.
  3. Context is finite and degrades before it fills. Long runs blow past the window, and quality falls well before the hard limit. Someone must compact, summarise, and offload.
  4. Capability is indistinguishable from danger. A shell tool is remote code execution; a fetch tool is an untrusted-input channel. Enforcement has to sit in the code path, because a system prompt saying "always ask before deleting" is a suggestion to a probabilistic system.
  5. The harness is the only deterministic half. You cannot unit-test the model. You can version, diff, and test the harness — which makes it the only surface where engineering discipline applies.

The alternative is not "no harness." It is an implicit harness scattered through your application: truncation in one file, retries in another, permissions in a prompt. That is the actual default, and it is why agent codebases rot fast.

The core idea

An agent harness is the non-model software that turns a completion API into an agent: it assembles what the model sees, runs the loop, dispatches the model's tool calls against a real environment, manages the context budget, enforces what is allowed, and decides when to stop. Anthropic's definition is the most quotable: "the system that enables a model to act as an agent: it processes inputs, orchestrates tool calls, and returns results" — and, critically, "when we evaluate 'an agent,' we're evaluating the harness and the model working together."

The vocabulary is genuinely unsettled, and it is worth knowing that rather than being surprised by it. The broad sense — the one this page uses, and the one products use — is "everything that isn't the weights": prompts, tools, sandbox, permissions, hooks, session. The narrow sense reserves harness for the execution layer only (call the model, route its tool calls, decide when to stop) and calls the behaviour-shaping layer — system prompt, tool descriptions, what carries across steps — the scaffold. Anthropic's own hosted-agent architecture splits the concept three ways: a session (the append-only log of everything that happened), a harness (the loop that calls the model and routes its tool calls to infrastructure), and a sandbox (where code actually runs). Hugging Face's glossary exists precisely because practitioners could not agree; it says so outright. Pick a sense, state it, and move on — an interviewer cares that you know the layer exists, not which dialect you speak.

The load-bearing consequence: every harness component is a hypothesis about a model weakness. Anthropic states it directly — "every component in a harness encodes an assumption about what the model can't do on its own, and those assumptions are worth stress testing." Their own example: context resets added to manage Sonnet 4.5's tendency to wrap up early near the context limit became "dead weight" on Opus 4.5, where the behaviour was simply gone. Harness code has a shelf life that model releases enforce.

How it actually works

The minimum viable harness

The famous claim — a working agent is a few hundred lines — is true, and worth internalising before you reach for a framework. Thorsten Ball's canonical walkthrough builds a file-editing agent in under 400 lines of Go, "most of which is boilerplate," with three tools (read_file, list_files, edit_file). His summary of the whole architecture: "It's an LLM, a loop, and enough tokens."

messages = [system_and_instruction_files, user_task]

for turn in range(max_turns):                       # harness cap, not a model decision
    response = model.generate(messages, tools=tool_schemas)
    messages.append(assistant(response))

    if not response.tool_calls:                     # stop: model emitted text only
        return response.text

    results = []
    for call in response.tool_calls:
        try:
            out = dispatch[call.name](call.arguments)
            results.append(tool_result(call.id, truncate(out)))
        except Exception as e:
            results.append(tool_result(call.id, str(e), is_error=True))  # observation, not a crash

    messages.append(tool_results_message(results))   # Anthropic: a user-role message

raise BudgetExceeded                                 # the loop never ends on its own

Three details in that skeleton are the whole trick. The stop condition is the absence of tool calls, not a "done" tool. Tool failures re-enter as observations rather than exceptions, because a crashed host loop removes the model's ability to self-correct. And the transcript is client-owned and resent in full every turn — the server remembers nothing.

What separates the toy from a production harness

Everything the skeleton omits. This is the actual content of the discipline.

ComponentWhat it solvesHow it's implementedWhat it costs
Instruction assemblyThe model needs project conventions it can't inferInstruction files (CLAUDE.md, AGENTS.md) merged from global → project → directory, injected each turnEvery token is resent forever; stale files silently misdirect
Tool catalogueSelection accuracy collapses with overlapping toolsFew, workflow-shaped tools; namespaced (jira_search); strict schemas; enums over free stringsSchemas sit in every request; large catalogues cost real tokens
Result shapingOne cat of a big file can eat the windowPagination, filters, default caps, truncation with a note telling the model what was cutSilent truncation loses the answer and the model can't tell
Error surfacingOpaque failures waste turnsErrors returned as tool results with actionable text ("file not found — did you mean X?")Tokens per failure, but far cheaper than a dead loop
Context managementQuality degrades long before the window fillsCompaction (summarise, keep recent verbatim), tool-result clearing, offload to files and pass referencesSummarisation is lossy and irreversible; inline compaction adds latency
SandboxA shell tool is RCE by designcwd jail → container → gVisor → microVM → VM, plus network egress policyCold starts, I/O friction, harder local debugging
PermissionsPrompts cannot enforce anythingPer-tool risk tiers, allow-lists, plan-vs-act modes, gates at the tool boundaryOver-gating causes approval fatigue — a measured, serious failure
Sub-agentsDeep exploration would flood the parent's windowSpawn a child with its own window; return a distilled summary, not the transcriptMultiplies tokens; the parent loses the detail it didn't ask for
Session lifecycleCrashes, cancels, and long human waitsDurable transcript, cancel tokens that kill subprocesses cleanly, resume, forkPersistence on every turn; consistency rules for interrupted tools
Telemetry"It didn't find the obvious answer" is undebuggable without tracesSpan per turn and per tool call: model, stop reason, tokens, cache hits, latency, permission decisionsStorage, and a privacy decision about capturing content

Common misconception

"Agents are 300 lines, so frameworks are pointless." The loop is 300 lines. The sandbox, the context policy, the permission model, and the eval suite are the hard parts, and they are where all the production time goes. The correct reading of the 300-line result is that the loop is not where your differentiation lives — not that the rest is optional.

Context management, one level down

This is the component most people underestimate, because the failure is quiet. Treat the window as a finite attention budget with diminishing returns, not a container to fill. Chroma's Context Rot report measured 18 models — including GPT-4.1, Claude 4, Gemini 2.5, and Qwen3 — and found performance is non-uniform as input grows even on deliberately trivial tasks; on LongMemEval, every model did substantially better with a focused ~300-token prompt than with the ~113k-token version containing the same relevant facts. The older Lost in the Middle result is the same geometry: accuracy is highest when the answer sits at the start or end of the context and sags in the middle, sometimes below the closed-book baseline.

The three levers, cheapest first:

  • Tool-result clearing — drop the raw payloads of old tool calls, keep the fact that they happened. Nearly free, and usually the largest single win.
  • Compaction — when nearing the limit, have the model write a summary that preserves decisions, constraints, and unresolved problems, discard redundant tool output, and continue from the summary plus the most recently touched files.
  • Offload and reference — write large outputs to disk or a store and pass a path, so the model re-reads only what it needs. Sub-agents are the extreme version: a child may burn tens of thousands of tokens and hand back one or two thousand.

The flows

FlowSequenceWhen it appliesWhat breaks it
Plain turnAssemble → model → tool calls → dispatch → shape results → append → repeatThe default pathNo cap, so a confused run never terminates
Gated writeModel requests a tiered tool → harness pauses → human decides → dispatch or refuse-as-observationIrreversible or high-blast actionsGating reads too, which trains humans to click yes
CompactionBudget check trips → summarise older turns → reinitialise with summary + recentAny run past ~half the windowSummarising the whole history every turn; dropping recent verbatim turns
Sub-agentParent spawns child with its own window → child works → returns distilled findingsDeep search whose intermediate steps the parent doesn't needParallel children making conflicting writes — they can't see each other's decisions
Interrupt / resumeCancel token fires → kill subprocess → mark the tool result cancelled → persist → resume laterUser hits stop; process dies mid-runA transcript with a tool_use and no matching tool_result — most APIs reject it

A worked example

On the governed enterprise platform, an employee asks agent mode:

"Find every purchase order over £50k that's been pending more than 30 days, work out what's blocking each one, and open a facilities ticket for the badge-reader items."

Harness configuration (illustrative): tools search_orders, get_approval_chain, lookup_user, read_file, create_ticket (tier 2, gated); instruction file with the org's PO taxonomy; container sandbox with no outbound network except the internal API gateway; 200k window; compaction at 70% utilisation; 40-turn cap.

TurnWhat the harness doesContext after
1Assembles system + instruction file + 5 tool schemas + user message~2.4k
2–7Dispatches search_orders, pages results at 200 rows, returns 31 POs~14k
8–2231 × get_approval_chain, several in parallel per turn; each ~600 tokens~ 96k
23Budget check trips at 70%. Compaction: 31 chains → a table of PO / blocker / owner; raw chains discarded~21k
24Model requests create_tickettier 2. Harness pauses, surfaces the draft to an approver~22k
Approver returns 3 hours later; the session sleeps in durable storage, holding no compute
25Approved: dispatch with an idempotency key derived from the PO id~23k
26Final text: 31 POs, 4 tickets opened, 2 escalations recommended

The interesting turn is 23. Without compaction the run dies at turn 30 with a context-limit error and loses three minutes of work. With compaction the raw approval chains are gone — so if the user's next question is "what was the exact timestamp on PO-99102's L2 rejection?", the harness must re-fetch. That is the trade, and it is the right one: the summary preserved the answer to the question actually asked.

What omitted stages look like in production

  • No result shaping — one search_orders with no page limit returns 40k tokens of JSON; the window is half gone at turn 2.
  • Permission tiers by prompt instead of by code — the model opens 31 tickets on turn 24 because the instruction "confirm before creating tickets" lost a coin flip.
  • Compaction that drops recent turns — the model re-fetches the same approval chains it just summarised, and the run loops.
  • Non-idempotent create_ticket — the process dies after the API call but before the result is persisted; resume opens duplicates.
  • No is_error on failures — one 429 on turn 11 raises, the loop dies, and the user sees a blank failure instead of a retry.

Common drill-downs

The questions that separate someone who has read about agents from someone who has operated one.

Define an agent harness in one sentence. The non-model software that turns a completion API into an agent: it assembles context, runs the loop, dispatches tool calls against a real environment, manages the context budget, enforces permissions, and owns the stop condition. Anthropic's phrasing: the system that processes inputs, orchestrates tool calls, and returns results. Add the consequence — you never evaluate a model alone, you evaluate a model/harness pairing.

What actually ends a turn? The model returning text with no tool-call blocks. Everything else is a harness-enforced stop: turn cap, token or cost budget, wall-clock timeout, or no-progress detection on repeated identical calls. Never rely on the model to terminate itself; a confused loop is the default failure, not an edge case.

Why return tool errors to the model instead of raising? Because the model's main advantage in a loop is self-correction, and an exception that kills the loop removes it. A tool_result with is_error: true and actionable text ("no such column owner_id; available: owner, owner_email") lets the model fix its own call next turn. Reserve hard failures for things the model cannot act on: auth, budget exhaustion, or the same failure N times in a row.

Your tool returns 80k tokens. What do you do? Never pass it through. Cap it at a sensible default, paginate or filter at the tool boundary, and — this is the part people skip — tell the model what was cut and how to get more, so truncation becomes a navigable interface rather than silent data loss. For genuinely large artefacts, write to the filesystem or a store and return a reference the model can re-read selectively.

When do you compact, and what do you keep? Compact on a budget threshold rather than on the hard limit, because quality degrades before the window fills. Keep decisions, constraints, and unresolved problems; keep the most recent turns verbatim so the user's "as I just said" still works; discard redundant tool payloads first, since they are the bulk. Prefer clearing old tool results over summarising — it's cheaper and lossless about what happened, only lossy about detail.

What does a sub-agent actually buy you, and what does it cost? Context isolation: the child burns its window on a search whose intermediate steps the parent never needs, and returns a distillation. That's a real architectural win for breadth-first read work. The cost is token multiplication and a coordination problem — children can't see each other's implicit decisions, which is why parallel sub-agents doing writes produce work that doesn't merge.

Where do permissions have to live, and why not in the prompt? At the tool dispatch boundary, in code, before execution. A prompt is a probabilistic request; a permission check is an invariant. Score tools on reversibility × blast radius, auto-approve reads, gate irreversible external effects, and watch your rejection rate — a gate nobody ever rejects is fatigue theatre, not a control.

How do you tell whether a harness change was an improvement? Fix the model, vary the harness, and run many trials per task, because a single run tells you nothing under non-determinism. Report pass@k (at least one of k trials succeeds — right when retries are cheap) or pass^k (all k succeed — right when consistency is the product), and say which. They diverge violently: as k rises, pass@k climbs toward 100% while pass^k collapses. Grade on outcomes — tests green, ticket exists — not on whether the trajectory matched one you liked.

Isn't the harness just going to get absorbed into the model? Partly, and that's the interesting question rather than a settled one. See the next section.

Production concerns

  • Every component is a hypothesis with an expiry date. Anthropic's guidance is explicit: "every component in a harness encodes an assumption about what the model can't do on its own, and those assumptions are worth stress testing." Their context-reset machinery for Sonnet 4.5 became "dead weight" on Opus 4.5. Re-run your harness ablations on every model upgrade; deleting scaffolding is a normal outcome.
  • How much the harness matters is genuinely contested, and you should know both sides. For interface choices the effect is large and cleanly measured: Stencil held 16 models fixed and changed only the edit-tool format, and saw +15 points average pass rate, with Grok Code Fast 1 going from 6.7% to 68.3%. But METR's long-horizon evaluation found the opposite for product harnesses — Claude Code beat their plain ReAct scaffold in only 50.7% of bootstrap samples on Opus 4.5, and Codex beat Triframe in 14.5% on GPT-5, concluding that neither specialised harness outperformed the default scaffolds. Reconcile them like this: harness effects are large when the bottleneck is expression — how the model states an edit, reads a result, or navigates a tool — and small when the bottleneck is raw long-horizon capability and a competent loop already exists.
  • Approval fatigue is measured, not theoretical. Anthropic reports that Claude Code users approve 93% of permission prompts — at which point the prompt is a formality and the real control is whatever the user has already stopped reading. Their mitigation is a classifier, and even that lands at a 17% false-negative rate on genuinely overeager actions with a 0.4% false-positive rate on real traffic; better than skipping permissions entirely, explicitly not a substitute for review on high-stakes infrastructure. Gate narrowly so the gates that remain get read.
  • The blast radius is real and documented. Anthropic's own incident list from internal use includes deleting remote git branches from a misinterpreted instruction, uploading an engineer's GitHub auth token to an internal cluster, and attempting migrations against a production database. None of these were jailbreaks — they were overeager initiative. The lesson is that sandbox scope and tool tiers are load-bearing, not defence in depth.
  • Any harness with private data, untrusted content, and an outbound channel is exploitable — Simon Willison's "lethal trifecta." Tool results are an injection surface: fetched pages, issue comments, and file contents all arrive as tokens the model cannot reliably distinguish from your instructions. See security and agent reliability.
  • Caching changes the cost curve but not its shape. Put the stable prefix — system prompt, instruction files, tool schemas — before the cache breakpoint and it bills at a large discount. The growing transcript after the breakpoint still bills at full rate, so caching flattens the constant and compaction flattens the growth. You need both. See cost.
  • Instrument at the right granularity or you cannot debug at all. Session → turn → tool call, with model, stop reason, token counts, cache hits, latency, and permission decisions on each span. The OpenTelemetry GenAI semantic conventions give you a shared vocabulary here, though they are still evolving — instrument now, don't freeze dashboards on attribute names. See observability.

Test yourself

A teammate says: 'we don't need a harness, we're using the Agent SDK.' What's wrong with that sentence?

You change the file-edit tool from a diff format to a line-anchored format and pass rate jumps 15 points. What did you actually learn about your system?

Your agent's quality degrades over long sessions even though you never hit the context limit. What's happening and what do you change first?

Your harness gates every tool call for approval. Rejections run at under 1%. Is that a well-controlled system?

A model upgrade lands and your agent gets worse, not better. Where do you look first?

Go deeper

Where this connects

  • Harness vs orchestration — the layer above this one, and which concerns belong to which.
  • Tool calling — the schemas and wire format the dispatch layer implements.
  • Agent reliability — stop conditions, HITL gates, and sandboxing as reliability engineering rather than as components.
  • Multi-agent systems — what happens when sub-agent spawning becomes the architecture rather than a context-management trick.
  • Evals & testing — how to measure a harness change when the thing you changed and the thing you're measuring are both non-deterministic.
Agent Reliability

On this page