diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml index f90230b6a1..155fd25736 100644 --- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml @@ -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/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md -2026-08-04-claude-code-and-codex-subagent-backends.md: 923ac10573ee34746ff26d97b650edf4cddcaa9d -2026-08-04-claude-code-and-codex-subagent-backends.zh.md: c1368296f2f9c3887db74f16516fad6d0de8d620 +2026-08-04-claude-code-and-codex-subagent-backends.md: 27e805b142ed3541d5157e2440cb0a0d57ae461b +2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 633a0de7653b8e985b5bd6dcf5b9424d1764bf43 diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md index 923ac10573..27e805b142 100644 --- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md +++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md @@ -8,7 +8,7 @@ English | [中文](2026-08-04-claude-code-and-codex-subagent-backends.zh.md) The named [`ctx.subagents`](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. Each route must hand the 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. Required keyless evidence therefore separates two facts: a real-product test proves the official integration, 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 prove the former; a hand-mounted plugin cannot prove the latter. +The product integrations must not become second owners for task text, cwd, cancellation, result settlement, or process trees. Required evidence therefore separates three facts: a keyless real-product test proves the official integration, native authentication shape, deterministic answer, and teardown; a Loader composition test proves that the public package and documented tool configuration load without starting the product; and a credentialed e2e proves that the production provider and real product can obtain a unique answer from the real DeepSeek service. Direct model HTTP or a product double cannot replace either product-running tier, and a hand-mounted plugin cannot replace the Loader tier. ## Decision @@ -43,6 +43,8 @@ For command and file approvals, the unattended wire selects a non-approval decis 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. +Codex 0.146.0 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively. + ## Claude Code provider `@deepseek-ai/dsh-subagent-claude-code` registers the fixed `claude-code` provider and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. The SDK's platform `optionalDependency` supplies the real Claude Code 2.1.220 CLI. The provider uses the official `query()` entrypoint and passes the SDK's `spawnClaudeCodeProcess` command, arguments, cwd, environment, and forwarded signal unchanged to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires. @@ -53,14 +55,20 @@ The provider publishes only after both the SDK `Query` and a live managed CLI ha Startup rollback and published disposal close the SDK query, abort the per-run controller, invoke shared process-tree termination, and wait for whole-tree exit. `Query.close()` expresses graceful protocol intent but does not replace the subprocess owner's exit proof. Query-close failure, process failure, and teardown failure remain independently observable. +The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract directly: the runtime-only DeepSeek key becomes `ANTHROPIC_AUTH_TOKEN`, the fixed official base gains `/anthropic`, and the main and subagent model variables select the documented DeepSeek models. It starts the production provider and real SDK/CLI, requires one random nonce as the complete answer, persists no credential in settings, and waits for every managed handle to exit. + ## Distribution and evidence -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 Loader tier boots the README-shaped user configuration, verifies both fixed foreground-only tools in one context, and starts neither product process. +Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless 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 Loader tier boots the README-shaped user configuration, verifies both fixed foreground-only tools in one context, and starts neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret. 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. Production still supplies `codex` on `PATH`. +The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit. + The Claude Code evidence pins Agent SDK 0.3.220 and its platform-distributed Claude Code 2.1.220 CLI. Its real-product spec observes the exact `x-api-key`, original task, byte-exact final answer, inherited temporary host-setting marker, process failure, local cancellation, and whole-tree exit. The Loader e2e resolves both product packages by name while neither product command is available and records zero child starts. +The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test. + The project owner's distribution authorization is scoped to the official `@anthropic-ai/claude-agent-sdk` identity and the official Claude Code CLI/platform payloads each SDK version declares through `optionalDependencies`. [`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) derives and discloses the current payload set without reclassifying its declared terms as permissive. Version, license-field, and payload-set changes still undergo ordinary dependency, lockfile, compatibility, terms, and notices review; unrelated non-permissive runtime packages continue to fail closed. ## Alternatives considered @@ -81,6 +89,6 @@ The project owner's distribution authorization is scoped to the official `@anthr Users can install either or both product providers, bind stable foreground tools in their own Cordis configuration, and delegate one self-contained task through the existing subagent contract. Official product integrations preserve native settings and behavior while shared services retain the sole ownership of task settlement and process-tree quiescence. -Every delegation pays for a fresh product process and independent model context, and only final text reaches the parent. Product-native configuration makes behavior depend on the deployment's installed product, account state, and workspace settings. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout. +Every delegation pays for a fresh product process and independent model context, and only final text reaches the parent. Product-native configuration makes behavior depend on the deployment's installed product, account state, and workspace settings. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout. -Compatibility is pinned by package-level unit coverage, real-product loopback tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product baseline change must refresh those facts; production performs no separate runtime version probe. +Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe. diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md index c1368296f2..633a0de765 100644 --- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md +++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md @@ -8,7 +8,7 @@ Status: implemented 命名的 [`ctx.subagents`](2026-06-21-subagent-capability-seam.md) 注册表让父 agent(智能体)无需了解子级的运行方式即可委派工作,但 harness 需要通往真实 Codex 与 Claude Code 产品的第一方路径。每条路径都必须向产品交付一项自包含任务,让它在父会话的工作区中执行,返回最终回答或明确的失败或取消结果,并且不留下任何受管的产品进程。 -产品集成不得成为任务文本、cwd、取消、结果结算或进程树的第二责任方。因此,强制性的无密钥证据要区分两个事实:真实产品测试证明官方集成、原生身份验证形态、最终回答与资源清理;Loader 组合测试证明公开包(package)和文档所示的工具配置无需启动产品即可加载。直接发起模型 HTTP 请求或使用产品替身无法证明前者;手工挂载插件无法证明后者。 +产品集成不得成为任务文本、cwd、取消、结果结算或进程树的第二责任方。因此,所需证据要区分三个事实:无密钥真实产品测试证明官方集成、原生身份验证形态、确定性答案与资源清理;Loader 组合测试证明公开包(package)和文档所示的工具配置无需启动产品即可加载;带密钥 e2e 证明生产提供方与真实产品能够从真实 DeepSeek 服务取得唯一答案。直接发起模型 HTTP 请求或使用产品替身无法取代上述任一产品运行层级;手工挂载插件无法取代 Loader 层级。 ## 决策 @@ -43,6 +43,8 @@ fixed tool → shared subagent service → product provider → official product 若启动在发布前失败,提供方会关闭协议连接、终止已获取的进程树并等待其退出,然后拒绝 `start()`。对已发布的运行执行资源释放时,提供方会尽力中断已知轮次、关闭协议连接、结束标准输入、调用共享的逐级终止机制,并等待整棵进程树退出。结果失败与清理失败仍可彼此独立地观察。 +Codex 0.146.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSE(Server-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。 + ## Claude Code 提供方 `@deepseek-ai/dsh-subagent-claude-code` 注册固定的 `claude-code` 提供方,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。SDK 的平台 `optionalDependency` 提供真实的 Claude Code 2.1.220 CLI。提供方使用官方 `query()` 入口点,并将 SDK 的 `spawnClaudeCodeProcess` 命令、参数、cwd、环境和转发的信号原样传入 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。 @@ -53,14 +55,20 @@ fixed tool → shared subagent service → product provider → official product 启动回滚和已发布运行的资源释放都会关闭 SDK query、中止该次运行的控制器、调用共享的进程树终止机制,并等待整棵进程树退出。`Query.close()` 表达优雅的协议关闭意图,但不能取代子进程责任方的退出证明。Query 关闭失败、进程失败和清理失败仍可彼此独立地观察。 +带密钥 Claude Code e2e 直接使用官方 DeepSeek Claude Code 契约:仅在运行时提供的 DeepSeek 密钥会映射为 `ANTHROPIC_AUTH_TOKEN`,固定的官方基础 URL 会追加 `/anthropic`,主模型与 subagent 模型变量会选择文档所示的 DeepSeek 模型。该测试会启动生产提供方与真实 SDK 和 CLI,要求一个随机数作为完整答案,不会把任何凭据持久化到设置中,并等待所有受管句柄退出。 + ## 分发与证据 -每个产品都负责覆盖所有分支的包测试、一项必跑的真实产品测试和一项 Loader 组合 e2e。真实产品测试层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Loader 层级会启动 README 所示形态的用户配置,在同一个上下文中验证两个固定且只支持前台执行的工具,并且不会启动任何产品进程。 +每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Loader 层级会启动 README 所示形态的用户配置,在同一个上下文中验证两个固定且只支持前台执行的工具,并且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。 Codex 证据锁定 `@openai/codex@0.146.0` 与 `codex-cli 0.146.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、不会产生文件副作用的无人值守命令拒绝、本地取消以及整棵进程树退出。生产环境仍提供 `codex`,并通过 `PATH` 解析。 +带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。 + Claude Code 证据锁定 Agent SDK 0.3.220 及其平台分发的 Claude Code 2.1.220 CLI。其真实产品测试会观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消以及整棵进程树退出。Loader e2e 会在两个产品命令均不可用时按名称解析两个产品包,并记录零次子级启动。 +带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。 + 项目所有者的分发授权范围限定为官方 `@anthropic-ai/claude-agent-sdk` 身份,以及每个 SDK 版本通过 `optionalDependencies` 声明的官方 Claude Code CLI 与平台载荷。[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) 会推导并披露当前载荷集合,但不会将其声明条款重新归类为宽松条款。版本、许可证字段和载荷集合发生变化时,仍须经过常规的依赖、锁文件、兼容性、条款和声明评审;无关的非宽松运行时包继续以默认拒绝方式失败。 ## 曾考虑的替代方案 @@ -81,6 +89,6 @@ Claude Code 证据锁定 Agent SDK 0.3.220 及其平台分发的 Claude Code 2.1 用户可以安装任一或两个产品提供方,在自己的 Cordis 配置中绑定稳定的前台工具,并通过现有 subagent 契约委派一项自包含任务。官方产品集成会保留原生设置与行为,而共享服务继续独占任务结算与进程树完全停稳的责任。 -每次委派都要承担新建产品进程和独立模型上下文的开销,且只有最终文本会到达父级。产品原生配置使行为取决于部署环境中安装的产品、账户状态和工作区设置。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。 +每次委派都要承担新建产品进程和独立模型上下文的开销,且只有最终文本会到达父级。产品原生配置使行为取决于部署环境中安装的产品、账户状态和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。 -兼容性由包级单元测试覆盖率、真实产品回环测试、公开 Loader 组合、已构建包与 NodeNext 消费方检查、生成的文档与声明以及仓库 CI 矩阵共同锁定。更改受支持的产品基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。 +兼容性由包级单元测试覆盖率、无密钥真实产品回环测试、带密钥 DeepSeek 随机数测试、公开 Loader 组合、已构建包与 NodeNext 消费方检查、生成的文档与声明以及仓库 CI 矩阵共同锁定。更改受支持的产品基线或 DeepSeek 端点/模型基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。 diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml index 63570333f1..0b6f38373b 100644 --- a/packages/subagent/subagent-claude-code/README.i18n.yaml +++ b/packages/subagent/subagent-claude-code/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md -README.md: e19b119e355953a388ec4fca6a2db511e7eb43da -README.zh.md: 6d3b8719329691681582abd91b82abd167e89f6d +README.md: 59d7dcd8406cb244854bbb704f644af1a6aa8589 +README.zh.md: 90ea0a316b91da3249b934984a3f512b71b8b3b2 diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md index e19b119e35..59d7dcd840 100644 --- a/packages/subagent/subagent-claude-code/README.md +++ b/packages/subagent/subagent-claude-code/README.md @@ -51,7 +51,7 @@ Install this package and add the following rows to your own `cordis.yml`. Shippe ## Product compatibility and evidence -The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`, whose platform optional dependency supplies Claude Code 2.1.220. The real-product spec drives that official SDK and CLI against a loopback Anthropic Messages SSE service with a non-empty fake key. It proves the exact task, authentication, byte-exact answer, inherited host-setting marker, process failure, cancellation, and process-tree exit. A separate Loader composition e2e boots the README-shaped user configuration alongside the Codex provider with neither product command available, verifies both fixed foreground-only tools, and records zero child starts. +The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`, whose platform optional dependency supplies Claude Code 2.1.220. The keyless real-product spec drives that official SDK and CLI against a loopback Anthropic Messages SSE service with a non-empty fake key. It proves the exact task, authentication, byte-exact answer, inherited host-setting marker, process failure, cancellation, and process-tree exit. A separate Loader composition e2e boots the README-shaped user configuration alongside the Codex provider with neither product command available, verifies both fixed foreground-only tools, and records zero child starts. A credentialed e2e maps a runtime-only DeepSeek key to the official Claude Code environment contract, starts the production provider and real SDK/CLI against the fixed official `/anthropic` endpoint, requires a unique nonce, and proves process-tree exit without persisting the key or calling the Messages API directly from the test. The project owner's identity-scoped distribution authorization covers the official SDK and the official CLI/platform payloads declared by each SDK version. [`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) discloses the current optional payload closure without classifying its declared terms as permissive; unrelated non-permissive runtime dependencies continue to fail the notices gate. diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md index 6d3b871932..90ea0a316b 100644 --- a/packages/subagent/subagent-claude-code/README.zh.md +++ b/packages/subagent/subagent-claude-code/README.zh.md @@ -51,7 +51,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK ## 产品兼容性与证据 -运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`,其平台可选依赖提供 Claude Code 2.1.220。真实产品测试使用非空伪密钥,驱动该官方 SDK 与 CLI 连接回环 Anthropic Messages SSE 服务,并证明原始任务、身份验证、逐字节一致的答案、继承的宿主设置标记、进程失败、取消与进程树退出。独立的 Loader 装配 e2e 会在两个产品命令均不可用时,与 Codex 提供方一起启动和 README 所示形态相同的用户配置,验证两个固定且只支持前台执行的工具,并记录零次子级启动。 +运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`,其平台可选依赖提供 Claude Code 2.1.220。无密钥真实产品测试使用非空伪密钥,驱动该官方 SDK 与 CLI 连接回环 Anthropic Messages SSE 服务,并证明原始任务、身份验证、逐字节一致的答案、继承的宿主设置标记、进程失败、取消与进程树退出。独立的 Loader 装配 e2e 会在两个产品命令均不可用时,与 Codex 提供方一起启动和 README 所示形态相同的用户配置,验证两个固定且只支持前台执行的工具,并记录零次子级启动。带密钥 e2e 会将仅在运行时提供的 DeepSeek 密钥映射到 Claude Code 官方环境契约,启动生产提供方与真实 SDK 和 CLI 并连接固定的官方 `/anthropic` 端点,要求获得一个唯一随机数,并在不持久化密钥且不由测试直接调用 Messages API 的情况下证明进程树退出。 项目所有者按身份范围授权分发官方 SDK 及每个 SDK 版本声明的官方 CLI/平台载荷。[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) 会披露当前可选载荷闭包,但不会把其声明条款归类为宽松许可证;其他无关的非宽松运行时依赖仍会使第三方声明门禁失败。 diff --git a/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts b/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts new file mode 100644 index 0000000000..806181ad13 --- /dev/null +++ b/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts @@ -0,0 +1,160 @@ +import { execFile } from 'node:child_process' +import { randomUUID } from 'node:crypto' +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' +import { Context } from 'cordis' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { Agent } from '@deepseek-ai/dsh-agent' +import SubagentService from '@deepseek-ai/dsh-subagent' +import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess' +import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local' +import * as claudeCode from '../src/index.ts' + +const execFileAsync = promisify(execFile) +const OFFICIAL_DEEPSEEK_BASE_URL = 'https://api.deepseek.com' +const sdkRoot = dirname(fileURLToPath( + import.meta.resolve('@anthropic-ai/claude-agent-sdk'), +)) +const sdkPackage = JSON.parse(readFileSync( + join(sdkRoot, 'package.json'), + 'utf8', +)) as { + version: string + claudeCodeVersion: string + optionalDependencies: Record +} +const platformPackage = `@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}` +const platformRoot = resolve(sdkRoot, '..', platformPackage.split('/')[1]!) +const claudeBin = join( + platformRoot, + process.platform === 'win32' ? 'claude.exe' : 'claude', +) + +const roots: string[] = [] +const contexts: Context[] = [] + +afterEach(async () => { + await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +function deepSeekBaseUrl(): string { + const configured = (process.env.DEEPSEEK_BASE_URL ?? OFFICIAL_DEEPSEEK_BASE_URL) + .replace(/\/+$/, '') + if (configured !== OFFICIAL_DEEPSEEK_BASE_URL) { + throw new Error('Claude Code DeepSeek e2e requires the official DeepSeek base URL') + } + return configured +} + +async function expectQuiescent(handles: readonly SubprocessHandle[]): Promise { + expect(handles.length).toBeGreaterThan(0) + for (const handle of handles) { + await expect(handle.waitForExit()).resolves.toBe(true) + await expect(handle.done).resolves.toHaveProperty('exitCode') + } +} + +describe.skipIf(!process.env.DEEPSEEK_API_KEY)( + 'Claude Code provider with real DeepSeek API', + () => { + it('returns one unique nonce through the production provider and real SDK/CLI', async () => { + const apiKey = process.env.DEEPSEEK_API_KEY + if (apiKey === undefined) throw new Error('e2e ran without DEEPSEEK_API_KEY') + const root = mkdtempSync(join(tmpdir(), 'dsh-claude-deepseek-e2e-')) + roots.push(root) + const workspace = join(root, 'workspace') + const claudeConfig = join(root, 'claude-config') + const xdgConfig = join(root, 'xdg-config') + const xdgCache = join(root, 'xdg-cache') + const xdgData = join(root, 'xdg-data') + const xdgState = join(root, 'xdg-state') + for (const directory of [ + workspace, + claudeConfig, + xdgConfig, + xdgCache, + xdgData, + xdgState, + ]) mkdirSync(directory) + + const env = { + ANTHROPIC_AUTH_TOKEN: apiKey, + ANTHROPIC_BASE_URL: `${deepSeekBaseUrl()}/anthropic`, + ANTHROPIC_MODEL: 'deepseek-v4-pro[1m]', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'deepseek-v4-pro[1m]', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'deepseek-v4-pro[1m]', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'deepseek-v4-flash', + CLAUDE_CODE_SUBAGENT_MODEL: 'deepseek-v4-flash', + CLAUDE_CODE_EFFORT_LEVEL: 'max', + CLAUDE_CONFIG_DIR: claudeConfig, + HOME: root, + XDG_CONFIG_HOME: xdgConfig, + XDG_CACHE_HOME: xdgCache, + XDG_DATA_HOME: xdgData, + XDG_STATE_HOME: xdgState, + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', + CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL: '1', + DISABLE_TELEMETRY: '1', + DISABLE_ERROR_REPORTING: '1', + HTTP_PROXY: '', + HTTPS_PROXY: '', + ALL_PROXY: '', + NO_PROXY: '127.0.0.1,localhost', + } + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SubagentService) + await ctx.plugin(LocalSubprocessService) + const handles: SubprocessHandle[] = [] + const spawn = ctx.subprocess.spawn.bind(ctx.subprocess) + vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => { + const handle = spawn(spec) + handles.push(handle) + return handle + }) + await ctx.plugin(claudeCode, { env, disposeGraceMs: 3_000 }) + + expect(sdkPackage.version).toBe('0.3.220') + expect(sdkPackage.claudeCodeVersion).toBe('2.1.220') + expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.220') + const version = await execFileAsync(claudeBin, ['--version'], { + env: { ...process.env, ...env }, + }) + expect(version.stdout.trim()).toBe('2.1.220 (Claude Code)') + + const nonce = `DSH_CLAUDE_DEEPSEEK_${randomUUID()}` + const parent = { + id: 'deepseek-e2e-parent', + session: { header: { cwd: workspace } }, + } as unknown as Agent + const run = await ctx.subagents.start('claude-code', { + prompt: [{ + type: 'text', + text: `Reply with exactly ${nonce} and nothing else. Do not use tools.`, + }], + parent, + signal: new AbortController().signal, + }) + const result = await run.result + await run.dispose() + + expect(result.stopReason).toBe('completed') + const text = result.output + .filter(block => block.type === 'text') + .map(block => block.text) + .join('') + .trim() + expect(text).toBe(nonce) + await expectQuiescent(handles) + }, 180_000) + }, +) diff --git a/packages/subagent/subagent-codex/README.i18n.yaml b/packages/subagent/subagent-codex/README.i18n.yaml index c3d4da77bf..a40167b79a 100644 --- a/packages/subagent/subagent-codex/README.i18n.yaml +++ b/packages/subagent/subagent-codex/README.i18n.yaml @@ -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: d7293a0ef37e4ec0f0cf983c254f9e22f830fcd8 -README.zh.md: 110953312162e146f01ef037a40d2f70b136850c +README.md: 1dde57e10e27786ae06d395c7088976bf6f60ece +README.zh.md: cb56281d9018e7a400ceb770e31da8a60d10d54f diff --git a/packages/subagent/subagent-codex/README.md b/packages/subagent/subagent-codex/README.md index d7293a0ef3..1dde57e10e 100644 --- a/packages/subagent/subagent-codex/README.md +++ b/packages/subagent/subagent-codex/README.md @@ -47,7 +47,7 @@ Install this package and add the following rows to your own `cordis.yml`. Shippe ## 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`: 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`. +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 keyless 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. A credentialed e2e starts the production provider and real Codex, then obtains a unique answer from the fixed official DeepSeek service through a loopback-only test bridge from Responses to Chat Completions; that bridge is not production functionality or native Codex support for DeepSeek's Chat Completions API. The npm package is a test-only dependency; deployments still supply `codex` on `PATH`. ## Model Experience @@ -83,7 +83,7 @@ Append-only: the new tool result follows the reusable parent request prefix. - **One fresh process, thread, and turn per run** — there is no continuation, resume, pooling, progress stream, or product-session persistence. - **Host-managed product installation and account state** — a missing or incompatible `codex`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer, login flow, or runtime version gate. -- **Compatibility is pinned by development evidence** — upgrading from the verified 0.146.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, and real-product tests. +- **Compatibility is pinned by development evidence** — upgrading from the verified 0.146.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests. - **No human approval path** — known unattended approval requests are denied and unknown server requests fail closed; deployments cannot configure an allow policy through this package. - **Final text only** — reasoning, commentary, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local. - **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider. diff --git a/packages/subagent/subagent-codex/README.zh.md b/packages/subagent/subagent-codex/README.zh.md index 1109533121..cb56281d90 100644 --- a/packages/subagent/subagent-codex/README.zh.md +++ b/packages/subagent/subagent-codex/README.zh.md @@ -47,7 +47,7 @@ ## 产品兼容性与证据 -生产环境的协议层有意只实现这一单次执行契约所需的 app-server 方法。开发证据锁定在 `@openai/codex@0.146.0` / `codex-cli 0.146.0`:真实产品测试使用非空的伪密钥,驱动官方二进制程序连接回环 Responses 服务,并证明任务、身份验证、精确回答、取消、审批与进程树退出。独立的 Loader 装配 e2e 会在没有可用 `codex` 命令时启动与 README 同形的用户配置,验证固定提供方与只支持前台执行的工具 schema,并记录零次子级启动。该 NPM 包仅作为测试依赖;部署环境仍需通过 `PATH` 提供 `codex`。 +生产环境的协议层有意只实现这一单次执行契约所需的 app-server 方法。开发证据锁定在 `@openai/codex@0.146.0` / `codex-cli 0.146.0`:无密钥真实产品测试使用非空的伪密钥,驱动官方二进制程序连接回环 Responses 服务,并证明任务、身份验证、精确回答、取消、审批与进程树退出。独立的 Loader 装配 e2e 会在没有可用 `codex` 命令时启动与 README 同形的用户配置,验证固定提供方与只支持前台执行的工具 schema,并记录零次子级启动。带密钥 e2e 会启动生产提供方和真实 Codex,再通过一个仅限回环、将 Responses 转为 Chat Completions 的测试桥接层,从固定的 DeepSeek 官方服务获得唯一答案;该桥接层既不属于生产功能,也不代表 Codex 原生支持 DeepSeek 的 Chat Completions API。该 NPM 包仅作为测试依赖;部署环境仍需通过 `PATH` 提供 `codex`。 ## 模型体验 @@ -83,7 +83,7 @@ Codex 子任务会在一个全新的临时线程中,以单个轮次接收这 - **每次运行均新建一个进程、一个线程和一个轮次**:不支持续接、恢复、池化、进度流或产品会话持久化。 - **产品安装和账户状态由宿主管理**:`codex` 缺失或不兼容、配置错误或身份验证失败,都会呈现为启动错误或运行错误;本插件不提供安装程序、登录流程或运行时版本门禁。 -- **兼容性由开发证据锁定**:若要从已验证的 0.146.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消和真实产品测试。 +- **兼容性由开发证据锁定**:若要从已验证的 0.146.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。 - **没有人工审批路径**:已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;部署方无法通过本包配置允许策略。 - **仅返回最终文本**:推理、过程说明、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部。 - **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。 diff --git a/packages/subagent/subagent-codex/tests/deepseek-responses-bridge.ts b/packages/subagent/subagent-codex/tests/deepseek-responses-bridge.ts new file mode 100644 index 0000000000..b59738031e --- /dev/null +++ b/packages/subagent/subagent-codex/tests/deepseek-responses-bridge.ts @@ -0,0 +1,190 @@ +import { createServer } from 'node:http' +import type { + IncomingMessage, + Server, + ServerResponse, +} from 'node:http' +import { completeResponsesEvents } from './responses-fixture.ts' + +const OFFICIAL_DEEPSEEK_BASE_URL = 'https://api.deepseek.com' +const MAX_REQUEST_BYTES = 1_048_576 + +/** One running test-only Responses-to-DeepSeek bridge. */ +export interface DeepSeekResponsesBridge { + readonly baseUrl: string + readonly completedRequests: number + close(): Promise +} + +function readRequest(request: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let body = '' + request.setEncoding('utf8') + request.on('data', (chunk: string) => { + body += chunk + if (Buffer.byteLength(body) > MAX_REQUEST_BYTES) { + request.destroy(new Error('DeepSeek bridge request exceeded its byte limit')) + } + }) + request.on('end', () => { resolve(body) }) + request.on('error', reject) + }) +} + +function responseInputTexts(body: Record): 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).content + if (!Array.isArray(content)) return [] + return content.flatMap((part): string[] => ( + part !== null + && typeof part === 'object' + && typeof (part as Record).text === 'string' + ? [(part as Record).text as string] + : [] + )) + }) +} + +function taskText(body: Record): string { + const input = responseInputTexts(body).join('\n') + if (input.trim().length > 0) return input + return typeof body.instructions === 'string' ? body.instructions : '' +} + +function deepSeekBaseUrl(): string { + const configured = (process.env.DEEPSEEK_BASE_URL ?? OFFICIAL_DEEPSEEK_BASE_URL) + .replace(/\/+$/, '') + if (configured !== OFFICIAL_DEEPSEEK_BASE_URL) { + throw new Error('Codex DeepSeek e2e requires the official DeepSeek base URL') + } + return configured +} + +async function completeWithDeepSeek( + authorization: string, + task: string, +): Promise { + const response = await fetch(`${deepSeekBaseUrl()}/chat/completions`, { + method: 'POST', + headers: { + authorization, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model: 'deepseek-v4-flash', + messages: [ + { + role: 'system', + content: 'Follow the user instruction and return only the requested nonce.', + }, + { role: 'user', content: task }, + ], + temperature: 0, + max_tokens: 64, + stream: false, + }), + }) + if (!response.ok) { + void response.body?.cancel() + throw new Error(`DeepSeek bridge upstream returned HTTP ${response.status}`) + } + const payload = await response.json() as { + choices?: Array<{ message?: { content?: unknown } }> + } + const content = payload.choices?.[0]?.message?.content + if (typeof content !== 'string' || content.trim().length === 0) { + throw new Error('DeepSeek bridge upstream returned no text') + } + return content +} + +function closeServer(server: Server): Promise { + return new Promise((resolve, reject) => { + server.close((error) => { + if (error !== undefined) reject(error) + else resolve() + }) + server.closeAllConnections() + }) +} + +/** + * Start the single-purpose loopback bridge used by the Codex credentialed e2e. + * @param nonce - unique answer the incoming Responses task must request. + * @returns loopback endpoint, completion count, and close operation. + */ +export async function startDeepSeekResponsesBridge( + nonce: string, +): Promise { + let seenRequests = 0 + let completedRequests = 0 + const openResponses = new Set() + const server = createServer((request, response) => { + openResponses.add(response) + response.on('close', () => { openResponses.delete(response) }) + void (async () => { + if (request.method !== 'POST' || request.url !== '/v1/responses') { + response.writeHead(404) + response.end() + return + } + if (seenRequests !== 0) { + response.writeHead(409) + response.end() + return + } + seenRequests += 1 + const authorization = request.headers.authorization + if ( + typeof authorization !== 'string' + || !authorization.startsWith('Bearer ') + || authorization.length === 'Bearer '.length + ) { + throw new Error('Codex DeepSeek bridge received no bearer credential') + } + const body = JSON.parse(await readRequest(request)) as Record + const task = taskText(body) + if (!task.includes(nonce)) { + throw new Error('Codex DeepSeek bridge request omitted the expected nonce') + } + const text = await completeWithDeepSeek(authorization, task) + completedRequests += 1 + response.writeHead(200, { + 'content-type': 'text/event-stream', + 'cache-control': 'no-cache', + connection: 'keep-alive', + 'x-request-id': 'req_deepseek_e2e', + }) + for (const event of completeResponsesEvents(text)) { + response.write(`data: ${JSON.stringify(event)}\n\n`) + } + response.end('data: [DONE]\n\n') + })().catch(() => { + if (!response.headersSent) { + response.writeHead(502, { 'content-type': 'application/json' }) + } + response.end(JSON.stringify({ error: { message: 'DeepSeek bridge request failed' } })) + }) + }) + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + server.off('error', reject) + resolve() + }) + }) + const address = server.address() + if (address === null || typeof address === 'string') { + throw new Error('DeepSeek bridge did not acquire a TCP port') + } + return { + baseUrl: `http://127.0.0.1:${address.port}/v1`, + get completedRequests(): number { return completedRequests }, + async close(): Promise { + for (const response of openResponses) response.destroy() + await closeServer(server) + }, + } +} diff --git a/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts b/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts new file mode 100644 index 0000000000..29c5536bc0 --- /dev/null +++ b/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts @@ -0,0 +1,141 @@ +import { execFile } from 'node:child_process' +import { randomUUID } from 'node:crypto' +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { delimiter, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' +import { Context } from 'cordis' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { Agent } from '@deepseek-ai/dsh-agent' +import SubagentService from '@deepseek-ai/dsh-subagent' +import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess' +import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local' +import * as codex from '../src/index.ts' +import { + startDeepSeekResponsesBridge, + type DeepSeekResponsesBridge, +} from './deepseek-responses-bridge.ts' + +const execFileAsync = promisify(execFile) +const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url))) +const codexBinDir = join(packageRoot, 'node_modules', '.bin') +const codexPackage = JSON.parse(readFileSync( + join(packageRoot, 'node_modules', '@openai', 'codex', 'package.json'), + 'utf8', +)) as { version: string } + +const roots: string[] = [] +const contexts: Context[] = [] +const bridges: DeepSeekResponsesBridge[] = [] + +afterEach(async () => { + await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) + await Promise.all(bridges.splice(0).map(bridge => bridge.close())) + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +async function expectQuiescent(handles: readonly SubprocessHandle[]): Promise { + expect(handles.length).toBeGreaterThan(0) + for (const handle of handles) { + await expect(handle.waitForExit()).resolves.toBe(true) + await expect(handle.done).resolves.toHaveProperty('exitCode') + } +} + +describe.skipIf(!process.env.DEEPSEEK_API_KEY)( + 'Codex provider with real DeepSeek API', + () => { + it('returns one unique nonce through the production provider and real Codex', async () => { + const apiKey = process.env.DEEPSEEK_API_KEY + if (apiKey === undefined) throw new Error('e2e ran without DEEPSEEK_API_KEY') + const root = mkdtempSync(join(tmpdir(), 'dsh-codex-deepseek-e2e-')) + roots.push(root) + const workspace = join(root, 'workspace') + const codexHome = join(root, 'codex-home') + mkdirSync(workspace) + mkdirSync(codexHome) + const nonce = `DSH_CODEX_DEEPSEEK_${randomUUID()}` + const bridge = await startDeepSeekResponsesBridge(nonce) + bridges.push(bridge) + writeFileSync(join(codexHome, 'config.toml'), [ + 'model = "deepseek-v4-flash"', + 'model_provider = "deepseek-e2e"', + 'approval_policy = "never"', + 'sandbox_mode = "read-only"', + 'disable_response_storage = true', + 'check_for_update_on_startup = false', + '', + '[model_providers.deepseek-e2e]', + 'name = "DeepSeek E2E bridge"', + `base_url = "${bridge.baseUrl}"`, + 'env_key = "DEEPSEEK_API_KEY"', + 'wire_api = "responses"', + 'requires_openai_auth = false', + '', + '[analytics]', + 'enabled = false', + '', + ].join('\n')) + const env = { + DEEPSEEK_API_KEY: apiKey, + CODEX_HOME: codexHome, + HOME: root, + XDG_CONFIG_HOME: join(root, 'xdg-config'), + PATH: `${codexBinDir}${delimiter}${process.env.PATH ?? ''}`, + HTTP_PROXY: '', + HTTPS_PROXY: '', + ALL_PROXY: '', + NO_PROXY: '127.0.0.1,localhost', + } + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SubagentService) + await ctx.plugin(LocalSubprocessService) + const handles: SubprocessHandle[] = [] + const spawn = ctx.subprocess.spawn.bind(ctx.subprocess) + vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => { + const handle = spawn(spec) + handles.push(handle) + return handle + }) + await ctx.plugin(codex, { env, disposeGraceMs: 2_000 }) + const version = await execFileAsync(join(codexBinDir, 'codex'), ['--version'], { + env: { ...process.env, ...env }, + }) + expect(codexPackage.version).toBe('0.146.0') + expect(version.stdout.trim()).toBe('codex-cli 0.146.0') + + const parent = { + id: 'deepseek-e2e-parent', + session: { header: { cwd: workspace } }, + } as unknown as Agent + const run = await ctx.subagents.start('codex', { + prompt: [{ + type: 'text', + text: `Reply with exactly ${nonce} and nothing else. Do not use tools.`, + }], + parent, + signal: new AbortController().signal, + }) + const result = await run.result + await run.dispose() + + expect(result.stopReason).toBe('completed') + const text = result.output + .filter(block => block.type === 'text') + .map(block => block.text) + .join('') + .trim() + expect(text).toBe(nonce) + expect(bridge.completedRequests).toBe(1) + await expectQuiescent(handles) + }, 180_000) + }, +) diff --git a/packages/subagent/subagent-codex/tests/responses-fixture.ts b/packages/subagent/subagent-codex/tests/responses-fixture.ts index 940b0d52a0..cac49b9158 100644 --- a/packages/subagent/subagent-codex/tests/responses-fixture.ts +++ b/packages/subagent/subagent-codex/tests/responses-fixture.ts @@ -85,7 +85,12 @@ function responseObject(text: string): Record { } } -function completeEvents(text: string): Record[] { +/** + * Build the minimal Responses SSE event sequence consumed by Codex 0.146.0. + * @param text - exact assistant answer. + * @returns ordered response lifecycle events. + */ +export function completeResponsesEvents(text: string): Record[] { const completed = responseObject(text) const message = (completed.output as Record[])[0]! const part = (message.content as Record[])[0]! @@ -250,7 +255,7 @@ export async function startResponsesFixture( }) if (behavior.kind === 'hold') return const events = behavior.kind === 'complete' - ? completeEvents(behavior.text) + ? completeResponsesEvents(behavior.text) : functionCallEvents(behavior.name, behavior.arguments) for (const event of events) { response.write(`data: ${JSON.stringify(event)}\n\n`)