Rensei docs
Coordinator

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

ToolRequired scope on the callerWhat it needs to target
dispatch_childSpawn credential-
cancel_sessionSpawn credentialA session you directly spawned - grandchildren are not reachable
watch_sessionAttested caller identity onlyYourself, or any descendant in your subtree
replay_sessionAttested caller identity onlyYourself, or any descendant in your subtree
get_session_receiptAttested caller identity onlyYourself (default), or any descendant
steer_childAttested caller identity onlyA 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-credential header, 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:

LimitDefault
Max spawn depth3
Max direct children per parent20
Max sessions in one subtree40
Max child session duration3600 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:

ClassMeaningHTTP status
terminalWill never succeed as asked. Don't retry with the same args.403
terminal_for_subtreeThis branch of the spawn tree is done growing.403
retryable_when_child_exitsA capacity limit, not a permissions problem - wait for a child to finish, then retry.429
CodeClass
interactive_spawn_not_authorizedterminal
interactive_spawn_unattested_parentterminal
interactive_spawn_orphan_authorizerterminal
interactive_spawn_ceiling_unsupportedterminal
interactive_spawn_ceiling_violationterminal
interactive_spawn_depth_exceededterminal_for_subtree
interactive_spawn_subtree_budget_exhaustedterminal_for_subtree
interactive_spawn_fanout_exceededretryable_when_child_exits
interactive_spawn_subtree_exceededretryable_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 over a2a_send_message), and timeouts.maxDurationSec.
  • executionSelection, capabilities, and workspace - the richer selection/wiring/bootstrap fields, described below.
FieldWhat it does
executionSelectionReal 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.
capabilitiesExplicit 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.
workspaceBootstrap 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:

CodeWhenCarries
INTERACTIVE_LAUNCH_SELECTION_INVALIDA ref in executionSelection doesn't resolve - unavailable, revision-stale, or unknown.axis
INTERACTIVE_COMPOSITION_DENIEDA 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_CONFIGUREDA 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.

On this page