Chapter 5 of 7 · 8 min read
Deferral fails when the agent cannot act without what you moved
On this page
The deploy-preview-env skill has a 93% selection rate on the requests it should handle and a 31% completion rate on the runs it selects. The transcripts of the failures are almost identical to each other. The agent reads the request, selects the skill, receives the body, states a plan, runs the first two steps — clone the manifest, patch the image tag — and then issues:
POST /v1/preview-envs HTTP/1.1
Host: api.internal
Authorization: Bearer sk-agent-3f9c...
Content-Type: application/json
{"manifest": "...", "ttl_hours": 24}and receives:
{"error": {"code": "insufficient_scope",
"message": "token lacks preview:write",
"request_id": "req_01JC8Z"}}The agent then does something reasonable and unhelpful: it retries once, reports that it lacks permission to create preview environments, and suggests the user check with the platform team. Every visible instruction was followed. Nothing in the trace looks like a defect.
The body ends with this line, in position 14 of 14:
Prerequisites and first-time setup: see `setup.md`.setup.md says to mint a scoped token with authctl issue --scope preview:write --ttl 1h and to export REGION to match the target cluster, because the API infers the cluster from the region header and defaults to us-east-1, which is not where preview environments live. Both of those are conditions the run has to establish before step one. They were placed behind a fetch, on the reasoning that setup is reference material, and on 69% of runs the fetch did not happen.
The layer that failed, and how to be sure
The tempting reading of that trace is that the model skimmed. It read a line pointing at setup, judged it optional, and pressed on — which sounds like a compliance problem and invites a compliance fix. That reading gets the layer wrong, and the fix that follows from it fails in a way that is worse than not fixing it, because it moves the number a little.
The precise statement is that the agent could not comply with a constraint it never read. The bytes of setup.md were never in the model's context on those 69% of runs, so no property of how the run treated the pointer changes what the run knew. Putting MUST in capitals at the top and bottom of the body raises the fetch rate, the share of runs that select a skill and go on to issue the read its body asks for; on this skill the rate went from 31% to 58%, which is enough to make the completion metric visibly improve, enough for the change to be merged as a success, and not enough to stop 42% of runs failing for the original reason. Emphasis on a pointer is a probabilistic nudge applied to a boundary that needed a structural change.
Distinguishing this from a routing failure takes three observations from the trace, and they are all cheap to collect:
Only the rightmost leaf is a compliance problem, and it is the rarest of the five. The deploy-preview-env failures all landed on the E branch: skill named, body present, no Read call for setup.md anywhere in the turn. That is a mechanical query over stored traces rather than an interpretation of them, which matters because the interpretations people reach for by default are the two leaves at the extremes.
The same boundary error wears a second costume that is harder to spot, because the run succeeds. A skill for writing API changelog entries selects reliably, its body loads, and in roughly a third of runs the agent produces an entry that ignores the format the skill exists to impose — no BREAKING: prefix on breaking changes, version ranges written as prose rather than as the semver interval the downstream parser expects. The body is short and ends with see examples/ for the required format. Nothing errors. A file is written, a human reviews it or does not, and the parser fails a week later on a batch import. The trace query returns the same answer as the deploy case: body present, no Read for examples/format.md on 29% of runs.
Two remedies, and what each one charges
There are two structural fixes and they charge different people. The first is to inline the material into the body, which means every run that selects the skill pays for it whether or not that run needed it. The second is to make the fetch an explicit numbered step in the procedure rather than a closing pointer, which means the read has an ordinal, a position before the first action, and a stated dependency:
## Steps
1. Read `examples/format.md`. Do not draft the entry before you have it;
the version-range syntax cannot be inferred from the examples in step 4.
2. Determine whether the change is breaking (see the decision table below).
3. ...The changelog skill was measured both ways over 2,900 selected runs a day. The body was 620 tokens; examples/format.md was 480.
| pointer in closing line | fetch as numbered step 1 | inlined into body | |
|---|---|---|---|
| format constraint honoured | 71% | 93% | 100% |
| tokens per selected run | 620 + 480 on 71% = 961 avg | 620 + 480 on 93% = 1,066 avg | 1,100 flat |
| p50 latency, selected runs | 2.41 s | 2.68 s | 2.03 s |
| residual failure | 29% silently wrong | 7% silently wrong | none at this boundary |
| what it costs | nothing new; it is the status quo | a round trip on runs that now fetch | 480 tokens on runs that select for another reason |
Inlining wins here on every column including latency, and the latency result is the one worth pausing on, because it inverts the assumption that deferral is a performance optimisation. The 71% of runs that were already fetching correctly were paying a full round trip — a second model call, its own time to first token, its own decode — to save 480 tokens of prefill. On this deployment a round trip cost about 540 ms at p50 and 480 tokens of additional prefill cost about 20 ms. Deferring content that most runs need is a 27:1 latency trade in the wrong direction, and the runs that were "already fine" get faster when you stop deferring, rather than paying for the fix.
That result does not generalise to every skill, and the quantity that decides it is the fetch rate under a correct implementation. If 93% of selected runs genuinely need the file, inline it. If 8% need it — the 38-code ledger error table from the refund skill, consulted only when a call returns 409 — deferral is buying a real saving on 92% of runs and the round trip lands only where the content is genuinely required. The crossover on this platform, working from a 540 ms round trip against roughly 40 µs per token of prefill, sits somewhere near a 40% fetch rate, which is a number each deployment has to compute for itself from its own two measurements rather than inherit.
The numbered-step remedy keeps one property the inline remedy does not, and it is the reason to prefer it in a narrow case. A skill that serves several distinct tasks, only one of which needs the 3,000-token schema, can name the fetch as step 1 of that branch and charge only the runs that enter it. What it leaves behind is the residual 7%: the read is still an instruction the model may skip, and steps early in a numbered list are followed far more reliably than closing lines but not always. That residual is not reducible by better wording. It is the honest cost of the boundary, and a skill whose correctness depends on a step being followed 100% of the time should not have that step be a read.
The case for keeping prerequisites in the body specifically
Prerequisites are the sub-case where the choice is not really open. deploy-preview-env was fixed by moving both preconditions into the body as a numbered step 0, with the exact command:
## Step 0 — establish preconditions
Run these before anything else. The API rejects the create call without both.
authctl issue --scope preview:write --ttl 1h
export REGION=eu-west-1 # preview clusters do not exist in us-east-1
Confirm `authctl whoami --scopes` lists `preview:write` before continuing.That is 84 tokens, paid on every run that selects the skill, and completion went from 31% to 96%. The remaining 4% were environments where authctl was not installed, which is a different failure and produces a loud error at a known step.
The general shape is that a prerequisite establishes state the rest of the instructions assume. Reference material is consulted; a prerequisite is executed, and the steps that follow are written as though it already happened. A run that skips a lookup notices, because it reaches the point where it needs the lookup and has nothing. A run that skips a prerequisite does not notice, because every subsequent instruction reads as though it is possible. That asymmetry is what makes deferred prerequisites produce runs that follow fourteen steps correctly and die at the first call, and it is why the size of the prerequisite is irrelevant to where it goes. Eighty-four tokens or eight hundred, it is the state the body presumes.
Finding these before they reach production is a review question rather than a tool. For each file a body defers, ask what a run does after skipping it: if the answer is that the run stops at a known point and reports that it needs something, the deferral is safe, because the failure is loud and lands on the person who can fix it. If the answer is that the run proceeds and produces output, the file is a candidate for the body. Applied across the platform team's registry, that question flagged nineteen skills, of which eleven were deferring credentials, region or environment selection, or an output format — and all eleven had been written by authors who thought of that material as setup notes rather than as steps.
What this is not
This is not the argument that bundled files are a mistake. The ledger error table, the six unusual output shapes, the API surface consulted once a quarter — those are consulted for a subset of cases, a run that needs one knows it needs one, and deferring them is what makes a large skill affordable. The failure in this chapter is specific to material the run cannot act without, moved outward on a criterion that did not ask whether the run could act.
It is also not a permissions or packaging problem, and the diagnostic flow above separates them deliberately. In every trace here the file existed, was readable, and would have returned its contents had the read been issued. The model simply did not issue it. That is a different defect from a read that was issued and came back empty, or came back with the contents of some other directory, which produces its own species of failure and its own reason for the run to look successful.
Which is the case worth taking next, because it is the one where the check you would naturally write does not catch it. A read that resolves against the wrong directory fails on a colleague's machine and not on yours, and the run that fails often reports no error at all.
Progressive disclosure — read the current version at https://daviddominguez.dev/learn/en/progressive-disclosure/deferral-fails-when-the-agent-cannot-act-without-what-you-moved