dsh-subagent-profile 0.3.2 → 0.3.3
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 +38 -29
- package/README.zh.md +38 -29
- package/index.mjs +188 -62
- package/lib/client.js +1240 -47
- package/lib/core/catalog-cache.mjs +45 -7
- package/lib/core/catalog.mjs +6 -6
- package/lib/core/cost-guard.mjs +71 -44
- package/lib/core/decision-trace.mjs +433 -0
- package/lib/core/delegation.mjs +106 -48
- package/lib/core/dispatch-gates.mjs +146 -0
- package/lib/core/dispatch-guard.mjs +150 -0
- package/lib/core/dispatch-schema.mjs +98 -14
- package/lib/core/dispatch-tool.mjs +208 -199
- package/lib/core/escape.mjs +130 -0
- package/lib/core/evolution-ledger.mjs +289 -0
- package/lib/core/evolution-summary.mjs +432 -0
- package/lib/core/http-routes.mjs +155 -40
- package/lib/core/intersection.mjs +6 -9
- package/lib/core/presets-sync.mjs +256 -136
- package/lib/core/profile-provider.mjs +41 -39
- package/lib/core/profiles-store.mjs +103 -76
- package/lib/core/pure.mjs +110 -66
- package/lib/core/shims.mjs +56 -75
- package/lib/core/whitelist.mjs +23 -17
- package/package.json +2 -3
- package/presets/orchestrator/agent.cordis.yml +243 -271
- package/presets/orchestrator/NOTICE +0 -3
|
@@ -1,136 +1,256 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// Windows
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, utimesSync } from 'node:fs';
|
|
16
|
-
import { basename, dirname, join, relative } from 'node:path';
|
|
17
|
-
import { fileURLToPath } from 'node:url';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
1
|
+
// lib/core/presets-sync.mjs — bundled agent 预设自安装(从 index.mjs 逐字移出;
|
|
2
|
+
// 仅 node 内置,无 @deepseek-ai 依赖)。
|
|
3
|
+
//
|
|
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
|
+
// 在源路径含非 ASCII(CJK 主目录,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
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
// 本包内 bundled 预设树的绝对路径。本模块在 lib/core/,比包根低两层,URL 必须
|
|
21
|
+
// 上爬两段才能指向仓库根 `presets/` 目录(与它还在包根的 index.mjs 时解析到的
|
|
22
|
+
// 值相同;Task 8.6 把模块从 lib/ 挪到 lib/core/ 后爬升必须写成 `../../` 而不是
|
|
23
|
+
// `../`——preflight 的 preset 树对账抓到了那个错误版本指向不存在的 lib/presets,
|
|
24
|
+
// 它会静默禁用启动自安装)。
|
|
25
|
+
export function bundledPresetsRoot() {
|
|
26
|
+
return fileURLToPath(new URL('../../presets', import.meta.url));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const MTIME_TOLERANCE_MS = 1000;
|
|
30
|
+
|
|
31
|
+
// 同步元数据:sidecar 记录最近一次 bundled/目标内容哈希,归档前缀命名三方
|
|
32
|
+
// 用户编辑备份。两者都是插件本地记账、非 preset 内容——hashTree 排除它们、
|
|
33
|
+
// prune 保留它们(否则 sidecar 自身写入会让目标哈希自指漂移,归档会被剪掉)。
|
|
34
|
+
export const SIDECAR_NAME = '.dsh-sync.json';
|
|
35
|
+
export const ARCHIVE_PREFIX = '.user-modified-';
|
|
36
|
+
|
|
37
|
+
export function filesUnder(root) {
|
|
38
|
+
const out = [];
|
|
39
|
+
const walk = (dir) => {
|
|
40
|
+
for (const entry of readdirSync(dir)) {
|
|
41
|
+
const path = join(dir, entry);
|
|
42
|
+
if (statSync(path).isDirectory()) walk(path);
|
|
43
|
+
else out.push(path);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
walk(root);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 文件同一性按字节;size/mtime 只作快速否定检查。
|
|
51
|
+
export function sameFile(a, b) {
|
|
52
|
+
const sa = statSync(a);
|
|
53
|
+
const sb = statSync(b);
|
|
54
|
+
if (sa.size !== sb.size) return false;
|
|
55
|
+
if (Math.abs(sa.mtimeMs - sb.mtimeMs) > MTIME_TOLERANCE_MS) return false;
|
|
56
|
+
return readFileSync(a).equals(readFileSync(b));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function copyTreeSync(sourceDir, targetDir) {
|
|
60
|
+
mkdirSync(targetDir, { recursive: true });
|
|
61
|
+
for (const entry of readdirSync(sourceDir)) {
|
|
62
|
+
const source = join(sourceDir, entry);
|
|
63
|
+
const target = join(targetDir, entry);
|
|
64
|
+
const st = statSync(source);
|
|
65
|
+
if (st.isDirectory()) copyTreeSync(source, target);
|
|
66
|
+
else {
|
|
67
|
+
copyFileSync(source, target);
|
|
68
|
+
utimesSync(target, st.atime, st.mtime);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 删除 `keep` 之外的 target 文件,再只删因此清空的目录。
|
|
74
|
+
export function pruneExtras(root, keep) {
|
|
75
|
+
const parents = new Set();
|
|
76
|
+
for (const file of filesUnder(root)) {
|
|
77
|
+
if (!keep.has(relative(root, file))) {
|
|
78
|
+
parents.add(dirname(file));
|
|
79
|
+
rmSync(file, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const start of parents) {
|
|
83
|
+
let dir = start;
|
|
84
|
+
while (dir !== undefined && relative(root, dir) !== '') {
|
|
85
|
+
if (existsSync(dir) && readdirSync(dir).length === 0) {
|
|
86
|
+
rmSync(dir, { recursive: true, force: true });
|
|
87
|
+
dir = dirname(dir);
|
|
88
|
+
} else dir = undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// preset 树内容哈希:每文件的相对路径 + 字节喂进 sha1。同步元数据(sidecar 与
|
|
94
|
+
// 任何 `.user-modified-*` 归档)被排除——它是插件记账、非 preset 内容,纳入会使
|
|
95
|
+
// 派生树哈希在 sidecar 被重写(自指)或归档创建时立即漂移。
|
|
96
|
+
export function hashTree(root) {
|
|
97
|
+
const hash = createHash('sha1');
|
|
98
|
+
for (const file of filesUnder(root).sort()) {
|
|
99
|
+
const rel = relative(root, file);
|
|
100
|
+
if (isSyncMetadata(rel)) continue;
|
|
101
|
+
hash.update(rel);
|
|
102
|
+
hash.update('\0');
|
|
103
|
+
hash.update(readFileSync(file));
|
|
104
|
+
hash.update('\0');
|
|
105
|
+
}
|
|
106
|
+
return hash.digest('hex');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 相对 preset 根的路径是否为同步元数据(sidecar 文件,或 `.user-modified-*`
|
|
110
|
+
// 归档目录下的任何内容)。
|
|
111
|
+
export function isSyncMetadata(rel) {
|
|
112
|
+
const head = rel.split(sep)[0];
|
|
113
|
+
return head === SIDECAR_NAME || head.startsWith(ARCHIVE_PREFIX);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function sidecarPath(targetDir) {
|
|
117
|
+
return join(targetDir, SIDECAR_NAME);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 读 sidecar;缺失或畸形(版本错 / 哈希字段缺 / 解析失败)按缺失处理,使下次
|
|
121
|
+
// 同步走保守的首跑路径,而不是信任损坏的记账。
|
|
122
|
+
function readSidecar(targetDir) {
|
|
123
|
+
const path = sidecarPath(targetDir);
|
|
124
|
+
if (!existsSync(path)) return undefined;
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
127
|
+
if (parsed && parsed.version === 1 && typeof parsed.bundledHash === 'string' && typeof parsed.targetHash === 'string') {
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
} catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function writeSidecar(targetDir, bundledHash, targetHash) {
|
|
137
|
+
writeFileSync(sidecarPath(targetDir), `${JSON.stringify({ version: 1, bundledHash, targetHash }, null, 2)}\n`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 复制一个文件系统条目(文件或目录),保留逐条语义与源 mtime——与 copyTreeSync
|
|
141
|
+
// 自身文件分支同形。
|
|
142
|
+
function copyEntry(source, dest) {
|
|
143
|
+
const st = statSync(source);
|
|
144
|
+
if (st.isDirectory()) copyTreeSync(source, dest);
|
|
145
|
+
else {
|
|
146
|
+
copyFileSync(source, dest);
|
|
147
|
+
utimesSync(dest, st.atime, st.mtime);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 覆盖前把当前(用户可见的)preset 树归档到
|
|
152
|
+
// <targetDir>/.user-modified-<timestamp>/。sidecar 与既有归档被排除,归档不会
|
|
153
|
+
// 嵌套。写失败返回 false——调用方必须跳过本次同步、保留用户编辑原样
|
|
154
|
+
// (fail-safe:备份失败绝不允许成为销毁用户内容的许可)。
|
|
155
|
+
function archiveUserTree(targetDir) {
|
|
156
|
+
const archiveDir = join(targetDir, `${ARCHIVE_PREFIX}${Date.now()}`);
|
|
157
|
+
// 先拍下要归档的条目再创建 archiveDir:归档位于 targetDir 之下,mkdirSync 后
|
|
158
|
+
// 再读目录会把归档本身读进来,无限递归进它自己。
|
|
159
|
+
const entries = [];
|
|
160
|
+
for (const entry of readdirSync(targetDir)) {
|
|
161
|
+
if (entry === SIDECAR_NAME || entry.startsWith(ARCHIVE_PREFIX)) continue;
|
|
162
|
+
entries.push(entry);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
copyEntry(join(targetDir, entry), join(archiveDir, entry));
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
try { rmSync(archiveDir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// pruneExtras 的 keep 集:同步后树必须保留的一切——bundled 文件、sidecar、
|
|
177
|
+
// 既有 `.user-modified-*` 归档下的每个文件。keep 里没有归档条目的话,
|
|
178
|
+
// pruneExtras 会在同步当下删掉归档,静默丢弃它本该保留的用户编辑。
|
|
179
|
+
function computeKeep(sourceDir, targetDir) {
|
|
180
|
+
const keep = new Set(filesUnder(sourceDir).map((f) => relative(sourceDir, f)));
|
|
181
|
+
keep.add(SIDECAR_NAME);
|
|
182
|
+
if (existsSync(targetDir)) {
|
|
183
|
+
for (const entry of readdirSync(targetDir)) {
|
|
184
|
+
if (!entry.startsWith(ARCHIVE_PREFIX)) continue;
|
|
185
|
+
const dir = join(targetDir, entry);
|
|
186
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
187
|
+
for (const f of filesUnder(dir)) keep.add(relative(targetDir, f));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return keep;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 同步一个 preset 目录。返回 'synced'(写入或升级)、'current'(未变)或
|
|
194
|
+
// 'user-modified'(target 偏离上次同步快照而跳过——绝不覆盖用户编辑)。判定表:
|
|
195
|
+
// - target 不存在 → 复制 + 写 sidecar → 'synced'
|
|
196
|
+
// - 无 sidecar、target 存在 → 一次性归档 + 同步 → 'synced'
|
|
197
|
+
// - bundled 未变、target 相同 → 'current'
|
|
198
|
+
// - bundled 未变、target 漂移 → 'user-modified'(跳过)
|
|
199
|
+
// - bundled 变了 → 漂移则先归档,再同步 → 'synced'
|
|
200
|
+
export function syncOnePreset(sourceDir, targetDir) {
|
|
201
|
+
const bundledHash = hashTree(sourceDir);
|
|
202
|
+
if (existsSync(targetDir) && !statSync(targetDir).isDirectory()) {
|
|
203
|
+
// M8:target 意外是文件(同名冲突)时先改名备份再清,避免直接 rmSync 丢失用户内容。
|
|
204
|
+
try { renameSync(targetDir, `${targetDir}.removed-${Date.now()}`); } catch { rmSync(targetDir, { recursive: true, force: true }); }
|
|
205
|
+
}
|
|
206
|
+
if (!existsSync(targetDir)) {
|
|
207
|
+
copyTreeSync(sourceDir, targetDir);
|
|
208
|
+
writeSidecar(targetDir, bundledHash, hashTree(targetDir));
|
|
209
|
+
return 'synced';
|
|
210
|
+
}
|
|
211
|
+
const sidecar = readSidecar(targetDir);
|
|
212
|
+
// 无 sidecar + 既有 target = 门控前状态:无法判断用户是否编辑过,按「可能编辑
|
|
213
|
+
// 过」处理,在首次门控同步前归档一次(一次性迁移)。
|
|
214
|
+
if (sidecar === undefined) {
|
|
215
|
+
if (!archiveUserTree(targetDir)) return 'user-modified';
|
|
216
|
+
copyTreeSync(sourceDir, targetDir);
|
|
217
|
+
pruneExtras(targetDir, computeKeep(sourceDir, targetDir));
|
|
218
|
+
writeSidecar(targetDir, bundledHash, hashTree(targetDir));
|
|
219
|
+
return 'synced';
|
|
220
|
+
}
|
|
221
|
+
const currentTargetHash = hashTree(targetDir);
|
|
222
|
+
if (sidecar.bundledHash === bundledHash) {
|
|
223
|
+
return sidecar.targetHash === currentTargetHash ? 'current' : 'user-modified';
|
|
224
|
+
}
|
|
225
|
+
// bundled 升级。用户也改过 target(快照漂移)时先归档其树,编辑不会静默丢失。
|
|
226
|
+
if (sidecar.targetHash !== currentTargetHash) {
|
|
227
|
+
if (!archiveUserTree(targetDir)) return 'user-modified';
|
|
228
|
+
}
|
|
229
|
+
copyTreeSync(sourceDir, targetDir);
|
|
230
|
+
pruneExtras(targetDir, computeKeep(sourceDir, targetDir));
|
|
231
|
+
writeSidecar(targetDir, bundledHash, hashTree(targetDir));
|
|
232
|
+
return 'synced';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 把 `presets/` 下每个 preset 目录同步进目标发现根。
|
|
236
|
+
export function syncBundledPresets(targetRoot) {
|
|
237
|
+
const result = { synced: [], current: [], userModified: [], failed: [] };
|
|
238
|
+
const sourceRoot = bundledPresetsRoot();
|
|
239
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
240
|
+
if (existsSync(sourceRoot)) {
|
|
241
|
+
for (const entry of readdirSync(sourceRoot)) {
|
|
242
|
+
const source = join(sourceRoot, entry);
|
|
243
|
+
if (!statSync(source).isDirectory()) continue;
|
|
244
|
+
const id = basename(source);
|
|
245
|
+
try {
|
|
246
|
+
const outcome = syncOnePreset(source, join(targetRoot, id));
|
|
247
|
+
if (outcome === 'synced') result.synced.push(id);
|
|
248
|
+
else if (outcome === 'current') result.current.push(id);
|
|
249
|
+
else result.userModified.push(id);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
result.failed.push({ id, error: error instanceof Error ? error.message : String(error) });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
// `ctx.subagents.registerProvider({...})` 块逐字拆出。仅引用 lib + shims;
|
|
4
4
|
// 无 @deepseek-ai 依赖(shims 是唯一入口)。
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// subagents
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
92
|
-
//
|
|
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:
|
|
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
|
-
// ①
|
|
122
|
-
//
|
|
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:
|
|
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
|
-
// ③
|
|
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
|
|
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,29 @@ 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
|
-
// ⑤
|
|
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(M4:descriptor 非空才 append)。
|
|
155
158
|
let appended = false;
|
|
156
159
|
childCtx.on('agent/pre-step', async ({ agent }, next) => {
|
|
157
160
|
const decision = await next();
|
|
158
161
|
if (!appended && decision.kind === 'enter') {
|
|
159
162
|
appended = true;
|
|
160
|
-
agent.session.append('subagent/descriptor', request.descriptor);
|
|
163
|
+
if (typeof request.descriptor === 'string' && request.descriptor !== '') agent.session.append('subagent/descriptor', request.descriptor);
|
|
161
164
|
}
|
|
162
165
|
return decision;
|
|
163
166
|
});
|
|
164
|
-
// ⑦
|
|
167
|
+
// ⑦ 委派策略追加(官方助手)。
|
|
165
168
|
appendDelegatedPolicyOverrides(childCtx.agent.session, delegated);
|
|
166
169
|
}
|
|
167
170
|
|
|
168
|
-
// ②
|
|
169
|
-
//
|
|
170
|
-
// lib/core/intersection.mjs: computeEffectiveAllow).
|
|
171
|
+
// ② 工具交集(安全门 1):父集 ∩ 子集,减去 run_code、减去 deny,存在 allow
|
|
172
|
+
// 时再收窄(纯核心在 lib/core/intersection.mjs:computeEffectiveAllow)。
|
|
171
173
|
function restrictChildTools(childCtx, parent, profile) {
|
|
172
174
|
const parentNames = new Set(parent.ctx.tools.schemas(parent).map((schema) => schema.name));
|
|
173
175
|
const childNames = childCtx.tools.schemas(childCtx.agent).map((schema) => schema.name);
|
|
@@ -175,14 +177,14 @@ function restrictChildTools(childCtx, parent, profile) {
|
|
|
175
177
|
// shipped restrict 不对 allow:[] throw——在此 fail-loud,让空交集显式化
|
|
176
178
|
// (throw ⇒ setupAndPublish 回滚创建)。
|
|
177
179
|
if (effective.length === 0) {
|
|
178
|
-
throw new Error('dispatch:
|
|
180
|
+
throw new Error('dispatch: 子工具交集为空(父工具集与子工具集无交集,或 toolFilter 过滤后为空,可放宽过滤器后重试)');
|
|
179
181
|
}
|
|
180
|
-
// restrict
|
|
181
|
-
//
|
|
182
|
+
// restrict 对未知/scope-local/保留 allow 集抛错:包成干净错误再 rethrow,
|
|
183
|
+
// 触发创建回滚。
|
|
182
184
|
try {
|
|
183
185
|
childCtx.tools.restrict({ allow: effective });
|
|
184
186
|
} catch (error) {
|
|
185
|
-
throw new Error(`dispatch:
|
|
187
|
+
throw new Error(`dispatch: 子工具限制失败(未能按交集限制子工具集:${error instanceof Error ? error.message : String(error)}),可检查 toolFilter 是否含未知工具`, { cause: error });
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
|
|
@@ -210,7 +212,7 @@ function wireChildLifecycle(handle, request, childId, swapPreset, profile, logge
|
|
|
210
212
|
await child.whenIdle();
|
|
211
213
|
}
|
|
212
214
|
const settled = readResult(child, boundary, flags.cancelled);
|
|
213
|
-
//
|
|
215
|
+
// 决策级日志在结果结算时(readResult,返回前)。
|
|
214
216
|
logger.info('[dsh-subagent-profile] child:', JSON.stringify({ childId, preset: profile.preset ?? 'inherit', swapPreset, stopReason: settled.stopReason }));
|
|
215
217
|
return settled;
|
|
216
218
|
} finally {
|
|
@@ -230,8 +232,8 @@ function wireChildLifecycle(handle, request, childId, swapPreset, profile, logge
|
|
|
230
232
|
};
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
export function createProfileProvider({ subagents, store, getEnabled, logger, catalog }) {
|
|
234
|
-
const deps = { store, getEnabled, logger, catalog };
|
|
235
|
+
export function createProfileProvider({ subagents, store, getEnabled, logger, catalog, getEscapeSet }) {
|
|
236
|
+
const deps = { store, getEnabled, logger, catalog, getEscapeSet };
|
|
235
237
|
return subagents.registerProvider({
|
|
236
238
|
name: 'profile',
|
|
237
239
|
capabilities: { outputSchema: false, depthLimit: true, toolFilter: true, persona: true },
|