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.
- package/README.md +49 -20
- package/README.zh.md +102 -73
- package/docs/screenshots/dispatch-card.png +0 -0
- package/docs/screenshots/settings-page1.png +0 -0
- package/docs/screenshots/settings-page2.png +0 -0
- package/index.mjs +153 -75
- package/lib/client.js +3081 -444
- package/lib/core/adoption-reminder.mjs +48 -0
- package/lib/core/adoption-tracker.mjs +430 -0
- package/lib/core/background-ledger.mjs +71 -0
- package/lib/core/cost-evidence.mjs +145 -0
- package/lib/core/cost-guard.mjs +1 -1
- package/lib/core/decision-trace.mjs +11 -31
- package/lib/core/delegation.mjs +11 -7
- package/lib/core/dispatch-gates.mjs +12 -5
- package/lib/core/dispatch-guard.mjs +6 -0
- package/lib/core/dispatch-schema.mjs +10 -5
- package/lib/core/dispatch-tool.mjs +52 -45
- package/lib/core/draft-gates.mjs +45 -0
- package/lib/core/drafts-store.mjs +45 -0
- package/lib/core/evolution-advice.mjs +323 -0
- package/lib/core/evolution-assets.mjs +167 -0
- package/lib/core/evolution-draft.mjs +312 -0
- package/lib/core/evolution-engine.mjs +269 -0
- package/lib/core/evolution-generate.mjs +196 -0
- package/lib/core/evolution-ledger.mjs +149 -33
- package/lib/core/evolution-persistence.mjs +213 -0
- package/lib/core/evolution-renewal.mjs +64 -0
- package/lib/core/evolution-routes.mjs +109 -0
- package/lib/core/evolution-summary.mjs +46 -223
- package/lib/core/http-helpers.mjs +35 -0
- package/lib/core/http-routes.mjs +169 -115
- package/lib/core/presets-sync.mjs +254 -256
- package/lib/core/prices.mjs +46 -0
- package/lib/core/profile-directory.mjs +139 -0
- package/lib/core/profile-provider.mjs +5 -3
- package/lib/core/profiles-store.mjs +24 -6
- package/lib/core/reminder-store.mjs +172 -0
- package/lib/core/session-read.mjs +17 -0
- package/lib/core/shims.mjs +13 -2
- package/package.json +82 -82
- package/presets/orchestrator/agent.cordis.yml +243 -243
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// lib/core/profile-directory.mjs — 派发方案目录的单一数据源。
|
|
2
|
+
// profileSectionText(模型可见段)与 profileSnapshotOf(决策轨迹记录面)都从
|
|
3
|
+
// 本模块的 profileDirectoryRows 派生,构造保证「模型看到 ≡ 记录面」的 id、顺序与
|
|
4
|
+
// 字段集。未来 F 数据层注入 avg_cost / success_rate 只改这里一处。
|
|
5
|
+
//
|
|
6
|
+
// 仅依赖 lib/core/pure.mjs(tierSortKey);无 @deepseek-ai 依赖,可被 bare-CI
|
|
7
|
+
// 单测直接 import。
|
|
8
|
+
|
|
9
|
+
import { tierSortKey } from './pure.mjs';
|
|
10
|
+
|
|
11
|
+
// 决策输入快照阈值:profiles_snapshot 条目数与 description 截断长度。
|
|
12
|
+
const MAX_PROFILE_SNAPSHOT_ENTRIES = 6;
|
|
13
|
+
const PROFILE_DESC_CHARS = 80;
|
|
14
|
+
|
|
15
|
+
function snapshotEntry(row, maxDesc) {
|
|
16
|
+
const description = typeof row.description === 'string' ? row.description : '';
|
|
17
|
+
return {
|
|
18
|
+
id: row.id,
|
|
19
|
+
...(description !== '' ? { description: description.length > maxDesc ? `${description.slice(0, maxDesc)}…` : description } : {}),
|
|
20
|
+
...(typeof row.preset === 'string' && row.preset !== '' ? { preset: row.preset } : {}),
|
|
21
|
+
...(typeof row.model === 'string' && row.model !== '' ? { model: row.model } : {}),
|
|
22
|
+
...(typeof row.provider === 'string' && row.provider !== '' ? { provider: row.provider } : {}),
|
|
23
|
+
...(typeof row.tokenTier === 'string' && row.tokenTier !== '' ? { tokenTier: row.tokenTier } : {}),
|
|
24
|
+
...(typeof row.avgCost === 'number' && Number.isFinite(row.avgCost) ? { avgCost: row.avgCost } : {}),
|
|
25
|
+
...(typeof row.successRate === 'number' && Number.isFinite(row.successRate) ? { successRate: row.successRate } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 模型可见 profile 目录快照:输入为 profileDirectoryRows 的结构化行。description
|
|
30
|
+
// 截断、条目数截到 maxEntries;chosenId 超出 maxEntries 时并入该条并打
|
|
31
|
+
// chosenOutsideSnapshot(37b:截断不得制造审计空洞)。
|
|
32
|
+
export function profileSnapshotOf(profiles, { maxEntries = MAX_PROFILE_SNAPSHOT_ENTRIES, maxDesc = PROFILE_DESC_CHARS, chosenId } = {}) {
|
|
33
|
+
const list = Array.isArray(profiles) ? profiles : [];
|
|
34
|
+
const full = [];
|
|
35
|
+
for (const row of list) {
|
|
36
|
+
if (row === null || typeof row !== 'object' || typeof row.id !== 'string' || row.id === '') continue;
|
|
37
|
+
if (row.enabled === false) continue;
|
|
38
|
+
full.push(snapshotEntry(row, maxDesc));
|
|
39
|
+
}
|
|
40
|
+
const entries = full.slice(0, maxEntries);
|
|
41
|
+
let truncated = full.length > maxEntries;
|
|
42
|
+
if (typeof chosenId === 'string' && chosenId !== '' && chosenId !== '(inline)' && !entries.some((e) => e.id === chosenId)) {
|
|
43
|
+
const chosen = full.find((e) => e.id === chosenId);
|
|
44
|
+
if (chosen !== undefined) {
|
|
45
|
+
entries.push({ ...chosen, chosenOutsideSnapshot: true });
|
|
46
|
+
truncated = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { entries, truncated, total: full.length };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// store 兼容 createProfileStore 返回形态(含 profiles Map);也接受数组(测试与
|
|
53
|
+
// 非标准调用方)。只保留启用且 id 合法的方案;按 tokenTier cheap-first 稳定排序,
|
|
54
|
+
// tier 相同按 id 升序(确定性,不依赖 Map 插入序的跨运行差异)。
|
|
55
|
+
export function profileDirectoryRows(store) {
|
|
56
|
+
const source = store !== null && typeof store === 'object' && store.profiles !== undefined
|
|
57
|
+
? [...store.profiles.values()]
|
|
58
|
+
: (Array.isArray(store) ? store : []);
|
|
59
|
+
return source
|
|
60
|
+
.filter((p) => p !== null && typeof p === 'object' && typeof p.id === 'string' && p.id !== '' && p.enabled !== false)
|
|
61
|
+
.sort((a, b) => tierSortKey(a.tokenTier) - tierSortKey(b.tokenTier) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
62
|
+
.map((p) => ({
|
|
63
|
+
id: p.id,
|
|
64
|
+
description: typeof p.description === 'string' ? p.description : '',
|
|
65
|
+
...(typeof p.preset === 'string' && p.preset !== '' ? { preset: p.preset } : {}),
|
|
66
|
+
...(typeof p.model === 'string' && p.model !== '' ? { model: p.model } : {}),
|
|
67
|
+
...(typeof p.provider === 'string' && p.provider !== '' ? { provider: p.provider } : {}),
|
|
68
|
+
...(typeof p.tokenTier === 'string' && p.tokenTier !== '' ? { tokenTier: p.tokenTier } : {}),
|
|
69
|
+
...(typeof p.avgCost === 'number' && Number.isFinite(p.avgCost) ? { avgCost: p.avgCost } : {}),
|
|
70
|
+
...(typeof p.successRate === 'number' && Number.isFinite(p.successRate) ? { successRate: p.successRate } : {}),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// 从 summaries.json 的 l1/l2 聚合为每个 profile 提取 { avgCost, successRate, n }。
|
|
76
|
+
// 匹配优先级:profile.model → L2 model 轴;profile.preset → L2 preset 轴。多个 L1
|
|
77
|
+
// 组共享同一轴值时按成本与部署量合并(avg_cost = Σestimated_cost / Σpriced;
|
|
78
|
+
// successRate = Σcompleted / Σdeployments)。无数据返回 null,调用方省略显示。
|
|
79
|
+
export function profileStatsFromSummaries(store, summaries) {
|
|
80
|
+
const rows = profileDirectoryRows(store);
|
|
81
|
+
const l2 = summaries !== null && typeof summaries === 'object' && summaries.l2 !== null && typeof summaries.l2 === 'object' ? summaries.l2 : {};
|
|
82
|
+
const byModel = new Map();
|
|
83
|
+
const byPreset = new Map();
|
|
84
|
+
for (const [key, group] of Object.entries(l2)) {
|
|
85
|
+
if (group === null || typeof group !== 'object') continue;
|
|
86
|
+
const modelMatch = /:model:([^|]+)$/.exec(key);
|
|
87
|
+
if (modelMatch) {
|
|
88
|
+
const arr = byModel.get(modelMatch[1]) ?? [];
|
|
89
|
+
arr.push(group);
|
|
90
|
+
byModel.set(modelMatch[1], arr);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const presetMatch = /:preset:([^|]+)$/.exec(key);
|
|
94
|
+
if (presetMatch) {
|
|
95
|
+
const arr = byPreset.get(presetMatch[1]) ?? [];
|
|
96
|
+
arr.push(group);
|
|
97
|
+
byPreset.set(presetMatch[1], arr);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const merge = (groups) => {
|
|
101
|
+
let deployments = 0;
|
|
102
|
+
let completed = 0;
|
|
103
|
+
let priced = 0;
|
|
104
|
+
let cost = 0;
|
|
105
|
+
for (const g of groups) {
|
|
106
|
+
deployments += Number.isFinite(g.deployments_total) ? g.deployments_total : 0;
|
|
107
|
+
completed += Number.isFinite(g.outcome?.completed) ? g.outcome.completed : 0;
|
|
108
|
+
priced += Number.isFinite(g.cost?.priced) ? g.cost.priced : 0;
|
|
109
|
+
cost += Number.isFinite(g.cost?.estimated_cost) ? g.cost.estimated_cost : 0;
|
|
110
|
+
}
|
|
111
|
+
if (deployments <= 0 && priced <= 0) return null;
|
|
112
|
+
return {
|
|
113
|
+
avgCost: priced > 0 ? Number((cost / priced).toFixed(4)) : null,
|
|
114
|
+
successRate: deployments > 0 ? Number((completed / deployments).toFixed(3)) : null,
|
|
115
|
+
n: deployments,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
const out = new Map();
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
let stats = null;
|
|
121
|
+
if (typeof row.model === 'string' && row.model !== '') stats = merge(byModel.get(row.model) ?? []);
|
|
122
|
+
if (stats === null && typeof row.preset === 'string' && row.preset !== '') stats = merge(byPreset.get(row.preset) ?? []);
|
|
123
|
+
if (stats !== null) out.set(row.id, stats);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 用 stats map 为目录行补 avgCost/successRate(F 数据层 join)。stats 为
|
|
129
|
+
// profileStatsFromSummaries 的返回值;行上已有 avgCost/successRate 时优先保留
|
|
130
|
+
// 显式值(store 侧/测试注入),stats 只补缺失。
|
|
131
|
+
export function applyProfileStats(rows, stats) {
|
|
132
|
+
const map = stats instanceof Map ? stats : new Map(Object.entries(stats ?? {}));
|
|
133
|
+
return rows.map((row) => ({
|
|
134
|
+
...row,
|
|
135
|
+
...(row.avgCost === undefined && map.get(row.id)?.avgCost !== null && map.get(row.id)?.avgCost !== undefined ? { avgCost: map.get(row.id).avgCost } : {}),
|
|
136
|
+
...(row.successRate === undefined && map.get(row.id)?.successRate !== null && map.get(row.id)?.successRate !== undefined ? { successRate: map.get(row.id).successRate } : {}),
|
|
137
|
+
...(map.get(row.id)?.n !== undefined ? { n: map.get(row.id).n } : {}),
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
@@ -30,6 +30,7 @@ import { computeEffectiveAllow } from './intersection.mjs';
|
|
|
30
30
|
import { resolveWhitelist } from './whitelist.mjs';
|
|
31
31
|
import { assertCostGuard } from './cost-guard.mjs';
|
|
32
32
|
import { DELEGATION_CONTEXT, buildDispatchMeta } from './delegation.mjs';
|
|
33
|
+
import { readSessionEvents } from './session-read.mjs';
|
|
33
34
|
|
|
34
35
|
// --- start 预检段(从 start 拆出)-------------------------------------------------
|
|
35
36
|
|
|
@@ -154,13 +155,14 @@ async function setupChild(childCtx, { parent, profile, swapPreset, delegated, re
|
|
|
154
155
|
return { ...resolved, reasoningEffort: profile.reasoningEffort };
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
|
-
// ⑥ Descriptor append inside the child's first turn(
|
|
158
|
+
// ⑥ Descriptor append inside the child's first turn(descriptor 存在才 append;
|
|
159
|
+
// 核心注入的是对象(snapshotJsonValue 快照),早期版本为字符串,故只判存在不判类型)。
|
|
158
160
|
let appended = false;
|
|
159
161
|
childCtx.on('agent/pre-step', async ({ agent }, next) => {
|
|
160
162
|
const decision = await next();
|
|
161
163
|
if (!appended && decision.kind === 'enter') {
|
|
162
164
|
appended = true;
|
|
163
|
-
if (
|
|
165
|
+
if (request.descriptor !== undefined && request.descriptor !== null) agent.session.append('subagent/descriptor', request.descriptor);
|
|
164
166
|
}
|
|
165
167
|
return decision;
|
|
166
168
|
});
|
|
@@ -194,7 +196,7 @@ function restrictChildTools(childCtx, parent, profile) {
|
|
|
194
196
|
// 已取消时 result 闭包跳过 followup。
|
|
195
197
|
function wireChildLifecycle(handle, request, childId, swapPreset, profile, logger) {
|
|
196
198
|
const child = handle.agent;
|
|
197
|
-
const boundary = child.session.
|
|
199
|
+
const boundary = readSessionEvents(child.session).length;
|
|
198
200
|
const flags = { cancelled: false };
|
|
199
201
|
const onAbort = () => {
|
|
200
202
|
flags.cancelled = true;
|
|
@@ -166,12 +166,20 @@ function persistProfiles(state, logger) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
// 插件开关态(state.json):enabled(默认开,禁用即关派发工具)+ evolutionAdvice
|
|
169
|
-
//
|
|
170
|
-
// 底板,仅设置页显式 opt-in
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
// (派发优化建议注入,默认关,需显式开)+ escapeEnabled(逃生舱放行,默认关——信任
|
|
170
|
+
// 底板,仅设置页显式 opt-in 可放开)+ applyEnabled(进化候选生成与应用写路径,默认
|
|
171
|
+
// 关——应用能力只由人显式开启)+ candidateMode/candidateTtlH(候选方案更新策略与
|
|
172
|
+
// 有效期,用户可配置)。开关共用同一 state.json,读写整份对象以互不覆盖;
|
|
173
|
+
// 缺失文件回退默认值(全新部署),损坏文件 fail-closed(全关,绝不静默回退
|
|
174
|
+
// 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 };
|
|
177
|
+
|
|
178
|
+
// 候选有效期小时数归一:0(不自动过期)或 1~8760(一年内)的整数原样保留,
|
|
179
|
+
// 其余值回退默认 24 小时(读取与写入共用同一口径,防坏值落盘)。
|
|
180
|
+
function validCandidateTtlH(value) {
|
|
181
|
+
return typeof value === 'number' && Number.isInteger(value) && (value === 0 || (value >= 1 && value <= 8760)) ? value : 24;
|
|
182
|
+
}
|
|
175
183
|
|
|
176
184
|
function loadState(state) {
|
|
177
185
|
if (!existsSync(state.stateFile)) return { ...STATE_DEFAULTS };
|
|
@@ -182,6 +190,9 @@ function loadState(state) {
|
|
|
182
190
|
enabled: parsed.enabled !== false,
|
|
183
191
|
evolutionAdvice: parsed.evolutionAdvice === true,
|
|
184
192
|
escapeEnabled: parsed.escapeEnabled === true,
|
|
193
|
+
applyEnabled: parsed.applyEnabled === true,
|
|
194
|
+
candidateMode: parsed.candidateMode === 'manual' ? 'manual' : 'auto',
|
|
195
|
+
candidateTtlH: validCandidateTtlH(parsed.candidateTtlH),
|
|
185
196
|
};
|
|
186
197
|
} catch (error) {
|
|
187
198
|
// fail-closed(S2):损坏的 state.json 若回退 enabled:true,会把「安全禁用」静默撤销。
|
|
@@ -246,6 +257,13 @@ export function createProfileStore({ dshHome, logger, onGovernanceFailure = () =
|
|
|
246
257
|
persistEvolutionAdvice: (value) => persistState(state, { evolutionAdvice: value === true }, onGovernanceFailure),
|
|
247
258
|
loadEscapeEnabled: () => loadState(state).escapeEnabled,
|
|
248
259
|
persistEscapeEnabled: (value) => persistState(state, { escapeEnabled: value === true }, onGovernanceFailure),
|
|
260
|
+
loadApplyEnabled: () => loadState(state).applyEnabled,
|
|
261
|
+
persistApplyEnabled: (value) => persistState(state, { applyEnabled: value === true }, onGovernanceFailure),
|
|
262
|
+
// 候选更新策略与有效期(state.json 同源读写,坏值归一默认)。
|
|
263
|
+
loadCandidateMode: () => loadState(state).candidateMode,
|
|
264
|
+
persistCandidateMode: (value) => persistState(state, { candidateMode: value === 'manual' ? 'manual' : 'auto' }, onGovernanceFailure),
|
|
265
|
+
loadCandidateTtlH: () => loadState(state).candidateTtlH,
|
|
266
|
+
persistCandidateTtlH: (value) => persistState(state, { candidateTtlH: validCandidateTtlH(value) }, onGovernanceFailure),
|
|
249
267
|
// getAllowFailOpen:cost guard 必须在派发时读取迁移标志;
|
|
250
268
|
// loadProfiles/persistProfiles 持有该写入。
|
|
251
269
|
getAllowFailOpen: () => state.allowFailOpen,
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// lib/core/reminder-store.mjs — 提醒系统 host 侧 store + 审计同源。
|
|
2
|
+
// 一条提醒 = 一条治理审计记录(经 audit 钩子写入 governance-audit.jsonl,与 [6] 审计
|
|
3
|
+
// 同源);每个用户动作(采纳/忽略/拒绝/关闭)与「标记已读」都追加审计留痕。P0 三要件:
|
|
4
|
+
// severity 上色(P0 由 client 用错误色渲染)、常驻至已读(unread 标记,绝不自动消失)、
|
|
5
|
+
// 带 session 标识(sessionId 字段;全局异常显式 null,client 标注「全局」)。
|
|
6
|
+
// 隐私红线:只落结构字段(severity/kind/title/detail/sessionId/时间戳)与「未采纳」
|
|
7
|
+
// 条目的展示字段(子会话 id/模式、prompt 摘要 ≤200 字符、结算摘要结构、估算成本)。
|
|
8
|
+
// prompt 摘要不存原文,结算摘要无子输出正文;其余提醒类不携带这些字段(null)。
|
|
9
|
+
// 落盘 subagent-evolution/reminders.json(原子写 tmp+rename;损坏/形状不符
|
|
10
|
+
// fail-soft 从空起步)。import-free(仅 node 内置),可被裸 CI 单测。
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const SEVERITIES = new Set(['P0', 'P1', 'P2']);
|
|
16
|
+
const MAX_REMINDERS = 200;
|
|
17
|
+
|
|
18
|
+
function isRecordShape(rec) {
|
|
19
|
+
return rec !== null && typeof rec === 'object' && typeof rec.id === 'string'
|
|
20
|
+
&& typeof rec.severity === 'string' && typeof rec.kind === 'string'
|
|
21
|
+
&& typeof rec.title === 'string' && typeof rec.detail === 'string';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 结算摘要形状守卫(宽松):只接受非数组对象,具体字段逐个归一。
|
|
25
|
+
function isSettleShape(value) {
|
|
26
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeSettled(value) {
|
|
30
|
+
if (!isSettleShape(value)) return null;
|
|
31
|
+
return {
|
|
32
|
+
stopReason: typeof value.stopReason === 'string' ? value.stopReason : '',
|
|
33
|
+
elapsedMs: Number.isFinite(value.elapsedMs) ? value.elapsedMs : null,
|
|
34
|
+
...(Number.isFinite(value.childTotalTokens) ? { childTotalTokens: value.childTotalTokens } : {}),
|
|
35
|
+
...(isSettleShape(value.childUsage) ? { childUsage: value.childUsage } : {}),
|
|
36
|
+
...(typeof value.childSessionId === 'string' && value.childSessionId !== '' ? { childSessionId: value.childSessionId } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadState(stateFile, state, warn) {
|
|
41
|
+
if (stateFile === undefined) return;
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(stateFile)) return;
|
|
44
|
+
const parsed = JSON.parse(readFileSync(stateFile, 'utf8'));
|
|
45
|
+
if (parsed === null || typeof parsed !== 'object' || parsed.v !== 1 || !Array.isArray(parsed.reminders)) {
|
|
46
|
+
warn('reminder store: 提醒文件版本或形状不符,从空状态起步');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const raw of parsed.reminders) {
|
|
50
|
+
if (!isRecordShape(raw)) continue;
|
|
51
|
+
state.reminders.push({
|
|
52
|
+
id: raw.id,
|
|
53
|
+
severity: SEVERITIES.has(raw.severity) ? raw.severity : 'P2',
|
|
54
|
+
kind: raw.kind,
|
|
55
|
+
title: raw.title,
|
|
56
|
+
detail: raw.detail,
|
|
57
|
+
sessionId: typeof raw.sessionId === 'string' && raw.sessionId !== '' ? raw.sessionId : null,
|
|
58
|
+
childId: typeof raw.childId === 'string' && raw.childId !== '' ? raw.childId : null,
|
|
59
|
+
childSessionId: typeof raw.childSessionId === 'string' && raw.childSessionId !== '' ? raw.childSessionId : null,
|
|
60
|
+
childMode: typeof raw.childMode === 'string' && raw.childMode !== '' ? raw.childMode : null,
|
|
61
|
+
promptExcerpt: typeof raw.promptExcerpt === 'string' ? raw.promptExcerpt : '',
|
|
62
|
+
settled: normalizeSettled(raw.settled),
|
|
63
|
+
costEstimated: Number.isFinite(raw.costEstimated) ? raw.costEstimated : null,
|
|
64
|
+
unread: raw.unread !== false,
|
|
65
|
+
createdAt: Number.isFinite(raw.createdAt) ? raw.createdAt : 0,
|
|
66
|
+
action: typeof raw.action === 'string' ? raw.action : null,
|
|
67
|
+
actedAt: Number.isFinite(raw.actedAt) ? raw.actedAt : null,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
warn('reminder store: 提醒文件读取失败,从空状态起步(' + (error instanceof Error ? error.message : String(error)) + ')');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function persistState(stateFile, state, warn) {
|
|
76
|
+
if (stateFile === undefined) return;
|
|
77
|
+
const tmp = stateFile + '.tmp';
|
|
78
|
+
try {
|
|
79
|
+
mkdirSync(dirname(stateFile), { recursive: true });
|
|
80
|
+
writeFileSync(tmp, JSON.stringify({ v: 1, reminders: state.reminders }, null, 2), 'utf8');
|
|
81
|
+
renameSync(tmp, stateFile);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
|
|
84
|
+
warn('reminder store: 提醒文件写入失败(' + (error instanceof Error ? error.message : String(error)) + ')');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function auditReminder(audit, kind, reminder, action) {
|
|
89
|
+
audit({
|
|
90
|
+
kind,
|
|
91
|
+
reminderId: reminder.id,
|
|
92
|
+
severity: reminder.severity,
|
|
93
|
+
reminderKind: reminder.kind,
|
|
94
|
+
...(reminder.sessionId !== null ? { sessionId: reminder.sessionId } : {}),
|
|
95
|
+
...(action !== undefined ? { action } : {}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 产生一条提醒:severity 非法回退 P2;空 sessionId 归一 null(全局)。「未采纳」
|
|
100
|
+
// 条目额外携带子会话 id/模式、prompt 摘要(≤200 字符再截一次防超)、结算摘要与估算
|
|
101
|
+
// 成本;其余提醒类这些字段为空/null。创建即审计。
|
|
102
|
+
function recordReminder(state, persist, audit, { severity, kind, title, detail, sessionId, childId, childSessionId, childMode, promptExcerpt, settled, costEstimated }) {
|
|
103
|
+
const reminder = {
|
|
104
|
+
id: 'r-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
|
|
105
|
+
severity: SEVERITIES.has(severity) ? severity : 'P2',
|
|
106
|
+
kind: typeof kind === 'string' && kind !== '' ? kind : 'event',
|
|
107
|
+
title: typeof title === 'string' ? title : '',
|
|
108
|
+
detail: typeof detail === 'string' ? detail : '',
|
|
109
|
+
sessionId: typeof sessionId === 'string' && sessionId !== '' ? sessionId : null,
|
|
110
|
+
childId: typeof childId === 'string' && childId !== '' ? childId : null,
|
|
111
|
+
childSessionId: typeof childSessionId === 'string' && childSessionId !== '' ? childSessionId : null,
|
|
112
|
+
childMode: typeof childMode === 'string' && childMode !== '' ? childMode : null,
|
|
113
|
+
promptExcerpt: typeof promptExcerpt === 'string' ? promptExcerpt.slice(0, 200) : '',
|
|
114
|
+
settled: normalizeSettled(settled),
|
|
115
|
+
costEstimated: Number.isFinite(costEstimated) ? costEstimated : null,
|
|
116
|
+
unread: true,
|
|
117
|
+
createdAt: Date.now(),
|
|
118
|
+
action: null,
|
|
119
|
+
actedAt: null,
|
|
120
|
+
};
|
|
121
|
+
state.reminders.push(reminder);
|
|
122
|
+
if (state.reminders.length > state.maxReminders) state.reminders.shift();
|
|
123
|
+
persist();
|
|
124
|
+
auditReminder(audit, 'reminder-created', reminder);
|
|
125
|
+
return reminder;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 标记已读(unread=false):P0 角标常驻至已读的「已读」动作,逐条留痕。
|
|
129
|
+
function ackReminders(state, persist, audit, ids) {
|
|
130
|
+
const list = Array.isArray(ids) ? ids : [];
|
|
131
|
+
const acked = [];
|
|
132
|
+
for (const reminder of state.reminders) {
|
|
133
|
+
if (reminder.unread !== true || !list.includes(reminder.id)) continue;
|
|
134
|
+
reminder.unread = false;
|
|
135
|
+
acked.push(reminder.id);
|
|
136
|
+
auditReminder(audit, 'reminder-acked', reminder);
|
|
137
|
+
}
|
|
138
|
+
if (acked.length > 0) persist();
|
|
139
|
+
return acked;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 用户动作(采纳/忽略/拒绝/关闭):互斥终态 + 审计留痕(关闭留痕要求)。
|
|
143
|
+
function actOnReminder(state, persist, audit, id, action) {
|
|
144
|
+
const reminder = state.reminders.find((r) => r.id === id);
|
|
145
|
+
if (reminder === undefined) return false;
|
|
146
|
+
if (action !== 'adopt' && action !== 'ignore' && action !== 'reject' && action !== 'dismiss') return false;
|
|
147
|
+
reminder.action = action;
|
|
148
|
+
reminder.actedAt = Date.now();
|
|
149
|
+
reminder.unread = false;
|
|
150
|
+
persist();
|
|
151
|
+
auditReminder(audit, 'reminder-action', reminder, action);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function listReminders(state, unreadOnly) {
|
|
156
|
+
const all = [...state.reminders].reverse();
|
|
157
|
+
return unreadOnly ? all.filter((r) => r.unread === true) : all;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function createReminderStore({ dshHome, audit = () => {}, warn = () => {}, maxReminders = MAX_REMINDERS } = {}) {
|
|
161
|
+
const state = { reminders: [], maxReminders: Number.isFinite(maxReminders) && maxReminders > 0 ? maxReminders : MAX_REMINDERS };
|
|
162
|
+
const stateFile = dshHome !== undefined && dshHome !== '' ? join(dshHome, 'subagent-evolution', 'reminders.json') : undefined;
|
|
163
|
+
loadState(stateFile, state, warn);
|
|
164
|
+
const persist = () => persistState(stateFile, state, warn);
|
|
165
|
+
return {
|
|
166
|
+
record: (entry) => recordReminder(state, persist, audit, entry),
|
|
167
|
+
ack: (ids) => ackReminders(state, persist, audit, ids),
|
|
168
|
+
actOn: (id, action) => actOnReminder(state, persist, audit, id, action),
|
|
169
|
+
list: ({ unreadOnly = false } = {}) => listReminders(state, unreadOnly),
|
|
170
|
+
unreadCount: () => state.reminders.filter((r) => r.unread === true).length,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// lib/core/session-read.mjs — 会话事件统一读取口(双轨兼容)。
|
|
2
|
+
//
|
|
3
|
+
// 读取会话事件的两个不兼容环境:
|
|
4
|
+
// * 稳定线:session.events 是数组(本插件当前发布环境的形状);
|
|
5
|
+
// * 新线:session.events 属性被移除,改为按需读取 session.snapshotEvents()。
|
|
6
|
+
// 直接改成新 API 会让稳定线崩溃;直接保留旧读法会在新线崩溃。因此本 helper
|
|
7
|
+
// 逐次 feature-detect:events 数组优先,snapshotEvents() 其次,两轨都不可用
|
|
8
|
+
// 时返回空数组(fall-soft,绝不抛出)。
|
|
9
|
+
//
|
|
10
|
+
// 返回语义:稳定线分支返回 session.events 原数组引用(不拷贝),保持既有
|
|
11
|
+
// identity 语义与零开销;新线分支返回 snapshotEvents() 的返回值(默认全量快照)。
|
|
12
|
+
export function readSessionEvents(session) {
|
|
13
|
+
if (session === null || session === undefined) return [];
|
|
14
|
+
if (Array.isArray(session.events)) return session.events;
|
|
15
|
+
if (typeof session.snapshotEvents === 'function') return session.snapshotEvents();
|
|
16
|
+
return [];
|
|
17
|
+
}
|
package/lib/core/shims.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { createRequire } from 'node:module';
|
|
|
23
23
|
// 导出缺失会在 apply 能运行之前就以明确错误中止模块加载——这正是本类存在的隔离。
|
|
24
24
|
import { assertSubagentMaxDepth, resolveChildDepth } from '@deepseek-ai/dsh-subagent';
|
|
25
25
|
import { toStopReason } from './pure.mjs';
|
|
26
|
+
import { readSessionEvents } from './session-read.mjs';
|
|
26
27
|
|
|
27
28
|
// warn:模块顶层没有 ctx / logger,降级用 console.warn。
|
|
28
29
|
// 前缀让来源在共享宿主日志中可辨认。
|
|
@@ -162,7 +163,7 @@ const [foldConsumedWork, finalAssistantOutput, createUserMessage, appendDelegate
|
|
|
162
163
|
// finalAssistantOutput(最后一个非空 assistant/message,否则拼接的 text-delta
|
|
163
164
|
// 分片,再否则 undefined -> [])。
|
|
164
165
|
function readResult(child, boundary, cancelled) {
|
|
165
|
-
const own = child.session.
|
|
166
|
+
const own = readSessionEvents(child.session).slice(boundary);
|
|
166
167
|
const end = foldConsumedWork(own).end;
|
|
167
168
|
const recorded = toStopReason(end?.data.reason);
|
|
168
169
|
const stopReason = cancelled && recorded !== 'completed' ? 'aborted' : recorded;
|
|
@@ -191,6 +192,16 @@ function readPackageVersion(pkg) {
|
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
// 读本插件自身 package.json version(fail-soft 'unknown'),供维护区显示当前版本。
|
|
196
|
+
function readOwnPluginVersion() {
|
|
197
|
+
try {
|
|
198
|
+
const manifest = requirePkg('../../package.json');
|
|
199
|
+
return typeof manifest.version === 'string' && manifest.version !== '' ? manifest.version : 'unknown';
|
|
200
|
+
} catch {
|
|
201
|
+
return 'unknown';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
194
205
|
// 极简 semver 比较:major.minor.patch + 可选 `-预发布` 段(覆盖 peerDependencies
|
|
195
206
|
// 范围判断所需)。预发布 < 正式版;预发布段逐段比较,纯数字段按数值、否则按字典序
|
|
196
207
|
// (`0.1.0-rc.10` > `0.1.0-rc.6`)。
|
|
@@ -249,7 +260,7 @@ export function detectVersions(reader = readPackageVersion) {
|
|
|
249
260
|
warnings.push(`@deepseek-ai/${pkg} 版本 ${version} 超出 peerDependencies 范围(>=${PEER_MIN} <${PEER_MAX}),派发行为可能与预期不符`);
|
|
250
261
|
}
|
|
251
262
|
}
|
|
252
|
-
return { versions, warnings };
|
|
263
|
+
return { versions, warnings, pluginVersion: readOwnPluginVersion() };
|
|
253
264
|
}
|
|
254
265
|
|
|
255
266
|
// 仅供测试访问本地降级实现(包导入在这里可解析,真函数胜出;`__fallbacks`
|
package/package.json
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-subagent-profile",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"repository": {
|
|
6
|
-
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/muzyLink/dsh-subagent-profile.git"
|
|
8
|
-
},
|
|
9
|
-
"type": "module",
|
|
10
|
-
"main": "index.mjs",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
"
|
|
41
|
-
"@deepseek-ai/dsh-agent":
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"@deepseek-ai/dsh-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"@deepseek-ai/dsh-
|
|
48
|
-
"optional": true
|
|
49
|
-
},
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"optional": true
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
},
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-subagent-profile",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "DeepSeek Harness 子 Agent 派发插件:按任务为子代理选模型、推理强度与工具范围,常用组合存成命名方案随时复用;内置安全检查、成本估算与节省分析、完整决策台账。",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/muzyLink/dsh-subagent-profile.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "index.mjs",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
13
|
+
"test:bare": "node --test test/pure.test.mjs test/input-schema.test.mjs test/catalog-integrity.test.mjs",
|
|
14
|
+
"preflight": "node scripts/preflight.mjs",
|
|
15
|
+
"leak-scan": "node scripts/leak-scan.mjs"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./index.mjs",
|
|
19
|
+
"./client": "./lib/client.js",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"index.mjs",
|
|
24
|
+
"lib",
|
|
25
|
+
"cordis.patch.yml",
|
|
26
|
+
"presets",
|
|
27
|
+
"docs/screenshots",
|
|
28
|
+
"README.md",
|
|
29
|
+
"README.zh.md"
|
|
30
|
+
],
|
|
31
|
+
"dsh": {
|
|
32
|
+
"bundle": {
|
|
33
|
+
"patch": "./cordis.patch.yml"
|
|
34
|
+
},
|
|
35
|
+
"client": {
|
|
36
|
+
"inject": [],
|
|
37
|
+
"platform": "web"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@deepseek-ai/dsh-agent": ">=0.1.0-rc.6 <0.2.0",
|
|
42
|
+
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.6 <0.2.0",
|
|
43
|
+
"@deepseek-ai/dsh-subagent": ">=0.1.0-rc.6 <0.2.0",
|
|
44
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <0.2.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@deepseek-ai/dsh-agent": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@deepseek-ai/dsh-subagent": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@deepseek-ai/dsh-llm": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"@deepseek-ai/dsh-tools": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@deepseek-ai/dsh-agent": "0.1.0-rc.8",
|
|
62
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
|
63
|
+
"@deepseek-ai/dsh-subagent": "0.1.0-rc.8",
|
|
64
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.8",
|
|
65
|
+
"@eslint/js": "^10.0.1",
|
|
66
|
+
"eslint": "^10.9.0"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": "^22.19.0 || >=24"
|
|
70
|
+
},
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public"
|
|
73
|
+
},
|
|
74
|
+
"keywords": [
|
|
75
|
+
"deepseek-harness",
|
|
76
|
+
"dsh",
|
|
77
|
+
"subagent",
|
|
78
|
+
"dispatch",
|
|
79
|
+
"profiles"
|
|
80
|
+
],
|
|
81
|
+
"license": "MIT"
|
|
82
|
+
}
|