dsh-subagent-profile 0.3.2 → 0.3.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.
Files changed (40) hide show
  1. package/README.md +77 -40
  2. package/README.zh.md +111 -74
  3. package/docs/screenshots/dispatch-card.png +0 -0
  4. package/docs/screenshots/settings-page1.png +0 -0
  5. package/docs/screenshots/settings-page2.png +0 -0
  6. package/index.mjs +276 -81
  7. package/lib/client.js +3218 -166
  8. package/lib/core/adoption-reminder.mjs +48 -0
  9. package/lib/core/adoption-tracker.mjs +430 -0
  10. package/lib/core/background-ledger.mjs +71 -0
  11. package/lib/core/catalog-cache.mjs +45 -7
  12. package/lib/core/catalog.mjs +6 -6
  13. package/lib/core/cost-evidence.mjs +145 -0
  14. package/lib/core/cost-guard.mjs +71 -44
  15. package/lib/core/decision-trace.mjs +413 -0
  16. package/lib/core/delegation.mjs +111 -50
  17. package/lib/core/dispatch-gates.mjs +153 -0
  18. package/lib/core/dispatch-guard.mjs +156 -0
  19. package/lib/core/dispatch-schema.mjs +103 -14
  20. package/lib/core/dispatch-tool.mjs +220 -204
  21. package/lib/core/draft-gates.mjs +45 -0
  22. package/lib/core/drafts-store.mjs +45 -0
  23. package/lib/core/escape.mjs +130 -0
  24. package/lib/core/evolution-advice.mjs +224 -0
  25. package/lib/core/evolution-ledger.mjs +300 -0
  26. package/lib/core/evolution-summary.mjs +255 -0
  27. package/lib/core/http-routes.mjs +256 -72
  28. package/lib/core/intersection.mjs +6 -9
  29. package/lib/core/presets-sync.mjs +161 -43
  30. package/lib/core/prices.mjs +46 -0
  31. package/lib/core/profile-directory.mjs +139 -0
  32. package/lib/core/profile-provider.mjs +42 -39
  33. package/lib/core/profiles-store.mjs +103 -76
  34. package/lib/core/pure.mjs +110 -66
  35. package/lib/core/reminder-store.mjs +172 -0
  36. package/lib/core/shims.mjs +67 -76
  37. package/lib/core/whitelist.mjs +23 -17
  38. package/package.json +82 -83
  39. package/presets/orchestrator/agent.cordis.yml +59 -87
  40. package/presets/orchestrator/NOTICE +0 -3
@@ -1,34 +1,37 @@
1
- // lib/core/presets-sync.mjs — bundled agent-preset self-install (moved verbatim from
2
- // index.mjs; import-free — node builtins only, no @deepseek-ai dependency).
1
+ // lib/core/presets-sync.mjs — bundled agent 预设自安装(从 index.mjs 逐字移出;
2
+ // node 内置,无 @deepseek-ai 依赖)。
3
3
  //
4
- // On host startup the plugin syncs the bundled `presets/` tree into the DSH
5
- // agent-presets discovery root (~/.dsh/.agent-presets) so the "orchestrator"
6
- // mode appears in the new-session picker without manual copying — the same
7
- // self-install pattern as the shipped dsh-liangshen bundle. The sync is
8
- // per-directory and idempotent (byte-identical trees are skipped; target files
9
- // the bundle no longer ships are pruned); directories the plugin does not own
10
- // are never touched. node:fs cpSync is avoided deliberately: on Node 22 for
11
- // Windows, fs.cpSync({ recursive: true }) can crash the process when a source
12
- // path contains non-ASCII (CJK home dir, nodejs/node#54476), so the copy is
13
- // per-entry, preserving source mtimes.
14
-
15
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, utimesSync } from 'node:fs';
16
- import { basename, dirname, join, relative } from 'node:path';
4
+ // 宿主启动时插件把 bundled `presets/` 树同步进 DSH agent-presets 发现根
5
+ // (~/.dsh/.agent-presets),使「编排者模式」无需手动复制就出现在新建会话
6
+ // 选择器里——与官方 dsh-liangshen bundle 相同的自安装模式。同步按目录、
7
+ // 经哈希门控:per-target sidecar(.dsh-sync.json)记录 bundled 内容哈希与派生树
8
+ // 快照哈希,下次运行跳过未变化的树、跳过(绝不覆盖)用户改过的树;两侧都变时
9
+ // 先把用户侧归档到 .user-modified-<timestamp>/ 再写入升级后的 bundled 内容。
10
+ // bundle 不再提供的目标文件会被剪除;插件不拥有的目录永不触碰。
11
+ // 刻意不用 node:fs cpSync:Node 22 的 Windows 上,fs.cpSync({ recursive: true })
12
+ // 在源路径含非 ASCIICJK 主目录,nodejs/node#54476)时可能崩溃进程,故逐条
13
+ // 复制并保留源 mtime。
14
+
15
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs';
16
+ import { basename, dirname, join, relative, sep } from 'node:path';
17
17
  import { fileURLToPath } from 'node:url';
18
+ import { createHash } from 'node:crypto';
18
19
 
19
- // Absolute path of the bundled preset tree inside this package. This module
20
- // lives in lib/core/, two levels below the package root, so the URL must climb
21
- // back up two segments to still point at the repo-root `presets/` directory
22
- // (same resolved value as when this function lived in index.mjs at the package
23
- // root; Task 8.6 moved the module from lib/ to lib/core/ and the climb must be
24
- // `../../`, not `../` — preflight's preset-tree reconciliation caught the stale
25
- // one returning lib/presets, which silently disabled the startup self-install).
20
+ // 本包内 bundled 预设树的绝对路径。本模块在 lib/core/,比包根低两层,URL 必须
21
+ // 上爬两段才能指向仓库根 `presets/` 目录(与它还在包根的 index.mjs 时解析到的
22
+ // 值相同;写错成 `../` 会指向不存在的 lib/presets,从而静默禁用启动自安装)。
26
23
  export function bundledPresetsRoot() {
27
24
  return fileURLToPath(new URL('../../presets', import.meta.url));
28
25
  }
29
26
 
30
27
  export const MTIME_TOLERANCE_MS = 1000;
31
28
 
29
+ // 同步元数据:sidecar 记录最近一次 bundled/目标内容哈希,归档前缀命名三方
30
+ // 用户编辑备份。两者都是插件本地记账、非 preset 内容——hashTree 排除它们、
31
+ // prune 保留它们(否则 sidecar 自身写入会让目标哈希自指漂移,归档会被剪掉)。
32
+ export const SIDECAR_NAME = '.dsh-sync.json';
33
+ export const ARCHIVE_PREFIX = '.user-modified-';
34
+
32
35
  export function filesUnder(root) {
33
36
  const out = [];
34
37
  const walk = (dir) => {
@@ -42,7 +45,7 @@ export function filesUnder(root) {
42
45
  return out;
43
46
  }
44
47
 
45
- // File identity is bytes; size/mtime are only a fast negative check.
48
+ // 文件同一性按字节;size/mtime 只作快速否定检查。
46
49
  export function sameFile(a, b) {
47
50
  const sa = statSync(a);
48
51
  const sb = statSync(b);
@@ -65,7 +68,7 @@ export function copyTreeSync(sourceDir, targetDir) {
65
68
  }
66
69
  }
67
70
 
68
- // Remove target files not in `keep`, then only the directories emptied by it.
71
+ // 删除 `keep` 之外的 target 文件,再只删因此清空的目录。
69
72
  export function pruneExtras(root, keep) {
70
73
  const parents = new Set();
71
74
  for (const file of filesUnder(root)) {
@@ -85,38 +88,151 @@ export function pruneExtras(root, keep) {
85
88
  }
86
89
  }
87
90
 
88
- // Copy `sourceDir` into `targetDir` idempotently; returns 'synced' or 'current'.
91
+ // preset 树内容哈希:每文件的相对路径 + 字节喂进 sha1。同步元数据(sidecar
92
+ // 任何 `.user-modified-*` 归档)被排除——它是插件记账、非 preset 内容,纳入会使
93
+ // 派生树哈希在 sidecar 被重写(自指)或归档创建时立即漂移。
94
+ export function hashTree(root) {
95
+ const hash = createHash('sha1');
96
+ for (const file of filesUnder(root).sort()) {
97
+ const rel = relative(root, file);
98
+ if (isSyncMetadata(rel)) continue;
99
+ hash.update(rel);
100
+ hash.update('\0');
101
+ hash.update(readFileSync(file));
102
+ hash.update('\0');
103
+ }
104
+ return hash.digest('hex');
105
+ }
106
+
107
+ // 相对 preset 根的路径是否为同步元数据(sidecar 文件,或 `.user-modified-*`
108
+ // 归档目录下的任何内容)。
109
+ export function isSyncMetadata(rel) {
110
+ const head = rel.split(sep)[0];
111
+ return head === SIDECAR_NAME || head.startsWith(ARCHIVE_PREFIX);
112
+ }
113
+
114
+ export function sidecarPath(targetDir) {
115
+ return join(targetDir, SIDECAR_NAME);
116
+ }
117
+
118
+ // 读 sidecar;缺失或畸形(版本错 / 哈希字段缺 / 解析失败)按缺失处理,使下次
119
+ // 同步走保守的首跑路径,而不是信任损坏的记账。
120
+ function readSidecar(targetDir) {
121
+ const path = sidecarPath(targetDir);
122
+ if (!existsSync(path)) return undefined;
123
+ try {
124
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
125
+ if (parsed && parsed.version === 1 && typeof parsed.bundledHash === 'string' && typeof parsed.targetHash === 'string') {
126
+ return parsed;
127
+ }
128
+ return undefined;
129
+ } catch {
130
+ return undefined;
131
+ }
132
+ }
133
+
134
+ function writeSidecar(targetDir, bundledHash, targetHash) {
135
+ writeFileSync(sidecarPath(targetDir), `${JSON.stringify({ version: 1, bundledHash, targetHash }, null, 2)}\n`);
136
+ }
137
+
138
+ // 复制一个文件系统条目(文件或目录),保留逐条语义与源 mtime——与 copyTreeSync
139
+ // 自身文件分支同形。
140
+ function copyEntry(source, dest) {
141
+ const st = statSync(source);
142
+ if (st.isDirectory()) copyTreeSync(source, dest);
143
+ else {
144
+ copyFileSync(source, dest);
145
+ utimesSync(dest, st.atime, st.mtime);
146
+ }
147
+ }
148
+
149
+ // 覆盖前把当前(用户可见的)preset 树归档到
150
+ // <targetDir>/.user-modified-<timestamp>/。sidecar 与既有归档被排除,归档不会
151
+ // 嵌套。写失败返回 false——调用方必须跳过本次同步、保留用户编辑原样
152
+ // (fail-safe:备份失败绝不允许成为销毁用户内容的许可)。
153
+ function archiveUserTree(targetDir) {
154
+ const archiveDir = join(targetDir, `${ARCHIVE_PREFIX}${Date.now()}`);
155
+ // 先拍下要归档的条目再创建 archiveDir:归档位于 targetDir 之下,mkdirSync 后
156
+ // 再读目录会把归档本身读进来,无限递归进它自己。
157
+ const entries = [];
158
+ for (const entry of readdirSync(targetDir)) {
159
+ if (entry === SIDECAR_NAME || entry.startsWith(ARCHIVE_PREFIX)) continue;
160
+ entries.push(entry);
161
+ }
162
+ try {
163
+ mkdirSync(archiveDir, { recursive: true });
164
+ for (const entry of entries) {
165
+ copyEntry(join(targetDir, entry), join(archiveDir, entry));
166
+ }
167
+ return true;
168
+ } catch {
169
+ try { rmSync(archiveDir, { recursive: true, force: true }); } catch { /* best effort */ }
170
+ return false;
171
+ }
172
+ }
173
+
174
+ // pruneExtras 的 keep 集:同步后树必须保留的一切——bundled 文件、sidecar、
175
+ // 既有 `.user-modified-*` 归档下的每个文件。keep 里没有归档条目的话,
176
+ // pruneExtras 会在同步当下删掉归档,静默丢弃它本该保留的用户编辑。
177
+ function computeKeep(sourceDir, targetDir) {
178
+ const keep = new Set(filesUnder(sourceDir).map((f) => relative(sourceDir, f)));
179
+ keep.add(SIDECAR_NAME);
180
+ if (existsSync(targetDir)) {
181
+ for (const entry of readdirSync(targetDir)) {
182
+ if (!entry.startsWith(ARCHIVE_PREFIX)) continue;
183
+ const dir = join(targetDir, entry);
184
+ if (!statSync(dir).isDirectory()) continue;
185
+ for (const f of filesUnder(dir)) keep.add(relative(targetDir, f));
186
+ }
187
+ }
188
+ return keep;
189
+ }
190
+
191
+ // 同步一个 preset 目录。返回 'synced'(写入或升级)、'current'(未变)或
192
+ // 'user-modified'(target 偏离上次同步快照而跳过——绝不覆盖用户编辑)。判定表:
193
+ // - target 不存在 → 复制 + 写 sidecar → 'synced'
194
+ // - 无 sidecar、target 存在 → 一次性归档 + 同步 → 'synced'
195
+ // - bundled 未变、target 相同 → 'current'
196
+ // - bundled 未变、target 漂移 → 'user-modified'(跳过)
197
+ // - bundled 变了 → 漂移则先归档,再同步 → 'synced'
89
198
  export function syncOnePreset(sourceDir, targetDir) {
90
- const sourceFiles = filesUnder(sourceDir);
91
- const sourceSet = new Set(sourceFiles.map((f) => relative(sourceDir, f)));
199
+ const bundledHash = hashTree(sourceDir);
92
200
  if (existsSync(targetDir) && !statSync(targetDir).isDirectory()) {
93
- rmSync(targetDir, { recursive: true, force: true });
201
+ // target 意外是文件(同名冲突)时先改名备份再清,避免直接 rmSync 丢失用户内容。
202
+ try { renameSync(targetDir, `${targetDir}.removed-${Date.now()}`); } catch { rmSync(targetDir, { recursive: true, force: true }); }
94
203
  }
95
204
  if (!existsSync(targetDir)) {
96
205
  copyTreeSync(sourceDir, targetDir);
97
- pruneExtras(targetDir, sourceSet);
206
+ writeSidecar(targetDir, bundledHash, hashTree(targetDir));
98
207
  return 'synced';
99
208
  }
100
- let dirty = false;
101
- for (const file of sourceFiles) {
102
- const dest = join(targetDir, relative(sourceDir, file));
103
- if (!existsSync(dest) || !sameFile(file, dest)) { dirty = true; break; }
209
+ const sidecar = readSidecar(targetDir);
210
+ // sidecar + 既有 target = 门控前状态:无法判断用户是否编辑过,按「可能编辑
211
+ // 过」处理,在首次门控同步前归档一次(一次性迁移)。
212
+ if (sidecar === undefined) {
213
+ if (!archiveUserTree(targetDir)) return 'user-modified';
214
+ copyTreeSync(sourceDir, targetDir);
215
+ pruneExtras(targetDir, computeKeep(sourceDir, targetDir));
216
+ writeSidecar(targetDir, bundledHash, hashTree(targetDir));
217
+ return 'synced';
104
218
  }
105
- if (!dirty) {
106
- for (const file of filesUnder(targetDir)) {
107
- if (!sourceSet.has(relative(targetDir, file))) { dirty = true; break; }
108
- }
219
+ const currentTargetHash = hashTree(targetDir);
220
+ if (sidecar.bundledHash === bundledHash) {
221
+ return sidecar.targetHash === currentTargetHash ? 'current' : 'user-modified';
222
+ }
223
+ // bundled 升级。用户也改过 target(快照漂移)时先归档其树,编辑不会静默丢失。
224
+ if (sidecar.targetHash !== currentTargetHash) {
225
+ if (!archiveUserTree(targetDir)) return 'user-modified';
109
226
  }
110
- if (!dirty) return 'current';
111
- pruneExtras(targetDir, sourceSet);
112
227
  copyTreeSync(sourceDir, targetDir);
113
- pruneExtras(targetDir, sourceSet);
228
+ pruneExtras(targetDir, computeKeep(sourceDir, targetDir));
229
+ writeSidecar(targetDir, bundledHash, hashTree(targetDir));
114
230
  return 'synced';
115
231
  }
116
232
 
117
- // Sync every preset directory under `presets/` into the target discovery root.
233
+ // `presets/` 下每个 preset 目录同步进目标发现根。
118
234
  export function syncBundledPresets(targetRoot) {
119
- const result = { synced: [], current: [], failed: [] };
235
+ const result = { synced: [], current: [], userModified: [], failed: [] };
120
236
  const sourceRoot = bundledPresetsRoot();
121
237
  mkdirSync(targetRoot, { recursive: true });
122
238
  if (existsSync(sourceRoot)) {
@@ -126,7 +242,9 @@ export function syncBundledPresets(targetRoot) {
126
242
  const id = basename(source);
127
243
  try {
128
244
  const outcome = syncOnePreset(source, join(targetRoot, id));
129
- (outcome === 'synced' ? result.synced : result.current).push(id);
245
+ if (outcome === 'synced') result.synced.push(id);
246
+ else if (outcome === 'current') result.current.push(id);
247
+ else result.userModified.push(id);
130
248
  } catch (error) {
131
249
  result.failed.push({ id, error: error instanceof Error ? error.message : String(error) });
132
250
  }
@@ -0,0 +1,46 @@
1
+ // lib/core/prices.mjs — 模型单价表(纯数据)。
2
+ // 来源:docs/measured-params.md / docs/cost-closed-loop-design.md(2026-08-24 实测)。
3
+ // 口径:估算、非精确计费;A 级为「单次均价」、B 级为「五段单价」(元/千 token)。
4
+ // 可被 A″ / F 数据层复用(同源同表,不散落魔法数)。
5
+
6
+ // A 级:模型 → 单次均价(元/次)。同规模派发实测:flash 0.0039、继承父(pro)0.0104。
7
+ export const MODEL_AVG_COST = Object.freeze({
8
+ 'deepseek-v4-flash': 0.0039,
9
+ 'deepseek-v4-pro': 0.0104,
10
+ });
11
+
12
+ // B 级:模型 → 五段单价(元/千 token)。当前仅有单次均价实测,五段单价先按
13
+ // 均价占位(input/output 同价),保留键结构供未来实测回填;cache 段缺数据记 null。
14
+ export const MODEL_USAGE_PRICES = Object.freeze({
15
+ 'deepseek-v4-flash': Object.freeze({
16
+ inputTokens: null,
17
+ outputTokens: null,
18
+ cacheReadTokens: null,
19
+ cacheWriteTokens: null,
20
+ reasoningTokens: null,
21
+ avgCost: MODEL_AVG_COST['deepseek-v4-flash'],
22
+ }),
23
+ 'deepseek-v4-pro': Object.freeze({
24
+ inputTokens: null,
25
+ outputTokens: null,
26
+ cacheReadTokens: null,
27
+ cacheWriteTokens: null,
28
+ reasoningTokens: null,
29
+ avgCost: MODEL_AVG_COST['deepseek-v4-pro'],
30
+ }),
31
+ });
32
+
33
+ // 模型名归一:常见别名/前缀统一到价格键;未知返回 undefined(fail-soft 不猜价)。
34
+ export function priceKeyFor(model) {
35
+ if (typeof model !== 'string' || model === '') return undefined;
36
+ const m = model.trim().toLowerCase();
37
+ if (MODEL_AVG_COST[m] !== undefined) return m;
38
+ if (m.includes('flash')) return 'deepseek-v4-flash';
39
+ if (m.includes('pro')) return 'deepseek-v4-pro';
40
+ return undefined;
41
+ }
42
+
43
+ export function avgCostFor(model) {
44
+ const key = priceKeyFor(model);
45
+ return key === undefined ? undefined : MODEL_AVG_COST[key];
46
+ }
@@ -0,0 +1,139 @@
1
+ // lib/core/profile-directory.mjs — 派发方案目录的单一数据源。
2
+ // profileSectionText(模型可见段)与 profileSnapshotOf(决策轨迹记录面)都从
3
+ // 本模块的 profileDirectoryRows 派生,构造保证「模型看到 ≡ 记录面」的 id、顺序与
4
+ // 字段集。未来 F 数据层注入 avg_cost / success_rate 只改这里一处。
5
+ //
6
+ // 仅依赖 lib/core/pure.mjs(tierSortKey);无 @deepseek-ai 依赖,可被 bare-CI
7
+ // 单测直接 import。
8
+
9
+ import { tierSortKey } from './pure.mjs';
10
+
11
+ // 决策输入快照阈值:profiles_snapshot 条目数与 description 截断长度。
12
+ const MAX_PROFILE_SNAPSHOT_ENTRIES = 6;
13
+ const PROFILE_DESC_CHARS = 80;
14
+
15
+ function snapshotEntry(row, maxDesc) {
16
+ const description = typeof row.description === 'string' ? row.description : '';
17
+ return {
18
+ id: row.id,
19
+ ...(description !== '' ? { description: description.length > maxDesc ? `${description.slice(0, maxDesc)}…` : description } : {}),
20
+ ...(typeof row.preset === 'string' && row.preset !== '' ? { preset: row.preset } : {}),
21
+ ...(typeof row.model === 'string' && row.model !== '' ? { model: row.model } : {}),
22
+ ...(typeof row.provider === 'string' && row.provider !== '' ? { provider: row.provider } : {}),
23
+ ...(typeof row.tokenTier === 'string' && row.tokenTier !== '' ? { tokenTier: row.tokenTier } : {}),
24
+ ...(typeof row.avgCost === 'number' && Number.isFinite(row.avgCost) ? { avgCost: row.avgCost } : {}),
25
+ ...(typeof row.successRate === 'number' && Number.isFinite(row.successRate) ? { successRate: row.successRate } : {}),
26
+ };
27
+ }
28
+
29
+ // 模型可见 profile 目录快照:输入为 profileDirectoryRows 的结构化行。description
30
+ // 截断、条目数截到 maxEntries;chosenId 超出 maxEntries 时并入该条并打
31
+ // chosenOutsideSnapshot(37b:截断不得制造审计空洞)。
32
+ export function profileSnapshotOf(profiles, { maxEntries = MAX_PROFILE_SNAPSHOT_ENTRIES, maxDesc = PROFILE_DESC_CHARS, chosenId } = {}) {
33
+ const list = Array.isArray(profiles) ? profiles : [];
34
+ const full = [];
35
+ for (const row of list) {
36
+ if (row === null || typeof row !== 'object' || typeof row.id !== 'string' || row.id === '') continue;
37
+ if (row.enabled === false) continue;
38
+ full.push(snapshotEntry(row, maxDesc));
39
+ }
40
+ const entries = full.slice(0, maxEntries);
41
+ let truncated = full.length > maxEntries;
42
+ if (typeof chosenId === 'string' && chosenId !== '' && chosenId !== '(inline)' && !entries.some((e) => e.id === chosenId)) {
43
+ const chosen = full.find((e) => e.id === chosenId);
44
+ if (chosen !== undefined) {
45
+ entries.push({ ...chosen, chosenOutsideSnapshot: true });
46
+ truncated = true;
47
+ }
48
+ }
49
+ return { entries, truncated, total: full.length };
50
+ }
51
+
52
+ // store 兼容 createProfileStore 返回形态(含 profiles Map);也接受数组(测试与
53
+ // 非标准调用方)。只保留启用且 id 合法的方案;按 tokenTier cheap-first 稳定排序,
54
+ // tier 相同按 id 升序(确定性,不依赖 Map 插入序的跨运行差异)。
55
+ export function profileDirectoryRows(store) {
56
+ const source = store !== null && typeof store === 'object' && store.profiles !== undefined
57
+ ? [...store.profiles.values()]
58
+ : (Array.isArray(store) ? store : []);
59
+ return source
60
+ .filter((p) => p !== null && typeof p === 'object' && typeof p.id === 'string' && p.id !== '' && p.enabled !== false)
61
+ .sort((a, b) => tierSortKey(a.tokenTier) - tierSortKey(b.tokenTier) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
62
+ .map((p) => ({
63
+ id: p.id,
64
+ description: typeof p.description === 'string' ? p.description : '',
65
+ ...(typeof p.preset === 'string' && p.preset !== '' ? { preset: p.preset } : {}),
66
+ ...(typeof p.model === 'string' && p.model !== '' ? { model: p.model } : {}),
67
+ ...(typeof p.provider === 'string' && p.provider !== '' ? { provider: p.provider } : {}),
68
+ ...(typeof p.tokenTier === 'string' && p.tokenTier !== '' ? { tokenTier: p.tokenTier } : {}),
69
+ ...(typeof p.avgCost === 'number' && Number.isFinite(p.avgCost) ? { avgCost: p.avgCost } : {}),
70
+ ...(typeof p.successRate === 'number' && Number.isFinite(p.successRate) ? { successRate: p.successRate } : {}),
71
+ }));
72
+ }
73
+
74
+
75
+ // 从 summaries.json 的 l1/l2 聚合为每个 profile 提取 { avgCost, successRate, n }。
76
+ // 匹配优先级:profile.model → L2 model 轴;profile.preset → L2 preset 轴。多个 L1
77
+ // 组共享同一轴值时按成本与部署量合并(avg_cost = Σestimated_cost / Σpriced;
78
+ // successRate = Σcompleted / Σdeployments)。无数据返回 null,调用方省略显示。
79
+ export function profileStatsFromSummaries(store, summaries) {
80
+ const rows = profileDirectoryRows(store);
81
+ const l2 = summaries !== null && typeof summaries === 'object' && summaries.l2 !== null && typeof summaries.l2 === 'object' ? summaries.l2 : {};
82
+ const byModel = new Map();
83
+ const byPreset = new Map();
84
+ for (const [key, group] of Object.entries(l2)) {
85
+ if (group === null || typeof group !== 'object') continue;
86
+ const modelMatch = /:model:([^|]+)$/.exec(key);
87
+ if (modelMatch) {
88
+ const arr = byModel.get(modelMatch[1]) ?? [];
89
+ arr.push(group);
90
+ byModel.set(modelMatch[1], arr);
91
+ continue;
92
+ }
93
+ const presetMatch = /:preset:([^|]+)$/.exec(key);
94
+ if (presetMatch) {
95
+ const arr = byPreset.get(presetMatch[1]) ?? [];
96
+ arr.push(group);
97
+ byPreset.set(presetMatch[1], arr);
98
+ }
99
+ }
100
+ const merge = (groups) => {
101
+ let deployments = 0;
102
+ let completed = 0;
103
+ let priced = 0;
104
+ let cost = 0;
105
+ for (const g of groups) {
106
+ deployments += Number.isFinite(g.deployments_total) ? g.deployments_total : 0;
107
+ completed += Number.isFinite(g.outcome?.completed) ? g.outcome.completed : 0;
108
+ priced += Number.isFinite(g.cost?.priced) ? g.cost.priced : 0;
109
+ cost += Number.isFinite(g.cost?.estimated_cost) ? g.cost.estimated_cost : 0;
110
+ }
111
+ if (deployments <= 0 && priced <= 0) return null;
112
+ return {
113
+ avgCost: priced > 0 ? Number((cost / priced).toFixed(4)) : null,
114
+ successRate: deployments > 0 ? Number((completed / deployments).toFixed(3)) : null,
115
+ n: deployments,
116
+ };
117
+ };
118
+ const out = new Map();
119
+ for (const row of rows) {
120
+ let stats = null;
121
+ if (typeof row.model === 'string' && row.model !== '') stats = merge(byModel.get(row.model) ?? []);
122
+ if (stats === null && typeof row.preset === 'string' && row.preset !== '') stats = merge(byPreset.get(row.preset) ?? []);
123
+ if (stats !== null) out.set(row.id, stats);
124
+ }
125
+ return out;
126
+ }
127
+
128
+ // 用 stats map 为目录行补 avgCost/successRate(F 数据层 join)。stats 为
129
+ // profileStatsFromSummaries 的返回值;行上已有 avgCost/successRate 时优先保留
130
+ // 显式值(store 侧/测试注入),stats 只补缺失。
131
+ export function applyProfileStats(rows, stats) {
132
+ const map = stats instanceof Map ? stats : new Map(Object.entries(stats ?? {}));
133
+ return rows.map((row) => ({
134
+ ...row,
135
+ ...(row.avgCost === undefined && map.get(row.id)?.avgCost !== null && map.get(row.id)?.avgCost !== undefined ? { avgCost: map.get(row.id).avgCost } : {}),
136
+ ...(row.successRate === undefined && map.get(row.id)?.successRate !== null && map.get(row.id)?.successRate !== undefined ? { successRate: map.get(row.id).successRate } : {}),
137
+ ...(map.get(row.id)?.n !== undefined ? { n: map.get(row.id).n } : {}),
138
+ }));
139
+ }
@@ -3,15 +3,13 @@
3
3
  // `ctx.subagents.registerProvider({...})` 块逐字拆出。仅引用 lib + shims;
4
4
  // 无 @deepseek-ai 依赖(shims 是唯一入口)。
5
5
  //
6
- // Injection: every apply-closure / ctx dependency is an explicit parameter —
7
- // subagents the registry this provider is registered into
8
- // (subagents.registerProvider is called by the factory),
9
- // store the profile store (getAllowFailOpen for the cost guard),
10
- // getEnabled reads the apply-closure `enabled` flag (start fails loud when
11
- // off),
12
- // logger ctx.logger (decision-level child log).
13
- // The factory returns whatever registerProvider returns (the caller keeps the
14
- // original `if (typeof disposeProvider === 'function') ctx.effect(...)` shape).
6
+ // 注入:所有 apply 闭包 / ctx 依赖都是显式参数——
7
+ // subagents provider 注册进的注册表(工厂内调用 subagents.registerProvider),
8
+ // store profile store(getAllowFailOpen cost guard),
9
+ // getEnabled 读取 apply 闭包 `enabled` 标志(关时 start fail-loud),
10
+ // logger ctx.logger(决策级子会话日志)。
11
+ // 工厂返回 registerProvider 的返回值(调用方保持原有
12
+ // `if (typeof disposeProvider === 'function') ctx.effect(...)` 形状)。
15
13
  //
16
14
  // start 按预检段(runStartPreflight)/ 结算+取消接线段(wireChildLifecycle)
17
15
  // 拆为模块级私有函数;start 内嵌 setup 按 ①-⑦ 步拆 setupChild +
@@ -40,26 +38,32 @@ import { DELEGATION_CONTEXT, buildDispatchMeta } from './delegation.mjs';
40
38
  // agentOptions,一次性返回 start 后续段所需的全部状态。
41
39
  async function runStartPreflight(request, deps) {
42
40
  if (!deps.getEnabled()) {
43
- throw new Error('dispatch: the subagent-profile plugin is disabled (re-enable it in 设置 → 子 Agent 方案)');
41
+ throw new Error('dispatch: 插件已禁用(可在设置 → 子 Agent 方案 中重新启用)');
44
42
  }
45
43
  const profile = request.profile;
46
44
  if (profile === undefined) {
47
- throw new Error('dispatch: request.profile is missing (the dispatch tool must resolve a profile before starting)');
45
+ throw new Error('dispatch: request.profile 缺失(dispatch 工具在启动子 Agent 前必须解析出 profile,请检查调用参数)');
48
46
  }
49
47
  const parent = request.parent;
50
48
  // 同步捕获委派策略,在首个 await 之前——之后的父会话切换属于父的未来,
51
49
  // 不属于本 child(shipped captureDelegatedPolicyOverrides)。
52
50
  const delegated = captureDelegatedPolicyOverrides(parent);
53
- // 权威 preset whitelist 检查(对照运行时名册)。
54
- const whitelist = new Set(await resolveWhitelist(parent.ctx.get('agentPresets')));
51
+ // 权威 preset whitelist 检查(对照运行时名册)。逃生舱只叠加:开关开时
52
+ // getEscapeSet 返回放行集,否则空数组(零叠加)。
53
+ const whitelist = new Set(await resolveWhitelist(parent.ctx.get('agentPresets'), deps.getEscapeSet()));
55
54
  if (typeof profile.preset === 'string' && profile.preset !== 'inherit' && !whitelist.has(profile.preset)) {
56
- throw new Error(`dispatch: preset "${profile.preset}" is not in the target-preset whitelist`);
55
+ throw new Error(`dispatch: 预设 "${profile.preset}" 不在 system-trust 白名单(可在设置页改用受信任预设)`);
56
+ }
57
+ // 放行审计(provider 渠道):成员经逃生舱放行时同样写高可见治理审计(「每个放行
58
+ // 都留痕」的绝对语义覆盖非 dispatch 渠道)。成本闸在本函数下方、交集在子会话
59
+ // setup 时执行——审计快照按 provider 渠道口径标注。
60
+ if (typeof deps.recordEscapeAllowProvider === 'function' && deps.getEscapeSet().includes(profile.preset)) {
61
+ deps.recordEscapeAllowProvider(parent, profile.preset);
57
62
  }
58
63
  // 权威 cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen 门控;
59
64
  // llm 目录读取走共享 catalog 快照)。
60
65
  await assertCostGuard(parent, profile, deps.store.getAllowFailOpen(), deps.logger, deps.catalog);
61
- // Delegation depth: shipped helpers — assert the cap value, then resolve
62
- // the child depth (parent floor + 1) and enforce the cap.
66
+ // 委派深度:官方助手——先断言上限值,再解析子深度(父底 + 1)并执行上限。
63
67
  assertSubagentMaxDepth(profile.maxDepth);
64
68
  const childDepth = resolveChildDepth(parent, profile.maxDepth);
65
69
  const childId = randomUUID();
@@ -88,9 +92,8 @@ function buildProviderMeta(parent, profile, swapPreset, childDepth) {
88
92
  });
89
93
  }
90
94
 
91
- // agentOptions: shipped resolveChildAgentOptions parent route inherited
92
- // unless the profile overrides provider/model/maxTokens, stamped with the
93
- // child's own delegation depth.
95
+ // agentOptions:官方 resolveChildAgentOptions——父路由继承,除非 profile 覆盖
96
+ // provider/model/maxTokens,并盖上子自身的委派深度。
94
97
  function buildProviderAgentOptions(parent, profile, childDepth) {
95
98
  return resolveChildAgentOptions(parent, {
96
99
  ...(profile.provider !== undefined ? { provider: profile.provider } : {}),
@@ -103,7 +106,7 @@ function buildProviderAgentOptions(parent, profile, childDepth) {
103
106
  async function startProvider(request, deps) {
104
107
  const { parent, profile, delegated, childId, swapPreset, meta, agentOptions } = await runStartPreflight(request, deps);
105
108
  if (request.signal !== undefined && request.signal.aborted) {
106
- throw new Error('dispatch: subagent request was aborted before child publication');
109
+ throw new Error('dispatch: Agent 请求在发布前已被取消(可重试一次派发)');
107
110
  }
108
111
  const handle = await parent.ctx.agents.create({
109
112
  sessionId: childId,
@@ -118,12 +121,12 @@ async function startProvider(request, deps) {
118
121
  // --- start 内嵌 setup(childCtx):按 ①-⑦ 步装配子 Agent 会话(从 start 拆出)-------
119
122
 
120
123
  async function setupChild(childCtx, { parent, profile, swapPreset, delegated, request }) {
121
- // ① Preset composition: explicit swap mounts the target preset; otherwise
122
- // compose from the parent. Rosterless + explicit swap fails loud.
124
+ // ① 预设组合:显式 swap 挂载目标预设;否则从父组合。无名册 + 显式 swap
125
+ // fail-loud
123
126
  const childPresets = childCtx.get('agentPresets');
124
127
  if (swapPreset) {
125
128
  if (childPresets === undefined) {
126
- throw new Error('dispatch: cannot swap preset in a rosterless deployment');
129
+ throw new Error('dispatch: 无法在无预设名册的部署中切换预设(该部署不支持自定义预设挂载,可省略 preset 以继承父预设)');
127
130
  }
128
131
  await childPresets.mount(childCtx, profile.preset);
129
132
  } else if (childPresets !== undefined) {
@@ -131,12 +134,12 @@ async function setupChild(childCtx, { parent, profile, swapPreset, delegated, re
131
134
  }
132
135
  // ② Tool intersection(safety gate 1,实现见 restrictChildTools)。
133
136
  restrictChildTools(childCtx, parent, profile);
134
- // ③ Delegation scope declaration (when systemPrompt is available).
137
+ // ③ 委派范围声明(systemPrompt 可用时)。
135
138
  const systemPrompt = childCtx.get('systemPrompt');
136
139
  if (systemPrompt !== undefined) {
137
140
  systemPrompt.context({ name: 'subagent:delegation', order: 120, text: DELEGATION_CONTEXT });
138
141
  }
139
- // ④ Persona shadow (overrides deployment:persona at order 0). 双防线 prefix.
142
+ // ④ Persona 影子段(覆盖 order 0 的 deployment:persona)。双防线 prefix
140
143
  if (profile.persona !== undefined && systemPrompt !== undefined) {
141
144
  systemPrompt.section({
142
145
  name: 'deployment:persona',
@@ -144,30 +147,30 @@ async function setupChild(childCtx, { parent, profile, swapPreset, delegated, re
144
147
  text: profile.persona.length > 0 ? `${GUIDANCE_PREFIX}${profile.persona}` : profile.persona,
145
148
  });
146
149
  }
147
- // ⑤ Reasoning-effort injection into every child request.
150
+ // ⑤ 推理档位注入每个子请求。
148
151
  if (profile.reasoningEffort !== undefined) {
149
152
  childCtx.on('agent/request', async (_payload, next) => {
150
153
  const resolved = await next();
151
154
  return { ...resolved, reasoningEffort: profile.reasoningEffort };
152
155
  });
153
156
  }
154
- // ⑥ Descriptor append inside the child's first turn.
157
+ // ⑥ Descriptor append inside the child's first turn(descriptor 存在才 append;
158
+ // 核心注入的是对象(snapshotJsonValue 快照),早期版本为字符串,故只判存在不判类型)。
155
159
  let appended = false;
156
160
  childCtx.on('agent/pre-step', async ({ agent }, next) => {
157
161
  const decision = await next();
158
162
  if (!appended && decision.kind === 'enter') {
159
163
  appended = true;
160
- agent.session.append('subagent/descriptor', request.descriptor);
164
+ if (request.descriptor !== undefined && request.descriptor !== null) agent.session.append('subagent/descriptor', request.descriptor);
161
165
  }
162
166
  return decision;
163
167
  });
164
- // ⑦ Delegation policy appends (shipped helper).
168
+ // ⑦ 委派策略追加(官方助手)。
165
169
  appendDelegatedPolicyOverrides(childCtx.agent.session, delegated);
166
170
  }
167
171
 
168
- // ② Tool intersection (safety gate 1): parent set child set, minus
169
- // run_code, minus deny, then narrowed by allow when present (pure core in
170
- // lib/core/intersection.mjs: computeEffectiveAllow).
172
+ // ② 工具交集(安全门 1):父集 子集,减去 run_code、减去 deny,存在 allow
173
+ // 时再收窄(纯核心在 lib/core/intersection.mjs:computeEffectiveAllow)。
171
174
  function restrictChildTools(childCtx, parent, profile) {
172
175
  const parentNames = new Set(parent.ctx.tools.schemas(parent).map((schema) => schema.name));
173
176
  const childNames = childCtx.tools.schemas(childCtx.agent).map((schema) => schema.name);
@@ -175,14 +178,14 @@ function restrictChildTools(childCtx, parent, profile) {
175
178
  // shipped restrict 不对 allow:[] throw——在此 fail-loud,让空交集显式化
176
179
  // (throw ⇒ setupAndPublish 回滚创建)。
177
180
  if (effective.length === 0) {
178
- throw new Error('dispatch: child tool intersection is empty (zero tools)');
181
+ throw new Error('dispatch: 子工具交集为空(父工具集与子工具集无交集,或 toolFilter 过滤后为空,可放宽过滤器后重试)');
179
182
  }
180
- // restrict throws on unknown/scope-local/reserved allow sets: wrap
181
- // in a clean error and rethrow to trigger creation rollback.
183
+ // restrict 对未知/scope-local/保留 allow 集抛错:包成干净错误再 rethrow,
184
+ // 触发创建回滚。
182
185
  try {
183
186
  childCtx.tools.restrict({ allow: effective });
184
187
  } catch (error) {
185
- throw new Error(`dispatch: child tool restriction failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
188
+ throw new Error(`dispatch: 子工具限制失败(未能按交集限制子工具集:${error instanceof Error ? error.message : String(error)}),可检查 toolFilter 是否含未知工具`, { cause: error });
186
189
  }
187
190
  }
188
191
 
@@ -210,7 +213,7 @@ function wireChildLifecycle(handle, request, childId, swapPreset, profile, logge
210
213
  await child.whenIdle();
211
214
  }
212
215
  const settled = readResult(child, boundary, flags.cancelled);
213
- // Decision-level log at result settlement (readResult, before return).
216
+ // 决策级日志在结果结算时(readResult,返回前)。
214
217
  logger.info('[dsh-subagent-profile] child:', JSON.stringify({ childId, preset: profile.preset ?? 'inherit', swapPreset, stopReason: settled.stopReason }));
215
218
  return settled;
216
219
  } finally {
@@ -230,8 +233,8 @@ function wireChildLifecycle(handle, request, childId, swapPreset, profile, logge
230
233
  };
231
234
  }
232
235
 
233
- export function createProfileProvider({ subagents, store, getEnabled, logger, catalog }) {
234
- const deps = { store, getEnabled, logger, catalog };
236
+ export function createProfileProvider({ subagents, store, getEnabled, logger, catalog, getEscapeSet }) {
237
+ const deps = { store, getEnabled, logger, catalog, getEscapeSet };
235
238
  return subagents.registerProvider({
236
239
  name: 'profile',
237
240
  capabilities: { outputSchema: false, depthLimit: true, toolFilter: true, persona: true },