dsh-subagent-profile 0.3.4 → 0.5.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 (38) hide show
  1. package/README.md +13 -8
  2. package/README.zh.md +13 -8
  3. package/index.mjs +99 -95
  4. package/lib/client.js +1288 -189
  5. package/lib/core/decision-trace.mjs +20 -1
  6. package/lib/core/delegation.mjs +3 -2
  7. package/lib/core/dispatch-gates.mjs +122 -9
  8. package/lib/core/dispatch-schema.mjs +45 -7
  9. package/lib/core/dispatch-tool.mjs +24 -25
  10. package/lib/core/evolution-advice.mjs +154 -34
  11. package/lib/core/evolution-assets.mjs +167 -0
  12. package/lib/core/evolution-canary.mjs +427 -0
  13. package/lib/core/evolution-draft.mjs +333 -0
  14. package/lib/core/evolution-engine.mjs +413 -0
  15. package/lib/core/evolution-generate.mjs +180 -0
  16. package/lib/core/evolution-lag.mjs +78 -0
  17. package/lib/core/evolution-ledger.mjs +135 -27
  18. package/lib/core/evolution-persistence.mjs +213 -0
  19. package/lib/core/evolution-renewal.mjs +64 -0
  20. package/lib/core/evolution-routes.mjs +234 -0
  21. package/lib/core/http-helpers.mjs +35 -0
  22. package/lib/core/http-routes.mjs +35 -39
  23. package/lib/core/model-policy.mjs +77 -0
  24. package/lib/core/presets-sync.mjs +24 -2
  25. package/lib/core/profile-directory.mjs +17 -0
  26. package/lib/core/profile-provider.mjs +9 -3
  27. package/lib/core/profiles-query.mjs +178 -0
  28. package/lib/core/profiles-store.mjs +56 -8
  29. package/lib/core/pure.mjs +1 -1
  30. package/lib/core/session-read.mjs +17 -0
  31. package/lib/core/shims.mjs +2 -1
  32. package/package.json +2 -2
  33. package/presets/orchestrator-v2/NOTICE +7 -0
  34. package/presets/{orchestrator → orchestrator-v2}/agent.cordis.yml +78 -7
  35. package/presets/orchestrator-v2/custom-bash.mjs +213 -0
  36. package/presets/orchestrator-v2/preset.yml +2 -0
  37. package/presets/orchestrator-v2/tool-bootstrap.mjs +620 -0
  38. package/presets/orchestrator/preset.yml +0 -2
@@ -0,0 +1,333 @@
1
+ // lib/core/evolution-draft.mjs — 进化建议 → 可应用候选资产的确定性生成器。
2
+ // 纯函数 + 数据注入(catalog 快照 / 白名单 / 已占用 id 集),零 LLM、零 fs、零新依赖:
3
+ // 同输入恒同输出,可裸 import 单测;只产出候选 config,落盘/审计由装配层负责。
4
+ // 推导规则(确定性,一条建议 → ≤3 条互斥候选,保守 → 更激进排序):
5
+ // 降(能力轴只降):单步 = 模型更低档 / 移除 persona·toolFilter 结构段 / 白名单内
6
+ // 相邻低档预设;组合 = 单步并列补充(如模型低档 + 移除工具过滤)。
7
+ // 升(预算轴只升):推理强度升一级(阶梯与派发档位同源)、maxTokens ×2 封顶
8
+ // 硬上限,可组合。跨方向禁止(全降或全升);全无候选返回空表。平 → 空表。
9
+ // 隐私:不引入 prompt/persona 原文;config 只含字段与值,basis 只含摘要文本。
10
+
11
+ import { createHash } from 'node:crypto';
12
+ import { MAX_TOKENS, sanitizeProfile } from './pure.mjs';
13
+ import { profileKeyOf } from './evolution-summary.mjs';
14
+ import { avgCostFor } from './prices.mjs';
15
+
16
+ // 推理档位阶梯:升档「升一级」的口径与派发工具档位同源;未知档位不升(保守)。
17
+ export const EFFORT_LADDER = ['off', 'low', 'medium', 'high', 'max'];
18
+
19
+ const ID_MAX_LEN = 32;
20
+ const ID_RETRY_MAX = 5;
21
+
22
+ // profileKey 轴段解析(evolution-summary 的 L1 身份键口径):
23
+ // preset:<名> / provider:<名> / model:<名> / persona:1 / toolFilter:1。
24
+ function parseProfileKey(profileKey) {
25
+ const parsed = { preset: null, provider: null, model: null, persona: false, toolFilter: false };
26
+ const text = typeof profileKey === 'string' ? profileKey : '';
27
+ for (const part of text.split('|')) {
28
+ if (part.startsWith('preset:')) parsed.preset = part.slice('preset:'.length);
29
+ else if (part.startsWith('provider:')) parsed.provider = part.slice('provider:'.length);
30
+ else if (part.startsWith('model:')) parsed.model = part.slice('model:'.length);
31
+ else if (part === 'persona:1') parsed.persona = true;
32
+ else if (part === 'toolFilter:1') parsed.toolFilter = true;
33
+ }
34
+ return parsed;
35
+ }
36
+
37
+ // 身份摘要:只保留小写字母/数字/连字符,供候选 id 使用(sanitize 友好)。
38
+ export function lowerHyphen(text) {
39
+ return String(text ?? '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
40
+ }
41
+
42
+ // 候选 id 基名:evo-<方向>-<身份摘要>,总长 ≤32。摘要在长键时截断。
43
+ function baseDraftId(direction, profileKey) {
44
+ const slug = lowerHyphen(profileKey).slice(0, 24) || 'inline';
45
+ return `evo-${direction}-${slug}`.slice(0, ID_MAX_LEN);
46
+ }
47
+
48
+ // id 冲突重试:与已占用 id(profiles + 资产)冲突时追加 -2、-3……(≤5 次),
49
+ // 仍冲突返回 null(装配层放弃生成,不产出会撞名的候选)。基名可由调用方给定
50
+ // (多候选在基名后追加 -c<N> 序号)。
51
+ function nextIdForBase(base, taken) {
52
+ const ids = taken instanceof Set ? taken : new Set(Array.isArray(taken) ? taken : []);
53
+ if (!ids.has(base)) return base;
54
+ for (let i = 2; i <= ID_RETRY_MAX + 1; i += 1) {
55
+ const candidate = `${base}-${i}`.slice(0, ID_MAX_LEN);
56
+ if (!ids.has(candidate)) return candidate;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ export function nextDraftId(direction, profileKey, taken) {
62
+ return nextIdForBase(baseDraftId(direction, profileKey), taken);
63
+ }
64
+
65
+ // 同 provider 单价更低且最接近的模型档。当前模型无单价或同 provider 无更低档时
66
+ // 返回 null(调用方跳过该步,走更保守的降档策略)。
67
+ function modelDowngrade(modelName, providerName, models) {
68
+ if (typeof modelName !== 'string' || modelName === '') return null;
69
+ const currentCost = avgCostFor(modelName);
70
+ if (currentCost === undefined) return null;
71
+ const list = Array.isArray(models) ? models : [];
72
+ let provider = providerName;
73
+ if (provider === null || provider === '') {
74
+ const current = list.find((m) => m && m.id === modelName);
75
+ provider = current !== undefined && typeof current.provider === 'string' ? current.provider : null;
76
+ }
77
+ if (provider === null) return null;
78
+ const candidates = list
79
+ .filter((m) => m && typeof m.id === 'string' && m.id !== modelName && m.provider === provider)
80
+ .map((m) => ({ id: m.id, provider: m.provider, cost: avgCostFor(m.id) }))
81
+ .filter((m) => m.cost !== undefined && m.cost < currentCost)
82
+ .sort((a, b) => b.cost - a.cost);
83
+ const pick = candidates[0];
84
+ return pick === undefined ? null : { provider: pick.provider, model: pick.id };
85
+ }
86
+
87
+ // 同 L1 身份键下的预算轴当前值(从 L2 键反查):maxTokens 取组内最大值,
88
+ // reasoningEffort 取组内阶梯最高档。
89
+ function higherEffort(a, b) {
90
+ const ia = a === null ? -1 : EFFORT_LADDER.indexOf(a);
91
+ const ib = b === null ? -1 : EFFORT_LADDER.indexOf(b);
92
+ return ib > ia ? b : a;
93
+ }
94
+
95
+ function budgetFromL2(profileKey, l2) {
96
+ const source = l2 !== null && typeof l2 === 'object' ? l2 : {};
97
+ let maxTokens = null;
98
+ let effort = null;
99
+ for (const [axisKey] of Object.entries(source)) {
100
+ if (typeof axisKey !== 'string') continue;
101
+ const tokensPrefix = `${profileKey}:maxTokens:`;
102
+ if (axisKey.startsWith(tokensPrefix)) {
103
+ const value = Number(axisKey.slice(tokensPrefix.length));
104
+ if (Number.isFinite(value) && value > 0) maxTokens = maxTokens === null ? value : Math.max(maxTokens, value);
105
+ continue;
106
+ }
107
+ const effortPrefix = `${profileKey}:reasoningEffort:`;
108
+ if (axisKey.startsWith(effortPrefix)) effort = higherEffort(effort, axisKey.slice(effortPrefix.length));
109
+ }
110
+ return { maxTokens, reasoningEffort: effort };
111
+ }
112
+
113
+ // 变更前配置(审计 from 口径):能力轴取值来自身份键,预算轴取值反查 L2。
114
+ export function sourceConfigFromKey(profileKey, l2) {
115
+ const parsed = parseProfileKey(profileKey);
116
+ const budget = budgetFromL2(profileKey, l2);
117
+ const config = {};
118
+ if (parsed.preset !== null) config.preset = parsed.preset;
119
+ if (parsed.provider !== null) config.provider = parsed.provider;
120
+ if (parsed.model !== null) config.model = parsed.model;
121
+ if (parsed.persona) config.persona_present = true;
122
+ if (parsed.toolFilter) config.toolFilter_present = true;
123
+ if (budget.reasoningEffort !== null) config.reasoningEffort = budget.reasoningEffort;
124
+ if (budget.maxTokens !== null) config.maxTokens = budget.maxTokens;
125
+ return config;
126
+ }
127
+
128
+ // 候选名中的改动摘要(人话,零配置原文):模型/预设给新值,结构移除按存在性组词。
129
+ function structureChangeSummary(parsed) {
130
+ if (parsed.persona && parsed.toolFilter) return '去掉自定义人格与工具过滤';
131
+ if (parsed.persona) return '去掉自定义人格';
132
+ return '移除工具过滤';
133
+ }
134
+
135
+ // 保守度标签:按「保守 → 更激进」排序,两端为保守/更激进,三档时中间为均衡。
136
+ function radicalLabel(index, total) {
137
+ if (total <= 1) return '保守';
138
+ if (index === 0) return '保守';
139
+ if (index === total - 1) return '更激进';
140
+ return '均衡';
141
+ }
142
+
143
+ // 白名单内相邻低档预设(名册口径);无可用低档返回 null。
144
+ function downPresetPick(parsed, whitelist, presets) {
145
+ if (parsed.preset === null) return null;
146
+ const roster = Array.isArray(presets) ? presets : [];
147
+ const index = roster.findIndex((p) => p && p.id === parsed.preset);
148
+ for (let i = index - 1; i >= 0; i -= 1) {
149
+ const candidate = roster[i];
150
+ if (candidate === null || candidate === undefined || typeof candidate.id !== 'string' || candidate.id === '') continue;
151
+ const allowed = whitelist instanceof Set ? whitelist.has(candidate.id) : Array.isArray(whitelist) && whitelist.includes(candidate.id);
152
+ if (allowed) return { id: candidate.id };
153
+ }
154
+ return null;
155
+ }
156
+
157
+ // 降档候选清单(能力轴只降,互斥替代):单步(模型 → 结构移除 → 预设)在前,
158
+ // 组合在后(候选 2 可含候选 1 全部改动再加一个);同一建议只产同一方向。
159
+ // 每个候选是完整 config 快照(身份键内其余能力字段保留、被移除的结构段缺席)。
160
+ function downCandidateConfigs(parsed, models, whitelist, presets) {
161
+ const out = [];
162
+ const modelPick = parsed.model !== null ? modelDowngrade(parsed.model, parsed.provider, models) : null;
163
+ if (modelPick !== null) {
164
+ const config = { provider: modelPick.provider, model: modelPick.model };
165
+ if (parsed.preset !== null) config.preset = parsed.preset;
166
+ out.push({ config, summary: `模型改 ${modelPick.model}` });
167
+ }
168
+ if (parsed.persona || parsed.toolFilter) {
169
+ const config = {};
170
+ if (parsed.preset !== null) config.preset = parsed.preset;
171
+ if (parsed.provider !== null) config.provider = parsed.provider;
172
+ if (parsed.model !== null) config.model = parsed.model;
173
+ out.push({ config, summary: structureChangeSummary(parsed) });
174
+ }
175
+ const presetPick = downPresetPick(parsed, whitelist, presets);
176
+ if (presetPick !== null) {
177
+ out.push({ config: { preset: presetPick.id }, summary: `预设改 ${presetPick.id}` });
178
+ }
179
+ if (modelPick !== null && (parsed.persona || parsed.toolFilter)) {
180
+ const config = { provider: modelPick.provider, model: modelPick.model };
181
+ if (parsed.preset !== null) config.preset = parsed.preset;
182
+ out.push({ config, summary: `模型改 ${modelPick.model} + ${structureChangeSummary(parsed)}` });
183
+ }
184
+ if (modelPick !== null && presetPick !== null) {
185
+ out.push({ config: { provider: modelPick.provider, model: modelPick.model, preset: presetPick.id }, summary: `模型改 ${modelPick.model} + 预设改 ${presetPick.id}` });
186
+ }
187
+ return out;
188
+ }
189
+
190
+ // 推理强度下一档(阶梯同派发档位);已在顶档或无值返回 null。
191
+ function nextEffortOf(effort) {
192
+ if (typeof effort !== 'string' || effort === '') return null;
193
+ const index = EFFORT_LADDER.indexOf(effort);
194
+ if (index < 0 || index >= EFFORT_LADDER.length - 1) return null;
195
+ return EFFORT_LADDER[index + 1];
196
+ }
197
+
198
+ // maxTokens ×2 封顶硬上限;已在上限或无值返回 null。
199
+ function nextTokensOf(maxTokens) {
200
+ if (typeof maxTokens !== 'number' || maxTokens <= 0) return null;
201
+ const capped = Math.min(maxTokens * 2, MAX_TOKENS);
202
+ return capped > maxTokens ? capped : null;
203
+ }
204
+
205
+ // 升档候选清单(预算轴只升):单步(推理强度 → token 上限)在前、组合在后;
206
+ // 每个候选是完整配置快照(未升级的预算字段保留现值)。
207
+ function upCandidateConfigs(fromConfig) {
208
+ const out = [];
209
+ const effortNext = nextEffortOf(fromConfig.reasoningEffort);
210
+ const tokensNext = nextTokensOf(fromConfig.maxTokens);
211
+ if (effortNext !== null) {
212
+ const config = { reasoningEffort: effortNext };
213
+ if (typeof fromConfig.maxTokens === 'number' && fromConfig.maxTokens > 0) config.maxTokens = fromConfig.maxTokens;
214
+ out.push({ config, summary: '推理强度上调一档' });
215
+ }
216
+ if (tokensNext !== null) {
217
+ const config = { maxTokens: tokensNext };
218
+ if (typeof fromConfig.reasoningEffort === 'string' && fromConfig.reasoningEffort !== '') config.reasoningEffort = fromConfig.reasoningEffort;
219
+ out.push({ config, summary: 'token 上限上调' });
220
+ }
221
+ if (effortNext !== null && tokensNext !== null) {
222
+ out.push({ config: { reasoningEffort: effortNext, maxTokens: tokensNext }, summary: '推理强度上调一档 + token 上限上调' });
223
+ }
224
+ return out;
225
+ }
226
+
227
+ // 候选经 sanitizeProfile(strict)校验;有警告即返回 null(生成器输入受控,
228
+ // 此分支只防御未来 schema 变更)。
229
+ function validDraftOrNull(config, name, description) {
230
+ const { clean, warnings } = sanitizeProfile({ ...config, name, description }, { strict: true });
231
+ if (warnings.length > 0) return null;
232
+ return clean;
233
+ }
234
+
235
+ // 组装候选清单:一条建议 → ≤maxCandidates 条互斥候选(保守 → 更激进排序)。
236
+ // 每条候选 = { config, name, description, basis };config 含 id(必填非空)+ 能力/预算
237
+ // 字段;name 是「改动摘要(保守度标签)」;basis 供「为什么生成」展示(只含摘要,
238
+ // 无原文)。id = evo-<方向>-<摘要>-c<N>(N=1..3),冲突追加 -2/-3 同旧规则;
239
+ // 跨方向禁止(全降或全升,方向由建议本身决定)。sanitize 不过的候选整条丢弃。
240
+ export function buildEvolutionCandidates({ advice, l1Entry, l2, snapshot, whitelist, takenIds, now = Date.now(), maxCandidates = 3 } = {}) {
241
+ const a = advice !== null && typeof advice === 'object' ? advice : {};
242
+ const key = typeof a.profileKey === 'string' ? a.profileKey : '';
243
+ const suggestion = a.suggestion;
244
+ if (suggestion !== '降' && suggestion !== '升') return [];
245
+ const entry = l1Entry !== null && typeof l1Entry === 'object' ? l1Entry : {};
246
+ const snap = snapshot !== null && typeof snapshot === 'object' ? snapshot : {};
247
+ const from = sourceConfigFromKey(key, l2);
248
+ const direction = suggestion === '降' ? 'down' : 'up';
249
+ const steps = suggestion === '降'
250
+ ? downCandidateConfigs(parseProfileKey(key), snap.models, whitelist, snap.presets)
251
+ : upCandidateConfigs(from);
252
+ const taken = takenIds instanceof Set ? new Set(takenIds) : new Set(Array.isArray(takenIds) ? takenIds : []);
253
+ const base = baseDraftId(direction, key);
254
+ const directionText = suggestion === '降' ? '降级能力配置' : '上调预算';
255
+ const total = Math.min(steps.length, maxCandidates);
256
+ const out = [];
257
+ for (const [index, step] of steps.slice(0, maxCandidates).entries()) {
258
+ // -c<N> 序号与冲突重试后缀(-2)都必须完整落在 32 字符内:基名先让位再拼。
259
+ const suffix = `-c${index + 1}`;
260
+ const stem = base.slice(0, ID_MAX_LEN - suffix.length - 2);
261
+ const id = nextIdForBase(`${stem}${suffix}`, taken);
262
+ if (id === null) continue;
263
+ const name = `${step.summary}(${radicalLabel(index, total)})`;
264
+ const description = `${a.performanceText ?? ''};建议:${directionText}`;
265
+ const clean = validDraftOrNull({ ...step.config, id }, name, description);
266
+ if (clean === null) continue;
267
+ const configClean = { ...clean };
268
+ delete configClean.name;
269
+ delete configClean.description;
270
+ taken.add(id);
271
+ out.push({
272
+ config: configClean,
273
+ name,
274
+ description,
275
+ basis: {
276
+ profileKey: key,
277
+ suggestion,
278
+ confidence: a.confidence ?? entry.confidence?.level ?? '',
279
+ score: entry.score?.weighted_success ?? 0,
280
+ n: entry.confidence?.n ?? 0,
281
+ performanceText: a.performanceText ?? '',
282
+ generatedAt: now,
283
+ },
284
+ });
285
+ }
286
+ return out;
287
+ }
288
+
289
+ // 单候选兼容入口:取候选清单第一条(最保守);无候选返回 null。
290
+ export function buildEvolutionDraft(options = {}) {
291
+ const list = buildEvolutionCandidates(options);
292
+ return list.length > 0 ? list[0] : null;
293
+ }
294
+
295
+ // profile 配置 → 身份键口径(与聚合层同口径:persona/toolFilter 只认存在性)。
296
+ export function configIdentityKey(profile) {
297
+ const cfg = {};
298
+ if (typeof profile.preset === 'string' && profile.preset !== '' && profile.preset !== 'inherit') cfg.preset = profile.preset;
299
+ if (typeof profile.provider === 'string' && profile.provider !== '') cfg.provider = profile.provider;
300
+ if (typeof profile.model === 'string' && profile.model !== '') cfg.model = profile.model;
301
+ if (typeof profile.persona === 'string' && profile.persona.length > 0) cfg.persona_present = true;
302
+ if (profile.toolFilter !== undefined && profile.toolFilter !== null) cfg.toolFilter_present = true;
303
+ return profileKeyOf(cfg);
304
+ }
305
+
306
+ // 注册表中与某 L1 身份键匹配的方案(净化后快照)——指纹输入,编辑任一方案都会使
307
+ // 候选指纹失配(观察期人工编辑中止同源判据)。
308
+ export function matchingRegistryProfiles(store, profileKey) {
309
+ const profiles = store.profiles instanceof Map ? [...store.profiles.values()] : [];
310
+ return profiles
311
+ .filter((p) => p !== null && typeof p === 'object' && configIdentityKey(p) === profileKey)
312
+ .map((p) => sanitizeProfile(p, { strict: false }).clean);
313
+ }
314
+
315
+ // 对象键排序归一(供指纹输入稳定化)。
316
+ function canonicalEntry(entry) {
317
+ if (entry === null || typeof entry !== 'object') return entry;
318
+ const out = {};
319
+ for (const key of Object.keys(entry).sort()) out[key] = entry[key];
320
+ return out;
321
+ }
322
+
323
+ // 候选携带的「生成时刻 profile 指纹」:身份键 + 变更前配置 + 匹配该身份键的
324
+ // 注册表方案(排序归一后哈希)。人工编辑方案或聚合数据变化都会使指纹失配,
325
+ // 装配层据此拒绝应用(不静默应用过期候选)。
326
+ export function profileFingerprint({ profileKey, from, registryEntries = [] } = {}) {
327
+ const entries = Array.isArray(registryEntries) ? registryEntries : [];
328
+ const sorted = entries
329
+ .map(canonicalEntry)
330
+ .sort((a, b) => String(a?.id ?? '').localeCompare(String(b?.id ?? '')));
331
+ const payload = JSON.stringify({ profileKey, from: canonicalEntry(from), registry: sorted });
332
+ return createHash('sha1').update(payload, 'utf8').digest('hex');
333
+ }