dsh-subagent-profile 0.3.1 → 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/index.mjs CHANGED
@@ -2,51 +2,114 @@
2
2
  // 源自原型动态插件 `code.host` 主体;宿主导入面(registerTool/defineTool/
3
3
  // handle)收敛在 lib/core/shims.mjs,装配块按模块拆分驻留 lib/core/(catalog /
4
4
  // presets-sync / profiles-store / cost-guard / whitelist / intersection /
5
- // delegation / pure / shims / http-routes / profile-provider / dispatch-tool)。
5
+ // delegation / pure / shims / http-routes / profile-provider / dispatch-tool /
6
+ // dispatch-guard)。
6
7
  // apply 的装配辅助函数(syncBundledPresetsToHome / provideProfileService /
7
8
  // registerSystemPromptSections / registerSettingsRoutes)保持 section 文本与
8
9
  // 门控逐字不变;`enabled` 始终经 getter 注入,门控读取当前值。
9
10
 
11
+ import { readdirSync, renameSync, rmSync } from 'node:fs';
12
+ import { createRequire } from 'node:module';
10
13
  import { join } from 'node:path';
11
14
  import { syncBundledPresets } from './lib/core/presets-sync.mjs';
12
15
  import { dshHome, createProfileStore } from './lib/core/profiles-store.mjs';
13
16
  import { createHttpRoutes } from './lib/core/http-routes.mjs';
14
17
  import { createProfileProvider } from './lib/core/profile-provider.mjs';
15
18
  import { createDispatchTool } from './lib/core/dispatch-tool.mjs';
19
+ import { createDispatchGuard } from './lib/core/dispatch-guard.mjs';
16
20
  import { createCatalogCache } from './lib/core/catalog-cache.mjs';
21
+ import { createFailureLedger } from './lib/core/decision-trace.mjs';
22
+ import { createEvolutionLedger } from './lib/core/evolution-ledger.mjs';
23
+ import { createEscapeStore, recordEscapeAllowProvider } from './lib/core/escape.mjs';
17
24
  import { tierSortKey } from './lib/core/pure.mjs';
25
+ import { resolveWhitelist, FALLBACK_WHITELIST } from './lib/core/whitelist.mjs';
26
+ import { buildAdviceText, refreshSummaries } from './lib/core/evolution-summary.mjs';
18
27
 
19
28
  export const name = 'dsh-subagent-profile';
20
29
  export const inject = ['subagents', 'tools', 'agents'];
21
30
 
22
- // Self-install the bundled "orchestrator" agent preset into the DSH
23
- // agent-presets root so the mode appears in the new-session picker without
24
- // manual copying (mirrors the shipped dsh-liangshen self-install). Idempotent:
25
- // byte-identical trees are skipped; a bundle change rewrites the preset — the
26
- // intended upgrade path. Fail-soft: the dispatch tool and settings page keep
27
- // working even if the write is denied.
31
+ // 自装 bundled "orchestrator" agent 预设到 DSH agent-presets 根,让该模式
32
+ // 无需手动复制就出现在新建会话选择器里(与官方 dsh-liangshen 自安装相同)。
33
+ // 幂等:字节一致的树跳过;bundle 变更重写预设——即预期的升级路径。fail-soft:
34
+ // 写入被拒时 dispatch 工具与设置页照常工作。
28
35
  function syncBundledPresetsToHome(ctx) {
29
36
  try {
30
37
  const presetRoot = join(dshHome(), '.agent-presets');
31
38
  const sync = syncBundledPresets(presetRoot);
32
39
  for (const { id, error } of sync.failed) ctx.logger.warn(`[dsh-subagent-profile] preset ${id} sync failed: ${error}`);
33
40
  if (sync.synced.length > 0) ctx.logger.info(`[dsh-subagent-profile] presets synced into ${presetRoot}: ${sync.synced.join(', ')}`);
41
+ if (sync.userModified.length > 0) ctx.logger.warn(`[dsh-subagent-profile] presets left untouched (user-modified): ${sync.userModified.join(', ')}`);
34
42
  } catch (error) {
35
43
  ctx.logger.warn('[dsh-subagent-profile] preset sync failed:', error instanceof Error ? error.message : String(error));
36
44
  }
37
45
  }
38
46
 
39
- // subagent-profiles service over the store's per-apply profiles Map —
40
- // lets the outside world enumerate and extend the registry without touching
41
- // internals.
47
+ // 卸载清理:把本插件持久化的 3 个数据文件 + 自装的 orchestrator 预设目录改名备份
48
+ // (只动本插件拥有的 orchestrator 目录,不碰 .agent-presets 下其它插件/用户的目录)。
49
+ // 改为「改名到 <name>.removed-<ts>」而非 rmSync 删除:ctx.effect 的清理器在禁用/热
50
+ // 重载/进程退出时都可能触发,硬删除会静默丢失用户 profiles/state/失败台账(S1)。
51
+ // 备份保留可恢复副本;规范路径被清空,重装/重启后经 syncBundledPresetsToHome 重新
52
+ // 同步预设、数据文件按需重新生成。fail-soft:改名失败不抛(卸载不应因清理失败而阻断)。
53
+ function removeOwnedData(home) {
54
+ const stamp = Date.now();
55
+ for (const file of ['subagent-profiles.json', 'subagent-profiles.state.json', 'subagent-profiles.failed-traces.json']) {
56
+ try { renameSync(join(home, file), join(home, `${file}.removed-${stamp}`)); } catch { /* 源不存在(正常首启)等:best effort */ }
57
+ }
58
+ try { renameSync(join(home, '.agent-presets', 'orchestrator'), join(home, '.agent-presets', `orchestrator.removed-${stamp}`)); } catch { /* best effort */ }
59
+ }
60
+
61
+ // 启动清理 S1 改名备份残留:.removed-<ts> 备份在下次正常启动后已无保留价值(数据
62
+ // 文件按需重新生成),启动时清掉防积累(热重载/禁用反复触发 effect 会持续产生新
63
+ // 备份)。只清本插件前缀(subagent-profiles.* 与 orchestrator.removed-*),不碰其它
64
+ // 文件;卸载后用户手动改回原名的场景不受影响(改回后无 .removed 残留)。fail-soft。
65
+ function cleanRemovedBackups(home) {
66
+ for (const dir of [home, join(home, '.agent-presets')]) {
67
+ let names;
68
+ try { names = readdirSync(dir); } catch { continue; }
69
+ for (const name of names) {
70
+ try {
71
+ if (name.startsWith('subagent-profiles.') && name.includes('.removed-')) {
72
+ rmSync(join(dir, name), { force: true });
73
+ } else if (name.startsWith('orchestrator.removed-')) {
74
+ rmSync(join(dir, name), { recursive: true, force: true });
75
+ }
76
+ } catch { /* best effort */ }
77
+ }
78
+ }
79
+ }
80
+
81
+ // 卸载接线:注销 dispatch 工具(若仍注册)+ 清空失败台账 + 级联取消在途派发 +
82
+ // 清空守卫记账 + 删本插件数据文件与自装预设目录。另挂钩宿主 agent/disposed
83
+ // (Agent 注册 fiber 卸载时发射):父会话逻辑结束时按父释放并发/token 记账,
84
+ // 防配额随会话累积驻留。
85
+ function registerTeardown(ctx, dispatch, ledger, guard, home) {
86
+ ctx.effect(() => dispatch.dispose);
87
+ ctx.effect(() => ledger.clear);
88
+ ctx.effect(() => () => {
89
+ guard.cancelAll();
90
+ guard.reset();
91
+ removeOwnedData(home);
92
+ });
93
+ const onAgentDisposed = ({ agent }) => {
94
+ const parentSessionId = agent?.session?.header?.id;
95
+ if (parentSessionId !== undefined && parentSessionId !== null && parentSessionId !== '') {
96
+ guard.resetParent(parentSessionId);
97
+ }
98
+ };
99
+ ctx.on('agent/disposed', onAgentDisposed);
100
+ ctx.effect(() => () => ctx.off('agent/disposed', onAgentDisposed));
101
+ }
102
+
103
+ // subagent-profiles service:包在 store 的 per-apply profiles Map 之上,对外只暴露
104
+ // 枚举/扩展注册表的窄接口,不触碰内部结构。
42
105
  function provideProfileService(ctx, store) {
43
106
  ctx.provide('subagent-profiles', {
44
107
  register(profile) {
45
108
  if (!profile || typeof profile.id !== 'string' || profile.id.length === 0) {
46
- throw new Error('subagent-profiles: profile id must be a non-empty string');
109
+ throw new Error('subagent-profiles: profile id 必须为非空字符串');
47
110
  }
48
111
  if (store.profiles.has(profile.id)) {
49
- throw new Error(`subagent-profiles: profile "${profile.id}" is already registered`);
112
+ throw new Error(`subagent-profiles: profile "${profile.id}" 已注册(id 需唯一)`);
50
113
  }
51
114
  const registered = { ...profile };
52
115
  store.profiles.set(profile.id, registered);
@@ -106,18 +169,28 @@ function profileSectionText(store, gate, context) {
106
169
  return `- ${p.id}: ${desc}${p.preset !== undefined ? ` (preset: ${p.preset})` : ''}`;
107
170
  });
108
171
  if (rows.length === 0) return '';
109
- const note = '- 别把 1-2 步即可自查/可搜完的小事委派出去 —— 几分钟内能自查完的直接做。';
172
+ const note = '- 选择方案时先匹配任务复杂度与方案描述的能力边界,成本只在能力都胜任的方案之间比较——复杂任务不得为省 token 改用能力不足的便宜方案。\n- 别把 1-2 步即可自查/可搜完的小事委派出去 —— 几分钟内能自查完的直接做。';
110
173
  return `Available dispatch profiles (dispatch.profile):\n${rows.join('\n')}\n${note}`;
111
174
  }
112
175
 
113
176
  // orchestrator:mode section 文本(逐字保留;与 profiles 同门控)。
114
177
  const ORCHESTRATOR_MODE_TEXT = '本机已安装 dsh-subagent-profile 插件的「编排者模式」agent preset:新建会话的预设选择器中可选「编排者模式」。该模式把 Agent 定位为主协调者——拆解任务后按场景用 dispatch(内置 swap-standard=标准编码、researcher=调研检索,可在「子 Agent 方案」设置页自定义)与 subagent/subagent_fork/workflow 委派给子 Agent,再整合结果。preset 文件由插件维护于 ~/.dsh/.agent-presets,安装/升级时自动同步;用户提到「编排者模式 / orchestrator / 主协调模式」时即指本预设,请据此协作。';
115
178
 
116
- // Directory section rendering the available profiles (systemPrompt's
117
- // section `text` accepts a function, as the shipped tool-subagent proves).
118
- // gate: `enabled` must be read live (getter), so /set-enabled toggles
119
- // apply immediately without a restart.
120
- function registerSystemPromptSections(ctx, store, getEnabled) {
179
+ // 建议注入候选池:system-trust 白名单(agentPresets 可选;解析失败回退内置
180
+ // 回退名单——fail-loud 检查在建议生成路径进行,不在此处)。刻意不叠加逃生舱放行
181
+ // 集:只读建议不享受逃生舱(逃生舱仅放行「其余三道闸全过」的显式派发,不扩建议池)。
182
+ async function resolveAdviceWhitelist(ctx) {
183
+ try {
184
+ return new Set(await resolveWhitelist(ctx.get('agentPresets')));
185
+ } catch {
186
+ return new Set(FALLBACK_WHITELIST);
187
+ }
188
+ }
189
+
190
+ // 目录 section:渲染可用方案(systemPrompt 的 section text 接受函数,官方
191
+ // tool-subagent 即此用法)。门控注意:`enabled` 必须经 getter 实时读取,
192
+ // 使 /set-enabled 切换立即生效、无需重启。
193
+ function registerSystemPromptSections(ctx, store, getEnabled, getEvolutionAdvice, adviceEnv) {
121
194
  const pluginSystemPrompt = ctx.get('systemPrompt');
122
195
  if (pluginSystemPrompt === undefined) return;
123
196
  const gate = (context) => sectionGatePasses(ctx, getEnabled, context);
@@ -126,25 +199,38 @@ function registerSystemPromptSections(ctx, store, getEnabled) {
126
199
  order: 116.5,
127
200
  text: (context) => profileSectionText(store, gate, context),
128
201
  });
129
- // Announce the self-installed orchestrator preset so the current agent
130
- // knows the mode exists and can point the user to it. Gated the same
131
- // way — only a dispatch-capable agent sees it.
202
+ // 宣告自装的 orchestrator 预设,让当前 agent 知道该模式存在、可引导用户使用。
203
+ // 门控与 profiles 段相同——只有可派发的 agent 才看得到。
132
204
  pluginSystemPrompt.section({
133
205
  name: 'orchestrator:mode',
134
206
  order: 117,
135
207
  text: (context) => (gate(context) ? ORCHESTRATOR_MODE_TEXT : ''),
136
208
  });
209
+ // 只读建议段:门控 = orchestrator(sectionGatePasses)+ evolutionAdvice 开关。
210
+ // 只进父 Agent(复用同一门控)、默认关、只含确定性聚合数字与建议文案,不含派生原文。
211
+ // 生成异常 try/catch 兜底:fail-loud 检查(非 system 候选)落 warn 日志但绝不
212
+ // 阻断每次提示装配——损坏/手改的 summaries 不得让 orchestrator 会话无法启动。
213
+ pluginSystemPrompt.section({
214
+ name: 'evolution:advice',
215
+ order: 116.8,
216
+ text: (context) => {
217
+ if (!getEvolutionAdvice() || !gate(context)) return '';
218
+ try {
219
+ return buildAdviceText(adviceEnv);
220
+ } catch (error) {
221
+ adviceEnv.logger.warn(`[dsh-subagent-profile] evolution:advice 生成失败,本次不注入:${error instanceof Error ? error.message : String(error)}`);
222
+ return '';
223
+ }
224
+ },
225
+ });
137
226
  }
138
227
 
139
- // HTTP loopback routes for the Client settings UI (webServer.register ↔
140
- // client fetch; JSON only) — the routes themselves live in
141
- // lib/core/http-routes.mjs (imported above); this wiring only injects the
142
- // per-apply deps. webServer is optional — a headless deployment keeps the
143
- // dispatch tool and drops only the settings page. webServer's activation
144
- // (listen) is async and may not be ready when this plugin's inject deps
145
- // resolve, so register inside an inject sub-scope that waits for it
146
- // (ctx.get would read undefined at apply time).
147
- function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, catalog) {
228
+ // Client 设置 UI HTTP loopback 路由(webServer.register ↔ client fetch,纯 JSON)。
229
+ // 路由本体在 lib/core/http-routes.mjs(上文已 import);这里只注入 per-apply 依赖。
230
+ // webServer 可选——无头部署保留 dispatch 工具、只丢设置页。webServer 的激活
231
+ // (listen)是异步的,可能晚于本插件 inject 依赖解析完成,故在等它的 inject
232
+ // scope 内注册(apply ctx.get 会读到 undefined)。
233
+ function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, catalog, ledger, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice) {
148
234
  ctx.inject(['webServer'], (scope) => {
149
235
  scope.effect(createHttpRoutes({
150
236
  webServer: scope.webServer,
@@ -153,6 +239,14 @@ function registerSettingsRoutes(ctx, store, getEnabled, setEnabled, syncTool, ca
153
239
  setEnabled,
154
240
  syncTool,
155
241
  catalog,
242
+ ledger,
243
+ getAudit,
244
+ getEvolutionAdvice,
245
+ setEvolutionAdvice,
246
+ getEscapeEnabled,
247
+ setEscapeEnabled,
248
+ escape,
249
+ refreshAdvice,
156
250
  logger: ctx.logger,
157
251
  }), 'dsh-subagent-profile: settings routes');
158
252
  });
@@ -170,48 +264,80 @@ function createSharedCatalog(ctx) {
170
264
  });
171
265
  }
172
266
 
173
- export async function apply(ctx) {
174
- // Enable/disable switch (default on, runtime-toggled by the settings
175
- // page, persisted across restarts) + profile registry — lib/profiles-store
176
- // .mjs: createProfileStore. loadProfiles runs once at startup via the
177
- // explicit call below (the factory itself does not auto-load).
178
- const store = createProfileStore({ dshHome: dshHome(), logger: ctx.logger });
179
- let enabled = store.loadEnabled();
180
- store.loadProfiles();
181
- // Self-install the bundled "orchestrator" preset (idempotent, fail-soft).
182
- syncBundledPresetsToHome(ctx);
183
- // 进程级共享 catalog 快照(/options 三路由 + dispatch cost guard 共用)。
184
- const catalog = createSharedCatalog(ctx);
185
- // `dispatch` tool — lib/core/dispatch-tool.mjs: defineTool block (schema +
186
- // execute), the result-schema consistency lock and the syncTool
187
- // register/unregister logic.
188
- // Created BEFORE the HTTP inject so createHttpRoutes can capture
189
- // dispatch.syncTool (/set-enabled).
190
- const dispatch = createDispatchTool({
267
+ // 读本插件 package.json version 供派发台账 provenance 使用。fail-soft:读取/解析
268
+ // 失败回退 'unknown',绝不阻断插件启动。
269
+ function readPluginVersion() {
270
+ try {
271
+ const pkg = createRequire(import.meta.url)('./package.json');
272
+ return typeof pkg?.version === 'string' && pkg.version !== '' ? pkg.version : 'unknown';
273
+ } catch {
274
+ return 'unknown';
275
+ }
276
+ }
277
+
278
+ // 逃生舱装配:放行集 store + 开关态(state.json 持久化)+ 访问器。开关态经对象字段
279
+ // 可变(/set-escape 改写),getEscapeSet 开关关时恒空数组(零叠加、零放行审计)。
280
+ function setupEscape(ctx, store, evoLedger, home) {
281
+ const escape = createEscapeStore({ dshHome: home, logger: ctx.logger, onGovernanceFailure: () => evoLedger.markGovernanceFailure() });
282
+ const state = { escapeEnabled: store.loadEscapeEnabled() };
283
+ return {
284
+ escape,
285
+ getEscapeSet: () => (state.escapeEnabled ? escape.list() : []),
286
+ getEscapeEnabled: () => state.escapeEnabled,
287
+ setEscapeEnabled: (next) => { state.escapeEnabled = next; },
288
+ recordEscapeAllowProvider: (parent, preset) => recordEscapeAllowProvider(evoLedger, parent, preset),
289
+ };
290
+ }
291
+
292
+ // `dispatch` 工具装配(defineTool + execute + syncTool)。须在 HTTP inject 之前
293
+ // 构造,使 createHttpRoutes 能拿 dispatch.syncTool 供 /set-enabled 注册/注销。
294
+ function createDispatch(ctx, store, catalog, ledger, guard, evoLedger, getEscapeSet, getEnabled) {
295
+ return createDispatchTool({
191
296
  register: (tool) => ctx.tools.register(tool),
192
297
  store,
193
- getEnabled: () => enabled,
298
+ getEnabled,
194
299
  getService: (name) => ctx.get(name),
195
300
  logger: ctx.logger,
196
301
  subagents: ctx.subagents,
197
302
  catalog,
303
+ ledger,
304
+ guard,
305
+ evoLedger,
306
+ getEscapeSet,
198
307
  });
199
- // subagent-profiles service over the store's per-apply profiles Map.
308
+ }
309
+
310
+ export async function apply(ctx) {
311
+ const home = dshHome();
312
+ // 启动清理:上个生命周期留下的 .removed-* 备份残留(S1 改名的副产物)。
313
+ cleanRemovedBackups(home);
314
+ // 派发台账 + 审计分级须在 store 之前构造:store 的治理审计钩子指向其 markGovernanceFailure。
315
+ const evoLedger = createEvolutionLedger({ dshHome: home, pluginVersion: readPluginVersion(), warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`) });
316
+ const store = createProfileStore({ dshHome: home, logger: ctx.logger, onGovernanceFailure: () => evoLedger.markGovernanceFailure() });
317
+ const escapeCtl = setupEscape(ctx, store, evoLedger, home);
318
+ let enabled = store.loadEnabled();
319
+ let evolutionAdvice = store.loadEvolutionAdvice();
320
+ store.loadProfiles();
321
+ // 自装 bundled 的 orchestrator 预设(幂等、fail-soft)。
322
+ syncBundledPresetsToHome(ctx);
323
+ const catalog = createSharedCatalog(ctx);
324
+ const ledger = createFailureLedger({ warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`), stateFile: join(home, 'subagent-profiles.failed-traces.json') });
325
+ const guard = createDispatchGuard({ warn: (message) => ctx.logger.warn(`[dsh-subagent-profile] ${message}`) });
326
+ const dispatch = createDispatch(ctx, store, catalog, ledger, guard, evoLedger, escapeCtl.getEscapeSet, () => enabled);
200
327
  provideProfileService(ctx, store);
201
- // Gated system-prompt sections (profile directory + orchestrator mode).
202
- registerSystemPromptSections(ctx, store, () => enabled);
203
- // `profile` subagent provider — lib/core/profile-provider.mjs:
204
- // createProfileProvider registers the provider and returns the disposer.
205
- const disposeProvider = createProfileProvider({
206
- subagents: ctx.subagents,
207
- store,
208
- getEnabled: () => enabled,
328
+ const adviceWhitelist = await resolveAdviceWhitelist(ctx);
329
+ const adviceEnv = { summariesFile: join(home, 'subagent-evolution', 'summaries.json'), dispatchFile: join(home, 'subagent-evolution', 'dispatch.jsonl'), whitelist: adviceWhitelist, logger: ctx.logger };
330
+ // 生产聚合触发点(T1 修复):/options/refresh 手动刷新时重算 summaries.json,让
331
+ // evolution:advice 有初始生成路径(原先 computeSummaries/writeSummaries 只被测试调用)。
332
+ const refreshAdvice = () => refreshSummaries({
333
+ dispatchFile: join(home, 'subagent-evolution', 'dispatch.jsonl'),
334
+ summariesFile: join(home, 'subagent-evolution', 'summaries.json'),
209
335
  logger: ctx.logger,
210
- catalog,
211
336
  });
337
+ registerSystemPromptSections(ctx, store, () => enabled, () => evolutionAdvice, adviceEnv);
338
+ const disposeProvider = createProfileProvider({ subagents: ctx.subagents, store, getEnabled: () => enabled, logger: ctx.logger, catalog, getEscapeSet: escapeCtl.getEscapeSet, recordEscapeAllowProvider: escapeCtl.recordEscapeAllowProvider });
212
339
  if (typeof disposeProvider === 'function') ctx.effect(() => disposeProvider);
213
- // HTTP loopback routes for the Client settings UI lib/core/http-routes.mjs.
214
- registerSettingsRoutes(ctx, store, () => enabled, (next) => { enabled = next; }, dispatch.syncTool, catalog);
215
- // Teardown: unregister the dispatch tool (if still registered).
216
- ctx.effect(() => dispatch.dispose);
340
+ // Client 设置 UI HTTP loopback 路由 —— lib/core/http-routes.mjs
341
+ registerSettingsRoutes(ctx, store, () => enabled, (next) => { enabled = next; }, dispatch.syncTool, catalog, ledger, () => evoLedger.auditState(), () => evolutionAdvice, (next) => { evolutionAdvice = next; }, escapeCtl.getEscapeEnabled, escapeCtl.setEscapeEnabled, escapeCtl.escape, refreshAdvice);
342
+ registerTeardown(ctx, dispatch, ledger, guard, home);
217
343
  }