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 streamThe package family
| Package | Role | ctx key | Transport |
|---|---|---|---|
subagent | Service Definition: provider registry, contracts, descriptors, continuation | ctx.subagents | — |
subagent-in-process-driver | Shared one-shot run driver (depth, composition, structured output) | — | in-process |
subagent-spawn-in-process | Fresh child, no parent history | registers on ctx.subagents | in-process |
subagent-fork-in-process | Child seeded with parent's completed turns | registers on ctx.subagents | in-process |
subagent-dsh-sdk | Out-of-process Harness child via the TS SDK | registers on ctx.subagents | external process |
subagent-acp | Out-of-process child over the Agent Client Protocol | registers on ctx.subagents | external process |
subagent-claude-code | Real Claude Code child (official SDK) | registers on ctx.subagents | external process |
subagent-codex | Real Codex app-server child | registers on ctx.subagents | external process |
tool-subagent | Model-facing delegation tool (subagent) | registers on ctx.tools | — |
tool-subagent-control | Model-facing send_message / interrupt_agent / list_agents | registers on ctx.tools | — |
tool-subagent-report | Child-scoped report return channel | registers in child scopes | — |
client/ui-subagent | Web catalog tree, @ reference, composer control | ctx 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.
| Member | Meaning |
|---|---|
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:
| Field | Meaning |
|---|---|
prompt | Content delivered as the child's user message. |
parent | The spawning agent; in-process providers derive workspace, lineage, and delegation depth from it. |
signal | The canonical cancellation channel, before and after startup. |
agentOptions | Optional child provider, model, maxTokens overrides. |
outputSchema | Object-rooted JSON Schema for a structured final result (needs outputSchema capability). |
maxDepth | Absolute delegation-depth cap for the child (needs depthLimit). |
toolFilter | Child tool restriction, applied as a scoped tools.restrict() (needs toolFilter). |
persona | Per-child persona, shadowing the deployment persona (needs persona). |
label | Optional 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:
| Aspect | spawn | fork |
|---|---|---|
| Session seed | none | balanced completed-turn prefix (up to the last turn/end) |
| Parent history visible | no | yes (one-time snapshot) |
| Capabilities | all four | all four |
| Model | inherits parent unless overridden | copies inherited prefix bytes for cache reuse |
| Continuable path | usable | implemented but no shipped composition uses it (bind to backgroundMode: one-shot) |
The shared driver's startInProcessRun() clears the parent depth, 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-acpspeaks the Agent Client Protocol (@agentclientprotocol/sdk) over a child subprocess with a configurablepermissionpolicy (allow/reject) for the child's own permission prompts.subagent-codexdrives a real Codex app-server child.subagent-claude-codedrives Claude Code through the official Claude Agent SDK.subagent-dsh-sdkruns 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:
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:
| Config | Default | Meaning |
|---|---|---|
provider (required) | — | Provider name (spawn, fork, acp, …) |
toolName | subagent | Model-facing name, distinct per instance |
enableRunInBackground | true | Exposes background mode |
backgroundMode | one-shot | one-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).
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 wakeup (default, 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; onlyinterrupt()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.subagentspowers. - 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'sREADME.mdunderpackages/subagent/<provider>/. - Agent Notes that own the decisions:
.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.mdand2026-07-21-continuable-background-subagents.md. - The full contract types:
packages/subagent/subagent/src/types.ts.