← All writing

24 November 2026

Subagents Without the Theatre: Isolation, Evidence, and Where Authority Lives

When delegating to child agents is worth it, why a child shouldn't see the parent's transcript, how to accept its results, and why I'd fence children with a parent-only denylist and per-call permission checks rather than a tiny allowlist.

In this article One level, declared up front

Most subagent demos are role-play: a “researcher”, a “critic”, and a “writer” passing messages to each other. It looks impressive, and it usually costs three times as much as one agent doing the job, with more places to go wrong.

The case I find convincing is narrower. Someone asks an agent to compare two companies’ annual results. Each company means finding a filing, reading a few hundred pages selectively, and extracting numbers with their scope checked. Doing both inside one conversation fills the context with two filings’ worth of evidence before any comparison happens, and the second extraction is done by a model already primed by the first.

Delegation fixes that: one child per company, each with a clean context, each returning a structured result. The parent does the comparison. That’s the whole pattern. The interesting part is everything around it.

One level, declared up front

I’d allow exactly one level of delegation. The parent is generation 0, children are generation 1, and a spawn request from a child is rejected. Children never get the spawn tool in the first place. Grandchildren and peer-to-peer agent networks multiply cost and make cancellation and accounting genuinely hard, and I’ve yet to see a task in this kind of product that needed them.

I’d also make delegation something a procedure declares, not something the model decides on a whim. If a “source review” procedure is the thing that benefits from parallel isolated reads, let that procedure’s definition say it may spawn, how many children at most, and how many at once:

delegation:
  mode: isolated_child
  max_children: 4
  max_concurrent: 4

Gate it on a permission as well, and consider a per-user daily limit for expensive deep-research runs. Reserve against that limit idempotently, so a retried spawn doesn’t count twice.

Children are durable records, not coroutines

A child that only exists in the parent’s memory dies with the parent’s worker. I’d make each child a row in a table, with a lease, a heartbeat, an attempt counter, and an idempotency key derived from the task. That gives you states you can show and reason about: submitted, queued, running, needs input, completed, partial, failed, cancel requested, cancelled, timed out.

The parent gets three operations: start, check status, and cancel. Starting waits briefly — say fifteen seconds — and then returns a handle the parent can poll. Cancelling the parent cascades to its children. After a crash, reconciliation re-queues only the work that’s safe to run again.

Running out of capacity should be a normal result, not an exception. If spawning returns “capacity reached” as data, the parent can do the work itself or wait. If it raises, models tend to treat it as a failure of the whole task and give up.

The child shouldn’t see the transcript

The function that builds a child’s context should have no parameter for the parent’s conversation. A child gets a short system prefix, one typed task envelope, the procedure it’s following, and only the injected context that procedure asks for.

{
  "objective": "Extract FY26 and FY25 consolidated revenue and net profit from the annual report",
  "acceptance": { "min_evidence": 1, "schema": "source_review.v1" },
  "inputs": { "company": "EXAMPLE", "period": "FY26" },
  "parent": { "run": "…", "workspace_version": 41 }
}

Leaving out the transcript isn’t only about tokens. The parent’s history is where its half-formed conclusions live. A child that reads “I think margins fell” in the parent’s notes tends to find margins falling. If you want independent evidence, give the child an independent starting point.

Accept results on evidence

A child’s result should be validated before the parent sees it. The output must match the declared schema. A “completed” result must carry at least the required number of evidence references that resolve to real, successful reads, plus at least one finding. Partial and failed results should still be accepted as partial or failed, so the parent can merge what was done instead of starting again.

What the parent receives should be a deterministic projection of the result, not another model’s summary of it. Adding a summarisation step between child and parent is how a careful extraction picks up a confident error.

One practical failure I’d plan for: children returning perfectly reasonable prose that fails the structured contract every time. The fix is almost always to show the child the exact output shape in its instructions, with an example, rather than describing it in words.

Fig. 1 — A procedure declares that it may delegate. The parent creates a durable child record on a reserved queue. The child sees a task envelope, not the transcript, and its result must pass schema and evidence checks before the parent can use it.

Allowlist or denylist?

This is the decision I’ve changed my mind about.

The instinctive design is strict: give each child a short allowlist of read-only tools, intersect it with the user’s permissions and the procedure’s suggestions, and put hard ceilings on everything — a handful of turns, ninety seconds, a few web fetches, a small dollar budget. It feels safe, and on paper it is.

In practice, a short allowlist starves children. A real source review needs a tool you didn’t anticipate: a calculation in a sandbox because the filing gives quarterly figures, a browser read because the PDF sits behind a viewer. A fixed time ceiling is shorter than some legitimate reads. The children time out or return partial work, the parent retries or gives up, and you’ve paid for the attempt either way.

The design I’d choose now inverts the list. A child gets every tool available at runtime, filtered by the user’s permissions, minus a parent-only set: spawning, changing procedures or extensions, publishing to the user’s workspace, and planning. Budgets become counters you observe rather than ceilings that kill.

tools = set(runtime_tools)
if user_permissions:
    tools &= allowed_for(user_permissions)
tools -= PARENT_ONLY            # spawn, procedures, extensions, publish, plans

This only works because authority doesn’t live in the list. It lives in the permission check that wraps every tool call and runs fresh each time, in run-level limits on tool calls, model calls and reserved tokens, in a breaker for identical repeated calls, and in deadlines and spend limits. A child can’t publish, can’t change procedures, can’t spawn, and can’t do anything the user couldn’t do. Children should also get read access to integrations but never write access, because writes need a confirmation from a person and a child has nobody to ask.

“Delegate work, not authority” is still right. The lesson is that authority is enforced at the call, not by how short you make the child’s menu.

Failure modes worth testing on purpose

Queue deadlock. Parents wait for children while holding a worker. If idle child workers are allowed to steal parent jobs, a busy system can end up with every slot holding a parent that’s waiting for a child with nowhere to run. Reserve child capacity for children, always.

Your own loop breaker. A breaker that stops two identical tool calls in a row is a good defence against loops. It will also kill a parent that’s legitimately polling a child’s status. Exempt delegation waits from that check but keep them under the overall tool-call limit, and make each wait long enough that polling isn’t frantic.

Missing cost. If a child’s cost comes from what the SDK happens to report, interrupted or retried children can look free. Project cost from your own call records, including retries and cancellations, and never read missing cost as zero.

When delegation works, it’s undramatic. Two children each return a figure with its source and scope, the parent computes the growth rate, and the user’s follow-up question gets answered without another tool call because the evidence is already structured. That’s the bar I’d hold it to: not that it looks like a team, but that the final answer is better sourced than one agent would have managed alone.

← Back to writing