Loop Engineering: Admission and Stopping
A loop is not a prompt on a timer: seven admission conditions, five layers of stop semantics, one inbox continuation rule. Idle is not done.
A loop is not "a prompt on a timer." A scheduled call has no persistent cursor, no idempotency key, and no independent verification; when it fails it can neither resume nor be audited — it does not constitute a loop. Real loop engineering answers two question groups at once: what earns the right to loop (admission), and what counts as stopped (stop semantics). while (hasToolCalls) is enough in a demo and not enough in production: the end has to be confirmed separately at several different places.
The harness owns one run; the loop owns many
Draw the boundary first. The harness defines the capabilities, constraints, and recovery of a single run; the loop manages triggering, state, verification, and continued improvement across runs. A complete loop takes five steps: Discover → Execute → Verify → Persist → Schedule — find work, execute, verify the result, persist state outside the conversation, schedule again dsh-pi-stop.
Two hard rules sit inside it: execution state lives outside the conversation (in-memory state dies with the process), and every work item carries a stable idempotency key (re-triggering the same item must not duplicate side effects). The loop is also forbidden from editing its own P0 — letting it rewrite its own approval rules and safety boundaries hands the brakes to the accelerator agent-trend-2026.
Seven admission conditions; miss one and you step back
| Condition | Requirement |
|---|---|
| Recurrence | The task appears periodically or is triggered by stable events |
| Verifiability | Results are auto-checkable, readable from environment state, or gated by a human |
| Actability | The agent has controlled read, modify, test, and deliver capabilities |
| Recoverability | Execution can be isolated, retried, rolled back, stopped |
| Economics | Per-round cost and cost-per-success are budgetable |
| Governability | There is an owner, permissions, audit, a kill switch, an escalation path |
| Sedimentability | A failed loop should still leave fixtures, rules, or skills — not just a chat log |
If any critical condition is missing, fall back to a single-run agent, a workflow, or a human process. This table is the same kind of instrument as the gates in When Multi-Agent Actually Pays: not a discouragement, but a demand that you answer every row before you build. Past admission sits a second question — what goes inside the loop and how it breaks, covered in Loop Engineering: The Control Surface and Its Failure Modes.
"Stopped" is five different things
One real incident lays all five layers on the table: an agent takes a ticket and creates a PR, then hits a 504 — the page shows no PR, but the server may have created one; a blind retry might create a duplicate PR, and stopping right there might bury a real failure; the PR exists but CI is still queued — does "the model said done" count as done dsh-pi-stop?
At moments like that, "stopped" has to be confirmed layer by layer: (1) model stream ended — only means this request received an end signal, which could be max-tokens, an error, or a cancel; (2) tool batch returned — tools stopping does not mean the turn stopped, the model still has to read the results; (3) turn ended — tool results delivered, no pending replies, inbox empty; (4) driver idle — no next turn for now; (5) goal ended — persistent state settles to complete / blocked / paused. Idle is not done — the outer goal driver's next check may still send a followup and wake it back up. When everything goes smoothly the layers are indistinguishable; the moment a 504, a cancel, a process crash, and a queued CI land together, the logs cannot say "which layer stopped here," and recovery turns into guessing.
Continuation goes through a message queue, not merged booleans
Several plugins and hooks all want to say "not yet" — how do you merge that? Boolean callbacks are the worst answer: a pile of should_continue flags fighting each other, nobody able to say who continued the run and when. The right design routes continuation intent through a message queue: a plugin that wants to continue writes a message to the next step's or turn's inbox, and the core loop rereads the inbox after the stopping hook, then decides stop or continue dsh-pi-stop.
Two payoffs: multi-plugin continuation needs no boolean merging, and every continuation intent leaves an auditable record — when something breaks you can find "which plugin slipped one more message in after the turn ended." This is the same design as events consumed only at safe points: the queue is read at boundaries, never mid-execution.
Record end reasons separately; truncated arguments are not executable
Each layer's end reason is recorded independently, in six kinds: completed / blocked / max-tokens / aborted / error / interrupted. The two easiest to confuse: aborted is voluntary cleanup after a cancel; interrupted is the state the recovery layer fills in after the process vanished — side-effect audit and retry decisions differ completely, and mixing them leads you to "retry the PR that may already exist." Turn-level end reasons survive to the turn's end: step 1 hit max-tokens and later steps finished the work, and the turn still records max-tokens; watching only the last step reads a truncation as success dsh-pi-stop.
One more safety line for writes: max-tokens truncation must happen before tool argument parsing. A half-finished JSON that a parser completes and a schema validates may already mean something else — once a write's side effect lands, there is no taking it back. When state is unclear, stop first and do not retry blind; that judgment returns to classify failures before counting them: classification first, and only then does the retry decision have grounds.
