Sub-agents
Handing a bounded piece of work to a cheaper, narrower version of the agent.
subagents is a roster on the definition, so it is versioned, diffable and auditable
exactly as tools is. An agent with no roster gets none of the tools and runs no
sub-agent code path at all.
export default definePlane({
agent: {
slug: "coding-agent",
model: { provider: "gateway", model: "anthropic/claude-sonnet-4.6" },
system: "You are a careful engineer.",
subagents: [
// Relational: the parent's setup, narrowed. Every omitted override means
// *inherit* — never "default".
{
kind: "derived",
name: "explore",
description: "Read-only codebase search. Give it a question, not a plan.",
extends: "parent",
overrides: {
model: { model: "anthropic/claude-haiku-4.6" },
tools: { subset: ["read", "grep", "glob"] },
system: { mode: "append", text: "You cannot write. Answer with findings." },
limits: { maxTurns: 12 }
}
},
// By reference: an agent that exists in its own right.
{ kind: "agent", name: "reviewer", description: "Second-opinion review.", agent: "code-reviewer" }
],
subagentPolicy: { maxDepth: 2, maxConcurrent: 4, maxTotalPerSession: 16 }
},
tools: [/* … */]
})description is not decoration. It is what the model reads to decide whether to
delegate at all, and it becomes the description of the name argument on
plane_spawn_subagent. Describe when to use this sub-agent, not what it is.
tools.subset can only narrow. It is intersected with what the base can already
reach, so explore's inability to write is a property of the resolver rather than of
its prompt. A name the parent cannot reach is refused by sync before anything is
sent.
extends: "parent" inherits the spawning session's effective setup, its own
overrides already applied. So a session created with a per-tenant paragraph appended
to its prompt spawns children that know which tenant they are working for.
The tools it gets
With a non-empty roster and below the depth limit, the agent gets five:
plane_spawn_subagent, plane_message_subagent, plane_wait_subagent,
plane_stop_subagent, plane_list_subagents. A sub-agent additionally gets
plane_report, the channel back to whoever delegated.
Spawning defaults to wait: true, which reads like an ordinary tool call.
wait: false plus one plane_wait_subagent over several ids is how a fan-out is
expressed.
A child is a real session
Its own transcript, its own trace, its own usage, its own id. You reach it the way you reach any other:
const children = await client.sessions.children({ id: sessionId })
for await (const event of client.sessions.stream(children[0].id)) { /* … */ }The parent's log carries only the child's outputs — subagent.spawned,
subagent.status, subagent.message, subagent.completed — never its working, so a
busy child cannot fill its parent's context. Subscribe to those four in
hooks.onSessionEvent for a "a sub-agent finished" webhook.
You cannot create a child yourself. sessions.create has no parentSessionId: a
caller who could name a parent could forge ownership, evade the depth limit by
claiming depth 0, and bill work to somebody else's tree. Children are created by the
spawn tool and by nothing else. You may still send, interrupt and stream a child
directly; doing so does not mirror into the parent's log, because attributing an
operator's words to the parent agent would be a lie.
Caps
maxDepth: 2, maxConcurrent: 4, maxTotalPerSession: 16, selfDelegation off. The
depth ceiling is the root session's, so a child cannot raise it by declaring a
bigger one. Usage rolls up: a session's descendantUsage is everything its tree
spent, and limits.maxInputTokensPerSession is checked against the total on the root
— so a budget cannot be evaded by delegating.
A refused spawn is a tool error, not a failed turn: the model can wait for a slot, pick a different sub-agent, or answer without one.
Policies and machines are shared
A sub-agent inherits its parent's policies, so a child's tool call is gated by the
same onToolCall hook — arriving with session.parentSessionId set, which is how you
tell delegated work from the conversation the user is having. The sub-agent tools
themselves are exempt: gating plane_spawn_subagent would ask a person to approve the
question and then approve every answer.
If the agent has a sandbox, the whole tree shares one
machine — same workspace, same processes — which is what makes a review-lens child
possible at all. Narrow what a child may do to it with overrides.sandbox.tools.