@carljia/omd-dsh 0.1.2 → 0.1.4
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 +16 -6
- package/lib/cli.js +132 -24
- package/lib/index.d.ts +22 -3
- package/lib/index.js +129 -28
- package/lib/mode.d.ts +33 -0
- package/lib/mode.js +145 -0
- package/lib/plan.d.ts +22 -0
- package/lib/plan.js +146 -0
- package/lib/startwork.d.ts +17 -0
- package/lib/startwork.js +147 -0
- package/lib/task.js +8 -1
- package/lib/vendor/omd-mode-switch.mjs +145 -0
- package/lib/vendor/omd-mode.mjs +129 -28
- package/lib/vendor/omd-plan.mjs +146 -0
- package/lib/vendor/omd-start-work.mjs +147 -0
- package/lib/vendor/omd-task.mjs +8 -1
- package/{omd-matrix.json → omd-matrix.default.json} +3 -9
- package/package.json +2 -2
- package/presets/omd-chat/agent.cordis.yml +2 -0
- package/presets/omd-executor/agent.cordis.yml +28 -1
- package/presets/omd-explorer/agent.cordis.yml +2 -0
- package/presets/omd-librarian/agent.cordis.yml +2 -0
- package/presets/omd-planner/agent.cordis.yml +8 -1
- package/presets/omd-reviewer/agent.cordis.yml +2 -0
- package/presets/{omd-architect → omd-ultraworker}/agent.cordis.yml +16 -2
- package/presets/omd-ultraworker/preset.yml +3 -0
- package/presets/omd-architect/preset.yml +0 -3
package/README.md
CHANGED
|
@@ -15,7 +15,14 @@ OMD 理念的 DSH 插件:两个 cordis 行 + 7 个模式 preset + 同步 CLI
|
|
|
15
15
|
|
|
16
16
|
机制:监听 system-prompt/assemble(注入 {{provider}}/{{model}} 提示词变量)与 agent/request(next() 后覆盖路由),两处均 prepend,保证覆盖入口的会话级选择。
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
**与 UI 模型切换的优先级**:矩阵模型是模式的默认路由,但用户在 UI 显式切换模型后,本次任务的顶层路由(与 persona 展示)跟随用户选择。判定规则(依据请求瀑布流解析出的入口选择):
|
|
19
|
+
|
|
20
|
+
1. 入口选择 == 矩阵模型 → 钉住矩阵模型(无操作);
|
|
21
|
+
2. 会话尚无任何请求(blank)且入口选择 == 挂载时的部署默认模型 → 视为未选择,钉住矩阵模型;
|
|
22
|
+
3. 会话已跑过请求且刚发生 preset 切换(/mode 或 UI 选择,日志中 agent-preset/selected 在最后一次 request/header 之后)且入口选择 == 切换前的路由 → 新模式认领矩阵模型;
|
|
23
|
+
4. 其余情况 → 入口选择与矩阵模型不同 = 用户显式切换:请求与 persona 变量均保持用户选择,本行在作用域 ctx 上记录 `omdModeOverride`(provider/model),omd-task 行据此把 deep tier 路由到用户选择的模型。
|
|
24
|
+
|
|
25
|
+
模型体验:persona 文本每次请求固定携带实际路由模型的声明;模式模型路由在 agent 生命周期内稳定(KV 前缀稳定)。
|
|
19
26
|
|
|
20
27
|
## 行:omd-task
|
|
21
28
|
|
|
@@ -32,21 +39,23 @@ tier 差异化子代理委派(等价 OMD 的 task(category=…))。仅限 pr
|
|
|
32
39
|
|
|
33
40
|
模型可见参数:description(展示名)、prompt、tier(枚举见工具描述)、run_in_background(仅 continuable 模式)。工具描述枚举各 tier 的 hint 与模型,模型据此选型。
|
|
34
41
|
|
|
42
|
+
**deep tier 与用户模型覆盖**:用户在 UI 显式切换模型后(omd-mode 让路并记录 `ctx.omdModeOverride`),名为 `deep` 的 tier 改用用户选择的模型(provider/model 整体替换),其余 tier 保持矩阵配置。
|
|
43
|
+
|
|
35
44
|
## CLI:omd-dsh
|
|
36
45
|
|
|
37
46
|
```
|
|
38
47
|
omd-dsh <command> [--harness <路径>] [--dry-run]
|
|
39
48
|
```
|
|
40
49
|
|
|
41
|
-
- `omd-dsh sync` — 把 presets/ 与 vendored 行模块同步到 `<DSH_HOME>/.agent-presets/`(hash 保护、orphan 报告、非管理目录零操作)。每个 preset 的 omd-mode / omd-task
|
|
42
|
-
- `omd-dsh setup` — 交互式向导:先读取 DSH 已有模型,再引导逐模式/逐 tier 选择模型,写回
|
|
50
|
+
- `omd-dsh sync` — 把 presets/ 与 vendored 行模块同步到 `<DSH_HOME>/.agent-presets/`(hash 保护、orphan 报告、非管理目录零操作)。每个 preset 的 omd-mode / omd-task 行由用户矩阵 `<DSH_HOME>/omd-matrix.json` 渲染。
|
|
51
|
+
- `omd-dsh setup` — 交互式向导:先读取 DSH 已有模型,再引导逐模式/逐 tier 选择模型,写回 `<DSH_HOME>/omd-matrix.json` 并可选立即同步。
|
|
43
52
|
- `omd-dsh models` — 打印发现的 DSH 模型目录(非交互)。
|
|
44
53
|
|
|
45
54
|
细节见 docs/ARCHITECTURE.md 的「vendored 分发与跨树符号风险」。
|
|
46
55
|
|
|
47
|
-
##
|
|
56
|
+
## 集中配置:<DSH_HOME>/omd-matrix.json
|
|
48
57
|
|
|
49
|
-
所有模式的模型路由(provider/model/reasoningEffort)与 omd_task 各 tier
|
|
58
|
+
所有模式的模型路由(provider/model/reasoningEffort)与 omd_task 各 tier 的模型集中在用户矩阵 `<DSH_HOME>/omd-matrix.json`(默认 `~/.dsh/omd-matrix.json`):`omd-dsh sync` 首次运行把随包的 deepseek 默认矩阵 `omd-matrix.default.json` 复制为默认配置(或从旧版本包内位置迁移一次),`omd-dsh setup` 更新它,`omd-dsh sync` 读取它并渲染进各 preset。仓库与 npm 包只携带默认矩阵文件,**不含任何个人模型配置**——个人模型配置只留在本机。preset 里 `# [omd-dsh:mode:start] / [omd-dsh:mode:end]` 与 `# [omd-dsh:task:start] / [omd-dsh:task:end]` 之间的区域是自动生成的——改模型请改矩阵后跑 sync,不要手改 fence 之间的内容。
|
|
50
59
|
|
|
51
60
|
## 开发
|
|
52
61
|
|
|
@@ -62,7 +71,8 @@ npm pack # prepack 自动重建,产出 tgz
|
|
|
62
71
|
### 子代理透传语义
|
|
63
72
|
|
|
64
73
|
omd-mode 对 subagentDepth > 0 的子代理一律透传(不覆盖 provider/model/变量):
|
|
65
|
-
omd-task 的 tier 模型通过显式 agentOptions 生效;顶层 agent
|
|
74
|
+
omd-task 的 tier 模型通过显式 agentOptions 生效;顶层 agent 才被钉到模式模型
|
|
75
|
+
(除非用户显式切换模型——此时顶层跟随用户选择,deep tier 通过 `ctx.omdModeOverride` 同步)。
|
|
66
76
|
这保证「强模型顶层 + tier 差异化子代理」的优先级正确。
|
|
67
77
|
|
|
68
78
|
### toolFilter 注意
|
package/lib/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { promises as fs, existsSync, realpathSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { promises as fs, existsSync, mkdirSync, realpathSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { dirname, join, relative, resolve, basename } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
@@ -10,25 +10,34 @@ import { createInterface } from "node:readline/promises";
|
|
|
10
10
|
* omd-dsh CLI
|
|
11
11
|
*
|
|
12
12
|
* omd-dsh sync materialize presets into <DSH_HOME>/.agent-presets,
|
|
13
|
-
* rendering each preset's omd-mode / omd-task rows from
|
|
14
|
-
* omd-matrix.json
|
|
15
|
-
*
|
|
13
|
+
* rendering each preset's omd-mode / omd-task rows from the
|
|
14
|
+
* user's model matrix at <DSH_HOME>/omd-matrix.json. On
|
|
15
|
+
* first run the shipped deepseek default matrix
|
|
16
|
+
* (omd-matrix.default.json) is copied there; personal
|
|
17
|
+
* model settings stay on the user's machine and are never
|
|
18
|
+
* shipped or uploaded.
|
|
16
19
|
* omd-dsh setup interactive wizard: discover the models DSH already has,
|
|
17
20
|
* then guide per-mode and per-tier model selection.
|
|
18
21
|
* omd-dsh models print the discovered model catalog (non-interactive).
|
|
19
22
|
*
|
|
20
23
|
* Distribution model (vendored + harness-anchored imports) is unchanged from
|
|
21
24
|
* the original omd-dsh sync: presets/omd-* are copied into .agent-presets/;
|
|
22
|
-
* their omd-mode/omd-task
|
|
23
|
-
* omd-
|
|
24
|
-
* .agent-presets/.omd-vendor/ with bare @deepseek-ai/*
|
|
25
|
-
* absolute file:// URLs into the harness node_modules
|
|
25
|
+
* their omd-mode / omd-task / omd-plan / omd-start-work / omd-mode-switch
|
|
26
|
+
* rows reference ../.omd-vendor/*.mjs by relative path; the vendored modules
|
|
27
|
+
* are copied into .agent-presets/.omd-vendor/ with bare @deepseek-ai/*
|
|
28
|
+
* imports rewritten to absolute file:// URLs into the harness node_modules
|
|
29
|
+
* tree.
|
|
26
30
|
*/
|
|
27
31
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
|
-
const VENDOR_SOURCES = ["omd-mode.mjs", "omd-task.mjs", "omd-ulw.mjs"];
|
|
29
|
-
|
|
32
|
+
const VENDOR_SOURCES = ["omd-mode.mjs", "omd-task.mjs", "omd-ulw.mjs", "omd-plan.mjs", "omd-start-work.mjs", "omd-mode-switch.mjs"];
|
|
33
|
+
/** User-owned model matrix: lives under DSH_HOME, never inside the package or the repo. */
|
|
34
|
+
const MATRIX_PATH = join(dshHome(), "omd-matrix.json");
|
|
35
|
+
/** Pre-migration location (package root) — migrated to MATRIX_PATH once when present. */
|
|
36
|
+
const LEGACY_MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.json");
|
|
30
37
|
const MODE_FENCE = { start: "# [omd-dsh:mode:start]", end: "# [omd-dsh:mode:end]" };
|
|
31
38
|
const TASK_FENCE = { start: "# [omd-dsh:task:start]", end: "# [omd-dsh:task:end]" };
|
|
39
|
+
/** Presets that were renamed: old directory name -> new preset name. */
|
|
40
|
+
const RENAMED_FROM = { "omd-architect": "omd-ultraworker" };
|
|
32
41
|
function usage() {
|
|
33
42
|
return [
|
|
34
43
|
"omd-dsh <command> [options]",
|
|
@@ -251,15 +260,66 @@ async function collectSourceFiles(rootDir) {
|
|
|
251
260
|
return out;
|
|
252
261
|
}
|
|
253
262
|
// ── matrix ──
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
/**
|
|
264
|
+
* The shipped default matrix (deepseek models) copied to
|
|
265
|
+
* <DSH_HOME>/omd-matrix.json on first run. The repo and the npm package
|
|
266
|
+
* ship ONLY this defaults file — personal model settings live exclusively
|
|
267
|
+
* in the user's <DSH_HOME>/omd-matrix.json and are never uploaded.
|
|
268
|
+
*/
|
|
269
|
+
const DEFAULT_MATRIX_PATH = join(PACKAGE_ROOT, "omd-matrix.default.json");
|
|
270
|
+
/** Parse matrix text; undefined when it is not a usable matrix document. */
|
|
271
|
+
function parseMatrix(text) {
|
|
272
|
+
try {
|
|
273
|
+
const parsed = JSON.parse(text);
|
|
274
|
+
if (parsed !== null && typeof parsed === "object" && parsed.modes !== null && typeof parsed.modes === "object")
|
|
275
|
+
return parsed;
|
|
276
|
+
}
|
|
277
|
+
catch { /* fall through */ }
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
/** Read the shipped default matrix, failing loud when the package is broken. */
|
|
281
|
+
function readDefaultMatrix() {
|
|
282
|
+
if (!existsSync(DEFAULT_MATRIX_PATH))
|
|
283
|
+
throw new Error("omd-dsh: missing default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
|
|
284
|
+
const parsed = parseMatrix(readFileSync(DEFAULT_MATRIX_PATH, "utf8"));
|
|
285
|
+
if (parsed === undefined)
|
|
286
|
+
throw new Error("omd-dsh: malformed default matrix file " + DEFAULT_MATRIX_PATH + " (broken package — reinstall @carljia/omd-dsh)");
|
|
287
|
+
return parsed;
|
|
288
|
+
}
|
|
289
|
+
/** Write a matrix file, creating its parent directory when needed. */
|
|
290
|
+
function writeMatrixFile(path, text) {
|
|
291
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
292
|
+
writeFileSync(path, text, "utf8");
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Load the user's model matrix from <DSH_HOME>/omd-matrix.json, creating it
|
|
296
|
+
* on first run: a still-valid package-root matrix (previous versions) is
|
|
297
|
+
* migrated over, otherwise the shipped deepseek default matrix is written.
|
|
298
|
+
* Dry runs never touch the filesystem and use the shipped defaults in memory.
|
|
299
|
+
*/
|
|
300
|
+
function loadMatrix(flags) {
|
|
301
|
+
if (!existsSync(MATRIX_PATH)) {
|
|
302
|
+
if (flags.dryRun)
|
|
303
|
+
return JSON.parse(JSON.stringify(readDefaultMatrix()));
|
|
304
|
+
const legacyText = existsSync(LEGACY_MATRIX_PATH) ? readFileSync(LEGACY_MATRIX_PATH, "utf8") : undefined;
|
|
305
|
+
if (legacyText !== undefined && parseMatrix(legacyText) !== undefined) {
|
|
306
|
+
writeMatrixFile(MATRIX_PATH, legacyText);
|
|
307
|
+
console.log("omd-dsh: migrated omd-matrix.json: " + LEGACY_MATRIX_PATH + " -> " + MATRIX_PATH);
|
|
308
|
+
console.log("omd-dsh: customize the model matrix any time with `omd-dsh setup`.");
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
const defaults = readDefaultMatrix();
|
|
312
|
+
writeMatrixFile(MATRIX_PATH, JSON.stringify(defaults, null, 2) + "\n");
|
|
313
|
+
console.log("omd-dsh: generated " + MATRIX_PATH + " from the shipped deepseek default matrix; customize it any time with `omd-dsh setup`.");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const text = readFileSync(MATRIX_PATH, "utf8");
|
|
317
|
+
const parsed = parseMatrix(text);
|
|
318
|
+
if (parsed === undefined)
|
|
319
|
+
throw new Error("omd-dsh: malformed " + MATRIX_PATH + " (restore it, or delete it and run omd-dsh setup)");
|
|
260
320
|
return parsed;
|
|
261
321
|
}
|
|
262
|
-
function saveMatrix(m) {
|
|
322
|
+
function saveMatrix(m) { writeMatrixFile(MATRIX_PATH, JSON.stringify(m, null, 2) + "\n"); }
|
|
263
323
|
// ── row rendering (relative indents; the splice prepends the fence indent) ──
|
|
264
324
|
function q(s) { return JSON.stringify(s); }
|
|
265
325
|
function renderModeRow(modeId, cfg) {
|
|
@@ -370,14 +430,14 @@ function discoverModels() {
|
|
|
370
430
|
}
|
|
371
431
|
// ── sync ──
|
|
372
432
|
async function runSync(flags, harnessNodeModules) {
|
|
373
|
-
const matrix = loadMatrix();
|
|
433
|
+
const matrix = loadMatrix(flags);
|
|
374
434
|
const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
375
435
|
const sourceVersion = manifest.version;
|
|
376
436
|
const presetsSourceDir = join(PACKAGE_ROOT, "presets");
|
|
377
437
|
const vendorSourceDir = join(PACKAGE_ROOT, "lib", "vendor");
|
|
378
438
|
const agentPresetsRoot = join(dshHome(), ".agent-presets");
|
|
379
439
|
const presetNames = (await fs.readdir(presetsSourceDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
380
|
-
const report = { synced: [], updated: [], conflicts: [], skipped: [], orphan: [] };
|
|
440
|
+
const report = { synced: [], updated: [], conflicts: [], skipped: [], orphan: [], removed: [] };
|
|
381
441
|
const log = (kind, text) => report[kind].push(text);
|
|
382
442
|
for (const presetName of presetNames) {
|
|
383
443
|
const modeId = presetName.replace(/^omd-/, "");
|
|
@@ -488,19 +548,67 @@ async function runSync(flags, harnessNodeModules) {
|
|
|
488
548
|
continue;
|
|
489
549
|
if (presetNames.includes(entry.name))
|
|
490
550
|
continue;
|
|
491
|
-
|
|
551
|
+
const orphanDir = join(agentPresetsRoot, entry.name);
|
|
552
|
+
const meta = readMeta(orphanDir);
|
|
553
|
+
if (meta === undefined)
|
|
554
|
+
continue;
|
|
555
|
+
const renamedTo = RENAMED_FROM[entry.name];
|
|
556
|
+
if (renamedTo !== undefined && presetNames.includes(renamedTo)) {
|
|
557
|
+
const dirty = await locallyModified(orphanDir, meta);
|
|
558
|
+
if (dirty === undefined) {
|
|
559
|
+
if (!flags.dryRun)
|
|
560
|
+
await fs.rm(orphanDir, { recursive: true, force: true });
|
|
561
|
+
log("removed", entry.name + "/ (renamed to " + renamedTo + " and unmodified -- removed" + (flags.dryRun ? ", dry-run" : "") + ")");
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
log("conflicts", entry.name + "/ (renamed to " + renamedTo + " but locally modified -- keeping your version: " + dirty + ")");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
492
568
|
log("orphan", entry.name + "/ (was installed by omd-dsh but no longer ships with v" + sourceVersion + " -- left untouched)");
|
|
569
|
+
}
|
|
493
570
|
}
|
|
494
571
|
}
|
|
495
572
|
console.log("omd-dsh sync: DSH_HOME=" + dshHome());
|
|
573
|
+
console.log("omd-dsh sync: matrix=" + MATRIX_PATH + " (customize the model matrix any time with `omd-dsh setup`)");
|
|
496
574
|
console.log("omd-dsh sync: harness node_modules=" + harnessNodeModules);
|
|
497
575
|
console.log("omd-dsh sync: source version=" + sourceVersion + (flags.dryRun ? " (dry-run)" : ""));
|
|
498
|
-
for (const key of ["synced", "updated", "skipped", "conflicts", "orphan"])
|
|
576
|
+
for (const key of ["synced", "updated", "skipped", "conflicts", "orphan", "removed"])
|
|
499
577
|
for (const line of report[key])
|
|
500
578
|
console.log(" [" + key + "] " + line);
|
|
501
|
-
const summary = ["synced", "updated", "conflicts", "orphan"].map((key) => report[key].length + " " + key).join(", ");
|
|
579
|
+
const summary = ["synced", "updated", "conflicts", "orphan", "removed"].map((key) => report[key].length + " " + key).join(", ");
|
|
502
580
|
console.log("omd-dsh sync: " + summary + (flags.dryRun ? " (dry-run)" : ""));
|
|
503
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Whether one omd-dsh-managed preset directory differs from the hashes its
|
|
584
|
+
* .omd-meta.json recorded. Returns a description of the first discrepancy,
|
|
585
|
+
* or undefined when every recorded file is present and unmodified and no
|
|
586
|
+
* extra files exist.
|
|
587
|
+
*/
|
|
588
|
+
async function locallyModified(dir, meta) {
|
|
589
|
+
const recorded = meta.files ?? {};
|
|
590
|
+
const current = {};
|
|
591
|
+
const walk = async (d) => {
|
|
592
|
+
for (const entry of await fs.readdir(d, { withFileTypes: true })) {
|
|
593
|
+
const full = join(d, entry.name);
|
|
594
|
+
if (entry.isDirectory())
|
|
595
|
+
await walk(full);
|
|
596
|
+
else if (entry.name !== ".omd-meta.json") {
|
|
597
|
+
const rel = relative(dir, full).split("\\").join("/");
|
|
598
|
+
current[rel] = sha256(await fs.readFile(full, "utf8"));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
await walk(dir);
|
|
603
|
+
for (const rel of new Set([...Object.keys(recorded), ...Object.keys(current)])) {
|
|
604
|
+
if (current[rel] === undefined)
|
|
605
|
+
return "missing file " + rel;
|
|
606
|
+
const recordedHash = recorded[rel] !== undefined && typeof recorded[rel] === "object" && recorded[rel] !== null ? recorded[rel].sha256 : undefined;
|
|
607
|
+
if (typeof recordedHash !== "string" || recordedHash !== current[rel])
|
|
608
|
+
return "modified file " + rel;
|
|
609
|
+
}
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
504
612
|
// ── setup (interactive) ──
|
|
505
613
|
function splitModel(answer) {
|
|
506
614
|
const a = answer.trim();
|
|
@@ -511,7 +619,7 @@ function splitModel(answer) {
|
|
|
511
619
|
return { provider: "deepseek-official", model: a };
|
|
512
620
|
}
|
|
513
621
|
async function runSetup(flags, harnessNodeModules) {
|
|
514
|
-
const matrix = loadMatrix();
|
|
622
|
+
const matrix = loadMatrix(flags);
|
|
515
623
|
const { models, currentDefault } = discoverModels();
|
|
516
624
|
console.log("omd-dsh setup: 发现 DSH 已有模型:");
|
|
517
625
|
for (const m of models)
|
|
@@ -521,7 +629,7 @@ async function runSetup(flags, harnessNodeModules) {
|
|
|
521
629
|
console.log("");
|
|
522
630
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
523
631
|
const ask = async (question) => (await rl.question(question)).trim();
|
|
524
|
-
const modeOrder = ["executor", "
|
|
632
|
+
const modeOrder = ["executor", "ultraworker", "planner", "reviewer", "explorer", "librarian", "chat"];
|
|
525
633
|
for (const modeId of modeOrder) {
|
|
526
634
|
const cfg = matrix.modes[modeId] ?? {};
|
|
527
635
|
const cur = cfg.provider && cfg.model ? cfg.provider + "/" + cfg.model : "";
|
package/lib/index.d.ts
CHANGED
|
@@ -9,9 +9,28 @@
|
|
|
9
9
|
* model and provider template variables), and overrides provider/model
|
|
10
10
|
* on the agent/request waterfall after next(), dropping any inherited
|
|
11
11
|
* reasoningEffort. Both listeners register with prepend: true so this
|
|
12
|
-
* row sits OUTSIDE the entry point per-session selection listener
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* row sits OUTSIDE the entry point per-session selection listener.
|
|
13
|
+
*
|
|
14
|
+
* Precedence vs. the UI model switch: the matrix model is the preset's
|
|
15
|
+
* DEFAULT route, but an explicit user selection wins for the task.
|
|
16
|
+
* The entry selection (installModelSelection) is invisible to this row
|
|
17
|
+
* (it is owned by the host entry point), so the decision is derived
|
|
18
|
+
* from what the waterfall actually resolved:
|
|
19
|
+
*
|
|
20
|
+
* - entry selection == matrix model -> pin (no-op);
|
|
21
|
+
* - entry selection missing -> pin (claim the mode);
|
|
22
|
+
* - session still blank (no request/header)
|
|
23
|
+
* and entry selection == the deployment
|
|
24
|
+
* default captured at mount -> pin (fallback, no pick);
|
|
25
|
+
* - a preset switch (agent-preset/selected)
|
|
26
|
+
* happened after the last request/header and
|
|
27
|
+
* entry selection == the route the session
|
|
28
|
+
* was running before the switch -> pin (new mode claims);
|
|
29
|
+
* - otherwise the user explicitly picked a
|
|
30
|
+
* different model -> yield: the request and
|
|
31
|
+
* the persona variables keep the user's selection, and the row
|
|
32
|
+
* records it on the scoped context as `omdModeOverride` so the
|
|
33
|
+
* omd-task row can route the "deep" tier to the user's model.
|
|
15
34
|
*
|
|
16
35
|
* When provider/model are not configured the row passes everything
|
|
17
36
|
* through and only serves the persona banner variables (inheriting the
|
package/lib/index.js
CHANGED
|
@@ -11,9 +11,28 @@ import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
|
11
11
|
* model and provider template variables), and overrides provider/model
|
|
12
12
|
* on the agent/request waterfall after next(), dropping any inherited
|
|
13
13
|
* reasoningEffort. Both listeners register with prepend: true so this
|
|
14
|
-
* row sits OUTSIDE the entry point per-session selection listener
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* row sits OUTSIDE the entry point per-session selection listener.
|
|
15
|
+
*
|
|
16
|
+
* Precedence vs. the UI model switch: the matrix model is the preset's
|
|
17
|
+
* DEFAULT route, but an explicit user selection wins for the task.
|
|
18
|
+
* The entry selection (installModelSelection) is invisible to this row
|
|
19
|
+
* (it is owned by the host entry point), so the decision is derived
|
|
20
|
+
* from what the waterfall actually resolved:
|
|
21
|
+
*
|
|
22
|
+
* - entry selection == matrix model -> pin (no-op);
|
|
23
|
+
* - entry selection missing -> pin (claim the mode);
|
|
24
|
+
* - session still blank (no request/header)
|
|
25
|
+
* and entry selection == the deployment
|
|
26
|
+
* default captured at mount -> pin (fallback, no pick);
|
|
27
|
+
* - a preset switch (agent-preset/selected)
|
|
28
|
+
* happened after the last request/header and
|
|
29
|
+
* entry selection == the route the session
|
|
30
|
+
* was running before the switch -> pin (new mode claims);
|
|
31
|
+
* - otherwise the user explicitly picked a
|
|
32
|
+
* different model -> yield: the request and
|
|
33
|
+
* the persona variables keep the user's selection, and the row
|
|
34
|
+
* records it on the scoped context as `omdModeOverride` so the
|
|
35
|
+
* omd-task row can route the "deep" tier to the user's model.
|
|
17
36
|
*
|
|
18
37
|
* When provider/model are not configured the row passes everything
|
|
19
38
|
* through and only serves the persona banner variables (inheriting the
|
|
@@ -30,6 +49,36 @@ const Config = z.object({
|
|
|
30
49
|
model: z.string(),
|
|
31
50
|
reasoningEffort: z.string(),
|
|
32
51
|
});
|
|
52
|
+
/**
|
|
53
|
+
* 子代理(subagentDepth > 0)透传:omd-task 的 tier 模型通过显式 agentOptions
|
|
54
|
+
* 落到子代理的 AgentOptions 上,本行若再覆盖会压回模式模型、破坏差异化委派。
|
|
55
|
+
* 无显式 agentOptions 的子代理按 DSH 原生语义继承父级入口选择。
|
|
56
|
+
*/
|
|
57
|
+
function isSubagent(agent) {
|
|
58
|
+
return agent !== undefined && agent !== null && agent.options !== undefined && agent.options !== null
|
|
59
|
+
&& typeof agent.options.subagentDepth === "number" && agent.options.subagentDepth > 0;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 最近一次 request/header 之后是否发生过 agent-preset/selected(UI 预设选择或 /mode 切换)。
|
|
63
|
+
* 切换事件由 api-proxy 与 omd-mode-switch 在 recompose 完成之后追加,因此必须每次实时计算——
|
|
64
|
+
* 本行挂载时事件尚未入日志,挂载时快照会漏判。
|
|
65
|
+
*/
|
|
66
|
+
function presetSwitchedAfterLastRequest(session) {
|
|
67
|
+
const events = session === undefined || session === null ? undefined : session.events;
|
|
68
|
+
if (events === undefined)
|
|
69
|
+
return false;
|
|
70
|
+
let lastHeader = -1;
|
|
71
|
+
let lastSwitch = -1;
|
|
72
|
+
for (const event of events) {
|
|
73
|
+
if (event === undefined || event === null || typeof event.seq !== "number")
|
|
74
|
+
continue;
|
|
75
|
+
if (event.type === "request/header")
|
|
76
|
+
lastHeader = event.seq;
|
|
77
|
+
else if (event.type === "agent-preset/selected")
|
|
78
|
+
lastSwitch = event.seq;
|
|
79
|
+
}
|
|
80
|
+
return lastSwitch > lastHeader;
|
|
81
|
+
}
|
|
33
82
|
function apply(ctx, config) {
|
|
34
83
|
if (scopeOf(ctx) === undefined) {
|
|
35
84
|
throw new Error("omd-mode: refusing to mount outside a scoped context (mode '" + config.mode + "'). " +
|
|
@@ -41,42 +90,94 @@ function apply(ctx, config) {
|
|
|
41
90
|
model: config.model,
|
|
42
91
|
}
|
|
43
92
|
: undefined;
|
|
44
|
-
// 子代理(subagentDepth > 0)透传:omd-task 的 tier 模型通过显式 agentOptions
|
|
45
|
-
// 落到子代理的 AgentOptions 上,本行若再覆盖会压回模式模型、破坏差异化委派。
|
|
46
|
-
// 无显式 agentOptions 的子代理按 DSH 原生语义继承父级入口选择。
|
|
47
|
-
const isSubagent = (agent) => agent !== undefined && agent !== null && agent.options !== undefined && agent.options !== null
|
|
48
|
-
&& typeof agent.options.subagentDepth === "number" && agent.options.subagentDepth > 0;
|
|
49
93
|
if (config.reasoningEffort !== undefined && pinned !== undefined) {
|
|
50
94
|
pinned.reasoningEffort = config.reasoningEffort;
|
|
51
95
|
}
|
|
96
|
+
// 挂载时快照部署默认模型(d0)。blank 会话的入口选择 == d0 视为「未显式选择」。
|
|
97
|
+
// 必须静态快照:session.selectModel 每次都会把用户选择写回全局默认,动态读取会把
|
|
98
|
+
// 用户选择误判为默认值。
|
|
99
|
+
let d0;
|
|
100
|
+
try {
|
|
101
|
+
const def = ctx.get("agentDefaultModel");
|
|
102
|
+
const current = def !== undefined && def !== null ? def.currentSelection() : undefined;
|
|
103
|
+
if (current !== undefined && current !== null && typeof current.provider === "string" && typeof current.model === "string") {
|
|
104
|
+
d0 = { provider: current.provider, model: current.model };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch { /* 读不到默认时退化为仅 entry == pinned 判定 */ }
|
|
108
|
+
// 供同 preset 内的 omd-task 行读取:用户显式切换模型后(本行让路),deep tier 沿用用户选择。
|
|
109
|
+
ctx.omdModeOverride = undefined;
|
|
110
|
+
/**
|
|
111
|
+
* 判定一次入口选择是否应钉到模式模型(true),还是让路给用户选择(false)。
|
|
112
|
+
* @param agent - 顶层 agent(子代理已由调用方过滤)。
|
|
113
|
+
* @param entry - 入口选择 { provider, model };provider/model 缺失 = 无入口选择。
|
|
114
|
+
*/
|
|
115
|
+
function shouldPin(agent, entry) {
|
|
116
|
+
if (pinned === undefined)
|
|
117
|
+
return false;
|
|
118
|
+
if (entry === undefined || entry.provider === undefined || entry.model === undefined)
|
|
119
|
+
return true;
|
|
120
|
+
if (entry.provider === pinned.provider && entry.model === pinned.model)
|
|
121
|
+
return true;
|
|
122
|
+
const session = agent !== undefined && agent !== null ? agent.session : undefined;
|
|
123
|
+
const logged = session === undefined ? undefined : session.requestHeader();
|
|
124
|
+
if (logged === undefined) {
|
|
125
|
+
// 会话尚无任何请求:入口选择要么是部署默认(未选择),要么是首请求前的显式选择。
|
|
126
|
+
// 只有默认值视为「未选择」;其余一律视为用户选择。
|
|
127
|
+
if (d0 !== undefined && entry.provider === d0.provider && entry.model === d0.model)
|
|
128
|
+
return true;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
// 会话已跑过请求:刚切换 preset(/mode 或 UI 选择)时,切换前的路由(logged)是
|
|
132
|
+
// 新模式认领矩阵模型的基线;否则入口选择与模式模型不同 = 用户显式切换,让路。
|
|
133
|
+
if (presetSwitchedAfterLastRequest(session)
|
|
134
|
+
&& logged.config !== undefined && logged.config !== null
|
|
135
|
+
&& entry.provider === logged.config.provider && entry.model === logged.config.model) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
52
140
|
ctx.on("system-prompt/assemble", async (assembly, _context, next) => {
|
|
53
141
|
const assembled = await next();
|
|
54
|
-
|
|
142
|
+
const agent = _context && _context.agent;
|
|
143
|
+
if (pinned === undefined || isSubagent(agent))
|
|
55
144
|
return assembled;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
...assembled
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
145
|
+
const variables = assembled.variables ?? {};
|
|
146
|
+
if (shouldPin(agent, { provider: variables.provider, model: variables.model })) {
|
|
147
|
+
return {
|
|
148
|
+
...assembled,
|
|
149
|
+
variables: {
|
|
150
|
+
...variables,
|
|
151
|
+
provider: pinned.provider,
|
|
152
|
+
model: pinned.model,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// 让路:保留入口(用户)选择注入的变量,persona 展示实际路由的模型。
|
|
157
|
+
return assembled;
|
|
64
158
|
}, { prepend: true });
|
|
65
159
|
ctx.on("agent/request", async (_payload, next) => {
|
|
66
160
|
const resolved = await next();
|
|
67
|
-
|
|
161
|
+
const agent = _payload && _payload.agent;
|
|
162
|
+
if (pinned === undefined || isSubagent(agent))
|
|
68
163
|
return resolved;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
164
|
+
if (shouldPin(agent, { provider: resolved.provider, model: resolved.model })) {
|
|
165
|
+
ctx.omdModeOverride = undefined;
|
|
166
|
+
const stripped = { ...resolved };
|
|
167
|
+
delete stripped.reasoningEffort;
|
|
168
|
+
const out = {
|
|
169
|
+
...stripped,
|
|
170
|
+
provider: pinned.provider,
|
|
171
|
+
model: pinned.model,
|
|
172
|
+
};
|
|
173
|
+
if (pinned.reasoningEffort !== undefined) {
|
|
174
|
+
out.reasoningEffort = pinned.reasoningEffort;
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
78
177
|
}
|
|
79
|
-
|
|
178
|
+
// 用户显式选择了别的模型:本次任务顶层路由用用户选择;deep tier 同步(omd-task 读取)。
|
|
179
|
+
ctx.omdModeOverride = { provider: resolved.provider, model: resolved.model };
|
|
180
|
+
return resolved;
|
|
80
181
|
}, { prepend: true });
|
|
81
182
|
}
|
|
82
183
|
export { Config, apply, inject, name };
|
package/lib/mode.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @carljia/omd-dsh/mode
|
|
3
|
+
*
|
|
4
|
+
* omd-mode-switch: human-facing `/mode` command -- switch the CURRENT
|
|
5
|
+
* session to another OMD agent preset, including mid-conversation.
|
|
6
|
+
*
|
|
7
|
+
* DSH's native `agentPreset.select` host API refuses to recompose a
|
|
8
|
+
* session that has already started (its preset is fixed at the UI level),
|
|
9
|
+
* and the AgentPresets.recompose method itself performs no history check
|
|
10
|
+
* ("the CALLER owns that check"). This row deliberately performs the
|
|
11
|
+
* in-session recompose, then keeps the log honest:
|
|
12
|
+
* - appends `agent-preset/selected`, so resume/fork rebuild the same
|
|
13
|
+
* composition ("model-visible <-> logged" rule);
|
|
14
|
+
* - appends `plan/mode { active: false }` when plan mode is still
|
|
15
|
+
* active, since the switch itself is a mode decision;
|
|
16
|
+
* - steers a notice message so the model knows the tool set changed.
|
|
17
|
+
*
|
|
18
|
+
* Mitigations for the swapped tool catalog: the omd-planner catalog is a
|
|
19
|
+
* subset of omd-executor's (the executor preset also mounts the plan-mode
|
|
20
|
+
* row), so logged planner tool calls stay renderable under the executor
|
|
21
|
+
* composition. Switching between other omd presets follows the same rule
|
|
22
|
+
* and the model simply receives the new catalog on the next request.
|
|
23
|
+
*/
|
|
24
|
+
/** Cordis plugin name. */
|
|
25
|
+
declare const name = "omd-mode-switch";
|
|
26
|
+
/**
|
|
27
|
+
* No mount-time injection: the roster service is resolved at runtime so a
|
|
28
|
+
* rosterless deployment fails only the /mode command, never the preset
|
|
29
|
+
* mount itself.
|
|
30
|
+
*/
|
|
31
|
+
declare const inject: never[];
|
|
32
|
+
declare function apply(ctx: any): void;
|
|
33
|
+
export { apply, inject, name };
|