Copy page
View as Markdown View this page as plain text

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
}
PropertyDescription
agentThe agent definition to instantiate. Any agent is eligible; exposeAsTool: true marks it as designed for composition and supplies the default toolDescription.
asThe 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).
descriptionModel-facing description override for this relationship.
maxInstancesCap on simultaneous instances. When reached, creation attempts SHOULD return a tool error explaining the cap and suggesting messaging an existing instance.
immediateSpawn at thread creation, before any side takes a turn (§3.2).
blockingsubagent_create waits for the child session to close and returns its result (§3.3).
parentCommunicationimplicit (default) auto-queues child completion/failure to the parent; explicit leaves escalation to tools, hooks, and handlers (§6.4).
scopedEnvEnvironment values copied into the child thread; runtime-only transfer data.
initUserMessageProperty / initAttachmentsProperty / initAgentNamePropertyMap creation arguments into the child’s initial user message, attachments, and display name.
optionalThe 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.
hiddenThe 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 — a tools entry attempting to configure a subagent relationship is a definition-time error.
  • Handler-backed owning side: handlers need no tool grant; they reach children through ThreadState directly. 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_create lifecycle tool (prompt-backed sides),
  • runtime code (invokeTool / queueTool on the lifecycle surface, or dedicated runtime APIs) from tools, hooks, and handlers,
  • immediate configuration 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 nameEnv and descriptionEnv as model-visible hints when deriving initial child arguments via a bootstrap pass.
  • runtimes MUST treat scopedEnv as runtime-only transfer data.
  • runtimes MUST NOT expose scopedEnv values to the model unless the same env name is explicitly designated by nameEnv or descriptionEnv.

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 (runningidle / 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.
  • blocking is 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 call subagents before subagent_create so existing instances are reused rather than duplicated.
  • subagent_create — creates (or continues, §3.3) an instance. Its agent argument MUST enumerate the side’s enabled relationship names. It MUST require a non-empty name argument 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.arguments and 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_required error 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, or queueMessage on the child’s ThreadState from 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 to terminated.

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'.