Most agents that die in production don't die because the model is bad. They die because the code wrapping the model (the harness) doesn't know when to stop, repeats the same error twenty times, duplicates a write after a retry, or asks the model to grade its own work. This module trains the eye for that boundary. The diagnostic that separates the people who ship from the people who demo is one question: did the model fail, or did the harness fail?

This isn't a fringe framing. Anthropic's "Building Effective Agents" describes agents as LLMs using tools in a loop, and spends most of its advice on the loop, not the model. The think → act → observe cycle (the technique that named this loop is An agent loop interleaving reasoning traces ('think') with environment-affecting actions ('act') and their feedback ('observe'); its append-only transcript adapts to dynamic tasks but causes context growth and error compounding on long horizons. glossary → ) is not the architecture. It's the iteration mechanism. What actually decides whether your agent works is two things: every step has to inject An observable external fact injected into the loop — a real tool result, a passing/failing test, a concrete error — as opposed to the model's own unverified self-assessment. glossary → from the environment (a real tool result, not a self-assessment), and the control flow has to live somewhere you own.

The model is a stateless reducer

Each turn, the model takes the current state plus the latest observation and produces a thought and a tool call. It holds no memory between turns, so you have to hold the memory for it. Said formally, the model is a Treating the model as a near-pure function from current state plus a new observation to the next action, holding no durable memory of its own so all persistent state lives outside it. glossary → : a nearly pure function (state, observation) → think + tool_call (read it as "give me where we are and what just happened, and I'll give you the next move"). The harness is the state machine that wraps it: invokes it, executes its tool calls, persists state outside it, and decides when to cut.

HumanLayer's "12-Factor Agents" names two distinct ideas here, and they get conflated constantly. Factor 12, "Make your agent a stateless reducer," is the shape of the model: a pure reduction from state to next action. Factor 8, "Own your control flow," is a separate factor about who drives the loop: you write the outer loop, you keep the kill-switch. Same spirit, two claims. Don't merge them.

turn.ts
// The model is the reducer. The harness is everything around it.
async function runTurn(state) {
  let s = state;                       // durable state lives OUTSIDE the model
  for (let i = 0; i < MAX_TURNS; i++) {
    const step = await model.reduce(s); // (state) -> think + tool_call  (Factor 12)
    if (step.done) return finish(s);

    const obs = await harness.execute(step.tool_call); // ground truth, not self-grade
    s = persist(apply(s, step, obs));    // checkpoint after each significant step
    if (loopGuard(s) || overBudget(s)) return stop(s);
  }
  return stop(s);                        // termination is a first-class decision
}

The model only lives on one line, model.reduce. Everything else is deterministic code you write, test, and instrument. That's the boundary the rest of this module defends.

The analogy: the model is the engine, the harness is the car

The model is a brutal but blind combustion engine. It turns fuel (context) into thrust (a tool call). It has no brakes, no steering wheel, no dashboard, no seatbelt. The harness is the whole car: the chassis that survives the crash (durable state), the brakes (stopping conditions), the steering wheel (deterministic control flow), the dashboard (observability), the airbag (human-in-the-loop). Nobody buys an engine. You buy a car. Yet almost every tutorial sells you the engine and calls it an agent.

Control flow: deterministic code vs delegated to the model

This is the most important architectural decision in the module. You don't hand the whole flow to the model. You write the outer loop and delegate to the model only the local decisions you can't hardcode ahead of time. Anthropic frames the same split as workflows vs agents: workflows follow predefined paths in code, agents direct their own process, and the production advice is to prefer the simplest thing and delegate the minimum.

What you can predict: persist, enqueue, take the lock, batch incoming messages, route approvals, count turns, enforce a budget, stop. Testable, instrumentable, kill-switchable. This is the bulk of a serious agent, and most of it is boring software done well.

What you can't hardcode: which tool to call given an ambiguous message, how to phrase a reply, what to search for. Probabilistic, not deterministic, which is exactly why you fence it in with tools and external verification.

A pattern worth stealing: keep the synchronous edge dumb. The HTTP edge that receives a message should never call the model. It persists and enqueues, full stop. A worker picks up the job, takes a per-conversation lock so two turns can't race, and only then runs the model. That edge/worker boundary is one of the most underrated control-flow decisions there is, and it almost never gets taught. It separates the cheap deterministic plane from the expensive stochastic one.

Ground truth beats self-reflection

There's a seductive cult around self-correction: the idea that the model can review its own reasoning and improve it. A technique where an agent verbally reflects on its own past failures to improve; fragile without an external truth signal because models tend to rationalize rather than correct. glossary → popularized this as the model keeping verbal notes on its own past failures. The evidence is blunter than the hype. Huang et al. (DeepMind, ICLR 2024) found that LLMs do not reliably self-correct reasoning without an external signal, and that intrinsic self-correction can degrade output, because the evaluator shares the generator's blind spots.

The design consequence is direct. Build your verification outside the model. Have the agent observe real filesystem or database results rather than asking it whether it found something. Grade with code-based checks that confirm the real effect, plus an LLM judge if you need nuance, but the model's own claim of success is not evidence.

Reward hacking and non-gameable graders

If your external verification is weak, the agent can "win" it without solving the task: overwrite the test, exit early to make the suite pass. DeepMind's MONA documents and mitigates this kind of long-horizon reward hacking. The implication: a grader must verify the real effect (the record actually written to the database) rather than the model's claim that it wrote it. Verify the world, not the narration.

Durable state lives outside the model

Because the model holds no state, all durable state (history, checkpoints, business state) lives outside it, in infrastructure that survives a crash. This is what lets you launch, pause, and resume a run, and it's what makes a turn replay-safe: if the model has no hidden state, you can reconstruct any turn from the external store.

A clean separation looks like this: a database as the source of truth (a pointer to the session plus a turn count), a session log as the worker's local record, and a materialized working directory as disposable per-turn cache. If the worker explodes, the cache regenerates and the session resumes. Nothing important ever lived only in memory.

Compact errors before re-injecting them

Raw errors (stack traces, verbose tool failures) are poison for the context window. They drown the signal and trigger repetitive loops: the model sees noise, doesn't understand the cause, retries the same thing. The harness should not be a pass-through for errors. It should be a translator that distills a failure into a compact, actionable line, and sometimes resets context instead of accumulating it. The practical pattern in hooks is "silent success, verbose failure": when something works, say nothing; when it fails, say exactly what broke and how to fix it.

Termination is design, not accident

When to stop is a first-order problem most tutorials ignore. There are two symmetric failures: non-termination (infinite loop, burned tokens) and early-stopping (the model declares "done" before it is). You need several conditions at once.

Termination needs several conditions, not one

A hard max-turns and a token budget are the easy two. The one people skip is the no-progress loop guard: hash the last N tool calls (name plus normalized args) and cut, or force a strategy change, when the same call repeats. Without it, an agent can burn a whole budget spinning before any coarse cap saves it. And a termination tool, an explicit "I'm done" or "escalate to a human" action, gives the model a clean way to stop instead of trailing off.

Human-in-the-loop is just another tool call

Contacting a human is not a special case bolted onto the side of the loop. It's another tool that pauses the reducer and waits for input. This unifies approvals, clarifications, and escalation under one mechanism. A common shape: reads auto-execute, writes get intercepted and surface a Confirm/Decline prompt. One elite detail worth copying: before showing the confirmation, validate and persist the normalized input, and have the gate return a denial so the model stops. When the human confirms, you execute that exact persisted input. That closes a semantic A class of bug where what was checked and what gets used drift apart between the two moments. In an agent: a human approves one action, but the model is re-invoked and runs a slightly different one. The fix is to persist the validated input at check time and execute that exact record, not a fresh model call. glossary → gap (time-of-check to time-of-use: the gap where what was approved drifts from what actually runs) where the model could "change its mind" between the prompt and the tap. What was confirmed equals what runs.

Idempotency: the failure nobody talks about

Agents have many failure points (orchestration, a probabilistic model, external tool calls, HITL pauses) that ordinary retries don't handle. The write that crashes halfway is the classic one. If a worker dies mid-write and the queue retries the job, without an A property where running an operation twice has the same effect as once — typically enforced by a deterministic key (e.g., from workflow_id + step_id, or a pending-action id) so retries after a crash, network failure, or queue redelivery are deduplicated rather than double-applied. glossary → key per confirmed action the retry duplicates the effect: two records where there should be one. Locks and session logs reduce the blast radius but don't guarantee this. Attach an idempotency key to every tool call with an external effect and you make the retry safe.

The thread through all of it is the same. Keep the model on its one line, doing the one thing it's good at: deciding the next action given the state. Push everything else, the boring durable software, into a harness you can read, test, and stop. The reliability you ship is mostly the quality of that boundary, and there's no clean point at which it's finished. Every production incident is one more rule the harness should have had.