@shuind/dsh-codex-harness 0.1.23 → 0.1.25
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 -5
- package/lib/client.js +61 -14
- package/lib/index.js +70 -28
- package/lib/installer.js +23 -15
- package/lib/types/activity-types.d.ts +1 -1
- package/lib/types/activity.d.ts +4 -3
- package/lib/types/activity.js +30 -4
- package/lib/types/client/activity.d.ts +13 -2
- package/lib/types/client/activity.js +45 -1
- package/lib/types/client/index.d.ts +2 -1
- package/lib/types/client/index.js +28 -18
- package/lib/types/context.d.ts +3 -0
- package/lib/types/context.js +9 -0
- package/lib/types/exec.d.ts +1 -1
- package/lib/types/exec.js +9 -4
- package/lib/types/index.js +3 -2
- package/lib/types/installer.d.ts +2 -0
- package/lib/types/installer.js +22 -14
- package/lib/types/patch.js +3 -11
- package/lib/types/remote.js +37 -5
- package/package.json +2 -1
- package/presets/codex-collaboration/agent.cordis.yml +91 -0
- package/presets/codex-collaboration/preset.yml +3 -0
package/README.md
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
- **旧版 Web 兼容**:在未提供新版模型设置/上下文设置 slot 的 DSH Web 中,Fast 和上下文容量会自动回退到旧版 composer slot;client 注入不依赖纯类型 slot 包。
|
|
13
13
|
- **Responses 原生 apply_patch**:在 GPT Responses 请求的插件传输边界,把普通 JSON function tool 改写为 `type: "custom"` + OpenAI `lark` grammar;下一轮历史同步改写为 `custom_tool_call`。如果中转站拒绝 custom tool,则自动回退到普通 function tool,不改变 DSH 工具执行器。
|
|
14
14
|
- **远程搜索与压缩**:OpenAI Responses 请求默认优先使用 hosted `web_search` 和 `/responses/compact`;失败时回退到 DSH 的本地实现。
|
|
15
|
+
- **远程压缩用量**:`/responses/compact` 返回标准 `usage` 时,插件会把 `input_tokens`、缓存读写和 `output_tokens` 原样拆分为 DSH 的用量字段;不再把远程压缩伪造成 `0`。中转站缺失或返回不一致的 usage 时,插件不会编造数字。
|
|
15
16
|
- **上下文容量**:在 Web 中设置 `1K`–`1000K` tokens,设置值作用于下一次请求。
|
|
16
|
-
- **活动状态**:Codex
|
|
17
|
+
- **活动状态**:Codex 请求模型、接收模型回复或进行上下文压缩时,在 `Deep diving...` 右侧显示对应阶段和耗时,让长时间 Deep Diving 不再像卡住。
|
|
17
18
|
|
|
18
19
|
## 安装
|
|
19
20
|
|
|
20
21
|
```sh
|
|
21
|
-
dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.
|
|
22
|
+
dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.24
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
重启 Web,创建新会话,在模式菜单中选择 **Codex 模式**。
|
|
@@ -57,15 +58,20 @@ Fast 设置保存在 `codex` 命名空间,只对 Codex preset 生效。
|
|
|
57
58
|
|
|
58
59
|
### 活动状态
|
|
59
60
|
|
|
60
|
-
Codex
|
|
61
|
+
Codex 发起模型请求时,Web 会在 `Deep diving...` 右侧显示 `请求模型... · Ns`;收到首个非空模型 token(非流式适配器则收到 `assistant/message`)后切换为 `模型回复... · Ns`。模型回复结束、进入工具调用、压缩结束或下一轮开始后自动隐藏。该状态由 Host 的 `sessionProjections` 投影驱动,因此本地压缩和远程 `/responses/compact` 都会显示同一种状态。
|
|
61
62
|
|
|
62
63
|
如果当前 DSH 组合没有加载 projection 服务,插件会跳过注册,客户端也会自动隐藏这行,不影响普通会话。
|
|
63
64
|
|
|
64
65
|
## 随包提供的 Codex preset
|
|
65
66
|
|
|
66
|
-
安装插件后,首次启动对应的 DSH profile
|
|
67
|
+
安装插件后,首次启动对应的 DSH profile 时,插件会安装两套随包提供的 preset:
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
- **Codex 模式**:使用精简版 Codex 提示词。
|
|
70
|
+
- **Codex 协作模式**:在精简版 Codex 提示词后追加协作提示词。
|
|
71
|
+
|
|
72
|
+
它们分别位于 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/codex/` 和 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/codex-collaboration/`。如果对应目录已经存在,插件会保留你的文件,不会覆盖自定义内容。
|
|
73
|
+
|
|
74
|
+
在 Web 的 **Agent 预设** 中选择 **Codex 模式** 或 **Codex 协作模式**,再新建会话即可使用随包提供的 Codex 工具、网页搜索、远程压缩和 Skills 组合。两套 preset 共用 Fast、思考强度、GPT 图片输入和上下文容量设置;只有系统提示词是否包含协作指导不同。
|
|
69
75
|
|
|
70
76
|
## 自定义 preset
|
|
71
77
|
|
package/lib/client.js
CHANGED
|
@@ -13,16 +13,56 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
/** User-selectable Codex context controls use whole K-token units. */
|
|
14
14
|
const CODEX_CONTEXT_UNIT = 1e3;
|
|
15
15
|
const CODEX_CONTEXT_MAX = 1e6;
|
|
16
|
+
const CODEX_PRESET_IDS = /* @__PURE__ */ new Set(["codex", "codex-collaboration"]);
|
|
17
|
+
/** Both shipped presets use the same Codex request controls in Web. */
|
|
18
|
+
function isCodexPresetId(value) {
|
|
19
|
+
return value !== void 0 && CODEX_PRESET_IDS.has(value);
|
|
20
|
+
}
|
|
16
21
|
//#endregion
|
|
17
22
|
//#region lib/types/client/activity.js
|
|
18
23
|
/** Client-only inference used while the host activity projection frame is in flight. */
|
|
19
|
-
/** Infer the
|
|
24
|
+
/** Infer the request-phase clock from the existing conversation snapshot. */
|
|
20
25
|
function awaitingModelStartedAt(snapshot) {
|
|
21
26
|
if (!snapshot.running || snapshot.runningCalls.length > 0) return void 0;
|
|
22
27
|
let latest;
|
|
23
28
|
for (const timing of snapshot.turnTimings.values()) if (timing.endTime === void 0) latest = timing.startTime;
|
|
24
29
|
return latest;
|
|
25
30
|
}
|
|
31
|
+
/** Infer the first non-empty model token timestamp from the current snapshot. */
|
|
32
|
+
function modelReplyStartedAt(snapshot) {
|
|
33
|
+
if (!snapshot.running || snapshot.runningCalls.length > 0) return void 0;
|
|
34
|
+
let openTurn;
|
|
35
|
+
for (const [turn, timing] of snapshot.turnTimings) if (timing.endTime === void 0) openTurn = {
|
|
36
|
+
turn,
|
|
37
|
+
startTime: timing.startTime
|
|
38
|
+
};
|
|
39
|
+
if (openTurn === void 0) return void 0;
|
|
40
|
+
if (snapshot.partial !== null && snapshot.partial.turn === openTurn.turn) {
|
|
41
|
+
const partialReplyStartedAt = snapshot.partial.replyStartedAt;
|
|
42
|
+
if (partialReplyStartedAt !== void 0) return partialReplyStartedAt;
|
|
43
|
+
}
|
|
44
|
+
const chat = snapshot.chat;
|
|
45
|
+
const step = (chat?.timeline.turns.get(openTurn.turn))?.steps.findLast((candidate) => candidate.end === void 0);
|
|
46
|
+
if (step === void 0) return void 0;
|
|
47
|
+
for (const node of [...chat?.nodes.values() ?? []].reverse()) {
|
|
48
|
+
if (node.kind !== "assistant-step") continue;
|
|
49
|
+
const data = node.data;
|
|
50
|
+
if (data.turn === openTurn.turn && data.step === step.step && typeof data.replyStartedAt === "number") return data.replyStartedAt;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Infer the two-phase model activity from the current client snapshot. */
|
|
54
|
+
function modelActivity(snapshot) {
|
|
55
|
+
const startedAt = awaitingModelStartedAt(snapshot);
|
|
56
|
+
if (startedAt === void 0) return void 0;
|
|
57
|
+
const replyStartedAt = modelReplyStartedAt(snapshot);
|
|
58
|
+
return replyStartedAt === void 0 ? {
|
|
59
|
+
activity: "requesting-model",
|
|
60
|
+
startedAt
|
|
61
|
+
} : {
|
|
62
|
+
activity: "model-reply",
|
|
63
|
+
startedAt: replyStartedAt
|
|
64
|
+
};
|
|
65
|
+
}
|
|
26
66
|
//#endregion
|
|
27
67
|
//#region lib/types/client/index.js
|
|
28
68
|
/** Browser controls for the Codex request settings mounted by the Host plugin. */
|
|
@@ -45,7 +85,8 @@ window.__ModuleLoader__.load({
|
|
|
45
85
|
contextSizeDescription: "Next request capacity in K tokens. The meter above is current.",
|
|
46
86
|
contextRestore: "Restore model default",
|
|
47
87
|
activityCompacting: "Compacting context...",
|
|
48
|
-
|
|
88
|
+
activityRequestingModel: "Requesting model...",
|
|
89
|
+
activityModelReply: "Model replying..."
|
|
49
90
|
};
|
|
50
91
|
const zh = {
|
|
51
92
|
fast: "Fast",
|
|
@@ -57,13 +98,14 @@ window.__ModuleLoader__.load({
|
|
|
57
98
|
contextSizeDescription: "设置下次请求的上下文容量,单位为 K tokens;上方是当前请求。",
|
|
58
99
|
contextRestore: "恢复模型默认值",
|
|
59
100
|
activityCompacting: "正在压缩上下文...",
|
|
60
|
-
|
|
101
|
+
activityRequestingModel: "请求模型...",
|
|
102
|
+
activityModelReply: "模型回复..."
|
|
61
103
|
};
|
|
62
104
|
function FastModeButton({ sessionId, useSessions, useSettings, setSetting, t }) {
|
|
63
105
|
const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
|
|
64
106
|
const snapshot = useSettings((state) => state);
|
|
65
107
|
const fast = snapshot.value?.fast ?? false;
|
|
66
|
-
if (agentPreset
|
|
108
|
+
if (!isCodexPresetId(agentPreset)) return null;
|
|
67
109
|
return (0, react_jsx_runtime.jsxs)("button", {
|
|
68
110
|
type: "button",
|
|
69
111
|
role: "menuitemcheckbox",
|
|
@@ -108,7 +150,7 @@ window.__ModuleLoader__.load({
|
|
|
108
150
|
const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
|
|
109
151
|
const snapshot = useSettings((state) => state);
|
|
110
152
|
const fast = snapshot.value?.fast ?? false;
|
|
111
|
-
if (agentPreset
|
|
153
|
+
if (!isCodexPresetId(agentPreset)) return null;
|
|
112
154
|
return (0, react_jsx_runtime.jsxs)("button", {
|
|
113
155
|
type: "button",
|
|
114
156
|
role: "switch",
|
|
@@ -278,22 +320,27 @@ window.__ModuleLoader__.load({
|
|
|
278
320
|
function ActivityLine({ sessionId, useSession, useSessions, useProjection, t }) {
|
|
279
321
|
const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
|
|
280
322
|
const projectedActivity = useProjection("codexActivity");
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
activity: "
|
|
284
|
-
startedAt:
|
|
285
|
-
} :
|
|
323
|
+
const fallbackActivity = useSession((snapshot) => modelActivity(snapshot));
|
|
324
|
+
const normalizedProjectedActivity = projectedActivity?.activity === "awaiting-model" ? {
|
|
325
|
+
activity: fallbackActivity?.activity ?? "requesting-model",
|
|
326
|
+
startedAt: fallbackActivity?.startedAt ?? projectedActivity.startedAt
|
|
327
|
+
} : projectedActivity;
|
|
328
|
+
const activity = projectedActivity !== void 0 ? normalizedProjectedActivity : isCodexPresetId(agentPreset) && fallbackActivity !== void 0 ? fallbackActivity : null;
|
|
286
329
|
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
287
330
|
(0, react.useEffect)(() => {
|
|
288
|
-
if (agentPreset
|
|
331
|
+
if (!isCodexPresetId(agentPreset) || activity === void 0 || activity === null) return void 0;
|
|
289
332
|
setNow(Date.now());
|
|
290
333
|
const timer = setInterval(() => {
|
|
291
334
|
setNow(Date.now());
|
|
292
335
|
}, 1e3);
|
|
293
336
|
return () => clearInterval(timer);
|
|
294
|
-
}, [
|
|
295
|
-
|
|
296
|
-
|
|
337
|
+
}, [
|
|
338
|
+
agentPreset,
|
|
339
|
+
activity?.activity,
|
|
340
|
+
activity?.startedAt
|
|
341
|
+
]);
|
|
342
|
+
if (!isCodexPresetId(agentPreset) || activity === void 0 || activity === null) return null;
|
|
343
|
+
const label = activity.activity === "compaction" ? t("activityCompacting") : activity.activity === "model-reply" ? t("activityModelReply") : t("activityRequestingModel");
|
|
297
344
|
return (0, react_jsx_runtime.jsxs)("span", {
|
|
298
345
|
id: "codex-activity",
|
|
299
346
|
role: "status",
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,7 @@ 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 { isTokenDelta } from "@deepseek-ai/dsh-llm/message";
|
|
8
9
|
import { z as z$1 } from "zod";
|
|
9
10
|
//#region lib/types/context.js
|
|
10
11
|
const CODEX_CONTEXT_MAX = 1e6;
|
|
@@ -13,9 +14,8 @@ const CODEX_CONTEXT_MAX = 1e6;
|
|
|
13
14
|
/** Parser and line-oriented applicator for Codex's `apply_patch` language. */
|
|
14
15
|
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
16
|
/** The grammar sent to providers that support OpenAI custom grammar tools. */
|
|
16
|
-
const APPLY_PATCH_GRAMMAR = String.raw`start: begin_patch
|
|
17
|
+
const APPLY_PATCH_GRAMMAR = String.raw`start: begin_patch hunk+ end_patch
|
|
17
18
|
begin_patch: "*** Begin Patch" LF
|
|
18
|
-
environment_id: "*** Environment ID: " filename LF
|
|
19
19
|
end_patch: "*** End Patch" LF?
|
|
20
20
|
hunk: add_hunk | delete_hunk | update_hunk
|
|
21
21
|
add_hunk: "*** Add File: " filename LF add_line+
|
|
@@ -37,7 +37,6 @@ function isFileHeader(line) {
|
|
|
37
37
|
const trimmed = line.trim();
|
|
38
38
|
return trimmed.startsWith("*** Add File: ") || trimmed.startsWith("*** Delete File: ") || trimmed.startsWith("*** Update File: ");
|
|
39
39
|
}
|
|
40
|
-
const ENVIRONMENT_ID_MARKER = "*** Environment ID:";
|
|
41
40
|
function unwrapHeredoc(input) {
|
|
42
41
|
const normalized = input.replaceAll("\r\n", "\n").trim();
|
|
43
42
|
const lines = normalized.split("\n");
|
|
@@ -66,11 +65,7 @@ function parsePatch(input) {
|
|
|
66
65
|
const files = [];
|
|
67
66
|
let index = 1;
|
|
68
67
|
const end = lines.length - 1;
|
|
69
|
-
if (lines[index]?.trim().startsWith(
|
|
70
|
-
if (lines[index].trim().slice(19).trim().length === 0) invalid("environment id cannot be empty");
|
|
71
|
-
index++;
|
|
72
|
-
if (lines[index]?.trim().startsWith(ENVIRONMENT_ID_MARKER) === true) invalid("environment id cannot be specified more than once");
|
|
73
|
-
}
|
|
68
|
+
if (lines[index]?.trim().startsWith("*** Environment ID:") === true) invalid("environment selection is unavailable in this single-environment DSH plugin");
|
|
74
69
|
while (index < end) {
|
|
75
70
|
const header = lines[index++]?.trim();
|
|
76
71
|
if (header === void 0) invalid("unexpected end of input");
|
|
@@ -218,6 +213,7 @@ function applyPatchHunks(original, hunks, label = "file") {
|
|
|
218
213
|
/** Codex `exec_command` and `write_stdin` execution over dsh capability seams. */
|
|
219
214
|
const STATES = /* @__PURE__ */ new WeakMap();
|
|
220
215
|
const MIN_YIELD_TIME_MS = 250;
|
|
216
|
+
const WINDOWS_INITIAL_EXEC_YIELD_TIME_MS = 1e4;
|
|
221
217
|
const MIN_EMPTY_YIELD_TIME_MS = 5e3;
|
|
222
218
|
const MAX_YIELD_TIME_MS = 3e4;
|
|
223
219
|
const MAX_EMPTY_YIELD_TIME_MS = 3e5;
|
|
@@ -236,8 +232,9 @@ function stateFor(agent) {
|
|
|
236
232
|
function positiveFinite(name, value) {
|
|
237
233
|
if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new Error(`${name} must be a non-negative finite number`);
|
|
238
234
|
}
|
|
239
|
-
function normalizeWaitMs(value, fallback, empty = false) {
|
|
240
|
-
|
|
235
|
+
function normalizeWaitMs(value, fallback, empty = false, initialExec = false) {
|
|
236
|
+
const lower = empty ? MIN_EMPTY_YIELD_TIME_MS : initialExec && process.platform === "win32" ? WINDOWS_INITIAL_EXEC_YIELD_TIME_MS : MIN_YIELD_TIME_MS;
|
|
237
|
+
return Math.max(lower, Math.min(empty ? MAX_EMPTY_YIELD_TIME_MS : MAX_YIELD_TIME_MS, Math.trunc(value ?? fallback)));
|
|
241
238
|
}
|
|
242
239
|
function outputLimit(maxOutputBytes, maxOutputTokens) {
|
|
243
240
|
const tokens = maxOutputTokens ?? DEFAULT_OUTPUT_TOKENS;
|
|
@@ -369,7 +366,7 @@ async function runExecCommand(ctx, args, exec, config) {
|
|
|
369
366
|
const sendRequest = {
|
|
370
367
|
text: commandFor(args),
|
|
371
368
|
submit: true,
|
|
372
|
-
waitMs: normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs),
|
|
369
|
+
waitMs: normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs, false, true),
|
|
373
370
|
signal: exec.signal
|
|
374
371
|
};
|
|
375
372
|
const result = await terminals.startSend(agent, spawned.sessionId, sendRequest).done;
|
|
@@ -432,7 +429,7 @@ async function runExecCommand(ctx, args, exec, config) {
|
|
|
432
429
|
signal: exec.signal
|
|
433
430
|
}));
|
|
434
431
|
try {
|
|
435
|
-
await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs), exec.signal);
|
|
432
|
+
await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs, false, true), exec.signal);
|
|
436
433
|
const output = readShellOutput(process.readOutput(), maxBytes);
|
|
437
434
|
if (process.status === "running") {
|
|
438
435
|
if (exec.agent === void 0) {
|
|
@@ -899,6 +896,29 @@ function compactText(body) {
|
|
|
899
896
|
const encrypted = text.join("\n\n").trim();
|
|
900
897
|
return encrypted.length === 0 ? "" : `${REMOTE_COMPACTION_OPEN}${encrypted}${REMOTE_COMPACTION_CLOSE}`;
|
|
901
898
|
}
|
|
899
|
+
function nonNegativeInteger(value) {
|
|
900
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
901
|
+
}
|
|
902
|
+
/** Map the Responses compact usage object without inventing missing counts. */
|
|
903
|
+
function compactUsage(body) {
|
|
904
|
+
if (typeof body.usage !== "object" || body.usage === null) return void 0;
|
|
905
|
+
const raw = body.usage;
|
|
906
|
+
const inputTokens = nonNegativeInteger(raw.input_tokens);
|
|
907
|
+
const outputTokens = nonNegativeInteger(raw.output_tokens);
|
|
908
|
+
if (inputTokens === void 0 || outputTokens === void 0) return void 0;
|
|
909
|
+
const details = typeof raw.input_tokens_details === "object" && raw.input_tokens_details !== null ? raw.input_tokens_details : void 0;
|
|
910
|
+
const cacheReadTokens = nonNegativeInteger(details?.cached_tokens);
|
|
911
|
+
const cacheWriteTokens = nonNegativeInteger(details?.cache_write_tokens);
|
|
912
|
+
const cached = cacheReadTokens ?? 0;
|
|
913
|
+
const writes = cacheWriteTokens ?? 0;
|
|
914
|
+
if (cached + writes > inputTokens) return void 0;
|
|
915
|
+
return {
|
|
916
|
+
inputTokens: inputTokens - cached - writes,
|
|
917
|
+
outputTokens,
|
|
918
|
+
...cacheReadTokens !== void 0 && cacheReadTokens > 0 ? { cacheReadTokens } : {},
|
|
919
|
+
...cacheWriteTokens !== void 0 && cacheWriteTokens > 0 ? { cacheWriteTokens } : {}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
902
922
|
async function remoteCompact(ctx, options) {
|
|
903
923
|
const profile = profileOf(ctx, options.provider);
|
|
904
924
|
if (!supportsResponses(profile, options.provider) || profile?.baseURL === void 0) throw new Error("Codex remote compaction requires an OpenAI Responses provider with baseURL");
|
|
@@ -911,15 +931,20 @@ async function remoteCompact(ctx, options) {
|
|
|
911
931
|
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
912
932
|
});
|
|
913
933
|
if (!response.ok) throw new Error(`remote compaction returned HTTP ${response.status}`);
|
|
914
|
-
const
|
|
934
|
+
const body = await response.json();
|
|
935
|
+
const text = compactText(body);
|
|
915
936
|
if (text.length === 0) throw new Error("remote compaction returned no compaction text");
|
|
916
|
-
|
|
937
|
+
const usage = compactUsage(body);
|
|
938
|
+
return {
|
|
939
|
+
text,
|
|
940
|
+
...usage === void 0 ? {} : { usage }
|
|
941
|
+
};
|
|
917
942
|
}
|
|
918
943
|
/** Remote-first compaction waterfall with the existing DSH path as fallback. */
|
|
919
944
|
function remoteCompactStream(ctx, options, next) {
|
|
920
945
|
return (async function* () {
|
|
921
946
|
try {
|
|
922
|
-
const
|
|
947
|
+
const result = await remoteCompact(ctx, options);
|
|
923
948
|
yield {
|
|
924
949
|
type: "block-start",
|
|
925
950
|
index: 0,
|
|
@@ -928,22 +953,19 @@ function remoteCompactStream(ctx, options, next) {
|
|
|
928
953
|
yield {
|
|
929
954
|
type: "text-delta",
|
|
930
955
|
index: 0,
|
|
931
|
-
text
|
|
956
|
+
text: result.text
|
|
932
957
|
};
|
|
933
958
|
yield {
|
|
934
959
|
type: "block-end",
|
|
935
960
|
index: 0,
|
|
936
961
|
block: {
|
|
937
962
|
type: "text",
|
|
938
|
-
text
|
|
963
|
+
text: result.text
|
|
939
964
|
}
|
|
940
965
|
};
|
|
941
|
-
yield {
|
|
966
|
+
if (result.usage !== void 0) yield {
|
|
942
967
|
type: "usage",
|
|
943
|
-
usage:
|
|
944
|
-
inputTokens: 0,
|
|
945
|
-
outputTokens: 0
|
|
946
|
-
}
|
|
968
|
+
usage: result.usage
|
|
947
969
|
};
|
|
948
970
|
yield {
|
|
949
971
|
type: "finish",
|
|
@@ -959,13 +981,28 @@ function remoteCompactStream(ctx, options, next) {
|
|
|
959
981
|
}
|
|
960
982
|
//#endregion
|
|
961
983
|
//#region lib/types/activity.js
|
|
962
|
-
/** Host-side activity projection for the Codex
|
|
984
|
+
/** Host-side activity projection for the Codex status line. */
|
|
985
|
+
/** Only a non-empty model delta starts the reply phase; usage and finish frames do not. */
|
|
986
|
+
function startsModelReply(event) {
|
|
987
|
+
if (event.type === "assistant/message") return true;
|
|
988
|
+
if (event.type !== "assistant/chunk" || typeof event.data !== "object" || event.data === null) return false;
|
|
989
|
+
const chunk = event.data.chunk;
|
|
990
|
+
return chunk !== void 0 && isTokenDelta(chunk);
|
|
991
|
+
}
|
|
963
992
|
/** Persisted and wire shape of the Codex activity value. */
|
|
964
993
|
const codexActivitySchema = z$1.union([
|
|
965
994
|
z$1.object({
|
|
966
995
|
activity: z$1.literal("compaction"),
|
|
967
996
|
startedAt: z$1.number().int().nonnegative()
|
|
968
997
|
}),
|
|
998
|
+
z$1.object({
|
|
999
|
+
activity: z$1.literal("requesting-model"),
|
|
1000
|
+
startedAt: z$1.number().int().nonnegative()
|
|
1001
|
+
}),
|
|
1002
|
+
z$1.object({
|
|
1003
|
+
activity: z$1.literal("model-reply"),
|
|
1004
|
+
startedAt: z$1.number().int().nonnegative()
|
|
1005
|
+
}),
|
|
969
1006
|
z$1.object({
|
|
970
1007
|
activity: z$1.literal("awaiting-model"),
|
|
971
1008
|
startedAt: z$1.number().int().nonnegative()
|
|
@@ -979,11 +1016,15 @@ function applyCodexActivity(state, event) {
|
|
|
979
1016
|
startedAt: event.time
|
|
980
1017
|
};
|
|
981
1018
|
if (event.type === "step/start" || event.type === "llm/retry-started") return {
|
|
982
|
-
activity: "
|
|
1019
|
+
activity: "requesting-model",
|
|
1020
|
+
startedAt: event.time
|
|
1021
|
+
};
|
|
1022
|
+
if (startsModelReply(event) && (state?.activity === "requesting-model" || state?.activity === "awaiting-model")) return {
|
|
1023
|
+
activity: "model-reply",
|
|
983
1024
|
startedAt: event.time
|
|
984
1025
|
};
|
|
985
1026
|
if (event.type === "turn/start" || event.type === "turn/end" || event.type === "compaction/end") return null;
|
|
986
|
-
if (state?.activity === "awaiting-model" && (event.type === "llm/retry" || event.type === "tool/call" || event.type === "step/end")) return null;
|
|
1027
|
+
if ((state?.activity === "requesting-model" || state?.activity === "model-reply" || state?.activity === "awaiting-model") && (event.type === "llm/retry" || event.type === "tool/call" || event.type === "step/end")) return null;
|
|
987
1028
|
return state;
|
|
988
1029
|
}
|
|
989
1030
|
const codexActivityProjection = {
|
|
@@ -997,7 +1038,7 @@ const codexActivityProjection = {
|
|
|
997
1038
|
},
|
|
998
1039
|
schema: codexActivitySchema,
|
|
999
1040
|
view: (state) => state,
|
|
1000
|
-
stateVersion:
|
|
1041
|
+
stateVersion: 2
|
|
1001
1042
|
};
|
|
1002
1043
|
/** Register the optional activity unit without requiring the projection seam in every assembly. */
|
|
1003
1044
|
function registerCodexActivityProjection(ctx) {
|
|
@@ -1018,12 +1059,13 @@ const inject = [
|
|
|
1018
1059
|
"credentials",
|
|
1019
1060
|
"settings"
|
|
1020
1061
|
];
|
|
1062
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 1048576;
|
|
1021
1063
|
/** Runtime configuration schema for the Codex tool bridge. */
|
|
1022
1064
|
const Config = z.object({
|
|
1023
1065
|
defaultYieldTimeMs: z.number().step(1).min(0).default(1e4),
|
|
1024
1066
|
pollYieldTimeMs: z.number().step(1).min(0).default(5e3),
|
|
1025
1067
|
writeYieldTimeMs: z.number().step(1).min(0).default(250),
|
|
1026
|
-
maxOutputBytes: z.number().step(1).min(1).default(
|
|
1068
|
+
maxOutputBytes: z.number().step(1).min(1).default(DEFAULT_MAX_OUTPUT_BYTES),
|
|
1027
1069
|
hostedWebSearch: z.boolean().default(true),
|
|
1028
1070
|
remoteCompact: z.boolean().default(true),
|
|
1029
1071
|
collaborationPrompt: z.boolean().default(false)
|
|
@@ -1566,7 +1608,7 @@ function apply(ctx, config = {}) {
|
|
|
1566
1608
|
defaultYieldTimeMs: config.defaultYieldTimeMs ?? 1e4,
|
|
1567
1609
|
pollYieldTimeMs: config.pollYieldTimeMs ?? 5e3,
|
|
1568
1610
|
writeYieldTimeMs: config.writeYieldTimeMs ?? 250,
|
|
1569
|
-
maxOutputBytes: config.maxOutputBytes ??
|
|
1611
|
+
maxOutputBytes: config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
|
|
1570
1612
|
hostedWebSearch: config.hostedWebSearch ?? true,
|
|
1571
1613
|
remoteCompact: config.remoteCompact ?? true,
|
|
1572
1614
|
collaborationPrompt: config.collaborationPrompt ?? false
|
package/lib/installer.js
CHANGED
|
@@ -4,9 +4,9 @@ import { dirname, join, resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region lib/types/installer.js
|
|
6
6
|
/** Install the user-visible Codex agent preset supplied by this bundle. */
|
|
7
|
-
const PRESET_ID = "codex";
|
|
8
7
|
const PRESET_FILES = ["agent.cordis.yml", "preset.yml"];
|
|
9
8
|
const SOURCE_PRESET_DIR = fileURLToPath(new URL("../presets/codex/", import.meta.url));
|
|
9
|
+
const SOURCE_COLLABORATION_PRESET_DIR = fileURLToPath(new URL("../presets/codex-collaboration/", import.meta.url));
|
|
10
10
|
function dshHomePath(...segments) {
|
|
11
11
|
const configured = process.env.DSH_HOME?.trim();
|
|
12
12
|
const expanded = configured === void 0 || configured.length === 0 ? join(homedir(), ".dsh") : configured === "~" ? homedir() : configured.startsWith("~/") || configured.startsWith("~\\") ? join(homedir(), configured.slice(2)) : configured;
|
|
@@ -14,21 +14,11 @@ function dshHomePath(...segments) {
|
|
|
14
14
|
}
|
|
15
15
|
/** Bundle plugin name for the preset installer. */
|
|
16
16
|
const name = "codex-preset-installer";
|
|
17
|
-
|
|
18
|
-
* Install the shipped Codex preset only when the user has not authored one.
|
|
19
|
-
*
|
|
20
|
-
* The directory is committed with a staging rename so a failed copy cannot
|
|
21
|
-
* leave a half-written preset that hides the mode from the roster. Existing
|
|
22
|
-
* directories are intentionally preserved, including user customizations.
|
|
23
|
-
*
|
|
24
|
-
* @param targetDir - destination preset directory.
|
|
25
|
-
* @param sourceDir - directory containing the packaged preset files.
|
|
26
|
-
*/
|
|
27
|
-
function installCodexPreset(targetDir = dshHomePath(".agent-presets", PRESET_ID), sourceDir = SOURCE_PRESET_DIR) {
|
|
17
|
+
function installPreset(targetDir, sourceDir, presetId) {
|
|
28
18
|
if (existsSync(targetDir)) return;
|
|
29
19
|
const parentDir = dirname(targetDir);
|
|
30
20
|
mkdirSync(parentDir, { recursive: true });
|
|
31
|
-
const stagingDir = mkdtempSync(join(parentDir, `.${
|
|
21
|
+
const stagingDir = mkdtempSync(join(parentDir, `.${presetId}-`));
|
|
32
22
|
try {
|
|
33
23
|
for (const file of PRESET_FILES) copyFileSync(join(sourceDir, file), join(stagingDir, file));
|
|
34
24
|
try {
|
|
@@ -43,12 +33,30 @@ function installCodexPreset(targetDir = dshHomePath(".agent-presets", PRESET_ID)
|
|
|
43
33
|
});
|
|
44
34
|
}
|
|
45
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Install the shipped Codex preset only when the user has not authored one.
|
|
38
|
+
*
|
|
39
|
+
* The directory is committed with a staging rename so a failed copy cannot
|
|
40
|
+
* leave a half-written preset that hides the mode from the roster. Existing
|
|
41
|
+
* directories are intentionally preserved, including user customizations.
|
|
42
|
+
*
|
|
43
|
+
* @param targetDir - destination preset directory.
|
|
44
|
+
* @param sourceDir - directory containing the packaged preset files.
|
|
45
|
+
*/
|
|
46
|
+
function installCodexPreset(targetDir = dshHomePath(".agent-presets", "codex"), sourceDir = SOURCE_PRESET_DIR) {
|
|
47
|
+
installPreset(targetDir, sourceDir, "codex");
|
|
48
|
+
}
|
|
49
|
+
/** Install the shipped Codex preset with the optional collaboration guidance enabled. */
|
|
50
|
+
function installCodexCollaborationPreset(targetDir = dshHomePath(".agent-presets", "codex-collaboration"), sourceDir = SOURCE_COLLABORATION_PRESET_DIR) {
|
|
51
|
+
installPreset(targetDir, sourceDir, "codex-collaboration");
|
|
52
|
+
}
|
|
46
53
|
/** Install the preset during profile boot without changing the host tool catalog. */
|
|
47
54
|
function apply(ctx) {
|
|
48
55
|
try {
|
|
49
56
|
installCodexPreset();
|
|
57
|
+
installCodexCollaborationPreset();
|
|
50
58
|
} catch (error) {
|
|
51
|
-
ctx.logger.warn(`dsh-codex: could not install the Codex
|
|
59
|
+
ctx.logger.warn(`dsh-codex: could not install the Codex presets: ${String(error)}`);
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
var installer_default = {
|
|
@@ -56,4 +64,4 @@ var installer_default = {
|
|
|
56
64
|
apply
|
|
57
65
|
};
|
|
58
66
|
//#endregion
|
|
59
|
-
export { apply, installer_default as default, installCodexPreset, name };
|
|
67
|
+
export { apply, installer_default as default, installCodexCollaborationPreset, installCodexPreset, name };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Pure client/host-shared type for the Codex activity projection value. */
|
|
2
2
|
export type CodexActivity = {
|
|
3
|
-
activity: 'compaction' | 'awaiting-model';
|
|
3
|
+
activity: 'compaction' | 'requesting-model' | 'model-reply' | 'awaiting-model';
|
|
4
4
|
startedAt: number;
|
|
5
5
|
};
|
|
6
6
|
//# sourceMappingURL=activity-types.d.ts.map
|
package/lib/types/activity.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
/** Host-side activity projection for the Codex
|
|
1
|
+
/** Host-side activity projection for the Codex status line. */
|
|
2
2
|
import type { Context } from '@deepseek-ai/cordis';
|
|
3
3
|
import { z, type ZodType } from 'zod';
|
|
4
4
|
import type { CodexActivity } from './activity-types.ts';
|
|
5
5
|
export type { CodexActivity } from './activity-types.ts';
|
|
6
|
-
/**
|
|
6
|
+
/** Long-running Codex activities currently surfaced to the client. */
|
|
7
7
|
export type CodexActivityState = CodexActivity | null;
|
|
8
8
|
/** Minimal event shape needed by this projection; no compaction package import is required. */
|
|
9
9
|
export interface CodexActivityEvent {
|
|
10
10
|
type: string;
|
|
11
11
|
time: number;
|
|
12
|
+
data?: unknown;
|
|
12
13
|
}
|
|
13
14
|
/** Persisted and wire shape of the Codex activity value. */
|
|
14
15
|
export declare const codexActivitySchema: ZodType<CodexActivityState>;
|
|
@@ -25,7 +26,7 @@ export declare const codexActivityProjection: {
|
|
|
25
26
|
};
|
|
26
27
|
readonly schema: z.ZodType<CodexActivityState, unknown, z.core.$ZodTypeInternals<CodexActivityState, unknown>>;
|
|
27
28
|
readonly view: (state: CodexActivityState) => CodexActivityState;
|
|
28
|
-
readonly stateVersion:
|
|
29
|
+
readonly stateVersion: 2;
|
|
29
30
|
};
|
|
30
31
|
/** Register the optional activity unit without requiring the projection seam in every assembly. */
|
|
31
32
|
export declare function registerCodexActivityProjection(ctx: Context): void;
|
package/lib/types/activity.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
|
-
/** Host-side activity projection for the Codex
|
|
1
|
+
/** Host-side activity projection for the Codex status line. */
|
|
2
|
+
import { isTokenDelta } from '@deepseek-ai/dsh-llm/message';
|
|
2
3
|
import { z } from 'zod';
|
|
4
|
+
/** Only a non-empty model delta starts the reply phase; usage and finish frames do not. */
|
|
5
|
+
function startsModelReply(event) {
|
|
6
|
+
if (event.type === 'assistant/message')
|
|
7
|
+
return true;
|
|
8
|
+
if (event.type !== 'assistant/chunk' || typeof event.data !== 'object' || event.data === null)
|
|
9
|
+
return false;
|
|
10
|
+
const chunk = event.data.chunk;
|
|
11
|
+
return chunk !== undefined && isTokenDelta(chunk);
|
|
12
|
+
}
|
|
3
13
|
/** Persisted and wire shape of the Codex activity value. */
|
|
4
14
|
export const codexActivitySchema = z.union([
|
|
5
15
|
z.object({
|
|
6
16
|
activity: z.literal('compaction'),
|
|
7
17
|
startedAt: z.number().int().nonnegative(),
|
|
8
18
|
}),
|
|
19
|
+
z.object({
|
|
20
|
+
activity: z.literal('requesting-model'),
|
|
21
|
+
startedAt: z.number().int().nonnegative(),
|
|
22
|
+
}),
|
|
23
|
+
z.object({
|
|
24
|
+
activity: z.literal('model-reply'),
|
|
25
|
+
startedAt: z.number().int().nonnegative(),
|
|
26
|
+
}),
|
|
27
|
+
// Accepted for rolling upgrades: an older Codex host may still send this
|
|
28
|
+
// ambiguous state until its projection definition is refreshed.
|
|
9
29
|
z.object({
|
|
10
30
|
activity: z.literal('awaiting-model'),
|
|
11
31
|
startedAt: z.number().int().nonnegative(),
|
|
@@ -17,14 +37,20 @@ export function applyCodexActivity(state, event) {
|
|
|
17
37
|
if (event.type === 'compaction/start')
|
|
18
38
|
return { activity: 'compaction', startedAt: event.time };
|
|
19
39
|
if (event.type === 'step/start' || event.type === 'llm/retry-started') {
|
|
20
|
-
return { activity: '
|
|
40
|
+
return { activity: 'requesting-model', startedAt: event.time };
|
|
41
|
+
}
|
|
42
|
+
if (startsModelReply(event)
|
|
43
|
+
&& (state?.activity === 'requesting-model' || state?.activity === 'awaiting-model')) {
|
|
44
|
+
return { activity: 'model-reply', startedAt: event.time };
|
|
21
45
|
}
|
|
22
46
|
if (event.type === 'turn/start' || event.type === 'turn/end' || event.type === 'compaction/end')
|
|
23
47
|
return null;
|
|
24
48
|
// Keep the line through reasoning and streamed output. It is the only
|
|
25
49
|
// visible indication that the model is still progressing; tool cards have
|
|
26
50
|
// their own status, so a tool dispatch retires this ambient line.
|
|
27
|
-
if (state?.activity === '
|
|
51
|
+
if ((state?.activity === 'requesting-model'
|
|
52
|
+
|| state?.activity === 'model-reply'
|
|
53
|
+
|| state?.activity === 'awaiting-model')
|
|
28
54
|
&& (event.type === 'llm/retry'
|
|
29
55
|
|| event.type === 'tool/call'
|
|
30
56
|
|| event.type === 'step/end'))
|
|
@@ -44,7 +70,7 @@ export const codexActivityProjection = {
|
|
|
44
70
|
// the `stateSchema`/`wire` split.
|
|
45
71
|
schema: codexActivitySchema,
|
|
46
72
|
view: (state) => state,
|
|
47
|
-
stateVersion:
|
|
73
|
+
stateVersion: 2,
|
|
48
74
|
};
|
|
49
75
|
/** Register the optional activity unit without requiring the projection seam in every assembly. */
|
|
50
76
|
export function registerCodexActivityProjection(ctx) {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/** Client-only inference used while the host activity projection frame is in flight. */
|
|
2
2
|
import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client';
|
|
3
|
-
export type ActivitySessionSnapshot = Pick<ConversationSnapshot, 'running' | 'partial' | 'runningCalls' | 'turnTimings'
|
|
4
|
-
/**
|
|
3
|
+
export type ActivitySessionSnapshot = Pick<ConversationSnapshot, 'running' | 'partial' | 'runningCalls' | 'turnTimings'> & {
|
|
4
|
+
/** Optional for hosts that predate the assembled Chat snapshot. */
|
|
5
|
+
readonly chat?: ConversationSnapshot['chat'];
|
|
6
|
+
};
|
|
7
|
+
export type CodexModelActivity = {
|
|
8
|
+
activity: 'requesting-model' | 'model-reply';
|
|
9
|
+
startedAt: number;
|
|
10
|
+
};
|
|
11
|
+
/** Infer the request-phase clock from the existing conversation snapshot. */
|
|
5
12
|
export declare function awaitingModelStartedAt(snapshot: ActivitySessionSnapshot): number | undefined;
|
|
13
|
+
/** Infer the first non-empty model token timestamp from the current snapshot. */
|
|
14
|
+
export declare function modelReplyStartedAt(snapshot: ActivitySessionSnapshot): number | undefined;
|
|
15
|
+
/** Infer the two-phase model activity from the current client snapshot. */
|
|
16
|
+
export declare function modelActivity(snapshot: ActivitySessionSnapshot): CodexModelActivity | undefined;
|
|
6
17
|
//# sourceMappingURL=activity.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Client-only inference used while the host activity projection frame is in flight. */
|
|
2
|
-
/** Infer the
|
|
2
|
+
/** Infer the request-phase clock from the existing conversation snapshot. */
|
|
3
3
|
export function awaitingModelStartedAt(snapshot) {
|
|
4
4
|
if (!snapshot.running || snapshot.runningCalls.length > 0)
|
|
5
5
|
return undefined;
|
|
@@ -10,4 +10,48 @@ export function awaitingModelStartedAt(snapshot) {
|
|
|
10
10
|
}
|
|
11
11
|
return latest;
|
|
12
12
|
}
|
|
13
|
+
/** Infer the first non-empty model token timestamp from the current snapshot. */
|
|
14
|
+
export function modelReplyStartedAt(snapshot) {
|
|
15
|
+
if (!snapshot.running || snapshot.runningCalls.length > 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
let openTurn;
|
|
18
|
+
for (const [turn, timing] of snapshot.turnTimings) {
|
|
19
|
+
if (timing.endTime === undefined)
|
|
20
|
+
openTurn = { turn, startTime: timing.startTime };
|
|
21
|
+
}
|
|
22
|
+
if (openTurn === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (snapshot.partial !== null && snapshot.partial.turn === openTurn.turn) {
|
|
25
|
+
const partialReplyStartedAt = snapshot.partial.replyStartedAt;
|
|
26
|
+
if (partialReplyStartedAt !== undefined)
|
|
27
|
+
return partialReplyStartedAt;
|
|
28
|
+
}
|
|
29
|
+
// A non-streaming adapter has no partial row. The assembled Chat view still
|
|
30
|
+
// carries the reply boundary, but only accept it for the currently open step
|
|
31
|
+
// so an earlier tool step in the same turn cannot mask a new request.
|
|
32
|
+
const chat = snapshot.chat;
|
|
33
|
+
const turn = chat?.timeline.turns.get(openTurn.turn);
|
|
34
|
+
const step = turn?.steps.findLast(candidate => candidate.end === undefined);
|
|
35
|
+
if (step === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
for (const node of [...(chat?.nodes.values() ?? [])].reverse()) {
|
|
38
|
+
if (node.kind !== 'assistant-step')
|
|
39
|
+
continue;
|
|
40
|
+
const data = node.data;
|
|
41
|
+
if (data.turn === openTurn.turn && data.step === step.step && typeof data.replyStartedAt === 'number') {
|
|
42
|
+
return data.replyStartedAt;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Infer the two-phase model activity from the current client snapshot. */
|
|
48
|
+
export function modelActivity(snapshot) {
|
|
49
|
+
const startedAt = awaitingModelStartedAt(snapshot);
|
|
50
|
+
if (startedAt === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const replyStartedAt = modelReplyStartedAt(snapshot);
|
|
53
|
+
return replyStartedAt === undefined
|
|
54
|
+
? { activity: 'requesting-model', startedAt }
|
|
55
|
+
: { activity: 'model-reply', startedAt: replyStartedAt };
|
|
56
|
+
}
|
|
13
57
|
//# sourceMappingURL=activity.js.map
|
|
@@ -11,7 +11,8 @@ declare const en: {
|
|
|
11
11
|
readonly contextSizeDescription: "Next request capacity in K tokens. The meter above is current.";
|
|
12
12
|
readonly contextRestore: "Restore model default";
|
|
13
13
|
readonly activityCompacting: "Compacting context...";
|
|
14
|
-
readonly
|
|
14
|
+
readonly activityRequestingModel: "Requesting model...";
|
|
15
|
+
readonly activityModelReply: "Model replying...";
|
|
15
16
|
};
|
|
16
17
|
type CodexKey = keyof typeof en;
|
|
17
18
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/** Browser controls for the Codex request settings mounted by the Host plugin. */
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
|
-
import { CODEX_CONTEXT_MAX, CODEX_CONTEXT_UNIT,
|
|
5
|
-
import {
|
|
4
|
+
import { CODEX_CONTEXT_MAX, CODEX_CONTEXT_UNIT, isCodexPresetId } from "../context.js";
|
|
5
|
+
import { modelActivity } from "./activity.js";
|
|
6
6
|
const NS = 'codex';
|
|
7
7
|
const SETTINGS_NAMESPACE = 'codex';
|
|
8
8
|
const MODEL_SETTINGS_SLOT = 'conversation.input.model.settings';
|
|
@@ -24,7 +24,8 @@ const en = {
|
|
|
24
24
|
contextSizeDescription: 'Next request capacity in K tokens. The meter above is current.',
|
|
25
25
|
contextRestore: 'Restore model default',
|
|
26
26
|
activityCompacting: 'Compacting context...',
|
|
27
|
-
|
|
27
|
+
activityRequestingModel: 'Requesting model...',
|
|
28
|
+
activityModelReply: 'Model replying...',
|
|
28
29
|
};
|
|
29
30
|
const zh = {
|
|
30
31
|
fast: 'Fast',
|
|
@@ -36,13 +37,14 @@ const zh = {
|
|
|
36
37
|
contextSizeDescription: '设置下次请求的上下文容量,单位为 K tokens;上方是当前请求。',
|
|
37
38
|
contextRestore: '恢复模型默认值',
|
|
38
39
|
activityCompacting: '正在压缩上下文...',
|
|
39
|
-
|
|
40
|
+
activityRequestingModel: '请求模型...',
|
|
41
|
+
activityModelReply: '模型回复...',
|
|
40
42
|
};
|
|
41
43
|
function FastModeButton({ sessionId, useSessions, useSettings, setSetting, t }) {
|
|
42
44
|
const agentPreset = useSessions(state => state.byId[sessionId]?.agentPreset);
|
|
43
45
|
const snapshot = useSettings(state => state);
|
|
44
46
|
const fast = snapshot.value?.fast ?? false;
|
|
45
|
-
if (agentPreset
|
|
47
|
+
if (!isCodexPresetId(agentPreset))
|
|
46
48
|
return null;
|
|
47
49
|
return (_jsxs("button", { type: "button", role: "menuitemcheckbox", disabled: snapshot.writable === false, "aria-pressed": fast, "aria-label": fast ? t('fastOn') : t('fastOff'), title: fast ? t('fastOn') : t('fastOff'), onClick: () => { void setSetting('fast', !fast); }, style: {
|
|
48
50
|
boxSizing: 'border-box',
|
|
@@ -70,7 +72,7 @@ function FastModeFallback(props) {
|
|
|
70
72
|
const agentPreset = useSessions(state => state.byId[sessionId]?.agentPreset);
|
|
71
73
|
const snapshot = useSettings(state => state);
|
|
72
74
|
const fast = snapshot.value?.fast ?? false;
|
|
73
|
-
if (agentPreset
|
|
75
|
+
if (!isCodexPresetId(agentPreset))
|
|
74
76
|
return null;
|
|
75
77
|
return (_jsxs("button", { type: "button", role: "switch", disabled: snapshot.writable === false, "aria-checked": fast, "aria-label": fast ? t('fastOn') : t('fastOff'), title: fast ? t('fastOn') : t('fastOff'), onClick: () => { void setSetting('fast', !fast); }, style: {
|
|
76
78
|
boxSizing: 'border-box',
|
|
@@ -143,28 +145,36 @@ export function ActivityLine({ sessionId, useSession, useSessions, useProjection
|
|
|
143
145
|
// Keep the projection seam optional: this package must still load when the
|
|
144
146
|
// host has no session-projection registry or has not carried this key.
|
|
145
147
|
const projectedActivity = useProjection('codexActivity');
|
|
146
|
-
// The projection is the authoritative clock. The session snapshot
|
|
147
|
-
// only covers the short wire gap before the next projection frame
|
|
148
|
-
//
|
|
149
|
-
const
|
|
148
|
+
// The projection is the authoritative phase and clock. The session snapshot
|
|
149
|
+
// fallback only covers the short wire gap before the next projection frame;
|
|
150
|
+
// tool calls intentionally remain silent because their cards own the status.
|
|
151
|
+
const fallbackActivity = useSession((snapshot) => modelActivity(snapshot));
|
|
150
152
|
// `undefined` means the key has not arrived yet; `null` is an authoritative
|
|
151
153
|
// clear and must not be resurrected by the local snapshot fallback.
|
|
152
|
-
const
|
|
153
|
-
? {
|
|
154
|
+
const normalizedProjectedActivity = projectedActivity?.activity === 'awaiting-model'
|
|
155
|
+
? {
|
|
156
|
+
activity: fallbackActivity?.activity ?? 'requesting-model',
|
|
157
|
+
startedAt: fallbackActivity?.startedAt ?? projectedActivity.startedAt,
|
|
158
|
+
}
|
|
159
|
+
: projectedActivity;
|
|
160
|
+
const activity = projectedActivity !== undefined ? normalizedProjectedActivity : (isCodexPresetId(agentPreset) && fallbackActivity !== undefined
|
|
161
|
+
? fallbackActivity
|
|
154
162
|
: null);
|
|
155
163
|
const [now, setNow] = useState(() => Date.now());
|
|
156
164
|
useEffect(() => {
|
|
157
|
-
if (agentPreset
|
|
165
|
+
if (!isCodexPresetId(agentPreset) || activity === undefined || activity === null)
|
|
158
166
|
return undefined;
|
|
159
167
|
setNow(Date.now());
|
|
160
168
|
const timer = setInterval(() => { setNow(Date.now()); }, 1_000);
|
|
161
169
|
return () => clearInterval(timer);
|
|
162
|
-
}, [agentPreset, activity?.startedAt]);
|
|
163
|
-
if (agentPreset
|
|
170
|
+
}, [agentPreset, activity?.activity, activity?.startedAt]);
|
|
171
|
+
if (!isCodexPresetId(agentPreset) || activity === undefined || activity === null)
|
|
164
172
|
return null;
|
|
165
|
-
const label = activity.activity === '
|
|
166
|
-
? t('
|
|
167
|
-
:
|
|
173
|
+
const label = activity.activity === 'compaction'
|
|
174
|
+
? t('activityCompacting')
|
|
175
|
+
: activity.activity === 'model-reply'
|
|
176
|
+
? t('activityModelReply')
|
|
177
|
+
: t('activityRequestingModel');
|
|
168
178
|
return (_jsxs("span", { id: "codex-activity", role: "status", "aria-live": "polite", style: {
|
|
169
179
|
display: 'inline-flex',
|
|
170
180
|
alignItems: 'center',
|
package/lib/types/context.d.ts
CHANGED
|
@@ -2,4 +2,7 @@
|
|
|
2
2
|
export declare const CODEX_CONTEXT_UNIT = 1000;
|
|
3
3
|
export declare const CODEX_CONTEXT_MAX = 1000000;
|
|
4
4
|
export declare const CODEX_PRESET_ID = "codex";
|
|
5
|
+
export declare const CODEX_COLLABORATION_PRESET_ID = "codex-collaboration";
|
|
6
|
+
/** Both shipped presets use the same Codex request controls in Web. */
|
|
7
|
+
export declare function isCodexPresetId(value: string | undefined): boolean;
|
|
5
8
|
//# sourceMappingURL=context.d.ts.map
|
package/lib/types/context.js
CHANGED
|
@@ -2,4 +2,13 @@
|
|
|
2
2
|
export const CODEX_CONTEXT_UNIT = 1_000;
|
|
3
3
|
export const CODEX_CONTEXT_MAX = 1_000_000;
|
|
4
4
|
export const CODEX_PRESET_ID = 'codex';
|
|
5
|
+
export const CODEX_COLLABORATION_PRESET_ID = 'codex-collaboration';
|
|
6
|
+
const CODEX_PRESET_IDS = new Set([
|
|
7
|
+
CODEX_PRESET_ID,
|
|
8
|
+
CODEX_COLLABORATION_PRESET_ID,
|
|
9
|
+
]);
|
|
10
|
+
/** Both shipped presets use the same Codex request controls in Web. */
|
|
11
|
+
export function isCodexPresetId(value) {
|
|
12
|
+
return value !== undefined && CODEX_PRESET_IDS.has(value);
|
|
13
|
+
}
|
|
5
14
|
//# sourceMappingURL=context.js.map
|
package/lib/types/exec.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface ExecResult {
|
|
|
29
29
|
exit_code?: number;
|
|
30
30
|
original_token_count?: number;
|
|
31
31
|
}
|
|
32
|
-
export declare function normalizeWaitMs(value: number | undefined, fallback: number, empty?: boolean): number;
|
|
32
|
+
export declare function normalizeWaitMs(value: number | undefined, fallback: number, empty?: boolean, initialExec?: boolean): number;
|
|
33
33
|
/** Render the ordinary Responses tool result text used by Codex's unified exec tools. */
|
|
34
34
|
export declare function renderExecResult(result: ExecResult): string;
|
|
35
35
|
/** Execute one Codex command through the configured pipe or PTY capability. */
|
package/lib/types/exec.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
3
3
|
import { resolve as resolvePath } from 'node:path';
|
|
4
4
|
const STATES = new WeakMap();
|
|
5
5
|
const MIN_YIELD_TIME_MS = 250;
|
|
6
|
+
const WINDOWS_INITIAL_EXEC_YIELD_TIME_MS = 10_000;
|
|
6
7
|
const MIN_EMPTY_YIELD_TIME_MS = 5_000;
|
|
7
8
|
const MAX_YIELD_TIME_MS = 30_000;
|
|
8
9
|
const MAX_EMPTY_YIELD_TIME_MS = 300_000;
|
|
@@ -21,8 +22,12 @@ function positiveFinite(name, value) {
|
|
|
21
22
|
throw new Error(`${name} must be a non-negative finite number`);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
|
-
export function normalizeWaitMs(value, fallback, empty = false) {
|
|
25
|
-
const lower = empty
|
|
25
|
+
export function normalizeWaitMs(value, fallback, empty = false, initialExec = false) {
|
|
26
|
+
const lower = empty
|
|
27
|
+
? MIN_EMPTY_YIELD_TIME_MS
|
|
28
|
+
: initialExec && process.platform === 'win32'
|
|
29
|
+
? WINDOWS_INITIAL_EXEC_YIELD_TIME_MS
|
|
30
|
+
: MIN_YIELD_TIME_MS;
|
|
26
31
|
const upper = empty ? MAX_EMPTY_YIELD_TIME_MS : MAX_YIELD_TIME_MS;
|
|
27
32
|
return Math.max(lower, Math.min(upper, Math.trunc(value ?? fallback)));
|
|
28
33
|
}
|
|
@@ -178,7 +183,7 @@ export async function runExecCommand(ctx, args, exec, config) {
|
|
|
178
183
|
const sendRequest = {
|
|
179
184
|
text: commandFor(args),
|
|
180
185
|
submit: true,
|
|
181
|
-
waitMs: normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs),
|
|
186
|
+
waitMs: normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs, false, true),
|
|
182
187
|
signal: exec.signal,
|
|
183
188
|
};
|
|
184
189
|
const operation = terminals.startSend(agent, spawned.sessionId, sendRequest);
|
|
@@ -250,7 +255,7 @@ export async function runExecCommand(ctx, args, exec, config) {
|
|
|
250
255
|
signal: exec.signal,
|
|
251
256
|
}));
|
|
252
257
|
try {
|
|
253
|
-
await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs), exec.signal);
|
|
258
|
+
await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs, false, true), exec.signal);
|
|
254
259
|
const output = readShellOutput(process.readOutput(), maxBytes);
|
|
255
260
|
if (process.status === 'running') {
|
|
256
261
|
if (exec.agent === undefined) {
|
package/lib/types/index.js
CHANGED
|
@@ -9,12 +9,13 @@ import { hostedWebSearchStream, installHostedWebSearch, remoteCompactStream } fr
|
|
|
9
9
|
import { registerCodexActivityProjection } from "./activity.js";
|
|
10
10
|
export const name = 'codex';
|
|
11
11
|
export const inject = ['tools', 'systemPrompt', 'shell', 'fs', 'llm', 'credentials', 'settings'];
|
|
12
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
12
13
|
/** Runtime configuration schema for the Codex tool bridge. */
|
|
13
14
|
export const Config = z.object({
|
|
14
15
|
defaultYieldTimeMs: z.number().step(1).min(0).default(10_000),
|
|
15
16
|
pollYieldTimeMs: z.number().step(1).min(0).default(5_000),
|
|
16
17
|
writeYieldTimeMs: z.number().step(1).min(0).default(250),
|
|
17
|
-
maxOutputBytes: z.number().step(1).min(1).default(
|
|
18
|
+
maxOutputBytes: z.number().step(1).min(1).default(DEFAULT_MAX_OUTPUT_BYTES),
|
|
18
19
|
hostedWebSearch: z.boolean().default(true),
|
|
19
20
|
remoteCompact: z.boolean().default(true),
|
|
20
21
|
collaborationPrompt: z.boolean().default(false),
|
|
@@ -451,7 +452,7 @@ export function apply(ctx, config = {}) {
|
|
|
451
452
|
defaultYieldTimeMs: config.defaultYieldTimeMs ?? 10_000,
|
|
452
453
|
pollYieldTimeMs: config.pollYieldTimeMs ?? 5_000,
|
|
453
454
|
writeYieldTimeMs: config.writeYieldTimeMs ?? 250,
|
|
454
|
-
maxOutputBytes: config.maxOutputBytes ??
|
|
455
|
+
maxOutputBytes: config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
|
|
455
456
|
hostedWebSearch: config.hostedWebSearch ?? true,
|
|
456
457
|
remoteCompact: config.remoteCompact ?? true,
|
|
457
458
|
collaborationPrompt: config.collaborationPrompt ?? false,
|
package/lib/types/installer.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare const name = "codex-preset-installer";
|
|
|
13
13
|
* @param sourceDir - directory containing the packaged preset files.
|
|
14
14
|
*/
|
|
15
15
|
export declare function installCodexPreset(targetDir?: string, sourceDir?: string): void;
|
|
16
|
+
/** Install the shipped Codex preset with the optional collaboration guidance enabled. */
|
|
17
|
+
export declare function installCodexCollaborationPreset(targetDir?: string, sourceDir?: string): void;
|
|
16
18
|
/** Install the preset during profile boot without changing the host tool catalog. */
|
|
17
19
|
export declare function apply(ctx: Context): void;
|
|
18
20
|
declare const _default: {
|
package/lib/types/installer.js
CHANGED
|
@@ -3,9 +3,9 @@ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync }
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
const PRESET_ID = 'codex';
|
|
7
6
|
const PRESET_FILES = ['agent.cordis.yml', 'preset.yml'];
|
|
8
7
|
const SOURCE_PRESET_DIR = fileURLToPath(new URL('../presets/codex/', import.meta.url));
|
|
8
|
+
const SOURCE_COLLABORATION_PRESET_DIR = fileURLToPath(new URL('../presets/codex-collaboration/', import.meta.url));
|
|
9
9
|
function dshHomePath(...segments) {
|
|
10
10
|
const configured = process.env.DSH_HOME?.trim();
|
|
11
11
|
const expanded = configured === undefined || configured.length === 0
|
|
@@ -19,22 +19,12 @@ function dshHomePath(...segments) {
|
|
|
19
19
|
}
|
|
20
20
|
/** Bundle plugin name for the preset installer. */
|
|
21
21
|
export const name = 'codex-preset-installer';
|
|
22
|
-
|
|
23
|
-
* Install the shipped Codex preset only when the user has not authored one.
|
|
24
|
-
*
|
|
25
|
-
* The directory is committed with a staging rename so a failed copy cannot
|
|
26
|
-
* leave a half-written preset that hides the mode from the roster. Existing
|
|
27
|
-
* directories are intentionally preserved, including user customizations.
|
|
28
|
-
*
|
|
29
|
-
* @param targetDir - destination preset directory.
|
|
30
|
-
* @param sourceDir - directory containing the packaged preset files.
|
|
31
|
-
*/
|
|
32
|
-
export function installCodexPreset(targetDir = dshHomePath('.agent-presets', PRESET_ID), sourceDir = SOURCE_PRESET_DIR) {
|
|
22
|
+
function installPreset(targetDir, sourceDir, presetId) {
|
|
33
23
|
if (existsSync(targetDir))
|
|
34
24
|
return;
|
|
35
25
|
const parentDir = dirname(targetDir);
|
|
36
26
|
mkdirSync(parentDir, { recursive: true });
|
|
37
|
-
const stagingDir = mkdtempSync(join(parentDir, `.${
|
|
27
|
+
const stagingDir = mkdtempSync(join(parentDir, `.${presetId}-`));
|
|
38
28
|
try {
|
|
39
29
|
for (const file of PRESET_FILES)
|
|
40
30
|
copyFileSync(join(sourceDir, file), join(stagingDir, file));
|
|
@@ -51,13 +41,31 @@ export function installCodexPreset(targetDir = dshHomePath('.agent-presets', PRE
|
|
|
51
41
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
52
42
|
}
|
|
53
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Install the shipped Codex preset only when the user has not authored one.
|
|
46
|
+
*
|
|
47
|
+
* The directory is committed with a staging rename so a failed copy cannot
|
|
48
|
+
* leave a half-written preset that hides the mode from the roster. Existing
|
|
49
|
+
* directories are intentionally preserved, including user customizations.
|
|
50
|
+
*
|
|
51
|
+
* @param targetDir - destination preset directory.
|
|
52
|
+
* @param sourceDir - directory containing the packaged preset files.
|
|
53
|
+
*/
|
|
54
|
+
export function installCodexPreset(targetDir = dshHomePath('.agent-presets', 'codex'), sourceDir = SOURCE_PRESET_DIR) {
|
|
55
|
+
installPreset(targetDir, sourceDir, 'codex');
|
|
56
|
+
}
|
|
57
|
+
/** Install the shipped Codex preset with the optional collaboration guidance enabled. */
|
|
58
|
+
export function installCodexCollaborationPreset(targetDir = dshHomePath('.agent-presets', 'codex-collaboration'), sourceDir = SOURCE_COLLABORATION_PRESET_DIR) {
|
|
59
|
+
installPreset(targetDir, sourceDir, 'codex-collaboration');
|
|
60
|
+
}
|
|
54
61
|
/** Install the preset during profile boot without changing the host tool catalog. */
|
|
55
62
|
export function apply(ctx) {
|
|
56
63
|
try {
|
|
57
64
|
installCodexPreset();
|
|
65
|
+
installCodexCollaborationPreset();
|
|
58
66
|
}
|
|
59
67
|
catch (error) {
|
|
60
|
-
ctx.logger.warn(`dsh-codex: could not install the Codex
|
|
68
|
+
ctx.logger.warn(`dsh-codex: could not install the Codex presets: ${String(error)}`);
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
export default { name, apply };
|
package/lib/types/patch.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/** Parser and line-oriented applicator for Codex's `apply_patch` language. */
|
|
2
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.';
|
|
3
3
|
/** The grammar sent to providers that support OpenAI custom grammar tools. */
|
|
4
|
-
export const APPLY_PATCH_GRAMMAR = String.raw `start: begin_patch
|
|
4
|
+
export const APPLY_PATCH_GRAMMAR = String.raw `start: begin_patch hunk+ end_patch
|
|
5
5
|
begin_patch: "*** Begin Patch" LF
|
|
6
|
-
environment_id: "*** Environment ID: " filename LF
|
|
7
6
|
end_patch: "*** End Patch" LF?
|
|
8
7
|
hunk: add_hunk | delete_hunk | update_hunk
|
|
9
8
|
add_hunk: "*** Add File: " filename LF add_line+
|
|
@@ -27,7 +26,6 @@ function isFileHeader(line) {
|
|
|
27
26
|
|| trimmed.startsWith('*** Delete File: ')
|
|
28
27
|
|| trimmed.startsWith('*** Update File: ');
|
|
29
28
|
}
|
|
30
|
-
const ENVIRONMENT_ID_MARKER = '*** Environment ID:';
|
|
31
29
|
function unwrapHeredoc(input) {
|
|
32
30
|
const normalized = input.replaceAll('\r\n', '\n').trim();
|
|
33
31
|
const lines = normalized.split('\n');
|
|
@@ -63,14 +61,8 @@ export function parsePatch(input) {
|
|
|
63
61
|
const files = [];
|
|
64
62
|
let index = 1;
|
|
65
63
|
const end = lines.length - 1;
|
|
66
|
-
if (lines[index]?.trim().startsWith(
|
|
67
|
-
|
|
68
|
-
if (environmentId.length === 0)
|
|
69
|
-
invalid('environment id cannot be empty');
|
|
70
|
-
index++;
|
|
71
|
-
if (lines[index]?.trim().startsWith(ENVIRONMENT_ID_MARKER) === true) {
|
|
72
|
-
invalid('environment id cannot be specified more than once');
|
|
73
|
-
}
|
|
64
|
+
if (lines[index]?.trim().startsWith('*** Environment ID:') === true) {
|
|
65
|
+
invalid('environment selection is unavailable in this single-environment DSH plugin');
|
|
74
66
|
}
|
|
75
67
|
while (index < end) {
|
|
76
68
|
const header = lines[index++]?.trim();
|
package/lib/types/remote.js
CHANGED
|
@@ -437,6 +437,36 @@ function compactText(body) {
|
|
|
437
437
|
const encrypted = text.join('\n\n').trim();
|
|
438
438
|
return encrypted.length === 0 ? '' : `${REMOTE_COMPACTION_OPEN}${encrypted}${REMOTE_COMPACTION_CLOSE}`;
|
|
439
439
|
}
|
|
440
|
+
function nonNegativeInteger(value) {
|
|
441
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
442
|
+
}
|
|
443
|
+
/** Map the Responses compact usage object without inventing missing counts. */
|
|
444
|
+
function compactUsage(body) {
|
|
445
|
+
if (typeof body.usage !== 'object' || body.usage === null)
|
|
446
|
+
return undefined;
|
|
447
|
+
const raw = body.usage;
|
|
448
|
+
const inputTokens = nonNegativeInteger(raw.input_tokens);
|
|
449
|
+
const outputTokens = nonNegativeInteger(raw.output_tokens);
|
|
450
|
+
if (inputTokens === undefined || outputTokens === undefined)
|
|
451
|
+
return undefined;
|
|
452
|
+
const details = typeof raw.input_tokens_details === 'object' && raw.input_tokens_details !== null
|
|
453
|
+
? raw.input_tokens_details
|
|
454
|
+
: undefined;
|
|
455
|
+
const cacheReadTokens = nonNegativeInteger(details?.cached_tokens);
|
|
456
|
+
const cacheWriteTokens = nonNegativeInteger(details?.cache_write_tokens);
|
|
457
|
+
const cached = cacheReadTokens ?? 0;
|
|
458
|
+
const writes = cacheWriteTokens ?? 0;
|
|
459
|
+
// Responses input_tokens includes the cache buckets. Reject an inconsistent
|
|
460
|
+
// response instead of clamping it and reporting numbers that no longer sum.
|
|
461
|
+
if (cached + writes > inputTokens)
|
|
462
|
+
return undefined;
|
|
463
|
+
return {
|
|
464
|
+
inputTokens: inputTokens - cached - writes,
|
|
465
|
+
outputTokens,
|
|
466
|
+
...cacheReadTokens !== undefined && cacheReadTokens > 0 ? { cacheReadTokens } : {},
|
|
467
|
+
...cacheWriteTokens !== undefined && cacheWriteTokens > 0 ? { cacheWriteTokens } : {},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
440
470
|
async function remoteCompact(ctx, options) {
|
|
441
471
|
const profile = profileOf(ctx, options.provider);
|
|
442
472
|
if (!supportsResponses(profile, options.provider) || profile?.baseURL === undefined) {
|
|
@@ -457,17 +487,19 @@ async function remoteCompact(ctx, options) {
|
|
|
457
487
|
const text = compactText(body);
|
|
458
488
|
if (text.length === 0)
|
|
459
489
|
throw new Error('remote compaction returned no compaction text');
|
|
460
|
-
|
|
490
|
+
const usage = compactUsage(body);
|
|
491
|
+
return { text, ...usage === undefined ? {} : { usage } };
|
|
461
492
|
}
|
|
462
493
|
/** Remote-first compaction waterfall with the existing DSH path as fallback. */
|
|
463
494
|
export function remoteCompactStream(ctx, options, next) {
|
|
464
495
|
return (async function* () {
|
|
465
496
|
try {
|
|
466
|
-
const
|
|
497
|
+
const result = await remoteCompact(ctx, options);
|
|
467
498
|
yield { type: 'block-start', index: 0, blockType: 'text' };
|
|
468
|
-
yield { type: 'text-delta', index: 0, text };
|
|
469
|
-
yield { type: 'block-end', index: 0, block: { type: 'text', text } };
|
|
470
|
-
|
|
499
|
+
yield { type: 'text-delta', index: 0, text: result.text };
|
|
500
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text: result.text } };
|
|
501
|
+
if (result.usage !== undefined)
|
|
502
|
+
yield { type: 'usage', usage: result.usage };
|
|
471
503
|
yield { type: 'finish', reason: { kind: 'stop' } };
|
|
472
504
|
}
|
|
473
505
|
catch (error) {
|
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.25",
|
|
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",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"lib/client.js",
|
|
36
36
|
"cordis.patch.yml",
|
|
37
37
|
"presets/codex",
|
|
38
|
+
"presets/codex-collaboration",
|
|
38
39
|
"README.md",
|
|
39
40
|
"README.zh.md",
|
|
40
41
|
"LICENSE"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# The Codex collaboration preset. It shares the standard Codex composition
|
|
2
|
+
# while enabling the optional collaboration guidance in the prompt layer.
|
|
3
|
+
|
|
4
|
+
- id: persona
|
|
5
|
+
name: '@deepseek-ai/dsh-persona'
|
|
6
|
+
config:
|
|
7
|
+
# Keep identity, model, cwd, and runtime context in one persona section.
|
|
8
|
+
text: >-
|
|
9
|
+
You are Codex, a coding agent based on the {{model}} model. Your working directory is {{cwd}}.
|
|
10
|
+
includeRuntimeContext: true
|
|
11
|
+
|
|
12
|
+
- id: agent-instructions
|
|
13
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
14
|
+
config:
|
|
15
|
+
maxBytes: 65536
|
|
16
|
+
|
|
17
|
+
# The collaboration variant only changes this flag; execution remains
|
|
18
|
+
# delegated to the same dsh Codex tool layer.
|
|
19
|
+
- id: codex-tools
|
|
20
|
+
name: '@shuind/dsh-codex-harness'
|
|
21
|
+
config:
|
|
22
|
+
collaborationPrompt: true
|
|
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
|
+
|
|
32
|
+
# The generic DSH pi-ai adapter still owns the user's configured provider,
|
|
33
|
+
# endpoint, API key, and model. Codex's remote-first transport wrapper is
|
|
34
|
+
# installed only in this Codex scope and falls back to the generic DSH path.
|
|
35
|
+
|
|
36
|
+
# Keep the local tool mounted as the fallback. Codex rewrites the final GPT
|
|
37
|
+
# Responses request to a hosted web_search tool first; a failed remote request
|
|
38
|
+
# is retried through this unchanged local function-tool path.
|
|
39
|
+
- id: codex-web-search
|
|
40
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
41
|
+
config:
|
|
42
|
+
search: true
|
|
43
|
+
fetch: false
|
|
44
|
+
|
|
45
|
+
# PTY execution is optional. Pipe-backed exec_command remains available on all
|
|
46
|
+
# platforms through the shell capability consumed by the Codex tool layer.
|
|
47
|
+
- id: codex-terminal
|
|
48
|
+
name: cordis:group
|
|
49
|
+
group: true
|
|
50
|
+
isolate:
|
|
51
|
+
terminals: true
|
|
52
|
+
config:
|
|
53
|
+
- id: terminals
|
|
54
|
+
name: '@deepseek-ai/dsh-terminal'
|
|
55
|
+
|
|
56
|
+
- id: terminal-bash
|
|
57
|
+
name: '@deepseek-ai/dsh-terminal-bash'
|
|
58
|
+
disabled: !!js process.platform === 'win32'
|
|
59
|
+
config:
|
|
60
|
+
timeoutMs: 300000
|
|
61
|
+
|
|
62
|
+
# Context compaction is agent-scoped in Web. The token meter stays on the host
|
|
63
|
+
# plane, while this group owns the backend, `/compact`, and optional tool-result
|
|
64
|
+
# pruning for Codex sessions.
|
|
65
|
+
- id: compaction
|
|
66
|
+
name: cordis:group
|
|
67
|
+
group: true
|
|
68
|
+
isolate:
|
|
69
|
+
compaction: true
|
|
70
|
+
toolResultPruner: true
|
|
71
|
+
config:
|
|
72
|
+
- id: compaction-basic
|
|
73
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
74
|
+
|
|
75
|
+
- id: command-compact
|
|
76
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
77
|
+
|
|
78
|
+
- id: tool-result-pruner
|
|
79
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
80
|
+
config:
|
|
81
|
+
thresholdChars: 8192
|
|
82
|
+
headChars: 4096
|
|
83
|
+
tailChars: 1024
|
|
84
|
+
|
|
85
|
+
# Skills are the dsh extensibility seam and do not change the four Codex core
|
|
86
|
+
# tool names.
|
|
87
|
+
- id: skill-filesystem
|
|
88
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
89
|
+
|
|
90
|
+
- id: tool-skill
|
|
91
|
+
name: '@deepseek-ai/dsh-tool-skill'
|