ADR 0008 — Human-in-the-loop: escalate gates that suspend¶
Status: Accepted
Context¶
escalate routes to a handler state (SPEC §6) — fine for in-machine escalation
(e.g. a reasoning-tier retry), but "a human decides" needed a way to actually
pause. ADR 0007 shipped checkpoints and reserved reason: "escalated" in the
envelope for exactly this.
Decision¶
- Opt-in flag
escalate_suspendonrun()(CLI:--hitl, requires--checkpoint). When a firedescalategate hasto != END, the runtime sets the position to the handler state and suspends there — after the transition, before the handler runs. Same frames, same envelope (reason: "escalated", plus ahitl: truemarker soresumekeeps the behavior), exit code 3. - Reply injection:
mklang resume ck.json --set human.reply="…"writes into the innermost frame's context before resuming, so the handler state can interpolate{{human.reply}}. Library callers mutateframes[-1]["ctx"]directly. No reply is required — resuming without one just runs the handler as authored. - Default off: without the flag
escalateroutes exactly as before, so machines using escalate for tier cascades are unaffected even under--checkpoint. escalatetoENDnever suspends (nothing downstream could read a reply); model a final human sign-off as an escalate to a terminal review state.- Fan-out branches never suspend (as in ADR 0007): inside a branch the flag is forced off and escalate just routes.
Consequences¶
- A machine can park indefinitely on a human decision at zero token cost and
continue exactly where it stopped — nested
calls included. - The language is untouched: same schema, same
.mkfiles, spec stays 0.2. Whether an escalate pauses is a host/run decision, not an author decision; if per-gate control turns out to matter, ahitl:gate field is the obvious later extension. - The trace keeps a complete audit: the escalate step (with its
whenlabel), then the handler step whose context includes the injected reply.