When the Pipeline Should Ask Instead of Guess

7 MIN READ

A deterministic stage in the agent pipeline finished its work and got stuck. It had picked a chain of functions to thread a new flag through, but the entry point for that chain could be one of two HTTP routes. Both accepted the same request schema. Both reached the same downstream function. The ticket prose said “the bulk action,” which did not disambiguate, because both routes were bulk actions, one synchronous and one streaming. The stage had three options: pick one and hope, log a warning and continue with whichever it happened to pick, or stop and ask the person who wrote the ticket. The pipeline stops and asks, before any Coder agent runs or any iteration loop starts.

Most agent pipelines have an implicit contract with their input. A ticket comes in, code goes out. If the input is vague, the pipeline guesses. The guess might be right; if it is wrong, the cost surfaces later as a Coder iteration loop, a wrong-feature merge, or a Reviewer rejection that nobody reads in time. The contract change is small but consequential. The pipeline is now allowed to produce a ticket, OR a structured request for clarification. Refusing ambiguous structural work is a legitimate output.

The trigger for that refusal is not a bad ticket. It is a normal vague ticket, the kind every engineer writes when they are moving fast, on a deadline, without thinking through every implementation detail. Vague tickets are how requirements actually get written. An LLM coding agent that pretends those tickets are unambiguous is making a worldview error, not a parsing error.

An LLM coding agent that pretends those tickets are unambiguous is making a worldview error, not a parsing error.

The worldview error is the assumption that an LLM can take any English sentence and produce the implementation the writer had in mind. “Clone SpaceX, make no mistake” sits at one extreme of that assumption, but the same shape shows up at every scale. “Add a flag to the bulk action” can mean two different routes. “Improve the dashboard” can mean fifty different changes. The LLM cannot know which interpretation the writer wanted. The choice is whether to design for that, or to keep guessing and absorb the cost of guessing wrong.

What the structured exit looks like

When the deterministic stage detects multiple equally-plausible interpretations of vague prose, it raises a typed exception. The agent loop catches it and writes a JSON payload to the run directory. The exit code is non-zero but distinct from “pipeline error” so an orchestrator can route it differently.

{
  "kind": "ClarificationNeeded",
  "stage": "decomposition",
  "reason": "2 routes reach the chain leaf 'processItem' with matching body schemas: POST /actions (3 fields), POST /actions/stream (3 fields). Prose did not name which.",
  "candidates": [
    {"handler_symbol": "<route:POST /actions>", "body_field_count": 3},
    {"handler_symbol": "<route:POST /actions/stream>", "body_field_count": 3}
  ],
  "suggestion": "Name the route by method and path in the ticket prose."
}

The payload structure is deliberate. The reason field carries the per-candidate facts so a human reading the JSON can see exactly what the pipeline saw. The suggestion field is generic and project-language agnostic, because the same exit lives across TypeScript, Python, and Go projects. The candidates array carries the structured data an orchestrator can render into a Jira comment, a GitHub PR comment, or an n8n workflow node.

Why a warning is not enough

An earlier post, Why a Warning Is Worse Than a Hard Stop, argued that warn-and-continue produces output no downstream gate can catch. That argument was about a structural precondition (zero test files in the project) discovered before any agent ran. The clarification case is different. The ambiguity is discovered early in the pipeline run, by a deterministic stage, after the registry sync and intent extraction have completed but before any agent has written a line of code. The temptation to warn-and-continue is still real, because the pipeline has already paid for that setup work.

Anyone who has watched a Coder loop iterate on the wrong shape knows the asymmetry. The cost of stopping is one early exit after a single deterministic stage, with no code written and no branch to reset. The cost of guessing wrong and continuing is the full pipeline run plus a Coder iteration loop on the wrong shape, plus a Reviewer cycle on a feature the user did not ask for, plus the engineer-time to figure out which interpretation the pipeline picked and revert it. The stop with a structured payload is the cheaper failure mode, and because no Coder has run, the working tree is clean.

The other difference is the wire. A warning lives in stderr inside a Docker container; nobody reads it. A structured JSON payload at a known path with a distinct exit code can be picked up by the orchestrator that submitted the ticket in the first place. That orchestrator is the missing link. It is the thing that knows the ticket came from a Jira issue, a GitHub PR comment, an n8n workflow, or a Slack message. It can post the clarification request back to the same surface the ticket came from. A warning has no return address, but a structured payload at a known path does.

The structured payload is what makes the return path possible: the orchestrator picks it up and routes it back to whichever surface the ticket came from.

Flow diagram with three columns. The left column shows the agent loop. The middle column shows the structured exit, with two boxes: exit code 2 and clarification.json at a known run path. The right column branches into three orchestrator surfaces: a Jira comment, a GitHub PR comment, and an n8n workflow node. Arrows from the middle column to the right are labelled structured payload.

The honesty boundary

The pipeline can refuse structural ambiguity. It cannot currently refuse semantic ambiguity. “Make the dashboard better” is not a bad ticket. It is a ticket where the writer has deliberately left scope open, and there may be a valid implementation the pipeline should proceed with. The clarification path only fires when the pipeline has two or more concrete candidates that the prose does not select between. Past that line, the pipeline extracts the intent the LLM produced, decomposes it, and generates the ticket. If the intent was wrong, the cost still falls on the downstream stages.

That boundary could be pushed further. A semantic ambiguity check (one that detects underspecification rather than just misselection between concrete candidates) is a solvable problem with a different set of tools. The current deterministic gate cannot do it; it would require an LLM call against a structured representation of what the ticket is actually asking for. That extension is not built yet. Naming the limit explicitly is more useful than pretending the gate covers more than it does.

What’s coming next: refusing duplicate work

The same exit shape handles a sibling case I have not shipped yet. When the pipeline is asked to add a feature, a deterministic stage can check whether the feature is already in the codebase, by walking the registry for symbols, routes, or fields that match the ticket’s intent. If a strong match exists, the pipeline can stop and emit an AlreadyImplemented payload through the same wire. Same exit code, same JSON shape, different kind field. The orchestrator surfaces it to the ticket writer with the matching code locations attached.

That extension matters because duplicate-feature tickets are an underdiscussed cost in any team using AI coding agents at scale. A human reviewer would notice that the new ticket overlaps with code merged six weeks ago. An LLM coding agent without a check will happily build the second copy. The clarification exit is the right place to plug in the check, because the contract change (“produce a ticket OR a structured request”) already covers it.