Merge pull request #2374 from deepseek-harness/codex/product-subagent-one-shot-background

feat(agent-presets): enable background Codex and Claude Code subagent tasks
This commit is contained in:
pku-xht
2026-08-14 15:49:58 +08:00
committed by GitHub
39 changed files with 403 additions and 139 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/bundle/base/README.md
README.md: bd38f39f58ee1f765ff34d40cf57cc6daed2b32b
README.zh.md: bcf4463587f42a245126f87388f3c0a4e57f0976
README.md: 00a177d4dd65de8706aa0e27a79d06a9a50eb55a
README.zh.md: ad70fc16035d3dd70b04e78c31aea90acf61119b

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and host-level subagent providers — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Codex and Claude Code providers load dormant; Agent Presets independently decide whether their agent contributes either model-facing delegation tool. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and host-level subagent providers — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. This bundle neither depends on nor mounts the optional Codex and Claude Code providers; an opting-in Profile installs and mounts the selected provider once on the host plane, while Agent Presets decide whether their agents receive the corresponding model-facing delegation tools. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
The patch gates both shell stacks by platform on its own rows: `bash-sandbox`/`tool-bash` carry `disabled: !!js process.platform === 'win32'` (bash has no Windows runner), and their twins `pwsh-sandbox`/`tool-pwsh` mount on win32 only with the inverted expression — one shared patch file, exactly one shell stack per host. The permission surface stays exactly as on POSIX: `sandbox`/`sandbox-policy` enforce the file-effect policy through the Windows ACL restricted-token runner (the win32 chain of `dsh-sandbox-local``@deepseek-ai/dsh-sandbox-windows-acl`), the permission switcher and the approval service run unchanged, and `fs-sandbox` keeps fencing `ctx.fs` writes — mounting `dsh-fs-local` alongside it would double-register `ctx.fs` and fail the load. A Windows host that prefers the unconfined local pwsh executor or full access overrides these rows through its profile or home `cordis.patch.yml` (the bash-restore recipe must be complete: disable `pwsh-sandbox`/`tool-pwsh` AND re-enable `bash-sandbox`/`tool-bash` — both executor families register the same `bash` service, so an incomplete recipe fails loud at load). POSIX hosts see the pwsh rows disabled.
@@ -19,5 +19,4 @@ None directly; each inserted row's package owns its effect.
## Known Limitations and Deferred Work
- **A patch replaces whole row configs** — profile overrides must restate every field a row keeps; there is no deep-merge layer.
- **Claude's SDK platform CLI remains in the Profile install closure** — the base bundle depends on the Claude provider, whose production path resolves the host `claude`; removing the SDK's unused optional payload is deferred to the product installation-closure follow-up.
- **The Windows temp grant is a private per-session subdirectory** — `workspace-write` confines writes to the workspace plus the session's own temp subdirectory (`<temp>\dsh-<hash>`, TMP/TEMP rewritten for confined children); `read-only` grants nothing. See `@deepseek-ai/dsh-sandbox-windows-acl`.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.md) 选择、工具、持久化、策略、settingscredentials、遥测与宿主级 subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。Codex 与 Claude Code provider 以休眠状态加载;Agent Preset 分别决定自己的 agent 是否贡献任一面向模型委派工具。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 APIprofile 组合器通过 manifest元数据清单`dsh.bundle.patch` 字段解析 patch绝不通过代码。
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.md) 选择、工具、持久化、策略、settingscredentials、遥测与宿主级 subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。这个 bundle 既不依赖也不挂载可选的 Codex 与 Claude Code provider;选择产品集成的 Profile 会安装目标 provider 并在 host plane宿主平面挂载一次Agent Preset 决定自己的 agent 是否获得对应的面向模型委派工具。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 APIprofile 组合器通过 manifest元数据清单`dsh.bundle.patch` 字段解析 patch绝不通过代码。
patch 在自身上按平台门控两个 shell 栈:`bash-sandbox`/`tool-bash` 携带 `disabled: !!js process.platform === 'win32'`bash 没有 Windows runner它们的孪生行 `pwsh-sandbox`/`tool-pwsh` 以取反的表达式仅在 win32 挂载——同一份 patch 文件,每个宿主恰好挂载一个 shell 栈。权限面与 POSIX 完全一致:`sandbox`/`sandbox-policy` 通过 Windows ACL 受限令牌 runner`dsh-sandbox-local` 的 win32 链 → `@deepseek-ai/dsh-sandbox-windows-acl`)执行文件效果策略,权限切换器与 approval 服务原样运行,`fs-sandbox` 继续围栏 `ctx.fs` 写入——在其旁再挂载 `dsh-fs-local` 会重复注册 `ctx.fs` 并在加载时失败。偏好不受沙盒约束的本地 pwsh 执行器或完整访问的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行bash 恢复配方必须完整:禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器家族注册同一个 `bash` 服务配方不完整会在加载时直接报错。POSIX 主机看到的是被禁用的 pwsh 行。
@@ -19,5 +19,4 @@ patch 在自身上按平台门控两个 shell 栈:`bash-sandbox`/`tool-bash`
## 已知限制与暂缓事项
- **patch 会替换整行 `config`**profile 覆盖必须重述该行需要保留的每个字段;不存在深度合并层。
- **Claude SDK 的平台 CLI命令行界面仍在 Profile 安装闭包中**base 组合包依赖 Claude 提供方,其生产路径解析宿主提供的 `claude`;移除 SDK 中未使用的可选载荷,推迟到产品安装闭包后续项处理。
- **Windows 的临时目录授权是按会话的私有子目录**——`workspace-write` 把写入限制在工作区与会话自己的 temp 子目录(`<temp>\dsh-<hash>`,受限子进程的 TMP/TEMP 被改写);`read-only` 不授予任何临时目录写入权限。见 `@deepseek-ai/dsh-sandbox-windows-acl`

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md
README.md: 17b14e847baea3eadda7129b5e49f5e65b668cc8
README.zh.md: 5c970f623c794e91ec2c4a6b2c71c6b28c887485
README.md: 1a0d6e32b8610769dcc5d8342a4fe88d0c884085
README.zh.md: 78dab14e5eaddc06ccd07b69dc952a09380e0428

View File

@@ -31,7 +31,9 @@ The provider advertises no optional start-time capabilities and reports `inherit
Production resolves `claude` from the subprocess execution world's credential-scrubbed `PATH`, with explicit `env` entries applied, and passes the resulting path to the SDK as `pathToClaudeCodeExecutable`. On Windows, a resolved `.cmd` or `.bat` path is carried as a quoted, per-spawn environment value that `cmd.exe /v:off` expands once, so valid path metacharacters remain data. The pinned SDK's fixed flags then occupy cmd's command tail and contain no cmd metacharacters; they are not ordinary Windows argv. Native settings and authentication remain authoritative. The plugin does not install another CLI, select a model, create a product home, log in, or probe an account. Credential-shaped ambient variables are removed before the explicit `env` overlay is applied, so an API key or token intended for the child must be supplied there. Non-credential endpoint variables such as `ANTHROPIC_BASE_URL`, along with ordinary ambient values such as `PATH` and `HOME`, remain inherited unless overridden.
Shipped profiles load this provider once on the host and start no Claude process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. A custom host composition can still use both rows directly.
Production `dsh` does not install or mount this optional provider. A Profile that opts in must install `@deepseek-ai/dsh-subagent-claude-code` and mount it once on the host plane; loading the provider starts no Claude process until a tool call. Full Agent Presets carry a matching product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
The standalone composition below shows the complete explicit capability. A Profile based on `@deepseek-ai/dsh-base` keeps its existing Job rows, adds the product provider row, and enables the preset tool row instead of mounting duplicate Job services.
```yaml
- id: subagent-claude-code
@@ -40,13 +42,18 @@ Shipped profiles load this provider once on the host and start no Claude process
env:
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
- id: jobs
name: '@deepseek-ai/dsh-jobs-local'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
- id: tool-subagent-claude-code
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
config:
provider: claude-code
toolName: subagent_claude_code
enableRunInBackground: false
backgroundMode: one-shot
maxDepth: provider-managed
```
@@ -72,19 +79,19 @@ The child pays for an independent Claude Code context and query. Child tokens do
Independent of the parent request cache. Reuse depends only on Claude Code's own model, instructions, tools, native settings, and fresh query.
### Parent tool result, indirectly
### Parent scheduling and results, indirectly
#### What the model sees
Through `dsh-tool-subagent`, the parent sees only the strict final Claude Code answer or the consumer's exact error for a non-completed result. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Job id; the generic job controls later deliver a completion notice, expose the final answer and status through `job_output`, and let `job_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
#### Token effect
Parent input grows only by the final answer or error retained in the tool result. This provider adds no parent tool schema by itself.
Foreground input grows by the retained final answer or error. Background input also includes the start acknowledgement, completion notice, and any `job_output`, `job_kill`, or later status results; child tokens still do not enter the parent context. This provider adds no parent tool schema by itself.
#### KV Cache effect
Append-only: the new tool result follows the reusable parent request prefix.
Append-only: foreground adds one result after the reusable parent prefix, while background appends the Job acknowledgement, notice, and later control or collection results. Background scheduling can add a notice-driven turn, but none of these messages rewrites the earlier prefix.
## Known Limitations and Deferred Work
@@ -93,6 +100,6 @@ Append-only: the new tool result follows the reusable parent request prefix.
- **Product installation and account state remain native** — a missing or incompatible `claude`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer or login flow.
- **The SDK platform CLI remains in the install closure** — production ignores it in favor of the host `claude`, but the current SDK optional dependency is still installed and supplies the keyless compatibility fixture. Removing that payload belongs to the separate product installation-closure follow-up.
- **No human interaction path** — `AskUserQuestion` is disabled and other interactive callbacks are absent, so tasks requiring new approval or input fail instead of suspending.
- **Final text only** — reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local.
- **Product payload is final text only** — reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local; generic Job ids, notices, and status come from the shared job runtime.
- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.
- **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored.

View File

@@ -31,7 +31,9 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
生产环境从子进程执行世界清除凭证后的 `PATH` 解析 `claude`,再应用显式 `env` 条目,并把所得路径作为 `pathToClaudeCodeExecutable` 交给 SDK。在 Windows 上,解析到的 `.cmd``.bat` 路径会作为带引号、仅供本次 spawn 使用的环境值交给 `cmd.exe /v:off` 展开一次,因此合法路径中的元字符仍只是数据。锁定版本的 SDK 随后把固定命令行选项放在 cmd 的命令尾部;这些选项不含 cmd 元字符,也并不是普通的 Windows argv。原生设置与身份验证继续是权威来源。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH``HOME` 等普通环境变量仍会被继承。
随附 profile 会在宿主上加载一次该提供方,而且在工具调用前不会启动 Claude 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`自定义宿主组装仍可直接使用两条配置行
生产 `dsh` 不会安装或挂载这个可选提供方。选择启用它的 Profile 必须安装 `@deepseek-ai/dsh-subagent-claude-code`,并在 host plane宿主平面挂载一次加载提供方本身不会在工具调用前启动 Claude 进程。完整 Agent Preset 携带对应的产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code``one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID`job_output``job_kill` 使用。base host基础宿主与完整 preset 已提供通用作业注册表和控制工具
下列独立组装展示完整的显式能力。基于 `@deepseek-ai/dsh-base` 的 Profile 保留已有 Job 行,只新增产品提供方行并启用 preset 工具行,禁止重复挂载 Job 服务。
```yaml
- id: subagent-claude-code
@@ -40,13 +42,18 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
env:
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
- id: jobs
name: '@deepseek-ai/dsh-jobs-local'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
- id: tool-subagent-claude-code
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
config:
provider: claude-code
toolName: subagent_claude_code
enableRunInBackground: false
backgroundMode: one-shot
maxDepth: provider-managed
```
@@ -72,19 +79,19 @@ Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。
这与父请求缓存相互独立。能否复用只取决于 Claude Code 自身的模型、指令、工具、原生设置和全新 query。
### 父级工具结果(间接)
### 父级调度与结果(间接)
#### 模型看到的内容
通过 `dsh-tool-subagent`,父级模型只会看到符合严格成功条件的 Claude Code 最终答案或者在结果未完成时看到消费方给出的原样错误。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
通过 `dsh-tool-subagent`前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Job id随后通用作业控制面会送达完成通知通过 `job_output` 公开最终答案与状态,并允许 `job_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
#### 对 token 的影响
父级输入会增加工具结果中保留的最终答案或错误内容。本提供方自身不添加父级工具 schema。
前台输入会增加工具结果中保留的最终答案或错误内容。后台输入还会包含启动确认、完成通知,以及 `job_output``job_kill` 或后续状态结果;子任务 token 仍不会进入父级上下文。本提供方自身不添加父级工具 schema。
#### 对 KV Cache 的影响
仅追加:新的工具结果接在可复用的父请求前缀后。
仅追加:前台会在可复用的父请求前缀后增加一个结果,后台则会继续追加 Job 启动确认、通知以及后续控制或收集结果。后台调度可能增加一个由通知唤醒的轮次,但这些消息都不会改写更早的前缀
## 已知限制与后续工作
@@ -93,6 +100,6 @@ Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。
- **产品安装与账户状态仍由原生机制管理**`claude` 缺失或不兼容、配置错误或身份验证失败都会呈现为启动错误或运行错误;本插件不提供安装程序或登录流程。
- **SDK 平台 CLI 仍在安装闭包内**:生产环境会忽略它,改用宿主提供的 `claude`,但当前 SDK 的可选依赖仍会安装,并提供无密钥兼容性 fixture。移除该载荷属于独立的产品安装闭包后续项。
- **没有人工交互路径**`AskUserQuestion` 被禁用,其他交互回调也不存在,因此需要新审批或输入的任务会失败而不会挂起。
- **仅返回最终文本**推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部。
- **产品载荷仅包含最终文本**推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部;通用 Job id、通知与状态来自共享作业运行时
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。
- **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。

View File

@@ -15,7 +15,7 @@ const configPath = join(fixtureDir, 'cordis.yml')
const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
describe('product-provider public Loader composition', () => {
it('loads both opt-in packages and foreground tools without starting either product', async () => {
it('loads both opt-in packages, one-shot task tools, and job controls without starting either product', async () => {
const { stdout, stderr } = await runLoaderSmoke({
label: 'product-provider Loader composition',
tempDirPrefix: 'dsh-product-provider-loader-',
@@ -57,15 +57,16 @@ describe('product-provider public Loader composition', () => {
tools: [
{
name: 'subagent_codex',
parameterNames: ['description', 'prompt'],
parameterNames: ['description', 'prompt', 'run_in_background'],
required: ['description', 'prompt'],
},
{
name: 'subagent_claude_code',
parameterNames: ['description', 'prompt'],
parameterNames: ['description', 'prompt', 'run_in_background'],
required: ['description', 'prompt'],
},
],
jobTools: ['job_kill', 'job_list', 'job_output'],
starts: 0,
})
}, LOADER_SMOKE_TEST_TIMEOUT_MS)

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md
README.md: 3d59ca1eaf3db9dd9d9d2cd451692ebd2a956ef4
README.zh.md: abff7b569e2ce8261366c004ab6d03ea53300fdb
README.md: 848d170585710b682fa4ce331010fce7080de673
README.zh.md: 34e9105e6a78bc16f16997c7df89d4f6412eb50c

View File

@@ -27,7 +27,9 @@ The provider advertises no optional start-time capabilities and reports `inherit
Production resolves `codex` from `PATH` and uses the host's native Codex configuration and authentication. The plugin does not install Codex, select a model, create `CODEX_HOME`, log in, or probe a version. Credential-shaped ambient variables are removed by the subprocess seam, so an API key intended for the child must be supplied explicitly in `env`; ordinary ambient values such as `PATH` and `HOME` remain available unless overridden.
Shipped profiles load this provider once on the host and start no Codex process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. A custom host composition can still use both rows directly.
Production `dsh` does not install or mount this optional provider. A Profile that opts in must install `@deepseek-ai/dsh-subagent-codex` and mount it once on the host plane; loading the provider starts no Codex process until a tool call. Full Agent Presets carry a matching product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
The standalone composition below shows the complete explicit capability. A Profile based on `@deepseek-ai/dsh-base` keeps its existing Job rows, adds the product provider row, and enables the preset tool row instead of mounting duplicate Job services.
```yaml
- id: subagent-codex
@@ -36,13 +38,18 @@ Shipped profiles load this provider once on the host and start no Codex process
env:
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
- id: jobs
name: '@deepseek-ai/dsh-jobs-local'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
config:
provider: codex
toolName: subagent_codex
enableRunInBackground: false
backgroundMode: one-shot
maxDepth: provider-managed
```
@@ -66,19 +73,19 @@ The child pays for an independent Codex context and turn. Child tokens do not en
Independent of the parent request cache. Reuse depends only on Codex's own provider, model, instructions, tools, and ephemeral-thread request.
### Parent tool result, indirectly
### Parent scheduling and results, indirectly
#### What the model sees
Through `dsh-tool-subagent`, the parent sees only the selected final Codex answer or the consumer's exact error for a non-completed result. Codex commentary, reasoning, tool activity, stderr, workspace diffs, and product ids are not copied into the parent Session.
Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or the consumer's exact error for a non-completed result. A background call first returns a Job id; the generic job controls later deliver a completion notice, expose the final answer and status through `job_output`, and let `job_kill` request cancellation. Codex commentary, reasoning, tool activity, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
#### Token effect
Parent input grows only by the final answer or error retained in the tool result. This provider adds no parent tool schema by itself.
Foreground input grows by the retained final answer or error. Background input also includes the start acknowledgement, completion notice, and any `job_output`, `job_kill`, or later status results; child tokens still do not enter the parent context. This provider adds no parent tool schema by itself.
#### KV Cache effect
Append-only: the new tool result follows the reusable parent request prefix.
Append-only: foreground adds one result after the reusable parent prefix, while background appends the Job acknowledgement, notice, and later control or collection results. Background scheduling can add a notice-driven turn, but none of these messages rewrites the earlier prefix.
## Known Limitations and Deferred Work
@@ -86,6 +93,6 @@ Append-only: the new tool result follows the reusable parent request prefix.
- **Host-managed product installation and account state** — a missing or incompatible `codex`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer, login flow, or runtime version gate.
- **Compatibility is pinned by development evidence** — upgrading from the verified 0.147.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests.
- **No human approval path** — known unattended approval requests are denied and unknown server requests fail closed; deployments cannot configure an allow policy through this package.
- **Final text only** — reasoning, commentary, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local.
- **Product payload is final text only** — reasoning, commentary, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local; generic Job ids, notices, and status come from the shared job runtime.
- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.
- **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored.

View File

@@ -27,7 +27,9 @@
生产环境会从 `PATH` 中解析 `codex`,并使用宿主机原生的 Codex 配置与身份验证。本插件不安装 Codex、不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测版本。子进程 seam 会移除具有凭证特征的环境变量,因此供子进程使用的 API 密钥必须在 `env` 中显式提供;除非被覆盖,`PATH``HOME` 等普通环境变量值仍然可用。
随附 profile 会在宿主上加载一次该提供方,而且在工具调用前不会启动 Codex 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`自定义宿主组装仍可直接使用两条配置行
生产 `dsh` 不会安装或挂载这个可选提供方。选择启用它的 Profile 必须安装 `@deepseek-ai/dsh-subagent-codex`,并在 host plane宿主平面挂载一次加载提供方本身不会在工具调用前启动 Codex 进程。完整 Agent Preset 携带对应的产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex``one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID`job_output``job_kill` 使用。base host基础宿主与完整 preset 已提供通用作业注册表和控制工具
下列独立组装展示完整的显式能力。基于 `@deepseek-ai/dsh-base` 的 Profile 保留已有 Job 行,只新增产品提供方行并启用 preset 工具行,禁止重复挂载 Job 服务。
```yaml
- id: subagent-codex
@@ -36,13 +38,18 @@
env:
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
- id: jobs
name: '@deepseek-ai/dsh-jobs-local'
- id: tool-jobs
name: '@deepseek-ai/dsh-tool-jobs'
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
config:
provider: codex
toolName: subagent_codex
enableRunInBackground: false
backgroundMode: one-shot
maxDepth: provider-managed
```
@@ -66,19 +73,19 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些
这与父请求缓存相互独立。能否复用只取决于 Codex 自身的提供方、模型、指令、工具和临时线程请求。
### 父级工具结果(间接)
### 父级调度与结果(间接)
#### 模型看到的内容
通过 `dsh-tool-subagent`,父级模型只会看到选定的 Codex 最终答案,或者看到消费方针对未成功完成的结果给出的原样错误。Codex 的过程说明、推理reasoning、工具活动、stderr、工作区差异和产品标识符均不会复制到父会话。
通过 `dsh-tool-subagent`前台调用会让父级模型看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Job id随后通用作业控制面会送达完成通知通过 `job_output` 公开最终答案与状态,并允许 `job_kill` 请求取消。Codex 的过程说明、推理reasoning、工具活动、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
#### 对 token 的影响
父级输入会增加工具结果中保留的最终答案或错误内容。本提供方自身不添加父级工具 schema。
前台输入会增加工具结果中保留的最终答案或错误内容。后台输入还会包含启动确认、完成通知,以及 `job_output``job_kill` 或后续状态结果;子任务 token 仍不会进入父级上下文。本提供方自身不添加父级工具 schema。
#### 对 KV Cache 的影响
仅追加:新的工具结果接在可复用的父请求前缀后。
仅追加:前台会在可复用的父请求前缀后增加一个结果,后台则会继续追加 Job 启动确认、通知以及后续控制或收集结果。后台调度可能增加一个由通知唤醒的轮次,但这些消息都不会改写更早的前缀
## 已知限制与后续工作
@@ -86,6 +93,6 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些
- **产品安装和账户状态由宿主管理**`codex` 缺失或不兼容、配置错误或身份验证失败,都会呈现为启动错误或运行错误;本插件不提供安装程序、登录流程或运行时版本门禁。
- **兼容性由开发证据锁定**:若要从已验证的 0.147.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。
- **没有人工审批路径**:已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;部署方无法通过本包配置允许策略。
- **仅返回最终文本**推理、过程说明、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部。
- **产品载荷仅包含最终文本**推理、过程说明、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部;通用 Job id、通知与状态来自共享作业运行时
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。
- **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。

View File

@@ -15,7 +15,7 @@ const configPath = join(fixtureDir, 'cordis.yml')
const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
describe('Codex provider public Loader composition', () => {
it('loads the opt-in package and foreground tool without starting Codex', async () => {
it('loads the opt-in package, one-shot task tool, and job controls without starting Codex', async () => {
const { stdout, stderr } = await runLoaderSmoke({
label: 'subagent-codex Loader composition',
tempDirPrefix: 'dsh-subagent-codex-loader-',
@@ -44,9 +44,10 @@ describe('Codex provider public Loader composition', () => {
},
tool: {
name: 'subagent_codex',
parameterNames: ['description', 'prompt'],
parameterNames: ['description', 'prompt', 'run_in_background'],
required: ['description', 'prompt'],
},
jobTools: ['job_kill', 'job_list', 'job_output'],
starts: 0,
})
}, LOADER_SMOKE_TEST_TIMEOUT_MS)

View File

@@ -113,7 +113,9 @@ async function settleStart(start: Promise<SubagentRun>, signal: AbortSignal): Pr
try {
return await settleRun(await start)
} catch (error: unknown) {
return signal.aborted
// Product providers aggregate startup and rollback failures. Cancellation
// must not turn a failed cleanup into a cleanly killed Job.
return signal.aborted && !(error instanceof AggregateError)
? { status: 'killed' }
: { status: 'failed', detail: String(error) }
}
@@ -357,7 +359,7 @@ export function apply(ctx: Context, config: Config): void {
render: (_args, value) => [{
type: 'text',
text: value.kind === 'background'
? `started background subagent task ${value.jobId}`
? `started background subagent job ${value.jobId}`
: value.kind === 'continuable'
? `started subagent ${value.subagentId}`
: outputValueText(value.output),

View File

@@ -85,7 +85,11 @@ describe('dsh-tool-subagent', () => {
it('registers a `subagent` tool that delegates to the configured provider and returns its output', async () => {
const ctx = await setup({ provider: 'mock' }, { reply: 'child says hi' })
const result = await callSubagent(ctx, { description: 'do a thing', prompt: 'go research X' })
const result = await callSubagent(ctx, {
description: 'do a thing',
prompt: 'go research X',
run_in_background: false,
})
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected subagent success')
expect(result.value).toEqual({
@@ -814,7 +818,7 @@ describe('dsh-tool-subagent background mode', () => {
agent: parent,
})
expect(text(started)).toBe('started background subagent task subagent-1')
expect(text(started)).toBe('started background subagent job subagent-1')
expect(prepareCalls).toBe(0)
})
@@ -826,7 +830,7 @@ describe('dsh-tool-subagent background mode', () => {
expect(start.isError).toBe(false)
if (start.isError) throw new Error('expected background subagent success')
expect(start.value).toEqual({ kind: 'background', jobId: 'subagent-1' })
expect(text(start)).toBe('started background subagent task subagent-1')
expect(text(start)).toBe('started background subagent job subagent-1')
const collected = await ctx.tools.execute({
signal: testToolSignal,
@@ -887,7 +891,7 @@ describe('dsh-tool-subagent background mode', () => {
arguments: { description: 'broken', prompt: 'p', run_in_background: true },
agent: parent,
})
expect(text(started)).toBe('started background subagent task subagent-1')
expect(text(started)).toBe('started background subagent job subagent-1')
const output = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-output'),
@@ -935,6 +939,48 @@ describe('dsh-tool-subagent background mode', () => {
expect(text(output)).toBe('(no new output)\n[status: killed]')
})
it('reports startup rollback failure after cancellation as a failed job', async () => {
const ctx = await backgroundSetup({ provider: 'mock' })
const parent = ownerAgent(ctx, 'sess-parent')
ctx.subagents.registerProvider({
name: 'broken-start-rollback',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
inheritsParentContext: false,
start: request => new Promise((_resolve, reject) => {
request.signal.addEventListener('abort', () => {
reject(new AggregateError(
[new Error('startup aborted'), new Error('cleanup failed')],
'startup failed and cleanup also failed',
))
}, { once: true })
}),
})
tool.apply(ctx, { provider: 'broken-start-rollback', toolName: 'subagent_broken_rollback' })
await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-rollback-start'),
name: 'subagent_broken_rollback',
arguments: { description: 'broken rollback', prompt: 'p', run_in_background: true },
agent: parent,
})
await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-rollback-kill'),
name: 'job_kill',
arguments: { job_id: 'subagent-1' },
agent: parent,
})
const output = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-rollback-output'),
name: 'job_output',
arguments: { job_id: 'subagent-1', wait: true },
agent: parent,
})
expect(text(output)).toContain('[status: failed, AggregateError: startup failed and cleanup also failed]')
})
it('forwards job_kill reasons through the run signal (and defaults one when absent)', async () => {
// Use a provider that remains live until its signal is aborted.
const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } })
@@ -966,8 +1012,8 @@ describe('dsh-tool-subagent background mode', () => {
const startOne = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('h1'), name: 'subagent_hang', arguments: { description: 'one', prompt: 'p', run_in_background: true }, agent: parent })
const startTwo = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('h2'), name: 'subagent_hang', arguments: { description: 'two', prompt: 'p', run_in_background: true }, agent: parent })
expect(text(startOne)).toBe('started background subagent task subagent-1')
expect(text(startTwo)).toBe('started background subagent task subagent-2')
expect(text(startOne)).toBe('started background subagent job subagent-1')
expect(text(startTwo)).toBe('started background subagent job subagent-2')
const withReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k1'), name: 'job_kill', arguments: { job_id: 'subagent-1', reason: 'superseded' }, agent: parent })
const withoutReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k2'), name: 'job_kill', arguments: { job_id: 'subagent-2' }, agent: parent })