@shuind/dsh-codex-harness 0.1.15 → 0.1.16
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/README.md +55 -23
- package/lib/client.js +75 -5
- package/lib/index.js +166 -58
- package/lib/types/activity-types.d.ts +6 -0
- package/lib/types/activity-types.js +3 -0
- package/lib/types/activity.d.ts +32 -0
- package/lib/types/activity.js +53 -0
- package/lib/types/client/index.d.ts +5 -0
- package/lib/types/client/index.js +50 -0
- package/lib/types/exec.d.ts +1 -0
- package/lib/types/exec.js +111 -53
- package/lib/types/index.js +3 -2
- package/lib/types/patch.js +39 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,28 +10,8 @@
|
|
|
10
10
|
- **Fast**:仅在 Codex preset 的模型选择菜单中显示,开启后向 Responses 请求发送 `service_tier: "priority"`。
|
|
11
11
|
- **远程搜索与压缩**:OpenAI Responses 请求默认优先使用 hosted `web_search` 和 `/responses/compact`;失败时回退到 DSH 的本地实现。
|
|
12
12
|
- **上下文容量**:在 Web 中设置 `1K`–`1000K` tokens,设置值作用于下一次请求。
|
|
13
|
+
- **活动状态**:Codex 等待模型响应或进行上下文压缩时,在输入框上方显示对应状态和耗时,让长时间 Deep Diving 不再像卡住。
|
|
13
14
|
|
|
14
|
-
## 可选协作提示词(私货)
|
|
15
|
-
|
|
16
|
-
这段提示词默认关闭,不会注入系统提示词。需要时,在自定义 `agent.cordis.yml` 中开启:
|
|
17
|
-
|
|
18
|
-
```yaml
|
|
19
|
-
- id: codex-tools
|
|
20
|
-
name: '@shuind/dsh-codex-harness'
|
|
21
|
-
config:
|
|
22
|
-
collaborationPrompt: true
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
开启后,Codex preset 会在系统提示词中注入以下协作要求:
|
|
26
|
-
|
|
27
|
-
```text
|
|
28
|
-
Ask, align, and clarify whenever uncertainty, assumptions, tradeoffs, or decisions could materially affect the outcome.
|
|
29
|
-
Understand the user's full picture, align it with your own, and leave no hidden assumptions or gaps.
|
|
30
|
-
Keep only the essential logic and core actions. There's no need to explain or test what was removed or why something wasn't done.
|
|
31
|
-
Convey enough valuable information with as few words as possible. Stay focused on the end goal.
|
|
32
|
-
Solve problems by thinking from first principles and at a higher level.
|
|
33
|
-
Make things as effortless as possible for the user.
|
|
34
|
-
```
|
|
35
15
|
## 安装
|
|
36
16
|
|
|
37
17
|
```sh
|
|
@@ -72,16 +52,68 @@ Fast 设置保存在 `codex` 命名空间,只对 Codex preset 生效。
|
|
|
72
52
|
|
|
73
53
|
上下文容量使用整数 K tokens,范围为 `1K` 到 `1000K`。实际可用上限仍取决于模型和中转站支持情况。
|
|
74
54
|
|
|
55
|
+
### 活动状态
|
|
56
|
+
|
|
57
|
+
Codex 等待模型首个输出时,Web 输入框上方会显示 `正在等待模型响应... · Ns`;开始上下文压缩后会切换为 `正在压缩上下文... · Ns`。收到模型输出、压缩结束或下一轮开始后自动隐藏。该状态由 Host 的 `sessionProjections` 投影驱动,因此本地压缩和远程 `/responses/compact` 都会显示同一种状态。
|
|
58
|
+
|
|
59
|
+
如果当前 DSH 组合没有加载 projection 服务,插件会跳过注册,客户端也会自动隐藏这行,不影响普通会话。
|
|
60
|
+
|
|
61
|
+
## 随包提供的 Codex preset
|
|
62
|
+
|
|
63
|
+
安装插件后,首次启动对应的 DSH profile 时,插件会把完整 preset 从随包提供的 `presets/codex` 安装到 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/codex/`;如果你已经创建了同名 preset,插件会保留你的文件。普通用户不需要自己创建 preset。
|
|
64
|
+
|
|
65
|
+
在 Web 的 **Agent 预设** 中选择 **Codex**,再新建会话即可使用随包提供的 Codex 工具、网页搜索、远程压缩和 Skills 组合。Fast、思考强度、GPT 图片输入和上下文容量可以在对应的设置位置配置。
|
|
66
|
+
|
|
75
67
|
## 自定义 preset
|
|
76
68
|
|
|
77
|
-
|
|
69
|
+
只有在想自己组合工具、提示词,或修改插件配置时才需要自定义 preset。自定义 preset 放在 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/` 目录中。
|
|
70
|
+
|
|
71
|
+
### 示例:创建一个带协作提示词的 Codex preset
|
|
72
|
+
|
|
73
|
+
例如创建目录 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/my-codex/`,并写入 `agent.cordis.yml`:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
- id: persona
|
|
77
|
+
name: '@deepseek-ai/dsh-persona'
|
|
78
|
+
config:
|
|
79
|
+
text: 你是一个使用 DSH 工具工作的 Codex 编程助手。
|
|
80
|
+
|
|
81
|
+
- id: codex-tools
|
|
82
|
+
name: '@shuind/dsh-codex-harness'
|
|
83
|
+
config:
|
|
84
|
+
collaborationPrompt: true
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
再写入 `preset.yml`,让 Web 中显示更清晰的名称:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
name: My Codex
|
|
91
|
+
description: 使用 Codex 工具并开启可选协作提示词。
|
|
92
|
+
order: 10
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
然后在 Web 的 **Agent 预设** 中选择 `My Codex`,再新建会话。这个 preset 会加载 Codex 工具,并开启协作提示词;网页搜索、远程压缩和 Skills 等其他功能需要在同一个 `agent.cordis.yml` 中按需添加。
|
|
96
|
+
|
|
97
|
+
### 可选协作提示词(私货)
|
|
98
|
+
|
|
99
|
+
这段提示词默认关闭,不会注入系统提示词。需要时,在自定义 `agent.cordis.yml` 中开启:
|
|
78
100
|
|
|
79
101
|
```yaml
|
|
80
102
|
- id: codex-tools
|
|
81
103
|
name: '@shuind/dsh-codex-harness'
|
|
104
|
+
config:
|
|
105
|
+
collaborationPrompt: true
|
|
82
106
|
```
|
|
83
107
|
|
|
84
|
-
|
|
108
|
+
开启后,Codex preset 会在系统提示词中注入以下协作要求:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
Ask, align, and clarify first. Try your best to understand the user's full picture.
|
|
112
|
+
Keep only the essential logic and core actions. There's no need to explain or test what was removed or why something wasn't done.
|
|
113
|
+
Convey enough valuable information with as few words as possible. Stay focused on the end goal.
|
|
114
|
+
Solve problems by thinking from first principles and at a higher level.
|
|
115
|
+
Make things as effortless as possible for the user.
|
|
116
|
+
```
|
|
85
117
|
|
|
86
118
|
## 注意事项
|
|
87
119
|
|
package/lib/client.js
CHANGED
|
@@ -398,15 +398,20 @@ window.__ModuleLoader__.load({
|
|
|
398
398
|
exports.jsxs = q;
|
|
399
399
|
}));
|
|
400
400
|
//#endregion
|
|
401
|
-
//#region
|
|
402
|
-
var
|
|
401
|
+
//#region node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.js
|
|
402
|
+
var require_jsx_runtime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
403
403
|
module.exports = require_react_jsx_runtime_production_min();
|
|
404
|
-
}))
|
|
404
|
+
}));
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region lib/types/context.js
|
|
407
|
+
var import_react = require_react();
|
|
408
|
+
var import_jsx_runtime = require_jsx_runtime();
|
|
405
409
|
/** User-selectable Codex context controls use whole K-token units. */
|
|
406
410
|
const CODEX_CONTEXT_UNIT = 1e3;
|
|
407
411
|
const CODEX_CONTEXT_MAX = 1e6;
|
|
408
412
|
//#endregion
|
|
409
413
|
//#region lib/types/client/index.js
|
|
414
|
+
/** Browser controls for the Codex request settings mounted by the Host plugin. */
|
|
410
415
|
const NS = "codex";
|
|
411
416
|
const SETTINGS_NAMESPACE = "codex";
|
|
412
417
|
const en = {
|
|
@@ -417,7 +422,9 @@ window.__ModuleLoader__.load({
|
|
|
417
422
|
fastStateOff: "Off",
|
|
418
423
|
contextSize: "Context size",
|
|
419
424
|
contextSizeDescription: "Next request capacity in K tokens. The meter above is current.",
|
|
420
|
-
contextRestore: "Restore model default"
|
|
425
|
+
contextRestore: "Restore model default",
|
|
426
|
+
activityCompacting: "Compacting context...",
|
|
427
|
+
activityAwaitingModel: "Waiting for model response..."
|
|
421
428
|
};
|
|
422
429
|
const zh = {
|
|
423
430
|
fast: "Fast",
|
|
@@ -427,7 +434,9 @@ window.__ModuleLoader__.load({
|
|
|
427
434
|
fastStateOff: "关",
|
|
428
435
|
contextSize: "上下文大小",
|
|
429
436
|
contextSizeDescription: "设置下次请求的上下文容量,单位为 K tokens;上方是当前请求。",
|
|
430
|
-
contextRestore: "恢复模型默认值"
|
|
437
|
+
contextRestore: "恢复模型默认值",
|
|
438
|
+
activityCompacting: "正在压缩上下文...",
|
|
439
|
+
activityAwaitingModel: "正在等待模型响应..."
|
|
431
440
|
};
|
|
432
441
|
function FastModeButton({ sessionId, useSessions, useSettings, setSetting, t }) {
|
|
433
442
|
const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
|
|
@@ -588,6 +597,60 @@ window.__ModuleLoader__.load({
|
|
|
588
597
|
]
|
|
589
598
|
});
|
|
590
599
|
}
|
|
600
|
+
function elapsedSeconds(startedAt, now) {
|
|
601
|
+
return Math.max(0, Math.floor((now - startedAt) / 1e3));
|
|
602
|
+
}
|
|
603
|
+
function ActivityLine({ sessionId, useSessions, useProjection, t }) {
|
|
604
|
+
const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
|
|
605
|
+
const activity = useProjection("codexActivity");
|
|
606
|
+
const [now, setNow] = (0, import_react.useState)(() => Date.now());
|
|
607
|
+
(0, import_react.useEffect)(() => {
|
|
608
|
+
if (agentPreset !== "codex" || activity === void 0 || activity === null) return void 0;
|
|
609
|
+
setNow(Date.now());
|
|
610
|
+
const timer = setInterval(() => {
|
|
611
|
+
setNow(Date.now());
|
|
612
|
+
}, 1e3);
|
|
613
|
+
return () => clearInterval(timer);
|
|
614
|
+
}, [agentPreset, activity?.startedAt]);
|
|
615
|
+
if (agentPreset !== "codex" || activity === void 0 || activity === null) return null;
|
|
616
|
+
const label = activity.activity === "awaiting-model" ? t("activityAwaitingModel") : t("activityCompacting");
|
|
617
|
+
return (0, import_jsx_runtime.jsxs)("div", {
|
|
618
|
+
id: "codex-activity",
|
|
619
|
+
role: "status",
|
|
620
|
+
"aria-live": "polite",
|
|
621
|
+
style: {
|
|
622
|
+
display: "flex",
|
|
623
|
+
alignItems: "center",
|
|
624
|
+
gap: 7,
|
|
625
|
+
minHeight: 28,
|
|
626
|
+
padding: "4px 10px",
|
|
627
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
628
|
+
fontSize: 12,
|
|
629
|
+
lineHeight: "20px"
|
|
630
|
+
},
|
|
631
|
+
children: [
|
|
632
|
+
(0, import_jsx_runtime.jsx)("span", {
|
|
633
|
+
"aria-hidden": "true",
|
|
634
|
+
style: {
|
|
635
|
+
width: 7,
|
|
636
|
+
height: 7,
|
|
637
|
+
flex: "0 0 auto",
|
|
638
|
+
borderRadius: "50%",
|
|
639
|
+
background: "var(--dsw-static-blue-500)"
|
|
640
|
+
}
|
|
641
|
+
}),
|
|
642
|
+
(0, import_jsx_runtime.jsx)("span", { children: label }),
|
|
643
|
+
(0, import_jsx_runtime.jsxs)("span", {
|
|
644
|
+
style: { color: "var(--dsw-alias-label-tertiary)" },
|
|
645
|
+
children: [
|
|
646
|
+
"· ",
|
|
647
|
+
elapsedSeconds(activity.startedAt, now),
|
|
648
|
+
"s"
|
|
649
|
+
]
|
|
650
|
+
})
|
|
651
|
+
]
|
|
652
|
+
});
|
|
653
|
+
}
|
|
591
654
|
const inject = [
|
|
592
655
|
"slots",
|
|
593
656
|
"locale",
|
|
@@ -619,12 +682,19 @@ window.__ModuleLoader__.load({
|
|
|
619
682
|
locale: NS,
|
|
620
683
|
inject: injected
|
|
621
684
|
}, ContextSizeControl));
|
|
685
|
+
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
686
|
+
name: "conversation.input.dock",
|
|
687
|
+
id: "codex-activity",
|
|
688
|
+
order: 5,
|
|
689
|
+
locale: NS
|
|
690
|
+
}, ActivityLine));
|
|
622
691
|
}
|
|
623
692
|
var client_default = {
|
|
624
693
|
inject,
|
|
625
694
|
apply
|
|
626
695
|
};
|
|
627
696
|
//#endregion
|
|
697
|
+
exports.ActivityLine = ActivityLine;
|
|
628
698
|
exports.apply = apply;
|
|
629
699
|
exports.default = client_default;
|
|
630
700
|
exports.inject = inject;
|
package/lib/index.js
CHANGED
|
@@ -5,10 +5,12 @@ import { randomBytes } from "node:crypto";
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
7
7
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
+
import { z as z$1 } from "zod";
|
|
8
9
|
//#region lib/types/context.js
|
|
9
10
|
const CODEX_CONTEXT_MAX = 1e6;
|
|
10
|
-
String.raw`start: begin_patch hunk+ end_patch
|
|
11
|
+
String.raw`start: begin_patch environment_id? hunk+ end_patch
|
|
11
12
|
begin_patch: "*** Begin Patch" LF
|
|
13
|
+
environment_id: "*** Environment ID: " filename LF
|
|
12
14
|
end_patch: "*** End Patch" LF?
|
|
13
15
|
hunk: add_hunk | delete_hunk | update_hunk
|
|
14
16
|
add_hunk: "*** Add File: " filename LF add_line+
|
|
@@ -30,6 +32,15 @@ function isFileHeader(line) {
|
|
|
30
32
|
const trimmed = line.trim();
|
|
31
33
|
return trimmed.startsWith("*** Add File: ") || trimmed.startsWith("*** Delete File: ") || trimmed.startsWith("*** Update File: ");
|
|
32
34
|
}
|
|
35
|
+
const ENVIRONMENT_ID_MARKER = "*** Environment ID:";
|
|
36
|
+
function unwrapHeredoc(input) {
|
|
37
|
+
const normalized = input.replaceAll("\r\n", "\n").trim();
|
|
38
|
+
const lines = normalized.split("\n");
|
|
39
|
+
const first = lines[0]?.trim();
|
|
40
|
+
const last = lines.at(-1)?.trim();
|
|
41
|
+
if (lines.length >= 4 && (first === "<<EOF" || first === "<<'EOF'" || first === "<<\"EOF\"") && last === "EOF") return lines.slice(1, -1).join("\n").trim();
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
33
44
|
function pathFrom(line, prefix) {
|
|
34
45
|
const path = line.slice(prefix.length).trim();
|
|
35
46
|
if (path.length === 0) invalid(`${prefix.trim()} requires a file path`);
|
|
@@ -44,12 +55,17 @@ function isPotentialChangeMarker(line) {
|
|
|
44
55
|
}
|
|
45
56
|
/** Parse one complete Codex patch after normalizing CRLF input to LF. */
|
|
46
57
|
function parsePatch(input) {
|
|
47
|
-
const lines = input
|
|
58
|
+
const lines = unwrapHeredoc(input).split("\n");
|
|
48
59
|
if (lines[0]?.trim() !== "*** Begin Patch") invalid("input must start with \"*** Begin Patch\"");
|
|
49
60
|
if (lines.at(-1)?.trim() !== "*** End Patch") invalid("input must end with \"*** End Patch\"");
|
|
50
61
|
const files = [];
|
|
51
62
|
let index = 1;
|
|
52
63
|
const end = lines.length - 1;
|
|
64
|
+
if (lines[index]?.trim().startsWith(ENVIRONMENT_ID_MARKER) === true) {
|
|
65
|
+
if (lines[index].trim().slice(19).trim().length === 0) invalid("environment id cannot be empty");
|
|
66
|
+
index++;
|
|
67
|
+
if (lines[index]?.trim().startsWith(ENVIRONMENT_ID_MARKER) === true) invalid("environment id cannot be specified more than once");
|
|
68
|
+
}
|
|
53
69
|
while (index < end) {
|
|
54
70
|
const header = lines[index++]?.trim();
|
|
55
71
|
if (header === void 0) invalid("unexpected end of input");
|
|
@@ -183,6 +199,12 @@ function applyPatchHunks(original, hunks) {
|
|
|
183
199
|
//#region lib/types/exec.js
|
|
184
200
|
/** Codex `exec_command` and `write_stdin` execution over dsh capability seams. */
|
|
185
201
|
const STATES = /* @__PURE__ */ new WeakMap();
|
|
202
|
+
const MIN_YIELD_TIME_MS = 250;
|
|
203
|
+
const MIN_EMPTY_YIELD_TIME_MS = 5e3;
|
|
204
|
+
const MAX_YIELD_TIME_MS = 3e4;
|
|
205
|
+
const MAX_EMPTY_YIELD_TIME_MS = 3e5;
|
|
206
|
+
const DEFAULT_OUTPUT_TOKENS = 1e4;
|
|
207
|
+
const APPROX_BYTES_PER_TOKEN = 4;
|
|
186
208
|
function stateFor(agent) {
|
|
187
209
|
const current = STATES.get(agent);
|
|
188
210
|
if (current !== void 0) return current;
|
|
@@ -196,13 +218,13 @@ function stateFor(agent) {
|
|
|
196
218
|
function positiveFinite(name, value) {
|
|
197
219
|
if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new Error(`${name} must be a non-negative finite number`);
|
|
198
220
|
}
|
|
199
|
-
function
|
|
200
|
-
return Math.max(
|
|
221
|
+
function normalizeWaitMs(value, fallback, empty = false) {
|
|
222
|
+
return Math.max(empty ? MIN_EMPTY_YIELD_TIME_MS : MIN_YIELD_TIME_MS, Math.min(empty ? MAX_EMPTY_YIELD_TIME_MS : MAX_YIELD_TIME_MS, Math.trunc(value ?? fallback)));
|
|
201
223
|
}
|
|
202
224
|
function outputLimit(maxOutputBytes, maxOutputTokens) {
|
|
203
|
-
|
|
204
|
-
positiveFinite("max_output_tokens", maxOutputTokens);
|
|
205
|
-
return Math.max(1, Math.min(maxOutputBytes, Math.trunc(
|
|
225
|
+
const tokens = maxOutputTokens ?? DEFAULT_OUTPUT_TOKENS;
|
|
226
|
+
if (maxOutputTokens !== void 0) positiveFinite("max_output_tokens", maxOutputTokens);
|
|
227
|
+
return Math.max(1, Math.min(maxOutputBytes, Math.trunc(tokens * APPROX_BYTES_PER_TOKEN)));
|
|
206
228
|
}
|
|
207
229
|
function limitOutput(text, maxBytes) {
|
|
208
230
|
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
@@ -269,6 +291,12 @@ async function waitForShell(process, ms, signal) {
|
|
|
269
291
|
if (signal.aborted) signal.throwIfAborted();
|
|
270
292
|
return completed === true || process.status !== "running";
|
|
271
293
|
}
|
|
294
|
+
/** Stop a pipe-backed session when the current Codex tool is cancelled. */
|
|
295
|
+
async function stopShellOnAbort(process, signal) {
|
|
296
|
+
if (!signal.aborted || process.status !== "running") return;
|
|
297
|
+
process.kill();
|
|
298
|
+
await process.done;
|
|
299
|
+
}
|
|
272
300
|
function allocateSession(agent, session) {
|
|
273
301
|
const state = stateFor(agent);
|
|
274
302
|
const id = ++state.nextId;
|
|
@@ -304,21 +332,29 @@ async function runExecCommand(ctx, args, exec, config) {
|
|
|
304
332
|
...workdir === void 0 ? {} : { cwd: workdir }
|
|
305
333
|
};
|
|
306
334
|
const spawned = await terminals.spawn(agent, spawnRequest, exec.signal);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
335
|
+
let retained = false;
|
|
336
|
+
let closeReason = "Codex command failed";
|
|
337
|
+
try {
|
|
338
|
+
const sendRequest = {
|
|
339
|
+
text: commandFor(args),
|
|
340
|
+
submit: true,
|
|
341
|
+
waitMs: normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs),
|
|
342
|
+
signal: exec.signal
|
|
343
|
+
};
|
|
344
|
+
const result = await terminals.startSend(agent, spawned.sessionId, sendRequest).done;
|
|
345
|
+
const output = terminalResult(result, maxBytes, startedAt);
|
|
346
|
+
if (result.sessionStatus.kind === "running") {
|
|
347
|
+
output.session_id = allocateSession(agent, {
|
|
348
|
+
kind: "terminal",
|
|
349
|
+
owner: agent,
|
|
350
|
+
id: spawned.sessionId
|
|
351
|
+
});
|
|
352
|
+
retained = true;
|
|
353
|
+
} else closeReason = "Codex command exited";
|
|
354
|
+
return output;
|
|
355
|
+
} finally {
|
|
356
|
+
if (!retained) await terminals.kill(agent, spawned.sessionId, closeReason);
|
|
357
|
+
}
|
|
322
358
|
}
|
|
323
359
|
const policy = ctx.get("sandboxPolicy")?.resolve(exec.agent === void 0 ? {} : { session: exec.agent.session });
|
|
324
360
|
const dshEnv = ctx.get("shellEnv")?.collect(exec);
|
|
@@ -328,35 +364,40 @@ async function runExecCommand(ctx, args, exec, config) {
|
|
|
328
364
|
login: args.login ?? true,
|
|
329
365
|
...workdir === void 0 ? {} : { workdir },
|
|
330
366
|
stdoutMaxBytes: maxBytes,
|
|
367
|
+
signal: exec.signal,
|
|
331
368
|
...dshEnv === void 0 ? {} : { dshEnv },
|
|
332
369
|
...policy === void 0 ? {} : { sandboxPolicy: policy }
|
|
333
370
|
};
|
|
334
371
|
const process = ctx.shell.start(ctx.shell.resolve(shellRequest));
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
try {
|
|
373
|
+
await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs), exec.signal);
|
|
374
|
+
const output = readShellOutput(process.readOutput(), maxBytes);
|
|
375
|
+
if (process.status === "running") {
|
|
376
|
+
if (exec.agent === void 0) {
|
|
377
|
+
process.kill();
|
|
378
|
+
await process.done;
|
|
379
|
+
return withChunkId({
|
|
380
|
+
wall_time_seconds: (performance.now() - startedAt) / 1e3,
|
|
381
|
+
output: readShellOutput(process.readOutput(), maxBytes)
|
|
382
|
+
});
|
|
383
|
+
}
|
|
341
384
|
return withChunkId({
|
|
342
385
|
wall_time_seconds: (performance.now() - startedAt) / 1e3,
|
|
343
|
-
output
|
|
386
|
+
output,
|
|
387
|
+
session_id: allocateSession(exec.agent, {
|
|
388
|
+
kind: "shell",
|
|
389
|
+
process
|
|
390
|
+
})
|
|
344
391
|
});
|
|
345
392
|
}
|
|
346
393
|
return withChunkId({
|
|
347
394
|
wall_time_seconds: (performance.now() - startedAt) / 1e3,
|
|
348
395
|
output,
|
|
349
|
-
|
|
350
|
-
kind: "shell",
|
|
351
|
-
process
|
|
352
|
-
})
|
|
396
|
+
...process.exitCode === null ? {} : { exit_code: process.exitCode }
|
|
353
397
|
});
|
|
398
|
+
} finally {
|
|
399
|
+
await stopShellOnAbort(process, exec.signal);
|
|
354
400
|
}
|
|
355
|
-
return withChunkId({
|
|
356
|
-
wall_time_seconds: (performance.now() - startedAt) / 1e3,
|
|
357
|
-
output,
|
|
358
|
-
...process.exitCode === null ? {} : { exit_code: process.exitCode }
|
|
359
|
-
});
|
|
360
401
|
}
|
|
361
402
|
/** Poll or write to one session returned by {@link runExecCommand}. */
|
|
362
403
|
async function runWriteStdin(ctx, args, exec, config) {
|
|
@@ -370,38 +411,57 @@ async function runWriteStdin(ctx, args, exec, config) {
|
|
|
370
411
|
const chars = args.chars ?? "";
|
|
371
412
|
if (session.kind === "shell") {
|
|
372
413
|
if (chars.length > 0) throw new Error("this unified exec session uses pipes and does not accept stdin; rerun exec_command with tty=true");
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
414
|
+
try {
|
|
415
|
+
await waitForShell(session.process, normalizeWaitMs(args.yield_time_ms, config.pollYieldTimeMs, true), exec.signal);
|
|
416
|
+
const output = readShellOutput(session.process.readOutput(), maxBytes);
|
|
417
|
+
if (session.process.status !== "running") {
|
|
418
|
+
forgetSession(agent, args.session_id);
|
|
419
|
+
return withChunkId({
|
|
420
|
+
wall_time_seconds: (performance.now() - startedAt) / 1e3,
|
|
421
|
+
output,
|
|
422
|
+
...session.process.exitCode === null ? {} : { exit_code: session.process.exitCode }
|
|
423
|
+
});
|
|
424
|
+
}
|
|
377
425
|
return withChunkId({
|
|
378
426
|
wall_time_seconds: (performance.now() - startedAt) / 1e3,
|
|
379
427
|
output,
|
|
380
|
-
|
|
428
|
+
session_id: args.session_id
|
|
381
429
|
});
|
|
430
|
+
} finally {
|
|
431
|
+
if (exec.signal.aborted) try {
|
|
432
|
+
await stopShellOnAbort(session.process, exec.signal);
|
|
433
|
+
} finally {
|
|
434
|
+
forgetSession(agent, args.session_id);
|
|
435
|
+
}
|
|
382
436
|
}
|
|
383
|
-
return withChunkId({
|
|
384
|
-
wall_time_seconds: (performance.now() - startedAt) / 1e3,
|
|
385
|
-
output,
|
|
386
|
-
session_id: args.session_id
|
|
387
|
-
});
|
|
388
437
|
}
|
|
389
438
|
const terminals = ctx.get("terminals");
|
|
390
439
|
if (terminals === void 0) throw new Error("the dsh terminal capability is no longer available");
|
|
391
440
|
const sendRequest = {
|
|
392
441
|
text: chars,
|
|
393
442
|
submit: false,
|
|
394
|
-
waitMs:
|
|
443
|
+
waitMs: normalizeWaitMs(args.yield_time_ms, chars.length > 0 ? config.writeYieldTimeMs : config.pollYieldTimeMs, chars.length === 0),
|
|
395
444
|
signal: exec.signal
|
|
396
445
|
};
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
446
|
+
let retained = false;
|
|
447
|
+
let closeReason = "Codex command failed";
|
|
448
|
+
try {
|
|
449
|
+
const result = await terminals.startSend(agent, session.id, sendRequest).done;
|
|
450
|
+
const output = terminalResult(result, maxBytes, startedAt);
|
|
451
|
+
if (result.sessionStatus.kind === "running") {
|
|
452
|
+
output.session_id = args.session_id;
|
|
453
|
+
retained = true;
|
|
454
|
+
} else {
|
|
455
|
+
closeReason = "Codex command exited";
|
|
456
|
+
forgetSession(agent, args.session_id);
|
|
457
|
+
}
|
|
458
|
+
return output;
|
|
459
|
+
} finally {
|
|
460
|
+
if (!retained) {
|
|
461
|
+
forgetSession(agent, args.session_id);
|
|
462
|
+
await terminals.kill(agent, session.id, closeReason);
|
|
463
|
+
}
|
|
403
464
|
}
|
|
404
|
-
return output;
|
|
405
465
|
}
|
|
406
466
|
//#endregion
|
|
407
467
|
//#region lib/types/remote.js
|
|
@@ -707,6 +767,54 @@ function remoteCompactStream(ctx, options, next) {
|
|
|
707
767
|
})();
|
|
708
768
|
}
|
|
709
769
|
//#endregion
|
|
770
|
+
//#region lib/types/activity.js
|
|
771
|
+
/** Host-side activity projection for the Codex compaction status line. */
|
|
772
|
+
/** Persisted and wire shape of the Codex activity value. */
|
|
773
|
+
const codexActivitySchema = z$1.union([
|
|
774
|
+
z$1.object({
|
|
775
|
+
activity: z$1.literal("compaction"),
|
|
776
|
+
startedAt: z$1.number().int().nonnegative()
|
|
777
|
+
}),
|
|
778
|
+
z$1.object({
|
|
779
|
+
activity: z$1.literal("awaiting-model"),
|
|
780
|
+
startedAt: z$1.number().int().nonnegative()
|
|
781
|
+
}),
|
|
782
|
+
z$1.null()
|
|
783
|
+
]);
|
|
784
|
+
/** Pure event fold, exported so its reset and reference-stability rules can be tested directly. */
|
|
785
|
+
function applyCodexActivity(state, event) {
|
|
786
|
+
if (event.type === "compaction/start") return {
|
|
787
|
+
activity: "compaction",
|
|
788
|
+
startedAt: event.time
|
|
789
|
+
};
|
|
790
|
+
if (event.type === "step/start" || event.type === "llm/retry-started") return {
|
|
791
|
+
activity: "awaiting-model",
|
|
792
|
+
startedAt: event.time
|
|
793
|
+
};
|
|
794
|
+
if (event.type === "turn/start" || event.type === "turn/end" || event.type === "compaction/end") return null;
|
|
795
|
+
if (state?.activity === "awaiting-model" && (event.type === "llm/retry" || event.type === "assistant/chunk" || event.type === "assistant/message" || event.type === "step/end")) return null;
|
|
796
|
+
return state;
|
|
797
|
+
}
|
|
798
|
+
const codexActivityProjection = {
|
|
799
|
+
key: "codexActivity",
|
|
800
|
+
stateSchema: codexActivitySchema,
|
|
801
|
+
init: () => null,
|
|
802
|
+
apply: applyCodexActivity,
|
|
803
|
+
wire: {
|
|
804
|
+
viewSchema: codexActivitySchema,
|
|
805
|
+
view: (state) => state
|
|
806
|
+
},
|
|
807
|
+
schema: codexActivitySchema,
|
|
808
|
+
view: (state) => state,
|
|
809
|
+
stateVersion: 1
|
|
810
|
+
};
|
|
811
|
+
/** Register the optional activity unit without requiring the projection seam in every assembly. */
|
|
812
|
+
function registerCodexActivityProjection(ctx) {
|
|
813
|
+
ctx.inject(["sessionProjections"], (projectionCtx) => {
|
|
814
|
+
projectionCtx.sessionProjections.register(codexActivityProjection);
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
//#endregion
|
|
710
818
|
//#region lib/types/index.js
|
|
711
819
|
/** Codex-compatible prompt overlay and core tools for a dsh agent preset. */
|
|
712
820
|
const name = "codex";
|
|
@@ -882,8 +990,7 @@ const CODEX_BASE_PROMPT = String.raw`You are Codex, based on {{model}}. You are
|
|
|
882
990
|
`;
|
|
883
991
|
const CODEX_COLLABORATION_PROMPT = String.raw`## Collaboration
|
|
884
992
|
|
|
885
|
-
- Ask, align, and clarify
|
|
886
|
-
- Understand the user's full picture, align it with your own, and leave no hidden assumptions or gaps.
|
|
993
|
+
- Ask, align, and clarify first. Try your best to understand the user's full picture.
|
|
887
994
|
- Keep only the essential logic and core actions. There's no need to explain or test what was removed or why something wasn't done.
|
|
888
995
|
- Convey enough valuable information with as few words as possible. Stay focused on the end goal.
|
|
889
996
|
- Solve problems by thinking from first principles and at a higher level.
|
|
@@ -1267,6 +1374,7 @@ function apply(ctx, config = {}) {
|
|
|
1267
1374
|
};
|
|
1268
1375
|
if (ctx.fs.sandboxMode !== void 0 && ctx.get("sandboxPolicy") === void 0) throw new Error("codex: a sandboxing filesystem requires ctx.sandboxPolicy");
|
|
1269
1376
|
watchConfiguredGptModels(ctx);
|
|
1377
|
+
registerCodexActivityProjection(ctx);
|
|
1270
1378
|
const codexSettings = installCodexSettings(ctx);
|
|
1271
1379
|
ctx.on("agent/request", async (_payload, next) => {
|
|
1272
1380
|
return applyCodexRequestSettings(await next(), codexSettings.current());
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Host-side activity projection for the Codex compaction status line. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import { z, type ZodType } from 'zod';
|
|
4
|
+
import type { CodexActivity } from './activity-types.ts';
|
|
5
|
+
export type { CodexActivity } from './activity-types.ts';
|
|
6
|
+
/** The only long-running Codex activity currently surfaced to the client. */
|
|
7
|
+
export type CodexActivityState = CodexActivity | null;
|
|
8
|
+
/** Minimal event shape needed by this projection; no compaction package import is required. */
|
|
9
|
+
export interface CodexActivityEvent {
|
|
10
|
+
type: string;
|
|
11
|
+
time: number;
|
|
12
|
+
}
|
|
13
|
+
/** Persisted and wire shape of the Codex activity value. */
|
|
14
|
+
export declare const codexActivitySchema: ZodType<CodexActivityState>;
|
|
15
|
+
/** Pure event fold, exported so its reset and reference-stability rules can be tested directly. */
|
|
16
|
+
export declare function applyCodexActivity(state: CodexActivityState, event: CodexActivityEvent): CodexActivityState;
|
|
17
|
+
export declare const codexActivityProjection: {
|
|
18
|
+
readonly key: "codexActivity";
|
|
19
|
+
readonly stateSchema: z.ZodType<CodexActivityState, unknown, z.core.$ZodTypeInternals<CodexActivityState, unknown>>;
|
|
20
|
+
readonly init: () => CodexActivityState;
|
|
21
|
+
readonly apply: typeof applyCodexActivity;
|
|
22
|
+
readonly wire: {
|
|
23
|
+
readonly viewSchema: z.ZodType<CodexActivityState, unknown, z.core.$ZodTypeInternals<CodexActivityState, unknown>>;
|
|
24
|
+
readonly view: (state: CodexActivityState) => CodexActivityState;
|
|
25
|
+
};
|
|
26
|
+
readonly schema: z.ZodType<CodexActivityState, unknown, z.core.$ZodTypeInternals<CodexActivityState, unknown>>;
|
|
27
|
+
readonly view: (state: CodexActivityState) => CodexActivityState;
|
|
28
|
+
readonly stateVersion: 1;
|
|
29
|
+
};
|
|
30
|
+
/** Register the optional activity unit without requiring the projection seam in every assembly. */
|
|
31
|
+
export declare function registerCodexActivityProjection(ctx: Context): void;
|
|
32
|
+
//# sourceMappingURL=activity.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Host-side activity projection for the Codex compaction status line. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/** Persisted and wire shape of the Codex activity value. */
|
|
4
|
+
export const codexActivitySchema = z.union([
|
|
5
|
+
z.object({
|
|
6
|
+
activity: z.literal('compaction'),
|
|
7
|
+
startedAt: z.number().int().nonnegative(),
|
|
8
|
+
}),
|
|
9
|
+
z.object({
|
|
10
|
+
activity: z.literal('awaiting-model'),
|
|
11
|
+
startedAt: z.number().int().nonnegative(),
|
|
12
|
+
}),
|
|
13
|
+
z.null(),
|
|
14
|
+
]);
|
|
15
|
+
/** Pure event fold, exported so its reset and reference-stability rules can be tested directly. */
|
|
16
|
+
export function applyCodexActivity(state, event) {
|
|
17
|
+
if (event.type === 'compaction/start')
|
|
18
|
+
return { activity: 'compaction', startedAt: event.time };
|
|
19
|
+
if (event.type === 'step/start' || event.type === 'llm/retry-started') {
|
|
20
|
+
return { activity: 'awaiting-model', startedAt: event.time };
|
|
21
|
+
}
|
|
22
|
+
if (event.type === 'turn/start' || event.type === 'turn/end' || event.type === 'compaction/end')
|
|
23
|
+
return null;
|
|
24
|
+
if (state?.activity === 'awaiting-model'
|
|
25
|
+
&& (event.type === 'llm/retry'
|
|
26
|
+
|| event.type === 'assistant/chunk'
|
|
27
|
+
|| event.type === 'assistant/message'
|
|
28
|
+
|| event.type === 'step/end'))
|
|
29
|
+
return null;
|
|
30
|
+
return state;
|
|
31
|
+
}
|
|
32
|
+
export const codexActivityProjection = {
|
|
33
|
+
key: 'codexActivity',
|
|
34
|
+
stateSchema: codexActivitySchema,
|
|
35
|
+
init: () => null,
|
|
36
|
+
apply: applyCodexActivity,
|
|
37
|
+
wire: {
|
|
38
|
+
viewSchema: codexActivitySchema,
|
|
39
|
+
view: (state) => state,
|
|
40
|
+
},
|
|
41
|
+
// Keep the old projection contract working for DSH bundles that predate
|
|
42
|
+
// the `stateSchema`/`wire` split.
|
|
43
|
+
schema: codexActivitySchema,
|
|
44
|
+
view: (state) => state,
|
|
45
|
+
stateVersion: 1,
|
|
46
|
+
};
|
|
47
|
+
/** Register the optional activity unit without requiring the projection seam in every assembly. */
|
|
48
|
+
export function registerCodexActivityProjection(ctx) {
|
|
49
|
+
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
|
50
|
+
projectionCtx.sessionProjections.register(codexActivityProjection);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=activity.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Browser controls for the Codex request settings mounted by the Host plugin. */
|
|
2
|
+
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
3
|
import type { Context } from '@deepseek-ai/cordis';
|
|
3
4
|
declare const en: {
|
|
4
5
|
readonly fast: "Fast mode";
|
|
@@ -9,6 +10,8 @@ declare const en: {
|
|
|
9
10
|
readonly contextSize: "Context size";
|
|
10
11
|
readonly contextSizeDescription: "Next request capacity in K tokens. The meter above is current.";
|
|
11
12
|
readonly contextRestore: "Restore model default";
|
|
13
|
+
readonly activityCompacting: "Compacting context...";
|
|
14
|
+
readonly activityAwaitingModel: "Waiting for model response...";
|
|
12
15
|
};
|
|
13
16
|
type CodexKey = keyof typeof en;
|
|
14
17
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
@@ -29,6 +32,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
29
32
|
};
|
|
30
33
|
}
|
|
31
34
|
}
|
|
35
|
+
type ActivityProps = PropsRuntime<'conversation.input.dock'> & PropsLocale<'codex'>;
|
|
36
|
+
export declare function ActivityLine({ sessionId, useSessions, useProjection, t }: ActivityProps): import("react").JSX.Element | null;
|
|
32
37
|
export declare const inject: string[];
|
|
33
38
|
/** Mount Fast inside the model selector and context size inside the meter panel. */
|
|
34
39
|
export declare function apply(ctx: Context): void;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Browser controls for the Codex request settings mounted by the Host plugin. */
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
2
4
|
import { CODEX_CONTEXT_MAX, CODEX_CONTEXT_UNIT, CODEX_PRESET_ID } from "../context.js";
|
|
3
5
|
const NS = 'codex';
|
|
4
6
|
const SETTINGS_NAMESPACE = 'codex';
|
|
@@ -11,6 +13,8 @@ const en = {
|
|
|
11
13
|
contextSize: 'Context size',
|
|
12
14
|
contextSizeDescription: 'Next request capacity in K tokens. The meter above is current.',
|
|
13
15
|
contextRestore: 'Restore model default',
|
|
16
|
+
activityCompacting: 'Compacting context...',
|
|
17
|
+
activityAwaitingModel: 'Waiting for model response...',
|
|
14
18
|
};
|
|
15
19
|
const zh = {
|
|
16
20
|
fast: 'Fast',
|
|
@@ -21,6 +25,8 @@ const zh = {
|
|
|
21
25
|
contextSize: '上下文大小',
|
|
22
26
|
contextSizeDescription: '设置下次请求的上下文容量,单位为 K tokens;上方是当前请求。',
|
|
23
27
|
contextRestore: '恢复模型默认值',
|
|
28
|
+
activityCompacting: '正在压缩上下文...',
|
|
29
|
+
activityAwaitingModel: '正在等待模型响应...',
|
|
24
30
|
};
|
|
25
31
|
function FastModeButton({ sessionId, useSessions, useSettings, setSetting, t }) {
|
|
26
32
|
const agentPreset = useSessions(state => state.byId[sessionId]?.agentPreset);
|
|
@@ -83,6 +89,44 @@ function ContextSizeControl({ contextWindow, useSettings, setSetting, unsetSetti
|
|
|
83
89
|
fontSize: 11,
|
|
84
90
|
}, children: t('contextRestore') }))] }));
|
|
85
91
|
}
|
|
92
|
+
function elapsedSeconds(startedAt, now) {
|
|
93
|
+
return Math.max(0, Math.floor((now - startedAt) / 1_000));
|
|
94
|
+
}
|
|
95
|
+
export function ActivityLine({ sessionId, useSessions, useProjection, t }) {
|
|
96
|
+
const agentPreset = useSessions(state => state.byId[sessionId]?.agentPreset);
|
|
97
|
+
// Keep the projection seam optional: this package must still load when the
|
|
98
|
+
// host has no session-projection registry or has not carried this key.
|
|
99
|
+
const activity = useProjection('codexActivity');
|
|
100
|
+
const [now, setNow] = useState(() => Date.now());
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (agentPreset !== CODEX_PRESET_ID || activity === undefined || activity === null)
|
|
103
|
+
return undefined;
|
|
104
|
+
setNow(Date.now());
|
|
105
|
+
const timer = setInterval(() => { setNow(Date.now()); }, 1_000);
|
|
106
|
+
return () => clearInterval(timer);
|
|
107
|
+
}, [agentPreset, activity?.startedAt]);
|
|
108
|
+
if (agentPreset !== CODEX_PRESET_ID || activity === undefined || activity === null)
|
|
109
|
+
return null;
|
|
110
|
+
const label = activity.activity === 'awaiting-model'
|
|
111
|
+
? t('activityAwaitingModel')
|
|
112
|
+
: t('activityCompacting');
|
|
113
|
+
return (_jsxs("div", { id: "codex-activity", role: "status", "aria-live": "polite", style: {
|
|
114
|
+
display: 'flex',
|
|
115
|
+
alignItems: 'center',
|
|
116
|
+
gap: 7,
|
|
117
|
+
minHeight: 28,
|
|
118
|
+
padding: '4px 10px',
|
|
119
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
120
|
+
fontSize: 12,
|
|
121
|
+
lineHeight: '20px',
|
|
122
|
+
}, children: [_jsx("span", { "aria-hidden": "true", style: {
|
|
123
|
+
width: 7,
|
|
124
|
+
height: 7,
|
|
125
|
+
flex: '0 0 auto',
|
|
126
|
+
borderRadius: '50%',
|
|
127
|
+
background: 'var(--dsw-static-blue-500)',
|
|
128
|
+
} }), _jsx("span", { children: label }), _jsxs("span", { style: { color: 'var(--dsw-alias-label-tertiary)' }, children: ["\u00B7 ", elapsedSeconds(activity.startedAt, now), "s"] })] }));
|
|
129
|
+
}
|
|
86
130
|
export const inject = ['slots', 'locale', 'settingsScope'];
|
|
87
131
|
/** Mount Fast inside the model selector and context size inside the meter panel. */
|
|
88
132
|
export function apply(ctx) {
|
|
@@ -107,6 +151,12 @@ export function apply(ctx) {
|
|
|
107
151
|
locale: NS,
|
|
108
152
|
inject: injected,
|
|
109
153
|
}, ContextSizeControl));
|
|
154
|
+
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
155
|
+
name: 'conversation.input.dock',
|
|
156
|
+
id: 'codex-activity',
|
|
157
|
+
order: 5,
|
|
158
|
+
locale: NS,
|
|
159
|
+
}, ActivityLine));
|
|
110
160
|
}
|
|
111
161
|
export default { inject, apply };
|
|
112
162
|
//# sourceMappingURL=index.js.map
|
package/lib/types/exec.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface ExecResult {
|
|
|
27
27
|
exit_code?: number;
|
|
28
28
|
original_token_count?: number;
|
|
29
29
|
}
|
|
30
|
+
export declare function normalizeWaitMs(value: number | undefined, fallback: number, empty?: boolean): number;
|
|
30
31
|
/** Render the ordinary Responses tool result text used by Codex's unified exec tools. */
|
|
31
32
|
export declare function renderExecResult(result: ExecResult): string;
|
|
32
33
|
/** Execute one Codex command through the configured pipe or PTY capability. */
|
package/lib/types/exec.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { resolve as resolvePath } from 'node:path';
|
|
4
4
|
const STATES = new WeakMap();
|
|
5
|
+
const MIN_YIELD_TIME_MS = 250;
|
|
6
|
+
const MIN_EMPTY_YIELD_TIME_MS = 5_000;
|
|
7
|
+
const MAX_YIELD_TIME_MS = 30_000;
|
|
8
|
+
const MAX_EMPTY_YIELD_TIME_MS = 300_000;
|
|
9
|
+
const DEFAULT_OUTPUT_TOKENS = 10_000;
|
|
10
|
+
const APPROX_BYTES_PER_TOKEN = 4;
|
|
5
11
|
function stateFor(agent) {
|
|
6
12
|
const current = STATES.get(agent);
|
|
7
13
|
if (current !== undefined)
|
|
@@ -15,14 +21,16 @@ function positiveFinite(name, value) {
|
|
|
15
21
|
throw new Error(`${name} must be a non-negative finite number`);
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
|
-
function
|
|
19
|
-
|
|
24
|
+
export function normalizeWaitMs(value, fallback, empty = false) {
|
|
25
|
+
const lower = empty ? MIN_EMPTY_YIELD_TIME_MS : MIN_YIELD_TIME_MS;
|
|
26
|
+
const upper = empty ? MAX_EMPTY_YIELD_TIME_MS : MAX_YIELD_TIME_MS;
|
|
27
|
+
return Math.max(lower, Math.min(upper, Math.trunc(value ?? fallback)));
|
|
20
28
|
}
|
|
21
29
|
function outputLimit(maxOutputBytes, maxOutputTokens) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return Math.max(1, Math.min(maxOutputBytes, Math.trunc(
|
|
30
|
+
const tokens = maxOutputTokens ?? DEFAULT_OUTPUT_TOKENS;
|
|
31
|
+
if (maxOutputTokens !== undefined)
|
|
32
|
+
positiveFinite('max_output_tokens', maxOutputTokens);
|
|
33
|
+
return Math.max(1, Math.min(maxOutputBytes, Math.trunc(tokens * APPROX_BYTES_PER_TOKEN)));
|
|
26
34
|
}
|
|
27
35
|
function limitOutput(text, maxBytes) {
|
|
28
36
|
if (Buffer.byteLength(text, 'utf8') <= maxBytes)
|
|
@@ -100,6 +108,13 @@ async function waitForShell(process, ms, signal) {
|
|
|
100
108
|
signal.throwIfAborted();
|
|
101
109
|
return completed === true || process.status !== 'running';
|
|
102
110
|
}
|
|
111
|
+
/** Stop a pipe-backed session when the current Codex tool is cancelled. */
|
|
112
|
+
async function stopShellOnAbort(process, signal) {
|
|
113
|
+
if (!signal.aborted || process.status !== 'running')
|
|
114
|
+
return;
|
|
115
|
+
process.kill();
|
|
116
|
+
await process.done;
|
|
117
|
+
}
|
|
103
118
|
function allocateSession(agent, session) {
|
|
104
119
|
const state = stateFor(agent);
|
|
105
120
|
const id = ++state.nextId;
|
|
@@ -142,22 +157,33 @@ export async function runExecCommand(ctx, args, exec, config) {
|
|
|
142
157
|
...workdir === undefined ? {} : { cwd: workdir },
|
|
143
158
|
};
|
|
144
159
|
const spawned = await terminals.spawn(agent, spawnRequest, exec.signal);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
let retained = false;
|
|
161
|
+
let closeReason = 'Codex command failed';
|
|
162
|
+
try {
|
|
163
|
+
const sendRequest = {
|
|
164
|
+
text: commandFor(args),
|
|
165
|
+
submit: true,
|
|
166
|
+
waitMs: normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs),
|
|
167
|
+
signal: exec.signal,
|
|
168
|
+
};
|
|
169
|
+
const operation = terminals.startSend(agent, spawned.sessionId, sendRequest);
|
|
170
|
+
const result = await operation.done;
|
|
171
|
+
const output = terminalResult(result, maxBytes, startedAt);
|
|
172
|
+
if (result.sessionStatus.kind === 'running') {
|
|
173
|
+
output.session_id = allocateSession(agent, { kind: 'terminal', owner: agent, id: spawned.sessionId });
|
|
174
|
+
retained = true;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
closeReason = 'Codex command exited';
|
|
178
|
+
}
|
|
179
|
+
return output;
|
|
156
180
|
}
|
|
157
|
-
|
|
158
|
-
|
|
181
|
+
finally {
|
|
182
|
+
// A PTY is published by spawn before the first send starts. Do not leave
|
|
183
|
+
// that session behind when send setup or its foreground operation fails.
|
|
184
|
+
if (!retained)
|
|
185
|
+
await terminals.kill(agent, spawned.sessionId, closeReason);
|
|
159
186
|
}
|
|
160
|
-
return output;
|
|
161
187
|
}
|
|
162
188
|
const policy = ctx.get('sandboxPolicy')?.resolve(exec.agent === undefined ? {} : { session: exec.agent.session });
|
|
163
189
|
const dshEnv = ctx.get('shellEnv')?.collect(exec);
|
|
@@ -167,32 +193,38 @@ export async function runExecCommand(ctx, args, exec, config) {
|
|
|
167
193
|
login: args.login ?? true,
|
|
168
194
|
...workdir === undefined ? {} : { workdir },
|
|
169
195
|
stdoutMaxBytes: maxBytes,
|
|
196
|
+
signal: exec.signal,
|
|
170
197
|
...dshEnv === undefined ? {} : { dshEnv },
|
|
171
198
|
...policy === undefined ? {} : { sandboxPolicy: policy },
|
|
172
199
|
};
|
|
173
200
|
const process = ctx.shell.start(ctx.shell.resolve(shellRequest));
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
try {
|
|
202
|
+
await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs), exec.signal);
|
|
203
|
+
const output = readShellOutput(process.readOutput(), maxBytes);
|
|
204
|
+
if (process.status === 'running') {
|
|
205
|
+
if (exec.agent === undefined) {
|
|
206
|
+
process.kill();
|
|
207
|
+
await process.done;
|
|
208
|
+
return withChunkId({
|
|
209
|
+
wall_time_seconds: (performance.now() - startedAt) / 1000,
|
|
210
|
+
output: readShellOutput(process.readOutput(), maxBytes),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
180
213
|
return withChunkId({
|
|
181
214
|
wall_time_seconds: (performance.now() - startedAt) / 1000,
|
|
182
|
-
output
|
|
215
|
+
output,
|
|
216
|
+
session_id: allocateSession(exec.agent, { kind: 'shell', process }),
|
|
183
217
|
});
|
|
184
218
|
}
|
|
185
219
|
return withChunkId({
|
|
186
220
|
wall_time_seconds: (performance.now() - startedAt) / 1000,
|
|
187
221
|
output,
|
|
188
|
-
|
|
222
|
+
...process.exitCode === null ? {} : { exit_code: process.exitCode },
|
|
189
223
|
});
|
|
190
224
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
...process.exitCode === null ? {} : { exit_code: process.exitCode },
|
|
195
|
-
});
|
|
225
|
+
finally {
|
|
226
|
+
await stopShellOnAbort(process, exec.signal);
|
|
227
|
+
}
|
|
196
228
|
}
|
|
197
229
|
/** Poll or write to one session returned by {@link runExecCommand}. */
|
|
198
230
|
export async function runWriteStdin(ctx, args, exec, config) {
|
|
@@ -211,21 +243,33 @@ export async function runWriteStdin(ctx, args, exec, config) {
|
|
|
211
243
|
if (chars.length > 0) {
|
|
212
244
|
throw new Error('this unified exec session uses pipes and does not accept stdin; rerun exec_command with tty=true');
|
|
213
245
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
246
|
+
try {
|
|
247
|
+
await waitForShell(session.process, normalizeWaitMs(args.yield_time_ms, config.pollYieldTimeMs, true), exec.signal);
|
|
248
|
+
const output = readShellOutput(session.process.readOutput(), maxBytes);
|
|
249
|
+
if (session.process.status !== 'running') {
|
|
250
|
+
forgetSession(agent, args.session_id);
|
|
251
|
+
return withChunkId({
|
|
252
|
+
wall_time_seconds: (performance.now() - startedAt) / 1000,
|
|
253
|
+
output,
|
|
254
|
+
...session.process.exitCode === null ? {} : { exit_code: session.process.exitCode },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
218
257
|
return withChunkId({
|
|
219
258
|
wall_time_seconds: (performance.now() - startedAt) / 1000,
|
|
220
259
|
output,
|
|
221
|
-
|
|
260
|
+
session_id: args.session_id,
|
|
222
261
|
});
|
|
223
262
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
263
|
+
finally {
|
|
264
|
+
if (exec.signal.aborted) {
|
|
265
|
+
try {
|
|
266
|
+
await stopShellOnAbort(session.process, exec.signal);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
forgetSession(agent, args.session_id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
229
273
|
}
|
|
230
274
|
const terminals = ctx.get('terminals');
|
|
231
275
|
if (terminals === undefined)
|
|
@@ -233,19 +277,33 @@ export async function runWriteStdin(ctx, args, exec, config) {
|
|
|
233
277
|
const sendRequest = {
|
|
234
278
|
text: chars,
|
|
235
279
|
submit: false,
|
|
236
|
-
waitMs:
|
|
280
|
+
waitMs: normalizeWaitMs(args.yield_time_ms, chars.length > 0 ? config.writeYieldTimeMs : config.pollYieldTimeMs, chars.length === 0),
|
|
237
281
|
signal: exec.signal,
|
|
238
282
|
};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
283
|
+
let retained = false;
|
|
284
|
+
let closeReason = 'Codex command failed';
|
|
285
|
+
try {
|
|
286
|
+
const operation = terminals.startSend(agent, session.id, sendRequest);
|
|
287
|
+
const result = await operation.done;
|
|
288
|
+
const output = terminalResult(result, maxBytes, startedAt);
|
|
289
|
+
if (result.sessionStatus.kind === 'running') {
|
|
290
|
+
output.session_id = args.session_id;
|
|
291
|
+
retained = true;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
closeReason = 'Codex command exited';
|
|
295
|
+
forgetSession(agent, args.session_id);
|
|
296
|
+
}
|
|
297
|
+
return output;
|
|
244
298
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
299
|
+
finally {
|
|
300
|
+
// A failed send must not strand either side of the two-level session
|
|
301
|
+
// registry: the local numeric id and the underlying PTY must be retired
|
|
302
|
+
// together. A successful running result is the only retained case.
|
|
303
|
+
if (!retained) {
|
|
304
|
+
forgetSession(agent, args.session_id);
|
|
305
|
+
await terminals.kill(agent, session.id, closeReason);
|
|
306
|
+
}
|
|
248
307
|
}
|
|
249
|
-
return output;
|
|
250
308
|
}
|
|
251
309
|
//# sourceMappingURL=exec.js.map
|
package/lib/types/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { CODEX_CONTEXT_MAX } from "./context.js";
|
|
|
6
6
|
import { applyPatchHunks, parsePatch } from "./patch.js";
|
|
7
7
|
import { renderExecResult, runExecCommand, runWriteStdin } from "./exec.js";
|
|
8
8
|
import { hostedWebSearchStream, installHostedWebSearch, remoteCompactStream } from "./remote.js";
|
|
9
|
+
import { registerCodexActivityProjection } from "./activity.js";
|
|
9
10
|
export const name = 'codex';
|
|
10
11
|
export const inject = ['tools', 'systemPrompt', 'shell', 'fs', 'llm', 'credentials', 'settings'];
|
|
11
12
|
/** Runtime configuration schema for the Codex tool bridge. */
|
|
@@ -178,8 +179,7 @@ const CODEX_BASE_PROMPT = String.raw `You are Codex, based on {{model}}. You are
|
|
|
178
179
|
`;
|
|
179
180
|
const CODEX_COLLABORATION_PROMPT = String.raw `## Collaboration
|
|
180
181
|
|
|
181
|
-
- Ask, align, and clarify
|
|
182
|
-
- Understand the user's full picture, align it with your own, and leave no hidden assumptions or gaps.
|
|
182
|
+
- Ask, align, and clarify first. Try your best to understand the user's full picture.
|
|
183
183
|
- Keep only the essential logic and core actions. There's no need to explain or test what was removed or why something wasn't done.
|
|
184
184
|
- Convey enough valuable information with as few words as possible. Stay focused on the end goal.
|
|
185
185
|
- Solve problems by thinking from first principles and at a higher level.
|
|
@@ -459,6 +459,7 @@ export function apply(ctx, config = {}) {
|
|
|
459
459
|
// provider routes. Codex adds only scoped transport behavior and capability
|
|
460
460
|
// metadata; failed remote operations continue through the generic path.
|
|
461
461
|
watchConfiguredGptModels(ctx);
|
|
462
|
+
registerCodexActivityProjection(ctx);
|
|
462
463
|
const codexSettings = installCodexSettings(ctx);
|
|
463
464
|
// Keep these controls in the request config rather than mutating provider
|
|
464
465
|
// settings. That makes a change apply to the next step without rebuilding
|
package/lib/types/patch.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/** Parser and line-oriented applicator for Codex's `apply_patch` language. */
|
|
2
2
|
/** The grammar sent to providers that support OpenAI custom grammar tools. */
|
|
3
|
-
export const APPLY_PATCH_GRAMMAR = String.raw `start: begin_patch hunk+ end_patch
|
|
4
|
-
begin_patch: "*** Begin Patch" LF
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
export const APPLY_PATCH_GRAMMAR = String.raw `start: begin_patch environment_id? hunk+ end_patch
|
|
4
|
+
begin_patch: "*** Begin Patch" LF
|
|
5
|
+
environment_id: "*** Environment ID: " filename LF
|
|
6
|
+
end_patch: "*** End Patch" LF?
|
|
7
|
+
hunk: add_hunk | delete_hunk | update_hunk
|
|
8
|
+
add_hunk: "*** Add File: " filename LF add_line+
|
|
9
|
+
delete_hunk: "*** Delete File: " filename LF
|
|
10
|
+
update_hunk: "*** Update File: " filename LF change_move? change?
|
|
11
|
+
filename: /(.+)/
|
|
12
|
+
add_line: "+" /(.*)/ LF -> line
|
|
13
|
+
change_move: "*** Move to: " filename LF
|
|
14
|
+
change: (change_context | change_line)+ eof_line?
|
|
15
|
+
change_context: ("@@" | "@@ " /(.+)/) LF
|
|
16
|
+
change_line: ("+" | "-" | " ") /(.*)/ LF
|
|
17
|
+
eof_line: "*** End of File" LF
|
|
18
|
+
%import common.LF
|
|
18
19
|
`;
|
|
19
20
|
function invalid(message) {
|
|
20
21
|
throw new Error(`apply_patch: ${message}`);
|
|
@@ -25,6 +26,19 @@ function isFileHeader(line) {
|
|
|
25
26
|
|| trimmed.startsWith('*** Delete File: ')
|
|
26
27
|
|| trimmed.startsWith('*** Update File: ');
|
|
27
28
|
}
|
|
29
|
+
const ENVIRONMENT_ID_MARKER = '*** Environment ID:';
|
|
30
|
+
function unwrapHeredoc(input) {
|
|
31
|
+
const normalized = input.replaceAll('\r\n', '\n').trim();
|
|
32
|
+
const lines = normalized.split('\n');
|
|
33
|
+
const first = lines[0]?.trim();
|
|
34
|
+
const last = lines.at(-1)?.trim();
|
|
35
|
+
if (lines.length >= 4
|
|
36
|
+
&& (first === '<<EOF' || first === "<<'EOF'" || first === '<<"EOF"')
|
|
37
|
+
&& last === 'EOF') {
|
|
38
|
+
return lines.slice(1, -1).join('\n').trim();
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
28
42
|
function pathFrom(line, prefix) {
|
|
29
43
|
const path = line.slice(prefix.length).trim();
|
|
30
44
|
if (path.length === 0)
|
|
@@ -40,7 +54,7 @@ function isPotentialChangeMarker(line) {
|
|
|
40
54
|
}
|
|
41
55
|
/** Parse one complete Codex patch after normalizing CRLF input to LF. */
|
|
42
56
|
export function parsePatch(input) {
|
|
43
|
-
const lines = input
|
|
57
|
+
const lines = unwrapHeredoc(input).split('\n');
|
|
44
58
|
if (lines[0]?.trim() !== '*** Begin Patch')
|
|
45
59
|
invalid('input must start with "*** Begin Patch"');
|
|
46
60
|
if (lines.at(-1)?.trim() !== '*** End Patch')
|
|
@@ -48,6 +62,15 @@ export function parsePatch(input) {
|
|
|
48
62
|
const files = [];
|
|
49
63
|
let index = 1;
|
|
50
64
|
const end = lines.length - 1;
|
|
65
|
+
if (lines[index]?.trim().startsWith(ENVIRONMENT_ID_MARKER) === true) {
|
|
66
|
+
const environmentId = lines[index].trim().slice(ENVIRONMENT_ID_MARKER.length).trim();
|
|
67
|
+
if (environmentId.length === 0)
|
|
68
|
+
invalid('environment id cannot be empty');
|
|
69
|
+
index++;
|
|
70
|
+
if (lines[index]?.trim().startsWith(ENVIRONMENT_ID_MARKER) === true) {
|
|
71
|
+
invalid('environment id cannot be specified more than once');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
51
74
|
while (index < end) {
|
|
52
75
|
const header = lines[index++]?.trim();
|
|
53
76
|
if (header === undefined)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shuind/dsh-codex-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "A minimal Codex harness for GPT models that do not fit DSH's native interface, while remaining compatible with the DSH plugin ecosystem.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|