2.6 Human-in-the-loop
We’ve reached the last failure mode, #6 — waiting for a human. Back in 2.2, the policy gate blocked a dangerous action and asked for approval. But how do you wait for a person? The naive answer breaks everything.
Why the obvious approach fails
Section titled “Why the obvious approach fails”The obvious approach: when the agent needs approval, just… wait. Hold the request open,
block the thread, await the human.
That fails twice. A held-open request ties up a server connection for however long the human takes — minutes, hours, a weekend. And the instant the process restarts (a deploy, a crash), the wait is gone — the agent forgets it was ever waiting, and the pending action is lost.
Approval is a workflow state, not a pause
Section titled “Approval is a workflow state, not a pause”The fix reframes the whole thing. Waiting for a human isn’t the agent pausing — it’s the workflow entering a first-class, durable state called suspended:
- When approval is needed, the harness suspends the workflow and checkpoints it to the durable log (2.1 again).
- Nothing is held open. No thread blocks. The workflow simply isn’t running.
- When a human finally approves (or rejects), the harness resumes the workflow from exactly where it stopped.
Because the suspended state lives in the durable log, it survives restarts. The human can take a week; the process can die and come back a dozen times. The approval is still there, waiting.
Watch it suspend, survive, and resume
Section titled “Watch it suspend, survive, and resume”Start the task. It’ll hit a payment that needs approval and suspend. Before you approve, press Simulate restart a few times — the process “dies and comes back a day later” — and notice the pending approval is still there. Then decide.
The workflow sat in SUSPENDED, survived every restart, and resumed the moment you
decided — running the payment on approve, skipping it on reject. No thread was ever held
open.
What that looks like in code
Section titled “What that looks like in code”async function runStep(call) { if (policy.check(call) === 'needs_approval') { await suspend(workflowId, { awaiting: call }); // checkpoint + stop running return SUSPENDED; // no thread held open } return run(call);}
// Later — could be seconds or days, in a totally different process — a human decides:async function onDecision(workflowId, decision) { const state = await load(workflowId); // recovered from the durable log if (decision === 'approve') await resume(workflowId, run(state.awaiting)); else await resume(workflowId, skip(state.awaiting));}suspend and resume are two halves of the same durable machinery from 2.1. Approval
didn’t need a new mechanism — it needed durability applied to waiting.
In production
Section titled “In production”- Durable-execution engines (Temporal, DBOS, Inngest) expose exactly this: signals/waits that suspend a workflow and resume it on an external event — human approval, a webhook, a timer.
The harness, assembled
Section titled “The harness, assembled”Look back at what you built. Every failure mode from 1.2 now has an answer, and the answers reuse each other:
| Failure | Fix | Builds on |
|---|---|---|
| Crash loses progress | Durable execution (2.1) | — |
| Tools can do anything | Sandbox + policy (2.2) | — |
| Context grows forever | Memory & hydration (2.3) | 2.1’s log |
| One agent guesses wrong | Routing & handoffs (2.4) | — |
| Serial & fragile | Supervision (2.5) | 2.2’s isolation |
| Waiting on a human | Human-in-the-loop (2.6) | 2.1 + 2.2 |
That interlocking set of capabilities is the harness. The model still just decides the next step — everything that makes it survive production is the layer you built around it.