Agents

Where to put the human

Gate placement for autonomous agents. Reversibility is the criterion, not confidence, and the consent surface matters as much as the question.

MobileByteSensei 8 min read
A long unbroken run of steps with a single gate near its end
agentsorchestrationguardrails

An agent asked me eleven times whether it should continue. It never asked before the one action that could not be undone.

Both halves of that are placement failures, and the second is the one that costs money.

The case this post is about

One irreversible operation and the sentence that was read as authorising it:

Ready to create ONE draft on Medium: <slug> — "<title>"
This cannot be undone (the API has no delete endpoint).

Run:  /idea-medium-post-draft <slug> --live

The instruction in play was write and publish my next post. That is a clear request, and it was written before the specifics existed — before there was a slug, a title, or a count of how many posts "next" covered.

So the agent does not perform the create. It stops and prints the command, and typing that command is the consent. This post follows that one gate, because the interesting failures are on both sides: a general instruction mistaken for approval, and a prompt fired so often it becomes a keystroke.

The symptom

Give an agent a long task and it will hit two failure modes, usually in the same run.

It stops constantly. This is a big change — shall I continue? Want me to commit? Should I run the full version or the quick one? Each question is individually polite and collectively ruinous: the point of delegating the task was not to be consulted at every step, and an operator who answers eleven prompts has done the work of supervising without the work being done.

And it proceeds silently through the one door that matters. A deploy that pushes a store listing. A create call against an API with no delete endpoint. A branch operation that discards untracked work.

The distribution is backwards. The questions cluster where the agent feels uncertain, and uncertainty is not correlated with consequence.

Why the obvious fix failed

The obvious criterion is confidence: ask when you are not sure.

It fails because an agent's confidence is about the local step, and the damage lives in the step's effect on the world. An agent is perfectly confident about POST /posts — the request is well-formed, the payload is right, it will return 201. Nothing about that confidence knows the API has no delete endpoint, so a wrong slug becomes a permanent public artifact that a human has to remove by hand in a web UI.

Meanwhile it is genuinely unsure whether a refactor is elegant, and that is where the question arrives.

The second thing I tried was a blanket dry-run default: nothing irreversible without an explicit flag. That is real protection and I kept it, but it does not answer the question — it moves it. The second --apply is exactly as destructive as the first, and by then the flag has become muscle memory.

The actual mechanism

The criterion that works is reversibility, and it is a property you can evaluate without knowing anything about the agent's internal state.

Four actions, sorted by whether the world can be put back

Three tiers fall out of it:

Reversible and local — file edits, refactors, regenerating an asset, running tests. No gate. The agent proceeds and reports. Git history is the undo.

Reversible but outward-facing — a draft, a preview deploy, a pull request opened as a draft. No gate to do it, a gate to finish it. The framework here opens upstream fixes as draft PRs and never merges them; the draft is autonomous, the merge is human.

Irreversible or public — a store submission, a create against a delete-less API, a force push. Gate, always, and the gate is not a question the agent asks itself.

The fix

The part I had wrong for longest was not where to gate but what counts as consent.

An agent running a broad instruction — "write and publish my next post" — can read that sentence as authorisation for the publish. It is not, and the reason is that a sentence is a bad consent surface for an irreversible act: it was written before the specifics existed, it is ambiguous under pressure, and the person who wrote it was describing a goal rather than approving an operation.

So the rule became: the agent never performs the irreversible step, whatever it was told. It stops and names the command:

Ready to create ONE draft on Medium: <slug> — "<title>"
This cannot be undone (the API has no delete endpoint).

Run:  /idea-medium-post-draft <slug> --live

Typing that command is the consent. It is unambiguous, it is scoped to one item, and it happens after the specifics are visible. A general instruction cannot be mistaken for it, because it is a different act.

What makes that hold under a broad instruction is that the gate is a property of the operation, not a decision the run makes about how confident it feels:

type Op = { name: string; reversible: boolean; blastRadius: number };
type Authority =
  | { kind: "standing-instruction"; text: string }
  | { kind: "explicit-invocation"; command: string; target: string };

function authorise(op: Op, auth: Authority): { allowed: boolean; why: string } {
  if (op.reversible) return { allowed: true, why: "reversible — no gate" };

  // A sentence written BEFORE the specifics existed cannot authorise an
  // irreversible act, however clearly it seems to ask for one.
  if (auth.kind === "standing-instruction") {
    return {
      allowed: false,
      why: `irreversible; "${auth.text}" predates the specifics — surface the command instead`,
    };
  }
  // An explicit invocation authorises ITS target and a blast radius of one.
  if (!auth.command.includes(op.name)) {
    return { allowed: false, why: `invocation names ${auth.command}, not ${op.name}` };
  }
  if (op.blastRadius > 1) {
    return { allowed: false, why: `blast radius ${op.blastRadius}; consent is per item` };
  }
  return { allowed: true, why: `explicit invocation for ${auth.target}` };
}

The publish path, under both kinds of authority:

const draft = { name: "medium-post-draft", reversible: false, blastRadius: 1 };
const batch = { name: "medium-post-draft", reversible: false, blastRadius: 84 };
const deploy = { name: "site-deploy", reversible: true, blastRadius: 199 };

console.log(authorise(deploy, { kind: "standing-instruction", text: "write and publish my next post" }));
console.log(authorise(draft,  { kind: "standing-instruction", text: "write and publish my next post" }));
console.log(authorise(draft,  { kind: "explicit-invocation", command: "/idea-medium-post-draft --live", target: "three-phases" }));
console.log(authorise(batch,  { kind: "explicit-invocation", command: "/idea-medium-post-draft --live", target: "all" }));
{ allowed: true,  why: 'reversible — no gate' }
{ allowed: false, why: 'irreversible; "write and publish my next post" predates the specifics — surface the command instead' }
{ allowed: true,  why: 'explicit invocation for three-phases' }
{ allowed: false, why: 'blast radius 84; consent is per item' }

The first line is the half people get wrong in the other direction: deploying 199 pages is a bigger operation than creating one draft and it needs no gate at all, because it can be run again. And the last line is why "publish them all" is not a consent surface — the invocation is real and the blast radius is not one, so the gate holds for a batch even though it clears for a single item.

The same shape covers the other gates. Which store-listing variant ships is chosen by a letter the operator types, never inferred — a release once pushed a template placeholder listing to a live store because that promotion step was skipped, and no amount of agent confidence would have caught it. Policy-bypass flags exist for the human: if a halt looks like it needs --force, that is the signal to ask, not to pass the flag.

How we proved it

The direct test is whether a broad instruction can reach the irreversible call. Given "keep writing the planned posts", the run that produced this article wrote four posts, generated twenty visual assets, regenerated the site, and stopped — handing back the command to type for each post it could have created remotely.

Four posts produced autonomously. Zero created on Medium. The one call that cannot be undone did not happen, and the instruction that would have been read as permission was the one that started the run.

And the negative case, which is the more interesting evidence — the gate that was missing:

HALT store-listing-not-promoted
  deployment-layer/store-listing/variants/  →  v1 | v2
  which variant ships? (operator)

The halt that now stands where a placeholder listing once got through

That halt exists because the deploy before it did not have one. The listing that reached the live store said App Toolkit and com.example.

The honest limitation: every gate here protects the operation. None of them checks that the operation is the right one. A create against the correct API with the correct payload and the wrong slug passes the gate, because the human typing the command is approving an act they have not fully verified either. Gates convert an automatic mistake into a manual one, which is better — the human sees the slug and the title — but it is not the same as correctness.

The failure that looks like caution

The opposite error deserves equal weight, because it is the one that makes people stop using the tool.

In an autonomous run, stopping to ask is also a defect. Not a lesser one, a different one. The rule here names the forbidden reasons explicitly — conservative pause, scope doubt, agent tired, this is a big change, shall I continue — and treats each as a failure of the run rather than a display of care.

The reasoning is that a question with no decision in it is not consultation, it is the work being handed back. If the answer is going to be yes, obviously, the question was a way of not doing the step. And a large task is not a reason to stop, it is a reason to break it up.

This cuts against the instinct that more confirmation is safer. It is not. Every unnecessary prompt trains the operator to approve without reading, which is precisely the state you do not want them in when the one that matters arrives.

Many prompts wearing down attention, against a single one that lands

What this generalizes to

Sort the actions by whether the world can be put back, not by how the agent feels about them. Run the reversible ones without asking, including the big ones. Gate the irreversible ones always, including the small ones.

Then make the gate a real consent surface — something the person does, scoped to the specific thing, after they can see what it is. A sentence written in advance is not consent, and a stream of confirmations is not safety; it is the same permission being granted eleven times by someone who has stopped reading.

One post a week, no digest

New writing when it lands. No roundups, no course pitch, no "10 tips".

Follow by RSS