@shuind/dsh-codex-harness 0.1.21 → 0.1.23
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 +11 -3
- package/lib/client.js +33 -8
- package/lib/index.js +193 -11
- package/lib/types/client/index.d.ts +2 -1
- package/lib/types/client/index.js +34 -10
- package/lib/types/exec.d.ts +2 -0
- package/lib/types/exec.js +53 -2
- package/lib/types/index.js +17 -17
- package/lib/types/patch.d.ts +1 -0
- package/lib/types/patch.js +1 -0
- package/lib/types/remote.d.ts +6 -0
- package/lib/types/remote.js +163 -3
- package/package.json +11 -1
- package/presets/codex/agent.cordis.yml +8 -0
- package/presets/codex/preset.yml +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
- **精简 Codex coding-agent 提示词**:使用精简版系统提示词;身份、模型和工作目录由 `dsh-persona` 提供,避免重复的 Harness 身份说明,同时保留必要的工具契约和编码工作流。
|
|
8
8
|
- **Codex 工具**:`exec_command`、`write_stdin`、`apply_patch`、`update_plan`;`apply_patch` 匹配失败时会显示文件、hunk、行号、可见空白和附近文件内容,便于修正上下文。
|
|
9
|
+
- **后台任务**:`exec_command` 支持 `run_in_background: true`,立即返回 DSH `job_id`;使用 `job_output` 读取输出、`job_kill` 停止任务。任务完成时,忙碌中的 agent 会在下一步收到 inbox 注入;空闲 agent 默认保持安静,等下一次唤醒时再处理完成消息。
|
|
9
10
|
- **GPT 能力补全**:为 GPT 系列模型补充图片输入和思考强度选项;不会覆盖用户已有的显式配置。
|
|
10
11
|
- **Fast**:仅在 Codex preset 的模型选择菜单中显示,开启后向 Responses 请求发送 `service_tier: "priority"`。
|
|
11
12
|
- **旧版 Web 兼容**:在未提供新版模型设置/上下文设置 slot 的 DSH Web 中,Fast 和上下文容量会自动回退到旧版 composer slot;client 注入不依赖纯类型 slot 包。
|
|
13
|
+
- **Responses 原生 apply_patch**:在 GPT Responses 请求的插件传输边界,把普通 JSON function tool 改写为 `type: "custom"` + OpenAI `lark` grammar;下一轮历史同步改写为 `custom_tool_call`。如果中转站拒绝 custom tool,则自动回退到普通 function tool,不改变 DSH 工具执行器。
|
|
12
14
|
- **远程搜索与压缩**:OpenAI Responses 请求默认优先使用 hosted `web_search` 和 `/responses/compact`;失败时回退到 DSH 的本地实现。
|
|
13
15
|
- **上下文容量**:在 Web 中设置 `1K`–`1000K` tokens,设置值作用于下一次请求。
|
|
14
16
|
- **活动状态**:Codex 等待模型响应或进行上下文压缩时,在输入框上方显示对应状态和耗时,让长时间 Deep Diving 不再像卡住。
|
|
@@ -16,7 +18,7 @@
|
|
|
16
18
|
## 安装
|
|
17
19
|
|
|
18
20
|
```sh
|
|
19
|
-
dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.
|
|
21
|
+
dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.23
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
重启 Web,创建新会话,在模式菜单中选择 **Codex 模式**。
|
|
@@ -55,7 +57,7 @@ Fast 设置保存在 `codex` 命名空间,只对 Codex preset 生效。
|
|
|
55
57
|
|
|
56
58
|
### 活动状态
|
|
57
59
|
|
|
58
|
-
Codex 等待模型首个输出时,Web
|
|
60
|
+
Codex 等待模型首个输出时,Web 会在 `Deep diving...` 右侧显示 `正在等待模型响应... · Ns`;开始上下文压缩后会切换为 `正在压缩上下文... · Ns`。收到模型输出、压缩结束或下一轮开始后自动隐藏。该状态由 Host 的 `sessionProjections` 投影驱动,因此本地压缩和远程 `/responses/compact` 都会显示同一种状态。
|
|
59
61
|
|
|
60
62
|
如果当前 DSH 组合没有加载 projection 服务,插件会跳过注册,客户端也会自动隐藏这行,不影响普通会话。
|
|
61
63
|
|
|
@@ -83,6 +85,11 @@ Codex 等待模型首个输出时,Web 输入框上方会显示 `正在等待
|
|
|
83
85
|
name: '@shuind/dsh-codex-harness'
|
|
84
86
|
config:
|
|
85
87
|
collaborationPrompt: true
|
|
88
|
+
|
|
89
|
+
- id: codex-jobs
|
|
90
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
91
|
+
config:
|
|
92
|
+
completionDelivery: quiet
|
|
86
93
|
```
|
|
87
94
|
|
|
88
95
|
再写入 `preset.yml`,让 Web 中显示更清晰的名称:
|
|
@@ -93,7 +100,7 @@ description: 使用 Codex 工具并开启可选协作提示词。
|
|
|
93
100
|
order: 10
|
|
94
101
|
```
|
|
95
102
|
|
|
96
|
-
然后在 Web 的 **Agent 预设** 中选择 `My Codex`,再新建会话。这个 preset 会加载 Codex
|
|
103
|
+
然后在 Web 的 **Agent 预设** 中选择 `My Codex`,再新建会话。这个 preset 会加载 Codex 工具,并开启协作提示词;网页搜索、远程压缩、后台任务控制和 Skills 等其他功能需要在同一个 `agent.cordis.yml` 中按需添加。
|
|
97
104
|
|
|
98
105
|
### 可选协作提示词(私货)
|
|
99
106
|
|
|
@@ -121,6 +128,7 @@ Make things as effortless as possible for the user.
|
|
|
121
128
|
- preset 在创建会话时确定;已有会话不会自动切换到 Codex 模式。
|
|
122
129
|
- GPT 模型的图片输入和思考强度属于能力补全;用户已明确配置的字段会被保留。
|
|
123
130
|
- hosted search、远程压缩以及 reasoning 参数的可用性取决于实际 Responses 中转站和模型。
|
|
131
|
+
- `apply_patch` 的 custom grammar 改写只作用于 GPT Responses 请求;中转站必须接受 OpenAI custom tools 和 Lark grammar。拒绝时插件会自动发送原始 JSON function-call 请求。
|
|
124
132
|
|
|
125
133
|
## License
|
|
126
134
|
|
package/lib/client.js
CHANGED
|
@@ -30,6 +30,8 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
const SETTINGS_NAMESPACE = "codex";
|
|
31
31
|
const MODEL_SETTINGS_SLOT = "conversation.input.model.settings";
|
|
32
32
|
const CONTEXT_SETTINGS_SLOT = "conversation.input.context.settings";
|
|
33
|
+
const ACTIVITY_SLOT = "conversation.chat.status";
|
|
34
|
+
const ACTIVITY_FALLBACK_SLOT = "conversation.composer.dock";
|
|
33
35
|
const FAST_FALLBACK_SLOT = "conversation.input.right";
|
|
34
36
|
const CONTEXT_FALLBACK_SLOT = "conversation.input.dock";
|
|
35
37
|
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
@@ -292,19 +294,19 @@ window.__ModuleLoader__.load({
|
|
|
292
294
|
}, [agentPreset, activity?.startedAt]);
|
|
293
295
|
if (agentPreset !== "codex" || activity === void 0 || activity === null) return null;
|
|
294
296
|
const label = activity.activity === "awaiting-model" ? t("activityAwaitingModel") : t("activityCompacting");
|
|
295
|
-
return (0, react_jsx_runtime.jsxs)("
|
|
297
|
+
return (0, react_jsx_runtime.jsxs)("span", {
|
|
296
298
|
id: "codex-activity",
|
|
297
299
|
role: "status",
|
|
298
300
|
"aria-live": "polite",
|
|
299
301
|
style: {
|
|
300
|
-
display: "flex",
|
|
302
|
+
display: "inline-flex",
|
|
301
303
|
alignItems: "center",
|
|
302
304
|
gap: 7,
|
|
303
|
-
minHeight:
|
|
304
|
-
padding: "4px 10px",
|
|
305
|
+
minHeight: 26,
|
|
305
306
|
color: "var(--dsw-alias-label-secondary)",
|
|
306
307
|
fontSize: 12,
|
|
307
|
-
lineHeight: "20px"
|
|
308
|
+
lineHeight: "20px",
|
|
309
|
+
whiteSpace: "nowrap"
|
|
308
310
|
},
|
|
309
311
|
children: [
|
|
310
312
|
(0, react_jsx_runtime.jsx)("span", {
|
|
@@ -329,6 +331,18 @@ window.__ModuleLoader__.load({
|
|
|
329
331
|
]
|
|
330
332
|
});
|
|
331
333
|
}
|
|
334
|
+
/** Preserve the old composer-dock footprint for hosts without the chat status seat. */
|
|
335
|
+
function ActivityDock(props) {
|
|
336
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
337
|
+
style: {
|
|
338
|
+
display: "flex",
|
|
339
|
+
alignItems: "center",
|
|
340
|
+
minHeight: 28,
|
|
341
|
+
padding: "4px 10px"
|
|
342
|
+
},
|
|
343
|
+
children: (0, react_jsx_runtime.jsx)(ActivityLine, { ...props })
|
|
344
|
+
});
|
|
345
|
+
}
|
|
332
346
|
const inject = [
|
|
333
347
|
"slots",
|
|
334
348
|
"locale",
|
|
@@ -342,6 +356,16 @@ window.__ModuleLoader__.load({
|
|
|
342
356
|
const registry = ctx.slots;
|
|
343
357
|
return typeof registry.spec === "function" && registry.spec.call(ctx.slots, slot) !== void 0;
|
|
344
358
|
}
|
|
359
|
+
/** Register an optional host slot without claiming it in older SlotMap declarations. */
|
|
360
|
+
function injectOptionalActivitySlot(ctx, slot) {
|
|
361
|
+
const registry = ctx.slots;
|
|
362
|
+
registry.inject(slot, () => registry.register({
|
|
363
|
+
name: slot,
|
|
364
|
+
id: "codex-activity",
|
|
365
|
+
order: 5,
|
|
366
|
+
locale: NS
|
|
367
|
+
}, ActivityLine));
|
|
368
|
+
}
|
|
345
369
|
/** Mount Codex controls in the newest named seats, with legacy composer fallbacks. */
|
|
346
370
|
function apply(ctx) {
|
|
347
371
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
@@ -382,12 +406,13 @@ window.__ModuleLoader__.load({
|
|
|
382
406
|
locale: NS,
|
|
383
407
|
inject: injected
|
|
384
408
|
}, LegacyContextSizeControl));
|
|
385
|
-
|
|
386
|
-
|
|
409
|
+
if (chooseDeclaredSlot(ACTIVITY_SLOT, ACTIVITY_FALLBACK_SLOT, (slot) => slotIsDeclared(ctx, slot)) === ACTIVITY_SLOT) injectOptionalActivitySlot(ctx, ACTIVITY_SLOT);
|
|
410
|
+
else ctx.slots.inject(ACTIVITY_FALLBACK_SLOT, () => ctx.slots.register({
|
|
411
|
+
name: ACTIVITY_FALLBACK_SLOT,
|
|
387
412
|
id: "codex-activity",
|
|
388
413
|
order: 5,
|
|
389
414
|
locale: NS
|
|
390
|
-
},
|
|
415
|
+
}, ActivityDock));
|
|
391
416
|
}
|
|
392
417
|
var client_default = {
|
|
393
418
|
inject,
|
package/lib/index.js
CHANGED
|
@@ -8,7 +8,12 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
8
8
|
import { z as z$1 } from "zod";
|
|
9
9
|
//#region lib/types/context.js
|
|
10
10
|
const CODEX_CONTEXT_MAX = 1e6;
|
|
11
|
-
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region lib/types/patch.js
|
|
13
|
+
/** Parser and line-oriented applicator for Codex's `apply_patch` language. */
|
|
14
|
+
const APPLY_PATCH_DESCRIPTION = "Edits files using Codex patch syntax with Begin/End Patch markers and file update directives. In hunk lines, the first character is the operation marker; repeat a source-leading marker when the source line itself starts with one.";
|
|
15
|
+
/** The grammar sent to providers that support OpenAI custom grammar tools. */
|
|
16
|
+
const APPLY_PATCH_GRAMMAR = String.raw`start: begin_patch environment_id? hunk+ end_patch
|
|
12
17
|
begin_patch: "*** Begin Patch" LF
|
|
13
18
|
environment_id: "*** Environment ID: " filename LF
|
|
14
19
|
end_patch: "*** End Patch" LF?
|
|
@@ -261,6 +266,7 @@ function renderExecResult(result) {
|
|
|
261
266
|
sections.push(`Wall time: ${result.wall_time_seconds.toFixed(4)} seconds`);
|
|
262
267
|
if (result.exit_code !== void 0) sections.push(`Process exited with code ${result.exit_code}`);
|
|
263
268
|
if (result.session_id !== void 0) sections.push(`Process running with session ID ${result.session_id}`);
|
|
269
|
+
if (result.job_id !== void 0) sections.push(`Background job ID: ${result.job_id}`);
|
|
264
270
|
if (result.original_token_count !== void 0) sections.push(`Original token count: ${result.original_token_count}`);
|
|
265
271
|
sections.push("Output:", result.output);
|
|
266
272
|
return sections.join("\n");
|
|
@@ -281,6 +287,17 @@ function terminalResult(result, maxBytes, startedAt) {
|
|
|
281
287
|
...result.sessionStatus.kind === "exited" ? { ...result.sessionStatus.exitCode === null ? {} : { exit_code: result.sessionStatus.exitCode } } : {}
|
|
282
288
|
});
|
|
283
289
|
}
|
|
290
|
+
/** Map a detached shell process onto the generic DSH job outcome contract. */
|
|
291
|
+
function backgroundOutcome(process) {
|
|
292
|
+
if (process.status === "killed") return {
|
|
293
|
+
status: "killed",
|
|
294
|
+
detail: process.signal !== null ? `signal: ${process.signal}` : "killed before exit"
|
|
295
|
+
};
|
|
296
|
+
return {
|
|
297
|
+
status: "completed",
|
|
298
|
+
detail: `exit code: ${process.exitCode ?? 0}`
|
|
299
|
+
};
|
|
300
|
+
}
|
|
284
301
|
function sleep(ms, signal) {
|
|
285
302
|
return new Promise((resolve) => {
|
|
286
303
|
let timer;
|
|
@@ -334,6 +351,7 @@ async function runExecCommand(ctx, args, exec, config) {
|
|
|
334
351
|
const maxBytes = outputLimit(config.maxOutputBytes, args.max_output_tokens);
|
|
335
352
|
const workdir = sessionCwd$1(exec, args.workdir);
|
|
336
353
|
const startedAt = performance.now();
|
|
354
|
+
if (args.run_in_background === true && args.tty === true) throw new Error("run_in_background is only supported for pipe-backed commands; omit tty");
|
|
337
355
|
if (args.tty === true) {
|
|
338
356
|
const agent = exec.agent;
|
|
339
357
|
const terminals = ctx.get("terminals");
|
|
@@ -377,11 +395,42 @@ async function runExecCommand(ctx, args, exec, config) {
|
|
|
377
395
|
login: args.login ?? true,
|
|
378
396
|
...workdir === void 0 ? {} : { workdir },
|
|
379
397
|
stdoutMaxBytes: maxBytes,
|
|
380
|
-
signal: exec.signal,
|
|
381
398
|
...dshEnv === void 0 ? {} : { dshEnv },
|
|
382
399
|
...policy === void 0 ? {} : { sandboxPolicy: policy }
|
|
383
400
|
};
|
|
384
|
-
|
|
401
|
+
if (args.run_in_background === true) {
|
|
402
|
+
const owner = exec.agent;
|
|
403
|
+
if (owner === void 0) throw new Error("run_in_background requires an owning agent session");
|
|
404
|
+
const jobs = ctx.get("jobs");
|
|
405
|
+
if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
|
|
406
|
+
if (exec.signal.aborted) {
|
|
407
|
+
exec.signal.throwIfAborted();
|
|
408
|
+
throw new Error("tool call aborted");
|
|
409
|
+
}
|
|
410
|
+
const id = jobs.start({
|
|
411
|
+
kind: "bash",
|
|
412
|
+
label: args.cmd,
|
|
413
|
+
owner,
|
|
414
|
+
outputLimitBytes: maxBytes,
|
|
415
|
+
run: () => {
|
|
416
|
+
const process = ctx.shell.start(ctx.shell.resolve(shellRequest));
|
|
417
|
+
return {
|
|
418
|
+
cancel: () => void process.kill(),
|
|
419
|
+
done: process.done.then(() => backgroundOutcome(process)),
|
|
420
|
+
readOutput: () => readShellOutput(process.readOutput(), maxBytes)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
return withChunkId({
|
|
425
|
+
wall_time_seconds: (performance.now() - startedAt) / 1e3,
|
|
426
|
+
job_id: id,
|
|
427
|
+
output: `Started background job ${id}. Use job_output with job_id "${id}" to read output.`
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const process = ctx.shell.start(ctx.shell.resolve({
|
|
431
|
+
...shellRequest,
|
|
432
|
+
signal: exec.signal
|
|
433
|
+
}));
|
|
385
434
|
try {
|
|
386
435
|
await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs), exec.signal);
|
|
387
436
|
const output = readShellOutput(process.readOutput(), maxBytes);
|
|
@@ -531,6 +580,134 @@ function isWebSearchTool(tool) {
|
|
|
531
580
|
function hasWebSearchTool(body) {
|
|
532
581
|
return Array.isArray(body.tools) && body.tools.some(isWebSearchTool);
|
|
533
582
|
}
|
|
583
|
+
function toolName(tool) {
|
|
584
|
+
if (typeof tool !== "object" || tool === null) return void 0;
|
|
585
|
+
const value = tool;
|
|
586
|
+
if (typeof value.name === "string") return value.name;
|
|
587
|
+
const fn = value.function;
|
|
588
|
+
return typeof fn === "object" && fn !== null && typeof fn.name === "string" ? fn.name : void 0;
|
|
589
|
+
}
|
|
590
|
+
function isApplyPatchTool(tool) {
|
|
591
|
+
return toolName(tool) === "apply_patch";
|
|
592
|
+
}
|
|
593
|
+
function isFunctionApplyPatchTool(tool) {
|
|
594
|
+
if (!isApplyPatchTool(tool) || typeof tool !== "object" || tool === null) return false;
|
|
595
|
+
return tool.type !== "custom";
|
|
596
|
+
}
|
|
597
|
+
function hasFunctionApplyPatchTool(body) {
|
|
598
|
+
return Array.isArray(body.tools) && body.tools.some(isFunctionApplyPatchTool);
|
|
599
|
+
}
|
|
600
|
+
function applyPatchTool(tool) {
|
|
601
|
+
const value = tool;
|
|
602
|
+
const fn = value.function;
|
|
603
|
+
const custom = {
|
|
604
|
+
type: "custom",
|
|
605
|
+
name: "apply_patch",
|
|
606
|
+
description: typeof value.description === "string" ? value.description : typeof fn === "object" && fn !== null && typeof fn.description === "string" ? fn.description : APPLY_PATCH_DESCRIPTION,
|
|
607
|
+
format: {
|
|
608
|
+
type: "grammar",
|
|
609
|
+
syntax: "lark",
|
|
610
|
+
definition: APPLY_PATCH_GRAMMAR
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
if (value.defer_loading !== void 0) custom.defer_loading = value.defer_loading;
|
|
614
|
+
return custom;
|
|
615
|
+
}
|
|
616
|
+
/** Replace DSH's JSON function declaration with the Responses custom grammar tool. */
|
|
617
|
+
function addCodexApplyPatch(body) {
|
|
618
|
+
if (!Array.isArray(body.tools)) return body;
|
|
619
|
+
let changed = false;
|
|
620
|
+
const tools = body.tools.map((tool) => {
|
|
621
|
+
if (!isApplyPatchTool(tool) || typeof tool !== "object" || tool === null) return tool;
|
|
622
|
+
if (tool.type === "custom") return tool;
|
|
623
|
+
changed = true;
|
|
624
|
+
return applyPatchTool(tool);
|
|
625
|
+
});
|
|
626
|
+
return changed ? {
|
|
627
|
+
...body,
|
|
628
|
+
tools
|
|
629
|
+
} : body;
|
|
630
|
+
}
|
|
631
|
+
function applyPatchInputOfCall(item) {
|
|
632
|
+
if (item.type !== "function_call" || item.name !== "apply_patch") return void 0;
|
|
633
|
+
if (typeof item.call_id !== "string" || item.call_id.length === 0) return void 0;
|
|
634
|
+
const raw = item.arguments;
|
|
635
|
+
const argumentsObject = typeof raw === "string" ? (() => {
|
|
636
|
+
try {
|
|
637
|
+
return JSON.parse(raw);
|
|
638
|
+
} catch {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
})() : raw;
|
|
642
|
+
if (typeof argumentsObject !== "object" || argumentsObject === null) return void 0;
|
|
643
|
+
const input = argumentsObject.input;
|
|
644
|
+
return typeof input === "string" ? {
|
|
645
|
+
callId: item.call_id,
|
|
646
|
+
input
|
|
647
|
+
} : void 0;
|
|
648
|
+
}
|
|
649
|
+
function canRewriteApplyPatchHistory(body) {
|
|
650
|
+
if (!Array.isArray(body.input)) return true;
|
|
651
|
+
return body.input.every((item) => {
|
|
652
|
+
if (typeof item !== "object" || item === null) return true;
|
|
653
|
+
const value = item;
|
|
654
|
+
return value.type !== "function_call" || value.name !== "apply_patch" || applyPatchInputOfCall(value) !== void 0;
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
/** Replay apply_patch calls as custom tool items after pi-ai's generic conversion. */
|
|
658
|
+
function rewriteCodexApplyPatchHistory(body) {
|
|
659
|
+
if (!Array.isArray(body.input) || !canRewriteApplyPatchHistory(body)) return body;
|
|
660
|
+
const applyCallIds = /* @__PURE__ */ new Set();
|
|
661
|
+
for (const item of body.input) {
|
|
662
|
+
if (typeof item !== "object" || item === null) continue;
|
|
663
|
+
const value = item;
|
|
664
|
+
if (value.type === "custom_tool_call" && value.name === "apply_patch" && typeof value.call_id === "string") applyCallIds.add(value.call_id);
|
|
665
|
+
const call = applyPatchInputOfCall(value);
|
|
666
|
+
if (call !== void 0) applyCallIds.add(call.callId);
|
|
667
|
+
}
|
|
668
|
+
if (applyCallIds.size === 0) return body;
|
|
669
|
+
let changed = false;
|
|
670
|
+
const input = body.input.map((item) => {
|
|
671
|
+
if (typeof item !== "object" || item === null) return item;
|
|
672
|
+
const value = item;
|
|
673
|
+
const call = applyPatchInputOfCall(value);
|
|
674
|
+
if (call !== void 0) {
|
|
675
|
+
const next = {
|
|
676
|
+
type: "custom_tool_call",
|
|
677
|
+
call_id: call.callId,
|
|
678
|
+
name: "apply_patch",
|
|
679
|
+
input: call.input
|
|
680
|
+
};
|
|
681
|
+
if (typeof value.id === "string") next.id = value.id;
|
|
682
|
+
changed = true;
|
|
683
|
+
return next;
|
|
684
|
+
}
|
|
685
|
+
if (value.type === "function_call_output" && typeof value.call_id === "string" && applyCallIds.has(value.call_id)) {
|
|
686
|
+
changed = true;
|
|
687
|
+
return {
|
|
688
|
+
type: "custom_tool_call_output",
|
|
689
|
+
call_id: value.call_id,
|
|
690
|
+
output: value.output
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return item;
|
|
694
|
+
});
|
|
695
|
+
return changed ? {
|
|
696
|
+
...body,
|
|
697
|
+
input
|
|
698
|
+
} : body;
|
|
699
|
+
}
|
|
700
|
+
/** Compose the plugin-only Responses rewrites without changing DSH's tool API. */
|
|
701
|
+
function rewriteCodexResponsesBody(body) {
|
|
702
|
+
let next = body;
|
|
703
|
+
if (hasWebSearchTool(next)) next = addHostedWebSearch(next);
|
|
704
|
+
else if (hasRemoteCompaction(next)) next = replaceRemoteCompactions(next);
|
|
705
|
+
if (hasFunctionApplyPatchTool(next) && canRewriteApplyPatchHistory(next)) {
|
|
706
|
+
next = addCodexApplyPatch(next);
|
|
707
|
+
next = rewriteCodexApplyPatchHistory(next);
|
|
708
|
+
}
|
|
709
|
+
return next;
|
|
710
|
+
}
|
|
534
711
|
/** Convert a generic DSH Responses tool list to the hosted Codex variant. */
|
|
535
712
|
function addHostedWebSearch(body) {
|
|
536
713
|
const tools = Array.isArray(body.tools) ? body.tools.filter((tool) => !isWebSearchTool(tool)) : [];
|
|
@@ -574,12 +751,13 @@ function hasRemoteCompaction(body) {
|
|
|
574
751
|
});
|
|
575
752
|
}
|
|
576
753
|
function canPatchBody(body) {
|
|
577
|
-
return isGptModel$1(body.model) && (hasWebSearchTool(body) || hasRemoteCompaction(body));
|
|
754
|
+
return isGptModel$1(body.model) && (hasWebSearchTool(body) || hasRemoteCompaction(body) || hasFunctionApplyPatchTool(body) && canRewriteApplyPatchHistory(body));
|
|
578
755
|
}
|
|
579
756
|
/**
|
|
580
757
|
* Install a scoped fetch shim for pi-ai's already-built Responses request.
|
|
581
758
|
* The generic DSH adapter remains the owner of auth, streaming, replay, and
|
|
582
|
-
* attachments; this shim changes only
|
|
759
|
+
* attachments; this shim changes only Codex's Responses tool and compaction
|
|
760
|
+
* representation at the wire boundary.
|
|
583
761
|
*/
|
|
584
762
|
function installGlobalHostedWebSearchPatch() {
|
|
585
763
|
const original = globalThis.fetch;
|
|
@@ -598,7 +776,7 @@ function installGlobalHostedWebSearchPatch() {
|
|
|
598
776
|
const headers = new Headers(request.headers);
|
|
599
777
|
headers.delete("content-length");
|
|
600
778
|
const hostedRequest = new Request(request, {
|
|
601
|
-
body: JSON.stringify(
|
|
779
|
+
body: JSON.stringify(rewriteCodexResponsesBody(body)),
|
|
602
780
|
headers
|
|
603
781
|
});
|
|
604
782
|
try {
|
|
@@ -1017,9 +1195,8 @@ function normalizeCodexPromptAssembly(assembly) {
|
|
|
1017
1195
|
function buildCodexSystemPrompt(config = {}) {
|
|
1018
1196
|
return config.collaborationPrompt === true ? `${CODEX_BASE_PROMPT}\n\n${CODEX_COLLABORATION_PROMPT}` : CODEX_BASE_PROMPT;
|
|
1019
1197
|
}
|
|
1020
|
-
const EXEC_COMMAND_DESCRIPTION = "Runs a command in a PTY, returning output
|
|
1198
|
+
const EXEC_COMMAND_DESCRIPTION = "Runs a command in a PTY, returning output, a session ID for ongoing interaction, or a background job ID when requested.";
|
|
1021
1199
|
const WRITE_STDIN_DESCRIPTION = "Writes characters to an existing unified exec session and returns recent output.";
|
|
1022
|
-
const APPLY_PATCH_DESCRIPTION = "Edits files using Codex patch syntax with Begin/End Patch markers and file update directives. In hunk lines, the first character is the operation marker; repeat a source-leading marker when the source line itself starts with one.";
|
|
1023
1200
|
const UPDATE_PLAN_DESCRIPTION = "Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.";
|
|
1024
1201
|
const PLAN_STATUSES = [
|
|
1025
1202
|
"pending",
|
|
@@ -1165,6 +1342,10 @@ function registerExecTools(ctx, config) {
|
|
|
1165
1342
|
type: "boolean",
|
|
1166
1343
|
description: "True allocates a PTY for the command; false or omitted uses plain pipes."
|
|
1167
1344
|
},
|
|
1345
|
+
run_in_background: {
|
|
1346
|
+
type: "boolean",
|
|
1347
|
+
description: "Run as a DSH background job and return its job id immediately; collect output with job_output and stop it with job_kill. Only supported for pipe-backed commands."
|
|
1348
|
+
},
|
|
1168
1349
|
yield_time_ms: {
|
|
1169
1350
|
type: "number",
|
|
1170
1351
|
description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms."
|
|
@@ -1194,6 +1375,7 @@ function registerExecTools(ctx, config) {
|
|
|
1194
1375
|
},
|
|
1195
1376
|
exit_code: { type: "number" },
|
|
1196
1377
|
session_id: { type: "number" },
|
|
1378
|
+
job_id: { type: "string" },
|
|
1197
1379
|
original_token_count: { type: "number" },
|
|
1198
1380
|
output: {
|
|
1199
1381
|
type: "string",
|
|
@@ -1396,13 +1578,13 @@ function apply(ctx, config = {}) {
|
|
|
1396
1578
|
ctx.on("agent/request", async (_payload, next) => {
|
|
1397
1579
|
return applyCodexRequestSettings(await next(), codexSettings.current());
|
|
1398
1580
|
});
|
|
1399
|
-
if (
|
|
1581
|
+
if (typeof ctx.effect === "function") {
|
|
1400
1582
|
const disposeHostedWebSearch = installHostedWebSearch();
|
|
1401
1583
|
ctx.effect(() => disposeHostedWebSearch, "codex: Responses transport wrapper");
|
|
1402
1584
|
}
|
|
1403
|
-
|
|
1585
|
+
ctx.on("llm/stream", ((options, next) => {
|
|
1404
1586
|
if (resolved.remoteCompact && options.purpose === "compaction" && isGptModel(options.model)) return remoteCompactStream(ctx, options, next);
|
|
1405
|
-
if (
|
|
1587
|
+
if (options.purpose === void 0 && isGptModel(options.model)) return hostedWebSearchStream(next);
|
|
1406
1588
|
return next();
|
|
1407
1589
|
}));
|
|
1408
1590
|
ctx.on("system-prompt/assemble", async (_assembly, _context, next) => normalizeCodexPromptAssembly(await next()));
|
|
@@ -32,7 +32,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
type
|
|
35
|
+
type ActivityDockProps = PropsRuntime<'conversation.composer.dock'> & PropsLocale<'codex'>;
|
|
36
|
+
type ActivityProps = ActivityDockProps;
|
|
36
37
|
export declare function ActivityLine({ sessionId, useSession, useSessions, useProjection, t }: ActivityProps): import("react").JSX.Element | null;
|
|
37
38
|
export declare const inject: string[];
|
|
38
39
|
/** Select a preferred slot when the host declares it, otherwise use a stable legacy composer seat. */
|
|
@@ -7,6 +7,10 @@ const NS = 'codex';
|
|
|
7
7
|
const SETTINGS_NAMESPACE = 'codex';
|
|
8
8
|
const MODEL_SETTINGS_SLOT = 'conversation.input.model.settings';
|
|
9
9
|
const CONTEXT_SETTINGS_SLOT = 'conversation.input.context.settings';
|
|
10
|
+
// Prefer a dedicated status seat when a newer host declares it; the current
|
|
11
|
+
// conversation contract falls back to the ambient composer dock below.
|
|
12
|
+
const ACTIVITY_SLOT = 'conversation.chat.status';
|
|
13
|
+
const ACTIVITY_FALLBACK_SLOT = 'conversation.composer.dock';
|
|
10
14
|
const FAST_FALLBACK_SLOT = 'conversation.input.right';
|
|
11
15
|
const CONTEXT_FALLBACK_SLOT = 'conversation.input.dock';
|
|
12
16
|
const DEFAULT_CONTEXT_WINDOW = 262_144;
|
|
@@ -161,15 +165,15 @@ export function ActivityLine({ sessionId, useSession, useSessions, useProjection
|
|
|
161
165
|
const label = activity.activity === 'awaiting-model'
|
|
162
166
|
? t('activityAwaitingModel')
|
|
163
167
|
: t('activityCompacting');
|
|
164
|
-
return (_jsxs("
|
|
165
|
-
display: 'flex',
|
|
168
|
+
return (_jsxs("span", { id: "codex-activity", role: "status", "aria-live": "polite", style: {
|
|
169
|
+
display: 'inline-flex',
|
|
166
170
|
alignItems: 'center',
|
|
167
171
|
gap: 7,
|
|
168
|
-
minHeight:
|
|
169
|
-
padding: '4px 10px',
|
|
172
|
+
minHeight: 26,
|
|
170
173
|
color: 'var(--dsw-alias-label-secondary)',
|
|
171
174
|
fontSize: 12,
|
|
172
175
|
lineHeight: '20px',
|
|
176
|
+
whiteSpace: 'nowrap',
|
|
173
177
|
}, children: [_jsx("span", { "aria-hidden": "true", style: {
|
|
174
178
|
width: 7,
|
|
175
179
|
height: 7,
|
|
@@ -178,6 +182,10 @@ export function ActivityLine({ sessionId, useSession, useSessions, useProjection
|
|
|
178
182
|
background: 'var(--dsw-static-blue-500)',
|
|
179
183
|
} }), _jsx("span", { children: label }), _jsxs("span", { style: { color: 'var(--dsw-alias-label-tertiary)' }, children: ["\u00B7 ", elapsedSeconds(activity.startedAt, now), "s"] })] }));
|
|
180
184
|
}
|
|
185
|
+
/** Preserve the old composer-dock footprint for hosts without the chat status seat. */
|
|
186
|
+
function ActivityDock(props) {
|
|
187
|
+
return (_jsx("div", { style: { display: 'flex', alignItems: 'center', minHeight: 28, padding: '4px 10px' }, children: _jsx(ActivityLine, { ...props }) }));
|
|
188
|
+
}
|
|
181
189
|
export const inject = ['slots', 'locale', 'settingsScope'];
|
|
182
190
|
/** Select a preferred slot when the host declares it, otherwise use a stable legacy composer seat. */
|
|
183
191
|
export function chooseDeclaredSlot(preferred, fallback, isDeclared) {
|
|
@@ -187,6 +195,16 @@ function slotIsDeclared(ctx, slot) {
|
|
|
187
195
|
const registry = ctx.slots;
|
|
188
196
|
return typeof registry.spec === 'function' && registry.spec.call(ctx.slots, slot) !== undefined;
|
|
189
197
|
}
|
|
198
|
+
/** Register an optional host slot without claiming it in older SlotMap declarations. */
|
|
199
|
+
function injectOptionalActivitySlot(ctx, slot) {
|
|
200
|
+
const registry = ctx.slots;
|
|
201
|
+
registry.inject(slot, () => registry.register({
|
|
202
|
+
name: slot,
|
|
203
|
+
id: 'codex-activity',
|
|
204
|
+
order: 5,
|
|
205
|
+
locale: NS,
|
|
206
|
+
}, ActivityLine));
|
|
207
|
+
}
|
|
190
208
|
/** Mount Codex controls in the newest named seats, with legacy composer fallbacks. */
|
|
191
209
|
export function apply(ctx) {
|
|
192
210
|
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'codex client: dictionaries');
|
|
@@ -234,12 +252,18 @@ export function apply(ctx) {
|
|
|
234
252
|
inject: injected,
|
|
235
253
|
}, LegacyContextSizeControl));
|
|
236
254
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
255
|
+
const activitySlot = chooseDeclaredSlot(ACTIVITY_SLOT, ACTIVITY_FALLBACK_SLOT, slot => slotIsDeclared(ctx, slot));
|
|
256
|
+
if (activitySlot === ACTIVITY_SLOT) {
|
|
257
|
+
injectOptionalActivitySlot(ctx, ACTIVITY_SLOT);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
ctx.slots.inject(ACTIVITY_FALLBACK_SLOT, () => ctx.slots.register({
|
|
261
|
+
name: ACTIVITY_FALLBACK_SLOT,
|
|
262
|
+
id: 'codex-activity',
|
|
263
|
+
order: 5,
|
|
264
|
+
locale: NS,
|
|
265
|
+
}, ActivityDock));
|
|
266
|
+
}
|
|
243
267
|
}
|
|
244
268
|
export default { inject, apply };
|
|
245
269
|
//# sourceMappingURL=index.js.map
|
package/lib/types/exec.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface ExecCommandArgs {
|
|
|
6
6
|
cmd: string;
|
|
7
7
|
workdir?: string;
|
|
8
8
|
tty?: boolean;
|
|
9
|
+
run_in_background?: boolean;
|
|
9
10
|
yield_time_ms?: number;
|
|
10
11
|
max_output_tokens?: number;
|
|
11
12
|
shell?: string;
|
|
@@ -24,6 +25,7 @@ export interface ExecResult {
|
|
|
24
25
|
wall_time_seconds: number;
|
|
25
26
|
output: string;
|
|
26
27
|
session_id?: number;
|
|
28
|
+
job_id?: string;
|
|
27
29
|
exit_code?: number;
|
|
28
30
|
original_token_count?: number;
|
|
29
31
|
}
|
package/lib/types/exec.js
CHANGED
|
@@ -56,6 +56,8 @@ export function renderExecResult(result) {
|
|
|
56
56
|
sections.push(`Process exited with code ${result.exit_code}`);
|
|
57
57
|
if (result.session_id !== undefined)
|
|
58
58
|
sections.push(`Process running with session ID ${result.session_id}`);
|
|
59
|
+
if (result.job_id !== undefined)
|
|
60
|
+
sections.push(`Background job ID: ${result.job_id}`);
|
|
59
61
|
if (result.original_token_count !== undefined)
|
|
60
62
|
sections.push(`Original token count: ${result.original_token_count}`);
|
|
61
63
|
sections.push('Output:', result.output);
|
|
@@ -80,6 +82,16 @@ function terminalResult(result, maxBytes, startedAt) {
|
|
|
80
82
|
} : {},
|
|
81
83
|
});
|
|
82
84
|
}
|
|
85
|
+
/** Map a detached shell process onto the generic DSH job outcome contract. */
|
|
86
|
+
function backgroundOutcome(process) {
|
|
87
|
+
if (process.status === 'killed') {
|
|
88
|
+
return {
|
|
89
|
+
status: 'killed',
|
|
90
|
+
detail: process.signal !== null ? `signal: ${process.signal}` : 'killed before exit',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { status: 'completed', detail: `exit code: ${process.exitCode ?? 0}` };
|
|
94
|
+
}
|
|
83
95
|
function sleep(ms, signal) {
|
|
84
96
|
return new Promise(resolve => {
|
|
85
97
|
let timer;
|
|
@@ -144,6 +156,9 @@ export async function runExecCommand(ctx, args, exec, config) {
|
|
|
144
156
|
const maxBytes = outputLimit(config.maxOutputBytes, args.max_output_tokens);
|
|
145
157
|
const workdir = sessionCwd(exec, args.workdir);
|
|
146
158
|
const startedAt = performance.now();
|
|
159
|
+
if (args.run_in_background === true && args.tty === true) {
|
|
160
|
+
throw new Error('run_in_background is only supported for pipe-backed commands; omit tty');
|
|
161
|
+
}
|
|
147
162
|
if (args.tty === true) {
|
|
148
163
|
const agent = exec.agent;
|
|
149
164
|
const terminals = ctx.get('terminals');
|
|
@@ -187,17 +202,53 @@ export async function runExecCommand(ctx, args, exec, config) {
|
|
|
187
202
|
}
|
|
188
203
|
const policy = ctx.get('sandboxPolicy')?.resolve(exec.agent === undefined ? {} : { session: exec.agent.session });
|
|
189
204
|
const dshEnv = ctx.get('shellEnv')?.collect(exec);
|
|
205
|
+
// Background jobs intentionally omit the tool-call signal. Once this call
|
|
206
|
+
// returns, the job lifetime belongs to the jobs registry, not the request.
|
|
190
207
|
const shellRequest = {
|
|
191
208
|
command: commandFor(args),
|
|
192
209
|
...args.shell === undefined ? {} : { shell: args.shell },
|
|
193
210
|
login: args.login ?? true,
|
|
194
211
|
...workdir === undefined ? {} : { workdir },
|
|
195
212
|
stdoutMaxBytes: maxBytes,
|
|
196
|
-
signal: exec.signal,
|
|
197
213
|
...dshEnv === undefined ? {} : { dshEnv },
|
|
198
214
|
...policy === undefined ? {} : { sandboxPolicy: policy },
|
|
199
215
|
};
|
|
200
|
-
|
|
216
|
+
if (args.run_in_background === true) {
|
|
217
|
+
const owner = exec.agent;
|
|
218
|
+
if (owner === undefined)
|
|
219
|
+
throw new Error('run_in_background requires an owning agent session');
|
|
220
|
+
const jobs = ctx.get('jobs');
|
|
221
|
+
if (jobs === undefined) {
|
|
222
|
+
throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs');
|
|
223
|
+
}
|
|
224
|
+
if (exec.signal.aborted) {
|
|
225
|
+
exec.signal.throwIfAborted();
|
|
226
|
+
throw new Error('tool call aborted');
|
|
227
|
+
}
|
|
228
|
+
const id = jobs.start({
|
|
229
|
+
kind: 'bash',
|
|
230
|
+
label: args.cmd,
|
|
231
|
+
owner,
|
|
232
|
+
outputLimitBytes: maxBytes,
|
|
233
|
+
run: () => {
|
|
234
|
+
const process = ctx.shell.start(ctx.shell.resolve(shellRequest));
|
|
235
|
+
return {
|
|
236
|
+
cancel: () => void process.kill(),
|
|
237
|
+
done: process.done.then(() => backgroundOutcome(process)),
|
|
238
|
+
readOutput: () => readShellOutput(process.readOutput(), maxBytes),
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
return withChunkId({
|
|
243
|
+
wall_time_seconds: (performance.now() - startedAt) / 1000,
|
|
244
|
+
job_id: id,
|
|
245
|
+
output: `Started background job ${id}. Use job_output with job_id "${id}" to read output.`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const process = ctx.shell.start(ctx.shell.resolve({
|
|
249
|
+
...shellRequest,
|
|
250
|
+
signal: exec.signal,
|
|
251
|
+
}));
|
|
201
252
|
try {
|
|
202
253
|
await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs), exec.signal);
|
|
203
254
|
const output = readShellOutput(process.readOutput(), maxBytes);
|
package/lib/types/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
3
3
|
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
4
4
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
5
5
|
import { CODEX_CONTEXT_MAX } from "./context.js";
|
|
6
|
-
import { applyPatchHunks, parsePatch } from "./patch.js";
|
|
6
|
+
import { APPLY_PATCH_DESCRIPTION, applyPatchHunks, parsePatch } from "./patch.js";
|
|
7
7
|
import { renderExecResult, runExecCommand, runWriteStdin } from "./exec.js";
|
|
8
8
|
import { hostedWebSearchStream, installHostedWebSearch, remoteCompactStream } from "./remote.js";
|
|
9
9
|
import { registerCodexActivityProjection } from "./activity.js";
|
|
@@ -194,9 +194,8 @@ export function buildCodexSystemPrompt(config = {}) {
|
|
|
194
194
|
? `${CODEX_BASE_PROMPT}\n\n${CODEX_COLLABORATION_PROMPT}`
|
|
195
195
|
: CODEX_BASE_PROMPT;
|
|
196
196
|
}
|
|
197
|
-
const EXEC_COMMAND_DESCRIPTION = 'Runs a command in a PTY, returning output
|
|
197
|
+
const EXEC_COMMAND_DESCRIPTION = 'Runs a command in a PTY, returning output, a session ID for ongoing interaction, or a background job ID when requested.';
|
|
198
198
|
const WRITE_STDIN_DESCRIPTION = 'Writes characters to an existing unified exec session and returns recent output.';
|
|
199
|
-
const APPLY_PATCH_DESCRIPTION = 'Edits files using Codex patch syntax with Begin/End Patch markers and file update directives. In hunk lines, the first character is the operation marker; repeat a source-leading marker when the source line itself starts with one.';
|
|
200
199
|
const UPDATE_PLAN_DESCRIPTION = 'Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.';
|
|
201
200
|
const PLAN_STATUSES = ['pending', 'in_progress', 'completed'];
|
|
202
201
|
function sessionCwd(exec) {
|
|
@@ -306,6 +305,7 @@ function registerExecTools(ctx, config) {
|
|
|
306
305
|
cmd: { type: 'string', required: true, description: 'Shell command to execute.' },
|
|
307
306
|
workdir: { type: 'string', description: 'Working directory for the command. Defaults to the turn cwd.' },
|
|
308
307
|
tty: { type: 'boolean', description: 'True allocates a PTY for the command; false or omitted uses plain pipes.' },
|
|
308
|
+
run_in_background: { type: 'boolean', description: 'Run as a DSH background job and return its job id immediately; collect output with job_output and stop it with job_kill. Only supported for pipe-backed commands.' },
|
|
309
309
|
yield_time_ms: { type: 'number', description: 'Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms.' },
|
|
310
310
|
max_output_tokens: { type: 'number', description: 'Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy.' },
|
|
311
311
|
shell: { type: 'string', description: "Shell binary to launch. Defaults to the user's default shell." },
|
|
@@ -320,6 +320,7 @@ function registerExecTools(ctx, config) {
|
|
|
320
320
|
wall_time_seconds: { type: 'number', required: true },
|
|
321
321
|
exit_code: { type: 'number' },
|
|
322
322
|
session_id: { type: 'number' },
|
|
323
|
+
job_id: { type: 'string' },
|
|
323
324
|
original_token_count: { type: 'number' },
|
|
324
325
|
output: { type: 'string', required: true },
|
|
325
326
|
},
|
|
@@ -471,23 +472,22 @@ export function apply(ctx, config = {}) {
|
|
|
471
472
|
const request = await next();
|
|
472
473
|
return applyCodexRequestSettings(request, codexSettings.current());
|
|
473
474
|
});
|
|
474
|
-
if (
|
|
475
|
+
if (typeof ctx.effect === 'function') {
|
|
475
476
|
const disposeHostedWebSearch = installHostedWebSearch();
|
|
476
477
|
ctx.effect(() => disposeHostedWebSearch, 'codex: Responses transport wrapper');
|
|
477
478
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
479
|
+
ctx.on('llm/stream', ((options, next) => {
|
|
480
|
+
if (resolved.remoteCompact && options.purpose === 'compaction' && isGptModel(options.model)) {
|
|
481
|
+
return remoteCompactStream(ctx, options, next);
|
|
482
|
+
}
|
|
483
|
+
// Keep all ordinary GPT Responses calls inside the plugin-owned transport
|
|
484
|
+
// scope so apply_patch can use the same wire bridge even when hosted search
|
|
485
|
+
// and remote compaction are disabled.
|
|
486
|
+
if (options.purpose === undefined && isGptModel(options.model)) {
|
|
487
|
+
return hostedWebSearchStream(next);
|
|
488
|
+
}
|
|
489
|
+
return next();
|
|
490
|
+
}));
|
|
491
491
|
ctx.on('system-prompt/assemble', async (_assembly, _context, next) => normalizeCodexPromptAssembly(await next()));
|
|
492
492
|
ctx.systemPrompt.section({ name: 'codex:base', order: 10, text: buildCodexSystemPrompt(resolved) });
|
|
493
493
|
registerExecTools(ctx, resolved);
|
package/lib/types/patch.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Parser and line-oriented applicator for Codex's `apply_patch` language. */
|
|
2
|
+
export declare const APPLY_PATCH_DESCRIPTION = "Edits files using Codex patch syntax with Begin/End Patch markers and file update directives. In hunk lines, the first character is the operation marker; repeat a source-leading marker when the source line itself starts with one.";
|
|
2
3
|
/** The grammar sent to providers that support OpenAI custom grammar tools. */
|
|
3
4
|
export declare const APPLY_PATCH_GRAMMAR: string;
|
|
4
5
|
export type PatchLine = {
|
package/lib/types/patch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Parser and line-oriented applicator for Codex's `apply_patch` language. */
|
|
2
|
+
export const APPLY_PATCH_DESCRIPTION = 'Edits files using Codex patch syntax with Begin/End Patch markers and file update directives. In hunk lines, the first character is the operation marker; repeat a source-leading marker when the source line itself starts with one.';
|
|
2
3
|
/** The grammar sent to providers that support OpenAI custom grammar tools. */
|
|
3
4
|
export const APPLY_PATCH_GRAMMAR = String.raw `start: begin_patch environment_id? hunk+ end_patch
|
|
4
5
|
begin_patch: "*** Begin Patch" LF
|
package/lib/types/remote.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface RemoteCodexConfig {
|
|
|
7
7
|
interface JsonObject {
|
|
8
8
|
[key: string]: unknown;
|
|
9
9
|
}
|
|
10
|
+
/** Replace DSH's JSON function declaration with the Responses custom grammar tool. */
|
|
11
|
+
export declare function addCodexApplyPatch(body: JsonObject): JsonObject;
|
|
12
|
+
/** Replay apply_patch calls as custom tool items after pi-ai's generic conversion. */
|
|
13
|
+
export declare function rewriteCodexApplyPatchHistory(body: JsonObject): JsonObject;
|
|
14
|
+
/** Compose the plugin-only Responses rewrites without changing DSH's tool API. */
|
|
15
|
+
export declare function rewriteCodexResponsesBody(body: JsonObject): JsonObject;
|
|
10
16
|
/** Convert a generic DSH Responses tool list to the hosted Codex variant. */
|
|
11
17
|
export declare function addHostedWebSearch(body: JsonObject): JsonObject;
|
|
12
18
|
/** Replace the text placeholder written by dsh-compaction-basic with the native item. */
|
package/lib/types/remote.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { APPLY_PATCH_DESCRIPTION, APPLY_PATCH_GRAMMAR } from "./patch.js";
|
|
3
4
|
const SETTINGS_NAMESPACE = 'llm-pi-ai';
|
|
4
5
|
const REMOTE_COMPACTION_OPEN = '<codex-remote-compaction>';
|
|
5
6
|
const REMOTE_COMPACTION_CLOSE = '</codex-remote-compaction>';
|
|
@@ -59,6 +60,162 @@ function isWebSearchTool(tool) {
|
|
|
59
60
|
function hasWebSearchTool(body) {
|
|
60
61
|
return Array.isArray(body.tools) && body.tools.some(isWebSearchTool);
|
|
61
62
|
}
|
|
63
|
+
function toolName(tool) {
|
|
64
|
+
if (typeof tool !== 'object' || tool === null)
|
|
65
|
+
return undefined;
|
|
66
|
+
const value = tool;
|
|
67
|
+
if (typeof value.name === 'string')
|
|
68
|
+
return value.name;
|
|
69
|
+
const fn = value.function;
|
|
70
|
+
return typeof fn === 'object' && fn !== null && typeof fn.name === 'string'
|
|
71
|
+
? fn.name
|
|
72
|
+
: undefined;
|
|
73
|
+
}
|
|
74
|
+
function isApplyPatchTool(tool) {
|
|
75
|
+
return toolName(tool) === 'apply_patch';
|
|
76
|
+
}
|
|
77
|
+
function isFunctionApplyPatchTool(tool) {
|
|
78
|
+
if (!isApplyPatchTool(tool) || typeof tool !== 'object' || tool === null)
|
|
79
|
+
return false;
|
|
80
|
+
return tool.type !== 'custom';
|
|
81
|
+
}
|
|
82
|
+
function hasFunctionApplyPatchTool(body) {
|
|
83
|
+
return Array.isArray(body.tools) && body.tools.some(isFunctionApplyPatchTool);
|
|
84
|
+
}
|
|
85
|
+
function applyPatchTool(tool) {
|
|
86
|
+
const value = tool;
|
|
87
|
+
const fn = value.function;
|
|
88
|
+
const description = typeof value.description === 'string'
|
|
89
|
+
? value.description
|
|
90
|
+
: typeof fn === 'object' && fn !== null && typeof fn.description === 'string'
|
|
91
|
+
? fn.description
|
|
92
|
+
: APPLY_PATCH_DESCRIPTION;
|
|
93
|
+
const custom = {
|
|
94
|
+
type: 'custom',
|
|
95
|
+
name: 'apply_patch',
|
|
96
|
+
description,
|
|
97
|
+
format: {
|
|
98
|
+
type: 'grammar',
|
|
99
|
+
syntax: 'lark',
|
|
100
|
+
definition: APPLY_PATCH_GRAMMAR,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
if (value.defer_loading !== undefined)
|
|
104
|
+
custom.defer_loading = value.defer_loading;
|
|
105
|
+
return custom;
|
|
106
|
+
}
|
|
107
|
+
/** Replace DSH's JSON function declaration with the Responses custom grammar tool. */
|
|
108
|
+
export function addCodexApplyPatch(body) {
|
|
109
|
+
if (!Array.isArray(body.tools))
|
|
110
|
+
return body;
|
|
111
|
+
let changed = false;
|
|
112
|
+
const tools = body.tools.map(tool => {
|
|
113
|
+
if (!isApplyPatchTool(tool) || typeof tool !== 'object' || tool === null)
|
|
114
|
+
return tool;
|
|
115
|
+
if (tool.type === 'custom')
|
|
116
|
+
return tool;
|
|
117
|
+
changed = true;
|
|
118
|
+
return applyPatchTool(tool);
|
|
119
|
+
});
|
|
120
|
+
return changed ? { ...body, tools } : body;
|
|
121
|
+
}
|
|
122
|
+
function applyPatchInputOfCall(item) {
|
|
123
|
+
if (item.type !== 'function_call' || item.name !== 'apply_patch')
|
|
124
|
+
return undefined;
|
|
125
|
+
if (typeof item.call_id !== 'string' || item.call_id.length === 0)
|
|
126
|
+
return undefined;
|
|
127
|
+
const raw = item.arguments;
|
|
128
|
+
const argumentsObject = typeof raw === 'string'
|
|
129
|
+
? (() => {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(raw);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
})()
|
|
137
|
+
: raw;
|
|
138
|
+
if (typeof argumentsObject !== 'object' || argumentsObject === null)
|
|
139
|
+
return undefined;
|
|
140
|
+
const input = argumentsObject.input;
|
|
141
|
+
return typeof input === 'string' ? { callId: item.call_id, input } : undefined;
|
|
142
|
+
}
|
|
143
|
+
function canRewriteApplyPatchHistory(body) {
|
|
144
|
+
if (!Array.isArray(body.input))
|
|
145
|
+
return true;
|
|
146
|
+
return body.input.every(item => {
|
|
147
|
+
if (typeof item !== 'object' || item === null)
|
|
148
|
+
return true;
|
|
149
|
+
const value = item;
|
|
150
|
+
return value.type !== 'function_call' || value.name !== 'apply_patch'
|
|
151
|
+
|| applyPatchInputOfCall(value) !== undefined;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** Replay apply_patch calls as custom tool items after pi-ai's generic conversion. */
|
|
155
|
+
export function rewriteCodexApplyPatchHistory(body) {
|
|
156
|
+
if (!Array.isArray(body.input) || !canRewriteApplyPatchHistory(body))
|
|
157
|
+
return body;
|
|
158
|
+
const applyCallIds = new Set();
|
|
159
|
+
for (const item of body.input) {
|
|
160
|
+
if (typeof item !== 'object' || item === null)
|
|
161
|
+
continue;
|
|
162
|
+
const value = item;
|
|
163
|
+
if (value.type === 'custom_tool_call' && value.name === 'apply_patch' && typeof value.call_id === 'string') {
|
|
164
|
+
applyCallIds.add(value.call_id);
|
|
165
|
+
}
|
|
166
|
+
const call = applyPatchInputOfCall(value);
|
|
167
|
+
if (call !== undefined)
|
|
168
|
+
applyCallIds.add(call.callId);
|
|
169
|
+
}
|
|
170
|
+
if (applyCallIds.size === 0)
|
|
171
|
+
return body;
|
|
172
|
+
let changed = false;
|
|
173
|
+
const input = body.input.map(item => {
|
|
174
|
+
if (typeof item !== 'object' || item === null)
|
|
175
|
+
return item;
|
|
176
|
+
const value = item;
|
|
177
|
+
const call = applyPatchInputOfCall(value);
|
|
178
|
+
if (call !== undefined) {
|
|
179
|
+
const next = {
|
|
180
|
+
type: 'custom_tool_call',
|
|
181
|
+
call_id: call.callId,
|
|
182
|
+
name: 'apply_patch',
|
|
183
|
+
input: call.input,
|
|
184
|
+
};
|
|
185
|
+
if (typeof value.id === 'string')
|
|
186
|
+
next.id = value.id;
|
|
187
|
+
changed = true;
|
|
188
|
+
return next;
|
|
189
|
+
}
|
|
190
|
+
if (value.type === 'function_call_output'
|
|
191
|
+
&& typeof value.call_id === 'string'
|
|
192
|
+
&& applyCallIds.has(value.call_id)) {
|
|
193
|
+
changed = true;
|
|
194
|
+
return {
|
|
195
|
+
type: 'custom_tool_call_output',
|
|
196
|
+
call_id: value.call_id,
|
|
197
|
+
output: value.output,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return item;
|
|
201
|
+
});
|
|
202
|
+
return changed ? { ...body, input } : body;
|
|
203
|
+
}
|
|
204
|
+
/** Compose the plugin-only Responses rewrites without changing DSH's tool API. */
|
|
205
|
+
export function rewriteCodexResponsesBody(body) {
|
|
206
|
+
let next = body;
|
|
207
|
+
if (hasWebSearchTool(next)) {
|
|
208
|
+
next = addHostedWebSearch(next);
|
|
209
|
+
}
|
|
210
|
+
else if (hasRemoteCompaction(next)) {
|
|
211
|
+
next = replaceRemoteCompactions(next);
|
|
212
|
+
}
|
|
213
|
+
if (hasFunctionApplyPatchTool(next) && canRewriteApplyPatchHistory(next)) {
|
|
214
|
+
next = addCodexApplyPatch(next);
|
|
215
|
+
next = rewriteCodexApplyPatchHistory(next);
|
|
216
|
+
}
|
|
217
|
+
return next;
|
|
218
|
+
}
|
|
62
219
|
/** Convert a generic DSH Responses tool list to the hosted Codex variant. */
|
|
63
220
|
export function addHostedWebSearch(body) {
|
|
64
221
|
const tools = Array.isArray(body.tools) ? body.tools.filter(tool => !isWebSearchTool(tool)) : [];
|
|
@@ -107,12 +264,15 @@ function hasRemoteCompaction(body) {
|
|
|
107
264
|
}
|
|
108
265
|
function canPatchBody(body) {
|
|
109
266
|
return isGptModel(body.model)
|
|
110
|
-
&& (hasWebSearchTool(body)
|
|
267
|
+
&& (hasWebSearchTool(body)
|
|
268
|
+
|| hasRemoteCompaction(body)
|
|
269
|
+
|| (hasFunctionApplyPatchTool(body) && canRewriteApplyPatchHistory(body)));
|
|
111
270
|
}
|
|
112
271
|
/**
|
|
113
272
|
* Install a scoped fetch shim for pi-ai's already-built Responses request.
|
|
114
273
|
* The generic DSH adapter remains the owner of auth, streaming, replay, and
|
|
115
|
-
* attachments; this shim changes only
|
|
274
|
+
* attachments; this shim changes only Codex's Responses tool and compaction
|
|
275
|
+
* representation at the wire boundary.
|
|
116
276
|
*/
|
|
117
277
|
function installGlobalHostedWebSearchPatch() {
|
|
118
278
|
const original = globalThis.fetch;
|
|
@@ -135,7 +295,7 @@ function installGlobalHostedWebSearchPatch() {
|
|
|
135
295
|
const headers = new Headers(request.headers);
|
|
136
296
|
headers.delete('content-length');
|
|
137
297
|
const hostedRequest = new Request(request, {
|
|
138
|
-
body: JSON.stringify(
|
|
298
|
+
body: JSON.stringify(rewriteCodexResponsesBody(body)),
|
|
139
299
|
headers,
|
|
140
300
|
});
|
|
141
301
|
try {
|
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.23",
|
|
4
4
|
"description": "A Codex harness with a streamlined system prompt 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",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"@deepseek-ai/dsh-credentials": ">=0.1.0-rc.8",
|
|
90
90
|
"@deepseek-ai/dsh-fs": ">=0.0.1-rc.1",
|
|
91
91
|
"@deepseek-ai/dsh-invariants": ">=0.0.1-rc.1",
|
|
92
|
+
"@deepseek-ai/dsh-jobs": ">=0.1.0-rc.8",
|
|
92
93
|
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.8",
|
|
93
94
|
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.8",
|
|
94
95
|
"@deepseek-ai/dsh-sandbox": ">=0.0.1-rc.1",
|
|
@@ -99,6 +100,7 @@
|
|
|
99
100
|
"@deepseek-ai/dsh-tool-web": ">=0.1.0-rc.8",
|
|
100
101
|
"@deepseek-ai/dsh-tool-todo": ">=0.0.1-rc.1",
|
|
101
102
|
"@deepseek-ai/dsh-terminal": ">=0.0.1-rc.3",
|
|
103
|
+
"@deepseek-ai/dsh-tool-jobs": ">=0.1.0-rc.8",
|
|
102
104
|
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1"
|
|
103
105
|
},
|
|
104
106
|
"peerDependenciesMeta": {
|
|
@@ -132,6 +134,9 @@
|
|
|
132
134
|
"@deepseek-ai/dsh-invariants": {
|
|
133
135
|
"optional": true
|
|
134
136
|
},
|
|
137
|
+
"@deepseek-ai/dsh-jobs": {
|
|
138
|
+
"optional": true
|
|
139
|
+
},
|
|
135
140
|
"@deepseek-ai/dsh-llm": {
|
|
136
141
|
"optional": true
|
|
137
142
|
},
|
|
@@ -162,6 +167,9 @@
|
|
|
162
167
|
"@deepseek-ai/dsh-terminal": {
|
|
163
168
|
"optional": true
|
|
164
169
|
},
|
|
170
|
+
"@deepseek-ai/dsh-tool-jobs": {
|
|
171
|
+
"optional": true
|
|
172
|
+
},
|
|
165
173
|
"@deepseek-ai/dsh-tools": {
|
|
166
174
|
"optional": true
|
|
167
175
|
}
|
|
@@ -177,6 +185,7 @@
|
|
|
177
185
|
"@deepseek-ai/dsh-credentials": "0.1.0-rc.8",
|
|
178
186
|
"@deepseek-ai/dsh-fs": "0.1.0-rc.6",
|
|
179
187
|
"@deepseek-ai/dsh-invariants": "0.1.0-rc.6",
|
|
188
|
+
"@deepseek-ai/dsh-jobs": "0.1.0-rc.8",
|
|
180
189
|
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
|
181
190
|
"@deepseek-ai/dsh-settings": "0.1.0-rc.8",
|
|
182
191
|
"@deepseek-ai/dsh-sandbox": "0.1.0-rc.6",
|
|
@@ -187,6 +196,7 @@
|
|
|
187
196
|
"@deepseek-ai/dsh-tool-web": "0.1.0-rc.8",
|
|
188
197
|
"@deepseek-ai/dsh-tool-todo": "0.1.0-rc.6",
|
|
189
198
|
"@deepseek-ai/dsh-terminal": "0.1.0-rc.6",
|
|
199
|
+
"@deepseek-ai/dsh-tool-jobs": "0.1.0-rc.8",
|
|
190
200
|
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
191
201
|
"@types/node": "^22.20.0",
|
|
192
202
|
"@types/react": "~18.3.1",
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
config:
|
|
22
22
|
collaborationPrompt: false
|
|
23
23
|
|
|
24
|
+
# Background jobs use the host-plane registry. A busy agent receives a
|
|
25
|
+
# completion in its next step; an idle agent keeps it pending until the next
|
|
26
|
+
# user/tool wake, so finishing work never opens an unsolicited model turn.
|
|
27
|
+
- id: codex-jobs
|
|
28
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
29
|
+
config:
|
|
30
|
+
completionDelivery: quiet
|
|
31
|
+
|
|
24
32
|
# The generic DSH pi-ai adapter still owns the user's configured provider,
|
|
25
33
|
# endpoint, API key, and model. Codex's remote-first transport wrapper is
|
|
26
34
|
# installed only in this Codex scope and falls back to the generic DSH path.
|
package/presets/codex/preset.yml
CHANGED