fix(subagent): finalize Codex provider composition

This commit is contained in:
pku-xht
2026-08-04 18:38:05 +08:00
parent 8a24a68333
commit c09b20f96b
31 changed files with 270 additions and 405 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 .agents/notes/proposed/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
2026-08-04-claude-code-and-codex-subagent-backends.md: 37f45f844c9411af0467397272649533ed4d44cc
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: dcd7b90231bdaed47435c27deef413d20f0b7f28
2026-08-04-claude-code-and-codex-subagent-backends.md: 3b9fd51632439da5b3c3fd9187de552d6c9ca5e2
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 36be903640ad1c839d45ed1bf5e605f4e4d6e000

View File

@@ -8,15 +8,15 @@ English | [中文](2026-08-04-claude-code-and-codex-subagent-backends.zh.md)
The named [`ctx.subagents`](../../implemented/feature/2026-06-21-subagent-capability-seam.md) registry lets a parent agent delegate work without knowing how the child runs, but the harness needs first-party routes to the real Codex and Claude Code products. A useful first version must hand either product one self-contained task, let it work in the parent Session's workspace, return a final answer or an explicit failure or cancellation, and leave no managed product process behind.
The product integrations must not become second owners for task text, cwd, cancellation, result settlement, or process trees. They must also prove the real assembled path in required keyless tests. A direct model HTTP request, product double, or hand-mounted plugin cannot show that the Loader, fixed tool, provider registration, official product protocol, native authentication shape, final answer, and teardown work together.
The product integrations must not become second owners for task text, cwd, cancellation, result settlement, or process trees. Required keyless evidence therefore separates two facts: a real-product test proves the official protocol, native authentication shape, final answer, and teardown, while a Loader composition test proves that the public package and documented tool configuration load without starting the product. Direct model HTTP or a product double cannot replace the former; a hand-mounted plugin cannot replace the latter.
## Proposal
The harness provides two sibling one-shot providers behind two fixed model-facing tools. `subagent_codex` selects the `codex` provider, and `subagent_claude_code` selects the `claude-code` provider. Each tool accepts only a standalone text task and binds its provider at deployment time; product selection and background execution are not model arguments.
The harness publishes two sibling one-shot providers as independently installable, opt-in packages. A user loads a provider and the existing common subagent tool in their own `cordis.yml`: `subagent_codex` binds `codex`, while `subagent_claude_code` binds `claude-code`. The shipped CLI dependency closure and base, Web, and headless configurations load neither provider. Each tool accepts only a standalone text task; product selection and background execution are not model arguments.
The Codex provider is implemented against Codex 0.146.0. The Claude Code provider remains unimplemented. This Note remains proposed until both siblings and their combined evidence are present.
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Every call creates a fresh product process and a non-resumable product conversation. The shared subagent service continues to own request resolution, lifecycle events, result settlement, and foreground collection; the shared subprocess service owns credential scrubbing, process-tree termination, and whole-tree exit observation.
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools disable background execution and use `maxDepth: 'provider-managed'`, leaving recursion policy with the out-of-process product instead of sending a limit the provider cannot enforce. Every call creates a fresh product process and a non-resumable product conversation. The shared subagent service continues to own request resolution, lifecycle events, result settlement, and foreground collection; the shared subprocess service owns credential scrubbing, process-tree termination, and whole-tree exit observation.
```text
fixed tool → shared subagent service → product provider → official product process
@@ -30,7 +30,7 @@ fixed tool → shared subagent service → product provider → official product
| --- | --- | --- | --- |
| Resolve | `dsh-tool-subagent` and `ctx.subagents` | Validate the product's text-only input and derive native startup parameters | Unsupported context or malformed input fails before a run is published |
| Start | `dsh-subprocess` owns every acquired process tree | Reach the smallest native point at which the product conversation and process can both be controlled | `start()` publishes one existing `SubagentRun`, or cleans up and rejects |
| Run | The product owns its native protocol facts; the holder owns their mapping | Submit exactly one task and derive one shared `completed`, `error`, or `aborted` result | The parent receives only a final answer or an explicit failure |
| Run | The product owns its native protocol facts; the holder owns their mapping | Submit exactly one task and derive an existing shared stop reason; Codex uses `max-tokens` only for explicit context exhaustion | The parent receives only a final answer or an explicit failure |
| Dispose | The foreground consumer requests release; `dsh-subprocess` proves exit | Close the native protocol and express any best-effort native cancellation | Disposal is idempotent and returns only after the whole process tree exits |
## Codex provider
@@ -39,9 +39,9 @@ fixed tool → shared subagent service → product provider → official product
Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize``initialized`, and creates an `ephemeral: true` thread. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
`turn/completed` is the authoritative remote terminal fact. The latest nonblank `agentMessage` with `phase: "final_answer"` wins. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback; commentary never replaces either answer. A completed turn without an answer, a failed or interrupted remote turn, malformed wire data, protocol closure, early process exit, or unknown server request becomes `error`. Local cancellation wins its race and remains `aborted`.
`turn/completed` is the authoritative remote terminal fact. The latest nonblank `agentMessage` with `phase: "final_answer"` wins. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback; commentary never replaces either answer. A failed turn with `error.codexErrorInfo: "contextWindowExceeded"` becomes `max-tokens`. A completed turn without an answer, every other failed or interrupted remote turn, malformed wire data, protocol closure, early process exit, or unknown server request becomes `error`; this version has no native refusal terminal and therefore produces no `refusal`. Local cancellation wins its race and remains `aborted`.
The unattended wire declines command and file approvals, grants no requested permissions for the turn, and declines MCP elicitation. Any other server request fails the run instead of waiting for a user interface the provider does not supply.
For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.146.0 request shape without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.
An unpublished startup failure closes the wire, terminates the acquired process tree, waits for exit, and then rejects `start()`. Published disposal best-effort interrupts a known turn, closes the wire, ends stdin, invokes the shared termination escalation, and waits for whole-tree exit. Result failure and teardown failure stay independently observable.
@@ -51,11 +51,11 @@ The Claude Code sibling is not yet implemented. Its product version, official in
## Evidence contract
Each product owns branch-complete package tests, a required real-product spec, and a real Loader snapshot. The real-product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test.
Each product owns branch-complete package tests, a required real-product spec, and a Loader composition e2e. The real-product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The separate Loader tier boots the README-shaped user configuration, verifies the fixed provider and foreground-only common tool, and must not start a product process.
The Codex evidence pins `@openai/codex@0.146.0` and `codex-cli 0.146.0`. Its real-product spec observes the exact Bearer key, original task, byte-exact final answer, unattended command rejection with no file side effect, local cancellation, and whole-tree exit. Its Loader snapshot fixes the no-background tool schema, exact tool call and result, complete persisted parent Session, product request, and pre-teardown quiescence. The npm package is a development dependency for reproducible evidence; production still supplies `codex` on `PATH`.
The Codex evidence pins `@openai/codex@0.146.0` and `codex-cli 0.146.0`. Its real-product spec observes the exact Bearer key, original task, byte-exact final answer, unattended command rejection with no file side effect, local cancellation, and whole-tree exit. Its Loader e2e resolves `@deepseek-ai/dsh-subagent-codex` by package name, verifies the `codex` registration and `subagent_codex` schema with background omitted, accepts `maxDepth: 'provider-managed'`, and records zero child starts while no `codex` command is available. The npm package is a development dependency for reproducible real-product evidence; production still supplies `codex` on `PATH`.
The combined contract is complete only when the Claude sibling has equivalent real-product evidence and one assembled Loader run proves both fixed tools coexist without changing the common subagent contract.
The combined contract is complete only when the Claude sibling has equivalent real-product evidence and both public Loader configurations prove the fixed tools use the unchanged common subagent contract.
## Alternatives considered
@@ -73,7 +73,7 @@ The combined contract is complete only when the Claude sibling has equivalent re
## Acceptance criteria
Both fixed tools reach their corresponding real products through the Loader, return exact final answers or explicit failure or cancellation, persist the complete model-visible parent transcript, and prove managed process-tree quiescence in required keyless CI. Both packages document their configuration, lifecycle, failure behavior, model experience, and limitations; generated package, configuration, capability, dependency, and third-party records agree with the shipped manifests.
Both public provider packages load from user-owned Cordis configurations and form their fixed foreground tools without appearing in the shipped CLI defaults. Separate required real-product specs return exact final answers or explicit failure or cancellation and prove managed process-tree quiescence. Both packages document their configuration, lifecycle, failure behavior, model experience, and limitations; generated package, configuration, capability, dependency, and third-party records agree with the shipped manifests.
The implemented Codex half satisfies this contract for its fixed tool and 0.146.0 baseline. The proposal becomes implemented only after the Claude Code sibling and the combined two-product evidence satisfy the same ownership and lifecycle boundaries.

View File

@@ -8,15 +8,15 @@ Status: proposed
命名的 [`ctx.subagents`](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 注册表让父 agent智能体无需了解子级的运行方式即可委派工作但 harness 需要通往真实 Codex 与 Claude Code 产品的第一方路径。可用的首版必须能向任一产品交付一项自包含任务,让它在父会话的工作区中执行,返回最终回答或明确的失败或取消结果,并且不留下任何受管的产品进程。
产品集成不得成为任务文本、cwd、取消、结果结算或进程树的第二责任方。它们还必须在强制性的无密钥测试中证明真实组装路径。直接发起模型 HTTP 请求使用产品替身或手工挂载插件,都无法证明 Loader、固定工具、提供方注册、官方产品协议、原生身份验证形态、最终回答和资源清理能够协同工作
产品集成不得成为任务文本、cwd、取消、结果结算或进程树的第二责任方。因此,强制性的无密钥证据会分别证明两个事实真实产品测试证明官方协议、原生身份验证形态、最终回答和资源清理Loader 装配测试证明公开包与文档中的工具配置可以加载,且不会启动产品。直接发起模型 HTTP 请求使用产品替身无法取代前者,手工挂载插件则无法取代后者
## 提案
harness 在两个固定的面向模型工具背后提供两个一次性兄弟提供方。`subagent_codex` 选择 `codex` 提供方`subagent_claude_code` 选择 `claude-code` 提供方。每个工具只接受独立文本任务,并在部署时绑定其提供方;产品选择与后台执行都不作为模型参数。
harness 将两个一次性兄弟提供方发布为可独立安装的可选包。用户在自己的 `cordis.yml` 中加载提供方与现有的通用 subagent 工具:`subagent_codex` 绑定 `codex``subagent_claude_code` 绑定 `claude-code`。正式 CLI 的依赖闭包以及基础、Web 和 headless 配置都不加载这两个提供方。每个工具只接受独立文本任务;产品选择与后台执行都不作为模型参数。
Codex 提供方基于 Codex 0.146.0 实现。Claude Code 提供方仍未实现。在两个兄弟提供方及其组合证据全部具备之前,本 Agent Note 将保持提案状态。
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动时功能,并传递父会话 cwd但不会复制父级对话。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。共享 subagent 服务继续负责请求解析、生命周期事件、结果结算和前台收集;共享子进程服务负责凭证清洗、进程树终止以及整棵进程树的退出观测。
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动时功能,并传递父会话 cwd但不会复制父级对话。文档中的工具会关闭后台执行并使用 `maxDepth: 'provider-managed'`,让进程外产品自行负责递归策略,而不会向提供方发送其无法执行的限制。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。共享 subagent 服务继续负责请求解析、生命周期事件、结果结算和前台收集;共享子进程服务负责凭证清洗、进程树终止以及整棵进程树的退出观测。
```text
fixed tool → shared subagent service → product provider → official product process
@@ -30,7 +30,7 @@ fixed tool → shared subagent service → product provider → official product
| --- | --- | --- | --- |
| 解析 | `dsh-tool-subagent``ctx.subagents` | 验证产品的纯文本输入并推导原生启动参数 | 不受支持的上下文或格式错误的输入会在发布运行前报错 |
| 启动 | `dsh-subprocess` 负责每棵已获取的进程树 | 到达能够同时控制产品对话与进程的最小原生控制点 | `start()` 发布一个已存在的 `SubagentRun`,否则清理后拒绝调用 |
| 运行 | 产品负责其原生协议事实;持有方负责映射这些事实 | 只提交一项任务,并推导一个共享的 `completed``error``aborted` 结果 | 父级只会收到最终回答或明确失败 |
| 运行 | 产品负责其原生协议事实;持有方负责映射这些事实 | 只提交一项任务,并推导出一种现有的共享停止原因Codex 仅在明确发生上下文耗尽时使用 `max-tokens` | 父级只会收到最终回答或明确失败 |
| dispose资源释放 | 前台消费方请求释放;`dsh-subprocess` 证明进程已退出 | 关闭原生协议,并发出尽力而为的原生取消请求 | 释放操作具有幂等性,且仅在整棵进程树退出后才返回 |
## Codex 提供方
@@ -39,9 +39,9 @@ fixed tool → shared subagent service → product provider → official product
发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server完成 `initialize``initialized` 握手,并创建一个 `ephemeral: true` 线程。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
`turn/completed` 是权威的远端终止事实。以最后一条非空白的 `agentMessage` 为准,但它必须带有 `phase: "final_answer"`。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退;过程说明绝不会取代上述任一答案。轮次完成却没有答案、远端轮次失败或中断、协议数据格式错误、协议关闭、进程提前退出或未知的服务器请求,都会产生 `error`。本地取消在竞态中胜出并保持为 `aborted`
`turn/completed` 是权威的远端终止事实。以最后一条非空白的 `agentMessage` 为准,但它必须带有 `phase: "final_answer"`。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退;过程说明绝不会取代上述任一答案。带有 `error.codexErrorInfo: "contextWindowExceeded"` 的失败轮次会成为 `max-tokens`轮次完成却没有答案、其他任何远端失败或中断轮次、协议数据格式错误、协议关闭、进程提前退出或未知的服务器请求,都会产生 `error`;本版本没有原生的拒绝终止状态,因此不会产生 `refusal`。本地取消在竞态中胜出并保持为 `aborted`
无人值守的协议连接会拒绝命令与文件审批,不授予该轮次请求的任何权限,并拒绝 MCP elicitation。其他任何服务器请求都会导致此次运行失败,而不会等待本提供方没有提供的用户界面。
对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.146.0 请求形态没有决策选项列表,因此回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。若请求在无人值守模式下没有合法响应或是未知服务器请求此次运行就会失败,而不会等待本提供方没有提供的用户界面。
若启动在发布前失败,提供方会关闭协议连接、终止已获取的进程树并等待其退出,然后拒绝 `start()`。对已发布的运行执行释放时,提供方会尽力中断已知轮次、关闭协议连接、结束标准输入、调用共享的进程树逐级终止机制,并等待整棵进程树退出。结果失败与清理失败仍可彼此独立地观察。
@@ -51,11 +51,11 @@ Claude Code 兄弟提供方尚未实现。其中间提案不固定产品版本
## 证据契约
每个产品都负责覆盖所有分支的包package测试、一项必跑的真实产品测试和一个真实 Loader 快照。真实产品测试层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。
每个产品都负责覆盖所有分支的包package测试、一项必跑的真实产品测试和一 Loader 装配 e2e。真实产品测试层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。独立的 Loader 层级会启动与 README 同形的用户配置,验证固定提供方与只支持前台执行的通用工具,并且不得启动产品进程。
Codex 证据锁定 `@openai/codex@0.146.0``codex-cli 0.146.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、不会产生文件副作用的无人值守命令拒绝、本地取消以及整棵进程树退出。其 Loader 快照锁定不支持后台执行的工具 schema、确切的工具调用与结果、完整的已持久化父会话、产品请求以及清理前的完全停稳状态。该 NPM 包是用于复现证据的开发依赖;生产环境仍提供 `codex`,并通过 `PATH` 解析。
Codex 证据锁定 `@openai/codex@0.146.0``codex-cli 0.146.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、不会产生文件副作用的无人值守命令拒绝、本地取消以及整棵进程树退出。其 Loader e2e 会按包名解析 `@deepseek-ai/dsh-subagent-codex`,验证 `codex` 注册与省略后台参数的 `subagent_codex` schema接受 `maxDepth: 'provider-managed'`,并在环境中没有可用 `codex` 命令时记录零次子级启动。该 NPM 包是用于复现真实产品证据的开发依赖;生产环境仍提供 `codex`,并通过 `PATH` 解析。
只有在 Claude 兄弟提供方具备同等的真实产品证据,并且一次组装后的 Loader 运行证明两个固定工具可以共存且无需更改通用 subagent 契约时,组合契约才算完整。
只有在 Claude 兄弟提供方具备同等的真实产品证据,并且两个公开 Loader 配置都证明固定工具使用未变的通用 subagent 契约时,组合契约才算完整。
## 曾考虑的替代方案
@@ -73,7 +73,7 @@ Codex 证据锁定 `@openai/codex@0.146.0` 与 `codex-cli 0.146.0`。其真实
## 验收标准
两个固定工具都通过 Loader 到达相应的真实产品返回完全一致的最终回答或明确的失败或取消结果,持久化完整的模型可见父级 transcript并在强制性的无密钥 CI 中证明受管进程树完全停稳。两个包都会记录其配置、生命周期、失败行为、模型体验和限制;生成的包、配置、功能、依赖与第三方记录均与已交付的 manifest元数据清单一致。
两个公开提供方包都能从用户自有的 Cordis 配置加载并组成固定的前台工具,而且不会出现在正式 CLI 默认配置中。独立的强制真实产品测试会返回完全一致的最终回答或明确的失败或取消结果,证明受管进程树完全停稳。两个包都会记录其配置、生命周期、失败行为、模型体验和限制;生成的包、配置、功能、依赖与第三方记录均与已交付的 manifest元数据清单一致。
已经实现的 Codex 部分为其固定工具和 0.146.0 基线满足了本契约。只有在 Claude Code 兄弟提供方及两种产品的组合证据满足相同的归属与生命周期边界后,本提案才会进入 implemented 状态。

View File

@@ -104,6 +104,7 @@ flowchart LR
pkg_lsp_local["lsp-local"]
pkg_subagent_acp["subagent-acp"]
pkg_subagent_codex["subagent-codex"]
pkg_subagent_dsh_sdk["subagent-dsh-sdk"]
pkg_bash["bash"]
svc_bash["ctx.bash<br/>Bash executor seam"]
svc_bashEnv["ctx.bashEnv<br/>Managed bash environment registry"]
@@ -225,6 +226,7 @@ flowchart LR
pkg_subagent --> svc_subagents
pkg_subagent_acp --> svc_subagents
pkg_subagent_codex --> svc_subagents
pkg_subagent_dsh_sdk --> svc_subagents
pkg_subagent_fork --> svc_subagents
pkg_subagent_spawn --> svc_subagents
pkg_subprocess --> svc_subprocess
@@ -314,6 +316,7 @@ flowchart LR
svc_subprocess --> pkg_lsp_local
svc_subprocess --> pkg_subagent_acp
svc_subprocess --> pkg_subagent_codex
svc_subprocess --> pkg_subagent_dsh_sdk
svc_systemPrompt --> pkg_agent_loop
svc_systemPrompt --> pkg_tool_fs
svc_systemPrompt --> pkg_tool_pty
@@ -373,7 +376,7 @@ flowchart LR
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex) | - | The bash executors, the LSP host, and the out-of-process ACP and Codex subagent backends spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation. |
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | - | The bash executors, the LSP host, and the out-of-process ACP, Codex, and DSH SDK subagent backends spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation. |
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them. |
| `ctx.bashEnv` | `core` | [`tool-bash`](../packages/bash/tool-bash) | - | - | - | Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace. |
| `ctx.pty` | `seam` | [`pty`](../packages/pty/pty) | [`pty-local`](../packages/pty/pty-local) | [`tool-pty`](../packages/pty/tool-pty) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model surface. |
@@ -384,7 +387,7 @@ flowchart LR
| `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | [`tools`](../packages/core/tools) | - | Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the tool registry consumes it for Code Mode). |
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate. |
| `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; a model-facing compact tool remains deferred. |
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pty`](../packages/pty/tool-pty), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it; tasks-local is the process-local registry. |
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |

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 docs/cookbook/extension-cookbook.md
extension-cookbook.md: 379bd2644a4a7de005c8ea56d4857b6a6f9143b8
extension-cookbook.zh.md: 09a5163c3b47e52e0e0e88f000a8f04302fa93e4
extension-cookbook.md: 820d7fce8560028f592ec101f4f222013105f035
extension-cookbook.zh.md: bb48f584729ad3a67fd43ca8c4c9f6f4902de656

View File

@@ -118,7 +118,7 @@ Every product feature maps to a listener on a documented extension seam — the
| Subprocess sandbox (landlock / sandbox-exec) | use a `ctx.sandbox` backend through `dsh-bash-sandbox`; use `tools/pre-execute` for capability-level denial |
| Permission system / AskUserQuestion | return `ask` from `tools/pre-execute` and answer through `ctx.approval`; register a separate model-facing ask tool for ordinary user questions |
| Plan mode | Shipped: [`@deepseek-ai/dsh-plan-mode`](../../packages/plan/plan-mode/README.md) — logged `plan/mode` state, the `plan:policy` guidance section, `/plan [message]` entry, `/plan off` direct exit, and the user-reviewed `exit_plan_mode` exit; enforcement stays on the independent sandbox/approval axes |
| Sub-agent delegation | the `ctx.subagents` provider registry (`dsh-subagent-spawn`/`-fork`/`-acp`/`-codex`) + `dsh-tool-subagent` exposing one configured provider to the model |
| Sub-agent delegation | the `ctx.subagents` provider registry (`dsh-subagent-spawn`/`-fork`/`-acp`/`-codex`/`-dsh-sdk`) + `dsh-tool-subagent` exposing one configured provider to the model |
| MCP | one plugin per server: discover tools → `ctx.tools.register()` |
| Skills | section + tool registration; `inject()` skill content on invocation |
| Memory | section provider + tool |

View File

@@ -118,7 +118,7 @@ export function apply(ctx: Context) {
| 子进程沙箱landlock / sandbox-exec | 通过 `dsh-bash-sandbox` 使用 `ctx.sandbox` 后端;能力级别的拒绝使用 `tools/pre-execute` |
| 权限系统 / AskUserQuestion | 从 `tools/pre-execute` 返回 `ask` 并通过 `ctx.approval` 应答;为普通用户提问注册一个独立的面向模型的 ask 工具 |
| Plan mode | 已交付:[`@deepseek-ai/dsh-plan-mode`](../../packages/plan/plan-mode/README.md) — 落日志的 `plan/mode` 状态、`plan:policy` 引导段、`/plan [message]` 入口、`/plan off` 直接退出,以及经用户评审的 `exit_plan_mode` 出口;强制约束留在独立的沙箱/审批轴上 |
| 子 agent 委派 | `ctx.subagents` 提供方注册表(`dsh-subagent-spawn`/`-fork`/`-acp`/`-codex`+ `dsh-tool-subagent` 向模型暴露一个已配置的提供方 |
| 子 agent 委派 | `ctx.subagents` 提供方注册表(`dsh-subagent-spawn`/`-fork`/`-acp`/`-codex`/`-dsh-sdk`+ `dsh-tool-subagent` 向模型暴露一个已配置的提供方 |
| MCP | 每个服务器一个插件:发现工具 → `ctx.tools.register()` |
| Skill技能 | section + 工具注册;调用时通过 `inject()` 注入 skill 内容 |
| 记忆 | section provider + 工具 |

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 docs/core-data-structures/subagent.md
subagent.md: 917913470da389dccac83e455cf23486a94c23b1
subagent.zh.md: efe12a5a5fe40d664f3071036157acd704b10c57
subagent.md: c810ae40f57a0f84f1a6b53e092d6bec88af206f
subagent.zh.md: efeec0a0f20b7ac85020df012878cf41f264073d

View File

@@ -4,7 +4,7 @@ English | [中文](subagent.zh.md)
The subagent seam — an agent delegating work to a child agent. Like [bash](bash.md) it is **one optional capability**, not part of the agent-loop spine, so its vocabulary lives here rather than in [core.md](core.md). But it differs from every other seam on one axis: **multiple provider implementations coexist** in one context, registered by name (`ctx.subagents`), where bash allows only one executor. The registry shape mirrors the [LLM adapter registry](llm-streaming.md), not the single-service bash executor.
Interface: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Implementations are sibling packages (`dsh-subagent-spawn`, `-fork`, `-acp`, `-codex`); the model-facing consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation), [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the optional global `send_message` and `list_agents` controls), and [dsh-tool-subagent-report](../../packages/subagent/tool-subagent-report) (the optional child-scoped `report` return channel). The same `ctx.subagents` service owns continuable-child orchestration through an internal activation manager and read-only direct-child discovery through optional session query. The rationale lives in [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [the continuable subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), [the report-tool Agent Note](../../.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md), [the durable catalog Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md), and [the merged-service Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md).
Interface: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Implementations are sibling packages (`dsh-subagent-spawn`, `-fork`, `-acp`, `-codex`, `-dsh-sdk`); the model-facing consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation), [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the optional global `send_message` and `list_agents` controls), and [dsh-tool-subagent-report](../../packages/subagent/tool-subagent-report) (the optional child-scoped `report` return channel). The same `ctx.subagents` service owns continuable-child orchestration through an internal activation manager and read-only direct-child discovery through optional session query. The rationale lives in [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [the continuable subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), [the report-tool Agent Note](../../.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md), [the durable catalog Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md), and [the merged-service Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md).
Sources: [`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts), [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts), and [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts)

View File

@@ -4,7 +4,7 @@
subagent seam一个 agent智能体将工作委派给子 agent。与 [bash](bash.md) 一样,它是**一项可选能力**,不属于 agent loop智能体循环主干因此其词汇定义在此而非 [core.md](core.md) 中。但它在一个维度上与其他所有 seam 不同:**同一上下文中可共存多个提供方实现**,按名称注册(`ctx.subagents`),而 bash 只允许一个执行器。注册表的形状参照 [LLM大语言模型适配器注册表](llm-streaming.md),而非单服务的 bash 执行器。
接口:[dsh-subagent](../../packages/subagent/subagent)`ctx.subagents` + 下文词汇)。实现为个兄弟包package`dsh-subagent-spawn``-fork``-acp``-codex`;面向模型的消费方包括 [dsh-tool-subagent](../../packages/subagent/tool-subagent)(按提供方委派)、[dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)(可选的全局 `send_message``list_agents` 控制工具)和 [dsh-tool-subagent-report](../../packages/subagent/tool-subagent-report)(可选的 child 作用域 `report` 返回通道)。同一个 `ctx.subagents` 服务通过内部激活管理器负责可继续子 agent 编排,并通过可选的会话查询负责只读的直接 child 发现。设计理由见 [subagent Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可继续 subagent Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)、[report 工具 Agent Note](../../.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md)、[持久化目录 Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)和[服务合并 Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)。
接口:[dsh-subagent](../../packages/subagent/subagent)`ctx.subagents` + 下文词汇)。实现为个兄弟包package`dsh-subagent-spawn``-fork``-acp``-codex``-dsh-sdk`;面向模型的消费方包括 [dsh-tool-subagent](../../packages/subagent/tool-subagent)(按提供方委派)、[dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)(可选的全局 `send_message``list_agents` 控制工具)和 [dsh-tool-subagent-report](../../packages/subagent/tool-subagent-report)(可选的 child 作用域 `report` 返回通道)。同一个 `ctx.subagents` 服务通过内部激活管理器负责可继续子 agent 编排,并通过可选的会话查询负责只读的直接 child 发现。设计理由见 [subagent Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可继续 subagent Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)、[report 工具 Agent Note](../../.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md)、[持久化目录 Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)和[服务合并 Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)。
源码:[`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)、[`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts)和 [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts)

View File

@@ -1,5 +1,5 @@
# Test-only composition: one real Codex app-server delegation through the
# Loader, fixed provider tool, common foreground settlement, and JSONL store.
# Test-only composition of the public opt-in provider and foreground tool.
# The owning e2e boots this tree but never invokes the model or Codex.
- id: fixture
name: './fixture.ts'
@@ -11,17 +11,6 @@
- id: subagent-codex
name: '@deepseek-ai/dsh-subagent-codex'
config:
env:
OPENAI_API_KEY: !!js process.env.DSH_TEST_OPENAI_API_KEY
CODEX_HOME: !!js process.env.DSH_TEST_CODEX_HOME
HOME: !!js process.cwd()
XDG_CONFIG_HOME: !!js process.cwd() + '/xdg'
PATH: !!js process.env.PATH
HTTP_PROXY: ''
HTTPS_PROXY: ''
ALL_PROXY: ''
NO_PROXY: '127.0.0.1,localhost'
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
@@ -36,7 +25,5 @@
config:
provider: mock
model: mock-delegate
persona: 'Delegate the task through the fixed Codex tool.'
persistenceRoot: './.sessions'
persistenceCompression: 'none'
persona: 'This composition test must not start a model turn.'
workspaceContext: false

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env node
/** Inspect the public Codex provider composition without invoking the product. */
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import type {} from '@deepseek-ai/dsh-subagent'
import type {} from '@deepseek-ai/dsh-tools'
const configPath = process.argv[2]
if (configPath === undefined) {
throw new Error('subagent-codex Loader composition driver requires a config path')
}
let starts = 0
const ctx = await boot(
'subagent-codex-loader-composition',
resolveConfigPath(configPath, undefined),
undefined,
(hostCtx) => {
hostCtx.on('subagent/start', () => {
starts += 1
})
},
)
try {
const provider = ctx.subagents.getProvider('codex')
if (provider === undefined) throw new Error('Codex provider was not registered')
const tool = ctx.tools.schemas().find(schema => schema.name === 'subagent_codex')
if (tool === undefined) throw new Error('subagent_codex tool was not registered')
const properties = tool.parameters.properties
if (typeof properties !== 'object' || properties === null || Array.isArray(properties)) {
throw new Error('subagent_codex tool has invalid parameter properties')
}
process.stdout.write(`${JSON.stringify({
providers: ctx.subagents.list(),
provider: {
name: provider.name,
capabilities: provider.capabilities,
inheritsParentContext: provider.inheritsParentContext,
},
tool: {
name: tool.name,
parameterNames: Object.keys(properties).sort(),
required: tool.parameters.required,
},
starts,
})}\n`)
} finally {
await ctx.fiber.dispose()
}

View File

@@ -1,102 +1,22 @@
/** Deterministic parent model and process-quiescence observer for the Codex Loader snapshot. */
/** Parent adapter that fails if the composition-only Loader test starts a turn. */
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import type { Context } from 'cordis'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type {
SubprocessHandle,
SubprocessSpawnSpec,
} from '@deepseek-ai/dsh-subprocess'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
const CODEX_TASK = 'Return the Loader snapshot sentinel exactly.'
const QUIESCENCE_FILE = '.codex-quiescence.json'
function toolResultText(options: GenerateOptions): string {
return options.messages.at(-1)?.content
.filter(block => block.type === 'tool-result')
.flatMap(block => block.content)
.filter(block => block.type === 'text')
.map(block => block.text)
.join('') ?? ''
}
class CodexDelegatingAdapter extends LlmAdapter {
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const result = toolResultText(options)
if (result.length === 0) {
const args = JSON.stringify({
description: 'Codex Loader snapshot',
prompt: CODEX_TASK,
})
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
yield {
type: 'tool-call-delta',
index: 0,
id: CallId('call-codex-loader'),
name: 'subagent_codex',
argumentsDelta: args,
}
yield {
type: 'block-end',
index: 0,
block: {
type: 'tool-call',
id: CallId('call-codex-loader'),
name: 'subagent_codex',
arguments: args,
},
}
yield { type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } }
yield { type: 'finish', reason: { kind: 'tool-calls' } }
return
}
const reply = `Codex child returned: ${result}`
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'text-delta', index: 0, text: reply }
yield { type: 'block-end', index: 0, block: { type: 'text', text: reply } }
yield { type: 'usage', usage: { inputTokens: 10, outputTokens: reply.length } }
yield { type: 'finish', reason: { kind: 'stop' } }
class CompositionOnlyAdapter extends LlmAdapter {
async * stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
throw new Error('subagent-codex Loader composition must not invoke a model')
}
}
interface ObservedProcess {
readonly spec: SubprocessSpawnSpec
readonly handle: SubprocessHandle
}
export const name = 'codex-loader-snapshot-fixture'
export const inject = ['llm', 'subprocess']
export const name = 'codex-loader-composition-fixture'
export const inject = ['llm']
/**
* Register the deterministic parent adapter and record whether every spawned
* product tree was already quiet when the assembled application disposed.
* @param ctx - Loader context supplying the LLM and subprocess seams.
* Register a parent adapter solely so the host composition is complete.
* @param ctx - Loader context supplying the LLM seam.
*/
export function apply(ctx: Context): void {
ctx.llm.registerAdapter(['mock'], new CodexDelegatingAdapter())
ctx.effect(() => {
const observed: ObservedProcess[] = []
const originalSpawn = ctx.subprocess.spawn.bind(ctx.subprocess)
ctx.subprocess.spawn = (spec: SubprocessSpawnSpec): SubprocessHandle => {
const handle = originalSpawn(spec)
observed.push({ spec, handle })
return handle
}
return async () => {
ctx.subprocess.spawn = originalSpawn
const alreadyExited = AbortSignal.abort()
const processes = await Promise.all(observed.map(async ({ spec, handle }) => ({
argv: [...spec.argv],
quiescent: await handle.waitForExit(alreadyExited),
outcome: await handle.done,
})))
await writeFile(
join(process.cwd(), QUIESCENCE_FILE),
`${JSON.stringify({ processes })}\n`,
)
}
}, 'codex Loader snapshot process observer')
ctx.llm.registerAdapter(['mock'], new CompositionOnlyAdapter())
}

View File

@@ -1,38 +0,0 @@
{
"stdout": {
"type": "result",
"success": true,
"sessionId": "{{sessionId}}",
"turn": 1,
"result": "Codex child returned: REAL_CODEX_LOADER_SENTINEL_0_146_0",
"reason": {
"kind": "completed"
},
"usage": {
"inputTokens": 20,
"outputTokens": 61
}
},
"request": {
"method": "POST",
"path": "/v1/responses",
"authorization": "Bearer dsh-fake-openai-loader-key",
"taskObserved": true
},
"quiescence": {
"processes": [
{
"argv": [
"codex",
"app-server",
"--stdio"
],
"quiescent": true,
"outcome": {
"exitCode": 0,
"signal": null
}
}
]
}
}

View File

@@ -1,25 +0,0 @@
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Delegate through Codex once."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
{"type":"session/title","seq":2,"time":0,"data":{"title":"Delegate through Codex once.","messageSeqs":[1],"source":{"kind":"fallback"}}}
{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}
{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"mock","model":"mock-delegate"},"system":"{{system}}","tools":[{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent_codex","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}}]},"reason":"initial"}}
{"type":"request/context","seq":5,"time":0,"data":{"provider":"mock","model":"mock-delegate"}}
{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call-codex-loader","name":"subagent_codex","argumentsDelta":"{\"description\":\"Codex Loader snapshot\",\"prompt\":\"Return the Loader snapshot sentinel exactly.\"}"}}}
{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call-codex-loader","name":"subagent_codex","arguments":"{\"description\":\"Codex Loader snapshot\",\"prompt\":\"Return the Loader snapshot sentinel exactly.\"}"}}}}
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":11,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call-codex-loader","name":"subagent_codex","arguments":"{\"description\":\"Codex Loader snapshot\",\"prompt\":\"Return the Loader snapshot sentinel exactly.\"}"}],"source":{"kind":"model","provider":"mock","model":"mock-delegate"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[6,7,8,9,10],"surfaceOp":"append"}
{"type":"tool/call","seq":12,"time":0,"data":{"turn":1,"step":1,"callId":"call-codex-loader","name":"subagent_codex","arguments":"{\"description\":\"Codex Loader snapshot\",\"prompt\":\"Return the Loader snapshot sentinel exactly.\"}"}}
{"type":"tool/result","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call-codex-loader"},"content":[{"type":"tool-result","toolCallId":"call-codex-loader","content":[{"type":"text","text":"REAL_CODEX_LOADER_SENTINEL_0_146_0"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[12],"surfaceOp":"append"}
{"type":"step/end","seq":14,"time":0,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":15,"time":0,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"Codex child returned: REAL_CODEX_LOADER_SENTINEL_0_146_0"}}}
{"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Codex child returned: REAL_CODEX_LOADER_SENTINEL_0_146_0"}}}}
{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":56}}}}
{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"Codex child returned: REAL_CODEX_LOADER_SENTINEL_0_146_0"}],"source":{"kind":"model","provider":"mock","model":"mock-delegate"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":56}},"sourceEventSeqs":[16,17,18,19,20],"surfaceOp":"append"}
{"type":"step/end","seq":22,"time":0,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":23,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}

View File

@@ -1,171 +0,0 @@
/**
* Real-product Loader snapshots for fixed subagent providers.
*
* PR1 owns the Codex scenario. PR2 extends this file with the sibling Claude
* Code scenario and reruns both from its final stacked candidate.
*/
import { homedir } from 'node:os'
import { dirname, delimiter, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { describe, expect, it } from 'vitest'
import {
normalizeSessionLog,
normalizeStdout,
scrubSystemPrompts,
type NormalizeContext,
} from '@deepseek-ai/dsh-acp-snapshot'
import {
LOADER_SMOKE_TEST_TIMEOUT_MS,
runLoaderSmoke,
} from '@deepseek-ai/dsh-loader-smoke'
import { startResponsesFixture } from '../../../packages/subagent/subagent-codex/tests/responses-fixture.ts'
const testsDir = dirname(fileURLToPath(import.meta.url))
const repoRoot = fileURLToPath(new URL('../../..', import.meta.url))
const fixtureDir = join(testsDir, 'fixtures/subagent/subagent-codex')
const configPath = join(fixtureDir, 'cordis.yml')
const snapshotDir = join(testsDir, 'product-provider-snapshots/codex')
const sessionExpected = join(snapshotDir, 'session.expected.jsonl')
const evidenceExpected = join(snapshotDir, 'evidence.expected.json')
const cliBin = join(repoRoot, 'packages/examples/cli-demo/src/bin.ts')
const repoTsconfig = join(repoRoot, 'tsconfig.json')
const codexBinDir = join(
repoRoot,
'packages/subagent/subagent-codex/node_modules/.bin',
)
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
const CODEX_SENTINEL = 'REAL_CODEX_LOADER_SENTINEL_0_146_0'
const FAKE_KEY = 'dsh-fake-openai-loader-key'
interface PersistedSession {
readonly content: string
readonly header: {
readonly id: string
readonly cwd: string
}
}
async function onlySession(root: string): Promise<PersistedSession> {
const paths = (await readdir(root, { recursive: true }))
.filter(path => path.endsWith('.jsonl'))
expect(paths).toHaveLength(1)
const path = paths[0]
if (path === undefined) throw new Error('Codex Loader snapshot persisted no session')
const content = await readFile(join(root, path), 'utf8')
const header = JSON.parse(content.slice(0, content.indexOf('\n'))) as PersistedSession['header']
return { content, header }
}
function responseInputTexts(body: Record<string, unknown>): string[] {
if (!Array.isArray(body.input)) return []
return body.input.flatMap((item): string[] => {
if (item === null || typeof item !== 'object') return []
const content = (item as Record<string, unknown>).content
if (!Array.isArray(content)) return []
return content.flatMap((part): string[] => (
part !== null
&& typeof part === 'object'
&& typeof (part as Record<string, unknown>).text === 'string'
? [(part as Record<string, unknown>).text as string]
: []
))
})
}
describe('real product subagent providers through the Loader', () => {
it('pins the Codex tool, result, persisted Session, and process quiescence', async () => {
const codexHome = await mkdtemp(join(homedir(), '.dsh-subagent-codex-loader-'))
const responses = await startResponsesFixture([
{ kind: 'complete', text: CODEX_SENTINEL },
])
let session: PersistedSession | undefined
let quiescence: unknown
try {
const result = await runLoaderSmoke({
label: 'Codex subagent Loader snapshot',
tempDirPrefix: 'dsh-subagent-codex-loader-',
binScript: cliBin,
configPath,
binArgs: [
'--config',
configPath,
'--output-format',
'json',
'Delegate through Codex once.',
],
tsconfigPath: repoTsconfig,
processTimeoutMs: 45_000,
env: {
DSH_TEST_CODEX_HOME: codexHome,
DSH_TEST_OPENAI_API_KEY: FAKE_KEY,
PATH: `${codexBinDir}${delimiter}${process.env.PATH ?? ''}`,
},
async prepare(): Promise<void> {
await writeFile(join(codexHome, 'config.toml'), [
'model = "fixture-model"',
'model_provider = "fixture"',
'approval_policy = "on-request"',
'sandbox_mode = "read-only"',
'disable_response_storage = true',
'check_for_update_on_startup = false',
'',
'[model_providers.fixture]',
'name = "Fixture Responses"',
`base_url = "${responses.baseUrl}"`,
'env_key = "OPENAI_API_KEY"',
'wire_api = "responses"',
'requires_openai_auth = false',
'',
'[analytics]',
'enabled = false',
'',
].join('\n'))
},
async inspect(cwd): Promise<void> {
session = await onlySession(join(cwd, '.sessions'))
quiescence = JSON.parse(await readFile(join(cwd, '.codex-quiescence.json'), 'utf8'))
},
})
expect(result.stderr).toBe('')
expect(session).toBeDefined()
if (session === undefined) throw new Error('Codex Loader snapshot session was not inspected')
const context: NormalizeContext = {
sessionIds: [session.header.id],
cwd: session.header.cwd,
}
const normalizedSession = scrubSystemPrompts(normalizeSessionLog(session.content, context))
const request = responses.requests[0]
expect(request).toBeDefined()
if (request === undefined) throw new Error('Codex Loader snapshot made no Responses request')
const evidence = `${JSON.stringify({
stdout: JSON.parse(normalizeStdout(result.stdout, context)) as unknown,
request: {
method: request.method,
path: request.path,
authorization: request.headers.authorization,
taskObserved: responseInputTexts(request.body)
.includes('Return the Loader snapshot sentinel exactly.'),
},
quiescence,
}, null, 2)}\n`
if (refreshing) {
await mkdir(snapshotDir, { recursive: true })
await Promise.all([
writeFile(sessionExpected, normalizedSession),
writeFile(evidenceExpected, evidence),
])
}
expect(normalizedSession).toBe(await readFile(sessionExpected, 'utf8'))
expect(evidence).toBe(await readFile(evidenceExpected, 'utf8'))
} finally {
await Promise.all([
responses.close(),
rm(codexHome, { recursive: true, force: true }),
])
}
}, LOADER_SMOKE_TEST_TIMEOUT_MS + 30_000)
})

View File

@@ -67,7 +67,6 @@
"@deepseek-ai/dsh-subagent-dsh-sdk": "workspace:*",
"@deepseek-ai/dsh-subagent-fork": "workspace:*",
"@deepseek-ai/dsh-subagent-spawn": "workspace:*",
"@deepseek-ai/dsh-subprocess": "workspace:*",
"@deepseek-ai/dsh-subprocess-local": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-tasks-local": "workspace:*",

View File

@@ -46,6 +46,7 @@
"acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts",
"acp-agent/tests/fixtures/subagent/subagent-acp/driver.ts",
"acp-agent/tests/fixtures/subagent/subagent-codex/fixture.ts",
"acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts",
"jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/driver.ts",
"jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/child-mock-llm.ts",
"jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/mock-delegating-llm.ts",
@@ -543,7 +544,8 @@
},
"packages/subagent/subagent-codex": {
"entry": [
"tests/**/*.spec.ts"
"tests/**/*.spec.ts",
"tests/**/*.e2e.ts"
],
"project": [
"src/**/*.ts",

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: ca92f935539812351dd578dca700c9a0113dcd46
README.zh.md: 6f6690ea51970dd39c738ad0ec4f55c2a5ab2467
README.md: ce1c66427b562c08af06320f012f28b9e125ac45
README.zh.md: bef47586db77c70bec741629d8579ba0e2efba1e

View File

@@ -10,9 +10,9 @@ This package registers the fixed `codex` subagent provider. Each accepted run st
The published `run.result` starts exactly one turn. It accepts only notifications for that run's thread and turn, then waits for the authoritative `turn/completed` terminal notification. The latest `agentMessage` with `phase: "final_answer"` wins; when Codex emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback. Commentary never replaces either answer, and a successful turn with no nonblank answer settles as an error.
The unattended provider answers command and file approvals with `decline`, answers permission requests with an empty turn-scoped permission set, and declines MCP elicitation. Any other server request fails the run instead of waiting for interaction that this provider cannot supply.
For command and file approvals, the unattended provider selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.146.0 request shape without an offered-decision list falls back to `decline`. It answers permission requests with an empty turn-scoped permission set, answers user-input requests with no answers, and declines MCP elicitation. A request with no legal unattended response, or any unknown server request, fails the run.
Local cancellation wins the result race and maps to `aborted`; a remote interrupted or failed turn maps to `error`. `dispose()` is idempotent: it requests a best-effort `turn/interrupt` when the current ids are known, closes the JSON-RPC wire, ends stdin, invokes the shared process-tree termination escalation, and waits for whole-tree exit. Result failure and independent teardown failure remain separate.
Local cancellation wins the result race and maps to `aborted`. A failed turn whose `codexErrorInfo` is `contextWindowExceeded` maps to `max-tokens`; every other remote interrupted or failed turn maps to `error`, and this version produces no `refusal`. `dispose()` is idempotent: it requests a best-effort `turn/interrupt` with both current ids when they are known, closes the JSON-RPC wire, ends stdin, invokes the shared process-tree termination escalation, and waits for whole-tree exit. Result failure and independent teardown failure remain separate.
## Capabilities and context
@@ -27,6 +27,8 @@ 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.
Install this package and add the following rows to your own `cordis.yml`. Shipped CLI configurations do not load this provider or expose `subagent_codex` by default.
```yaml
- id: subagent-codex
name: '@deepseek-ai/dsh-subagent-codex'
@@ -45,7 +47,7 @@ Production resolves `codex` from `PATH` and uses the host's native Codex configu
## Product compatibility and evidence
The production wire intentionally implements only the app-server methods required by this one-shot contract. Development evidence is pinned to `@openai/codex@0.146.0` / `codex-cli 0.146.0`: package tests drive the real binary against a loopback Responses service with a non-empty fake key, and the Loader snapshot fixes the model-visible tool schema, exact tool result, persisted parent Session, original child task, authentication header, and pre-teardown process-tree quiescence. The npm package is a test-only dependency; deployments still supply `codex` on `PATH`.
The production wire intentionally implements only the app-server methods required by this one-shot contract. Development evidence is pinned to `@openai/codex@0.146.0` / `codex-cli 0.146.0`: the real-product spec drives the official binary against a loopback Responses service with a non-empty fake key and proves the task, authentication, exact answer, cancellation, approvals, and process-tree exit. A separate Loader composition e2e boots the README-shaped user configuration with no `codex` command available, verifies the fixed provider and foreground-only tool schema, and records zero child starts. The npm package is a test-only dependency; deployments still supply `codex` on `PATH`.
## Model Experience

View File

@@ -10,9 +10,9 @@
已发布的 `run.result` 恰好启动一个轮次。它只接受与此次运行的线程和轮次匹配的通知,随后等待权威的终止通知 `turn/completed`。以最后一条 `phase: "final_answer"``agentMessage` 为准;若 Codex 没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退。过程说明绝不会取代上述任一答案;成功完成的轮次若没有非空白答案,结果也会判为错误。
无人值守的提供方对命令与文件审批答复 `decline`对权限请求返回作用域限于当前轮次的空权限集,并拒绝 MCP elicitation。其他任何服务器请求都会导致此次运行失败,而不会等待本提供方无法提供的交互
对于命令与文件审批,无人值守的提供方会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.146.0 请求形态没有决策选项列表,因此回退到 `decline`。它对权限请求返回作用域限于当前轮次的空权限集,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败
本地取消会在结果竞态中胜出并映射为 `aborted`;远端轮次若中断或失败,则映射为 `error``dispose()` 具有幂等性:如果当前标识符已知,它会尽力请求 `turn/interrupt`,关闭 JSON-RPC 通信链路,结束标准输入,调用共享的进程树逐级终止机制,并等待整棵进程树退出。结果失败与独立的清理失败仍彼此分离。
本地取消会在结果竞态中胜出并映射为 `aborted`。失败轮次的 `codexErrorInfo` 若为 `contextWindowExceeded`,则映射为 `max-tokens`;其他任何远端中断或失败轮次都映射为 `error`,且本版本不会产生 `refusal``dispose()` 具有幂等性:如果当前的两个标识符已知,它会尽力请求 `turn/interrupt`,关闭 JSON-RPC 通信链路,结束标准输入,调用共享的进程树逐级终止机制,并等待整棵进程树退出。结果失败与独立的清理失败仍彼此分离。
## 能力与上下文
@@ -27,6 +27,8 @@
生产环境会从 `PATH` 中解析 `codex`,并使用宿主机原生的 Codex 配置与身份验证。本插件不安装 Codex、不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测版本。子进程 seam 会移除具有凭证特征的环境变量,因此供子进程使用的 API 密钥必须在 `env` 中显式提供;除非被覆盖,`PATH``HOME` 等普通环境变量值仍然可用。
请安装此包,并将以下配置项添加到你自己的 `cordis.yml`。正式 CLI 配置默认不会加载此提供方,也不会暴露 `subagent_codex`
```yaml
- id: subagent-codex
name: '@deepseek-ai/dsh-subagent-codex'
@@ -45,7 +47,7 @@
## 产品兼容性与证据
生产环境的协议层有意只实现这一单次执行契约所需的 app-server 方法。开发证据锁定在 `@openai/codex@0.146.0` / `codex-cli 0.146.0`测试使用非空的伪密钥,驱动真实二进制程序连接回环 Responses 服务Loader 快照则锁定模型可见的工具 schema、确切的工具结果、已持久化的父会话、原始子任务、身份验证请求头以及清理前进程树的完全停稳状态。该 NPM 包仅作为测试依赖;部署环境仍需通过 `PATH` 提供 `codex`
生产环境的协议层有意只实现这一单次执行契约所需的 app-server 方法。开发证据锁定在 `@openai/codex@0.146.0` / `codex-cli 0.146.0`真实产品测试使用非空的伪密钥,驱动官方二进制程序连接回环 Responses 服务,并证明任务、身份验证、精确回答、取消、审批与进程树退出。独立的 Loader 装配 e2e 会在没有可用 `codex` 命令时启动与 README 同形的用户配置,验证固定提供方与只支持前台执行的工具 schema并记录零次子级启动。该 NPM 包仅作为测试依赖;部署环境仍需通过 `PATH` 提供 `codex`
## 模型体验

View File

@@ -42,6 +42,7 @@
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-loader-smoke": "workspace:^",
"@deepseek-ai/dsh-sdk-protocol": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",

View File

@@ -39,6 +39,25 @@ function string(value: unknown, label: string): string {
return value
}
function unattendedDecision(params: JsonObject): 'cancel' | 'decline' {
const available = params.availableDecisions
if (available === undefined || available === null) return 'decline'
if (Array.isArray(available)) {
if (available.includes('cancel')) return 'cancel'
if (available.includes('decline')) return 'decline'
}
throw new Error('subagent-codex: app-server offered no unattended approval decision')
}
function isContextWindowExceeded(turn: JsonObject): boolean {
if (turn.status !== 'failed') return false
const error = turn.error
return error !== null
&& typeof error === 'object'
&& !Array.isArray(error)
&& (error as JsonObject).codexErrorInfo === 'contextWindowExceeded'
}
function thrown(value: unknown): Error {
/* v8 ignore next -- typed protocol and stream failures reject with Error. */
return value instanceof Error ? value : new Error(String(value))
@@ -160,7 +179,7 @@ export class CodexAppServerWire {
* @param texts - already validated task text blocks.
* @param signal - local cancellation for the published run.
* @param cancelled - whether local cancellation has already won.
* @returns the shared three-state subagent result.
* @returns the shared subagent result.
*/
async runTurn(
texts: readonly string[],
@@ -182,6 +201,9 @@ export class CodexAppServerWire {
const terminal = object(completed.turn, 'turn/completed turn')
const status = terminal.status
if (isContextWindowExceeded(terminal)) {
return { output: this.collectOutput(), stopReason: 'max-tokens' }
}
if (status !== 'completed') {
const detail = status === 'failed'
? `: ${JSON.stringify(terminal.error)}`
@@ -292,10 +314,13 @@ export class CodexAppServerWire {
case 'item/commandExecution/requestApproval':
case 'item/fileChange/requestApproval':
this.validateRunIds(params)
return Promise.resolve({ decision: 'decline' })
return Promise.resolve({ decision: unattendedDecision(params) })
case 'item/permissions/requestApproval':
this.validateRunIds(params)
return Promise.resolve({ permissions: {}, scope: 'turn' })
case 'item/tool/requestUserInput':
this.validateRunIds(params)
return Promise.resolve({ answers: {} })
case 'mcpServer/elicitation/request':
this.validateRunIds(params, true)
return Promise.resolve({ action: 'decline', content: null, _meta: null })

View File

@@ -0,0 +1,53 @@
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import {
LOADER_SMOKE_TEST_TIMEOUT_MS,
runLoaderSmoke,
} from '@deepseek-ai/dsh-loader-smoke'
const fixtureDir = fileURLToPath(new URL(
'../../../../examples/acp-agent/tests/fixtures/subagent/subagent-codex/',
import.meta.url,
))
const driver = join(fixtureDir, 'driver.ts')
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 () => {
const { stdout, stderr } = await runLoaderSmoke({
label: 'subagent-codex Loader composition',
tempDirPrefix: 'dsh-subagent-codex-loader-',
binScript: driver,
libBinScript: driver,
configPath,
tsconfigPath: repoTsconfig,
env: {
// Loading the optional package must not probe or start a Codex binary.
PATH: '',
},
})
expect(stderr).toBe('')
expect(JSON.parse(stdout)).toEqual({
providers: ['codex'],
provider: {
name: 'codex',
capabilities: {
outputSchema: false,
depthLimit: false,
toolFilter: false,
persona: false,
},
inheritsParentContext: false,
},
tool: {
name: 'subagent_codex',
parameterNames: ['description', 'prompt'],
required: ['description', 'prompt'],
},
starts: 0,
})
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
})

View File

@@ -172,8 +172,7 @@ describe('real @openai/codex 0.146.0 product', () => {
await expectQuiescent(harness.handles)
}, 20_000)
it('declines a real app-server command approval without executing the command', async () => {
const sentinel = 'REAL_CODEX_APPROVAL_DECLINED'
it('cancels a real app-server command approval without executing the command', async () => {
const { harness, fixture } = await realHarness([
{
kind: 'functionCall',
@@ -184,7 +183,6 @@ describe('real @openai/codex 0.146.0 product', () => {
justification: 'exercise the unattended approval boundary',
},
},
{ kind: 'complete', text: sentinel },
])
const sideEffect = join(harness.workspace, 'approval-side-effect')
const run = await harness.ctx.subagents.start('codex', {
@@ -193,20 +191,17 @@ describe('real @openai/codex 0.146.0 product', () => {
signal: new AbortController().signal,
})
await expect(run.result).resolves.toEqual({
output: [{ type: 'text', text: sentinel }],
stopReason: 'completed',
output: [],
stopReason: 'error',
})
await run.dispose()
expect(existsSync(sideEffect)).toBe(false)
expect(fixture.requests).toHaveLength(2)
expect(fixture.requests).toHaveLength(1)
const tools = fixture.requests[0]!.body.tools as Array<Record<string, unknown>>
expect(tools).toEqual(expect.arrayContaining([
expect.objectContaining({ type: 'function', name: 'exec_command' }),
]))
const followup = JSON.stringify(fixture.requests[1]!.body)
expect(followup).toContain('call_fixture')
expect(followup).toContain('rejected by user')
expect(fixture.requests.every(requestEntry =>
requestEntry.headers.authorization === 'Bearer dsh-fake-openai-key',
)).toBe(true)

View File

@@ -417,6 +417,25 @@ describe('CodexAppServerWire', () => {
wire.close()
})
it('maps only an explicit context-window failure to max-tokens', async () => {
const { child, wire } = await initializeWire()
const result = wire.runTurn(['task'], new AbortController().signal, () => false)
const turnStart = await child.peer.nextMethod('turn/start')
child.peer.respond(turnStart, { turn: { id: 'turn-1' } })
child.peer.send(
agentMessage('partial answer', null),
turnCompleted('failed', 'turn-1', 'thread-1', {
message: 'too much context',
codexErrorInfo: 'contextWindowExceeded',
}),
)
await expect(result).resolves.toEqual({
output: [{ type: 'text', text: 'partial answer' }],
stopReason: 'max-tokens',
})
wire.close()
})
it('rejects invalid handshake, thread, and turn response shapes', async () => {
{
const child = fakeChild()
@@ -516,7 +535,7 @@ describe('CodexAppServerWire', () => {
wire.close()
})
it('answers all four unattended request classes without granting authority', async () => {
it('answers all five unattended request classes without granting authority', async () => {
const { child, wire } = await initializeWire()
const result = wire.runTurn(['task'], new AbortController().signal, () => false)
const turnStart = await child.peer.nextMethod('turn/start')
@@ -524,10 +543,14 @@ describe('CodexAppServerWire', () => {
child.peer.send({
id: 'command',
method: 'item/commandExecution/requestApproval',
params: { threadId: 'thread-1', turnId: 'turn-1' },
params: {
threadId: 'thread-1',
turnId: 'turn-1',
availableDecisions: ['decline', 'cancel'],
},
})
expect(await child.peer.nextResponse('command')).toMatchObject({
result: { decision: 'decline' },
result: { decision: 'cancel' },
})
child.peer.respond(turnStart, { turn: { id: 'turn-1' } })
@@ -536,6 +559,16 @@ describe('CodexAppServerWire', () => {
{
id: 'file',
method: 'item/fileChange/requestApproval',
params: {
threadId: 'thread-1',
turnId: 'turn-1',
availableDecisions: ['decline'],
},
result: { decision: 'decline' },
},
{
id: 'file-default',
method: 'item/fileChange/requestApproval',
params: { threadId: 'thread-1', turnId: 'turn-1' },
result: { decision: 'decline' },
},
@@ -545,6 +578,12 @@ describe('CodexAppServerWire', () => {
params: { threadId: 'thread-1', turnId: 'turn-1' },
result: { permissions: {}, scope: 'turn' },
},
{
id: 'user-input',
method: 'item/tool/requestUserInput',
params: { threadId: 'thread-1', turnId: 'turn-1', questions: [] },
result: { answers: {} },
},
{
id: 'mcp',
method: 'mcpServer/elicitation/request',
@@ -568,9 +607,27 @@ describe('CodexAppServerWire', () => {
for (const serverRequest of [
{
id: 'unknown',
method: 'item/tool/requestUserInput',
method: 'future/request',
params: { threadId: 'thread-1', turnId: 'turn-1' },
},
{
id: 'approval',
method: 'item/commandExecution/requestApproval',
params: {
threadId: 'thread-1',
turnId: 'turn-1',
availableDecisions: ['accept'],
},
},
{
id: 'malformed-approval',
method: 'item/fileChange/requestApproval',
params: {
threadId: 'thread-1',
turnId: 'turn-1',
availableDecisions: 'decline',
},
},
{
id: 'thread',
method: 'item/fileChange/requestApproval',

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/README.md
README.md: 4040f9a48bd61cc230adec1bd9725cf30bdfd8f7
README.zh.md: 5f6a041887e3227d92a88eac344524e55a598413
README.md: 4682b06ae105a0ae70ea7e78a80776ac18d817e7
README.zh.md: c39afb26d8c6baf4774ae3b4a8f8151529a29e15

View File

@@ -15,6 +15,7 @@ The family separates the stable interface from implementations and model-facing
| `@deepseek-ai/dsh-subagent-fork` | In-process child seeded with completed parent turns; supports continuable children. |
| `@deepseek-ai/dsh-subagent-acp` | Fresh out-of-process ACP child (one-shot). |
| `@deepseek-ai/dsh-subagent-codex` | Fresh real Codex app-server child with one ephemeral thread and turn (one-shot). |
| `@deepseek-ai/dsh-subagent-dsh-sdk` | Fresh out-of-process harness child driven through the TypeScript SDK client (one-shot). |
| `@deepseek-ai/dsh-tool-subagent` | Model-facing delegation tool over one configured provider. |
| `@deepseek-ai/dsh-tool-subagent-control` | The globally named `send_message` follow-up tool. |
| `@deepseek-ai/dsh-tool-subagent-report` | Child-scoped return channel to the direct parent. |

View File

@@ -15,6 +15,7 @@ subagent seam 允许一个 agent智能体通过具名提供方把工作委
| `@deepseek-ai/dsh-subagent-fork` | 以父 agent 已完成轮次作为初始内容的进程内子 agent支持可继续子 agent。 |
| `@deepseek-ai/dsh-subagent-acp` | 全新的进程外 ACPAgent Client Protocol子 agent一次性。 |
| `@deepseek-ai/dsh-subagent-codex` | 全新的真实 Codex app-server 子 agent包含一个临时 thread 和一个轮次(一次性)。 |
| `@deepseek-ai/dsh-subagent-dsh-sdk` | 通过 TypeScript SDK 客户端驱动的全新进程外 harness 子 agent一次性。 |
| `@deepseek-ai/dsh-tool-subagent` | 基于一个已配置提供方、面向模型的委派工具。 |
| `@deepseek-ai/dsh-tool-subagent-control` | 全局具名 `send_message` 后续操作工具。 |
| `@deepseek-ai/dsh-tool-subagent-report` | 子级作用域的返回通道,指向直接父级。 |

6
pnpm-lock.yaml generated
View File

@@ -724,9 +724,6 @@ importers:
'@deepseek-ai/dsh-subagent-spawn':
specifier: workspace:*
version: link:../packages/subagent/subagent-spawn
'@deepseek-ai/dsh-subprocess':
specifier: workspace:*
version: link:../packages/subprocess/subprocess
'@deepseek-ai/dsh-subprocess-local':
specifier: workspace:*
version: link:../packages/subprocess/subprocess-local
@@ -4969,6 +4966,9 @@ importers:
'@deepseek-ai/dsh-llm':
specifier: workspace:^
version: link:../../llm/llm
'@deepseek-ai/dsh-loader-smoke':
specifier: workspace:^
version: link:../../support/loader-smoke
'@deepseek-ai/dsh-sdk-protocol':
specifier: workspace:^
version: link:../../sdk/sdk-protocol

View File

@@ -320,8 +320,8 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Subprocess seam',
mode: 'seam',
implementations: ['subprocess-local'],
consumers: ['bash-local', 'bash-sandbox', 'lsp-local', 'subagent-acp', 'subagent-codex'],
note: 'The bash executors, the LSP host, and the out-of-process ACP and Codex subagent backends spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation.',
consumers: ['bash-local', 'bash-sandbox', 'lsp-local', 'subagent-acp', 'subagent-codex', 'subagent-dsh-sdk'],
note: 'The bash executors, the LSP host, and the out-of-process ACP, Codex, and DSH SDK subagent backends spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation.',
},
{
key: 'bash',
@@ -416,7 +416,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'subagent',
title: 'Subagent provider and continuation service',
mode: 'seam',
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp', 'subagent-codex'],
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp', 'subagent-codex', 'subagent-dsh-sdk'],
consumers: ['tool-subagent', 'tool-subagent-control', 'tool-ralph'],
note: 'Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route.',
},