Agent Engineering

Agent Harness Design: The Layer That Actually Determines Behaviour

The harness — not the prompt, not the model — decides what your agent does. The state contract, the tool contract, the permission contract, in 200 lines and no framework.

Article 2 of 713 minIntermediate
Agent Engineering
Key Takeaway

- Swapping the model changes your agent's behavior at the margins. Swapping the harness changes it completely — which means the harness, not the model, is where your engineering actually lives. - A harness is eight named surfaces, not a `while` loop: prompt assembly, tool schemas, tool dispatch, result normalization, state policy, permission tiers, verification sensors, and the continue-or-stop decision. Name them and you can review them. - A framework sells durable execution, checkpointing, and integrations. It charges you an opinionated state model and debugging through someone else's abstraction. Write the harness first; adopt the framework when the bill for durability exceeds the bill for the abstraction.

Same Model, Two Harnesses, Two Different Agents

Take one model. Give it a task: migrate seven call sites off a deprecated database helper and leave the test suite green.

Harness A exposes a bash tool, streams stdout straight into context, and stops when the model produces a turn with no tool call. Harness B exposes read, edit, and run_tests with typed inputs, truncates command output to the last 40 lines plus a note that it truncated, re-runs the test suite every turn, and stops when the tests pass and a grep for the deprecated symbol returns zero — or when the token budget is spent, whichever comes first.

Same weights, same prompt. Harness A will confidently tell you it's done while two call sites are untouched, because "no more tool calls" is a statement about the model's confidence, not about your codebase. Harness B cannot make that mistake: the thing that decides "done" is a grep, not a sentence.

Now swap the model in both. Behavior shifts a little — better at editing, better at reading its own errors. Nothing structural moves, and Harness A's failure mode survives the upgrade, because the failure mode was never in the model.

That's the claim this article defends: the harness is the highest-leverage code you will write in an agent system, and most teams don't think of it as code at all. They think of it as glue.

What is an agent harness? An agent harness is the software layer around a model: it assembles the prompt, exposes tool schemas, dispatches calls, normalizes results, enforces permissions, and decides whether to continue or stop. The model proposes actions. The harness decides which ones happen, in what order, and with what evidence.

Anatomy: The Eight Surfaces

Most "build an agent" content shows you a loop and calls it an architecture. A loop is one line of the harness. Here is the whole surface area, and what goes wrong when you leave each one implicit.

SurfaceThe decision it ownsWhat breaks when it's implicit
Prompt assemblyWhat text goes into every request, in what orderCache misses on every turn; instructions that drift as context grows
Tool schemasThe vocabulary the model can express intent inThe model narrates actions it can't take
Tool dispatchValidate → permission-check → execute → time outMalformed input executes; one hung call hangs the run
Result normalizationWhat a tool returns as the model will read itRaw stack traces eat the context budget; retry storms
State policyWhat survives a step, and in what shapeSilent state-handoff loss — the cost nobody prices
Permission tiersWhich actions run unattended, which need a humanBlast radius is discovered in production
Verification sensorsCheap deterministic facts, re-read every turn"Done" means the model said so
Continue/stopBudget, progress, goal, and the human-escalation pathRunaway loops and the bill that follows

Print that list. It is a review checklist — when someone hands you an agent PR, you can ask eight specific questions instead of "does it work on the demo task."

Three of those surfaces get their own articles in this pathway: state policy in context engineering for long-running agents, the stop decision in loop engineering, and the tool surface in tool and MCP design. This article is about the contracts that hold them together.

A Harness in About 200 Lines

No framework. TypeScript strict, @anthropic-ai/sdk, claude-sonnet-5. The runnable version lives in github-repo/agent-engineering/00-harness-minimal/.

Start with state, because state is the part you will regret getting wrong.

// harness/state.ts
import type Anthropic from "@anthropic-ai/sdk";

export type StopReason =
  | { kind: "done"; summary: string }
  | { kind: "budget"; spentTokens: number }
  | { kind: "no_progress"; turns: number }
  | { kind: "needs_human"; question: string };

export interface HarnessState {
  readonly goal: string;
  messages: Anthropic.MessageParam[];
  turn: number;
  stallTurns: number;
  spentTokens: number;
  /** Verification sensors: cheap, deterministic facts, re-read every turn. */
  signals: { testsPass: boolean; deprecatedCallSites: number };
  stop: StopReason | null;
}

signals is the part people leave out. It is the only thing in the state that the model cannot talk its way past.

Next, the tool contract. Note that permission tier and idempotency are declared on the tool, not decided at the call site.

// harness/tool.ts
import type Anthropic from "@anthropic-ai/sdk";
import type { HarnessState } from "./state.js";

export interface ToolResult {
  ok: boolean;
  /** Written for a model to read: what happened, and what to try next. */
  text: string;
}

export interface HarnessTool<I> {
  readonly schema: Anthropic.Tool; // name, description, input_schema
  readonly tier: "read" | "write" | "ask_human";
  /** Same input twice must produce the same effect, not two effects. */
  readonly idempotent: boolean;
  parse(raw: unknown): I; // reject before you execute
  run(input: I, state: HarnessState): Promise<ToolResult>;
}

export type AnyTool = HarnessTool<unknown>;

Then the loop. This is the least interesting part of the harness, which is exactly why it should not be the whole article.

// harness/loop.ts
import Anthropic from "@anthropic-ai/sdk";
import type { AnyTool } from "./tool.js";
import type { HarnessState } from "./state.js";

const client = new Anthropic();

export async function step(
  state: HarnessState,
  tools: Record<string, AnyTool>,
): Promise<void> {
  const res = await client.messages.create({
    model: "claude-sonnet-5",
    max_tokens: 16000,
    thinking: { type: "adaptive" },
    output_config: { effort: "high" },
    system: assemblePrompt(state), // stable prefix first, volatile last
    tools: Object.values(tools).map((t) => t.schema),
    messages: state.messages,
  });

  state.spentTokens += res.usage.input_tokens + res.usage.output_tokens;
  state.messages.push({ role: "assistant", content: res.content });

  const calls = res.content.filter(
    (b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
  );
  if (calls.length === 0) {
    state.stop = { kind: "done", summary: textOf(res.content) };
    return;
  }

  // All results go back in ONE user message. Splitting them teaches the
  // model to stop calling tools in parallel.
  const results = await Promise.all(calls.map((c) => dispatch(c, tools, state)));
  state.messages.push({ role: "user", content: results });
  state.turn += 1;
}

And the stop decision, which is where the harness earns its keep.

// harness/stop.ts
import type { HarnessState, StopReason } from "./state.js";

const TOKEN_BUDGET = 400_000;
const MAX_STALL_TURNS = 3;

export function decideStop(state: HarnessState): StopReason | null {
  if (state.stop) return state.stop;
  if (state.spentTokens > TOKEN_BUDGET)
    return { kind: "budget", spentTokens: state.spentTokens };
  if (state.signals.testsPass && state.signals.deprecatedCallSites === 0)
    return { kind: "done", summary: "verified by sensors, not self-report" };
  if (state.stallTurns >= MAX_STALL_TURNS)
    return { kind: "no_progress", turns: state.stallTurns };
  return null;
}

stallTurns increments whenever a turn ends with signals byte-identical to the previous turn. Three turns of no measurable movement is not persistence, it's a paid-for coin flip.

Read the order of those checks: explicit stop, then budget, then verified success, then stall. That sequence is a policy decision, and it is reviewable precisely because it is four lines in one function instead of scattered across a graph.

The Three Contracts

Everything above collapses into three contracts. Each one exists to prevent a specific failure, and each maps onto one of the four costs of non-determinism from multi-agent architecture as premature optimization — the cost frame this pathway extends rather than re-derives.

State contract — what survives a step

Write down, as a type, exactly what crosses a step boundary. Everything not in that type is lost, and losing it becomes a compile error rather than a 3am mystery.

The failure this prevents is state-handoff loss: step three needed a fact step two knew and didn't write down. In a single-process harness that shows up as an agent re-reading the same file four times. Across a graph it shows up as two components confidently disagreeing about reality. A typed state object with no escape hatches makes the handoff auditable — diff two turns and you see exactly what changed.

Tool contract — schemas, idempotency, and errors written for a model

Three requirements, and the third is the one nobody does.

Schema: typed, validated at the boundary, rejected before execution. A malformed call should cost one cheap error message, not a partially applied write.

Idempotency: the same input twice produces the same effect, not two effects. Non-determinism guarantees some calls get retried, so design for the retry — the same discipline you already apply to payment webhooks, which is the point: most of agent engineering is API design you already know.

Error messages written for a model to read. Highest return, lowest effort, in any harness. Compare:

{ "ok": false, "text": "Error: ENOENT: no such file or directory, open 'src/db/queryRaw.ts'" }

against:

{
  "ok": false,
  "text": "No file at src/db/queryRaw.ts. Nearest matches: src/db/query-raw.ts, src/db/query.ts. Re-run with one of those paths, or use glob to list src/db/ first."
}

The first invites a retry storm — the model tries variations of a path that doesn't exist until the budget dies. The second ends the failure in one turn. You are writing for a reader that will act immediately and literally. Write an instruction, not a diagnostic.

Permission contract — tiers, and "ask the human" as a tool

Declare a tier on every tool: read, write, or ask_human. Reads run unattended. Writes run unattended only inside a declared boundary — a workspace path, a branch, a schema. Anything outside it escalates.

The move that makes this tractable: make asking the human a tool like any other. Give the model an ask_human tool with a real schema, and escalation stops being an exception in your control flow and becomes an ordinary tool result. The loop already knows how to wait for a tool result. It does not need to learn how to wait for a person.

The failure this prevents is blast radius — the agent that force-pushed, dropped the table, or emailed the customer list. Governance for the merge-and-deploy end of that spectrum is its own problem, covered in letting agents merge safely. What belongs in the harness is narrower and non-negotiable: no tool executes without a declared tier, and no write executes outside a declared boundary.

Harness or Framework: One Decision Rule

Stated fairly, a framework sells you durable execution (a run survives process death and resumes mid-trajectory), checkpointing and replay, a visualizable topology, and integrations you'd otherwise hand-roll.

It charges two things. An opinionated state model you inherit whether it fits or not, and debugging through someone else's abstraction — and in a non-deterministic system, where the repro is a distribution rather than a case, one extra layer between you and the trace is expensive in a way it isn't for ordinary software.

That cost is not hypothetical. Thoughtworks moved LangGraph from Adopt to Trial on the April 2026 Technology Radar, specifically over its globally-shared-state design. Read that as the general lesson rather than a verdict on one library: the state model is the part of a framework you cannot refactor later, and it is the part that gets flagged.

So, the rule. Write your own harness until one of these is true:

  1. A run must survive process death and resume mid-trajectory. Durable execution is genuinely hard and genuinely worth buying.
  2. More than one team needs identical orchestration semantics. At that point you are maintaining a framework anyway, badly, without documentation.
  3. Your integration surface is wider than your orchestration logic. If 80% of the code is connectors, buy the connectors.

None of those true? The framework is selling you a debugging tax in exchange for a diagram. Where the topology itself starts earning its cost is the subject of graph engineering.

Why the Orchestrator Was Leverage

Most of the work I do is POCs and MVPs — proving a technical concept, clarifying a fuzzy requirement, getting a stakeholder to a yes, unlocking a budget line. The bottleneck was never the building. It was the cycle time between "someone has an idea" and "someone can touch a working version of it." Ideas waited on a full development loop just to earn the right to be evaluated.

So I built a custom agent orchestrator that runs the build through agents, step by step, following the process we already used — then made it a repeatable framework the team runs without me.

What changed was not a percentage. It was a kind. A POC stopped being a small staffed project and became a run of an encoded process; the knowledge of how we take an idea to something testable moved out of my head and into a harness other people invoke. That is the whole of B-06: leverage is a system you build, not work you do faster. Getting faster at hand-building POCs would have made me a bottleneck with better throughput. The orchestrator is worth more than any single MVP I could have built inside it, because it removed a bottleneck for other people.

The harness is the part of that system you own. The model is rented and will change under you; the framework, if you adopt one, is borrowed. The harness — prompt assembly, tool contracts, permission tiers, stop conditions — is the encoded judgment, and it is the only piece that compounds.

What BENCH-1 Will Measure

One task runs through every article in this pathway so costs stay comparable. BENCH-1: a 40-file TypeScript service calls a deprecated internal helper db.queryRaw() in seven places. Migrate every call site to the parameterized db.query() equivalent, introduce no new queryRaw call sites, leave the test suite green. Scored per run on input tokens, output tokens, wall-clock seconds, and pass/fail; reported over 20 runs as pass rate, median tokens, and p95 wall clock.

This article's comparison is the baseline — one single call with the repo pasted in — against the minimal harness above.

The single call will sometimes pass. Seven call sites is not a hard edit, and a large context window holds a small service. What it cannot do is know it passed: with no sensor, a run that misses two call sites and a run that gets all seven are indistinguishable from the output. Pass rate is the number to watch, and the harness's advantage should come almost entirely from the sensors and the stop condition rather than anything clever in the prompt. Token cost is where the harness should look worse on the happy path — it re-reads signals every turn and pays for them. The real question is whether it wins on cost per verified migration, the only unit that means anything.

ConfigurationPass rate (20 runs)Median tokensp95 wall clock
Baseline single call
Minimal harness (00-harness-minimal)
<!-- BENCH:TBD — fill from github-repo/agent-engineering/00-harness-minimal/results.md after running -->

Do This Monday

Open your agent code and find the eight surfaces from the table above. Some will be functions. Some will be a comment. At least one will be nothing at all — for most teams that's verification sensors, and the symptom is that "done" currently means the model stopped calling tools. Add one deterministic sensor and one stop condition that reads it. That single change converts your agent from something that reports success into something that demonstrates it.

Then continue through the pathway: what agent engineering actually is frames the harness/loop/graph split this article sits inside.

FAQ

What is an agent harness?

An agent harness is the software layer around a model that turns generated text into controlled action. It owns prompt assembly, tool schemas, tool dispatch, result normalization, state policy, permission tiers, verification sensors, and the continue-or-stop decision. The model proposes; the harness decides what actually executes, with what limits, and on what evidence.

Do I need a framework to build an agent?

No. A functional harness is a few hundred lines with an SDK and no framework. Adopt one when a specific need appears: runs that must survive process death and resume mid-trajectory, multiple teams needing identical orchestration semantics, or an integration surface wider than your orchestration logic. Before that, you are paying a debugging tax for a diagram.

How many tools should a harness expose?

Fewer than feels natural — start at three to five and add only when a task genuinely fails without one. Every schema consumes context on every turn and adds a wrong choice the model can make. Prefer one flexible tool with a typed schema over five overlapping ones, and promote an action to its own tool when you need to gate, audit, or parallelize it.

What belongs in the harness vs the prompt?

Anything you need to guarantee belongs in the harness; anything you want to influence belongs in the prompt. Budgets, permissions, boundaries, validation, and stop conditions are code — a prompt asking the model to respect a token limit is a request, not a control. Task framing, tone, output shape, and strategy hints belong in the prompt.

How do you test an agent harness?

Test the harness deterministically, separately from the model. Stub the model client and assert on harness behavior: malformed tool input is rejected before execution, results return in one message, budget and stall conditions fire, an out-of-boundary write escalates instead of running. Then run the non-deterministic half as a trajectory eval over repeated runs, scoring pass rate rather than single outcomes.