Skip to content

The completion gate & guardrails

The loop stops when the model stops asking for tools. Nothing in that sentence checks whether the work the agent said it would do actually got done — so an agent can announce done over an unfinished plan, and the loop will believe it.

You can bolt a retry loop on the outside. But a host-side loop cannot follow a delegation: when agent A hands work to agent B via the task tool, B runs to completion inside the runtime and A’s caller never sees it. That is the gap this closes.

completion = { verify, maxAttempts }. When set, it runs at exactly the point the loop would otherwise report done. If the verifier fails, the loop hands the reason back to the agent and tries again — bounded by maxAttempts.

The built-in verifier reads the shipped todowrite builtin and requires every declared item to be checked. It is structural: it counts unchecked boxes and never learns what a todo means, so the loop stays domain-blind.

const shipper = agents.agent("shipper", {
does: "ships the release",
completion: { verify: agents.allTodosDone, maxAttempts: 3 },
})
const out = await shipper.loop(clientOptions, toolkit).run("Cut 0.15.0.")
if (out.status === "incomplete" && out.result.limit === "completion") {
console.error(out.stoppedBy) // "completion.verify failed 3×: 1 item(s) still open: proofread"
}

Every one of these was found by prototyping, not by design, and every one is tested in all seven ports. They are the difference between a gate and a suggestion:

  1. It judges accumulated work, not one attempt. Otherwise an agent escapes by simply not re-declaring its plan on the retry: the fresh run carries no todowrite, the verifier sees “no plan”, and passes.
  2. It never re-judges a run that stopped for its own reason. A suspension or a budget stop already carries its own reason, so the gate cannot override a budget stop or turn a pending into an incomplete. You can always tell whether you owe an answer or a fix.
  3. maxAttempts is required, not defaulted. An unbounded verify loop is a denial-of-service on your own bill.
  4. A failed gate stops loudlyincomplete, plus a structured limit: "completion" and a human reason. Never a silent done.
  5. When another limit fires mid-verification, you learn both. Otherwise a budget stop masks the verification failure and you never see why it was looping.
  6. It reaches delegated children, because it is compiled in at the registry boundary rather than wrapped around the caller.

Guardrails answer may it? — never is it right? They run before a tool executes and either allow the call or deny it with a reason.

They compose into a single beforeTool hook with first-deny-wins: a later guardrail can never widen an earlier denial, and any hook you already had runs only if every guardrail allows.

const ops = agents.agent("ops", {
does: "operates the fleet",
guardrails: [
(ev) => (ev.name === "deploy" && ev.args.env === "prod" ? "prod needs human approval" : "allow"),
(ev) => (ev.name === "bash" ? "shell is off in this agent" : "allow"),
],
})

A denied call never executes; the model receives the denial as the tool result and can react to it.

No completion and no guardrails is the pre-existing path, unchanged. Nothing was added to the status vocabulary either — SPEC.md pins the statuses across all seven ports, so the gate reuses incomplete and distinguishes itself through limit.

Next: proved against live models — the same mechanisms, run against four real providers.