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:
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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) 选择、工具、持久化、策略、settings/credentials、遥测与宿主级 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`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。
|
||||
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.md) 选择、工具、持久化、策略、settings/credentials、遥测与宿主级 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`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 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`。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 深度强制约束。
|
||||
- **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 深度强制约束。
|
||||
- **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user