Subagent Is a Seam: In-Process to Delegating Claude Code
A sub-Agent is a capability seam — in-process, remote, or another product can plug in. Core source: packages/subagent/.
Same sub-task “research this module and report back” — switch among four providers and delegate once each. Watch two things: what stays identical on both sides of the seam, and what changes with the impl. Then flip “attach persona requirement” and see how the capability latch blocks out-of-scope requests before start.
| Start-time capabilities | spawn |
|---|---|
| outputSchema (structured output) | Supported |
| depthLimit (delegation depth cap) | Supported |
| toolFilter (limit tools) | Supported |
| persona (swap persona) | Supported |
subagent-spawn-in-process/src/index.ts line 42, subagent-fork-in-process/src/index.ts line 62); Claude Code and Codex are both NO_START_CAPABILITIES (their src/index.ts lines 54 and 49). Error copy verbatim from packages/subagent/subagent/src/index.ts lines 490–493.Conclusion first. DSH didn’t build a standalone sub-Agent feature — it built a registry called ctx.subagents: any transport implementing SubagentProvider can register by name. The official release registers six: spawn (in-process fresh), fork (in-process with context), acp (protocol bridge), codex, claude-code (each starts a real product CLI process), sdk (remote DSH instance). Source: docs/subsystems/subagent.zh.md lines 5–7.
Input: the tool layer assembles the model’s delegation into a SubagentStartRequest with prompt, parent Agent, cancel signal, plus four optionals (structured-output schema, depth cap, tool filter, persona). What happens: the service checks the chosen provider’s static capability table first — each optional needs a matching capability flag; missing one throws UNSUPPORTED_CAPABILITY before start. Output: a SubagentRun handle; the parent awaits its result, which lands as an ordinary tool result. To the parent, all four providers return the same kind of thing.
Worth pausing on fork’s identity. Many frameworks make “carry parent context” a boolean. DSH makes fork its own provider because they differ by more than a switch: fork must slice a “balanced prefix of completed turns” from the parent log as seed, cutting at the last turn/end — in-flight turns are unbalanced and unreplayable, so they must be excluded. That’s a session-log contract a flag can’t explain.
fork and spawn are two providersDifference isn’t parameters — it’s the seed: fork slices the parent log to the last turn/end as child-session seed; spawn starts from zero. inheritsParentContext is descriptive only, so the tool layer can phrase honestly.
One-shot run vs continuable ActivationSubagentRun is one-and-done: await result, dispose, end. A continuable sub-Agent has no run — it’s a persistent session plus at most one resident Activation; the parent uses send_message to append turns, interrupt_agent to interrupt, and receives report.
Background end isn’t silentWhen a continuable sub-Agent settles, the manager unconditionally injects a subagent-settled notice to the parent with final output. It uses a different message-source kind than the sub-Agent’s own report, so the transcript won’t count runtime bookkeeping as the sub-Agent speaking.
First evidence is the capability latch itself — clear without pasting code. assertCapabilities turns the four optionals into a demand list: if the request carries outputSchema, the provider table must have outputSchema true; maxDepth demands depthLimit; toolFilter and persona likewise. Then item-by-item check — first mismatch throws SubagentError naming which provider lacks which capability, code UNSUPPORTED_CAPABILITY. No degrade, no warn-and-continue; no child process starts before this.
Source: packages/subagent/subagent/src/index.ts lines 481–495, verified on 2026-08-13. Demo error copy is verbatim from lines 490–493.
Second evidence is fork’s seed function — seven lines that say what “with context” actually carries:
function completedTurnPrefix(parent: Agent): SessionEvent[] {
const events = parent.session.events
const lastEnd = events.findLast(e => e.type === 'turn/end')
if (lastEnd === undefined) return []
// seq === array index (the append contract), so slice up to and including it.
return events.slice(0, lastEnd.seq + 1)
}
packages/subagent/subagent/src/index.ts and packages/subagent/subagent-fork-in-process/src/index.ts, verified on 2026-08-13. Code blocks keep the original source text.Two more mechanisms without pasted code — cite in prose. The whole Claude Code provider is: resolve the claude binary, start a real CLI via the official Agent SDK in the parent session’s cwd, hang it under the shared subprocess owner (subagent-claude-code/src/index.ts lines 62–91). In the four official presets, codex and claude-code delegation tool rows ship with disabled: true; comments say: copy the preset, delete disabled, and that product backend opens only for the copied session (apps/cli/config/agent-presets/standard/agent.cordis.yml lines 200–219). Delegating to another product in DSH is a config switch.
Claude Code puts delegation in the product layer. One Task tool (AgentTool) entry packs shapes into params: subagent_type picks role, run_in_background backgrounds, isolation: "worktree" opens an isolated copy, model swaps models (manuscript study/chapters/05-multi-agent.md lines 32–52 citing AgentTool.tsx). Above that sit Coordinator Mode and Agent Teams. Expressive — but every shape is a feature branch inside one product: the delegate is always another Claude Code instance; handing work to another product has no slot. DSH chooses the opposite: product differences sink to the provider layer; above the seam there’s one vocabulary.
Grok Build takes a third path: extract sub-Agent config resolution into a pure-logic crate xai-grok-subagent-resolution, resolving effective config by explicit override > role > persona > parent (that crate’s src/lib.rs lines 7–8), while execution stays in their own shell process. It abstracts what a sub-Agent looks like; DSH abstracts where a sub-Agent runs. Grok’s roles, context inheritance, and tree lineage already have three lessons on this site: Four sub-Agent roles, Context inheritance & depth control, Sub-Agent session lineage.
Walk through an out-of-scope delegation
Deploy enables the subagent_claude_code tool; the model’s delegation carries outputSchema (wants structured output). Walk the capability-latch code from this lesson: which line throws, what error code, and did the Claude Code CLI process ever start? Then: if DSH accepted the request but ignored the schema, what goes wrong in the parent’s tool result, and why is that harder to debug than failing loud?