Recursive agent delegation¶
Scope: the runtime contract for an agent that spawns other agents, as distinct from the question of when decomposition is a good idea. This page covers fire-and-forget admission (the spawn call returns a handle, not an answer), returning results by explicit message or file, depth limits and the fan-out they permit, the parent-scoped child registry and what it must survive, model selection that fails rather than substitutes, and the two token totals a delegating session has to keep apart. The decomposition strategy that decides what to delegate is hierarchical agent decomposition; the team-shaped variant is multi-agent collaboration; the process layer that makes a retained child addressable later is detached agent sessions; the execution surface a delegation is usually issued from is the kernel-backed runtime.
What is and is not reproduced here. No model was run and no agent was spawned here: no token count, latency, or cost figure on this page is measured on this machine. The Python block is executed on system
python33.12.3 with the standard library only, and the pasted output is byte-identical to the run. It checks accounting identities over a tree of sessions, which is arithmetic and holds independently of any harness. One figure in an earlier draft of that block was wrong (a predicted double-count of 1.06M against an actual 1.50M) and was corrected against the executed result rather than the other way round. Contract details attributed below to a published implementation are read from its design documents at commita18809e(2026-08-07), cited under References.
What it is¶
Most sub-agent APIs are blocking calls. The parent asks for a sub-task, the call suspends, the child runs its whole loop, and the child's final report is the return value. It reads like a function call, which is exactly why it is popular.
The alternative treats delegation as task admission rather than task completion. The spawn call returns as soon as the host has accepted the task and created the child, carrying a handle that identifies the child and nothing about its result. The child's answer never comes back through that call. It arrives later, if the child chooses to send it, as an explicit message into the parent's queue or as a file the parent reads.
# Illustrative shape, not a specific product API.
# Returns after admission. There is no result in `handle`.
api = await spawn("Review the public API", name="api-reviewer")
tests = await spawn("Review the test coverage", name="test-reviewer")
audit = await spawn("Run the slow integration audit", name="audit")
# Then end the turn. Do not poll, do not await completion.
An admission handle carries what the parent needs to address the child later and nothing else: a stable child id, the readable name, the child's session directory, and the model it was given. A design that put usage or a result in the handle would be quietly reintroducing the blocking contract.
The child replies only when an answer is wanted, and replying is an action it takes:
# In the child, when it has something to report.
await message.send(summary, receiver_role="parent")
# In the parent, later, to continue a retained child.
await message.send("Check the new regression test", receiver_role="child",
receiver_name=api.name)
Why use it¶
The parent's context stays proportional to what it needs, not to what its children did. In the blocking pattern the child's report lands in the parent's context whether or not the parent needed the detail, and a chain of five such calls is five reports the parent now carries forever. With an explicit reply, the child decides what is worth sending and the parent's context grows by that much only.
Concurrency comes for free and without a scheduler. Three spawn calls in one turn admit three children that run simultaneously, each with its own runtime. This is the practical answer to the serialisation of a single interpreter: one namespace runs one cell at a time, so parallelism has to come from delegation rather than from parallel execution in the parent.
No turn is held open for the duration of the work. A twenty-minute audit does not keep the parent's assistant turn suspended, does not sit inside a provider request, and does not fail the parent's turn when it fails.
A child can outlive the turn that made it. Once results arrive as messages rather than return values, there is no reason a child must finish before the parent moves on, and retained children can be given follow-up work across many later turns.
The costs are real and worth stating in the same breath. A child that is never asked to report and never writes a file is a silent no-op: the parent spent tokens and got nothing, and nothing in the control flow indicates it. Anything the parent genuinely needs before continuing has to be requested explicitly in the child's prompt. And because results are asynchronous, the parent needs a registry to find its children again, which is machinery the blocking pattern does not need.
When to use it (and when not)¶
Use it when the sub-tasks are independent, when they are long, when the parent has other work to do meanwhile, or when the parent's context is the scarce resource.
Use the blocking pattern when the parent literally cannot proceed without the answer and there is exactly one outstanding sub-task. Reproducing a blocking call on top of an admission contract by spawning and then polling is the worst of both: the latency of asynchrony with the context cost of synchrony.
Do not use delegation as a substitute for a loop. Spawning a child per item over a hundred items is a hundred sessions, a hundred contexts, and a hundred sets of provider calls to do what a for statement in the parent's interpreter would do for one turn's tokens. Delegate when the sub-task needs its own reasoning, not when it needs its own iteration.
One consequence to plan for before it surprises you: a delegating agent's trajectory is discontinuous. When the parent hands off and receives only a summary, its token stream is not an extension of what came before, so any downstream use that assumes a single growing prefix has to segment at the handoff. That is exactly the boundary discussed in rollout token correctness and chat rendering and loss masking, and it matters as soon as these traces become training data.
Architecture¶
sequenceDiagram
participant M as Parent model
participant H as Parent session host
participant K as Parent interpreter
participant C as Child session
participant P as Model provider
M->>H: code call
H->>K: execute spawn request
K->>H: typed host request
H->>H: check depth, resolve model, admit task, update registry
H-->>K: admission handle
K-->>M: handle only, no result
H->>C: create child runtime and prompt
loop child agent loop
C->>P: stream request
P-->>C: response
end
C-->>H: explicit reply message
H-->>M: ordinary message on a later turn
H->>H: attribute child usage to the launching turn
The ordering matters: the handle is returned before the child is even started, so admission is cheap and predictable, and the child's lifecycle proceeds independently of the call that created it.
How to use it¶
Name every child. The name is how the parent addresses it for follow-ups, how it appears in listings, and how a human recognises it. Generated ids are unusable for either.
Let model selection fail loudly. If the parent asks for a specific model for a child and that model is unavailable or fails an authentication preflight, the spawn should fail rather than silently substituting a different one. Silent substitution turns a cost or capability decision into a mystery: the audit ran, it ran on the wrong model, and nothing said so. Inheriting the parent's model when nothing is requested is the right default; substituting when something is requested is not.
Reject unknown options rather than ignoring them. A misspelled parameter that is silently dropped produces a child configured differently than the code says.
Treat depth as the load-bearing limit. Breadth is bounded by what the parent writes in one turn; depth is bounded only by policy, and it compounds. A conservative default of one level (a root may create children, and those children may not create grandchildren) is not timidity, it is the difference between a bounded and an exponential fan-out.
Delete deliberately. Removing a child should cancel or close its runtime, write a durable tombstone, and remove it from messaging and listings, while leaving its transcript and artifacts on disk. Delegated work is often the only record of why a decision was made, and deletion of a session should not be deletion of evidence.
How to develop with it¶
A delegating session has two token totals and they answer different questions. What did this session cost? That must include everything its children burned, because the invoice does. How full is this session's context window? That must exclude everything its children burned, because none of those tokens ever entered the parent's prompt.
Reporting one number for both breaks in opposite directions. Use the billed total as the context measurement and the parent compacts a nearly empty context every turn. Sum the reported per-node totals for billing and every delegated token is counted once per ancestor.
"""delegation_accounting.py -- a delegating agent has two token totals that must
not be confused: what it costs and what it holds in context.
Child work is billed to the session, so it belongs in the cost rollup. Child
tokens never enter the parent's prompt, so they must stay out of the parent's
context measurement. Reporting one number for both breaks in opposite
directions: double-counted invoices, or compaction fired against a context that
is nearly empty.
Also bounds the fan-out a recursion-depth limit actually permits.
Standard library only.
"""
from dataclasses import dataclass, field
@dataclass
class Node:
name: str
own_tokens: int # tokens this session itself sent/received
context_tokens: int # tokens currently in this session's prompt
children: list["Node"] = field(default_factory=list)
def attributed(node: Node) -> int:
"""Usage folded up from descendants. Excludes the node's own tokens."""
return sum(n.own_tokens + attributed(n) for n in node.children)
def aggregate(node: Node) -> int:
"""What the session is billed for: own plus everything below."""
return node.own_tokens + attributed(node)
def own_usage_total(node: Node) -> int:
"""Walk the tree summing OWN usage only."""
return node.own_tokens + sum(own_usage_total(n) for n in node.children)
def sum_of_aggregates(node: Node) -> int:
"""The natural-looking mistake: sum each node's reported total."""
return aggregate(node) + sum(sum_of_aggregates(n) for n in node.children)
def max_sessions(branching: int, max_depth: int) -> int:
"""Root plus every descendant a depth cap permits."""
return sum(branching**level for level in range(max_depth + 1))
if __name__ == "__main__":
tree = Node("root", own_tokens=40_000, context_tokens=40_000, children=[
Node("api-review", 180_000, 180_000),
Node("test-review", 210_000, 210_000),
Node("audit", 250_000, 250_000, children=[
Node("audit-helper", 60_000, 60_000),
]),
])
window = 200_000
compact_at = 0.80
print("one root with three children (one of which delegated once more)")
print(" root own tokens :", tree.own_tokens)
print(" attributed from children :", attributed(tree))
print(" root aggregate (billed) :", aggregate(tree))
print(" sum of node own usage :", own_usage_total(tree))
print(" sum of node aggregates :", sum_of_aggregates(tree))
ctx_correct = tree.context_tokens / window
ctx_wrong = aggregate(tree) / window
print("\nthe parent's context window")
print(f" measured from own context : {ctx_correct:.1%} of {window}")
print(f" measured from aggregate : {ctx_wrong:.1%} of {window}")
print(" compaction fires (own) :", ctx_correct >= compact_at)
print(" compaction fires (agg) :", ctx_wrong >= compact_at)
print("\nsessions a depth cap permits, 3-way fan-out per turn")
for depth in (1, 2, 3, 4):
print(f" max_depth={depth} : {max_sessions(3, depth):>3} sessions")
# --- assertions -------------------------------------------------------
# 1. The reconciliation identity: tree-wide own usage equals the root
# aggregate. This is what makes the rollup auditable.
assert own_usage_total(tree) == aggregate(tree) == 740_000
# 2. Summing reported aggregates instead of own usage double-counts every
# delegated token. Here it inflates a 740k session to 1.5M, and the
# deepest token (audit-helper's) is counted three times.
assert sum_of_aggregates(tree) == 1_500_000
assert sum_of_aggregates(tree) > own_usage_total(tree)
# 3. The parent holds 40k of context, 20% of the window. Nothing to compact.
assert tree.context_tokens == 40_000
assert ctx_correct < compact_at
# 4. The finding: driving compaction off the billed aggregate reports 370%
# of a 200k window and would compact a nearly empty context every turn.
# The two numbers are not interchangeable in either direction.
assert ctx_wrong >= compact_at
assert round(ctx_wrong, 2) == 3.70
# 5. Depth is the load-bearing cap, not breadth. One extra level of allowed
# recursion is a 3x jump in worst-case concurrent sessions, and each one
# is a real provider-billing process.
assert max_sessions(3, 1) == 4
assert max_sessions(3, 2) == 13
assert max_sessions(3, 3) == 40
assert max_sessions(3, 4) // max_sessions(3, 3) == 3
# 6. Adversarial: a leaf's aggregate is its own usage, so the identity is
# not an artifact of this particular tree shape.
leaf = Node("solo", 5, 5)
assert aggregate(leaf) == own_usage_total(leaf) == 5
assert attributed(leaf) == 0
print("\nall assertions passed")
Executed output:
one root with three children (one of which delegated once more)
root own tokens : 40000
attributed from children : 700000
root aggregate (billed) : 740000
sum of node own usage : 740000
sum of node aggregates : 1500000
the parent's context window
measured from own context : 20.0% of 200000
measured from aggregate : 370.0% of 200000
compaction fires (own) : False
compaction fires (agg) : True
sessions a depth cap permits, 3-way fan-out per turn
max_depth=1 : 4 sessions
max_depth=2 : 13 sessions
max_depth=3 : 40 sessions
max_depth=4 : 121 sessions
all assertions passed
Three results to carry away. The reconciliation identity (tree-wide own usage equals the root aggregate) is what makes the rollup auditable, and it is worth asserting in your own code because it catches attribution bugs that no eyeball review will. Summing the numbers each node reports inflates a 740k session to 1.5M, because the deepest child's tokens are counted once at every ancestor. And the parent that is billed for 370% of a context window is holding 20% of one, which is why the attributed total must be subtracted back out before anything decides to compact.
The persistence rule that follows: the attribution should be written into the transcript as its own record naming the assistant turn it applies to, the child usage, and the resulting aggregate, so that reloading the session reapplies it rather than recomputing it from children that may since have been deleted.
How to maintain it¶
The registry is authoritative and lives with the parent, not in the interpreter. Listing children has to work after context compaction, after an interpreter restart, and after the parent session is restored from disk, because all three happen routinely in long sessions and none of them should lose track of running work.
Registry scope follows the parent transcript. A new, unrelated parent must not inherit children. This sounds obvious until forking and importing sessions enter the picture, at which point "which transcript is this child's parent" becomes a real question that the implementation has to answer consistently.
Rehydrate completed children that are still worth addressing. A child that finished successfully is often exactly the one you want to ask a follow-up question, because it already holds the context. Keeping it addressable while the parent session is open, and rehydrating it from the parent's artifact registry after a restart, is the difference between delegation and one-shot subprocess invocation.
Distinguish children that a supervisor owns from children that ran inline. Only the former can be addressed as independent sessions later; the latter remain inspectable in the current process and have no active-session identity. Reporting both identically leads to follow-up messages addressed to something that cannot receive them.
Tear down descendants with the parent. When a parent is closed, its active descendants should be cancelled and their runtimes closed. Orphaned children continue to burn tokens against a session nobody is watching.
How to run it in production¶
Budget delegation in sessions, not in calls. The table above is the arithmetic to plan against: at three-way fan-out, allowing two levels of recursion instead of one takes the worst case from 4 concurrent sessions to 13, and three levels takes it to 40. Each one is a real process holding a model context and billing a provider. Depth limits are the cheapest control you have, and raising the default is a decision that deserves a number attached to it.
Make cost observable per node and per tree. The two totals to expose are each session's own usage and the tree aggregate at the root; publish both and the reconciliation identity gives you a free consistency check on your own telemetry. This is the token-economics view from agentic loop economics applied to a tree rather than a loop.
Watch for the silent no-op. A child that completed without ever replying and without writing a file is the characteristic failure of this contract, and it is invisible unless you look for it. Counting completed children with zero outbound messages and zero artifact writes surfaces it directly.
Keep tombstones and transcripts. Deleted children should leave a durable record that they existed and were removed, and their artifacts should remain, because the reason a delegated audit was discarded is frequently the thing an incident review needs.
Failure modes¶
| Failure | Symptom | What to do |
|---|---|---|
| Spawn treated as a blocking call | Parent polls or waits, latency and context cost both paid | End the turn after admission; results arrive as messages |
| Child never asked to report | Tokens spent, nothing returned, no error anywhere | Require an explicit reply or artifact in the child prompt; alert on zero-output children |
| Silent model substitution | Work runs on an unintended model at unintended cost | Fail the spawn when the requested model is unavailable |
| Unknown spawn option ignored | Child configured differently than the code reads | Reject unknown options |
| Aggregate usage drives compaction | Parent compacts a nearly empty context every turn | Subtract attributed child usage before measuring context |
| Per-node aggregates summed for billing | Cost inflated, deep trees inflated most | Sum own usage only; assert the reconciliation identity |
| Registry lost on compaction or restart | Running children become unreachable and unkillable | Keep the registry with the parent, outside the interpreter |
| Depth cap raised without a fan-out budget | Exponential session growth, provider rate limits, cost spike | Compute worst-case sessions before raising the limit |
| Parent closed with live descendants | Orphaned children keep billing | Cancel descendants on parent teardown |
| Delegation used in place of iteration | One session per list item | Loop in the parent; delegate reasoning, not iteration |
References¶
- Delegation runtime contract: admission handles, message-only returns, depth checks, model resolution, parent-scoped registry, tombstones, and usage attribution: https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/docs/rlm-runtime.md
- Programming-model document: independent delegation, child handles and lifecycle, state that outlives one turn: https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/docs/rlm.md
- Agent-to-agent messaging, delivery modes, retained sub-agents, and detached lifecycle: https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/docs/long-running-agents.md
- Discontinuous trajectories at a sub-agent handoff, and why a merge must segment there: https://github.com/PrimeIntellect-ai/prime-rl/blob/main/docs/algorithms.md
- Repository the agent-side documents are published in, read at commit
a18809e(2026-08-07): https://github.com/PrimeIntellect-ai/prime-agent
Related: Hierarchical agent decomposition · Multi-agent collaboration · Kernel-backed agent runtimes · Detached agent sessions and supervision · Agent harness architecture · Agentic loop economics · When to compact · Rollout token correctness · Agent communication protocols · Agent observability · Glossary