The Composable Harness: How Agents Actually Get Reliable
The loudest debate in agent infrastructure is thin harness versus thick harness: how much the runtime should do for you. It is the wrong debate. Step back and ask what a backend even is, and the answer is small: workers that orchestrate, triggers that invoke them, and functions that do the work. An agent is a worker. Its tools are functions, its memory is state, its orchestration is triggers. Once you see that, the harness stops being a layer bolted on top of the backend and becomes the backend, and composability stops being a design preference and becomes the mechanism that solves the hard agent problems.
The tax composability removes
Agent systems fail in a way normal backends do not, and the reason is combinatorial. A traditional backend is deterministic, so a bug lives on one path you can reproduce. An agent is stochastic on purpose, which is the entire point of using one, so the number of paths through the system multiplies rather than adds. Put a few agents over a handful of services and the failure surface scales closer to the number of agents squared times the number of services. One agent over five services is a few paths. Four agents over five services is dozens.
Most of that pain lives in the seams. Four services have six integration edges between them, twenty services have a hundred and ninety, and every edge is code you write, logs you correlate, and a page you take at 3am. When the harness retries on its schedule, the queue retries on its conditions, and the HTTP layer times out on its own, there is no single trace tying them together. You debug by reconstruction.
Composability, done right, deletes the seams instead of managing them. If every capability joins the system the same way, by connecting to one bus and registering what it can do, a new worker becomes callable by every other worker with no integration code between them. The hundred-and-ninety-edge graph collapses into one bus with twenty workers on it. The integration code does not get cleaner. It stops existing.
Three primitives, one bus
The whole model is three things and one substrate they run on.
Function
A unit of work with a stable string id like orders::validate or state::set, and a typed input and output schema the runtime validates before your code runs and before the result goes back to the caller. A function can live in any process, in any language. It calls another function by id and lets the engine handle routing and delivery.
Trigger
What causes a function to run, declared rather than wired. The same function can be invoked by a direct call, an HTTP route, a cron schedule, a queue message, or a change to a piece of state, and it does not change when you add a new way to invoke it. Triggers compose onto a function instead of being tangled into it.
Worker
Any process that connects to the engine and registers functions and triggers. A TypeScript API is a worker, a Python pipeline is a worker, a Rust service is a worker, and an agent is a worker. There is no privileged agent category.
The engine
Not a framework you link against, but a message bus with a live registry. It holds the connections, knows every function that exists, routes calls from worker to engine to worker, and propagates one trace across all of it. Because everything crosses this one bus, discovery, routing, and observability are properties of the wire, not features anyone bolts on later.
The payoff of collapsing everything to these primitives is that the agent's special parts stop being special. The tool the model sees is a function's input schema. The tool result is that function's typed output. Memory is reads and writes against a state worker. Orchestration is triggers and composition. There is no separate agent infrastructure because there does not need to be.
The loop is a durable state machine
Here is where the reframe earns its keep. In a framework, the agent loop is a while loop in a process, and when that process dies the turn dies with it. When the harness is the backend, the loop is a durable state machine and every step of it is a message on a queue, keyed by session so one long turn never blocks the others.
That single change buys crash recovery for free. If the process dies mid-turn, the queue holds the step and another worker picks it up where it left off. Redelivery is made safe by construction: a stale-step guard drops steps from an older version of the turn, message ids are derived deterministically so appending the same assistant message twice is a no-op, and each tool call carries a checkpoint so a call that already fired is never fired again. The contract that falls out of this is the one sentence to remember: step delivery is at-least-once, but function side effects are at-most-once. That is durable execution, the exact discipline queues and workflow engines have used for twenty years, applied to an agent turn.
Reliability was never a model problem. It is a distributed-systems problem the loop had been hiding.
What composability actually solves
This is the part a generic take on modularity skips. Each of these is a real agent failure, solved by a named capability that is a separate worker, not a feature inside a monolith.
Crash-resumable turns
The queue plus a durable turn record plus idempotent steps, described above. Restart the process and the turn continues from its last checkpoint instead of starting over or losing work.
Provider swap and failover
A router worker is the single front door to every model provider, and each provider is its own worker that registers itself at runtime. The loop calls one chat function and never talks to a provider directly, retries live in the router behind one failure contract, and adding or removing a provider is adding or removing a worker. Nothing recompiles.
Dev-to-prod transports
The queue, the state store, and pub/sub each pick their backend from config. An in-memory queue on a laptop and a Redis or RabbitMQ queue in production expose the identical function ids, so the loop is unchanged between them. Worth being honest here: the backends are not feature-identical, and a lighter transport can drop retries or durability, so what stays constant is the wire contract, not the guarantees. Choosing the backend is a deployment decision rather than a code change.
Human-in-the-loop approval
When a policy check says a call needs approval, that one call parks and the rest of the batch keeps moving. The turn wakes through a single state-change trigger: the console writes the decision to state, that write fires the trigger, and the parked turn resumes. There are no per-call resume handlers to register and nothing to re-scan on restart. To move approvals into Slack you write a Slack worker that calls the same resolve function; the orchestrator never learns the difference, because you added a worker rather than replacing one.
Sub-agents that do not collide
A sub-agent is an ordinary turn in a child session on the same queue, so children run in parallel across sessions without sharing memory. Two guards make this safe: a child's permissions can only narrow what the parent already allowed and never escalate, and depth and fan-out are bounded so a spawn storm cannot run away. The month-six failure of handing a sub-agent the parent's entire history simply does not arise, because a child session starts fresh unless you explicitly fork.
A tool surface that fails closed
Every tool call passes one chokepoint that checks a policy worker with a short timeout. With no allow rules the loop is a plain chat with no tools, and if the policy worker is unreachable or slow the call is denied, not allowed through. Swapping the policy engine for your own rules is registering the same policy function from a different worker.
Durable memory with provenance
Memory is a worker that writes one flushed line to disk before it touches RAM and supersedes rather than deletes, so any prior state is recoverable and there is no shutdown flush to get wrong. Every write carries the source that made it, which turns a sensitive field changing hands mid-session into an event the memory worker can reject or alert on. Provenance becomes a property of the call, not a convention someone has to remember.
One trace across the whole turn
Because every call crosses the engine, the runtime carries one trace id through the model call, the tool that call triggers, the downstream function that writes state, and every sub-agent underneath. A regression like a provider quietly shortening its reasoning shows up as a shift in a span distribution rather than a mystery you reconstruct from timestamps across three systems.
A loop that knows how to stop
The classic runaway is a plan-act-critique loop whose critic never says good enough, and it burns tokens until something breaks. The fix is not a cleverer prompt. It is a scheduled sweep that resolves any call parked past its timeout so a turn can never wedge, plus hard caps on turns, depth, and fan-out. In agent vocabulary that is a stopping condition. In engineering vocabulary it is a circuit breaker backed by a cron job.
Thin versus thick is a slider
Return to the debate we started with. Once every capability is a separate worker, thin versus thick is not a philosophy you commit to, it is a count of how many workers you install. Thin is an orchestrator and one provider for an autonomous research loop. Thick adds approval, budget caps, custom policy, history compaction, and a Slack approval surface for an audited customer workflow. The distance between them is editing a config file, not rewriting a codebase. A framework picks a point on that slider and locks you at it. Keeping the capabilities as workers leaves the slider in your hand.
The substrate is installable
The part that makes this more than an architecture diagram is that the pieces are installable, not just conceptual. Adding a capability is one command that pulls a running service from a registry, connects it, and registers its functions, rather than importing a library and writing a client. Installing the harness pulls the whole dependency set it needs, the queue and state and router and session and context workers, in one step, because the harness declares them. New capability, add a worker. The neat consequence is that an agent can extend its own environment at runtime with the same command a human would use, because installing a worker is just another function call on the same bus.
What it costs
Composition is not free, and the honest version of this argument names the bill. Every call that used to be an in-process function is now a hop across the engine, so a microsecond call becomes a low-millisecond one; that is the price of making it observable, routable, and language-agnostic, and it is real. You need a shared wire protocol that stays stable, because a contract everything depends on cannot churn. Two workers cannot both own the same trigger type, so the last one to register wins and workers refuse to boot against a conflict, which is a coordination cost you have to design around. And where the engine lacks a primitive, the gap leaks into your topology: without a compare-and-swap in the state store, you shard work by key instead of relying on optimistic concurrency. These are the trades. For a weekend prototype the monolith is fine. For a system you will run and change for years, paying a fixed protocol-and-observability cost once, in exchange for every capability being independently replaceable, is the better deal.
The durable artifacts here are the functions, the triggers, the workers, and the traces. The loop layout, the prompt, and the graph are scaffolding you throw away when the model or the task changes. The scaffolding is disposable. The substrate is what you keep, and building it out of composable workers is what turns an impressive demo into something you can operate.
FAQ
What is a composable harness? An agent harness built from the same primitives as a backend: workers that register functions, triggers that invoke them, one engine that routes between them. The agent is a worker, tools are functions, memory is state. Approval, memory, budgets, and sub-agents are separate workers you add, not features inside one framework.
Why is the harness the backend? Because the properties that make an agent reliable, retries, durable state, timeouts, tracing, are backend properties. Deconstruct a backend into workers, triggers, and functions and an agent is a worker using the same primitives, so there is no separate agent layer to bolt on.
How does it survive a crash mid-turn? Each loop step is a message on a durable queue keyed by session. If the process dies, the queue redelivers and another worker resumes. Deterministic ids and per-call checkpoints keep it safe: step delivery is at-least-once, side effects are at-most-once.
Thin or thick harness? Neither is a fork. It is a count of workers. Start with an orchestrator and one provider, add approval, budgets, policy, and compaction as workers when you need them. The distance is a config change.