fix(subagent): finalize Codex provider composition
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 .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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 状态。
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 + 工具 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 Note(agent 决策记录)](../../.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 Note(agent 决策记录)](../../.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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
51
examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts
vendored
Normal file
51
examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts
vendored
Normal 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()
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"}}}
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -15,6 +15,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
||||
| `@deepseek-ai/dsh-subagent-fork` | 以父 agent 已完成轮次作为初始内容的进程内子 agent;支持可继续子 agent。 |
|
||||
| `@deepseek-ai/dsh-subagent-acp` | 全新的进程外 ACP(Agent 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
6
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user