Subagents
A subagent is any agent composed as a child of another agent. The child runs in its own isolated thread — messages, filesystem, queue, lifecycle — linked to the parent through the thread hierarchy and the parent’s child registry.
Any agent may be composed as a child; composing one with a human side puts a person’s step inside the composition. A subagent relationship is part of the agent’s structure: it is declared on the agent side that owns it, never inside a prompt’s tools array.
1. SubagentRelationship
Relationships are declared in the subagents array of a prompt- or handler-backed side:
interface SubagentRelationship {
agent: string; // which agent definition to instantiate
as?: string; // relationship name; defaults to `agent`
description?: string; // model-facing override; falls back to the child's toolDescription
maxInstances?: number; // unset = unbounded
immediate?: boolean | { // spawn at THREAD CREATION
nameEnv?: string;
descriptionEnv?: string;
scopedEnv?: string[];
};
blocking?: boolean; // subagent_create awaits session close and returns its result
parentCommunication?: 'implicit' | 'explicit';
scopedEnv?: string[];
initUserMessageProperty?: string;
initAttachmentsProperty?: string;
initAgentNameProperty?: string;
optional?: string; // env flag gating this branch
hidden?: boolean; // spawnable by code, invisible to models
}
| Property | Description |
|---|---|
agent | The agent definition to instantiate. Any agent is eligible; exposeAsTool: true marks it as designed for composition and supplies the default toolDescription. |
as | The relationship name — the tool-surface name, registry identity, and reference name. Defaults to agent. Two relationships may target the same agent under different as names, each with independent config (§2.3). |
description | Model-facing description override for this relationship. |
maxInstances | Cap on simultaneous instances. When reached, creation attempts SHOULD return a tool error explaining the cap and suggesting messaging an existing instance. |
immediate | Spawn at thread creation, before any side takes a turn (§3.2). |
blocking | subagent_create waits for the child session to close and returns its result (§3.3). |
parentCommunication | implicit (default) auto-queues child completion/failure to the parent; explicit leaves escalation to tools, hooks, and handlers (§6.4). |
scopedEnv | Environment values copied into the child thread; runtime-only transfer data. |
initUserMessageProperty / initAttachmentsProperty / initAgentNameProperty | Map creation arguments into the child’s initial user message, attachments, and display name. |
optional | The branch is enabled only when the named environment flag resolves to true, 1, or yes (case-insensitive). Disabled branches are unavailable for creation and runtime invocation. |
hidden | The relationship stays declared — code paths can still spawn the child — but it is excluded from every LLM request’s tool surface. For infrastructure children the model must never invoke. |
2. Ownership Rules
2.1 The Owning Side
A relationship belongs to the side that declares it. Ownership determines where child traffic lands: notifyParent content, implicit completion/failure messages, and status-bearing queue traffic from instances of the relationship are delivered on the owning side’s perspective — queued in the role that prompts the owning side’s next turn (user for a side-A-owned relationship, assistant for a side-B-owned relationship, per the perspective mapping).
2.2 Single Owner
A relationship name (as, defaulted or explicit) may appear on at most one side of an agent. Declaring the same name on both sides is a definition-time error. The sibling side has no model-facing path to those children; code paths via ThreadState remain available to it. Cross-side grants (one side auditing the other side’s children through granted tools) are deliberately deferred — a door recorded closed, to be reopened with a real use case.
2.3 Aliases
Two relationships may target the same agent under different as names — for example, two calendar teams with different scopedEnv:
sideA: {
prompt: 'coordinator',
subagents: [
{ agent: 'calendar_agent', as: 'work_calendar', scopedEnv: ['WORK_CAL_ID'] },
{ agent: 'calendar_agent', as: 'personal_calendar', scopedEnv: ['PERSONAL_CAL_ID'] },
],
},
Everything model-facing — tool schemas, registries, lifecycle references — uses the relationship name. The agent argument of subagent_create / subagent_message enumerates relationship names, not agent names.
2.4 Reserved Names
Relationship names share the tool namespace. A relationship name that collides with another tool name is a definition-time error, and the injected lifecycle tool names — subagents, subagent_create, subagent_message — are explicitly reserved: a colliding as MUST be rejected with a dedicated validation code.
2.5 Visibility
- Prompt-backed owning side: the side’s prompt automatically receives the lifecycle tools (§5) for the side’s non-hidden relationships. Prompts do not declare relationships in
tools— atoolsentry attempting to configure a subagent relationship is a definition-time error. - Handler-backed owning side: handlers need no tool grant; they reach children through
ThreadStatedirectly. The declaration’s job for a handler side is existence, configuration, and lifecycle. - Human-backed side: may not own relationships (definition-time error) — nothing there can invoke them.
3. Spawning
3.1 Creation Paths
Instances are created by:
- the injected
subagent_createlifecycle tool (prompt-backed sides), - runtime code (
invokeTool/queueToolon the lifecycle surface, or dedicated runtime APIs) from tools, hooks, and handlers, immediateconfiguration at thread creation.
3.2 Immediate Spawning
immediate relationships spawn at thread creation, before any side takes a turn — immediate children exist even when the first inbound message arrives instantly, and when the owning side is a handler with no prompt to activate. Immediate spawning is recursive: if an immediate child’s own sides declare immediate relationships, those spawn as the child thread activates.
In the object form:
- runtimes MAY use
nameEnvanddescriptionEnvas model-visible hints when deriving initial child arguments via a bootstrap pass. - runtimes MUST treat
scopedEnvas runtime-only transfer data. - runtimes MUST NOT expose
scopedEnvvalues to the model unless the same env name is explicitly designated bynameEnvordescriptionEnv.
3.3 Blocking and Create-or-Continue
blocking: true makes subagent_create await the child session’s close and return its result payload.
When blocking: true is combined with maxInstances: 1 and an instance already exists, subagent_create MUST behave as create-or-continue: message the existing instance and await that session’s close, rather than erroring on the cap. This is what callers invariably mean by “run the child again.”
Warning: A handler that awaits a blocking child holds its thread’s one-at-a-time delivery slot for the whole child session. Prefer non-blocking spawning from handlers.
4. Instance Lifecycle
4.1 Persistence Is Universal
Every child instance is a persistent thread. There are no disposable instances and no opt-in persistence wrapper — there is one lifecycle:
- Session end never terminates a thread. When a child session ends — stop, fail, error, or turn-cap — the registry status transitions (
running→idle/failed), the thread remains addressable, and a later message starts a new session with history intact. Runtimes MUST NOT destroy or orphan child threads on any session-finalization path. - Only explicit termination destroys an instance:
state.terminate(), operator action, or a parent teardown policy. blockingis purely a waiting style — whether the creating call awaits the session — and is orthogonal to persistence.
What a runtime does with old or idle threads — archival, eviction, listing policy — is out of scope for this specification; retention is entirely the runtime’s business.
4.2 Registry
Instances are tracked in the parent’s ThreadState.children:
interface SubagentRegistryEntry {
reference: string; // stable instance reference (UUID)
relationship: string; // the relationship name (`as`)
name: string; // the child agent definition name
title?: string;
description: string;
blocking?: boolean;
threadName?: string;
initialArguments?: Record<string, unknown>;
spawnGroupId?: string;
createdAt?: number;
status: string; // e.g. running | idle | failed | awaiting_human | terminated
parentCommunication?: 'implicit' | 'explicit';
}
relationship is the composition identity (which declared relationship spawned this instance); name is the implementing agent. Aliased relationships targeting the same agent are distinguished by relationship.
Child visibility is pull-based: runtimes inject the subagents query tool (§5) and MUST NOT additionally inject an ambient registry system message — a registry block that mutates between requests invalidates provider prefix caches and reads as out-of-dialogue state. Completions arrive as messages.
4.3 Human-in-the-Loop Children
A child whose current turn belongs to a human-backed side parks with registry status awaiting_human. The session resumes when human input arrives on that side’s perspective; the specification defines only this waiting state, not who supplies the input or through which interface (Agents §7).
5. Lifecycle Tools
For a prompt-backed side with non-hidden relationships, runtimes inject three built-in tools scoped to that side’s declarations:
subagents— the pull-based visibility surface. Takes no arguments; returns the registry snapshot for the side’s relationships — reference, relationship name, agent name, status, working-status text, initial arguments, creation time. Injected tool descriptions SHOULD direct the model to callsubagentsbeforesubagent_createso existing instances are reused rather than duplicated.subagent_create— creates (or continues, §3.3) an instance. Itsagentargument MUST enumerate the side’s enabled relationship names. It MUST require a non-emptynameargument for the spawned instance; runtimes SHOULD persist this as a human-readable child thread display name.subagent_message— queues a message to an existing instance by reference.
All lifecycle references use relationship names, never an internal agent identifier that differs from them. Runtimes MUST reject lifecycle invocations that target undeclared or disabled relationships without creating a child.
subagent_create SHOULD expose a structured arguments object whose schema is dictated by the receiving child (§5.1).
5.1 Persistent Invocation Arguments
The invocation arguments a subagent accepts are dictated solely by the receiving agent, through the requiredSchema of its side A prompt. (Parent traffic enters the child in the side B perspective, role: 'user', so side A acts on it, exactly as for an external caller — see §6.1.) Required schema fields are mandatory; optional fields are suggested.
Unlike a plain tool call, these arguments are not ephemeral:
- runtimes MUST validate them against the schema and persist the validated arguments for the life of the child thread.
- runtimes MUST hydrate them into
ThreadState.argumentsand prompt variable interpolation for both child sides on every activation — the initial invocation and every later resume — so spawn-time context (a stylist child that needs sizes and preferences, say) is available for as long as the thread lives. - if required arguments are missing, runtimes MUST reject creation with a structured
subagent_arguments_requirederror rather than spawning the child.
5.2 Scoped Variable Bootstrap
If subagent_create cannot proceed because required scoped variables for the child graph are missing, runtimes SHOULD return a structured error (e.g. subagent_env_required) with a request ID and expose a temporary bootstrap endpoint:
GET /threads/{parent_thread_id}/variables/{request_id}— returns required/missing variable names.POST /threads/{parent_thread_id}/variables/{request_id}— stores the provided values and immediately boots the deferred child using the original creation payload.
6. Parent ↔ Child Communication
6.1 Parent → Child
- initial invocation mapping (
initUserMessageProperty,initAttachmentsProperty) - ongoing messaging (
subagent_message, orqueueMessageon the child’sThreadStatefrom code)
Parent traffic enters the child in the side B perspective (role: 'user'): the child’s side A acts on it, exactly as it would for an external caller. Routing is structural in both directions — parent→child entry is uniform, and child→parent delivery is determined by the relationship’s owning side (§2.1).
Attachment paths MUST be copied from the parent filesystem to the child filesystem before queuing (§7).
6.2 Child → Parent: Completion and Failure
When the child session ends via sessionStop and the effective parentCommunication is implicit, the parent receives a queued silent message on the owning side’s perspective:
Subagent (reference: {uuid}) has returned the following result:
{result}
sessionFail similarly delivers a failure message. Returned attachments (if any) are copied child → parent and included as parent-local paths. Session end transitions registry status — it never terminates the child thread (§4.1).
6.3 Child → Parent: Status
sessionStatus (and state.setStatus()) updates the parent’s registry status text without ending child execution.
6.4 Child → Parent: Explicit Escalation
When the effective parentCommunication is explicit, runtimes MUST NOT auto-queue completion/failure to the parent. Because the auto-queued completion is what triggers a parent turn, explicit means a child’s finish does not wake the parent at all. Tools, hooks, and handlers escalate deliberately:
state.notifyParent(content)state.setStatus(status)state.stopSession({ result, notifyParent, status })
Such children deliver results through another channel — the parent’s key-value store, for example — which the parent picks up on its next natural turn.
6.5 Provenance Metadata
Runtimes SHOULD set standard, OPTIONAL provenance metadata on cross-thread traffic (see Messages §1.1):
metadata.subagent_id— the child instance reference a message relates to.metadata.forwarded_from— the origin thread/message of forwarded content.
These conventions replace ad-hoc bracket-tag prefixes in userland relay code. They are conventions, not requirements.
7. Attachment Copying
Subagent communication that crosses thread boundaries MUST copy filesystem references:
- parent → child for initial and ongoing messaging attachments
- child → parent for completion and failure payload attachments
The destination thread MUST receive destination-local attachment paths.
8. Queue and Termination
These are properties of the underlying thread, not of subagents specifically. See Threads:
- queued messages are durable and ordered; injected before the next step when executing, otherwise forcing the next turn.
terminate()is the only soft-destruction path: it aborts in-flight execution, rejects new execution and queued-message entry, and updates parent registry status toterminated.
9. Example
A coordinator owning an aliased pair of calendar teams and a blocking research child:
defineAgent({
name: 'assistant_agent',
sideA: {
prompt: 'coordinator',
subagents: [
{ agent: 'calendar_agent', as: 'work_calendar', scopedEnv: ['WORK_CAL_ID'] },
{ agent: 'calendar_agent', as: 'personal_calendar', scopedEnv: ['PERSONAL_CAL_ID'] },
{ agent: 'research_agent', blocking: true, maxInstances: 1 },
{ agent: 'approval_gate' }, // human-in-the-loop child — parks awaiting_human
],
},
sideB: { human: true },
});
The coordinator’s prompt automatically receives subagents, subagent_create, and subagent_message, with subagent_create.agent enumerating 'work_calendar' | 'personal_calendar' | 'research_agent' | 'approval_gate'.