Chapter 4 of 7 · 9 min read
Over-passing is the invisible failure, not the safe one
On this page
The card number turned up during a PCI scoping exercise, the periodic exercise of establishing which systems fall under the Payment Card Industry Data Security Standard because card data can reach them. Someone was tracing which systems could see a primary account number and found 4539 8812 XXXX 7761 sitting in the execution agent's prompt, inside a retrieved record from the billing system, inside a payload the planner had sent it. Nobody had designed that. The planner retrieved the record because it needed the customer's subscription tier, the record happened to include stored payment details, and the payload carried every retrieved document.
The first proposal was a redaction filter: scan outbound payloads for card-shaped strings and mask them. It would have worked, in the sense that the scoping exercise would have come back clean. It also treats one field as the problem when the payload it sits inside is 11,400 tokens of which the executor needs perhaps 900, and it leaves in place the arrangement that put the card there — a boundary with no rule about what crosses, where the presence of any given item is a consequence of what the sender happened to touch.
Everything in the payload, and what it does
Here is the payload as it stood, item by item, with sizes measured across 200 recorded runs at the median.
| item | median tokens | changes the executor's next action? | what it costs to send |
|---|---|---|---|
| the plan (ordered steps, parameters) | 310 | yes, it is the instruction | — |
| constraints the customer stated | 95 | yes, they bound every step | — |
| subscription tier, account status | 40 | yes, gates two of the steps | — |
| full customer conversation (14 turns) | 4,180 | rarely; the constraints are already extracted | 4,180 tokens, and any personally identifiable information (PII) in it |
| all retrieved documents (9) | 3,900 | one of nine did; the rest are background | 3,900 tokens, PCI scope, retrieval noise |
| planner's reasoning trace | 2,400 | partly, and that part is the difficulty | 2,400 tokens, plus anchoring |
| stored card details | 30 | no | audit scope, breach exposure, retention duty |
| open questions and their blocking status | 110 | yes, one blocks step 3 | — |
| owner, correlation ID, schema version | 45 | yes, routing and traceability | — |
The four rows that carry the weight — conversation, documents, trace, card — total 10,510 tokens and answer "yes" to the question in the middle column for a total of about 200 tokens' worth of content buried inside them. That is the shape of over-passing: it is not that the extra material is useless, it is that the useful fraction of it is small, unmarked, and mixed in with material that looks identical.
The rule that resolves this is that only what changes the receiver's next action crosses the boundary. It is a demanding rule because it requires the sender to have a view of what the receiver does, which is a coupling some teams resist on architectural grounds. That coupling is unavoidable — a sender that does not know what its receiver needs cannot construct a useful message — and making it explicit in a projection function is better than leaving it implicit in a dump.
def executor_payload(state: PlannerState) -> Handoff:
"""Everything here is here because a step depends on it."""
return Handoff(
schema_version="1.1",
correlation_id=state.correlation_id,
goal=state.objective,
plan=state.plan,
constraints=state.constraints, # extracted, not the transcript
evidence=[e for e in state.evidence
if e.claim_id in state.plan.referenced_claims],
open_questions=[q for q in state.open_questions if q.blocks],
account=AccountFacts( # a projection, not the record
tier=state.account.tier,
status=state.account.status,
),
owner="executor",
returns_to="planner",
)The card never reaches the filter, because AccountFacts names the two fields the executor uses and there is no path by which anything else in the billing record travels. Redaction is a filter applied to a payload that should not have contained the field; a projection is a payload that cannot contain it. The distinction matters in review, because data minimisation is an argument about what you collect and transmit, and answering it with a masking rule concedes the point that the field was legitimately there.
A projection is only as narrow as the discipline that keeps it narrow, and the pressure on it never stops, because every time the executor gets something wrong the cheapest available fix is to add a field. Two things hold the line. The first is that the projection function is the only place a payload is constructed, so a field that is not named in it cannot cross, and a reviewer reading twenty lines can see the entire boundary rather than inferring it from wherever the sender happens to assemble state. The second is a test that serialises the payload and asserts that no key outside an explicit allowlist appears anywhere in the tree, run against fifty recorded planner states rather than a hand-written fixture, since the failure being guarded against is precisely a nested record that gained a field upstream. That test earned its keep four months after the scoping exercise, when someone under time pressure passed state.account whole instead of building AccountFacts, and the card came back. It cost two minutes in CI rather than a quarter of audit remediation.
The reasoning trace is the hard case
Everything above is easy except one row. The planner's trace — 2,400 tokens of "I considered refunding the full amount, but the customer's tier caps automatic refunds at $200, so I checked whether a partial refund plus a service credit would satisfy them, and the credit ledger shows an existing credit that expires in nine days" — contains material of two kinds, and the rule cuts between them rather than around them.
The constraints the trace discovered change the next action. The $200 cap, the nine-day expiry, the fact that the planner already checked the ledger and found something: an executor that does not know these will either redo the work or violate the cap. Those belong in the payload, as constraints and evidence, stated as facts with sources.
The derivation does not change the next action. That the planner first considered a full refund and rejected it, in what order it checked things, which branch it abandoned — none of that alters what the executor should do now, and sending it has an active cost beyond the tokens. A model given another model's reasoning trace tends to continue it rather than evaluate it. In the recorded runs, when the trace was included the executor reproduced the planner's mistake in 7 of 7 cases where the planner had misidentified the subscription tier; with the trace removed and the tier passed as a stated fact with its source, the executor caught the mismatch and raised it in 3 of those 7, because the fact contradicted what it read from the account and there was no narrative explaining the contradiction away. Shipping the derivation conditions the executor on the planner's path instead of informing it, which also means your executor evaluations stop being independent of your planner's quality.
The counter-argument is that the trace sometimes contains something nobody thought to extract, and that is true. The response is not to keep the trace but to treat each occurrence as a missing field, the same way the narrative field from chapter 2 works: if the executor needed something from the trace three times this month, that something is a field the projection is missing.
Finding out whether the trim cost anything
The reasonable worry about all of this is that the executor will miss something the payload used to carry incidentally. That worry is answerable with an experiment rather than an argument, and the experiment is cheap because the runs are already recorded, each one alongside its ground truth — the outcome a person established afterwards as the correct one for that case, which is what any grading run is scored against.
# replay 500 recorded runs under both payload constructions
for run in recorded_runs(n=500):
full = legacy_payload(run.planner_state) # 11,400 tokens, median
trimmed = executor_payload(run.planner_state) # 900 tokens, median
for variant, payload in (("full", full), ("trimmed", trimmed)):
result = executor.run(payload, tools=SANDBOXED_TOOLS)
record(run.id, variant,
outcome=grade_against_ground_truth(run, result),
requested_more=result.requests, # executor asked for a field
steps=len(result.actions),
tokens=payload.token_count,
latency_ms=result.latency_ms)The requested_more channel is what turns this from a benchmark into a diagnosis. Give the executor a tool that says "I cannot proceed without X" and count what it asks for. A trimmed payload that is missing something produces a specific, named request — need: prior refund history for this account — rather than a quiet degradation in quality that a success metric might not resolve at n=500. Across the 500 runs the executor requested additional material 23 times, and 19 of those were the same field, which became the tenth line of the projection function. The remaining four were the conversation transcript on runs where the customer had said something ambiguous that the planner's constraint extraction had flattened, and the answer there was to fix the extraction rather than to restore the transcript.
Five hundred runs resolves a difference of several points and does not resolve a difference of one, which is worth settling before reading the table below as though every row were decisive. At a success rate near 90%, the standard error on each proportion is about 1.3 points, so an unpaired gap of 2.8 points sits inside the noise; grading the same runs under both constructions tightens that, because only the runs that disagree carry information, and with 41 discordant pairs the interval still comfortably contains zero. The claim the experiment supports is that trimming did not cost anything, which is the claim it was run to test. The latency and cost differences are an order of magnitude apart and need no statistics at all.
The results after adding that tenth field, with latency given both at the median and at the 95th percentile, written p50 and p95 — the value below which half the runs land, and the value below which 95% of them do, which is where the slow tail shows up:
| full payload | trimmed payload | |
|---|---|---|
| median input tokens to executor | 11,400 | 940 |
| task success against ground truth | 88.4% | 91.2% |
| planner tier errors reproduced | 7 / 7 | 4 / 7 |
| p50 executor latency | 4.8 s | 1.9 s |
| p95 executor latency | 11.2 s | 3.4 s |
| cost per run at $5/M input | $0.057 | $0.005 |
| systems in PCI audit scope | 3 | 2 |
Success went up rather than down, which is the outcome that surprises teams and should not. An executor choosing between three plausible next steps in 940 tokens of relevant material makes a better choice than one doing the same selection inside 11,400 tokens where the relevant material is 8% of what it can see. Over-passing does not fail loudly; it converts a design decision the sender should have made into a judgement the receiver makes silently on every run, and that judgement is where constraints get dropped.
Where this rule is the wrong one
The rule assumes the sender knows what the receiver needs, and there are boundaries where it genuinely does not. An open-ended research agent handing to an analyst agent whose questions are not known in advance is one; a debugging agent handing to a human is another, where the human's next action is unpredictable and the derivation is exactly what they want to read. In those cases the honest design is a payload that carries pointers rather than content — a correlation ID and a reference to the stored trace, which the receiver retrieves when it decides it needs it. That keeps the boundary small while leaving the material reachable, and it makes retrieval an event you can count.
This is also not the same activity as context compression. Summarising the 14-turn conversation into 400 tokens still passes the conversation; it passes a lossy version chosen by a model, where what was dropped is unknowable and varies run to run. Selection differs in that the decision is in code, is the same on every run, and can be reviewed by a person who disagrees with it. When compression is the only option — the material is genuinely irregular and large — it is worth knowing that you have traded an auditable loss for an unauditable one.
The rule also has nothing to say about a value that several agents need. Everything in this chapter has assumed one sender and one receiver, with the sender deciding what the receiver requires. When the planner computes something that the executor, a verification agent and a notification agent all read, and an on-call engineer wants to see it while the run is still going, constructing three projections and keeping them consistent is a worse answer than the one that pushes the value somewhere all four can read.
Inter-agent communication — read the current version at https://daviddominguez.dev/learn/en/inter-agent-communication/over-passing-is-the-invisible-failure-not-the-safe-one