@deepseek-ai/dsh-subagent-codex 0.0.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +91 -0
- package/README.zh.md +91 -0
- package/lib/index.js +513 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +29 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/run.d.ts +58 -0
- package/lib/types/wire.d.ts +75 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md
|
|
5
|
+
README.md: 3d59ca1eaf3db9dd9d9d2cd451692ebd2a956ef4
|
|
6
|
+
README.zh.md: b60cb1bba9b2d7b3f61c544c1600862a0ad6ce5b
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-subagent-codex
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
This package registers the fixed `codex` subagent provider. Each accepted run starts the official `codex app-server --stdio` command in the delegating Session's workspace, creates one ephemeral Codex thread, submits one self-contained text task, and returns only the final answer through the shared [`dsh-subagent`](../subagent/README.md) result contract.
|
|
6
|
+
|
|
7
|
+
## Start and ownership
|
|
8
|
+
|
|
9
|
+
`start(request)` accepts only a non-empty sequence of text blocks and derives the child cwd from the parent Session. It then spawns the fixed command through [`dsh-subprocess`](../../subprocess/subprocess/README.md), performs `initialize` → `initialized` → `thread/start { cwd, ephemeral: true }`, and publishes the run only after Codex returns a valid ephemeral thread. A failure or cancellation before publication closes the wire, terminates the managed process tree, waits for it to exit, and rejects `start()`.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
For command and file approvals, the unattended provider selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.147.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.
|
|
14
|
+
|
|
15
|
+
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 the provider 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.
|
|
16
|
+
|
|
17
|
+
## Capabilities and context
|
|
18
|
+
|
|
19
|
+
The provider advertises no optional start-time capabilities and reports `inheritsParentContext: false`. Codex receives the standalone text task and the parent Session cwd, but not the parent conversation, persona, tool filter, depth policy, or structured-output contract. The ephemeral Codex thread id and turn id stay private to this run and are never persisted in the parent Session.
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
| Key | Default | Meaning |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `env` | `{}` | Explicit child environment layered over the subprocess seam's credential-scrubbed parent environment. |
|
|
26
|
+
| `disposeGraceMs` | `3000` | Positive finite grace in milliseconds, no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), between the shared process-tree owner's termination tiers; disposal then waits for whole-tree exit. |
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
Shipped profiles load this provider once on the host and start no Codex process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. A custom host composition can still use both rows directly.
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
- id: subagent-codex
|
|
34
|
+
name: '@deepseek-ai/dsh-subagent-codex'
|
|
35
|
+
config:
|
|
36
|
+
env:
|
|
37
|
+
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
|
|
38
|
+
|
|
39
|
+
- id: tool-subagent-codex
|
|
40
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
41
|
+
disabled: true
|
|
42
|
+
config:
|
|
43
|
+
provider: codex
|
|
44
|
+
toolName: subagent_codex
|
|
45
|
+
enableRunInBackground: false
|
|
46
|
+
maxDepth: provider-managed
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Product compatibility and evidence
|
|
50
|
+
|
|
51
|
+
The production wire intentionally implements only the app-server methods required by this one-shot contract. Development evidence is pinned to `@openai/codex@0.147.0` / `codex-cli 0.147.0`; the npm package is a test-only dependency, and deployments still supply `codex` on `PATH`.
|
|
52
|
+
|
|
53
|
+
## Model Experience
|
|
54
|
+
|
|
55
|
+
### Child request
|
|
56
|
+
|
|
57
|
+
#### What the model sees
|
|
58
|
+
|
|
59
|
+
The Codex child receives the standalone text blocks as one turn in a fresh ephemeral thread. Its workspace is the parent Session cwd, and its model, system instructions, tools, sandbox, and authentication come from the native Codex installation and configuration.
|
|
60
|
+
|
|
61
|
+
#### Token effect
|
|
62
|
+
|
|
63
|
+
The child pays for an independent Codex context and turn. Child tokens do not enter the parent's context.
|
|
64
|
+
|
|
65
|
+
#### KV Cache effect
|
|
66
|
+
|
|
67
|
+
Independent of the parent request cache. Reuse depends only on Codex's own provider, model, instructions, tools, and ephemeral-thread request.
|
|
68
|
+
|
|
69
|
+
### Parent tool result, indirectly
|
|
70
|
+
|
|
71
|
+
#### What the model sees
|
|
72
|
+
|
|
73
|
+
Through `dsh-tool-subagent`, the parent sees only the selected final Codex answer or the consumer's exact error for a non-completed result. Codex commentary, reasoning, tool activity, stderr, workspace diffs, and product ids are not copied into the parent Session.
|
|
74
|
+
|
|
75
|
+
#### Token effect
|
|
76
|
+
|
|
77
|
+
Parent input grows only by the final answer or error retained in the tool result. This provider adds no parent tool schema by itself.
|
|
78
|
+
|
|
79
|
+
#### KV Cache effect
|
|
80
|
+
|
|
81
|
+
Append-only: the new tool result follows the reusable parent request prefix.
|
|
82
|
+
|
|
83
|
+
## Known Limitations and Deferred Work
|
|
84
|
+
|
|
85
|
+
- **One fresh process, thread, and turn per run** — there is no continuation, resume, pooling, progress stream, or product-session persistence.
|
|
86
|
+
- **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.
|
|
87
|
+
- **Compatibility is pinned by development evidence** — upgrading from the verified 0.147.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests.
|
|
88
|
+
- **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.
|
|
89
|
+
- **Final text only** — reasoning, commentary, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local.
|
|
90
|
+
- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.
|
|
91
|
+
- **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-subagent-codex
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
本包(package)注册固定的 `codex` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中启动官方 `codex app-server --stdio` 命令,创建一个临时 Codex 线程,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定仅返回最终答案。
|
|
6
|
+
|
|
7
|
+
## 启动与所有权
|
|
8
|
+
|
|
9
|
+
`start(request)` 只接受非空的文本块序列,并根据父会话确定子级 cwd。随后,它通过 [`dsh-subprocess`](../../subprocess/subprocess/README.md) spawn 固定命令,依次执行 `initialize` → `initialized` → `thread/start { cwd, ephemeral: true }`,且仅在 Codex 返回有效的临时线程后才发布此次运行。若在发布前发生失败或取消,它会关闭通信链路、终止受管进程树并等待其退出,然后拒绝 `start()` 调用。
|
|
10
|
+
|
|
11
|
+
已发布的 `run.result` 恰好启动一个轮次。它只接受与此次运行的线程和轮次匹配的通知,随后等待权威的终止通知 `turn/completed`。以最后一条 `phase: "final_answer"` 的 `agentMessage` 为准;若 Codex 没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退。过程说明绝不会取代上述任一答案;成功完成的轮次若没有非空白答案,结果也会判为错误。
|
|
12
|
+
|
|
13
|
+
对于命令与文件审批,无人值守的提供方会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.147.0 请求形态没有决策选项列表,因此回退到 `decline`。它对权限请求返回作用域限于当前轮次的空权限集,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败。
|
|
14
|
+
|
|
15
|
+
本地取消会在结果竞态中胜出并映射为 `aborted`。失败轮次的 `codexErrorInfo` 若为 `contextWindowExceeded`,则映射为 `max-tokens`;其他任何远端中断或失败轮次都映射为 `error`,且该提供方不会产生 `refusal`。`dispose()` 具有幂等性:如果当前的两个标识符均已知,它会尽力请求 `turn/interrupt`,关闭 JSON-RPC 通信链路,结束标准输入,调用共享的进程树逐级终止机制,并等待整棵进程树退出。结果失败与独立的清理失败仍彼此分离。
|
|
16
|
+
|
|
17
|
+
## 能力与上下文
|
|
18
|
+
|
|
19
|
+
本提供方不声明任何可选的启动时能力,并报告 `inheritsParentContext: false`。Codex 会接收独立文本任务和父会话 cwd,但不会接收父会话的对话、角色设定、工具筛选器、深度策略或结构化输出约定。临时 Codex 线程 ID 与轮次 ID 仅在此次运行内部可见,绝不会持久化到父会话。
|
|
20
|
+
|
|
21
|
+
## 配置
|
|
22
|
+
|
|
23
|
+
| 配置键 | 默认值 | 含义 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `env` | `{}` | 显式指定的子进程环境,叠加在由子进程 seam 清除凭证后的父环境之上。 |
|
|
26
|
+
| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md);随后资源释放会等待整棵进程树退出。 |
|
|
27
|
+
|
|
28
|
+
生产环境会从 `PATH` 中解析 `codex`,并使用宿主机原生的 Codex 配置与身份验证。本插件不安装 Codex、不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测版本。子进程 seam 会移除具有凭证特征的环境变量,因此供子进程使用的 API 密钥必须在 `env` 中显式提供;除非被覆盖,`PATH` 和 `HOME` 等普通环境变量值仍然可用。
|
|
29
|
+
|
|
30
|
+
随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Codex 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。自定义宿主组装仍可直接使用两条配置行。
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
- id: subagent-codex
|
|
34
|
+
name: '@deepseek-ai/dsh-subagent-codex'
|
|
35
|
+
config:
|
|
36
|
+
env:
|
|
37
|
+
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
|
|
38
|
+
|
|
39
|
+
- id: tool-subagent-codex
|
|
40
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
41
|
+
disabled: true
|
|
42
|
+
config:
|
|
43
|
+
provider: codex
|
|
44
|
+
toolName: subagent_codex
|
|
45
|
+
enableRunInBackground: false
|
|
46
|
+
maxDepth: provider-managed
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 产品兼容性与证据
|
|
50
|
+
|
|
51
|
+
生产环境的协议层有意只实现这一单次执行约定所需的 app-server 方法。开发证据锁定在 `@openai/codex@0.147.0` / `codex-cli 0.147.0`;该 NPM 包仅作为测试依赖,部署环境仍需通过 `PATH` 提供 `codex`。
|
|
52
|
+
|
|
53
|
+
## 模型体验
|
|
54
|
+
|
|
55
|
+
### 子任务请求
|
|
56
|
+
|
|
57
|
+
#### 模型看到的内容
|
|
58
|
+
|
|
59
|
+
Codex 子任务会在一个全新的临时线程中,以单个轮次接收这些独立文本块。它的工作区是父会话 cwd;其模型、系统指令、工具、沙箱和身份验证来自原生 Codex 安装与配置。
|
|
60
|
+
|
|
61
|
+
#### 对 token 的影响
|
|
62
|
+
|
|
63
|
+
子任务需为独立的 Codex 上下文和轮次承担 token 开销。子任务 token 不会进入父级上下文。
|
|
64
|
+
|
|
65
|
+
#### 对 KV Cache 的影响
|
|
66
|
+
|
|
67
|
+
这与父请求缓存相互独立。能否复用只取决于 Codex 自身的提供方、模型、指令、工具和临时线程请求。
|
|
68
|
+
|
|
69
|
+
### 父级工具结果(间接)
|
|
70
|
+
|
|
71
|
+
#### 模型看到的内容
|
|
72
|
+
|
|
73
|
+
通过 `dsh-tool-subagent`,父级模型只会看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。Codex 的过程说明、推理(reasoning)、工具活动、stderr、工作区差异和产品标识符均不会复制到父会话。
|
|
74
|
+
|
|
75
|
+
#### 对 token 的影响
|
|
76
|
+
|
|
77
|
+
父级输入只会增加工具结果中保留的最终答案或错误内容。本提供方自身不添加父级工具 schema。
|
|
78
|
+
|
|
79
|
+
#### 对 KV Cache 的影响
|
|
80
|
+
|
|
81
|
+
仅追加:新的工具结果接在可复用的父请求前缀之后。
|
|
82
|
+
|
|
83
|
+
## 已知限制与后续工作
|
|
84
|
+
|
|
85
|
+
- **每次运行均新建一个进程、一个线程和一个轮次**:不支持续接、恢复、池化、进度流或产品会话持久化。
|
|
86
|
+
- **产品安装和账户状态由宿主管理**:`codex` 缺失或不兼容、配置错误或身份验证失败,都会呈现为启动错误或运行错误;本插件不提供安装程序、登录流程或运行时版本门禁。
|
|
87
|
+
- **兼容性由开发证据锁定**:若要从已验证的 0.147.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。
|
|
88
|
+
- **没有人工审批路径**:已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;部署方无法通过本包配置允许策略。
|
|
89
|
+
- **仅返回最终文本**:推理、过程说明、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部。
|
|
90
|
+
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。
|
|
91
|
+
- **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
3
|
+
import { NO_START_CAPABILITIES, assertPositiveFinite, resolveChildCwd, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
6
|
+
import { JsonRpcLineTransport } from "@deepseek-ai/dsh-sdk-protocol";
|
|
7
|
+
//#region lib/types/wire.js
|
|
8
|
+
/**
|
|
9
|
+
* Minimal Codex app-server 0.147.0 protocol adapter. The shared JSON-RPC
|
|
10
|
+
* transport owns framing and request correlation; this module owns only the
|
|
11
|
+
* product methods, current thread/turn association, unattended approval
|
|
12
|
+
* responses, and terminal-answer selection.
|
|
13
|
+
*
|
|
14
|
+
* @module @deepseek-ai/dsh-subagent-codex/wire
|
|
15
|
+
*/
|
|
16
|
+
function object(value, label) {
|
|
17
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`subagent-codex: app-server returned invalid ${label}`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function string(value, label) {
|
|
21
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`subagent-codex: app-server returned invalid ${label}`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function unattendedDecision(params) {
|
|
25
|
+
const available = params.availableDecisions;
|
|
26
|
+
if (available === void 0 || available === null) return "decline";
|
|
27
|
+
if (Array.isArray(available)) {
|
|
28
|
+
if (available.includes("cancel")) return "cancel";
|
|
29
|
+
if (available.includes("decline")) return "decline";
|
|
30
|
+
}
|
|
31
|
+
throw new Error("subagent-codex: app-server offered no unattended approval decision");
|
|
32
|
+
}
|
|
33
|
+
function isContextWindowExceeded(turn) {
|
|
34
|
+
if (turn.status !== "failed") return false;
|
|
35
|
+
const error = turn.error;
|
|
36
|
+
return error !== null && typeof error === "object" && !Array.isArray(error) && error.codexErrorInfo === "contextWindowExceeded";
|
|
37
|
+
}
|
|
38
|
+
function thrown$1(value) {
|
|
39
|
+
/* v8 ignore next -- typed protocol and stream failures reject with Error. */
|
|
40
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
41
|
+
}
|
|
42
|
+
function abortError(signal) {
|
|
43
|
+
return signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error(`subagent-codex: app-server request aborted: ${String(signal.reason)}`);
|
|
44
|
+
}
|
|
45
|
+
async function raceAbort(pending, signal) {
|
|
46
|
+
if (signal.aborted) {
|
|
47
|
+
pending.catch(() => {});
|
|
48
|
+
throw abortError(signal);
|
|
49
|
+
}
|
|
50
|
+
let rejectAbort;
|
|
51
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
52
|
+
rejectAbort = reject;
|
|
53
|
+
});
|
|
54
|
+
const onAbort = () => {
|
|
55
|
+
rejectAbort(abortError(signal));
|
|
56
|
+
};
|
|
57
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
58
|
+
try {
|
|
59
|
+
return await Promise.race([pending, aborted]);
|
|
60
|
+
} finally {
|
|
61
|
+
signal.removeEventListener("abort", onAbort);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* One app-server connection and its single ephemeral thread/turn.
|
|
66
|
+
*
|
|
67
|
+
* The class deliberately exposes no generic request surface. Supporting
|
|
68
|
+
* another product method must first become part of the provider contract.
|
|
69
|
+
*/
|
|
70
|
+
var CodexAppServerWire = class {
|
|
71
|
+
input;
|
|
72
|
+
transport;
|
|
73
|
+
fatal = Promise.withResolvers();
|
|
74
|
+
threadId;
|
|
75
|
+
turnId;
|
|
76
|
+
pendingTurnId;
|
|
77
|
+
turnCompleted;
|
|
78
|
+
earlyTurnNotifications = [];
|
|
79
|
+
lastFinalAnswer;
|
|
80
|
+
lastUnphasedAnswer;
|
|
81
|
+
closed = false;
|
|
82
|
+
constructor(input, output) {
|
|
83
|
+
this.input = input;
|
|
84
|
+
this.transport = new JsonRpcLineTransport(input, output);
|
|
85
|
+
this.fatal.promise.catch(() => {});
|
|
86
|
+
this.transport.onRequest((method, params) => this.handleServerRequest(method, params));
|
|
87
|
+
this.transport.onNotification((method, params) => {
|
|
88
|
+
try {
|
|
89
|
+
this.handleNotification(method, params);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.fail(thrown$1(error));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
this.input.on("error", this.onInputError);
|
|
95
|
+
this.input.on("end", this.onInputEnd);
|
|
96
|
+
output.on("error", this.onOutputError);
|
|
97
|
+
}
|
|
98
|
+
/** Start reading app-server frames. */
|
|
99
|
+
start() {
|
|
100
|
+
this.transport.start();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Perform the required app-server initialize/initialized handshake.
|
|
104
|
+
* @param signal - unpublished-start cancellation.
|
|
105
|
+
*/
|
|
106
|
+
async initialize(signal) {
|
|
107
|
+
object(await this.guarded(this.transport.request("initialize", {
|
|
108
|
+
clientInfo: {
|
|
109
|
+
name: "deepseek-harness",
|
|
110
|
+
title: "DeepSeek Harness",
|
|
111
|
+
version: "0.0.1"
|
|
112
|
+
},
|
|
113
|
+
capabilities: {
|
|
114
|
+
experimentalApi: false,
|
|
115
|
+
requestAttestation: false
|
|
116
|
+
}
|
|
117
|
+
}, signal), signal), "initialize response");
|
|
118
|
+
this.transport.notify("initialized");
|
|
119
|
+
await this.guarded(this.transport.flush(), signal);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Create the run's private ephemeral thread and retain its identity.
|
|
123
|
+
* @param cwd - parent Session workspace.
|
|
124
|
+
* @param signal - unpublished-start cancellation.
|
|
125
|
+
*/
|
|
126
|
+
async startThread(cwd, signal) {
|
|
127
|
+
const thread = object(object(await this.guarded(this.transport.request("thread/start", {
|
|
128
|
+
cwd,
|
|
129
|
+
ephemeral: true
|
|
130
|
+
}, signal), signal), "thread/start response").thread, "thread/start thread");
|
|
131
|
+
const id = string(thread.id, "thread/start thread id");
|
|
132
|
+
if (thread.ephemeral !== true) throw new Error("subagent-codex: app-server did not create an ephemeral thread");
|
|
133
|
+
this.threadId = id;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Submit the one text-only task and wait for this thread/turn's authoritative
|
|
137
|
+
* terminal notification.
|
|
138
|
+
* @param texts - already validated task text blocks.
|
|
139
|
+
* @param signal - local cancellation for the published run.
|
|
140
|
+
* @returns the shared subagent result.
|
|
141
|
+
*/
|
|
142
|
+
async runTurn(texts, signal) {
|
|
143
|
+
const completion = Promise.withResolvers();
|
|
144
|
+
this.turnCompleted = completion;
|
|
145
|
+
const threadId = this.threadId;
|
|
146
|
+
const turn = object(object(await this.guarded(this.transport.request("turn/start", {
|
|
147
|
+
threadId,
|
|
148
|
+
input: texts.map((text) => ({
|
|
149
|
+
type: "text",
|
|
150
|
+
text,
|
|
151
|
+
text_elements: []
|
|
152
|
+
}))
|
|
153
|
+
}, signal), signal), "turn/start response").turn, "turn/start turn");
|
|
154
|
+
this.commitTurnId(string(turn.id, "turn/start turn id"));
|
|
155
|
+
const terminal = object((await this.guarded(completion.promise, signal)).turn, "turn/completed turn");
|
|
156
|
+
const status = terminal.status;
|
|
157
|
+
if (isContextWindowExceeded(terminal)) return {
|
|
158
|
+
output: this.collectOutput(),
|
|
159
|
+
stopReason: "max-tokens"
|
|
160
|
+
};
|
|
161
|
+
if (status !== "completed") {
|
|
162
|
+
const detail = status === "failed" ? `: ${JSON.stringify(terminal.error)}` : "";
|
|
163
|
+
throw new Error(`subagent-codex: Codex turn ended with status ${String(status)}${detail}`);
|
|
164
|
+
}
|
|
165
|
+
const output = this.collectOutput();
|
|
166
|
+
if (output.length === 0) throw new Error("subagent-codex: Codex completed without a final answer");
|
|
167
|
+
return {
|
|
168
|
+
output,
|
|
169
|
+
stopReason: "completed"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Best-effort remote cancellation. Local settlement and process teardown
|
|
174
|
+
* remain authoritative when the child no longer accepts protocol requests.
|
|
175
|
+
*/
|
|
176
|
+
interrupt() {
|
|
177
|
+
if (this.threadId === void 0 || this.turnId === void 0 || this.closed) return;
|
|
178
|
+
this.transport.request("turn/interrupt", {
|
|
179
|
+
threadId: this.threadId,
|
|
180
|
+
turnId: this.turnId
|
|
181
|
+
}).catch(() => {});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The best non-commentary answer observed so far, preserving exact bytes.
|
|
185
|
+
* @returns the selected final or nullable-phase text block, if any.
|
|
186
|
+
*/
|
|
187
|
+
collectOutput() {
|
|
188
|
+
const selected = this.lastFinalAnswer ?? this.lastUnphasedAnswer;
|
|
189
|
+
return selected !== void 0 && selected.trim().length > 0 ? [{
|
|
190
|
+
type: "text",
|
|
191
|
+
text: selected
|
|
192
|
+
}] : [];
|
|
193
|
+
}
|
|
194
|
+
/** Detach JSON-RPC listeners and reject outstanding requests. Idempotent. */
|
|
195
|
+
close() {
|
|
196
|
+
if (this.closed) return;
|
|
197
|
+
this.closed = true;
|
|
198
|
+
this.input.off("end", this.onInputEnd);
|
|
199
|
+
this.transport.close();
|
|
200
|
+
}
|
|
201
|
+
async guarded(pending, signal) {
|
|
202
|
+
return raceAbort(Promise.race([this.fatal.promise, pending]), signal);
|
|
203
|
+
}
|
|
204
|
+
fail(error) {
|
|
205
|
+
this.fatal.reject(error);
|
|
206
|
+
}
|
|
207
|
+
onInputError = (error) => {
|
|
208
|
+
this.fail(error);
|
|
209
|
+
};
|
|
210
|
+
onOutputError = (error) => {
|
|
211
|
+
this.fail(error);
|
|
212
|
+
};
|
|
213
|
+
onInputEnd = () => {
|
|
214
|
+
this.fail(/* @__PURE__ */ new Error("subagent-codex: app-server protocol stream closed"));
|
|
215
|
+
};
|
|
216
|
+
observePendingTurnId(id) {
|
|
217
|
+
if (this.turnCompleted === void 0) throw new Error("subagent-codex: app-server referenced a turn before turn/start");
|
|
218
|
+
if (this.pendingTurnId !== void 0 && this.pendingTurnId !== id) throw new Error("subagent-codex: app-server referenced conflicting turns");
|
|
219
|
+
this.pendingTurnId = id;
|
|
220
|
+
}
|
|
221
|
+
commitTurnId(id) {
|
|
222
|
+
if (this.pendingTurnId !== void 0 && this.pendingTurnId !== id) throw new Error("subagent-codex: turn/start response did not match the active turn");
|
|
223
|
+
this.turnId = id;
|
|
224
|
+
const notifications = this.earlyTurnNotifications.splice(0);
|
|
225
|
+
for (const notification of notifications) this.handleNotification(notification.method, notification.params);
|
|
226
|
+
}
|
|
227
|
+
validateRunIds(params, nullableTurn = false) {
|
|
228
|
+
if (params.threadId !== this.threadId) throw new Error("subagent-codex: app-server request referenced another thread");
|
|
229
|
+
if (nullableTurn && params.turnId === null) return;
|
|
230
|
+
const id = string(params.turnId, "server request turn id");
|
|
231
|
+
if (this.turnId === void 0) {
|
|
232
|
+
this.observePendingTurnId(id);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (id !== this.turnId) throw new Error("subagent-codex: app-server request referenced another turn");
|
|
236
|
+
}
|
|
237
|
+
handleServerRequest(method, params) {
|
|
238
|
+
try {
|
|
239
|
+
switch (method) {
|
|
240
|
+
case "item/commandExecution/requestApproval":
|
|
241
|
+
case "item/fileChange/requestApproval":
|
|
242
|
+
this.validateRunIds(params);
|
|
243
|
+
return Promise.resolve({ decision: unattendedDecision(params) });
|
|
244
|
+
case "item/permissions/requestApproval":
|
|
245
|
+
this.validateRunIds(params);
|
|
246
|
+
return Promise.resolve({
|
|
247
|
+
permissions: {},
|
|
248
|
+
scope: "turn"
|
|
249
|
+
});
|
|
250
|
+
case "item/tool/requestUserInput":
|
|
251
|
+
this.validateRunIds(params);
|
|
252
|
+
return Promise.resolve({ answers: {} });
|
|
253
|
+
case "mcpServer/elicitation/request":
|
|
254
|
+
this.validateRunIds(params, true);
|
|
255
|
+
return Promise.resolve({
|
|
256
|
+
action: "decline",
|
|
257
|
+
content: null,
|
|
258
|
+
_meta: null
|
|
259
|
+
});
|
|
260
|
+
default: throw new Error(`subagent-codex: unsupported app-server request ${JSON.stringify(method)}`);
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const normalized = thrown$1(error);
|
|
264
|
+
this.fail(normalized);
|
|
265
|
+
return Promise.reject(normalized);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
handleNotification(method, params) {
|
|
269
|
+
if (method === "turn/started") {
|
|
270
|
+
if (string(params.threadId, "turn/started thread id") !== this.threadId) return;
|
|
271
|
+
const turn = object(params.turn, "turn/started turn");
|
|
272
|
+
if (this.turnCompleted !== void 0 && this.turnId === void 0) this.observePendingTurnId(string(turn.id, "turn/started turn id"));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (method === "item/completed") {
|
|
276
|
+
if (string(params.threadId, "item/completed thread id") !== this.threadId) return;
|
|
277
|
+
const id = string(params.turnId, "item/completed turn id");
|
|
278
|
+
if (this.turnId === void 0) {
|
|
279
|
+
if (this.turnCompleted !== void 0) {
|
|
280
|
+
this.observePendingTurnId(id);
|
|
281
|
+
this.earlyTurnNotifications.push({
|
|
282
|
+
method,
|
|
283
|
+
params
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (id !== this.turnId) return;
|
|
289
|
+
const item = object(params.item, "item/completed item");
|
|
290
|
+
if (item.type !== "agentMessage") return;
|
|
291
|
+
const text = typeof item.text === "string" ? item.text : (() => {
|
|
292
|
+
throw new Error("subagent-codex: app-server returned an invalid agent message");
|
|
293
|
+
})();
|
|
294
|
+
if (item.phase === "final_answer") this.lastFinalAnswer = text;
|
|
295
|
+
else if (item.phase === null) this.lastUnphasedAnswer = text;
|
|
296
|
+
else if (item.phase !== "commentary") throw new Error(`subagent-codex: app-server returned an unknown agent message phase ${JSON.stringify(item.phase)}`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (method !== "turn/completed") return;
|
|
300
|
+
if (string(params.threadId, "turn/completed thread id") !== this.threadId) return;
|
|
301
|
+
const turn = object(params.turn, "turn/completed turn");
|
|
302
|
+
const id = string(turn.id, "turn/completed turn id");
|
|
303
|
+
const turnCompleted = this.turnCompleted;
|
|
304
|
+
if (turnCompleted === void 0) return;
|
|
305
|
+
if (this.turnId === void 0) {
|
|
306
|
+
this.observePendingTurnId(id);
|
|
307
|
+
this.earlyTurnNotifications.push({
|
|
308
|
+
method,
|
|
309
|
+
params
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (id !== this.turnId) return;
|
|
314
|
+
if (![
|
|
315
|
+
"completed",
|
|
316
|
+
"interrupted",
|
|
317
|
+
"failed"
|
|
318
|
+
].includes(String(turn.status))) throw new Error(`subagent-codex: app-server returned invalid terminal turn status ${String(turn.status)}`);
|
|
319
|
+
turnCompleted.resolve(params);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region lib/types/run.js
|
|
324
|
+
/**
|
|
325
|
+
* One-shot Codex child lifecycle: spawn the real app-server through the
|
|
326
|
+
* subprocess seam, publish only after initialization and ephemeral thread
|
|
327
|
+
* creation, flatten post-publication failures, and dispose to whole-tree
|
|
328
|
+
* quiescence.
|
|
329
|
+
*
|
|
330
|
+
* @module @deepseek-ai/dsh-subagent-codex/run
|
|
331
|
+
*/
|
|
332
|
+
/** Default POSIX grace between subprocess termination tiers. */
|
|
333
|
+
const DEFAULT_DISPOSE_GRACE_MS = 3e3;
|
|
334
|
+
/**
|
|
335
|
+
* Resolve the fixed app-server command for a platform.
|
|
336
|
+
*
|
|
337
|
+
* Windows npm and pnpm installs expose `codex.cmd`, which requires `cmd.exe`;
|
|
338
|
+
* the argv is constant so no task or configuration text enters the
|
|
339
|
+
* shell boundary.
|
|
340
|
+
* @param platform - host platform used to select the executable boundary.
|
|
341
|
+
* @returns argv for the fixed Codex app-server command.
|
|
342
|
+
*/
|
|
343
|
+
function codexAppServerArgv(platform = process.platform) {
|
|
344
|
+
return platform === "win32" ? [
|
|
345
|
+
"cmd.exe",
|
|
346
|
+
"/d",
|
|
347
|
+
"/s",
|
|
348
|
+
"/c",
|
|
349
|
+
"codex",
|
|
350
|
+
"app-server",
|
|
351
|
+
"--stdio"
|
|
352
|
+
] : [
|
|
353
|
+
"codex",
|
|
354
|
+
"app-server",
|
|
355
|
+
"--stdio"
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
function thrown(value) {
|
|
359
|
+
/* v8 ignore next -- typed subprocess/wire failures reject with Error. */
|
|
360
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Validate and preserve the one-shot task before crossing the process boundary.
|
|
364
|
+
* @param prompt - task content accepted from the shared subagent service.
|
|
365
|
+
* @returns the exact non-empty text block sequence.
|
|
366
|
+
*/
|
|
367
|
+
function textTask(prompt) {
|
|
368
|
+
if (prompt.length === 0) throw new Error("subagent-codex: the one-shot task must contain only text blocks");
|
|
369
|
+
const texts = [];
|
|
370
|
+
for (const block of prompt) {
|
|
371
|
+
if (block.type !== "text") throw new Error("subagent-codex: the one-shot task must contain only text blocks");
|
|
372
|
+
texts.push(block.text);
|
|
373
|
+
}
|
|
374
|
+
if (texts.every((text) => text.trim().length === 0)) throw new Error("subagent-codex: the one-shot task must not be empty");
|
|
375
|
+
return texts;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Close the private wire, terminate the managed process tree, and wait for the
|
|
379
|
+
* subprocess owner to prove it is gone.
|
|
380
|
+
* @param wire - private app-server protocol connection.
|
|
381
|
+
* @param child - shared-service handle that owns the process tree.
|
|
382
|
+
*/
|
|
383
|
+
async function disposeCodexChild(wire, child) {
|
|
384
|
+
wire.close();
|
|
385
|
+
if (child.pid <= 0) {
|
|
386
|
+
await child.done.catch(() => {});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
child.stdin?.end();
|
|
391
|
+
} catch {}
|
|
392
|
+
child.terminate();
|
|
393
|
+
await child.waitForExit();
|
|
394
|
+
await child.done;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Start the real `codex app-server --stdio` child and publish its one-shot run.
|
|
398
|
+
* @param request - resolved shared subagent request.
|
|
399
|
+
* @param spec - Workspace, environment, process service, and diagnostic policy.
|
|
400
|
+
* @returns the published run after initialization and ephemeral thread creation.
|
|
401
|
+
*/
|
|
402
|
+
async function startCodexRun(request, spec) {
|
|
403
|
+
const texts = textTask(request.prompt);
|
|
404
|
+
if (request.signal.aborted) throw new Error("subagent-codex: request was aborted before app-server startup");
|
|
405
|
+
const child = spec.spawn({
|
|
406
|
+
argv: codexAppServerArgv(),
|
|
407
|
+
cwd: spec.cwd,
|
|
408
|
+
stdio: {
|
|
409
|
+
stdin: "pipe",
|
|
410
|
+
stdout: "pipe",
|
|
411
|
+
stderr: "inherit"
|
|
412
|
+
},
|
|
413
|
+
graceMs: spec.disposeGraceMs,
|
|
414
|
+
env: spec.env
|
|
415
|
+
});
|
|
416
|
+
const wire = new CodexAppServerWire(child.stdout, child.stdin);
|
|
417
|
+
const disposeProcess = () => disposeCodexChild(wire, child);
|
|
418
|
+
const processFailure = child.done.then((outcome) => Promise.reject(/* @__PURE__ */ new Error(`subagent-codex: app-server exited before the run settled (code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`)), (error) => Promise.reject(thrown(error)));
|
|
419
|
+
processFailure.catch(() => {});
|
|
420
|
+
const runAbort = new AbortController();
|
|
421
|
+
const requestCancel = () => {
|
|
422
|
+
if (runAbort.signal.aborted) return;
|
|
423
|
+
runAbort.abort(/* @__PURE__ */ new Error("subagent-codex: run cancelled locally"));
|
|
424
|
+
wire.interrupt();
|
|
425
|
+
};
|
|
426
|
+
const onAbort = () => {
|
|
427
|
+
requestCancel();
|
|
428
|
+
};
|
|
429
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
430
|
+
try {
|
|
431
|
+
wire.start();
|
|
432
|
+
await Promise.race([wire.initialize(request.signal), processFailure]);
|
|
433
|
+
await Promise.race([wire.startThread(spec.cwd, request.signal), processFailure]);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
request.signal.removeEventListener("abort", onAbort);
|
|
436
|
+
try {
|
|
437
|
+
await disposeProcess();
|
|
438
|
+
} catch (disposeError) {
|
|
439
|
+
throw new AggregateError([thrown(error), thrown(disposeError)], "subagent-codex: startup failed and app-server cleanup also failed");
|
|
440
|
+
}
|
|
441
|
+
if (runAbort.signal.aborted) throw new Error("subagent-codex: request was aborted before run publication");
|
|
442
|
+
throw thrown(error);
|
|
443
|
+
}
|
|
444
|
+
const collectOutput = () => wire.collectOutput();
|
|
445
|
+
const result = settleRunResult({
|
|
446
|
+
attempt: () => Promise.race([wire.runTurn(texts, runAbort.signal), processFailure]),
|
|
447
|
+
collectOutput,
|
|
448
|
+
cancelled: () => runAbort.signal.aborted,
|
|
449
|
+
onError: spec.onError,
|
|
450
|
+
signal: request.signal,
|
|
451
|
+
onAbort
|
|
452
|
+
});
|
|
453
|
+
return subprocessRunHandle({
|
|
454
|
+
id: SessionId(randomUUID()),
|
|
455
|
+
result,
|
|
456
|
+
signal: request.signal,
|
|
457
|
+
onAbort,
|
|
458
|
+
requestCancel,
|
|
459
|
+
teardown: disposeProcess
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region lib/types/index.js
|
|
464
|
+
/**
|
|
465
|
+
* Fixed Codex one-shot subagent provider. Every accepted run starts a fresh
|
|
466
|
+
* official `codex app-server --stdio` process in the delegating Session's
|
|
467
|
+
* workspace and publishes only after an ephemeral thread exists.
|
|
468
|
+
*
|
|
469
|
+
* @module @deepseek-ai/dsh-subagent-codex
|
|
470
|
+
*/
|
|
471
|
+
const name = "subagent-codex";
|
|
472
|
+
const inject = ["subagents", "subprocess"];
|
|
473
|
+
const Config = z.object({
|
|
474
|
+
env: z.dict(z.string()).default({}),
|
|
475
|
+
disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS)
|
|
476
|
+
});
|
|
477
|
+
var CodexProvider = class {
|
|
478
|
+
ctx;
|
|
479
|
+
config;
|
|
480
|
+
name = "codex";
|
|
481
|
+
capabilities = NO_START_CAPABILITIES;
|
|
482
|
+
inheritsParentContext = false;
|
|
483
|
+
constructor(ctx, config) {
|
|
484
|
+
this.ctx = ctx;
|
|
485
|
+
this.config = config;
|
|
486
|
+
}
|
|
487
|
+
start(request) {
|
|
488
|
+
const parentCwd = request.parent.session.header.cwd;
|
|
489
|
+
if (parentCwd === void 0) throw new Error("subagent-codex: no working directory for the child — delegate from a parent session that has one");
|
|
490
|
+
return startCodexRun(request, {
|
|
491
|
+
cwd: resolveChildCwd("subagent-codex", void 0, parentCwd),
|
|
492
|
+
env: this.config.env,
|
|
493
|
+
disposeGraceMs: this.config.disposeGraceMs,
|
|
494
|
+
spawn: (spawnSpec) => this.ctx.subprocess.spawn(spawnSpec),
|
|
495
|
+
onError: (error, stopReason) => {
|
|
496
|
+
this.ctx.logger.warn(`subagent-codex: child run failed (${stopReason}): ${error.message}`);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
/**
|
|
502
|
+
* Register the fixed `codex` provider.
|
|
503
|
+
* @param ctx - context carrying shared subagent and subprocess services.
|
|
504
|
+
* @param config - explicit child environment and disposal grace.
|
|
505
|
+
*/
|
|
506
|
+
function apply(ctx, config) {
|
|
507
|
+
const resolved = config;
|
|
508
|
+
assertPositiveFinite("subagent-codex", "disposeGraceMs", resolved.disposeGraceMs);
|
|
509
|
+
if (resolved.disposeGraceMs > MAX_TIMER_DELAY_MS) throw new Error(`subagent-codex: disposeGraceMs must be no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
510
|
+
ctx.subagents.registerProvider(new CodexProvider(ctx, resolved));
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
export { Config, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-codex`.
|
|
4
|
+
* @module @deepseek-ai/dsh-subagent-codex/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-subagent-codex";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "subagent-codex-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: lifecycle pairing belongs to the shared subagent
|
|
13
|
+
* service and process-tree ownership belongs to the subprocess service.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - plugin context carrying the invariant registry.
|
|
19
|
+
* @returns the installed registration's disposer.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed Codex one-shot subagent provider. Every accepted run starts a fresh
|
|
3
|
+
* official `codex app-server --stdio` process in the delegating Session's
|
|
4
|
+
* workspace and publishes only after an ephemeral thread exists.
|
|
5
|
+
*
|
|
6
|
+
* @module @deepseek-ai/dsh-subagent-codex
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import z from '@deepseek-ai/schemastery';
|
|
10
|
+
export declare const name = "subagent-codex";
|
|
11
|
+
export declare const inject: string[];
|
|
12
|
+
/** Deployment-owned environment and process-release bound. */
|
|
13
|
+
export interface Config {
|
|
14
|
+
/**
|
|
15
|
+
* Explicit environment entries layered over the subprocess seam's
|
|
16
|
+
* credential-scrubbed parent environment.
|
|
17
|
+
*/
|
|
18
|
+
env?: Record<string, string>;
|
|
19
|
+
/** Grace in milliseconds for app-server process-tree termination. */
|
|
20
|
+
disposeGraceMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const Config: z<Config>;
|
|
23
|
+
/**
|
|
24
|
+
* Register the fixed `codex` provider.
|
|
25
|
+
* @param ctx - context carrying shared subagent and subprocess services.
|
|
26
|
+
* @param config - explicit child environment and disposal grace.
|
|
27
|
+
*/
|
|
28
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-codex`.
|
|
3
|
+
* @module @deepseek-ai/dsh-subagent-codex/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "subagent-codex-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - plugin context carrying the invariant registry.
|
|
13
|
+
* @returns the installed registration's disposer.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot Codex child lifecycle: spawn the real app-server through the
|
|
3
|
+
* subprocess seam, publish only after initialization and ephemeral thread
|
|
4
|
+
* creation, flatten post-publication failures, and dispose to whole-tree
|
|
5
|
+
* quiescence.
|
|
6
|
+
*
|
|
7
|
+
* @module @deepseek-ai/dsh-subagent-codex/run
|
|
8
|
+
*/
|
|
9
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
10
|
+
import { type SubagentRun, type SubagentStartRequest, type SubagentStopReason } from '@deepseek-ai/dsh-subagent';
|
|
11
|
+
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess';
|
|
12
|
+
import { CodexAppServerWire } from './wire.ts';
|
|
13
|
+
/** Default POSIX grace between subprocess termination tiers. */
|
|
14
|
+
export declare const DEFAULT_DISPOSE_GRACE_MS = 3000;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the fixed app-server command for a platform.
|
|
17
|
+
*
|
|
18
|
+
* Windows npm and pnpm installs expose `codex.cmd`, which requires `cmd.exe`;
|
|
19
|
+
* the argv is constant so no task or configuration text enters the
|
|
20
|
+
* shell boundary.
|
|
21
|
+
* @param platform - host platform used to select the executable boundary.
|
|
22
|
+
* @returns argv for the fixed Codex app-server command.
|
|
23
|
+
*/
|
|
24
|
+
export declare function codexAppServerArgv(platform?: NodeJS.Platform): string[];
|
|
25
|
+
/** Fully resolved inputs for one Codex app-server run. */
|
|
26
|
+
export interface CodexRunSpec {
|
|
27
|
+
/** Parent Session workspace, also supplied to `thread/start`. */
|
|
28
|
+
readonly cwd: string;
|
|
29
|
+
/** Explicit deployment/test environment layered after the shared scrub. */
|
|
30
|
+
readonly env: Record<string, string>;
|
|
31
|
+
/** Subprocess termination grace passed to the shared process-tree owner. */
|
|
32
|
+
readonly disposeGraceMs: number;
|
|
33
|
+
/** Shared subprocess service spawn operation. */
|
|
34
|
+
readonly spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle;
|
|
35
|
+
/** Diagnostic sink for a post-publication error flattened into a result. */
|
|
36
|
+
readonly onError?: (error: Error, stopReason: SubagentStopReason) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate and preserve the one-shot task before crossing the process boundary.
|
|
40
|
+
* @param prompt - task content accepted from the shared subagent service.
|
|
41
|
+
* @returns the exact non-empty text block sequence.
|
|
42
|
+
*/
|
|
43
|
+
export declare function textTask(prompt: readonly ContentBlock[]): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Close the private wire, terminate the managed process tree, and wait for the
|
|
46
|
+
* subprocess owner to prove it is gone.
|
|
47
|
+
* @param wire - private app-server protocol connection.
|
|
48
|
+
* @param child - shared-service handle that owns the process tree.
|
|
49
|
+
*/
|
|
50
|
+
export declare function disposeCodexChild(wire: CodexAppServerWire, child: SubprocessHandle): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Start the real `codex app-server --stdio` child and publish its one-shot run.
|
|
53
|
+
* @param request - resolved shared subagent request.
|
|
54
|
+
* @param spec - Workspace, environment, process service, and diagnostic policy.
|
|
55
|
+
* @returns the published run after initialization and ephemeral thread creation.
|
|
56
|
+
*/
|
|
57
|
+
export declare function startCodexRun(request: SubagentStartRequest, spec: CodexRunSpec): Promise<SubagentRun>;
|
|
58
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Codex app-server 0.147.0 protocol adapter. The shared JSON-RPC
|
|
3
|
+
* transport owns framing and request correlation; this module owns only the
|
|
4
|
+
* product methods, current thread/turn association, unattended approval
|
|
5
|
+
* responses, and terminal-answer selection.
|
|
6
|
+
*
|
|
7
|
+
* @module @deepseek-ai/dsh-subagent-codex/wire
|
|
8
|
+
*/
|
|
9
|
+
import type { Readable, Writable } from 'node:stream';
|
|
10
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
11
|
+
import type { SubagentResult } from '@deepseek-ai/dsh-subagent';
|
|
12
|
+
/**
|
|
13
|
+
* One app-server connection and its single ephemeral thread/turn.
|
|
14
|
+
*
|
|
15
|
+
* The class deliberately exposes no generic request surface. Supporting
|
|
16
|
+
* another product method must first become part of the provider contract.
|
|
17
|
+
*/
|
|
18
|
+
export declare class CodexAppServerWire {
|
|
19
|
+
private readonly input;
|
|
20
|
+
private readonly transport;
|
|
21
|
+
private readonly fatal;
|
|
22
|
+
private threadId;
|
|
23
|
+
private turnId;
|
|
24
|
+
private pendingTurnId;
|
|
25
|
+
private turnCompleted;
|
|
26
|
+
private readonly earlyTurnNotifications;
|
|
27
|
+
private lastFinalAnswer;
|
|
28
|
+
private lastUnphasedAnswer;
|
|
29
|
+
private closed;
|
|
30
|
+
constructor(input: Readable, output: Writable);
|
|
31
|
+
/** Start reading app-server frames. */
|
|
32
|
+
start(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Perform the required app-server initialize/initialized handshake.
|
|
35
|
+
* @param signal - unpublished-start cancellation.
|
|
36
|
+
*/
|
|
37
|
+
initialize(signal: AbortSignal): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Create the run's private ephemeral thread and retain its identity.
|
|
40
|
+
* @param cwd - parent Session workspace.
|
|
41
|
+
* @param signal - unpublished-start cancellation.
|
|
42
|
+
*/
|
|
43
|
+
startThread(cwd: string, signal: AbortSignal): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Submit the one text-only task and wait for this thread/turn's authoritative
|
|
46
|
+
* terminal notification.
|
|
47
|
+
* @param texts - already validated task text blocks.
|
|
48
|
+
* @param signal - local cancellation for the published run.
|
|
49
|
+
* @returns the shared subagent result.
|
|
50
|
+
*/
|
|
51
|
+
runTurn(texts: readonly string[], signal: AbortSignal): Promise<SubagentResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Best-effort remote cancellation. Local settlement and process teardown
|
|
54
|
+
* remain authoritative when the child no longer accepts protocol requests.
|
|
55
|
+
*/
|
|
56
|
+
interrupt(): void;
|
|
57
|
+
/**
|
|
58
|
+
* The best non-commentary answer observed so far, preserving exact bytes.
|
|
59
|
+
* @returns the selected final or nullable-phase text block, if any.
|
|
60
|
+
*/
|
|
61
|
+
collectOutput(): ContentBlock[];
|
|
62
|
+
/** Detach JSON-RPC listeners and reject outstanding requests. Idempotent. */
|
|
63
|
+
close(): void;
|
|
64
|
+
private guarded;
|
|
65
|
+
private fail;
|
|
66
|
+
private readonly onInputError;
|
|
67
|
+
private readonly onOutputError;
|
|
68
|
+
private readonly onInputEnd;
|
|
69
|
+
private observePendingTurnId;
|
|
70
|
+
private commitTurnId;
|
|
71
|
+
private validateRunIds;
|
|
72
|
+
private handleServerRequest;
|
|
73
|
+
private handleNotification;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=wire.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-subagent-codex",
|
|
3
|
+
"description": "One-shot Codex subagent provider over the official app-server protocol",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/subagent/subagent-codex"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "BSD-3-Clause",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
36
|
+
"@deepseek-ai/dsh-sdk-protocol": "^0.0.1-rc.1",
|
|
37
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
38
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
39
|
+
"@deepseek-ai/dsh-subagent": "^0.0.1-rc.1",
|
|
40
|
+
"@deepseek-ai/dsh-subprocess": "^0.0.1-rc.1",
|
|
41
|
+
"@deepseek-ai/dsh-timeout": "^0.0.1-rc.1",
|
|
42
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@openai/codex": "0.147.0",
|
|
49
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.1-rc.1",
|
|
50
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
51
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
52
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
53
|
+
"@deepseek-ai/dsh-loader-smoke": "^0.0.1-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-sdk-protocol": "^0.0.1-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-subagent": "^0.0.1-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-subprocess": "^0.0.1-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-subprocess-local": "^0.0.1-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-timeout": "^0.0.1-rc.1",
|
|
60
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
61
|
+
}
|
|
62
|
+
}
|