Skip to content

What a subagent is ​

A subagent is a child agent with its own session, its own scope, and its own turn loop that one parent agent starts to offload a self-contained task. In DeepSeek Harness a subagent is not a special runtime mode — it is a capability seam (ctx.subagents, owned by packages/subagent/subagent) backed by a registry of named providers, so an agent delegates through one service API while the transport is swappable. Unlike a shell seam (one executor), the subagent seam lets multiple providers coexist by name. The second difference from a shell tool is that a subagent is not a tool: it is a scoped service, and the tools the model actually calls (subagent, send_message, report, …) are thin consumers of it.

The domain vocabulary lives in the glossary: a subagent gets its own scope, its parent/child facts travel as lineage data, and a creator composes the child's world in the setup window. Scope is deliberately flat — scoped registrations do not inherit down to children; anything the child needs is composed explicitly during setup.

parent Agent ──ctx.subagents.start/startContinuable──▶ provider
                                                          │ (spawn | fork | acp | codex | claude-code | dsh-sdk)
                                                          ▼
                                               child Agent + child Session
                                               (own scope, own turn loop)
                                                          │ report / settlement
                                                          ▼
                                                    parent turn stream

The package family ​

PackageRolectx keyTransport
subagentService Definition: provider registry, contracts, descriptors, continuationctx.subagents—
subagent-in-process-driverShared one-shot run driver (depth, composition, structured output)—in-process
subagent-spawn-in-processFresh child, no parent historyregisters on ctx.subagentsin-process
subagent-fork-in-processChild seeded with parent's completed turnsregisters on ctx.subagentsin-process
subagent-dsh-sdkOut-of-process Harness child via the TS SDKregisters on ctx.subagentsexternal process
subagent-acpOut-of-process child over the Agent Client Protocolregisters on ctx.subagentsexternal process
subagent-claude-codeReal Claude Code child (official SDK)registers on ctx.subagentsexternal process
subagent-codexReal Codex app-server childregisters on ctx.subagentsexternal process
tool-subagentModel-facing delegation tool (subagent)registers on ctx.tools—
tool-subagent-controlModel-facing send_message / interrupt_agent / list_agentsregisters on ctx.tools—
tool-subagent-reportChild-scoped report return channelregisters in child scopes—
client/ui-subagentWeb catalog tree, @ reference, composer controlctx events + ctx.inputTriggers—

The service contract (dsh-subagent) ​

The heart of the seam is the SubagentRuntime service in packages/subagent/subagent/src/index.ts, with the typed contracts in src/types.ts.

MemberMeaning
registerProvider(provider)Register a trusted same-process provider by name; duplicate names fail loud.
start(name, request)One-shot foreground delegation; resolves with a holder-owned SubagentRun after the child is published.
startContinuable(spec)Establish one durable continuable child and deliver its initial prompt; returns { childId, messageId }.
followup(parent, childId, content, …)Deliver a later message from the exact live direct parent as the child's next FIFO turn.
interrupt(targetSessionId, authority)Stop one live continuable child's current turn (keepInbox: true).
reportFrom(child, content, …)Deliver a selected message from the exact live continuable child to its direct parent.
listChildren(parentSessionId) / listDescendants(rootSessionId)Enumerate the durable subagent catalog.

The provider contract ​

SubagentProvider (in src/types.ts) is the interface transports implement: { name, capabilities, inheritsParentContext, start(request), prepareContinuable?() }. capabilities is four booleans that the service validates before delegating — outputSchema, depthLimit, toolFilter, persona — so a request needing an unsupported feature is rejected loudly rather than accepted-then-ignored. inheritsParentContext is descriptive, not enforceable: it says only whether the child sees completed parent history (fork does; spawn and out-of-process providers do not), never whether it inherits tools, services, or authority.

prepareContinuable?() is the optional method whose presence is the continuation capability. It returns only a detached ContinuableCreateSpec ({ seed? }) — data, never a capability — because the continuation manager owns identity reservation, composition, agent creation, prompt delivery, cold resume, ownership, and disposal after preparation.

Creation options ​

SubagentStartRequest carries the one-shot delegation options. The model-facing tool builds these from the model's { description, prompt } plus its own config:

FieldMeaning
promptContent delivered as the child's user message.
parentThe spawning agent; in-process providers derive workspace, lineage, and delegation depth from it.
signalThe canonical cancellation channel, before and after startup.
agentOptionsOptional child provider, model, maxTokens overrides.
outputSchemaObject-rooted JSON Schema for a structured final result (needs outputSchema capability).
maxDepthAbsolute delegation-depth cap for the child (needs depthLimit).
toolFilterChild tool restriction, applied as a scoped tools.restrict() (needs toolFilter).
personaPer-child persona, shadowing the deployment persona (needs persona).
labelOptional durable display label for a session-backed child.

The setup window and delegation policy ​

Composition happens inside the child's setup window — after the scope and agent exist, before the agent/session is published. For in-process children, applyChildComposition(childCtx, parent, composition) (in packages/subagent/subagent/src/child-agent.ts) joins the parent's agent-preset composition before applying the child's own persona and tool filter — the join is what gives a model-facing child a working tool registry. childSessionMeta() records the joined preset id on the durable header so a cold read rebuilds the same composition.

Delegation also fixes the child's permission scope at the boundary (src/child-agent.ts helpers): captureDelegatedPolicyOverrides(parent) snapshots the parent's explicit sandbox override and pins the child's approval policy to 'never' whenever the approval capability is composed, so every sandbox_permissions ask is rejected deterministically. appendDelegatedPolicyOverrides() writes each value to the child's own log as a source: 'delegation' sandbox/mode or approval/policy event. Every in-process child also gets a scoped runtime-context statement (subagent:delegation) telling it the scope is fixed.

In-process vs spawned vs external backends ​

The in-process providers share one run driver (in subagent-in-process-driver). Spawn and fork differ only in their seed:

Aspectspawnfork
Session seednonebalanced completed-turn prefix (up to the last turn/end)
Parent history visiblenoyes (one-time snapshot)
Capabilitiesall fourall four
Modelinherits parent unless overriddencopies inherited prefix bytes for cache reuse
Continuable pathusableshipped presets bind spawn/fork to it; the tool default is backgroundMode: one-shot, which the disabled-by-default codex/claude-code rows use (maxDepth: provider-managed). Opting in copies the preset, removes disabled, and mounts the provider on the host plane

The shared driver's startInProcessRun() validates the parent depth and derives the child's as parent depth plus one (persisted in the child session header), calls parent.ctx.agents.create directly, installs persona/tool-filter/structured-output in the unpublished setup window, drives one task with child.followup(prompt) + child.whenIdle(), then reads the child's output.

The external providers run the child in a separate process and return localAgent: undefined, so their one-shot runs are not part of trace-backed enumeration:

  • subagent-acp speaks the Agent Client Protocol (@agentclientprotocol/sdk) over a child subprocess with a configurable permission policy (allow/reject) for the child's own permission prompts.
  • subagent-codex drives a real Codex app-server child.
  • subagent-claude-code drives Claude Code through the official Claude Agent SDK.
  • subagent-dsh-sdk runs an out-of-process Harness child through the TypeScript SDK.

Delegation lineage and depth ​

Parent/child facts are carried as lineage data, never by scope structure: the child's SessionHeader records parentSession, a durable delegationDepth, and identity origin: 'subagent'. Depth accounting lives in packages/subagent/subagent/src/depth.ts:

ts
export function delegationDepthOf(agent: Agent): number {
  const runtime = agent.options.subagentDepth
  if (runtime !== undefined && (!Number.isSafeInteger(runtime) || runtime < 0 || Object.is(runtime, -0)))
    throw new TypeError('agent subagentDepth must be a non-negative safe integer')
  // The header value was validated at the session boundary (creation and load).
  return Math.max(agent.session.header.delegationDepth ?? 0, runtime ?? 0)
}

The persisted SessionHeader.delegationDepth is authoritative and monotone — runtime options may deepen it but never lower it — so a resumed child cannot be re-counted as top-level. assertSubagentMaxDepth() validates a recorded cap. The model tool's maxDepth default is 3 (0 forbids delegation).

The result contract ​

A one-shot SubagentRun is { id, localAgent, result, dispose() }. result resolves with SubagentResult = { output, structured?, stopReason }. The stop reasons mirror the harness turn vocabulary — completed, aborted, error, max-tokens, refusal. Crucially, result does not reject on a child-level failure (it resolves with stopReason: 'error' so a consumer can map it to an errored tool result); it rejects only on an infrastructure fault the seam cannot represent. dispose() is idempotent and cancels remaining work. The AssistantOutputFold/finalAssistantOutput helpers select the child's last non-empty assistant message (usage-only messages are skipped), else its accumulated assistant text.

Continuable children (the Activation) ​

A continuable child has one durable Session and at most one process-local Activation — a residency epoch for a reconstructed child Agent. The Agent inbox is the only turn queue, so the continuation manager (in src/continuation.ts) owns residency while the Agent loop owns turn ordering and execution. Every continuation message is Agent.followup() and becomes one FIFO turn. Routing depends only on residency: running enqueues, waiting wakes the same Agent, an absent Activation cold-resumes a new one from the durable subagent/descriptor. The manager cold-resumes never dispatches through a provider — the folded descriptor is the whole reconstruction input. On settlement, the child's durable direct parent receives a settlement notice (source kind subagent-settled), delivered before ownership release, as one ordinary later turn (waking) or by injection into a draining lineage.

The three model-facing tools ​

dsh-tool-subagent — delegation ​

Each plugin instance binds one provider to one toolName (default subagent). The model receives the { description, prompt } plus optional run_in_background:

ConfigDefaultMeaning
provider (required)—Provider name (spawn, fork, acp, …)
toolNamesubagentModel-facing name, distinct per instance
enableRunInBackgroundtrueExposes background mode
backgroundModeone-shotone-shot (Task-backed job) or continuable (durable child id)
agentOptions / persona / toolFilter / maxDepth—Child customization passed into start()

Foreground awaits run.result and always dispose(). One-shot background registers a plain parent-owned Task and returns { kind: 'background', jobId }. Continuable background calls ctx.subagents.startContinuable() and returns { kind: 'continuable', subagentId }.

dsh-tool-subagent-control — send_message / interrupt_agent / list_agents ​

Optional globally named control tools over ctx.subagents, registering once so multiple delegation tools never duplicate them. send_message(subagent_id, message) becomes the child's next FIFO turn and returns no reply. interrupt_agent(agent_id) stops only the current turn (keepInbox), parking queued messages. list_agents(scope: 'children' | 'descendants') projects the durable catalog to continuable children with status running / idle / ready (storage-only, resumable-not-terminal).

The experimental tool-agent-team (packages/experimental/) registers the same-named list_agents / interrupt_agent / wait_agent tools for its teammate model instead.

dsh-tool-subagent-report — child→parent report ​

A child-scoped tool installed into continuable child scopes via registerContinuableSetup(), not globally. report(output: string) reaches exactly the child's live direct parent (derived from durable parentSession); reportDelivery selects next-step (default — wakes the parent and enters at its nearest step boundary, as one ordinary parent turn) or quiet (parent.inject(), context without a model request). Report success returns the stable parent-accepted MessageId, not a delivery receipt.

Lifecycle events ​

The service emits subagent/start / subagent/end (scoped to the delegating parent, sharing a runId, with a local flag) for every one-shot run and every resident continuable Activation epoch, plus subagent/provider-added / subagent/provider-removed. Providers record the durable subagent/descriptor session event (versioned, snapshotSubagentDescriptor() / foldSubagentDescriptor()), which is log-only: absent from model history, retained across compaction.

UI (briefly) ​

client/ui-subagent contributes the lazily expandable subagent catalog tree to conversation.session.header.actions, reason-specific read-only composer replacements, and the existing @ reference source to ctx.inputTriggers. It reads subagentsByParent and session summaries through the standard useSessions hook; selecting a row calls SessionRuntime.openSubagent() with the exact { parentSessionId, childSessionId, mode } address. The catalog is model-free: subagent-origin rows are omitted from the sidebar, so the header catalog is their navigation entry point.

Known limitations ​

  • ACP children stay one-shot and non-trace-enumerable — they have no local child session in the parent's corpus.
  • No host-user continuation — followup() requires the exact live direct parent; only interrupt() accepts a durable parent-address.
  • No current-turn steering — continuable messages and waking reports enqueue later turns.
  • Process-local residency — Activations do not coordinate two harness processes; a durable mailbox and lease protocol are deferred.

Further reading ​

  • Session query & log export and Workflow & Ralph — sibling orchestration families that ctx.subagents powers.
  • Glossary — entries for scope, setup window, lineage, goal round.
  • The subsystem reference in the repo: docs/subsystems/subagent.md.
  • The seam's own README: packages/subagent/subagent/README.md, plus each provider's README.md under packages/subagent/<provider>/.
  • Agent Notes that own the decisions: .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md and 2026-07-21-continuable-background-subagents.md.
  • The full contract types: packages/subagent/subagent/src/types.ts.