dsh-subagent-profile 0.4.0 → 0.5.1

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 (37) hide show
  1. package/README.md +16 -57
  2. package/README.zh.md +16 -57
  3. package/index.mjs +97 -122
  4. package/lib/client.js +631 -88
  5. package/lib/core/cleanup.mjs +33 -0
  6. package/lib/core/data-maintenance.mjs +139 -0
  7. package/lib/core/decision-trace.mjs +20 -1
  8. package/lib/core/delegation.mjs +16 -0
  9. package/lib/core/dispatch-gates.mjs +122 -9
  10. package/lib/core/dispatch-schema.mjs +45 -7
  11. package/lib/core/dispatch-tool.mjs +30 -46
  12. package/lib/core/evolution-advice.mjs +31 -10
  13. package/lib/core/evolution-assets.mjs +13 -0
  14. package/lib/core/evolution-canary.mjs +427 -0
  15. package/lib/core/evolution-draft.mjs +21 -0
  16. package/lib/core/evolution-engine.mjs +176 -31
  17. package/lib/core/evolution-generate.mjs +7 -23
  18. package/lib/core/evolution-lag.mjs +78 -0
  19. package/lib/core/evolution-ledger.mjs +4 -1
  20. package/lib/core/evolution-routes.mjs +157 -16
  21. package/lib/core/http-routes.mjs +22 -7
  22. package/lib/core/model-policy.mjs +104 -0
  23. package/lib/core/presets-sync.mjs +24 -2
  24. package/lib/core/profile-directory.mjs +17 -0
  25. package/lib/core/profile-provider.mjs +7 -2
  26. package/lib/core/profiles-query.mjs +178 -0
  27. package/lib/core/profiles-store.mjs +40 -6
  28. package/lib/core/pure.mjs +1 -1
  29. package/lib/core/reminder-store.mjs +37 -3
  30. package/lib/core/snapshots.mjs +148 -0
  31. package/package.json +2 -2
  32. package/presets/orchestrator-v2/NOTICE +7 -0
  33. package/presets/{orchestrator → orchestrator-v2}/agent.cordis.yml +78 -7
  34. package/presets/orchestrator-v2/custom-bash.mjs +213 -0
  35. package/presets/orchestrator-v2/preset.yml +2 -0
  36. package/presets/orchestrator-v2/tool-bootstrap.mjs +620 -0
  37. package/presets/orchestrator/preset.yml +0 -2
@@ -0,0 +1,178 @@
1
+ // lib/core/profiles-query.mjs — profiles_query 工具(派发档案按需下钻):入参全可选
2
+ // (id / tier / provider+model 成对 / 无参全量),未知键由宿主 DSL 默认拒绝(键集闭合
3
+ // 断言在 test/profiles-query.test.mjs)。数据源分布:profileDirectoryRows(档案)/
4
+ // catalog-cache 快照(官方模型 + efforts,TTL 60s)/ summaries.json(统计,
5
+ // mtime+指纹,fail-soft)。官方目录没有的 model 只标「未在册」(advisory,不判失效);
6
+ // 「未授权」联动官方模型选择投影(model-policy):本会话投影配置了 allowedModels 且档案绑定路由不在
7
+ // 其中时追加标注。本工具不复制任何策略逻辑——授权性以官方为准(工具描述首行已指向
8
+ // list_subagent_models)。
9
+
10
+ import { defineTool } from './shims.mjs';
11
+ import { profileDirectoryRows, profileStatsFromSummaries } from './profile-directory.mjs';
12
+ import { readModelSelectionPolicy } from './model-policy.mjs';
13
+ import { readSummaries } from './evolution-advice.mjs';
14
+
15
+ // 入参 schema:全可选;provider+model 成对约束在 execute 内校验(schema 层无法表达
16
+ // 成对)。键集闭合 = 白名单 4 键(与 percall-spec 同口径:多出的键没有接受路径)。
17
+ export const PROFILES_QUERY_PARAMETERS = {
18
+ id: { type: 'string', description: '按 id 查单个派发档案明细(其余入参被忽略)' },
19
+ tier: { type: 'string', enum: ['cheap', 'balanced', 'premium'], description: '按成本/深度档位过滤档案' },
20
+ provider: { type: 'string', description: '与 model 成对使用:先查该模型的官方 reasoning efforts,再列出绑定该模型的档案' },
21
+ model: { type: 'string', description: '与 provider 成对使用:官方模型目录中的模型 id' },
22
+ };
23
+
24
+ // 输出 schema:模型可见面只有 text 一行(render 把 text 包成内容块)。
25
+ export const PROFILES_QUERY_OUTPUT_SCHEMA = {
26
+ type: 'object',
27
+ additionalProperties: false,
28
+ properties: {
29
+ text: { type: 'string', required: true, description: '模型可见的查询结果文本' },
30
+ },
31
+ };
32
+
33
+ // 列表行描述截断长度(单档案明细不截断)。
34
+ const LIST_DESC_CHARS = 80;
35
+
36
+ // 档案绑定的 provider/model 路由(两者齐备才绑定);未绑定返回 null。
37
+ export function boundRouteOf(row) {
38
+ const provider = row !== null && typeof row === 'object' && typeof row.provider === 'string' && row.provider !== '' ? row.provider : undefined;
39
+ const model = row !== null && typeof row === 'object' && typeof row.model === 'string' && row.model !== '' ? row.model : undefined;
40
+ return provider !== undefined && model !== undefined ? { provider, model } : null;
41
+ }
42
+
43
+ // 官方状态标注:在册(catalog models 含 provider+model)/ 未在册 / 未绑定。未在册只
44
+ // 标不判失效(advisory)。
45
+ export function catalogStatusOf(row, models) {
46
+ const route = boundRouteOf(row);
47
+ if (route === null) return '未绑定';
48
+ const listed = Array.isArray(models) && models.some((m) => m !== null && typeof m === 'object' && m.provider === route.provider && m.id === route.model);
49
+ return listed ? '在册' : '未在册';
50
+ }
51
+
52
+ // 未授权标注:投影配置且绑定路由不在 allowedModels → '未授权';投影未配置或未绑定
53
+ // → undefined(不加注)。授权性判定只读官方投影,不复制策略逻辑。
54
+ export function unauthorizedMarkOf(row, policy) {
55
+ const route = boundRouteOf(row);
56
+ if (route === null || policy === undefined || policy === null || policy.configured !== true) return undefined;
57
+ const allowed = Array.isArray(policy.allowedModels) && policy.allowedModels.some((r) => r !== null && typeof r === 'object' && r.provider === route.provider && r.model === route.model);
58
+ return allowed ? undefined : '未授权';
59
+ }
60
+
61
+ // efforts 条件式文案:绑定 → 该模型官方 reasoning efforts(目录无数据 → 占位);
62
+ // 未绑定 → 继承父模型、档位以父为准。
63
+ export function effortsTextOf(row, effortsByModel) {
64
+ const route = boundRouteOf(row);
65
+ if (route === null) return '继承父模型,档位以父为准';
66
+ const efforts = effortsByModel !== null && typeof effortsByModel === 'object' ? effortsByModel[route.model] : undefined;
67
+ if (!Array.isArray(efforts) || efforts.length === 0) return '(目录无该模型明细)';
68
+ return efforts.map((effort) => (effort !== null && typeof effort === 'object' && typeof effort.id === 'string' ? effort.id : String(effort))).join('/');
69
+ }
70
+
71
+ // 单档案明细(两行:主行 + efforts 行)。stats 为 profileStatsFromSummaries 的
72
+ // Map;truncate=true 时描述截断 80 字符 + …(列表口径,单档案明细不截断)。
73
+ export function profileDetailLines(row, { stats, models, effortsByModel, policy, truncate = false } = {}) {
74
+ const desc = row !== null && typeof row === 'object' && typeof row.description === 'string' && row.description !== '' ? row.description : '(无描述)';
75
+ const shown = truncate && desc.length > LIST_DESC_CHARS ? `${desc.slice(0, LIST_DESC_CHARS)}…` : desc;
76
+ const route = boundRouteOf(row);
77
+ const binding = route === null ? '继承父' : `${route.provider}/${route.model}`;
78
+ const tier = row !== null && typeof row === 'object' && typeof row.tokenTier === 'string' && row.tokenTier !== '' ? row.tokenTier : '未标注';
79
+ const stat = stats instanceof Map ? stats.get(row?.id) : (stats !== null && typeof stats === 'object' ? stats[row?.id] : undefined);
80
+ const cost = stat !== null && typeof stat === 'object' && typeof stat.avgCost === 'number' ? `平均 ¥${stat.avgCost}` : undefined;
81
+ const rate = stat !== null && typeof stat === 'object' && typeof stat.successRate === 'number'
82
+ ? `成功率 ${stat.successRate}${typeof stat.n === 'number' ? ` (N=${stat.n})` : ''}`
83
+ : undefined;
84
+ const status = catalogStatusOf(row, models);
85
+ const unauthorized = unauthorizedMarkOf(row, policy);
86
+ const meta = [binding, `tier ${tier}`, cost, rate, `官方状态:${status}${unauthorized !== undefined ? `·${unauthorized}` : ''}`].filter((part) => part !== undefined).join(' · ');
87
+ return [`${row?.id}: ${shown} — ${meta}`, `· efforts: ${effortsTextOf(row, effortsByModel)}`];
88
+ }
89
+
90
+ // 单次查询组装(纯函数):id 单档案 > tier 过滤 > provider+model 成对 > 无参全量。
91
+ // provider/model 只传其一 → fail-loud(成对约束)。
92
+ export function buildProfilesQueryText({ args, rows, stats, models, effortsByModel, policy }) {
93
+ const hasProvider = typeof args?.provider === 'string' && args.provider !== '';
94
+ const hasModel = typeof args?.model === 'string' && args.model !== '';
95
+ if (hasProvider !== hasModel) throw new Error('profiles_query: provider 与 model 须成对提供');
96
+ const details = (row, truncate = false) => profileDetailLines(row, { stats, models, effortsByModel, policy, truncate });
97
+ if (typeof args?.id === 'string' && args.id !== '') {
98
+ const row = rows.find((r) => r !== null && typeof r === 'object' && r.id === args.id);
99
+ if (row === undefined) return { text: `未找到 profile "${args.id}"。可用 id:${rows.map((r) => r?.id).join('、') || '(暂无)'}` };
100
+ return { text: details(row).join('\n') };
101
+ }
102
+ if (typeof args?.tier === 'string' && args.tier !== '') {
103
+ const tierRows = rows.filter((r) => r !== null && typeof r === 'object' && r.tokenTier === args.tier);
104
+ if (tierRows.length === 0) return { text: `无 tier=${args.tier} 的方案` };
105
+ const lines = tierRows.flatMap((row) => details(row, true));
106
+ return { text: `tier=${args.tier} 的方案(${tierRows.length}):\n${lines.join('\n')}` };
107
+ }
108
+ if (hasModel) {
109
+ const listed = Array.isArray(models) && models.some((m) => m !== null && typeof m === 'object' && m.provider === args.provider && m.id === args.model);
110
+ const rawEfforts = effortsByModel !== null && typeof effortsByModel === 'object' ? effortsByModel[args.model] : undefined;
111
+ const modelEfforts = Array.isArray(rawEfforts) && rawEfforts.length > 0
112
+ ? rawEfforts.map((effort) => (effort !== null && typeof effort === 'object' && typeof effort.id === 'string' ? effort.id : String(effort))).join('/')
113
+ : undefined;
114
+ const header = `模型 ${args.provider}/${args.model} 官方 reasoning efforts:${modelEfforts ?? '(目录无该模型明细)'}`;
115
+ const bound = rows.filter((r) => {
116
+ const route = boundRouteOf(r);
117
+ return route !== null && route.provider === args.provider && route.model === args.model;
118
+ });
119
+ if (bound.length === 0) return { text: `${header}\n无绑定该模型的方案${listed ? '' : '(该模型不在官方目录,未在册)'}` };
120
+ const lines = bound.flatMap((row) => details(row, true));
121
+ return { text: `${header}\n${lines.join('\n')}` };
122
+ }
123
+ if (rows.length === 0) return { text: '暂无可用派发档案(可在设置页「子 Agent 方案」创建)' };
124
+ const lines = rows.flatMap((row) => details(row, true));
125
+ return { text: `派发档案目录(${rows.length}):\n${lines.join('\n')}` };
126
+ }
127
+
128
+ // 查询主体:catalog 快照(含官方 efforts)→ summaries 统计(fail-soft)→ 官方
129
+ // 模型选择投影(未授权标注)→ 纯函数组装文本。
130
+ async function runProfilesQuery(args, exec, deps) {
131
+ const agent = exec !== null && typeof exec === 'object' ? exec.agent : undefined;
132
+ const snapshot = await deps.catalog.getSnapshot(typeof agent?.ctx?.get === 'function' ? agent.ctx.get('llm') : undefined);
133
+ const models = snapshot !== null && typeof snapshot === 'object' && Array.isArray(snapshot.models) ? snapshot.models : [];
134
+ const effortsByModel = snapshot !== null && typeof snapshot === 'object' && snapshot.efforts !== null && typeof snapshot.efforts === 'object' ? snapshot.efforts : {};
135
+ let summaries = null;
136
+ try { summaries = readSummaries(deps.summariesFile, deps.logger, { onLoss: deps.onLoss }); } catch { /* 统计只增强,查询不因统计失败而失败 */ }
137
+ const stats = profileStatsFromSummaries(deps.store, summaries);
138
+ const policy = readModelSelectionPolicy(agent?.ctx, agent?.session);
139
+ return buildProfilesQueryText({ args, rows: profileDirectoryRows(deps.store), stats, models, effortsByModel, policy });
140
+ }
141
+
142
+ // 工厂返回 { syncTool, dispose }:与 dispatch 同款 syncTool 机制(随 enabled 开关
143
+ // 注册/注销,工具列表自动可见,不依赖静态段)。
144
+ export function createProfilesQueryTool({ register, store, catalog, summariesFile, onLoss, logger, getEnabled }) {
145
+ const deps = { store, catalog, summariesFile, onLoss, logger, getEnabled };
146
+ const tool = defineTool({
147
+ name: 'profiles_query',
148
+ description: '按方案查询派发档案与明细(值不值:描述/档位/统计);授权路由(能不能用)请用官方 list_subagent_models。入参全可选:id 查单个档案;tier 按档位过滤;provider+model 成对查询(先查官方 efforts 再列绑定方案);无参返回全量目录。',
149
+ parameters: PROFILES_QUERY_PARAMETERS,
150
+ output: {
151
+ schema: PROFILES_QUERY_OUTPUT_SCHEMA,
152
+ render: (_args, value) => [{ type: 'text', text: value.text }],
153
+ },
154
+ isConcurrencySafe: () => true,
155
+ execute: (args, exec) => runProfilesQuery(args, exec, deps),
156
+ });
157
+ let disposeTool;
158
+ function syncTool() {
159
+ if (getEnabled() && disposeTool === undefined) {
160
+ disposeTool = register(tool);
161
+ } else if (!getEnabled() && disposeTool !== undefined) {
162
+ const dispose = disposeTool;
163
+ disposeTool = undefined;
164
+ if (typeof dispose === 'function') dispose();
165
+ }
166
+ }
167
+ syncTool();
168
+ return {
169
+ syncTool,
170
+ dispose() {
171
+ if (disposeTool !== undefined) {
172
+ const dispose = disposeTool;
173
+ disposeTool = undefined;
174
+ if (typeof dispose === 'function') dispose();
175
+ }
176
+ }
177
+ };
178
+ }
@@ -34,15 +34,30 @@ export function dshHome() {
34
34
  // 1. Profile 注册表(per-instance 状态;bundle 行是进程级,故本 Map 是单例
35
35
  // store,与动态插件完全一致)。内置种子带 `builtin: true`,reset/remove 可
36
36
  // 识别它们,改过的内置方案与纯用户 profile 可区分。
37
- // description 是语义化的:一行定位 + 何时用,帮助模型选择。reasoningEffort
37
+ // description 是语义化的:一行定位 + 何时用,帮助模型选择;description
38
+ // 须含适用场景词(落库契约,见 assertProfileDescription)。reasoningEffort
38
39
  // 档位经 llm-deepseek adapter 核验(off/high/max;见 resolveModel 对
39
40
  // connection.defaults.thinking 的门控——本部署未设 thinking,
40
41
  // 故对 deepseek-v4-flash 全档位宣传)。
41
42
  export const BUILTIN_SEEDS = [
42
- { id: 'swap-standard', name: '标准编码', description: '切换到 standard 预设的完整编码工具集。当父会话不是 standard、但子任务需要完整编码能力时用。', preset: 'standard', tokenTier: 'balanced', builtin: true },
43
- { id: 'researcher', name: '调研检索', description: '关闭深度推理省 token,继承父工具。适合查资料、汇总、背景调研,不适合改代码。', reasoningEffort: 'off', persona: 'You are a research subagent: search, read, and summarize only. Do not modify code or files.', tokenTier: 'cheap', builtin: true }
43
+ { id: 'swap-standard', name: '标准编码', description: '写代码与重构场景:切换到 standard 预设的完整编码工具集,适合多文件改动、修复与测试;当父会话不是 standard、但子任务需要完整编码能力时选用。', preset: 'standard', tokenTier: 'balanced', builtin: true },
44
+ { id: 'researcher', name: '调研检索', description: '调研与汇总场景:关闭深度推理省 token,继承父工具;适合查资料、检索、背景调研与只读分析,不适合改代码。', reasoningEffort: 'off', persona: 'You are a research subagent: search, read, and summarize only. Do not modify code or files.', tokenTier: 'cheap', builtin: true }
44
45
  ];
45
46
 
47
+ // description 落库契约词表:description 必须包含至少一个适用场景词(常量,可微调)。
48
+ export const DESCRIPTION_SCENARIO_WORDS = ['查', '调研', '检索', '汇总', '代码', '重构', '修复', '审查', '只读', '审计', '生成', '测试', '翻译', '编写', '改', '脚本', '分析'];
49
+
50
+ // description 落库校验(纯函数,zero-dep):非空(trim)+ 长度 ≥10 字符 + 含适用
51
+ // 场景词,任一不满足 fail-loud(文案定稿)。返回 trim 后的文本供调用方落库。
52
+ export function assertProfileDescription(description) {
53
+ const text = typeof description === 'string' ? description.trim() : '';
54
+ const valid = text.length >= 10 && DESCRIPTION_SCENARIO_WORDS.some((word) => text.includes(word));
55
+ if (!valid) {
56
+ throw new Error('description 需写明适用场景(如:查资料/周报汇总、多文件重构、只读审计),当前描述不含可用场景词');
57
+ }
58
+ return text;
59
+ }
60
+
46
61
  // --- 模块级 store 函数(从工厂拆出;per-apply 状态经 `state` 注入)---------------
47
62
 
48
63
  function resolveProfile(profiles, id) {
@@ -169,11 +184,12 @@ function persistProfiles(state, logger) {
169
184
  // (派发优化建议注入,默认关,需显式开)+ escapeEnabled(逃生舱放行,默认关——信任
170
185
  // 底板,仅设置页显式 opt-in 可放开)+ applyEnabled(进化候选生成与应用写路径,默认
171
186
  // 关——应用能力只由人显式开启)+ candidateMode/candidateTtlH(候选方案更新策略与
172
- // 有效期,用户可配置)。开关共用同一 state.json,读写整份对象以互不覆盖;
187
+ // 有效期,用户可配置)+ snapshotEnabled(派发配置快照,默认关——本机私有域,仅
188
+ // 设置页显式 opt-in 开启)。开关共用同一 state.json,读写整份对象以互不覆盖;
173
189
  // 缺失文件回退默认值(全新部署),损坏文件 fail-closed(全关,绝不静默回退
174
190
  // enabled:true —— 安全禁用会被撤销),内存态仍驱动本进程。
175
- const STATE_DEFAULTS = { enabled: true, evolutionAdvice: false, escapeEnabled: false, applyEnabled: false, candidateMode: 'auto', candidateTtlH: 24 };
176
- const STATE_FAIL_CLOSED = { enabled: false, evolutionAdvice: false, escapeEnabled: false, applyEnabled: false, candidateMode: 'auto', candidateTtlH: 24 };
191
+ const STATE_DEFAULTS = { enabled: true, evolutionAdvice: false, escapeEnabled: false, applyEnabled: false, candidateMode: 'auto', candidateTtlH: 24, snapshotEnabled: false };
192
+ const STATE_FAIL_CLOSED = { enabled: false, evolutionAdvice: false, escapeEnabled: false, applyEnabled: false, candidateMode: 'auto', candidateTtlH: 24, snapshotEnabled: false };
177
193
 
178
194
  // 候选有效期小时数归一:0(不自动过期)或 1~8760(一年内)的整数原样保留,
179
195
  // 其余值回退默认 24 小时(读取与写入共用同一口径,防坏值落盘)。
@@ -193,6 +209,7 @@ function loadState(state) {
193
209
  applyEnabled: parsed.applyEnabled === true,
194
210
  candidateMode: parsed.candidateMode === 'manual' ? 'manual' : 'auto',
195
211
  candidateTtlH: validCandidateTtlH(parsed.candidateTtlH),
212
+ snapshotEnabled: parsed.snapshotEnabled === true,
196
213
  };
197
214
  } catch (error) {
198
215
  // fail-closed(S2):损坏的 state.json 若回退 enabled:true,会把「安全禁用」静默撤销。
@@ -223,6 +240,20 @@ function persistState(state, patch, onGovernanceFailure) {
223
240
  }
224
241
  }
225
242
 
243
+ // 落库保存路径:description 值发生变化(新建或编辑改写)必须过契约校验;
244
+ // 与原值相同(存量未动)或未携带 description 时跳过——存量宽容不迁移。
245
+ // 校验失败 fail-loud,不落 Map。
246
+ function saveProfile(state, profile) {
247
+ if (profile !== null && typeof profile === 'object' && profile.description !== undefined) {
248
+ const existing = state.profiles.get(profile.id);
249
+ if (existing === undefined || existing.description !== profile.description) {
250
+ assertProfileDescription(profile.description);
251
+ }
252
+ }
253
+ state.profiles.set(profile.id, profile);
254
+ return profile;
255
+ }
256
+
226
257
  // 构造一个 store 实例。`dshHome` 是已解析的主目录字符串(来自导出的
227
258
  // dshHome());`logger` 是 apply 时的 ctx.logger。`onGovernanceFailure`(可选)
228
259
  // 是任何 profile/state 持久化写失败时触发的审计钩子——一次治理审计丢失事件。
@@ -244,6 +275,7 @@ export function createProfileStore({ dshHome, logger, onGovernanceFailure = () =
244
275
  profiles,
245
276
  deletedBuiltins,
246
277
  resolveProfile: (id) => resolveProfile(profiles, id),
278
+ saveProfile: (profile) => saveProfile(state, profile),
247
279
  loadProfiles: () => loadProfiles(state, logger),
248
280
  persistProfiles: () => {
249
281
  const result = persistProfiles(state, logger);
@@ -264,6 +296,8 @@ export function createProfileStore({ dshHome, logger, onGovernanceFailure = () =
264
296
  persistCandidateMode: (value) => persistState(state, { candidateMode: value === 'manual' ? 'manual' : 'auto' }, onGovernanceFailure),
265
297
  loadCandidateTtlH: () => loadState(state).candidateTtlH,
266
298
  persistCandidateTtlH: (value) => persistState(state, { candidateTtlH: validCandidateTtlH(value) }, onGovernanceFailure),
299
+ loadSnapshotEnabled: () => loadState(state).snapshotEnabled,
300
+ persistSnapshotEnabled: (value) => persistState(state, { snapshotEnabled: value === true }, onGovernanceFailure),
267
301
  // getAllowFailOpen:cost guard 必须在派发时读取迁移标志;
268
302
  // loadProfiles/persistProfiles 持有该写入。
269
303
  getAllowFailOpen: () => state.allowFailOpen,
package/lib/core/pure.mjs CHANGED
@@ -369,7 +369,7 @@ export function sanitizeProfile(profile, options = {}) {
369
369
  case 'name':
370
370
  case 'description':
371
371
  // id/preset 与 name/description 同走文本采样:压平换行,阻断经 \n 注入
372
- // orchestrator systemPrompt 的提示注入面(H1)。id/preset 此前走 default
372
+ // orchestrator-v2 systemPrompt 的提示注入面(H1)。id/preset 此前走 default
373
373
  // 分支原样拷贝,可注入换行破坏 dispatch:profiles 行。
374
374
  sanitizeTextField(key, value, clean, warnings);
375
375
  break;
@@ -65,6 +65,10 @@ function loadState(stateFile, state, warn) {
65
65
  createdAt: Number.isFinite(raw.createdAt) ? raw.createdAt : 0,
66
66
  action: typeof raw.action === 'string' ? raw.action : null,
67
67
  actedAt: Number.isFinite(raw.actedAt) ? raw.actedAt : null,
68
+ // 归档标记:数据维护「归档已处理提醒」置位;归档只移出活动列表、绝不物理删除
69
+ // (提醒=审计记录,存储与 governance-audit 保留)。
70
+ archived: raw.archived === true,
71
+ archivedAt: Number.isFinite(raw.archivedAt) ? raw.archivedAt : null,
68
72
  });
69
73
  }
70
74
  } catch (error) {
@@ -117,6 +121,8 @@ function recordReminder(state, persist, audit, { severity, kind, title, detail,
117
121
  createdAt: Date.now(),
118
122
  action: null,
119
123
  actedAt: null,
124
+ archived: false,
125
+ archivedAt: null,
120
126
  };
121
127
  state.reminders.push(reminder);
122
128
  if (state.reminders.length > state.maxReminders) state.reminders.shift();
@@ -153,8 +159,34 @@ function actOnReminder(state, persist, audit, id, action) {
153
159
  }
154
160
 
155
161
  function listReminders(state, unreadOnly) {
156
- const all = [...state.reminders].reverse();
157
- return unreadOnly ? all.filter((r) => r.unread === true) : all;
162
+ const active = state.reminders.filter((r) => r.archived !== true).reverse();
163
+ return unreadOnly ? active.filter((r) => r.unread === true) : active;
164
+ }
165
+
166
+ // 归档视图:只读、按处理时间倒序、分页(防大量已处理条目一次性渲染卡 UI)。
167
+ // 返回 { reminders, total, offset, limit };坏参数归一安全值。
168
+ function listArchivedReminders(state, offset, limit) {
169
+ const off = Number.isInteger(offset) && offset >= 0 ? offset : 0;
170
+ const lim = Number.isInteger(limit) && limit > 0 ? Math.min(limit, 100) : 20;
171
+ const archived = state.reminders
172
+ .filter((r) => r.archived === true)
173
+ .sort((a, b) => (b.archivedAt ?? b.actedAt ?? 0) - (a.archivedAt ?? a.actedAt ?? 0));
174
+ return { reminders: archived.slice(off, off + lim), total: archived.length, offset: off, limit: lim };
175
+ }
176
+
177
+ // 归档已处理条目:action 非空且未归档的一并置 archived(只移出活动列表、保留存储)。
178
+ // 返回本次归档条数;审计由调用方按一次清理记一条(数据维护口径)。
179
+ function archiveActedReminders(state, persist) {
180
+ const now = Date.now();
181
+ let count = 0;
182
+ for (const reminder of state.reminders) {
183
+ if (typeof reminder.action !== 'string' || reminder.action === '' || reminder.archived === true) continue;
184
+ reminder.archived = true;
185
+ reminder.archivedAt = now;
186
+ count += 1;
187
+ }
188
+ if (count > 0) persist();
189
+ return count;
158
190
  }
159
191
 
160
192
  export function createReminderStore({ dshHome, audit = () => {}, warn = () => {}, maxReminders = MAX_REMINDERS } = {}) {
@@ -167,6 +199,8 @@ export function createReminderStore({ dshHome, audit = () => {}, warn = () => {}
167
199
  ack: (ids) => ackReminders(state, persist, audit, ids),
168
200
  actOn: (id, action) => actOnReminder(state, persist, audit, id, action),
169
201
  list: ({ unreadOnly = false } = {}) => listReminders(state, unreadOnly),
170
- unreadCount: () => state.reminders.filter((r) => r.unread === true).length,
202
+ listArchived: ({ offset = 0, limit = 20 } = {}) => listArchivedReminders(state, offset, limit),
203
+ archiveActed: () => archiveActedReminders(state, persist),
204
+ unreadCount: () => state.reminders.filter((r) => r.unread === true && r.archived !== true).length,
171
205
  };
172
206
  }
@@ -0,0 +1,148 @@
1
+ // lib/core/snapshots.mjs — 派发配置快照域(subagent-evolution/snapshots/)。
2
+ // 开关开启后每次 dispatch effective 形成后写一条完整 config 快照(含 persona 原文
3
+ // 与 toolFilter 全量),供「保存为方案」事后完整复现该笔派发;快照是用户显式
4
+ // opt-in 的本机私有域(不是台账,dispatch.jsonl 不落原文——隐私红线保持)。
5
+ // 原子写(tmp+rename);索引 snapshots.index.json 记录元数据(无原文),按 ts
6
+ // 倒序读取;损坏文件/索引 fail-soft 跳过;总量护栏:快照 ≤200 条,到上限停止
7
+ // 新写并提示清理(不自动删旧)。import-free(仅 node 内置),可被裸 CI 单测。
8
+ // 审计:snapshot-written(写)/ snapshot-used(保存为方案命中,由调用方记)。
9
+
10
+ import { createHash } from 'node:crypto';
11
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+
14
+ // 快照总量护栏(B7 定稿值):到上限停止新写(不自动删旧,提示清理)。
15
+ export const SNAPSHOT_CAP = 200;
16
+
17
+ // 完整 config 字段集:与 dispatch merged 同源(persona 原文、toolFilter 完整对象)。
18
+ const CONFIG_FIELDS = ['preset', 'provider', 'model', 'reasoningEffort', 'maxTokens', 'maxDepth', 'persona', 'toolFilter'];
19
+
20
+ // profileKey 摘要(文件名安全前缀,非可逆)。
21
+ function digestOf(profileKey) {
22
+ return createHash('sha256').update(String(profileKey)).digest('hex').slice(0, 12);
23
+ }
24
+
25
+ // 原子写(tmp+rename):崩溃不产生截断文件;失败清理 tmp + warn + 治理审计钩子。
26
+ function atomicWrite(file, payload, logger, onGovernanceFailure) {
27
+ const tmp = `${file}.tmp`;
28
+ try {
29
+ mkdirSync(dirname(file), { recursive: true });
30
+ writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
31
+ renameSync(tmp, file);
32
+ return { persisted: true };
33
+ } catch (error) {
34
+ try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
35
+ if (logger !== undefined && typeof logger.warn === 'function') {
36
+ logger.warn(`[dsh-subagent-profile] 配置快照写入失败:${error instanceof Error ? error.message : String(error)}`);
37
+ }
38
+ if (typeof onGovernanceFailure === 'function') onGovernanceFailure();
39
+ return { persisted: false, error: error instanceof Error ? error.message : String(error) };
40
+ }
41
+ }
42
+
43
+ // 读索引;缺失回退空表,损坏/形状不符 fail-soft 从空起步(快照文件仍在,索引可重建)。
44
+ function readIndex(indexFile, logger) {
45
+ if (!existsSync(indexFile)) return [];
46
+ try {
47
+ const parsed = JSON.parse(readFileSync(indexFile, 'utf8'));
48
+ if (parsed !== null && typeof parsed === 'object' && parsed.v === 1 && Array.isArray(parsed.entries)) {
49
+ return parsed.entries.filter((e) => e !== null && typeof e === 'object' && typeof e.id === 'string');
50
+ }
51
+ } catch { /* fail-soft */ }
52
+ if (logger !== undefined && typeof logger.warn === 'function') {
53
+ logger.warn('[dsh-subagent-profile] 配置快照索引损坏,按空索引处理(快照文件保留)');
54
+ }
55
+ return [];
56
+ }
57
+
58
+ // 读单个快照文件;缺失/损坏/形状不符返回 null(fail-soft,调用方跳过)。
59
+ function readSnapshotFile(file, logger) {
60
+ if (!existsSync(file)) return null;
61
+ try {
62
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
63
+ if (parsed !== null && typeof parsed === 'object' && parsed.v === 1 && typeof parsed.id === 'string' && parsed.config !== null && typeof parsed.config === 'object') {
64
+ return parsed;
65
+ }
66
+ } catch { /* fail-soft */ }
67
+ if (logger !== undefined && typeof logger.warn === 'function') {
68
+ logger.warn(`[dsh-subagent-profile] 配置快照文件损坏,跳过:${file}`);
69
+ }
70
+ return null;
71
+ }
72
+
73
+ // dispatch effective 形成后调用:开关关恒跳过;到护栏上限停止新写(不自动删旧)。
74
+ // 幂等键 = 摘要+ts;同键碰撞跳过。审计只记元数据(无 persona/prompt 原文)。
75
+ function captureSnapshot(deps, { profileKey, config, mode }) {
76
+ if (!deps.getEnabled()) return null;
77
+ if (deps.state.index.length >= SNAPSHOT_CAP) {
78
+ if (deps.logger !== undefined && typeof deps.logger.warn === 'function') {
79
+ deps.logger.warn(`[dsh-subagent-profile] 配置快照已达 ${SNAPSHOT_CAP} 条护栏上限,停止新快照(可在设置页数据维护区清理)`);
80
+ }
81
+ return null;
82
+ }
83
+ const key = typeof profileKey === 'string' ? profileKey : '(inline)';
84
+ const ts = Date.now();
85
+ const id = `${digestOf(key)}-${ts}`;
86
+ if (deps.state.index.some((e) => e.id === id)) return null;
87
+ const payload = { v: 1, id, ts, profileKey: key, from: typeof mode === 'string' && mode !== '' ? mode : 'dispatch', config: {} };
88
+ for (const field of CONFIG_FIELDS) {
89
+ if (config !== null && typeof config === 'object' && config[field] !== undefined) payload.config[field] = config[field];
90
+ }
91
+ const size = Buffer.byteLength(JSON.stringify(payload));
92
+ const file = join(deps.dir, `${id}.json`);
93
+ const written = atomicWrite(file, payload, deps.logger, deps.onGovernanceFailure);
94
+ if (!written.persisted) return null;
95
+ deps.state.index.push({ key, ts, size, id, from: payload.from });
96
+ deps.persistIndex();
97
+ deps.recordAudit({ kind: 'snapshot-written', profile_key: key, file: `${id}.json`, size });
98
+ return { id, ts, profileKey: key };
99
+ }
100
+
101
+ // 索引元数据列表(ts 倒序;无 config 原文)。
102
+ function listSnapshots(state) {
103
+ return [...state.index].sort((a, b) => b.ts - a.ts);
104
+ }
105
+
106
+ // 单条完整快照(只读查看用;含 persona 原文,UI 默认折叠)。
107
+ function viewSnapshot(deps, id) {
108
+ if (typeof id !== 'string' || id === '' || !deps.state.index.some((e) => e.id === id)) return null;
109
+ return readSnapshotFile(join(deps.dir, `${id}.json`), deps.logger);
110
+ }
111
+
112
+ // 「保存为方案」数据源:按 profileKey 匹配最近一条快照(ts 倒序首条命中)。
113
+ function latestSnapshot(deps, profileKey) {
114
+ for (const entry of listSnapshots(deps.state)) {
115
+ if (entry.key !== profileKey) continue;
116
+ const snapshot = readSnapshotFile(join(deps.dir, `${entry.id}.json`), deps.logger);
117
+ if (snapshot !== null) return { ...snapshot, file: `${entry.id}.json` };
118
+ }
119
+ return null;
120
+ }
121
+
122
+ // 一键清理:删除全部快照文件 + 索引置空。返回删除条数(审计由调用方记)。
123
+ function clearSnapshots(deps) {
124
+ const count = deps.state.index.length;
125
+ for (const entry of deps.state.index) {
126
+ try { rmSync(join(deps.dir, `${entry.id}.json`), { force: true }); } catch { /* 单个删除失败不阻断其余 */ }
127
+ }
128
+ deps.state.index = [];
129
+ try { rmSync(deps.indexFile, { force: true }); } catch { /* best effort */ }
130
+ return count;
131
+ }
132
+
133
+ export function createSnapshotStore({ dshHome, logger, getEnabled = () => false, recordAudit = () => {}, onGovernanceFailure = () => {} } = {}) {
134
+ const dir = join(dshHome, 'subagent-evolution', 'snapshots');
135
+ const indexFile = join(dir, 'snapshots.index.json');
136
+ const state = { index: readIndex(indexFile, logger) };
137
+ const deps = { dir, indexFile, state, logger, getEnabled, recordAudit, onGovernanceFailure, persistIndex: () => atomicWrite(indexFile, { v: 1, entries: state.index }, logger, onGovernanceFailure) };
138
+ return {
139
+ dir,
140
+ indexFile,
141
+ capture: (input) => captureSnapshot(deps, input),
142
+ list: () => listSnapshots(state),
143
+ view: (id) => viewSnapshot(deps, id),
144
+ latestByProfileKey: (profileKey) => latestSnapshot(deps, profileKey),
145
+ count: () => state.index.length,
146
+ clear: () => clearSnapshots(deps),
147
+ };
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-subagent-profile",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "DeepSeek Harness 子 Agent 派发插件:按任务为子代理选模型、推理强度与工具范围,常用组合存成命名方案随时复用;内置安全检查、成本估算与节省分析、完整决策台账。",
5
5
  "repository": {
6
6
  "type": "git",
@@ -79,4 +79,4 @@
79
79
  "profiles"
80
80
  ],
81
81
  "license": "MIT"
82
- }
82
+ }
@@ -0,0 +1,7 @@
1
+ agent.cordis.yml 改编自 DeepSeek Harness 内置 Minimal 与 Standard preset(MIT)。
2
+ tool-bootstrap.mjs 来自 https://github.com/xiaobright/dsh-anchored-standard(MIT),
3
+ 并包含 dsh-liangshen 的两阶段隔离扩展。
4
+ custom-bash.mjs 移植自同一上游(xiaobright/dsh-anchored-standard,MIT)的
5
+ combo-anchored/custom-bash.mjs,用于 win32 上以跨平台子进程通道提供同名
6
+ `bash` 工具(Git Bash)。
7
+ 原始 DeepSeek 版权和 MIT 许可声明见开源仓库。
@@ -1,4 +1,4 @@
1
- # `orchestrator` 编排者预设:主 Agent 协调模式。
1
+ # `orchestrator-v2` 编排者预设 V2:主 Agent 协调模式 + 梁神 V2 两阶段引导。
2
2
  #
3
3
  # 与官方 `standard` 预设逐行一致,仅 persona 段改写为「主协调 Agent」身份:
4
4
  # 拆解任务、经宿主注册的 `dispatch` 工具(按场景选方案/预设/模型/推理强度)
@@ -6,8 +6,27 @@
6
6
  # 再集成结果。`dispatch` 工具本身由 dsh-subagent-profile 宿主行注册,对所有
7
7
  # 预设可见;本预设只补下面的委派工具。
8
8
  #
9
- # 改编自官方 `standard` 预设(仅 persona 段不同);原始 DeepSeek 版权与
10
- # MIT 许可声明见开源仓库。
9
+ # V2 改造(模仿梁神模式 V2,适配当前 DSH >= 0.1.2):
10
+ # - 引入两阶段引导 `./tool-bootstrap.mjs`:第一个持久工具/调用事件之前,
11
+ # 模型只看到 Minimal 双工具(`bash` + `str_replace_editor`)与本预设的
12
+ # persona 段,无运行时上下文、只放行白名单消息来源;锚定(或四步兜底、
13
+ # 或首轮结束)后升入 PTC 模式(单 `run_code` 工具)并恢复全量目录,
14
+ # 工作区指令与技能目录延迟一步注入。压缩后回落到受控阶段
15
+ # (双工具 + compactionTools 核心工作集,含 dispatch/subagent 以维持委派),
16
+ # 等待新的升态信号。
17
+ # - tool-bootstrap.mjs 已适配 0.1.2:经 `session.snapshotEvents()` 读事件日志
18
+ # (旧 `session.events` 属性已移除,直接读会每轮崩 "Cannot read properties
19
+ # of undefined (reading 'length')");persona 已含 `{{cwd}}` 模板时不再追加
20
+ # 字面工作目录行。
21
+ # - 补 `str_replace_editor`(phase-1 编辑器);win32 下补 `custom-bash`
22
+ # (Git Bash `bash` 工具)以保住 phase-1 的 `bash` 锚点——官方 tool-bash
23
+ # 在 win32 禁用、tool-pwsh 不叫 bash,没有它 phase-1 会退化成全量目录。
24
+ # posix 下官方 tool-bash 本身就叫 bash,无需补行。
25
+ # - 补 `command-goal`(/goal 人机命令,对齐当前官方 standard);spawn 行补
26
+ # `modelSelectionSettings: true`。
27
+ #
28
+ # 改编自官方 `standard` 预设与梁神模式 V2(liangshen-v2);原始 DeepSeek
29
+ # 版权与 MIT 许可声明见开源仓库与随附 NOTICE。
11
30
  #
12
31
  # 本文件是 agent 平面组成:由 roster 挂载一次到 standing scope,引用它的会话
13
32
  # 经 scope 父系加入,本文件注册的工具与 prompt 段覆盖每个加入的 agent;会话
@@ -37,6 +56,35 @@
37
56
 
38
57
  边界:子 Agent 能力 ⊆ 你,派发不豁免父会话的审批要求;子 Agent 失败或超出权限时自己接手重做,或用 ask_user_question 向用户澄清。
39
58
 
59
+ # 两阶段输入引导(同梁神 V2 的 tool-bootstrap):在第一个持久 tool/call 事件
60
+ # 之前,模型只看到 `bash` + `str_replace_editor` 双工具与本预设 persona 段、
61
+ # 无运行时上下文、只放行白名单消息来源(直接用户消息 + goal 自动轮次)。
62
+ # anchorGate 让升态保持到首个 minimal-like 推理块(四步兜底);
63
+ # promoteAfterFirstResponse 避免无工具首答后永久双工具,并在首轮结束时释放
64
+ # 锚门会话。升态后目录以 PTC 模式呈现(线上单一 run_code 工具,步边界切换),
65
+ # 工作区指令与技能目录延迟一步;bootstrapMaxTokens 封顶 phase-1 输出预算
66
+ # (1024),升态后再剥离。压缩后回落到受控阶段(双工具 + compactionTools),
67
+ # 直到新升态信号——本预设把 dispatch/subagent 加进 compactionTools,
68
+ # 让压缩后的编排者仍能继续委派。
69
+ - id: tool-bootstrap
70
+ name: ./tool-bootstrap.mjs
71
+ config:
72
+ shellTools: [bash]
73
+ commonTools: [str_replace_editor]
74
+ messageSources: [user, goal]
75
+ anchorGate: true
76
+ maxBootstrapSteps: 4
77
+ promoteAfterFirstResponse: true
78
+ bootstrapMaxTokens: 1024
79
+ compactionTools: [read, write, edit, glob, grep, todo_write, ask_user_question, dispatch, subagent]
80
+ deferredSources: [agent-instructions, skill-catalog]
81
+ deferredGraceSteps: 1
82
+ promotedPresentation: code
83
+ # 升态后以一次性非指令式提示替代 AGENTS.md 全文注入(梁神 V2 保留的
84
+ # issue #388 设定),知识经 read / skill_load 可达。设 false 恢复全文注入。
85
+ instructionHint: true
86
+
87
+ # 工作区指令在 phase 1 被 tool-bootstrap 隔离,升态后延迟一步注入并保持更新。
40
88
  - id: agent-instructions
41
89
  name: '@deepseek-ai/dsh-agent-instructions'
42
90
  config:
@@ -57,8 +105,25 @@
57
105
  name: '@deepseek-ai/dsh-tool-pwsh'
58
106
  disabled: !!js process.platform !== 'win32'
59
107
 
108
+ # win32 专用 `bash` 工具(custom-bash.mjs,同梁神 V2):官方 tool-bash 在
109
+ # win32 禁用、tool-pwsh 又不叫 bash,phase-1 的 Minimal 锚点(shellTools:
110
+ # [bash])会找不到 shell 而退化成全量目录。此工具经跨平台 subprocess 缝隙
111
+ # 执行 Git Bash(`bash -c`),保住锚点;posix 下官方 tool-bash 已提供
112
+ # 同名工具,此行走禁用。非零退出上报不抛;调用间状态不持久;Windows 无
113
+ # OS 沙箱限制。`bashPath` 可选(缺省按 Git 安装根推断)。
114
+ - id: custom-bash
115
+ name: ./custom-bash.mjs
116
+ disabled: !!js process.platform !== 'win32'
117
+
60
118
  # ── filesystem ──────────────────────────────────────────────────────────────
61
119
 
120
+ # Phase-1 编辑器(同梁神 V2):顶层直排,复用宿主沙箱 ctx.fs 而非裸本地
121
+ # 文件系统,schema 与官方 Minimal `str_replace_editor` 一致。
122
+ - id: str-replace-editor
123
+ name: '@deepseek-ai/dsh-tool-str-replace-editor'
124
+ config:
125
+ maxOutputChars: 16000
126
+
62
127
  # 两者都注册进宿主 tools 注册表且不提供服务,无需 realm;fs 服务与其策略留在宿主。
63
128
  - id: tool-fs
64
129
  name: '@deepseek-ai/dsh-tool-fs'
@@ -90,10 +155,15 @@
90
155
 
91
156
  # ── goals ───────────────────────────────────────────────────────────────────
92
157
 
93
- # 只放模型可见的工具。goal 服务、会话驱动与 /goal 命令留在宿主平面:
94
- # Gateway 以 Remote endpoint 服务 goal 域,接收方来自生成描述符,条目本地
95
- # realm 会隐藏它。注册表本就按会话键控,一个宿主实例服务所有会话;预设
96
- # 只决定其 agent 能否调用 goal 工具。
158
+ # goal 服务与会话驱动留在宿主平面,Gateway 在那里解析它们。人机命令与
159
+ # 模型可见工具注册进本预设的 scope 层。
160
+ - id: command-goal
161
+ name: '@deepseek-ai/dsh-command-goal'
162
+
163
+ # 只放模型可见的工具。goal 服务与会话驱动留在宿主平面:Gateway 以 Remote
164
+ # endpoint 服务 goal 域,接收方来自生成描述符,条目本地 realm 会隐藏它。
165
+ # 注册表本就按会话键控,一个宿主实例服务所有会话;预设只决定其 agent
166
+ # 能否调用 goal 工具。
97
167
  - id: tool-goal
98
168
  name: '@deepseek-ai/dsh-tool-goal'
99
169
 
@@ -182,6 +252,7 @@
182
252
  config:
183
253
  provider: spawn
184
254
  toolName: subagent
255
+ modelSelectionSettings: true
185
256
  backgroundMode: continuable
186
257
 
187
258
  - id: tool-subagent-fork