In February 2026, LangChain moved its coding agent from outside the top 30 to the top 5 on Terminal Bench 2.0. The score went from 52.8% to 66.5%, and nobody touched the model: it was gpt-5.2-codex before and after (Trivedy, 2026a). Most of the changes were middleware, the extension surface that every user of the framework also has.
Sort those changes and they fall into two piles. A verification pass before exit and a map of the working directory at startup are a sensor and a piece of context, and they help any model. Loop detection, time-budget warnings and a split reasoning budget compensate for how this particular model behaves, and that kind of scaffolding is what harness authors delete when the next model arrives.
Nearly fourteen points sat in that harness, and only some of them belong to you: the line runs between the inner agent harness you rent from a vendor and the outer harness you build.
The thesis: the inner harness is commoditizing, so your leverage is the outer one
Every agent product now ships a competent loop. Claude Code, Codex, the Claude Agent SDK, the OpenAI Agents SDK and LangGraph's prebuilt agents all call the model, run tools, manage the context window, ask for permission, and save sessions. That loop is converging across vendors, and its model-compensating parts churn with every model release. What it guarantees, and over what unit, is decided by whoever ships it.
So the consensus belief I want to argue with is this one: pick the best model and the best agent runtime, and reliability follows. It does not follow. You rent the runtime, and the reliability you can actually defend lives in what you build around it: the instructions, the checks, the state you own, and the tools you expose. Every guarantee you leave inside the rented layer is one that somebody else defines, and can redefine.
A second, narrower belief needs challenging along the way: that the outer harness is the instruction file. On the best evidence available, instruction files are its weakest part.
grid-rows: 1
grid-columns: 2
grid-gap: 40
belief: "The common belief" {
label.near: top-center
style: {fill: "#FDECEA"; stroke: "#E74C3C"; font-color: "#2C2C2A"}
grid-rows: 3
grid-columns: 1
grid-gap: 12
b1: "Pick the best model" {style: {fill: "#95A5A6"; stroke: "#6C7A7B"; font-color: "#2C2C2A"}}
b2: "Pick the best runtime" {style: {fill: "#95A5A6"; stroke: "#6C7A7B"; font-color: "#2C2C2A"}}
b3: "Write a long AGENTS.md" {style: {fill: "#E74C3C"; stroke: "#A93226"; font-color: "#FFFFFF"}}
}
thesis: "This article's claim" {
label.near: top-center
style: {fill: "#E6F7EA"; stroke: "#3F9E52"; font-color: "#2C2C2A"}
grid-rows: 3
grid-columns: 1
grid-gap: 12
t1: "Rent the model and runtime" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
t2: "Own the sensors and tools" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
t3: "Own every guarantee\nthat touches money" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
}
Most teams spend their effort in the left column. The durable part of the LangChain gain is in the right one, and so is the fix for each failure in the case study later in this article.
What is an agent harness? Inner vs outer in one definition
Birgitta Böckeler's definition is the one I use: the harness is "everything in an AI agent except the model itself" (Böckeler, 2026b), a phrasing she credits to LangChain's "Agent = Model + Harness" (Trivedy, 2026b). That covers the loop that keeps calling the model, the tools it can call, the context it sees, the permissions that limit it, and whatever it remembers between runs.
In early 2026 the term spread fast. Mitchell Hashimoto described the habit of engineering a fix each time an agent makes a mistake, wrote that he did not know of "a broad industry-accepted term for this yet", and said he had grown to calling it harness engineering (Hashimoto, 2026). Ryan Lopopolo's OpenAI post six days later put the phrase in a title (Lopopolo, 2026), and Böckeler's first memo on it guessed that the term was inspired by Hashimoto's post (Böckeler, 2026a). Her later article gave it structure, including a figure of concentric rings: the model at the center, the coding agent's builder harness around it, and the user harness outermost.
I use that layering with one change of name. Böckeler calls the outer ring the user harness or "outer harness", but she never writes "inner harness" for the middle one. That phrase comes from later community writing, such as the Nimbus explainer in the table below, and I use it only because it pairs cleanly with her "outer".
direction: down
outer: "Outer harness - yours" {
label.near: top-center
style: {fill: "#E6F7EA"; stroke: "#3F9E52"; font-color: "#2C2C2A"; font-size: 18}
inner: "Inner harness - the vendor's runtime" {
label.near: top-center
style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"; font-size: 16}
model: "Model\n(weights behind an API)" {
style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}
}
loop: "Loop, tool execution,\ncontext, permissions, sessions" {
style: {fill: "#98D8C8"; stroke: "#5FA898"; font-color: "#2C2C2A"}
}
loop -> model: "calls"
}
guides: "Guides: instructions,\nconventions, skills" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
sensors: "Sensors: tests, linters,\nevals, reviewers, CI" {style: {fill: "#FFA07A"; stroke: "#D9785A"; font-color: "#2C2C2A"}}
owned: "Owned state, memory,\ncustom tools, MCP servers" {style: {fill: "#955D37"; stroke: "#6E4426"; font-color: "#FFFFFF"}}
guides -> inner: "steer before"
inner -> sensors: "check after"
owned -> inner: "exposed as tools and hooks"
}
Working from the inside out: you cannot change the model at all, and you can configure the inner harness but not redesign it. Everything in the green ring is code and text that your team writes, versions, and owns, and it touches the inner harness only through the tools and hooks the vendor chose to expose.
The inner harness: the runtime you rent
By inner harness I mean the vendor's or the SDK's agent runtime. OpenAI's description of the Codex loop is a fair summary of all of them: call the model, run any tool calls it emits, append the results, and repeat until the model stops asking for tools, while keeping the context window under control (Bolin, 2026).
Every serious runtime now ships that loop as five components.
Orchestration loop. It decides when to call the model again and when to stop. In LangGraph it is the graph that LangChain's create_agent compiles, with a model node, a tool node, and the edges between them.
Tool execution. It parses tool calls, dispatches them, runs them in parallel or in sequence, and feeds the results back. It also decides what happens when a tool throws.
Context handling. Compaction, truncation, summarization of old turns, and the policy for what the model sees on the next call. This is the component that changes most between releases.
Permissions. Which tools need approval, which paths are writable, which commands are denied, and what a human sees when the agent pauses.
Sessions and persistence. Saving a run so it can resume after a crash, a deploy, or a human approval. In LangGraph this is the checkpointer; in Claude Code it is the session store.
direction: down
grid-rows: 2
grid-columns: 1
components: "Inner harness components" {
label.near: top-center
grid-rows: 1
grid-columns: 5
grid-gap: 16
style: {fill: "#EAF2FB"; stroke: "#4A90E2"; font-color: "#2C2C2A"}
loop: "Orchestration\nloop" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
tools: "Tool\nexecution" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
ctx: "Context\nhandling" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
perm: "Permissions" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
sess: "Sessions,\ncheckpoints" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
}
knobs: "What you actually control" {
label.near: top-center
grid-rows: 1
grid-columns: 5
grid-gap: 16
style: {fill: "#FFF6D5"; stroke: "#C9A800"; font-color: "#2C2C2A"}
k1: "recursion_limit,\nmax turns" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
k2: "tool list,\nmiddleware hooks" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
k3: "compaction\nthreshold" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
k4: "allow and\ndeny rules" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
k5: "checkpointer\nbackend" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
}
components -> knobs: "configuration only"
What you control is the bottom row: you pick numbers and backends, and you register hooks. You do not decide what a resume re-runs, how compaction chooses what to drop, or what unit a limit is counted over. Those are design decisions, and they belong to whoever ships the runtime.
Why the inner harness is commoditizing and getting thinner
Two forces push the inner harness toward a commodity.
Convergence is the first: the five components above look more alike with each release. Anthropic renamed the Claude Code SDK to the Claude Agent SDK in late 2025 and described it as built on the agent harness that powers Claude Code (Anthropic docs), and an Anthropic engineering post calls it "a powerful, general-purpose agent harness" (Young, 2025). When a vendor sells its own runtime as a general-purpose library, the runtime has become a product you buy, and its competitors ship the same five components.
Less expected is the second force: harnesses are losing parts. Prithvi Rajasekaran at Anthropic wrote that Opus 4.5 "largely removed" a failure his harness had worked around, so he "was able to drop context resets from this harness entirely", and after Opus 4.6 he removed a sprint construct as well. That was his own application harness on top of the SDK, not the vendor runtime, but the lesson transfers. His general rule is that "every component in a harness encodes an assumption about what the model can't do on its own" (Rajasekaran, 2026). Manus reported rebuilding its agent framework four times (Ji, 2025).
Scaffolding that compensates for the model gets deleted when the model improves. Scaffolding that encodes your system - your tests, your money rules, your approval policy - does not, because no model release knows about them.
grid-rows: 1
grid-columns: 2
grid-gap: 40
deleted: "Deleted when the model improves" {
label.near: top-center
style: {fill: "#FFF6D5"; stroke: "#C9A800"; font-color: "#2C2C2A"}
grid-rows: 3
grid-columns: 1
grid-gap: 12
d1: "Context resets\n(dropped with Opus 4.5)" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
d2: "Sprint structure\n(dropped with Opus 4.6)" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
d3: "Prompt rules for\nold model habits" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
}
kept: "Kept across every release" {
label.near: top-center
style: {fill: "#E6F7EA"; stroke: "#3F9E52"; font-color: "#2C2C2A"}
grid-rows: 3
grid-columns: 1
grid-gap: 12
k1: "Your test suite" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
k2: "Your refund and\napproval rules" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
k3: "Your records of\nwhat already happened" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
}
Vendors can keep shrinking the inner harness because the left column is its natural fate. Nothing in the right column depends on what the model can or cannot do, and that is the line between the two harnesses.
The outer harness: everything your team brings
Your outer harness is everything you add to make a rented runtime safe for your system. Böckeler's split of it into guides and sensors is the most useful vocabulary I have found, and I use it directly.
Guides are feedforward. They "anticipate the agent's behaviour and aim to steer it before it acts". Instruction files such as AGENTS.md and CLAUDE.md, coding conventions, architecture notes, skills, and example code are all guides. They change what the model is likely to do. I covered how they behave on a legacy codebase in harness engineering on a brownfield codebase.
Sensors are feedback. They "observe after the agent acts and help it self-correct". Tests, type checkers, linters, structural tests, evals, reviewer agents, and Continuous Integration (CI) gates are sensors. They change what the system accepts.
Böckeler also splits sensors by how they run. Computational sensors are "deterministic and fast, run by the CPU": a test suite or a linter. Inferential sensors are semantic checks such as AI code review or a Large Language Model (LLM) acting as judge, which cost more and give a less stable verdict.
Two more parts complete the outer harness. Persistent state and memory is any record that must outlive a session and that the runtime must not be able to roll back: a ledger, a system of record, a log of decisions. Codebase preparation is the work of making the system easy for an agent to check, such as fast tests, clear module boundaries, and one obvious way to do each thing. Böckeler calls this property "harnessability".
direction: down
guides: "Guides (feedforward)\nAGENTS.md, conventions, skills" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
agent: "Agent\n(model + inner harness)" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
output: "Proposed change\nor action" {style: {fill: "#98D8C8"; stroke: "#5FA898"; font-color: "#2C2C2A"}}
sensors: "Sensors (feedback)" {
label.near: top-center
grid-rows: 1
grid-columns: 2
grid-gap: 16
style: {fill: "#FDE3D6"; stroke: "#D9785A"; font-color: "#2C2C2A"}
comp: "Computational\ntests, types, linters" {style: {fill: "#FFA07A"; stroke: "#D9785A"; font-color: "#2C2C2A"}}
inf: "Inferential\nreviewer agent, LLM judge" {style: {fill: "#C2185B"; stroke: "#8E1043"; font-color: "#FFFFFF"}}
}
accepted: "Accepted" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
guides -> agent: "1. steer"
agent -> output: "2. act"
output -> sensors: "3. check"
sensors -> agent: "4. fail: error goes back"
sensors -> accepted: "5. pass"
sensors -> guides: "6. same failure twice: write a guide"
Edges 1 to 5 in the diagram run on every change. Edge 6 is the slow loop, and it is the one most teams skip: a sensor that keeps catching the same mistake is telling you which guide is missing. Hashimoto describes exactly this habit: each time the agent makes a mistake, he engineers a fix so it does not happen again, either as an AGENTS.md line or as a tool (Hashimoto, 2026).
Of the two halves, guides are the weaker. Gloaguen and colleagues evaluated repository context files on real tasks and found that adding one "does not generally improve task success rates" while raising inference cost by over 20% (Gloaguen et al., 2026). Sensors have their own condition: the feedback has to come from outside the model. LLMs asked to fix their own reasoning without external feedback did not reliably improve, and sometimes got worse (Huang et al., 2024). And Anthropic found that an untuned Claude reviewer would spot real issues, then "talk itself into deciding they weren't a big deal and approve the work anyway" (Rajasekaran, 2026).
So a guide is a suggestion and a computational sensor is a fact. A good outer harness has both, but when you can only afford one, buy the sensor. The durable part of LangChain's result fits this reading: a forced verification pass before exit and a map of the environment at startup are a sensor and a piece of context. Those two are generic, though, and generic sensors are exactly what vendors absorb. The sensors that stay yours are the ones that encode your system: your tests, your money rules.
Custom tools and Model Context Protocol (MCP) servers are the interface between the two layers. A tool is code you own that the rented loop is allowed to call. That makes it the strongest place to enforce a rule, because the rule runs whether or not the model agrees with it. Put the refund check inside the tool, refusing any call without a valid grant, and you have a sensor the model cannot talk past. Put the same rule in the system prompt as a request and you have a guide, which the model can ignore.
Inner harness vs outer harness: how sources define the split
Sources do not agree on where the lines are, and you will read all of these versions. Here is a translation table.
| Usage | What "inner" or the core harness means | What "outer" means | Example source |
|---|---|---|---|
| Böckeler | the builder harness: the vendor's agent | the user harness: what you add for your system | martinfowler.com, 2026-04-02 |
| LangChain | no split: the harness is everything except the model | - | LangChain, 2026-03-10 |
| Per-call wrapper | the code around one LLM call: input prep, output validation, retry | the agent loop that strings calls together | agent-zone |
| Repo vs company | the runtime for one developer and one codebase, including CLAUDE.md, hooks and tests | operators, live business systems, governance | Nimbus |
| This article | the vendor or SDK runtime you configure | everything your team writes and owns | - |
Two of these splits cause most of the confusion. Per-call usage puts the harness inside a single model call, so "outer" becomes the orchestration that this article calls inner. Company-scope usage moves CLAUDE.md and the test suite into the inner harness and reserves "outer" for the business around the agent, which directly contradicts Böckeler's placement. I use Böckeler's line and translate the others. When a source says "outer harness", check which line it draws before you borrow its advice. Separately, "harness" still means a test harness or an evaluation harness such as EleutherAI's lm-evaluation-harness, which is unrelated.
Why inner vs outer harness matters in production
A distinction earns its place by changing a decision, and this one changes four.
Ownership. Engineering effort spent on the inner harness is mostly configuration, and its value expires with the next release. Outer-harness guides can expire too, when a model outgrows them. The sensors and records that encode your system compound, because a test or a ledger that catches one failure keeps catching it across every model you ever run.
Portability. Swapping the model is a config change. Swapping the inner harness is a port rather than a rewrite if your outer harness talks to it through tools and MCP servers. Redesigning the outer harness is the expensive move, and that cost is a sign of where the value is.
Debugging. A failure belongs to the layer that could have prevented it. If the agent used a tool it should not have, the fix belongs to the tool or its gate, not to the prompt. If a limit stopped holding after a resume, the fix belongs to state you own, not to a bigger number.
Governance. Write gates, the identity a tool call runs as, approval records, and audit logs all live in the outer layer. A vendor permission prompt is a user interface. It is not an audit trail you can show a regulator.
grid-rows: 4
grid-columns: 2
grid-gap: 24
q1: "1. Would a better model\nhave avoided it?" {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
a1: "Model or prompt. Re-test on\nthe next release before building" {style: {fill: "#95A5A6"; stroke: "#6C7A7B"; font-color: "#2C2C2A"}}
q2: "2. Did a runtime mechanism\nbehave as documented?" {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
a2: "If not: vendor bug.\nPin, report, work around" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
q3: "3. Did you rely on it for\nsomething it never promised?" {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
a3: "Outer harness: move the\nguarantee into state you own" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
q4: "4. Did a check exist\nthat could have seen it?" {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
a4: "If not: add a sensor first,\nthen a guide" {style: {fill: "#FFA07A"; stroke: "#D9785A"; font-color: "#2C2C2A"}}
q1 -> q2
q2 -> q3
q3 -> q4
When an agent fails, ask the four questions in order. Question 3 is the scope question from Part 8 of my LangGraph series (did the guarantee hold over the unit you assumed?), asked at the layer level. It is where three of the five Atlas rows in the next section land, and it is the one nobody asks, because the runtime mechanism did exactly what its documentation says.
Case study: Atlas, a LangGraph refund agent
Atlas is the support agent from my series Building Real-World Agentic AI Systems with LangGraph: twenty-three tools, one of which issues refunds. The failures below were measured or reproduced on langgraph 1.2.9 in that series, with two exceptions noted in the table. Sorted by harness layer, they come down to two causes.
What Atlas leaves to LangGraph and what it builds outside it
Atlas leaves the loop, tool dispatch, message state, checkpoint persistence and the interrupt-and-resume mechanics to LangGraph. It uses recursion_limit as a runaway guard, which is the job that limit is good at.
It builds five things outside the runtime. A Postgres grant ledger records which refunds were authorised, which were spent, and what each refund returned. The refund tool refuses a call that carries no valid grant. An approval service records who approved what. An evaluation suite runs each case twice on the same thread. And the tools reach billing and ticketing through MCP servers, so the model-facing interface is code the team owns.
direction: down
inner: "Inner harness: LangGraph 1.2.9" {
label.near: top-center
style: {fill: "#EAF2FB"; stroke: "#4A90E2"; font-color: "#2C2C2A"}
grid-rows: 1
grid-columns: 3
grid-gap: 14
loop: "create_agent loop\nmodel node + tool node" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
ckpt: "Checkpointer\nstate, step counter, interrupts" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
mw: "Middleware hooks\nHITL (first-party), your wrap_* code" {style: {fill: "#98D8C8"; stroke: "#5FA898"; font-color: "#2C2C2A"}}
}
mcp: "MCP boundary\nlangchain-mcp-adapters" {
shape: hexagon
style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}
}
outer: "Outer harness: owned by the team" {
label.near: top-center
style: {fill: "#E6F7EA"; stroke: "#3F9E52"; font-color: "#2C2C2A"}
grid-rows: 2
grid-columns: 3
grid-gap: 14
billing: "Billing MCP server" {style: {fill: "#955D37"; stroke: "#6E4426"; font-color: "#FFFFFF"}}
tickets: "Ticketing MCP server" {style: {fill: "#955D37"; stroke: "#6E4426"; font-color: "#FFFFFF"}}
ledger: "Grant ledger (Postgres)\noutside the rollback surface" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
approvals: "Approval records\nwho, what, when" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
evals: "Second-entry eval suite\nin CI" {style: {fill: "#FFA07A"; stroke: "#D9785A"; font-color: "#2C2C2A"}}
guides: "System prompt,\nrefund policy text" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
}
inner -> mcp: "tool calls"
mcp -> outer: "typed requests"
Of the shapes here, the hexagon matters most. Everything above it can change in a LangGraph release. Everything below it is versioned in the team's own repositories, and the only way the loop reaches it is through a tool call that the team's code can refuse. Note the middleware box: it holds the first-party approval gate and any wrap_* code you write, which makes hooks the other door between the layers.
Five boundary mistakes and what each one cost
Each row is a guarantee an earlier version of Atlas assumed and did not have. Rows 2, 3 and 5 relied on the inner harness for something it never promised; nothing promised a static tool list, for example. Rows 1 and 4 are outer-harness mistakes: a guide used where a sensor was needed, and a sensor with a scope gap.
| Assumed guarantee | What actually happened | Where it is shown | Owned fix |
|---|---|---|---|
| A prompt rule stops unapproved refunds | nothing below the prompt stops it: REFUNDED 4999 on 8812 on a scripted turn one | Part 3 | refusal in the tool or wrap_tool_call |
recursion_limit caps a thread's total work | the limit is counted per invocation and restored on resume | Part 4 | step and spend counters in a ledger |
| one human approval means one refund | three gated refunds in one message: 12 tool-body runs, money moved six times, the first customer refunded three times | Part 8 | ledger consume before the effect |
| a green eval suite covers the agent | every case ran on a fresh thread, so no re-entry defect could exist during the test | Part 7 | run each case twice on one thread |
| the tool list is what the repo declares | MCP tools are fetched at runtime; per a reported adapter issue (since closed, superseded by a newer client), one failing server can empty the list, so no money moves but the agent can answer wrongly from the model alone | Part 3 | startup check on the tool set |
Not one of these is a LangGraph bug in the usual sense. In rows 2 and 3 the mechanism did what its documentation says, and Part 8 of the series named that shape a Scope-Blind Guarantee: a real, correctly enforced guarantee whose unit is not the unit you assumed. Part 4 named the mechanism under both rows a Self-Restoring Bound: an allowance re-derived from restored state on every entry, with no spent total to respect. Sorting the rows by layer adds the last piece, which is who gets to change the guarantee.
The Rented Guarantee: agent guarantees your vendor can change
I call it a Rented Guarantee: a property your system depends on whose existence, scope and lifetime are defined by the inner harness's vendor and not by you. It does not have to have changed yet. What makes it rented is who can change it.
Two older ideas sit nearby. Hyrum's Law says users come to depend on observable behaviour beyond the contract, and vendor lock-in is about the cost of leaving. A Rented Guarantee covers documented behaviour too, and it does not care whether you could leave. The question is narrower: if this changed, would your system notice?
The three names cover three axes of one failure. A Scope-Blind Guarantee is about reading: you misread the unit. A Self-Restoring Bound is about whether a spent total exists outside the state a restore can reach. A Rented Guarantee is about ownership: read the documentation perfectly, keep the record in the right place, and the guarantee can still change or disappear in a release you did not write.
That does happen. Through the 2025-11-25 revision, the Model Context Protocol gave servers a protocol session with an Mcp-Session-Id, and teams hung per-client state and session-bound auth on it. The 2026-07-28 revision removed protocol sessions, and the spec now tells servers to pass "explicit, server-minted handles" as ordinary tool arguments instead (MCP changelog; migration guide). Any guarantee built on the session was rented, and the terms changed in a spec revision. You can pin a version, and Atlas pins langgraph 1.2.9, but a pin delays the change. It does not move the guarantee into code you own.
Testing for one takes two questions per guarantee your agent relies on: could this change in a release I would only notice by reading a changelog, and would any code I own fail loudly if it did? If the first answer is yes and the second is no, the guarantee is rented. One condition keeps the test from flagging everything: it applies when a silent change would break a money, data or permission invariant and the contract exists only as one vendor's runtime behaviour. Postgres row-update semantics are rented too, in a sense, but they rest on a standard that has held for decades. An interrupt's re-run rule does not.
grid-rows: 1
grid-columns: 2
grid-gap: 40
rented: "Rented: guard inside rollback" {
label.near: top-center
style: {fill: "#FDECEA"; stroke: "#E74C3C"; font-color: "#2C2C2A"}
grid-rows: 4
grid-columns: 1
grid-gap: 12
s1: "Checkpoint holds\nrefund_allowed = true" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
s2: "Crash after the refund, or a\nsibling call's resume, re-runs the body" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
s3: "Restored state and replayed\napproval both say yes" {style: {fill: "#FFD93D"; stroke: "#C9A800"; font-color: "#2C2C2A"}}
s4: "Second refund" {style: {fill: "#E74C3C"; stroke: "#A93226"; font-color: "#FFFFFF"}}
}
owned: "Owned: record outside it" {
label.near: top-center
style: {fill: "#E6F7EA"; stroke: "#3F9E52"; font-color: "#2C2C2A"}
grid-rows: 4
grid-columns: 1
grid-gap: 12
o1: "Checkpoint holds a grant\nper ticket and amount" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
o2: "Crash after the refund\nre-runs the body" {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
o3: "consume() misses; ledger returns\nthis call's result (or 'pending')" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
o4: "One refund, and a\ntrue answer to the model" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
}
Both columns run the same runtime and face the same re-entry, and they differ only in where the record of having spent the grant lives. On the left it lives in state the checkpointer restores, so a faithful re-run reaches the same verdict and pays twice. On the right it lives in a table the checkpointer cannot touch, and the table also remembers what the first run returned.
Wrong way and right way: the refund approval
Here is the version that trusts the inner harness. Every check reads checkpointed state or the interrupt mechanism, and both sit inside the rollback surface.
from langchain.tools import ToolRuntime, toolfrom langgraph.types import interrupt# billing: the team's billing client, reached through the billing MCP server@tooldef issue_refund(ticket_id: str, cents: int, runtime: ToolRuntime) -> str: """Refund a ticket after a human approves it.""" if not runtime.state.get("refund_allowed"): return "refund not authorised" if interrupt({"approve_refund": ticket_id, "cents": cents}) != "yes": return "refund declined" billing.refund(ticket_id, cents) return f"REFUNDED {cents} on {ticket_id}"It reads well, and a single call with a single resume refunds once, so it passes a one-shot test. It fails in two situations the test never builds. When three of these calls arrive in one assistant message, each resume re-runs every body, including the ones already approved: that is the Part 8 measurement of twelve body runs and six refunds for three approvals. And a crash between the refund and the next checkpoint replays the body against restored state that still says yes, which is Part 4's case.
The right way rents what is safe to rent and owns the rest. The approval gate can stay rented: Part 8 measured LangChain's first-party HumanInTheLoopMiddleware raising one interrupt for all three calls before any tool body runs, so the twelve-run failure never starts (LangChain docs). By this article's own test that gate is still a Rented Guarantee on the money path, so Atlas rents it on purpose and makes the dependency loud: a startup check, shown after the main code, fails the deploy if issue_refund ever drops out of the middleware's interrupt_on map. The spend record cannot be rented, because a crash replay still reaches the tool. So the tool keeps no interrupt() and instead consumes a grant from a ledger the checkpointer cannot roll back.
Two changes to Part 4's design come with it. AtlasState holds refund_grants, a dict from "ticket:cents" to grant id with a merging reducer, so three entitlement checks in one message leave three grants instead of overwriting one slot. The dict allows one live grant per ticket and amount on a thread, so a second identical refund needs a later entitlement check. And the check lives in the tool rather than in Part 4's LedgerRefundGate middleware, because Atlas owns this tool and the consume and the effect then sit in one function. Use the middleware form for tools you did not write.
from langchain.tools import ToolRuntime, toolfrom psycopg.types.json import Jsonb# pool: a psycopg_pool.ConnectionPool for Part 4's grant_ledger table# billing: the team's billing client, reached through the billing MCP serverdef consume(conn, grant_id, thread_id, ticket_id, cents, effect_id) -> bool: """Spend a grant at most once, and record which tool call spent it.""" with conn.cursor() as cur: cur.execute( """ UPDATE grant_ledger SET consumed_at = now(), effect_id = %s, effect_result = NULL WHERE grant_id = %s AND thread_id = %s AND ticket_id = %s AND cents = %s AND consumed_at IS NULL """, (effect_id, grant_id, thread_id, ticket_id, cents), ) return cur.rowcount == 1def spent_by(conn, grant_id, effect_id): """None if this call never spent the grant, else its recorded result or 'pending'.""" with conn.cursor() as cur: cur.execute( "SELECT effect_result FROM grant_ledger" " WHERE grant_id = %s AND effect_id = %s", (grant_id, effect_id), ) row = cur.fetchone() return None if row is None else (row[0] or {"message": "refund pending"})@tooldef issue_refund(ticket_id: str, cents: int, runtime: ToolRuntime) -> str: """Refund a ticket once per entitlement grant.""" grant_id = runtime.state.get("refund_grants", {}).get(f"{ticket_id}:{cents}") if grant_id is None: return "refund not authorised: no grant for this ticket and amount" thread_id = runtime.config["configurable"]["thread_id"] call_id = runtime.tool_call_id with pool.connection() as conn: # commits on clean exit, before the effect spent_now = consume(conn, grant_id, thread_id, ticket_id, cents, call_id) if not spent_now: with pool.connection() as conn: earlier = spent_by(conn, grant_id, call_id) if earlier is not None: # a replay of this same call: report what happened return earlier["message"] return "refund not permitted: grant already spent by another call" # an exception here leaves a consumed row with no result, for the sweep result = billing.refund(ticket_id, cents, idempotency_key=f"{grant_id}:{call_id}") message = f"REFUNDED {cents} on {ticket_id}" if result.ok else "refund declined" with pool.connection() as conn: conn.execute( "UPDATE grant_ledger SET effect_result = %s," " consumed_at = CASE WHEN %s THEN consumed_at END" " WHERE grant_id = %s AND effect_id = %s", (Jsonb({"ok": result.ok, "message": message}), result.ok, grant_id, call_id), ) return messageThe startup check is six lines. Call it where the agent is built, on the same middleware list you pass to create_agent, and it is the difference between a rented gate you know about and one you discover:
from langchain.agents.middleware import HumanInTheLoopMiddlewaredef assert_refund_is_gated(middleware: list) -> None: """Fail the deploy if the rented approval gate stops covering issue_refund.""" gates = [m for m in middleware if isinstance(m, HumanInTheLoopMiddleware)] if not any("issue_refund" in m.interrupt_on for m in gates): raise RuntimeError("issue_refund is not behind HumanInTheLoopMiddleware")Every change is in the outer harness. The spend record lives in Postgres, keyed to the tool call that spent it, so a crash replay of the same call gets the recorded answer instead of a false "no refund issued", and a different call riding the same grant is refused. A declined refund clears consumed_at, so a later call can retry. That retry works only because the idempotency key is scoped to the attempt, grant_id:call_id, and not to the grant: providers store the first result for a key, a decline included. A crash replay of the declined call itself re-sends with the same key and gets the stored decline back, which is the right answer.
The trade Part 4 made still applies. The commit lands before the money moves, so a crash between consume() and billing.refund() loses a refund rather than duplicating one. That row is consumed with no effect_result, which a reconciliation sweep can find and re-drive. The idempotency key matters on that re-drive, not on the replay path. Providers prune keys, Stripe after at least 24 hours (Stripe docs), so the sweep has to run inside that window or look the refund up at the provider before it re-drives. It also needs an age threshold, so a refund still in flight is not mistaken for a crashed one.
Evals need the matching change. Part 7 called the gap the First-Entry Assumption: a suite that gives every case a fresh thread_id can never create the state a re-entry defect needs. Owning the fix means asserting on the ledger, not on the transcript, and running each case that touches money a second time on the same thread. The ledger assertion catches row 3 on the first run. The second run catches row 2. Both are computational sensors, and together they cost one extra run per case.
Designing your outer harness: a practical checklist
- Start from failures you have actually seen. List the last ten agent incidents or near misses. A harness designed from a framework diagram protects against imaginary failures and misses the real ones.
- For each failure, decide whether it needs a guide, a sensor, or both. If the agent did not know a rule, write a guide. If the agent knew and did it anyway, or if the cost of one miss is high, you need a sensor.
- Prefer computational sensors first. A test, a type check, a schema validation, or a ledger constraint gives the same verdict every time. Add an inferential reviewer only for properties no deterministic check can express, such as whether a change matches the design intent.
- Find every Rented Guarantee. For each limit, approval and "only once" property the agent relies on, ask whether a release could change it and whether code you own would fail loudly if it did. Move the ones that touch money, data or permissions into state you own.
- Put enforcement in tools, not prompts. If a rule matters, the tool that performs the action checks it. Prompts can explain the rule but cannot enforce it.
- Test re-entry, not only first entry. Run cases twice on the same thread, resume after an interrupt, and replay from an earlier checkpoint.
- Review the harness at every model upgrade. Remove guides that only compensated for an older model. Keep every sensor that encodes a rule about your system. Re-run the eval suite before and after, and keep the numbers.
What an outer harness costs: latency, tokens and over-building
A strong outer harness costs real money and real time, and the costs grow in a predictable order.
direction: down
ladder: "Sensor cost, cheapest first" {
label.near: top-center
grid-rows: 4
grid-columns: 2
grid-gap: 14
style: {fill: "#F4F6F6"; stroke: "#95A5A6"; font-color: "#2C2C2A"}
c1: "Types, linters, schema checks" {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
n1: "milliseconds, same verdict every run" {style: {fill: "#FFFFFF"; stroke: "#95A5A6"; font-color: "#2C2C2A"}}
c2: "Tests, ledger constraints, re-entry evals" {style: {fill: "#98D8C8"; stroke: "#5FA898"; font-color: "#2C2C2A"}}
n2: "seconds to minutes, deterministic" {style: {fill: "#FFFFFF"; stroke: "#95A5A6"; font-color: "#2C2C2A"}}
c3: "Reviewer agent, LLM judge" {style: {fill: "#FFA07A"; stroke: "#D9785A"; font-color: "#2C2C2A"}}
n3: "model calls per check, verdict can vary" {style: {fill: "#FFFFFF"; stroke: "#95A5A6"; font-color: "#2C2C2A"}}
c4: "Human approval" {style: {fill: "#E74C3C"; stroke: "#A93226"; font-color: "#FFFFFF"}}
n4: "minutes to days of waiting" {style: {fill: "#FFFFFF"; stroke: "#95A5A6"; font-color: "#2C2C2A"}}
}
Feedback loops cost latency and tokens. Anthropic's long-running harness work reported a solo agent run at about 20 minutes and $9, against about 6 hours and $200 for the full harness with planner and evaluator (Rajasekaran, 2026). That harness produced the better result, but a twenty-fold cost increase has to buy something you can name. Spend from the top of the ladder down, and push each check as low on it as it will go.
You can over-build around a model that will outgrow it. Context resets and sprint structures were reasonable scaffolding until a model release made them dead weight. To guard against that, label each outer-harness component with the failure it exists for. When the failure stops appearing in your evals across a model upgrade, the component is a candidate for deletion, and the label tells you so. Sensors that encode business rules almost never qualify, because the model was never the reason for them.
Enterprise environments limit what the inner harness can do, so more of the work moves outward. An approved-stack list may allow LangGraph and forbid a vendor CLI, which is one more reason to keep your tools and state behind MCP servers rather than inside one runtime's plugin format. Network inspection is the other common limit. TLS-inspecting proxies break agent CLIs unless they trust the corporate certificate authority, and Claude Code's enterprise documentation has a whole page on HTTPS_PROXY, NODE_EXTRA_CA_CERTS, a configurable certificate store, mutual TLS, and turning off request compression for proxies that mishandle it (Claude Code docs). That page also lists registry.npmjs.org on its allowlist so that npx-launched MCP servers can install. In an air-gapped or resource-constrained network that dependency fails, so pre-install and pin your MCP servers, and treat the tool set as an artifact you ship rather than something the agent fetches.
Conclusion: rent the loop, own the guarantees
You rent the inner harness. It improves every quarter, and its vendor can change any part of it, including the parts your reliability quietly depends on. You own the outer harness, and it is the only layer where a fix you make today still holds after the next model release and the next framework upgrade.
So spend your engineering effort where it compounds: on sensors before guides, on tools that enforce rules before prompts that request them, and on records the runtime cannot roll back. Then go through your agent and list every Rented Guarantee. Atlas had three on the refund path: rows 2 and 3 of the table, the step limit and the approval count, which it now owns, and the approval gate, which it rents on purpose behind a startup check.
One question I cannot answer yet. Thinner is not the whole story: model-compensating parts get deleted while product features get added, and vendors are moving outward. Claude Code ships hooks and skills, LangChain ships human-in-the-loop middleware, and LangChain's verification pass is now a feature. If the inner harness absorbs most of today's outer harness, what remains yours? My guess is that it will be the parts that encode your system rather than the model's weaknesses, and the ledger more than anything. If you are seeing a different line form, I would like to hear where.
References
- Böckeler, B. (2026b, 2026-04-02). Harness engineering for coding agent users. martinfowler.com. https://martinfowler.com/articles/harness-engineering.html
- Böckeler, B. (2026a, 2026-02-17). Harness Engineering - first thoughts. martinfowler.com. https://www.martinfowler.com/articles/exploring-gen-ai/harness-engineering-memo.html
- Hashimoto, M. (2026-02-05). My AI Adoption Journey. https://mitchellh.com/writing/my-ai-adoption-journey
- Lopopolo, R. (2026-02-11). Harness engineering: leveraging Codex in an agent-first world. OpenAI. https://openai.com/index/harness-engineering/
- Trivedy, V. (2026a, 2026-02-17). Improving Deep Agents with harness engineering. LangChain. https://www.langchain.com/blog/improving-deep-agents-with-harness-engineering
- Trivedy, V. (2026b, 2026-03-10). The Anatomy of an Agent Harness. LangChain. https://www.langchain.com/blog/the-anatomy-of-an-agent-harness
- Bolin, M. (2026-01-23). Unrolling the Codex agent loop. OpenAI. https://openai.com/index/unrolling-the-codex-agent-loop
- Rajasekaran, P. (2026-03-24). Harness design for long-running application development. Anthropic. https://www.anthropic.com/engineering/harness-design-long-running-apps
- Young, J. (2025-11-26). Effective harnesses for long-running agents. Anthropic. https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents
- Ji, Y. (2025-07-18). Context Engineering for AI Agents: Lessons from Building Manus. https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus
- Gloaguen, T. et al. (2026). Evaluating AGENTS.md. arXiv:2602.11988. https://arxiv.org/abs/2602.11988
- Huang, J. et al. (2024). Large Language Models Cannot Self-Correct Reasoning Yet. ICLR 2024. https://arxiv.org/abs/2310.01798
- Anthropic. Claude Agent SDK migration guide (accessed 2026-11-11). https://code.claude.com/docs/en/agent-sdk/migration-guide
- Anthropic. Claude Code enterprise network configuration (accessed 2026-11-11). https://code.claude.com/docs/en/network-config
- Agent Zone. Building LLM harnesses (accessed 2026-11-11). https://agent-zone.pages.dev/knowledge/agent-tooling/building-llm-harnesses
- Nimbus. Inner vs outer agent harness (accessed 2026-11-11). https://gonimbus.ai/blog/inner-vs-outer-agent-harness/
- Kumar, R. Building Real-World Agentic AI Systems with LangGraph, Parts 1 to 4, 7 and 8. https://ranjankumar.in/langgraph-why-agents-need-a-runtime
- Model Context Protocol. Specification 2026-07-28: changelog. https://modelcontextprotocol.io/specification/2026-07-28/changelog
- langchain-mcp-adapters issue #492 (opened 2026-04-24). https://github.com/langchain-ai/langchain-mcp-adapters/issues/492
- LangChain. Human-in-the-loop (accessed 2026-11-11). https://docs.langchain.com/oss/python/langchain/human-in-the-loop
- Stripe. Idempotent requests (accessed 2026-11-11). https://docs.stripe.com/api/idempotent_requests
- Terminal Bench (accessed 2026-11-11). https://www.tbench.ai/
- Kumar, R. LangGraph reducers and concurrent state writes (Part 2). https://ranjankumar.in/langgraph-reducers-concurrent-state-writes
- Kumar, R. Harness engineering on brownfield codebases. https://ranjankumar.in/claude-code-brownfield-harness-engineering
- Kumar, R. MCP Went Stateless: Your 2026-07-28 Migration Guide. https://ranjankumar.in/mcp-stateless-2026-spec-migration
Related Articles
- MCP Went Stateless: Your 2026-07-28 Migration Guide
- Do You Need an Agent Framework? Start With Plain Code
- LangGraph create_agent: The Graph Isn't the Runtime
- Agent Skills Are Not Prompts. They Are Production Knowledge Infrastructure.






