Skip to content

9 min read

What you withheld is what makes it a split

An RFC — a design document circulated for comment before anyone builds against it — lands in the platform channel titled Incident response agent mesh, and the diagram in it has five boxes: a triage agent, a log reader, a code reader, a hypothesis agent, and a writeup agent, with arrows running left to right. The prose says the design uses sub-agents for context isolation so that each stage reasons over a focused window. Three people have already left approving comments. The section on rollout is detailed, the tracing story is good, and the author has clearly built the thing.

The question that decides the review takes one line to ask: for each of those five boxes, what did you deliberately keep out of its window, and why that specific thing?

The answer for the log reader was that it receives the incident Slack transcript, the triage agent's full output, and the alert payload, which is everything the triage agent had plus one artifact. The answer for the code reader was the same list plus the log reader's output. By the hypothesis agent, the window held four upstream outputs and the original transcript, and the writeup agent held all of it. Nothing had been withheld from anything. What the RFC called isolation was five system prompts over one monotonically growing context, and the mechanism from the previous chapter applies at every hop: each stage is conditioned on the reasoning of every stage before it, and a stage whose job is to question an earlier conclusion is reading the derivation of that conclusion while it works.

Flowchart: triage 12.4k → log reader 38.9k (transcript + alert + its own output); log reader 38.9k → code reader 52.1k (everything above + logs); code reader 52.1k → hypothesis 71.6k (everything above + files); hypothesis 71.6k → writeup 84.3k (everything above).

Drawn with the token counts on the boxes, the RFC's diagram stops looking like five isolated stages and starts looking like what it is, which is one window with four labelled resumption points. The arrows carry accumulation rather than handoff.

Three kinds of evidence, none of them the diagram

A claimed split is a claim about data, so the evidence has to be about data rather than about naming. Three things settle it, and all three are cheap to check.

The first is transferred data: which bytes actually cross into the sub-agent's window. This is not a design question, it is an instrumentation question, and the numbers are usually already in your traces. Ledgerline's incident mesh, once someone logged per-agent input composition rather than just totals, looked like this over one production incident.

python
# emitted per spawn, from the caller that builds the child's messages
{
  "agent": "log_reader",
  "input_tokens": 38_940,
  "inherited_from_parent_tokens": 34_100,   # bytes also present in parent window
  "task_specific_tokens": 4_840,            # bytes assembled for this child only
}

Computing that field is a few lines at the spawn site, because the caller holds both message lists at the moment it builds the child's, and a token-level diff of the two is cheap. It is worth emitting as a span attribute — a field on the timed record a tracing system keeps for one step of a request — rather than a log line, so that the number is queryable across a week of production traffic instead of being something an engineer measures once during an argument. Ledgerline's rule after this review was that any spawn emitting an inherited fraction above 0.5 for more than a day fires a low-priority alert against the team that owns it, which is not a hard gate — some children legitimately need most of what the parent has — but it does mean nobody discovers the situation six months later while debugging something else.

An inherited fraction of 88% is the finding. A window that is seven-eighths a copy of the parent's window is the parent's window, and the remaining eighth is a prompt change. The number worth watching is not the child's total size — a child can be legitimately large — but the share of it that the parent already held.

The second is the dependency boundary, and the tell is in the type signature of whatever builds the child's input. Both of these functions appear in real codebases, and only one of them describes a boundary.

python
# costume: the child's input is a projection of the parent's state
def build_log_reader_input(messages: list[Message], alert: Alert) -> str:
    return render_transcript(messages) + "\n\n" + alert.text

# split: the child's input is constructible without the parent existing
def build_log_reader_input(
    incident_id: str,
    window: TimeRange,
    services: list[str],
    known_bad_signals: list[str],
) -> str:
    ...

If the builder takes the parent's message list, there is nothing to withhold, because withholding would mean deciding which turns to drop and no such decision is being made. The second signature can be called from a test, from a CLI, or from a different parent entirely, and the set of things it can possibly know is legible from four parameter names. That legibility is the boundary. It also has a side effect worth having on its own: when the child misbehaves, you can reproduce its window exactly, which you cannot do when the input was a rendering of forty turns of history that will never occur again in the same order.

The third is measurable benefit, and it is the one that most designs skip. A split is a runtime cost — an extra call, an extra prompt to maintain, an extra failure mode, a second trace to correlate — and the claim that it buys something is a claim about an observable. There are exactly three observables worth citing: a window that is smaller than the single-loop window would have been at the same point, work that runs concurrently and shortens wall-clock, and a quality difference on the subtask measured with everything else held constant. The third of those is the hardest and is the subject of the next chapter. The first two are arithmetic.

Claimed splitWhat was withheldDependency boundaryBenefit it can point to
Log reader in the RFC as writtennothing; parent transcript passed throughbuilder takes messagesnone; window is 88% inherited
Security reviewer from chapter 161 turns of derivation, including the turn that settled the questionbuilder takes plan, policy, trust facts47.3k → 4.1k tokens; flaw caught 9/10 vs 0/10
Log reader after reworktranscript, triage narrative, alert commentarybuilder takes incident id, time window, service list38.9k → 6.2k tokens; reproducible from four arguments
Doc-fetch helper with its own tool allowlistnothingbuilder takes messagesa permission property, not a context one

The framework you are using probably encodes the same distinction and does not enforce it. Claude's Subagents in the SDK documentation is explicit that a sub-agent gets its own context window and its own instructions, which is the contract; what it cannot do is stop the caller from filling that window with a rendering of the parent's history, because from the framework's side that is just a string. The isolation is real and it is a container, and a container is only as empty as whoever fills it.

The last row is a distinction worth holding onto. Giving a sub-agent a restricted tool allowlist — no shell, no write access, no network beyond one host — is a genuine and valuable boundary, and it is a boundary in the permission dimension. It constrains what the child can do. Isolation as this topic uses the term constrains what the child knows, and the two are independent: you can have either without the other. Calling a permission boundary context isolation is how a design ends up with the audit properties it wanted and none of the reasoning properties it claimed.

Where the confusion comes from, and where withholding goes wrong

The pattern arrived in most teams' vocabulary through coding agents, and the two features that arrived at the same time are easy to conflate. A slash command in a coding agent injects a prompt into the thread you are already in, so it runs against everything that thread has accumulated; a sub-agent runs the same prompt in a window that starts empty. Both are described in documentation as reusable prompts, both are configured in a markdown file with frontmatter, and both show up in a workflow diagram as a labelled step. Jason Liu's Slash Commands vs Subagents walks a test-run example through both and is the clearest short treatment of the difference. Teams that adopt the sub-agent file format because it is a nice way to organise prompts, and then pass the transcript in so the child "has enough context", have adopted the syntax of the split and none of its semantics.

The opposite error is real too, and it produces worse incidents because they look like model failures. Withholding is only free when what you withheld was not load-bearing. The reworked log reader above dropped the incident transcript, and for six weeks it was better than the version that had it — until an incident where the transcript contained a human engineer's note that the alerting pipeline itself had been restarted at 14:02, which explained a gap in the logs that the log reader then confidently attributed to the service being down. The fix was to add one more parameter to the builder, operator_notes: list[str], carrying the human-authored lines from the channel and nothing else. The general form is that every withheld fact is a bet that the child does not need it, and the way to lose the bet quietly is to withhold in bulk by passing a smaller slice of the transcript rather than by naming the inputs.

There is a third answer to the review question that is neither a costume nor a defect, and it comes up often enough to name. Sometimes the author says that nothing was withheld because nothing needed to be — the child is the first thing the system does, the parent has almost no context yet, and the child's window would be nearly identical either way. That is an honest answer and it means the split is not buying isolation, which leaves concurrency or nothing. It is worth taking at face value rather than treating as evasion, because a spawn early in a run genuinely has little to withhold, and the argument then moves to whether the branch runs alongside anything.

So the review question has a second half that matters as much as the first. What did you withhold exposes the costume; and why that specific thing exposes whether the author knows which parts of the parent's context were influencing the subtask's answer and which were inert. An author who says "we kept out the derivation of the plan, because the reviewer's job is to judge the plan on its own terms and the derivation is an argument for it" has a model of the mechanism. An author who says "we kept the window under 8k because that seemed cleaner" has a heuristic that will withhold the operator notes on the day it matters.

The incident mesh RFC was not rejected, which is the outcome this question usually produces. Two of the five boxes survived as splits: the log reader, once its builder took an incident id and a time window instead of a transcript, and the code reader, once it took a list of file paths and a symptom description. The hypothesis agent was folded back into the parent, because every attempt to name its inputs produced a list that was the parent's state with the words rearranged, which is the diagnostic result rather than a failure of imagination. The writeup agent stayed split for a reason that has nothing to do with context — it is the only stage that writes to a customer-visible channel, and keeping it a separate call with a separate tool allowlist means the blast radius of a prompt injection in a log line — instructions written into text the agent reads as data, which the model may then follow as though they came from its operator — is bounded by what that one call can do. Three different justifications for four boxes, and the RFC is better for having had to state them separately.

None of this yet tells you whether the split helped. Chapter 1's reviewer went from zero catches to nine out of ten, which sounds decisive, and the honest version of that claim has to survive the obvious objection: the two arms differed in their input assembly as well as their window, and somebody will point out that the second arm's brief was also just a better prompt. Establishing that the window itself is doing work requires running the same instruction and the same inputs twice, changing only what else is present, and that experiment has a specific shape.

Sub-agent context isolation — read the current version at https://daviddominguez.dev/learn/en/sub-agent-context-isolation/what-you-withheld-is-what-makes-it-a-split

Back to top