Files
deepseek-harness/packages/subagent/subagent
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
..

@deepseek-ai/dsh-subagent

The subagent seam: an abstract SubagentService (ctx.subagents) for an agent delegating work to another agent. A subagent is a child agent; a SubagentProvider is one transport for running it.

This package is the interface third of the capability seam, split so each concern evolves (and swaps) independently:

Package Role
@deepseek-ai/dsh-subagent (this) the interface: registry service + vocabulary types
@deepseek-ai/dsh-subagent-spawn an implementation: fresh in-process child
@deepseek-ai/dsh-subagent-fork an implementation: in-process child seeded from the parent's log
@deepseek-ai/dsh-subagent-acp an implementation: ACP client driving another process
@deepseek-ai/dsh-tool-subagent the model-facing tool over ctx.subagents

Unlike the bash seam (one executor per context, second load throws), multiple providers coexist here. Each registers under a unique name and a caller picks one by name — the shape mirrors the LLM adapter registry (LlmService.registerAdapter), not the single-service bash executor. This is the requirement that rules out the bash shape: an agent may want an in-process child for a cheap subtask and an out-of-process ACP child for an isolated one, in the same runtime.

Service API (ctx.subagents)

Member Semantics
registerProvider(provider) Register under provider.name. Throws SubagentError('DUPLICATE_PROVIDER') on a name clash. Effect-scoped (HMR-safe); returns the disposer.
getProvider(name) Look up a provider (undefined if absent).
list() Registered provider names (insertion order).
start(name, request) Resolve the provider (NO_PROVIDER if absent), validate every requested START-TIME capability (UNSUPPORTED_CAPABILITY for the first unmet one — before any child is created), then delegate to provider.start and emit subagent/start / subagent/end around the run.

Capabilities: two kinds, discovered two ways

  • Start-time features (outputSchema, depthLimit, toolFilter) are a static provider.capabilities descriptor, checked by the service BEFORE a run exists. A request that needs one the provider lacks is rejected loud (UNSUPPORTED_CAPABILITY), never accepted-then-ignored.
  • Runtime features (steering, resume) are optional methods on SubagentRun (sendMessage?, resume?). The method's presence IS the capability; TS narrowing is the discovery mechanism — a consumer cannot call an absent method without narrowing first, so there is no silent degradation path.

Beside capabilities sits one DESCRIPTIVE fact, not validated by the service: provider.inheritsParentContext — whether a child sees the parent conversation (fork: true — seeded with the completed-turn prefix; spawn/acp: false). The model-facing consumer (dsh-tool-subagent) derives truthful tool wording from it.

Run lifecycle

provider.start(request) returns a SubagentRun: a handle with a result promise, cancel(), dispose(), and the optional runtime methods. result resolves with a SubagentResult (output, optional structured, stopReason) — it does not reject on a child-level failure (a model/transport failure resolves with stopReason: 'error'), so the consumer maps a non-completed reason to an isError tool result. The consumer MUST dispose() on every path (success, error, abort) to reach child quiescence and avoid leaking an idle child / session.

The service also announces provider lifecycle: subagent/provider-added (the live provider) fires after a registration and subagent/provider-removed (the name) after an unregistration, so a consumer deriving state from a named provider (the model-facing tool wording) mirrors registry membership instead of assuming load order — the cordis Loader starts sibling plugins concurrently, so "listed earlier" does not mean "registered earlier". The service emits subagent/start (payload SubagentRunInfo) and subagent/end (payload SubagentRunEndInfo) around the run — both observe-only (plain emits; subagent/end fires from a detached .then and awaits no listener). subagent/end carries lastAssistantMessage (a deep clone of the child's final output) on the settle path, absent when the run rejected at the infrastructure level. The clone keeps the surface observe-only: the end emit fires from a detached .then before the caller's await run.result resumes, so a shared reference would let a mutating listener corrupt the caller's result. A subagent/start listener can still reach the live child via ctx.agents.get(info.id); a subagent/end listener can only observe (the run has settled). Any run-affecting decision (continuation, injection that changes the run) is out of scope for this observe-only surface.

Scope (first cut)

The consumer collects synchronously: it starts a run and awaits result. Steering (sendMessage) is part of the contract but intentionally unused. Background / poll / spill semantics are deferred to a future redesign unifying long-running-tool handling across subagents and bash. See the RFC: docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md.

See src/types.ts for the full contracts.