Files
deepseek-harness/packages/subagent/subagent

@deepseek-ai/dsh-subagent

English | 中文

The subagent seam lets one agent delegate work to a child through a named provider. Callers use one service API (ctx.subagents); providers decide whether the child runs in this process, in another process, or through a future transport.

Package roles

The family separates the stable interface from implementations and model-facing tools:

Package Role
@deepseek-ai/dsh-subagent Provider registry, request/result/descriptor types, lifecycle events, and continuable-child orchestration.
@deepseek-ai/dsh-subagent-spawn Fresh in-process child; supports continuable children.
@deepseek-ai/dsh-subagent-fork In-process child seeded with completed parent turns; supports continuable children.
@deepseek-ai/dsh-subagent-acp Fresh out-of-process ACP child (one-shot).
@deepseek-ai/dsh-tool-subagent Model-facing delegation tool over one configured provider.
@deepseek-ai/dsh-tool-subagent-control The globally named send_message follow-up tool.
@deepseek-ai/dsh-tool-subagent-report Child-scoped return channel to the direct parent.

Multiple providers may coexist under different names. This lets a deployment expose, for example, a cheap in-process child and an isolated ACP child without changing the service contract.

Service API

SubagentService has these operations:

Member Meaning
registerProvider(provider) Register one trusted same-process implementation by name. Registration is effect-scoped; removing it prevents new starts but does not revoke runs already returned to callers. Duplicate names fail loud.
getProvider(name) Return the provider, or undefined when absent.
list() Return provider names in insertion order.
start(name, request) Validate an ordinary caller request, resolve its detached one-shot descriptor, then await the provider until a real one-shot child is published. Fulfillment returns a holder-owned SubagentRun; rejection means the provider has already cleaned every unpublished startup resource, while post-publication turn or infrastructure faults settle through the run. Continuable children never enter through this operation.
startContinuable(spec) Establish one durable continuable child and deliver its initial prompt. Resolves with { childId, messageId } when the child's inbox accepts that prompt, without waiting for the turn to start or for the message to reach the Session log; any earlier failure rejects with no ids and rolls the child back entirely. Requires ctx.agents, session persistence, and a provider with the prepareContinuable capability.
followup(parent, childId, content, { source, signal }) Deliver one later message from the exact live direct parent as the child's next FIFO turn, matching Agent.followup() terminology, and return the accepted MessageId. A resident child's inbox accepts it directly (waking a waiting Activation); an absent one cold-resumes from its persisted Session. Requires ctx.agents; cold resume also requires session persistence.
reportFrom(child, content, { delivery, signal }) Deliver one selected message from the exact live continuable child to its exact live direct parent and return the accepted stable MessageId. Quiet delivery injects context; waking delivery submits one later parent turn.
registerContinuableSetup(contribution) Compose an optional deployment capability into each continuable child's unpublished scope, with immediate revocation from resident children.
drainContinuableDescendants(parents) Close admission below exact live host-owned parent Agents, stop only their visible continuable descendants, await materializations admitted below those roots through publication or rollback, then release the selected forests child-first. The cutoff lasts until each exact parent leaves the registry; unrelated parent forests and manager-wide admission remain live.
listChildren(parentSessionId, signal?) List direct session-backed subagents with their one-shot/continuable mode, running/inactive activity, origin-classified one-level hasChildren hint, and per-child diagnostics, in stable trace order without loading or resuming them. Requires session query; it does not require ctx.agents or the continuation manager.

SubagentStartRequest.label is an optional short durable display label for a session-backed one-shot child. Model-facing delegation supplies its existing description; lower-level callers need not invent presentation metadata. Continuable starts always carry their own required label. signal is required and is the canonical cancellation channel for a one-shot start. An abort before publication makes start() reject after rollback; an abort after publication cancels the returned run's remaining turn work without hiding its id. The request may also select a model, require structured output, cap delegation depth, restrict child tools, or set a child persona. For a continuable start or follow-up, the caller signal owns lookup, materialization, and admission only until inbox acceptance; afterward the manager owns the Activation independently, so later caller cancellation neither cancels the accepted turn nor disposes the child.

Follow-up authority comes from the exact live direct parent recorded in the child's durable header. Cold resume checks that authority before reconstruction and again in the final no-await inbox-admission span, so a parent unregistered or replaced during materialization cannot authorize delivery. The source on a follow-up is durable provenance retained on the delivered message and grants no authority.

Same-process requests, descriptors, results, and event payloads are trusted typed values borrowed as immutable. The service does not clone or freeze them; serialization and hostile-input validation belong at actual process, worker, persistence, and model boundaries.

Capabilities

Start-time features are advertised in provider.capabilities because the service must reject an unsupported one-shot request before child creation:

  • outputSchema — enforce a structured final result.
  • depthLimit — enforce maxDepth.
  • toolFilter — apply the requested child tool restriction.
  • persona — apply a per-child persona.

Continuable creation is the optional SubagentProvider.prepareContinuable?() method: its presence is the capability check, so the service rejects a configured continuable start on a provider without it, while a provider that has it may still serve ordinary one-shot delegations. The method returns only a detached ContinuableCreateSpec ({ seed? }) — data, never a capability: it carries no Agent, AgentHandle, prompt delivery, result, disposal, or resume operation, because the continuation manager owns identity reservation, composition, Agent creation, prompt delivery, cold resume, ownership, and disposal after preparation. A one-shot SubagentRun represents one disposable foreground delegation with one result and no cold-resume operation.

The durable descriptor

The seam owns the versioned subagent/descriptor session event vocabulary (src/descriptor.ts): snapshotSubagentDescriptor() validates and detaches the record before provider work, and foldSubagentDescriptor() validates the complete current-version payload before recovering it from a loaded child log. Every local session-backed start appends one descriptor with the provider name and lifecycle mode. A one-shot descriptor optionally carries the caller-owned durable display label; a continuable descriptor requires its durable creation label and additionally records resolved child agentOptions.provider/model and optional persona/toolFilter for cold resume. These are explicit fields, never the merge-extensible AgentOptions object, so an unrelated extension value cannot break continuation. The descriptor omits subagentDepth (the persisted header's delegationDepth is the monotone floor) and outputSchema (an Activation's result contract). The event is log-only: no surfaceOp, absent from model history, and retained by the append-only log across compaction. Malformed current-version payloads are corrupt; unsupported versions cannot be classified by this runtime.

Delegation depth

The seam owns the depth vocabulary shared by implementations and consumers: the AgentOptions.subagentDepth declaration, assertSubagentMaxDepth, and delegationDepthOf(agent). The persisted SessionHeader.delegationDepth is authoritative and monotone — runtime options may deepen the count but never lower it, so a resumed child cannot be re-counted as top-level.

inheritsParentContext is descriptive rather than enforceable. It says only whether the child sees completed parent conversation history (fork does; spawn and ACP do not), not whether it inherits tools, services, or authority.

One-shot ownership and lifecycle

provider.start(request): Promise<SubagentRun> is the ownership-transfer boundary; the delegation tool also uses it inside its one-shot Task-backed background path. Before fulfillment, the provider owns setup and must cancel, roll back, and quiesce unpublished resources on every failure. After fulfillment, the caller owns the run and must call dispose() on every path; remaining prompt and turn work belongs to SubagentRun.result.

SubagentRun.result resolves to { output, structured?, stopReason }. Child-level failures resolve with a non-completed reason; only an infrastructure fault that the seam cannot represent may reject. dispose() is idempotent, cancels remaining work, and waits for both result settlement and child-resource quiescence. A result rejection remains on result; dispose() rejects only for an independent resource-release failure.

A local run publishes an ordinary child agent/session before start() fulfills, returns that shared session id as SubagentRun.id, exposes the exact child as SubagentRun.localAgent, records request.parent.session.id in the child's parentSession header, and appends the resolved descriptor inside its initial turn. Remote providers instead mint a parent-scoped lifecycle id and return localAgent: undefined; without a local child session, their one-shot runs are not part of trace-backed enumeration.

Continuable children and Activations

A continuable child has one durable Session and at most one process-local Activation — one residency epoch for a reconstructed child Agent, not a request, result, cancellation, or Task boundary. The Agent inbox is the only turn queue, so the continuation manager owns residency while the Agent loop owns all turn ordering and execution. No continuable path creates a Task or an intermediate result-bearing wrapper.

The manager derives three internal residency conditions from Agent quiescence and the owned-child set rather than maintaining a second state machine: running (an active admission, open turn, or waking inbox work), waiting (quiescent but still owning at least one undisposed child), and settled (quiescent with every owned child disposed, so the manager disposes the AgentHandle and removes the Activation). Every continuation message uses Agent.followup() and becomes one FIFO turn with no steering of the current turn. Routing depends only on residency: running enqueues, waiting wakes the same Agent, and an absent Activation cold-resumes a new one.

The manager reserves the child identity, resolves the durable descriptor, calls ctx.agents.create() (or ctx.agents.resume() for cold resume) through a private activation-owner scope, installs the returned AgentHandle in the Activation, establishes any continuable-parent ownership, and then submits the prompt. Cold resume never dispatches through a provider because the persisted Session already holds the initial prefix and the folded descriptor is the whole reconstruction input.

A continuation-managed parent Activation records each child Session id in an ownedChildren set before the child can run and disposes only after every owned child Activation completes AgentHandle disposal (child-first). Teardown propagates Agent cancellation top-down before awaiting slow descendants, while handle release remains child-first. Top-level and other non-continuation Agents have no Activation and stay outside this waiting graph. Final settlement awaits a best-effort ctx.sessions.flush(child.session) before handle disposal. A listener rejection is logged without failing the Activation because listener participation does not identify a persistence backend; the persisted state may therefore be missing or stale on resume.

Lifecycle events

The service emits a subagent/start/subagent/end pair for each one-shot run and each resident continuable Activation epoch, so continuable children are observable with the same vocabulary as one-shot runs without exposing whether the manager materialized, woke, or cold-resumed them. For a one-shot start it attaches the result observer before the synchronous subagent/start, so even an already-settled child still produces subagent/start before subagent/end; a continuable epoch that fails before residency emits neither edge. The pair shares a service-minted runId; the local flag is snapshotted from the provider's exact localAgent (always true for a continuable child), so observers never infer run identity or locality from reusable provider/session names. The provider field is lifecycle provenance rather than a live-registry claim: an accepted one-shot run may settle after provider removal, and a cold-resumed epoch retains its descriptor's initial provider name without requiring that provider to be registered.

Run events are scoped to the delegating parent. Every listener is independently contained: a synchronous throw or rejected returned promise is logged without starving peer listeners or changing the run.

Provider additions and removals also emit subagent/provider-added and subagent/provider-removed. Consumers such as the model-facing tool use those events because Cordis may load sibling plugins concurrently; configuration order does not prove registration order.

Continuable children do not create SubagentRun or Tasks. The continuation manager directly owns one process-local Activation and retained AgentHandle per resident child Session, uses the Agent inbox as the only FIFO, and cold-resumes from the durable descriptor. Exact live direct-parent identity authorizes parent-to-child delivery. Exact live child identity authorizes reports; the manager derives the recipient from durable parentSession, and MessageSource remains provenance rather than authority.

When ctx.sessionProjections is available, the service registers subagentTiming. The projection resets at each descriptor so a fork seed's ancestor work cannot enter the child's total, then accumulates turn/startturn/end active time and retains same-cut active.since and active.through bounds for an open turn. While that turn remains open, active.through follows the latest folded event, giving an inactive consumer a conservative crash bound without mixing in newer session metadata.

registerContinuableSetup() lets optional packages add child-scoped capabilities without teaching the continuation manager their names. Contributions install synchronously before Activation publication, roll back with failed setup, and are released with the child scope. New grants wait for the next Activation, while contribution removal revokes every resident installation immediately.

Collection model

The model-facing tool collects synchronously by default: it awaits the child result and disposes the run before returning. One-shot background delegation registers a plain Task in the tool, whose generic status, collection, and cancellation tools own later interaction, and persists its model-supplied description as the optional display label. Continuable background delegation calls ctx.subagents.startContinuable() and returns only the durable child id; the child owns its own turns from inbox acceptance, so there is no Task, no result promise, and no public subagent cancellation — a caller sends later work with the send_message follow-up tool, and the durable child Session remains the source of the child's detailed output. The continuation manager exists only while ctx.agents is available, and session persistence is resolved per continuation operation. Independently, listChildren() resolves session query and dynamically imports its optional runtime only when called, then interprets a read-only live-preferred scan of all descriptor-bearing direct children without consulting the continuation manager, Agent registrations, Activations, or providers. Each healthy row derives its read-time hasChildren hint from traced direct-descendant headers carrying durable origin: 'subagent'; it does not read descendant event logs, and the descriptor-backed child catalog remains authoritative when expanded. Service consumers such as a UI can retain both modes and choose a fallback for an unlabeled one-shot child; the model-facing list_agents tool projects only continuable entries and maps service activity to its existing running/complete vocabulary. The scan forwards the caller's signal to cancellable trace and exact-read operations, checks cancellation around the remaining event-list read, and reports every observed abort as SubagentError code CANCELLED. See the background subagent tasks Agent Note, the continuable background subagents Agent Note, the durable catalog Agent Note, the merged-service Agent Note, the capability-seam Agent Note, and src/types.ts for the complete contracts.

Continuable Activations await a best-effort final session flush without treating listener participation as durability confirmation. One-shot runs retain best-effort session checkpointing, so a completed one-shot child is discoverable after disposal only when its session actually reached persistence; the service does not invent a catalog entry from Task history when that checkpoint is absent.

Model Experience

Indirectly, through dsh-tool-subagent, dsh-tool-subagent-control, and dsh-tool-subagent-report. The first owns delegation schemas, the second owns parent continuation and discovery, and the third contributes report only to continuable child scopes.

KV Cache effect

No direct invalidation; the named consumers own any request-prefix changes.

Known Limitations and Deferred Work

  • ACP children remain one-shot and are not trace-enumerable — an ACP run has no local child session in the parent's session corpus. An ACP prepareContinuable requires persisting the remote session id in provider-specific descriptor data and a per-child continuation advertisement, since ACP loadSession support is negotiated per child rather than established by the method's presence. Remote providers also require a separate Activation ownership contract with equivalent authenticated control and child-first quiescence before they support continuable children.
  • No host-user continuationfollowup() requires the exact live direct parent. A future host adapter needs a concrete authenticated interaction before the seam gains a separate user capability.
  • No current-turn steering — continuable messages and waking reports enqueue later turns; neither redirects an open turn.
  • Process-local residency — the Activation inbox and ownership graph do not coordinate two harness processes; concurrent access to one persistence store still requires a durable mailbox and cross-process lease protocol.
  • No replay of accepted-but-unlogged messages — only messages written to the child Session log are reconstructable with their admitted provenance. A crash may lose an accepted initial prompt or follow-up that never reached the log; a later authorized message can cold-resume the child, but the lost message is not replayed automatically.
  • No durable report mailbox — reports require a live direct parent and provide acceptance identity rather than exactly-once delivery or a read receipt.
  • Lifecycle events are observe-only — a run-affecting subagent/end continuation or decision surface waits for a concrete consumer.