Skip to main content
PathSubagents

Subagents and Parallelism

An Agent can delegate tasks to subagents. DSH's ctx.subagents is a capability seam: the caller uses a single ctx.subagents API, and the specific provider decides whether a subagent runs in this process, another process, or through a future transport. This is the mechanism for "processing complex tasks in parallel": splitting a multi-step task across several isolated subagents that advance concurrently.

Two kinds of subagent

DSH distinguishes two shapes with different responsibilities:

ShapeDescriptionKey APIs
one-shotspawn a subagent to do work and get a final resultctx.subagents.start(name, request)
continuablebuild a persistent sub-session that can receive multiple messages, be interrupted, and cold-resumestartContinuable(spec) / followup / interrupt

The model-facing side is tool-subagent (one-shot, ctx.subagents.start) and tool-subagent-control (send_message / interrupt_agent / list_agents), the latter being the control surface for the "continuable sub-sessions" covered in this page.

One-shot delegation

// the request can: pick a model, ask for structured output, limit delegation depth, limit subagent tools, set a sub-persona
const run = await ctx.subagents.start('spawn', {
prompt: 'Help me analyze the TODOs in this repo',
outputSchema, // optional: strongly constrains the final result structure
depthLimit, // optional: caps delegation depth
toolFilter, // optional: limits which tools the subagent sees
signal, // required: the cancellation channel before publication
})
const result = await run.result // the one-shot subagent's final result
  • label is an optional persistent display label
  • signal is the canonical cancellation channel for a one-shot start: abort before publication → rejection rolled back; abort after publication → cancels remaining rounds but does not hide the returned run
  • A provider advertises support through provider.capabilities (outputSchema / depthLimit / toolFilter), rejecting before creating a sub-session if unsupported

Continuable sub-sessions (continuable)

const { childId, messageId } = await ctx.subagents.startContinuable({
label: 'research-assistant',
initialPrompt: 'This is a resident research assistant; reach me anytime',
})
// send a later message (only released after exact direct-parent authentication)
await ctx.subagents.followup(parent, childId, 'Check the latest MCP progress again')
// interrupt a continuable subagent's current round (keeps its inbox and descendants)
await ctx.subagents.interrupt(targetSessionId, authority)
// list direct session-level subagents / flatten the whole session tree
await ctx.subagents.listChildren(parentSessionId)
await ctx.subagents.listDescendants(rootSessionId)

Key properties (source README essentials):

  • follow-up authority comes from the "exact direct parent" recorded in the subagent's persistent header: a parent deregistered/replaced during reassembly cannot authorize delivery
  • continuable sub-sessions require ctx.agents + session persistence + a provider with a prepareContinuable capability
  • Cold resume: when the parent agent is absent, rebuilds from the persisted Session (also requires session persistence)
  • reportFrom delivers one of the subagent's messages back to its direct parent (quiet = injected into context; waking = triggers a new round for the parent)

The provider family

Under packages/subagent/ the same seam has multiple implementations. subagent-in-process-driver is the shared run driver for the spawn / fork two in-process providers (not a separately registered provider name), while the other six are providers that getProvider(name) can retrieve:

providerWhere the subagent runsMount
spawna brand-new Agent in the current processmounted by default (base)
forkcurrent process, seeded with the parent's completed roundsmounted by default (base)
acpa fresh subprocess, via Agent Client Protocolopt-in
claude-codea fresh subprocess bridging the Claude Code CLIprovider mounted by default, delegation tool disabled by default
codexa fresh subprocess bridging the Codex CLIprovider mounted by default, delegation tool disabled by default
dsh-sdka fresh subprocess with a full DSH runtime (JSON-RPC)opt-in

Multiple providers can coexist behind a single ctx.subagents; getProvider(name) fetches by name.

in-process-driver: the shared run driver

subagent-in-process-driver is the single implementation for the spawn / fork two in-process providers — spawn passes no session seed, fork passes the parent's completed-round prefix, and everything else (depth validation, subagent creation, persona/tool-filter/structured-output installation, result reading, cancellation, dispose) lives in this one implementation:

  • startInProcessRun only fulfills once the subagent is published on ctx.agents; when startup is rejected, the unpublished creation transaction has already quiesced, so the caller never gets a half-created handle
  • depth reads the parent's delegationDepth (persistent header authoritative, runtime can only deepen, never lower), child depth = parent + 1 written into the child's header, so persistence + resume both preserve the budget; exceeding maxDepth reports a precise error
  • structured output installs a whole contract: a structured_output tool + an order-190 system prompt section + an observer that only commits after that execution's final tool result succeeds + a single-call guard + concludeTurn wrapping up
  • The same driver also installs a persona shadow and tool filtering for the subagent (removes global wire schemas / executable lookup / Code Mode SDK bindings, but keeps independently registered guidance sections), and applies the seam's delegation policy (parent's explicit sandbox override + never approval pin)

spawn: same-process new Agent

spawn creates a brand-new subagent in the current process: it has its own session, can't see the parent's conversation history, and reuses the host's agent factory and LLM/tool services. It advertises { outputSchema, depthLimit, toolFilter, persona } all true, because it can forcefully impose those four capabilities within the subagent's creation window. The subagent inherits the parent's working directory/session lineage and model (unless overridden), but starts from an empty conversation.

fork: same process + the parent's completed rounds

fork shares every run mechanism with spawn; the only difference is the session seed: it feeds the subagent the contiguous prefix of the parent session before its last turn/end — the parent's current in-flight round is excluded (otherwise the child would get an unbalanced session). It passes only the conversation history, inheriting neither the parent's tool limits nor authority. Capabilities advertised match spawn. Because a continuable child additionally carries a report tool and prompt section that would break the parent prefix cache fork wants to reuse, the delivery composition binds fork to backgroundMode: one-shot.

acp: an ACP client in a subprocess

acp runs each subagent in a fresh subprocess, driven as an Agent Client Protocol client: it fulfills only after spawn → ACP initializenewSession all succeed, meaning the remote session is ready and ownership is handed to the caller. It advertises no start-time capabilities (can't forcefully impose depth / toolFilter / persona / structured output in a remote process), inheritsParentContext: false, and the only thing carried over from the parent side is the working directory. permission: reject|allow auto-answers the sub-session's permission requests (no pop-up to a human). One fresh process per run, no process pool.

claude-code / codex: bridging two CLIs

These two are fixed providers:

  • claude-code calls the official Claude Agent SDK's query(), resolving the native claude executable in the parent-session workspace, submitting a self-contained text task, and returning only the strict final answer. The SDK reads the host's native Claude settings and auth, with persistSession: false and AskUserQuestion disabled (unattended).
  • codex starts the official codex app-server --stdio, creates an ephemeral thread, and likewise returns only the final answer. Under unattended operation, command/file approvals pick cancel/decline, and permission requests get the empty set. contextWindowExceeded maps to max-tokens; other exceptions map to error.

Neither advertises optional capabilities, inheritsParentContext: false; the child's model/tools/permissions/auth all come from the original product install. The provider rows mount by default on base's host plane (loaded but starting no product process), while the corresponding delegation tool rows are disabled: true in the standard preset — copy the preset and remove disabled to expose subagent_claude_code / subagent_codex to agents composed from that copy.

dsh-sdk: a full harness runtime in a subprocess

dsh-sdk runs each subagent as a complete DeepSeek Harness runtime in a fresh subprocess, communicating over stdio JSON-RPC through a TypeScript SDK client. The difference from acp is in the wire and child contract: the subprocess is a full peer harness — its own cordis.yml composition, session persistence, model routing, and tools. It likewise advertises no start-time capabilities and inheritsParentContext: false; its delegation tool sets maxDepth: provider-managed (the child harness holds its own recursion budget). A full runtime starts the whole plugin tree per run, so a single spawn costs more than acp's typical subprocess.

When to use it

  • Parallelism: split an independent subtask (fetch + summarize + write a file) across three subagents running at once
  • Isolation: a subagent has its own scope and tool limits and won't pollute the parent session
  • Continuable conversations: resident assistants needing state held across multiple rounds (use continuable)
  • Get a one-time result (use one-shot start)

Verification

# see the available subagent providers in the composition tree
dsh web --dump-config | grep -iE "subagent"
# see subagent events in the session log
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E "subagent/" | head

Next steps