Merge remote-tracking branch 'origin/master' into codex/rfc-subagent-background-tasks
# Conflicts: # docs/architecture.md # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/event-producer-consumer.md # docs/module-graph.md # docs/tool-catalog.md # packages/bash/tool-bash/tests/integration.spec.ts # packages/bash/tool-bash/tests/tools.spec.ts # packages/core/agent-core/tests/agent-core.spec.ts # packages/core/agent-loop/README.md # packages/core/agent-loop/src/index.ts # packages/core/agent/README.md # packages/core/agent/src/index.ts # packages/core/agent/tests/agent.spec.ts # packages/subagent/subagent/README.md # packages/subagent/subagent/src/index.ts # packages/subagent/tool-subagent/README.md # packages/subagent/tool-subagent/src/index.ts # pnpm-lock.yaml # scripts/doc-budgets.manifest.json
This commit is contained in:
@@ -1,26 +1,37 @@
|
||||
# @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`](../subagent/README.md) provider registry — an in-process, ACP, or future A2A backend swaps in without changing what the model sees.
|
||||
The `subagent` tool lets the model delegate one self-contained task and collect the child's final output. It is a thin consumer of `ctx.subagents`; changing the configured provider changes the transport without changing the model-facing execution contract.
|
||||
|
||||
## Provider selection is config, not model-facing
|
||||
## Provider selection
|
||||
|
||||
This plugin binds to **exactly one** provider (`Config.provider`). The model sees only `{ description, prompt, run_in_background? }` — 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 description is derived from `provider.inheritsParentContext`: spawn and ACP tell the model to provide a standalone prompt, while fork says the child already sees completed conversation turns. The plugin follows `subagent/provider-added` and `subagent/provider-removed`, so concurrent Cordis plugin loading does not create a registration-order dependency.
|
||||
|
||||
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).
|
||||
## Lifecycle
|
||||
|
||||
| Config key | Meaning |
|
||||
Foreground `execute` passes the tool execution's abort signal when present, otherwise supplies an inert signal to satisfy the required `SubagentStartRequest.signal`. It awaits `ctx.subagents.start(...)`, then awaits `run.result` inside a `try/finally` that always calls `run.dispose()`. The selected signal therefore covers startup and live execution, while disposal guarantees quiescence on success, failure, and abort.
|
||||
|
||||
A non-`completed` stop reason becomes an `isError` tool result; partial child output is never reported as success. With `run_in_background`, an independent task-owned signal covers both asynchronous startup and the ready child, while collection moves to the generic task tools.
|
||||
|
||||
## 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`. |
|
||||
| `enableRunInBackground` | Expose `run_in_background` in this instance's schema (default `true`). Disabled, the parameter is absent entirely AND a caller that forces the key anyway is refused at execution time (the arg validator allows undeclared keys) — delegation through this instance stays strictly synchronous. |
|
||||
| `agentOptions` | Default per-child `{ model? }` applied to every spawned child. (No per-child persona: the deployment persona is a context-wide section every agent shares.) |
|
||||
| `agentOptions` | Default child agent options, currently including `model`. |
|
||||
| `persona` | Per-child persona; requires provider `persona` capability. |
|
||||
| `toolFilter` | Per-child global-tool restriction; requires provider `toolFilter` capability. |
|
||||
| `maxDepth` | Absolute delegation-depth cap; requires provider `depthLimit` capability. |
|
||||
|
||||
## Foreground 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.
|
||||
`execute` awaits a ready run from the configured provider and then **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 required request signal is the canonical cancellation path across startup and live execution. A non-`completed` stop reason (aborted/error/max-tokens/refusal) maps to an `isError` tool result rather than returning partial output as success.
|
||||
|
||||
## Background delegation (a generic task)
|
||||
|
||||
`run_in_background: true` refuses an already-aborted `exec.signal`, starts the run, registers `{ kind: 'subagent', label: description, owner: parent, cancel, done }` with `ctx.tasks` (`@deepseek-ai/dsh-tasks`), and returns `started background subagent task <id>` — the parent keeps working and collects/stops the child through the generic `task_output`/`task_list`/`task_kill` tools (`@deepseek-ai/dsh-tool-tasks`). The tool-call signal is deliberately NOT wired to the run after the id is returned; cancellation belongs to `task_kill` (its logged `reason` is forwarded to `run.cancel`) and the runtime's owner-disposal cleanup. The task is final-output-only (no incremental transcript — the child session remains the detailed trace), and its `done` settles only after `run.dispose()` (child quiescence), so owner disposal cannot resolve before the child is actually gone. Mapping (exported for tests): `runOutcome` — `completed` carries the final text as the task output; `aborted` → `killed`; `error`/`max-tokens`/`refusal`/unknown → `failed` with the reason as status-line detail — and `settleRun`, which disposes on both result paths and contains an infrastructure rejection as `failed`. A missing `ctx.tasks` fails the call loud (`background tasks unavailable: load @deepseek-ai/dsh-tasks and @deepseek-ai/dsh-tool-tasks`). See the [background subagent tasks RFC](../../../docs/rfc/implemented/feature/2026-07-08-background-subagent-tasks.md).
|
||||
`run_in_background: true` refuses an already-aborted `exec.signal`, synchronously registers `{ kind: 'subagent', label: description, owner: parent, cancel, done }` with `ctx.tasks`, and returns `started background subagent task <id>`. The starter immediately calls async `ctx.subagents.start()` with an independent `AbortController`; `task_kill` and owner-scope teardown abort that signal whether startup is still pending or the child is ready. The task is final-output-only, and `done` settles only after startup rollback or `run.dispose()` reaches quiescence. Mapping: `runOutcome` turns `completed` into final output, `aborted` into `killed`, and other terminal reasons into `failed`; `settleRun` contains infrastructure and disposal failures. A missing task runtime fails loud. See the [background subagent tasks RFC](../../../docs/rfc/implemented/feature/2026-07-08-background-subagent-tasks.md).
|
||||
|
||||
`toolFilter` changes the child's visible global tool layer; it is not a parent-derived authority ceiling. See the [agent-scope security non-goal](../../../docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals).
|
||||
|
||||
Reference in New Issue
Block a user