Spawning Sub-Agents
The six-tool spawn/lifecycle vocabulary, the credential and authority-ceiling model, typed refusal codes, and per-child customization.
Beta. The tool vocabulary, credential model, and per-child customization below are live on both MCP doors. Tenant-wide swarm observability (a lineage dashboard, subtree cost roll-up) has not shipped - see Observability for what exists today instead.
dispatch_child and its five companions are how a coordinator session spawns, steers, and observes other sessions as children. They are served identically on /api/mcp/[sessionId] (on-platform) and /api/cli/mcp (off-platform) from one shared module - the tool names, JSON schemas, and result shapes cannot drift between the two doors.
The six tools
| Tool | Required scope on the caller | What it needs to target |
|---|---|---|
dispatch_child | Spawn credential | - |
cancel_session | Spawn credential | A session you directly spawned - grandchildren are not reachable |
watch_session | Attested caller identity only | Yourself, or any descendant in your subtree |
replay_session | Attested caller identity only | Yourself, or any descendant in your subtree |
get_session_receipt | Attested caller identity only | Yourself (default), or any descendant |
steer_child | Attested caller identity only | A direct child you spawned |
Off-platform, all six require the calling API key to carry the spawn:invoke scope, independent of any dispatch:invoke/a2a:invoke scopes the same key might also hold - a key with only spawn:invoke is admitted at the door and sees exactly this tool family in tools/list. On-platform, the tools are always present on your own session's MCP endpoint; there is no separate scope to grant, because your session's own authority is what every call is checked against.
Two credential lanes, one abstraction
dispatch_child and cancel_session need more than identity - they need a spawn credential, the same signed token POST /api/interactive-sessions accepts on its machine-principal path:
- On-platform: your session's own lineage row is the credential source. The platform mints a short-lived credential from it on your behalf for each call - there is nothing for you to manage.
- Off-platform: you present the credential yourself, as the
x-interactive-spawn-credentialheader, on every call.
watch_session, replay_session, get_session_receipt, and steer_child don't need a credential at all - only your attested caller identity (who you are, and where you sit in the spawn tree), resolved the same way regardless of which door you're calling.
Authority ceiling
Every session that can spawn carries an authority_ceiling document. A child's ceiling is always derived from its parent's and can only narrow - deriveChildCeiling never widens an axis, and a request that asks for more than the parent itself holds is refused, never silently narrowed to fit. There is no path to a child with more capability than its parent.
dispatch_child accepts convenience fields - pool, harness, modelProfile, repository - plus a raw requestedCeiling object for anything more specific. Read these carefully: they narrow the ceiling's allow-list, they do not select placement. Passing pool: "my-pool" means "refuse unless the resolved pool is my-pool" - the normal project/org default cascade still does the resolving. If the cascade would resolve a different pool, the spawn is refused (interactive_spawn_ceiling_violation), not silently redirected to your requested one. If you need to actually pin a launch axis rather than just constrain it, see Customization below.
Platform-wide defaults
Absent any org override, every spawn is subject to:
| Limit | Default |
|---|---|
| Max spawn depth | 3 |
| Max direct children per parent | 20 |
| Max sessions in one subtree | 40 |
| Max child session duration | 3600 seconds |
An org may only tighten these via its plan configuration - never widen them past the platform default. dispatch_child's response, and every session's lineage row, carries spawn_depth so you can see how close a branch is to the ceiling before you spawn further from it.
Typed refusal codes
Every refusal from dispatch_child/cancel_session carries one of nine typed codes, each with a declared class that tells you whether retrying makes sense:
| Class | Meaning | HTTP status |
|---|---|---|
terminal | Will never succeed as asked. Don't retry with the same args. | 403 |
terminal_for_subtree | This branch of the spawn tree is done growing. | 403 |
retryable_when_child_exits | A capacity limit, not a permissions problem - wait for a child to finish, then retry. | 429 |
| Code | Class |
|---|---|
interactive_spawn_not_authorized | terminal |
interactive_spawn_unattested_parent | terminal |
interactive_spawn_orphan_authorizer | terminal |
interactive_spawn_ceiling_unsupported | terminal |
interactive_spawn_ceiling_violation | terminal |
interactive_spawn_depth_exceeded | terminal_for_subtree |
interactive_spawn_subtree_budget_exhausted | terminal_for_subtree |
interactive_spawn_fanout_exceeded | retryable_when_child_exits |
interactive_spawn_subtree_exceeded | retryable_when_child_exits |
Every refusal body carries { code, class, message } plus axis/limit/current where relevant - a coordinator can branch on class alone without maintaining its own copy of which codes are worth retrying.
Treat a refusal as ground truth, never as prose to work around. A terminal refusal on a capability request (for example, asking for a pool outside your ceiling) means escalate to a human - it is not a signal to retry with slightly different arguments.
Customization
dispatch_child has full field parity with the underlying spawn REST contract (POST /api/interactive-sessions): every customization field the REST body accepts, dispatch_child validates against the exact same zod schemas and forwards verbatim into the same launchSpawnedInteractiveSession call, so the two doors can never resolve a given selection differently.
- The ceiling-narrowing convenience fields from Authority ceiling above -
pool,harness,modelProfile,repository,requestedCeiling. initialPrompt,agentHandle(the durable, addressable name other sessions use to reach this child overa2a_send_message), andtimeouts.maxDurationSec.executionSelection,capabilities, andworkspace- the richer selection/wiring/bootstrap fields, described below.
| Field | What it does |
|---|---|
executionSelection | Real launch-axis selection (not just narrowing) - revision-pinned intent refs for pool, harness, model profile, repository, and agent card. Resolved through the exact same admission cascade the human launch path uses; a viable ref outside the child's derived ceiling still refuses (interactive_spawn_ceiling_violation) exactly as an omitted axis that resolved outside it always would. |
capabilities | Explicit capability wiring for the child - a memory scope/namespace, or an A2A mailbox handle - read by the same structural readers an Agent-Composer-wired card already feeds. |
workspace | Bootstrap for the child's working copy: branchName, worktreeName, setupCommand. Layered on top of the resolved repository axis, never a substitute for it - requires a repository to already be resolved, or the launch refuses (INTERACTIVE_LAUNCH_NOT_CONFIGURED, reasonCode: WORKSPACE_BOOTSTRAP_NO_REPOSITORY). |
All three are intent refs only - they carry no new placement or selection authority beyond what the human launch path already has, and every one is still checked against the child's derived ceiling before it takes effect. A malformed executionSelection/capabilities/workspace value throws before the launch call is ever made, matching the REST body's own safeParse 400 posture; an admission-time rejection surfaces as one of the typed refusals below instead of a generic internal error.
Launch-selection refusal codes
A syntactically valid executionSelection/capabilities/workspace can still be refused by the admission gate underneath dispatch_child - a separate code family from the ceiling-violation codes above, all class terminal:
| Code | When | Carries |
|---|---|---|
INTERACTIVE_LAUNCH_SELECTION_INVALID | A ref in executionSelection doesn't resolve - unavailable, revision-stale, or unknown. | axis |
INTERACTIVE_COMPOSITION_DENIED | A ref conflicts with another selection, or is owned by something else (for example, an axis an Agent Card's own composition already claims). | fields |
INTERACTIVE_LAUNCH_NOT_CONFIGURED | A dependent field is missing - today, workspace bootstrap requested without a resolved repository axis (reasonCode: WORKSPACE_BOOTSTRAP_NO_REPOSITORY). | reasonCode |
Each mirrors the REST route's own 409 mapping for the identical failure, surfaced here as a structured { code, class, message, ... } refusal instead of collapsing to a generic internal error over JSON-RPC.
Observability
There is no dedicated tenant-wide swarm dashboard yet. What you have today:
watch_session- one session's live status plus its direct children (lineage-aware, but one level at a time).get_session_receipt- a subtree rollup rooted at any session in your lineage: total count, a per-status tally, and max depth.- Every session's lineage row carries
spawn_depth, useful for noticing a branch approaching the depth ceiling before you spawn further from it.
A cost roll-up and a lineage-aware view across an entire tenant's spawn activity (not just what you can see from your own subtree) is planned but not live - if you need that today, reconcile by walking get_session_receipt from your own root sessions.
Related pages
- Coordinator Overview - front-end comparison
- pi sub-agents - what a spawned pi child gets automatically
- A2A Delivery Tiers - what
steer_childand the mailbox tools actually guarantee - Interactive Sessions - the human launch path these same admission checks serve
- API Key Authentication - scoping an off-platform key with
spawn:invoke