AI Infra
0%
Part VI · Chapter 41

The Harness

AuthorChangkun Ou
Reading time~18 min

The harness is the runtime around the model: the layer that turns model output into tool calls and tool results back into model input, manages the context window, routes tools, sandboxes execution, and exposes the verbs that let a person pause, redirect, fork, or kill a running loop. A loop that looks trivial as a flowchart needs this machinery as soon as it meets production, and the shape of that machinery can move an evaluation score as much as a model swap does.

2026-06-22T11:57:20.949634 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/
Figure 41.1. Schematic of harness choices by runtime cost and isolation strength. Stronger sandboxes cost more, but they bound the damage when generated actions touch real systems. Idealized placement, not measured data.

A loop that refuses to stay simple

The contract is small. Create a session bound to an agent definition. Drive its loop: send context to the model, dispatch the tool calls it returns, append the results, repeat until done or interrupted. Resume after a crash or a wait. Interrupt mid-flight. Fork at a checkpoint. Drawn as a flowchart the loop is a box and an arrow back to itself.

It stops being trivial the moment it meets production. A tool that holds a connection across turns leaves the loop no longer stateless between steps. As the context window fills, something has to decide what to forget. A user hits cancel while a sandbox command is mid-execution, and now the loop must stop work it does not directly control. The sharpest case is two replicas that both think they own the same scheduled tick, where the loop has to coordinate with a copy of itself it cannot see. The harness is where these frictions are absorbed or leaked. It owns mechanism, not policy: it does not decide who may call (identity, Chapter 56), where the model comes from (model access), or which actions require a human (oversight, Chapter 55). It decides how those decisions are carried out, and the most consequential mechanism it owns is the ability to stop the loop.

The loop drawn bare is one box and a back-edge. What Figure 41.2 adds is where each of those production frictions bites the loop, which is the whole argument in one picture: four unrelated-sounding problems land at four distinct points on a structure that looked like it had none.

CTX Send context to model CALL Model returns tool calls CTX->CALL DISP Dispatch tool calls CALL->DISP APP Append results to context DISP->APP APP->CTX not done END Return to caller APP->END done F1 Tool holds a connection across turns: loop is no longer stateless between steps F1->DISP F2 Context window fills: loop must decide what to forget F2->APP F3 User cancels mid-execution: loop must stop work it does not directly control F3->DISP F4 Two replicas own one scheduled tick: loop must coordinate with a copy it cannot see F4->CTX
Figure 41.2. The agent loop with the four production frictions mapped to where each one bites. The bare loop is the solid cycle; the dashed callouts are the points where reality breaks the abstraction.

The six mechanisms that follow absorb those frictions. Each began as a simple version, broke at a specific point under load, and acquired exactly the machinery that point demanded. Read in order, the six are the harness: where an instance comes from, how a cancel reaches the work, who is allowed to run, what the lock actually protects, what survives a full context window, and how the model learns a tool exists. Each carries the lineage that produced it, the knee where its trade-off bites, and the operational hardening that lets the loop survive a long night unattended.

Where an instance comes from

The first decision is when an agent definition, a prompt plus a tool set plus a model binding, becomes a live instance. The obvious starting point is a singleton: one runner shared across every session, isolated only by a session_id, zero allocation per request. The hidden cost is not performance, it is rollout granularity. With a singleton, the prompt is a deploy-time constant, so a bad prompt regresses 100% of sessions until rollback completes. OpenAI's GPT-4o sycophancy episode is the shape of this: the update rolled out April 25, 2025, rollback began April 28, and the full revert took roughly a day (OpenAI 2025). A runtime registry resolves a prompt version per request instead, which lets a regression touch a canary slice while the stable version keeps serving everyone else. The blast radius now equals the canary, not the fleet. The cost is a resolution hop, a cross-replica consistency story, and a new variable in every incident.

The knee is when this stops being optional. A singleton is fine at one or two agent types and starts hurting past three or four, when a prompt regression in one agent forces a rollback that reverts an unrelated fix in another because they shared a deploy. That shared-deploy incident is the moment versioning stops being a nicety, because the cost of not having it has become a regression in code nobody touched.

How a cancel reaches the work

Cancellation is exactly as effective as its most-blind hop. The harness can see an interrupt flag instantly, but the flag has to travel down every layer that might be mid-execution: the model call has to honor its context deadline, the tool dispatch layer has to propagate cancellation into the tool, the sandbox command has to respect SIGTERM, and a stateful protocol call has to respect RPC-level cancellation. A model call that ignores its deadline holds the session for tens of seconds past cancel. A sandbox command that ignores SIGTERM holds it for the full grace period. So "the runtime supports cancellation" is a claim about every hop in the cascade, and a single uncooperative layer makes the cancel cosmetic. This is why interruption is a primitive and not a feature: the pause a human-in-the-loop approval depends on is threaded through every hop, or it does not actually stop anything. The decision about what requires approval belongs to oversight; the pause that lets the approval happen is the runtime's verb, and frameworks that expose it as a first-class interrupt make that pause inspectable rather than implicit (LangChain 2025). Figure 41.3 traces the cascade and the latency each uncooperative hop adds, so the claim that cancel is only as real as its most-blind hop becomes a property of the chain rather than an assertion.

FLAG Harness sets interrupt flag: instant MODEL Model call honors context deadline FLAG->MODEL TOOL Tool dispatch propagates cancellation MODEL->TOOL D1 Cosmetic cancel MODEL->D1 ignored: holds session tens of seconds SBX Sandbox command respects SIGTERM TOOL->SBX RPC Stateful protocol call respects RPC cancellation SBX->RPC D2 Cosmetic cancel SBX->D2 ignored: holds for full grace period
Figure 41.3. The cancellation cascade. The interrupt flag is instant, but the cancel is only effective once every downstream hop honors it. One uncooperative hop bounds the whole result, so the dangling delays mark where a cancel goes cosmetic.

Try setting obeys to False for one hop and watch the time-to-stop jump to that hop's grace period: the cancel is only as fast as its most-blind layer.

# Each hop either obeys the interrupt fast, or ignores it and only
# stops when its own grace period expires. Time-to-stop is the slowest hop.
hops = [
    {"name": "model call",   "obeys": True,  "grace_s": 30.0},
    {"name": "tool dispatch","obeys": True,  "grace_s": 5.0},
    {"name": "sandbox cmd",   "obeys": False, "grace_s": 10.0},  # ignores SIGTERM
    {"name": "RPC call",      "obeys": True,  "grace_s": 8.0},
]
flag_latency = 0.01  # harness sets the interrupt flag almost instantly
stop_times = [flag_latency if h["obeys"] else h["grace_s"] for h in hops]
for h, t in zip(hops, stop_times):
    print(f"{h['name']:14s} stops after {t:6.2f}s")
print(f"\ntime-to-stop = max = {max(stop_times):.2f}s "
      f"(bounded by the most-blind hop)")

The verb has four shapes, and the quality of human control is bounded by which one the harness offers. Hard cancel is kill(session) with no state preservation, simple and brutal, making every correction a context-losing event. Cooperative pause checks an interrupt flag at step boundaries and serializes state. Queued steering appends user input to the next turn without pausing, fast but able to commit a stale decision before it sees the message. Resume-at-point combines steering with a rewind. The pause that an approval depends on is exactly as good as the strongest of these the runtime implements, and no better.

Who is allowed to run

When the loop runs on a schedule, every replica runs its own scheduler, so without coordination every replica fires every tick. The usual fix is an agent-level distributed lock acquired before the run. It prevents two replicas from firing the same tick concurrently. It does not make the tick's side effects exactly-once. If a tick posts to Slack, opens a ticket, then crashes before recording "done," recovery replays it and the Slack message posts twice. The lock coordinates who runs; exactly-once requires coordinating what already happened to the outside world. Idempotency keys at the side-effect call sites close that gap, and they are a different guarantee that a scheduler with only a lock does not have.

A lock prevents overlap, not duplication, and there is a second, sharper way the altitude is wrong. A harness almost always has session-level locking, which stops two Run() calls from mutating one session at once. It says nothing about two different sessions of the same agent touching the same external resource. Two coding sessions push conflicting commits to one branch. A user session and a scheduled tick target the same external state, neither aware of the other. The lock is in the harness; the conflict is at the resource, and the harness lock does not reach it. Figure 41.4 shows the mismatch directly.

A Agent: acme-api S1 Session 1 A->S1 S2 Session 2 A->S2 S3 Scheduled tick A->S3 GIT git repo: acme/api S1->GIT git push main JIRA Jira: ACME project S1->JIRA create ticket S2->GIT git push main S3->JIRA create ticket
Figure 41.4. Session-level locking is the wrong altitude for a resource conflict. Each session is individually locked, yet two sessions of the same agent collide at a shared external resource the harness lock never reaches.

Every session in green is individually locked; every shared resource in red is unlocked. The fix lives at the resource: a worktree per session that moves the conflict to a merge, or a resource-level lock keyed on the named resource, or optimistic reconciliation on the external system. The collision is not hypothetical; merge conflicts in coding-agent pull requests are now frequent enough to form a labeled dataset (Ogenrwot and Businge 2026). The right choice depends on whether the downstream system already has its own concurrency control to lean on. Git ref updates and database row locks are real coordination primitives; where they exist the harness can defer to them.

The scheduler itself climbs the same pressure curve. In-process and external-cron schedulers give way to durable execution frameworks (Temporal, Inngest, Restate) once a tick needs multi-step choreography with external side effects, because probabilistic model behavior makes naive retry insufficient, which is exactly the case durable journaling is built for. Whether to keep the session persistent or ephemeral across ticks is a parallel choice with its own knee. A persistent session accumulates context, so a monitoring agent remembers last week's baseline, but events grow without bound, which makes context management mandatory. An ephemeral session is fresh per tick and correct when each tick is independent. Persistent for a stateless job buys unbounded storage for nothing; ephemeral for a job that needs memory throws the memory away every tick.

What survives a full window

A long-running loop hits the context window, and the harness has to answer two questions that do not answer themselves: what gets preserved, and who decides. Automatic compaction summarizes older turns so the session runs past the hard limit, and its failure mode is structural: summarization discards detail and the agent has no signal about what it lost. Anthropic's cookbook measured compaction preserving three of three high-level facts and zero of three obscure specifics (Anthropic 2026). The alternative is externalization, where the agent writes what it wants to keep to a file (progress.md, plan.md) and reloads after compaction. That trades the summary's blindness for the agent's blindness about its own future needs. Neither is mechanical, which is the subject of the contested box below.

"Just use a bigger window" is the tempting third option, and it relocates the failure rather than removing it. A larger context window reduces compaction frequency but does not remove the degradation: accuracy drops as input length grows even with perfect retrieval (Du et al. 2025), and the same rot shows up across realistic retrieval settings (Hong et al. 2025). "Fit everything in" moves the failure from "context overflowed" to "context is full and the model is quietly worse."

What's contested

Whether long-session context management can be made mechanical is unsettled, and the evidence says it cannot yet. Lindenbauer et al. showed that simply masking old tool outputs with placeholders matches LLM summarization on solve rate at the lowest cost in four of five settings, and that summaries cause trajectory elongation: agents persist 13 to 15% longer than optimal because the summary masks the failure signals that would have told them to stop (Lindenbauer et al. 2025). Compaction work that does help, such as learned context compaction for long-horizon tasks (Kang et al. 2025), still leaves the core question open. Bigger windows do not settle it either, since accuracy degrades with length even under minimal conditions and perfect retrieval (Du et al. 2025). Long-session quality rests on three things the runtime cannot guarantee mechanically: the model writing a good summary when asked, the model interpreting that summary on reload, and the runtime enforcing an externalization pattern rather than hoping the agent maintains one. The runtime can fire compaction at the right moment and reload the right file. It cannot make the summary true.

How the model learns a tool exists

Dispatch is how a tool runs; the registry is how the model learns the tool exists. A static flat list sends every tool in every system prompt and runs out at roughly 30 to 50 tools, because function-calling accuracy falls as the catalog grows and robustness suffers specifically when irrelevant tools are present (Yan et al. 2025). Beyond that the runtime needs per-turn selection. The security-flavored failure is the sharpest: a malicious tool description at the catalog layer can inject instructions into the model through the system prompt, bypassing the user prompt entirely (Invariant Labs' mcp-injection-experiments). The registry is not a neutral lookup table, which is why catalog admission is a trust decision handed to Chapter 56. What the registry admits to the catalog, it admits to the model's instructions.

Selection evolved as a ladder, each rung raising the catalog ceiling and adding a cost the runtime must then own. The static flat list gave way to dynamic tool-RAG, which retrieves only the tools relevant to each turn (embedding their schemas and fetching the top-k matches), scaling to thousands of tools at the cost of a retrieval layer with its own error modes. Hierarchical namespacing with mount and unmount shows a pruned subset per phase, read-only tools while understanding a problem and write tools while fixing it, which requires the runtime to track phase. Model-driven discovery gives the model a list_available_tools tool and lets it ask, which raises the ceiling but makes discovery quality a function of model capability the runtime does not own. Figure 41.5 lays the four rungs against the scaling ceiling each one buys and the cost it adds.

tool_ladder R1 Static flat list ceiling 30 to 50 tools irrelevant tools hurt accuracy R2 Dynamic tool-RAG scales to thousands cost: a retrieval layer with its own errors R1->R2  catalog grows R3 Hierarchical namespacing mount and unmount per phase cost: runtime must track phase R2->R3  catalog grows R4 Model-driven discovery model calls list_available_tools cost: quality follows model capability R3->R4  catalog grows
Figure 41.5. The tool-selection ladder. Each rung raises the catalog ceiling and adds a cost the runtime must then own, from a static prompt list to model-driven discovery.

What a stateful tool drags behind it

Tools fold into the design along a statefulness axis. Stateless tools (a web fetch, a sandbox exec given a sandbox ID) hold no harness-side state and scale trivially. Stateful-connection tools (MCP, gRPC streams) carry negotiated capabilities and cursors in a connection the harness holds for the session, and dispatching them through one mechanism shared with stateless calls mishandles one class or the other. The hardest variant is a single logical operation that mixes both, because its partial-failure space is the product of the two.

Stateful tool transport is the clearest lineage in the chapter. The simplest design holds connections in memory per replica, keyed by (session_id, server_url), opened lazily. It works until the platform scales horizontally, and then statefulness fights the load balancer: the MCP 2026 roadmap names stateful sessions as the primary scaling bottleneck (Model Context Protocol 2026). The concrete failure is the reconnect storm. When a replica holding N sessions, each connected to M servers, dies, recovery is N times M simultaneous reconnections against the surviving infrastructure, each renegotiating capabilities and losing cursor state. The mechanism dictated the mitigation: session affinity to keep a connection on one replica, reconnect backoff with jitter to desynchronize the herd, and resumable streams to make reconnection cheap. The MCP spec followed this curve directly, deprecating HTTP+SSE in favor of Streamable HTTP, where a reconnect replays from a cursor using Mcp-Session-Id plus Last-Event-ID instead of restarting (Model Context Protocol 2025; Model Context Protocol 2025). The spec also hardened a related hazard pooling alone does not touch: when several concurrent sessions for one user race to refresh a single-use OAuth token, the losers present an already-consumed token and fail, so the 2025-06-18 spec revision requires clients to implement RFC 8707 resource indicators to keep a token from being redeemed against the wrong server (Campbell et al. 2020; Model Context Protocol 2025). The 2026 spec work carries the lineage to its endpoint: the 2026-07-28 release candidate removes the protocol-level session entirely, dropping Mcp-Session-Id (SEP-2567) and the initialize handshake (SEP-2575), so any request can hit any replica and a server that needs state across calls mints an explicit handle the model passes back as an ordinary argument (Model Context Protocol 2026). The arc runs from held connections to resumable streams to no protocol session at all.

Multi-agent composition has a parallel arc, with OpenAI's experimental Swarm superseded by the Agents SDK, but composition itself belongs to Chapter 43 and the harness only owns whether the composed agents share or isolate session and sandbox state. That sharing choice is its own trade-off with a measured cost. Shared session and sandbox is cheapest and pays with races and prompt contamination. Full sandbox isolation is the most plumbing and the strongest containment: Geng and Neubig measured worktree isolation at 59.1% against soft instruction-level isolation at 56.1% on Commit0-Lite, and showed soft isolation falling below a single-agent baseline on PaperBench (Geng and Neubig 2026). Instruction-level constraints are advisory; real isolation does not depend on the model's compliance.

Hardening for the long night

The loop itself is small. The machinery around it is what the rest of this chapter has been about, and the last of it concentrates in a handful of operational choices that decide whether the loop survives running unattended.

The lock has two gotchas that come straight from the locking literature. Redis-based locks need fencing tokens to be safe under network partitions (Kleppmann's Redlock critique) (Kleppmann 2016). And with PgBouncer transaction pooling, pg_advisory_lock releases when the connection returns to the pool, so a caller that expects a session-scoped lock must use pg_advisory_xact_lock or a dedicated session pool, or the lock evaporates mid-tick.

A wall-clock timeout on every session, regardless of trigger, is the one limit that holds even when token accounting is wrong or lagging, because wall-clock time is the quantity the harness can measure locally without trusting a downstream meter. The evidence for why it is load-bearing is stark: a Claude Code recursion consumed about 1.67 billion tokens in five hours (anthropics/claude-code 2025), and a practitioner postmortem reports a LangChain pipeline whose agents, talking over agent-to-agent messaging (A2A, Chapter 43), looped for eleven days and produced roughly a $47,000 bill with neither agent carrying a budget ceiling (Waxell 2025).

Admission control belongs at the harness layer. Session creation that calls the Kubernetes API requests a sandbox rather than creating one, and the dangerous failure is not refusal but a hang: the pod sits Pending and the stream to the client never starts, with no error to surface because nothing errored. Checking capacity before promising the client a session turns a slow silent nothing into a fast legible "no" the client can retry against.

The remaining controls follow the same mechanical reasoning. Jitter prevents a thundering herd when many agents share a schedule. A circuit breaker disables an agent after N consecutive failures so it cannot burn tokens indefinitely: in the AWS Cost Explorer outage of December 2025, an internal coding agent deleted and recreated a production environment; AWS attributed the root cause to a misconfigured role, press reporting to the agent acting autonomously, and on either reading the permissions were broad and no breaker tripped (Singh 2025; Schuman 2026). And self-healing at startup replays ticks whose end was never recorded, which is safe only because the idempotency keys above absorb the replay. Sandboxing itself is thin from the harness's seat: it owns worktree isolation for correctness and admission control for capacity, and the sandbox primitive owns the rest, including keeping high-value secrets out of the box, by scoped-token exchange or egress substitution, so generated code cannot read or exfiltrate them (Chapter 56).

The harness matters because it is where the agent loop becomes operable. It buys capability by letting the loop run at full speed across stateful tools and composed agents. The price of that efficiency is context management and admission control, the machinery that keeps a runaway from burning the budget. Trust it earns in one place only: human authority over a running agent is exactly as real as the runtime's ability to pause it, which is a property designed in from the start or done without.

Constraint arrow

The harness moves an evaluation score as much as the model does. A benchmark number is produced by a model running inside a harness, and Berkeley RDI showed that top agent benchmarks can be gamed by exploiting the harness itself, reading answers from git log or reward-hacking the grader, with several benchmarks hitting near-100% without the agent solving the task at all (Berkeley RDI 2025). The exploit lives in the harness, not the task. The consequence reaches up into Chapter 47 and Chapter 52: a score is a joint property of the model and the scaffold around it, so a harness change is a measurement change, and the eval suite has to harden the harness, not just hold out a private answer key. A held-out set protects against memorizing answers; it does nothing against an agent that reads the grader.

Further reading

Cited sources appear on the book's References page. One item is referenced inline only as an attribution and is listed here for the reader who wants the primary artifact.

  • Invariant Labs, “mcp-injection-experiments,” 2025. github.com
    A GitHub repository providing code snippets to reproduce MCP tool poisoning and prompt injection attacks against AI agents.

Comments

Log in to comment