Files
deepseek-harness/packages/subagent/tool-subagent/README.md
T
Tianyi Cui e85e21c8b0 fix(review): close interpolation strictness holes; make tool-subagent mirror provider lifecycle
Codex round-1 findings, both confirmed:

- renderPrompt: variable lookup now uses Object.hasOwn (an unregistered
  {{constructor}} previously resolved through Object.prototype and spliced
  function source into the prompt), and a {{ that opens no complete group
  while a }} still follows ({{{model}}}, {{a{b}}) now throws instead of
  passing or partially interpolating. A lone {{ with no }} after it stays
  verbatim; substituted values are never re-scanned.
- tool-subagent: the apply-time provider lookup assumed a load order the
  cordis Loader does not guarantee (siblings start concurrently). The seam
  now announces subagent/provider-added/-removed and the tool mirrors the
  provider's lifecycle: registers when the provider is (or becomes)
  available, unregisters when it goes away, re-derives wording on reload.
  No load-order requirement remains.
- loop.spec containment test now proves live continuation: after the
  contained render failure, a waterfall listener rescues {{cwd}} and the
  same agent completes a real model turn.

RFC/READMEs updated to the shipped contract; cordis catalog regenerated.
2026-07-05 02:42:48 +08:00

3.1 KiB

@deepseek-ai/dsh-tool-subagent

The model-facing subagent tool: delegate a self-contained task to a child agent and return its final output. Pure schema + lifecycle shaping over the ctx.subagents provider registry — an in-process, ACP, or future A2A backend swaps in without changing what the model sees.

Provider selection is config, not model-facing

This plugin binds to exactly one provider (Config.provider). The model sees only { description, prompt } — there is no provider/type parameter in the schema. To expose more than one transport, load the plugin more than once, each bound to a different provider and a distinct toolName (the tool registry rejects a duplicate name, so a second load that kept the default subagent name would throw). Keeping selection in config (not the schema) is the deliberate split: the service holds a multi-provider registry; the tool picks one.

The description states the provider's context contract

The tool description and the prompt parameter description are DERIVED from the bound provider's inheritsParentContext (providerWording): a fresh-context provider (spawn, ACP) gets the standalone-prompt wording ("it does not see this conversation"), an inheriting provider (fork) tells the model the child already sees the conversation's completed turns and its prompt should state only what is new. Because the description is fixed at tool registration, the tool mirrors the provider's lifecycle (subagent/provider-added/-removed): it registers when the bound provider is (or becomes) available and unregisters when the provider goes away — no load-order requirement (the cordis Loader starts sibling entries concurrently, so "listed first" never guaranteed "registered first"), and an HMR reload of the backend re-derives the wording from the fresh provider. While the provider is absent the tool simply does not exist (a ctx.logger note records the wait; a typo'd provider name shows up as a tool that never materializes).

Config key Meaning
provider (required) The ctx.subagents provider name to start runs on (spawn, fork, acp, …).
toolName The model-facing tool name to register (default subagent). Set a distinct value per load when exposing multiple providers, e.g. subagent + subagent_acp.
agentOptions Default per-child { model?, systemPrompt? } applied to every spawned child.

Lifecycle (synchronous collect)

execute starts a run on the configured provider and awaits run.result inside a try/finally that always dispose()s the run — the owned child agent/session is torn down on every path (success, error, abort), never leaked. The tool's abort signal (exec.signal) is bridged to run.cancel(). A non-completed stop reason (aborted/error/max-tokens/refusal) maps to an isError tool result rather than returning partial output as success.

Background / poll collection is deferred (see the RFC); this cut blocks the parent turn until the child finishes.