dsh-subagent-profile 0.3.3 → 0.4.0

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 (42) hide show
  1. package/README.md +49 -20
  2. package/README.zh.md +102 -73
  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 +153 -75
  7. package/lib/client.js +3081 -444
  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/cost-evidence.mjs +145 -0
  12. package/lib/core/cost-guard.mjs +1 -1
  13. package/lib/core/decision-trace.mjs +11 -31
  14. package/lib/core/delegation.mjs +11 -7
  15. package/lib/core/dispatch-gates.mjs +12 -5
  16. package/lib/core/dispatch-guard.mjs +6 -0
  17. package/lib/core/dispatch-schema.mjs +10 -5
  18. package/lib/core/dispatch-tool.mjs +52 -45
  19. package/lib/core/draft-gates.mjs +45 -0
  20. package/lib/core/drafts-store.mjs +45 -0
  21. package/lib/core/evolution-advice.mjs +323 -0
  22. package/lib/core/evolution-assets.mjs +167 -0
  23. package/lib/core/evolution-draft.mjs +312 -0
  24. package/lib/core/evolution-engine.mjs +269 -0
  25. package/lib/core/evolution-generate.mjs +196 -0
  26. package/lib/core/evolution-ledger.mjs +149 -33
  27. package/lib/core/evolution-persistence.mjs +213 -0
  28. package/lib/core/evolution-renewal.mjs +64 -0
  29. package/lib/core/evolution-routes.mjs +109 -0
  30. package/lib/core/evolution-summary.mjs +46 -223
  31. package/lib/core/http-helpers.mjs +35 -0
  32. package/lib/core/http-routes.mjs +169 -115
  33. package/lib/core/presets-sync.mjs +254 -256
  34. package/lib/core/prices.mjs +46 -0
  35. package/lib/core/profile-directory.mjs +139 -0
  36. package/lib/core/profile-provider.mjs +5 -3
  37. package/lib/core/profiles-store.mjs +24 -6
  38. package/lib/core/reminder-store.mjs +172 -0
  39. package/lib/core/session-read.mjs +17 -0
  40. package/lib/core/shims.mjs +13 -2
  41. package/package.json +82 -82
  42. package/presets/orchestrator/agent.cordis.yml +243 -243
@@ -5,33 +5,31 @@
5
5
  // 注入:所有 apply 闭包 / ctx 依赖都是显式参数——
6
6
  // register ctx.tools.register(工具由 syncTool 注册/注销,设置开关可运行时移除),
7
7
  // store profile store(resolveProfile 取基方案、getAllowFailOpen 供 cost guard),
8
- // getEnabled 读取 apply 闭包 `enabled` 标志(continuable fail-loud 门 +
9
- // syncTool 注册条件),
8
+ // getEnabled 读取 apply 闭包 `enabled` 标志(continuable fail-loud 门 + syncTool 注册条件),
10
9
  // getService 请求时服务 getter(ctx.get('toolResultPruner') / ctx.get('jobs')
11
10
  // 按次读取,绝不在 apply 时读),
12
11
  // logger ctx.logger(决策级派发日志 + continuable 告警),
13
12
  // subagents ctx.subagents(start / startContinuable 驱动子 Agent),
14
- // guard dispatch guard(execute 入口 acquire / 结算 release+记账 /
15
- // syncTool 禁用时 cancelAll+reset)。
16
- // 工厂返回 { syncTool, dispose }:syncTool 交给 HTTP 路由(/set-enabled),
17
- // dispose 在插件卸载时运行。
13
+ // guard dispatch guard(execute 入口 acquire / 结算 release+记账 / syncTool 禁用时 cancelAll+reset)。
14
+ // 工厂返回 { syncTool, dispose }:syncTool 交给 HTTP 路由(/set-enabled),dispose 在插件卸载时运行。
18
15
  //
19
- // execute 按预检段 + 三分支拆为模块级私有函数;parameters / output schema 拆至
20
- // dispatch-schema.mjs,预检闸应用拆至 dispatch-gates.mjs(守文件行门),零漂移。
16
+ // execute 按预检段 + 三分支拆为模块级私有函数;parameters/output schema 拆至 dispatch-schema.mjs,预检闸拆至 dispatch-gates.mjs(守文件行门),零漂移。
21
17
 
22
18
  import { defineTool } from './shims.mjs';
23
- import { textFrom, stopReasonError, withPartialText, pruneBlocks, assertResultSchemaConsistency, trustLabel, trustAudit, dispatchLabel, tierSortKey } from './pure.mjs';
19
+ import { textFrom, stopReasonError, withPartialText, pruneBlocks, assertResultSchemaConsistency, trustLabel, trustAudit, dispatchLabel } from './pure.mjs';
24
20
  import { resolveWhitelist } from './whitelist.mjs';
25
21
  import { recordEscapeAllow } from './escape.mjs';
26
22
  import { settleStart, collectChildUsage, collectChildCalls } from './delegation.mjs';
27
23
  import { DISPATCH_OUTPUT_SCHEMA, DISPATCH_PARAMETERS, DISPATCH_RENDER } from './dispatch-schema.mjs';
28
24
  import { createDecisionTrace, finalizeTrace, assertTraceSize, parentContextOf, requestedOf, effectiveMeta, recordApprovalGate, recordFailure, profileSnapshotOf } from './decision-trace.mjs';
25
+ import { profileDirectoryRows } from './profile-directory.mjs';
29
26
  import { applyWhitelistGate, applyCostGuardGate, applyIntersectionGate, assertContinuableEnabled, readParentToolNames, applyBudgetGate } from './dispatch-gates.mjs';
30
27
  import { trackForegroundInflight, trackBackgroundInflight, trackContinuableInflight } from './dispatch-guard.mjs';
31
28
  import { finishForegroundLedger, finishBackgroundLedger, finishContinuableLedger } from './evolution-ledger.mjs';
29
+ import { profileKeyOf } from './evolution-summary.mjs';
30
+ import { promptExcerptOf } from './adoption-reminder.mjs';
32
31
 
33
- // 信封骨架:envelope:true 时追加进子 persona 的结构化汇报契约(三分支共享);骨架
34
- // 只进 persona(systemPrompt 影子段、模型可见),不进 descriptor(model-hidden,仅 session 记录)。
32
+ // 信封骨架:envelope:true 时追加进子 persona 的结构化汇报契约(三分支共享);只进 persona(模型可见),不进 descriptor(仅 session 记录)。
35
33
  const ENVELOPE_SKELETON = '完成前按以下骨架输出:## 结论(1-2 句)\n## 结构分项(逐项)\n## 关键发现落点(文件路径/数据位置)';
36
34
 
37
35
  // --- execute 预检段(从 execute 拆出;行为逐字不变)-----------------------------
@@ -126,7 +124,7 @@ async function runContinuable(args, merged, meta, parent, exec, deps, trace, eff
126
124
  if (args.run_in_background === true) {
127
125
  deps.logger.warn('[dsh-subagent-profile] dispatch: both continuable and run_in_background are true; continuable takes precedence');
128
126
  }
129
- // 已知降级:continuable 标准路径不支持 preset swap 和 reasoningEffort(subagent 包的 SubagentStartRequest 无 preset 字段、AgentOptions 无 reasoningEffort 字段)
127
+ // 已知降级:continuable 标准路径不支持 preset swap 和 reasoningEffort(宿主请求面无对应字段)。
130
128
  if (merged.preset !== undefined && merged.preset !== 'inherit') {
131
129
  deps.logger.warn(`[dsh-subagent-profile] continuable mode cannot swap preset; ignoring "${merged.preset}" (child inherits the parent preset)`);
132
130
  }
@@ -141,10 +139,10 @@ async function runContinuable(args, merged, meta, parent, exec, deps, trace, eff
141
139
  signal: exec.signal
142
140
  });
143
141
  trackContinuableInflight(deps.guard, deps.subagents, childId, parent.session?.header?.id);
144
- // Continuable 丢弃方案的 preset swap 与 reasoningEffort(子继承父预设),
145
- // 故可观测 meta 必须报告实际生效值,而非被忽略的请求值。
146
- // 可见性修复:`reasoningEffort` 回显**请求值**(经 meta.reasoningEffort),
147
- // `preset:'inherit'` 是真实生效值;`ignored` 明确列出被丢弃项。
142
+ deps.adoptionTracker?.register({ parentSessionId: parent.session?.header?.id, id: childId, ledgerKey: profileKeyOf(base.effective), mode: 'continuable', dispatchedAt: Date.now(), promptExcerpt: promptExcerptOf(args?.prompt), requestedModel: merged.model, parentModel: parent.options?.model });
143
+ // Continuable 丢弃方案的 preset swap 与 reasoningEffort(子继承父预设),故可观测
144
+ // meta 必须报告实际生效值:reasoningEffort 回显请求值,preset:'inherit' 是生效值,
145
+ // ignored 明确列出被丢弃项。
148
146
  // 信任标注不适用:continuable 只回 subagentId、无文本 output(后续经宿主的
149
147
  // send_message 流转),没有可加前缀的回收文本,故此处不加 trustLabel。
150
148
  const out = {
@@ -171,7 +169,7 @@ async function runContinuable(args, merged, meta, parent, exec, deps, trace, eff
171
169
 
172
170
  // 后台 one-shot(job)路径:jobs.start 包 start() 并提供原生
173
171
  // AbortController;仍是一轮即弃,非 continuable。
174
- async function runBackground(args, merged, meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base) {
172
+ async function runBackground(args, merged, meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base, exec) {
175
173
  const jobs = deps.getService('jobs');
176
174
  if (jobs === undefined) {
177
175
  throw new Error('dispatch: 后台派发不可用:缺少 jobs 服务(请安装 @deepseek-ai/dsh-jobs 与 @deepseek-ai/dsh-tool-jobs,或改用前台派发)');
@@ -181,6 +179,9 @@ async function runBackground(args, merged, meta, request, parent, deps, pruneRes
181
179
  const onSettled = (settled) => {
182
180
  guardHandle.finish(jobId, settled.childTotalTokens);
183
181
  finishBackgroundLedger(deps.evoLedger, base, jobId, settled);
182
+ if (deps.backgroundLedger !== undefined) deps.backgroundLedger.record(base.session_id, jobId, settled, exec?.callId);
183
+ deps.adoptionTracker?.updateOutput(base.session_id, jobId, typeof settled.output === 'string' ? settled.output : '');
184
+ deps.adoptionTracker?.updateSettled?.(base.session_id, jobId, settled);
184
185
  };
185
186
  jobId = jobs.start({
186
187
  kind: 'subagent',
@@ -188,10 +189,12 @@ async function runBackground(args, merged, meta, request, parent, deps, pruneRes
188
189
  owner: parent,
189
190
  run: () => {
190
191
  const controller = new AbortController();
192
+ const startPromise = deps.subagents.start('profile', { ...request, signal: controller.signal });
193
+ startPromise.then((run) => { if (run !== null && run !== undefined && typeof run.id === 'string') deps.backgroundLedger?.recordChild?.(base.session_id, jobId, run.id); }).catch(() => {}); // 结算前补记 jobId → childSessionId,供页头识别后台派发。
191
194
  return {
192
195
  cancel: (reason) => controller.abort(reason ?? 'dispatch: background subagent task killed'),
193
196
  done: settleStart(
194
- deps.subagents.start('profile', { ...request, signal: controller.signal }),
197
+ startPromise,
195
198
  controller.signal,
196
199
  meta,
197
200
  pruneResultOutput,
@@ -204,15 +207,15 @@ async function runBackground(args, merged, meta, request, parent, deps, pruneRes
204
207
  }
205
208
  });
206
209
  trackBackgroundInflight(deps.guard, jobs, jobId, parent);
207
- // 后台:execute 返回时 start 尚未 resolve,无 settled(结算经 job 结果携带,不入会话块)。
210
+ deps.adoptionTracker?.register({ parentSessionId: parent.session?.header?.id, id: jobId, ledgerKey: profileKeyOf(base.effective), mode: 'background', dispatchedAt: t0, promptExcerpt: promptExcerptOf(args?.prompt), requestedModel: merged.model, parentModel: parent.options?.model }); // 后台结算经 job 结果携带,不入会话块。
208
211
  const out = { kind: 'background', jobId, ...meta };
209
212
  finalizeTrace(trace, { effective: effectiveMeta(meta, []), execution: { kind: 'background', parentSessionId: parent.session?.header?.id, jobId, mode: 'one-shot' } });
210
213
  out.decisionTrace = assertTraceSize(trace);
211
214
  return out;
212
215
  }
213
216
 
214
- // childTotalTokens:宿主 tokenMeter 启发式估算(surface 口径,非计费 usage)。
215
- // 仅 completed 结算时测量;服务缺失/非函数/抛错均返回 undefined(fail-soft)。
217
+ // childTotalTokens:宿主 tokenMeter 启发式估算(surface 口径,非计费 usage);仅
218
+ // completed 结算时测量,服务缺失/非函数/抛错均返回 undefined(fail-soft)。
216
219
  function measureChildTokens(getService, session) {
217
220
  if (session === undefined || session === null) return undefined;
218
221
  const meter = getService('tokenMeter');
@@ -239,13 +242,13 @@ function measureForegroundRun(deps, run) {
239
242
  }
240
243
 
241
244
  // 前台:收集结果,无论 reject 与否都释放句柄,非 completed 停因 fail-loud;
242
- // run.result reject(undefined)也写失败 outcome(M9)。
243
- async function runForeground(meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base) {
245
+ // run.result reject(undefined)也写失败 outcome。
246
+ async function runForeground(meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base, adoptionBase) {
244
247
  const run = await deps.subagents.start('profile', request);
245
248
  finalizeTrace(trace, { execution: { kind: 'foreground', parentSessionId: parent.session?.header?.id, childSessionId: run.id, mode: 'one-shot' } });
246
249
  trackForegroundInflight(deps.guard, run);
247
- let result;
248
- let measurement;
250
+ deps.adoptionTracker?.register({ parentSessionId: parent.session?.header?.id, id: run.id, ledgerKey: profileKeyOf(base.effective), mode: 'foreground', dispatchedAt: t0, ...adoptionBase }); // 前台派发登记 parent_adopted 待判定。
251
+ let result, measurement;
249
252
  try {
250
253
  result = await run.result;
251
254
  if (result.stopReason === 'completed') measurement = measureForegroundRun(deps, run);
@@ -254,13 +257,13 @@ async function runForeground(meta, request, parent, deps, pruneResultOutput, t0,
254
257
  await run.dispose().catch(() => {});
255
258
  guardHandle.finish(run.id, measurement?.childTotalTokens); // untrack + recordTokens + release
256
259
  }
257
- const childTotalTokens = measurement?.childTotalTokens;
258
- const childUsage = measurement?.childUsage;
259
- const childCalls = measurement?.childCalls;
260
+ const { childTotalTokens, childUsage, childCalls } = measurement ?? {};
260
261
  // 台账:completed 与非 completed 都写 outcome(对照 settleStart 语义),随后非 completed 仍 throw。
261
262
  const pruned = pruneResultOutput(result.output);
262
263
  const elapsedMs = Date.now() - t0;
263
- finishForegroundLedger(deps.evoLedger, base, run.id, result, pruned, elapsedMs, childCalls);
264
+ finishForegroundLedger(deps.evoLedger, base, run.id, result, pruned, elapsedMs, childCalls, childTotalTokens, childUsage);
265
+ deps.adoptionTracker?.updateOutput(parent.session?.header?.id, run.id, textFrom(pruned));
266
+ deps.adoptionTracker?.updateSettled?.(parent.session?.header?.id, run.id, { stopReason: result.stopReason, elapsedMs, ...(childTotalTokens !== undefined ? { childTotalTokens } : {}), ...(childUsage !== undefined ? { childUsage } : {}), childSessionId: run.id }); // 结算摘要回填供未采纳提醒结果行,已拒/未完成同样登记。
264
267
  const failure = stopReasonError(result);
265
268
  if (failure !== undefined) throw new Error(withPartialText(failure, result.output));
266
269
  const metaOut = {
@@ -281,8 +284,7 @@ async function runForeground(meta, request, parent, deps, pruneResultOutput, t0,
281
284
  },
282
285
  });
283
286
  // 信任标注:completed 子结果回灌父上下文前加结构化前缀(profile/preset 元数据、
284
- // 无 prompt 原文),审计同字段 JSON 只进 logger、不进结果字符串。render 行首部
285
- // 格式不变——前缀只进 output 文本(模型看到的正文带头)。
287
+ // 无 prompt 原文),审计同字段 JSON 只进 logger、不进结果字符串;前缀只进 output 文本。
286
288
  const output = `${trustLabel(metaOut)}${textFrom(pruned)}`;
287
289
  deps.logger.info('[dsh-subagent-profile] trusted-output: ' + trustAudit(metaOut));
288
290
  return { output, ...metaOut, decisionTrace: assertTraceSize(trace) };
@@ -292,13 +294,17 @@ async function runForeground(meta, request, parent, deps, pruneResultOutput, t0,
292
294
  // 与轨迹(避免重复 list() / schemas()),并组装 trace 骨架与交集闸 input。
293
295
  async function prepareTrace(args, parent, deps) {
294
296
  const agentPresets = parent.ctx.get('agentPresets');
295
- const whitelist = new Set(await resolveWhitelist(agentPresets, deps.getEscapeSet()));
297
+ const baseWhitelist = new Set(await resolveWhitelist(agentPresets));
298
+ const escapeSet = new Set(deps.getEscapeSet());
299
+ const whitelist = new Set([...baseWhitelist, ...escapeSet]);
296
300
  const parentComposed = agentPresets !== undefined ? agentPresets.composedPreset(parent.ctx) : undefined;
297
301
  const parentToolNames = readParentToolNames(parent);
298
302
  const parentToolCount = parentToolNames.length;
299
- // 决策输入快照:与 dispatch:profiles section 同源同序(enabled、cheap-first)——
300
- // 「模型当时看到哪些候选」是回答「为什么这么选」的记录面(P5 补偿)。
301
- const snapshot = profileSnapshotOf([...deps.store.profiles.values()].filter((p) => p.enabled !== false).sort((a, b) => tierSortKey(a.tokenTier) - tierSortKey(b.tokenTier)));
303
+ // 决策输入快照:与 dispatch:profiles section 同源同序(profileDirectoryRows,
304
+ // enabled、cheap-first)——「模型当时看到哪些候选」是回答「为什么这么选」的记录面。
305
+ const rows = profileDirectoryRows(deps.store);
306
+ const snapshot = profileSnapshotOf(rows, { chosenId: args.profile });
307
+ const advicePresent = typeof deps.getEvolutionAdvice === 'function' && deps.getEvolutionAdvice() === true && parentComposed === 'orchestrator';
302
308
  const trace = createDecisionTrace(
303
309
  parentContextOf({
304
310
  parentPreset: parentComposed,
@@ -307,11 +313,11 @@ async function prepareTrace(args, parent, deps) {
307
313
  parentToolCount,
308
314
  allowFailOpen: deps.store.getAllowFailOpen(),
309
315
  }),
310
- requestedOf(args, { profiles: snapshot.entries })
316
+ requestedOf(args, { profiles: snapshot.entries, advicePresent })
311
317
  );
312
318
  const mode = args.continuable === true ? 'continuable' : (args.run_in_background === true ? 'background' : 'foreground');
313
319
  const intersectionInput = { parentToolCount, requestedToolFilter: args.toolFilter, mode };
314
- return { whitelist, parentComposed, parentToolNames, trace, mode, intersectionInput };
320
+ return { whitelist, baseWhitelist, escapeSet, parentComposed, parentToolNames, trace, mode, intersectionInput };
315
321
  }
316
322
 
317
323
  // execute 主体:预检段 + 三分支分派。任一闸 fail 先记 fail 闸再 rethrow 原错误
@@ -321,7 +327,7 @@ async function runDispatch(args, exec, deps) {
321
327
  if (!parent) throw new Error('dispatch: 缺少调用 Agent(值需来自模型/编排者会话,请勿直接调用本工具)');
322
328
  // 派发耗时基准:execute 入口记 t0,前台/后台各自在结算处算 elapsedMs。
323
329
  const t0 = Date.now();
324
- const { whitelist, parentComposed, parentToolNames, trace, mode, intersectionInput } = await prepareTrace(args, parent, deps);
330
+ const { whitelist, baseWhitelist, escapeSet, parentComposed, parentToolNames, trace, mode, intersectionInput } = await prepareTrace(args, parent, deps);
325
331
  // 预算闸句柄提至 try 外:任何分支 throw(前台 start reject / 后台 jobs 缺失等)
326
332
  // 都在 catch 里幂等 release 并发槽——防错误路径把父会话的并发额度永久耗尽
327
333
  // (正常路径 finish 已 release,catch 再调为 no-op)。
@@ -335,7 +341,7 @@ async function runDispatch(args, exec, deps) {
335
341
  merged.persona = hasPersona ? `${merged.persona}\n\n${ENVELOPE_SKELETON}` : ENVELOPE_SKELETON;
336
342
  }
337
343
  assertContinuableEnabled(deps, args);
338
- applyWhitelistGate(trace, merged, whitelist, parentComposed);
344
+ applyWhitelistGate(trace, merged, whitelist, parentComposed, escapeSet, baseWhitelist);
339
345
  // Cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen 门控)。
340
346
  await applyCostGuardGate(trace, merged, parent, deps);
341
347
  // 交集闸(三分支统一闸序,见 dispatch-gates.mjs);continuable 的 effectiveAllow
@@ -344,6 +350,7 @@ async function runDispatch(args, exec, deps) {
344
350
  recordApprovalGate(trace);
345
351
  const meta = buildMeta(args, merged, parent);
346
352
  const request = buildRequest(args, merged, parent, exec.signal);
353
+ const adoptionBase = { promptExcerpt: promptExcerptOf(args?.prompt), requestedModel: merged.model, parentModel: parent.options?.model };
347
354
  // 结果回收默认剪枝:在 textFrom 前复用宿主 toolResultPruner.pruneContent 预剪;
348
355
  // pruner 缺失时 pruneBlocks 回退为不剪(剪枝是增强、非硬依赖)。
349
356
  const pruneResultOutput = (blocks) => pruneBlocks(blocks, deps.getService('toolResultPruner'));
@@ -355,8 +362,8 @@ async function runDispatch(args, exec, deps) {
355
362
  const base = deps.evoLedger.baseEntry({ parent, args, merged, mode });
356
363
  recordEscapeAllow(deps, parent, merged, trace);
357
364
  if (args.continuable === true) return await runContinuable(args, merged, meta, parent, exec, deps, trace, effectiveAllow, base);
358
- if (args.run_in_background === true) return await runBackground(args, merged, meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base);
359
- return await runForeground(meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base);
365
+ if (args.run_in_background === true) return await runBackground(args, merged, meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base, exec);
366
+ return await runForeground(meta, request, parent, deps, pruneResultOutput, t0, trace, guardHandle, base, adoptionBase);
360
367
  } catch (error) {
361
368
  guardHandle.release();
362
369
  recordFailure(deps.ledger, parent, trace);
@@ -364,16 +371,16 @@ async function runDispatch(args, exec, deps) {
364
371
  }
365
372
  }
366
373
 
367
- export function createDispatchTool({ register, store, getEnabled, getService, logger, subagents, catalog, ledger, guard, evoLedger, getEscapeSet }) {
368
- const deps = { store, getEnabled, getService, logger, subagents, catalog, ledger, guard, evoLedger, getEscapeSet };
374
+ export function createDispatchTool({ register, store, getEnabled, getService, logger, subagents, catalog, ledger, guard, evoLedger, backgroundLedger, adoptionTracker, getEscapeSet, getEvolutionAdvice }) {
375
+ const deps = { store, getEnabled, getService, logger, subagents, catalog, ledger, guard, evoLedger, backgroundLedger, adoptionTracker, getEscapeSet, getEvolutionAdvice };
369
376
  const dispatchTool = defineTool({
370
377
  name: 'dispatch',
371
- description: '派发子任务给派生子 Agent,可逐个覆盖其 preset、model、provider、推理档位、persona、工具白名单、token 预算或递归深度。前台等待结果;run_in_background: true 启动后台任务(单轮即弃);continuable: true 启动持久子 Agent,后续轮次经 send_message 工具延续对话。前瞻:continuable 模式忽略 preset 换用与 reasoningEffort(结果以 ignored 提示)。',
378
+ description: '派发子任务给派生子 Agent,可逐个覆盖其 preset、model、provider、推理档位、persona、工具白名单、token 预算或递归深度。优先使用已保存的 profile,仅少数字段需要临时覆盖时才传 per-call 参数;未指定 model 时子代理继承父模型,查资料/汇总/读文件等轻任务请优先考虑 cheap 方案或显式 model=flash。前台等待结果;run_in_background: true 启动后台任务(单轮即弃);continuable: true 启动持久子 Agent,后续轮次经 send_message 工具延续对话。前瞻:continuable 模式忽略 preset 换用与 reasoningEffort(结果以 ignored 提示)。',
372
379
  parameters: DISPATCH_PARAMETERS,
373
380
  output: {
374
381
  schema: DISPATCH_OUTPUT_SCHEMA,
375
382
  render: DISPATCH_RENDER,
376
- // 决策轨迹经 presentationMeta 投影进会话块 meta(客户端账本数据源),不进 render 行。
383
+ // 决策轨迹经 presentationMeta 投影进会话块 meta(客户端台账数据源),不进 render 行。
377
384
  presentationMeta: (_args, value) => value.decisionTrace,
378
385
  },
379
386
  isConcurrencySafe: () => true,
@@ -0,0 +1,45 @@
1
+ // lib/core/draft-gates.mjs — auto-profile S1 draft 落库前的闸评估。
2
+ // 与派发守卫闸同源(形状/白名单/目录),但不落库、不 throw——只产出结构化 checks
3
+ // 供 /draft/preview 与应用复用。依赖注入:ctx / catalog 由调用方传入;store 只读
4
+ // profiles Map 判断 id 冲突。
5
+
6
+ import { sanitizeProfile } from './pure.mjs';
7
+ import { resolveWhitelist } from './whitelist.mjs';
8
+
9
+ // 评估一个 draft:sanitize(形状/硬上限)→ id 冲突 → 白名单 → 目录。checks 每项
10
+ // 为 { name, verdict: 'pass'|'fail', reason? };ok = 全部 pass。任一闸 fail 时 clean
11
+ // 仍返回(sanitize 归一后的形态),调用方可用其展示「将被写入的生效值」。
12
+ export async function assessDraftProfile({ ctx, store, catalog, getEscapeSet, draft }) {
13
+ if (draft === null || typeof draft !== 'object') {
14
+ return { ok: false, checks: [{ name: 'draft', verdict: 'fail', reason: 'draft 不存在' }], clean: null };
15
+ }
16
+ const config = draft.config !== null && typeof draft.config === 'object' ? draft.config : {};
17
+ const profile = { ...config, name: draft.name, description: draft.description };
18
+ const { clean, warnings } = sanitizeProfile(profile, { strict: true });
19
+ const checks = [];
20
+ checks.push(warnings.length > 0
21
+ ? { name: 'sanitize', verdict: 'fail', reason: warnings.map((w) => w.field + ':' + w.reason).join(';') }
22
+ : { name: 'sanitize', verdict: 'pass' });
23
+ if (typeof clean.id !== 'string' || clean.id === '') {
24
+ checks.push({ name: 'id', verdict: 'fail', reason: 'draft 缺少 profile id' });
25
+ } else if (store.profiles.has(clean.id)) {
26
+ checks.push({ name: 'id', verdict: 'fail', reason: 'profile ' + clean.id + ' 已存在(请先删除或改名)' });
27
+ } else {
28
+ checks.push({ name: 'id', verdict: 'pass' });
29
+ }
30
+ const whitelist = new Set(await resolveWhitelist(ctx.get('agentPresets'), getEscapeSet()));
31
+ if (typeof clean.preset === 'string' && clean.preset !== '' && clean.preset !== 'inherit' && !whitelist.has(clean.preset)) {
32
+ checks.push({ name: 'whitelist', verdict: 'fail', reason: '目标预设 ' + clean.preset + ' 不在 system-trust 白名单(可开启逃生舱并添加该预设后重试)' });
33
+ } else {
34
+ checks.push({ name: 'whitelist', verdict: 'pass' });
35
+ }
36
+ const snapshot = await catalog.getSnapshot();
37
+ if (typeof clean.model === 'string' && clean.model !== '' && !snapshot.models.some((m) => m && m.id === clean.model)) {
38
+ checks.push({ name: 'catalog', verdict: 'fail', reason: '模型 ' + clean.model + ' 不在当前模型目录' });
39
+ } else if (typeof clean.provider === 'string' && clean.provider !== '' && !snapshot.models.some((m) => m && m.provider === clean.provider)) {
40
+ checks.push({ name: 'catalog', verdict: 'fail', reason: '提供方 ' + clean.provider + ' 不在当前模型目录' });
41
+ } else {
42
+ checks.push({ name: 'catalog', verdict: 'pass' });
43
+ }
44
+ return { ok: checks.every((c) => c.verdict === 'pass'), checks, clean };
45
+ }
@@ -0,0 +1,45 @@
1
+ // lib/core/drafts-store.mjs — auto-profile draft 容器。
2
+ // 与生产注册表 subagent-profiles.json 隔离:draft 只写 subagent-evolution/drafts.json,
3
+ // 未经人确认并过闸不得进入 profiles Map。原子写(tmp+rename),损坏 fail-soft 从空。
4
+
5
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ function loadDrafts(file) {
9
+ if (!existsSync(file)) return [];
10
+ try {
11
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
12
+ if (parsed !== null && typeof parsed === 'object' && Array.isArray(parsed.drafts)) {
13
+ return parsed.drafts.filter((d) => d !== null && typeof d === 'object' && typeof d.id === 'string');
14
+ }
15
+ } catch { /* fail-soft 从空 */ }
16
+ return [];
17
+ }
18
+
19
+ function createDraftsStore({ dshHome, onGovernanceFailure = () => {} } = {}) {
20
+ const dir = join(dshHome, 'subagent-evolution');
21
+ const file = join(dir, 'drafts.json');
22
+ const drafts = loadDrafts(file);
23
+ const persist = () => {
24
+ const tmp = file + '.tmp';
25
+ try {
26
+ mkdirSync(dir, { recursive: true });
27
+ writeFileSync(tmp, JSON.stringify({ v: 1, drafts }, null, 2), 'utf8');
28
+ renameSync(tmp, file);
29
+ return { persisted: true };
30
+ } catch (error) {
31
+ try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
32
+ onGovernanceFailure();
33
+ return { persisted: false, error: error instanceof Error ? error.message : String(error) };
34
+ }
35
+ };
36
+ return {
37
+ list: () => drafts.map((d) => ({ ...d })),
38
+ get: (id) => { const d = drafts.find((x) => x.id === id); return d === undefined ? undefined : { ...d }; },
39
+ add: (draft) => { drafts.push(draft); return persist(); },
40
+ remove: (id) => { const i = drafts.findIndex((x) => x.id === id); if (i < 0) return { persisted: true, removed: false }; drafts.splice(i, 1); return { ...persist(), removed: true }; },
41
+ clear: () => { drafts.length = 0; },
42
+ };
43
+ }
44
+
45
+ export { createDraftsStore };