dsh-subagent-profile 0.3.2 → 0.3.4
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 +77 -40
- package/README.zh.md +111 -74
- 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 +276 -81
- package/lib/client.js +3218 -166
- 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/catalog-cache.mjs +45 -7
- package/lib/core/catalog.mjs +6 -6
- package/lib/core/cost-evidence.mjs +145 -0
- package/lib/core/cost-guard.mjs +71 -44
- package/lib/core/decision-trace.mjs +413 -0
- package/lib/core/delegation.mjs +111 -50
- package/lib/core/dispatch-gates.mjs +153 -0
- package/lib/core/dispatch-guard.mjs +156 -0
- package/lib/core/dispatch-schema.mjs +103 -14
- package/lib/core/dispatch-tool.mjs +220 -204
- package/lib/core/draft-gates.mjs +45 -0
- package/lib/core/drafts-store.mjs +45 -0
- package/lib/core/escape.mjs +130 -0
- package/lib/core/evolution-advice.mjs +224 -0
- package/lib/core/evolution-ledger.mjs +300 -0
- package/lib/core/evolution-summary.mjs +255 -0
- package/lib/core/http-routes.mjs +256 -72
- package/lib/core/intersection.mjs +6 -9
- package/lib/core/presets-sync.mjs +161 -43
- package/lib/core/prices.mjs +46 -0
- package/lib/core/profile-directory.mjs +139 -0
- package/lib/core/profile-provider.mjs +42 -39
- package/lib/core/profiles-store.mjs +103 -76
- package/lib/core/pure.mjs +110 -66
- package/lib/core/reminder-store.mjs +172 -0
- package/lib/core/shims.mjs +67 -76
- package/lib/core/whitelist.mjs +23 -17
- package/package.json +82 -83
- package/presets/orchestrator/agent.cordis.yml +59 -87
- package/presets/orchestrator/NOTICE +0 -3
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// lib/core/evolution-ledger.mjs — 派发台账(dispatch.jsonl)采集。JSONL 追加不可变:
|
|
2
|
+
// 只 appendFileSync 追加一行,绝不改写既有行。import-free(仅 node 内置
|
|
3
|
+
// fs/crypto/path + lib/core/pure.mjs),可被 bare-CI 单测直接 import。采集异常只
|
|
4
|
+
// warn + 丢失计数,绝不阻断派发(fail-soft)。
|
|
5
|
+
//
|
|
6
|
+
// 审计分级:每类写失败分别计数,计数与 health 落盘到同目录的 ledger.meta.json
|
|
7
|
+
// ({v:1, lostTelemetry, lostGovernance, health})。派发台账写失败计 lostTelemetry
|
|
8
|
+
// (fail-soft 仅告警);profile 变更/治理写失败走 markGovernanceFailure 计
|
|
9
|
+
// lostGovernance 且 health 置 degraded(高可见,设置页红字暴露)。meta 独立成域
|
|
10
|
+
// subagent-evolution/(以 dshHome 为根,不在 .agent-presets/ 下,不被 preset 同步
|
|
11
|
+
// 裁剪)。health 持久化:重启保持 degraded,直到一次 meta 写成功(成功派发触发恢复)
|
|
12
|
+
// 才回 ok。meta.json 原子写(tmp+rename),损坏 fail-soft 从零,写失败绝不抛。
|
|
13
|
+
//
|
|
14
|
+
// 隐私红线:绝不落子 Agent 原始输出 / prompt 原文 / persona 原文——每条记录只有长度
|
|
15
|
+
// (min-PII)与确定性结构指纹。persona 只存 present 布尔 + 文本结构指纹;toolFilter 只
|
|
16
|
+
// 存 present + allow/deny 数组结构指纹。parent_adopted / goal_achieved / human_edited
|
|
17
|
+
// 不入库(写入那个时刻它们尚不存在,事后回填会破坏 append-only 不可变性,只由聚合层
|
|
18
|
+
// 惰性 join)。
|
|
19
|
+
|
|
20
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { textFrom } from './pure.mjs';
|
|
24
|
+
|
|
25
|
+
const LEDGER_VERSION = 1;
|
|
26
|
+
const GOVERNANCE_AUDIT_VERSION = 1;
|
|
27
|
+
const SOURCE = 'system';
|
|
28
|
+
const ORIGIN = 'subagent';
|
|
29
|
+
|
|
30
|
+
// --- 结构指纹(确定性,输入相同恒产出相同指纹)----------------------------------
|
|
31
|
+
// 长度做导流 + sha1 前 8 区分内容:仅截断哈希可被碰撞枚举,叠加前缀长度后同时刻画
|
|
32
|
+
// 内容形状与体量,避免可枚举碰撞。不含原文。
|
|
33
|
+
function fingerprint(text) {
|
|
34
|
+
const value = String(text ?? '');
|
|
35
|
+
const hash = createHash('sha1').update(value, 'utf8').digest('hex').slice(0, 8);
|
|
36
|
+
return `L${value.length}_${hash}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// prompt 规范化结构指纹:长度 + 换行数 + sha1 前 8(内容形状标识,独缺原文)。
|
|
40
|
+
function structureFingerprint(prompt) {
|
|
41
|
+
const value = String(prompt ?? '');
|
|
42
|
+
let newlines = 0;
|
|
43
|
+
for (let i = 0; i < value.length; i += 1) if (value[i] === '\n') newlines += 1;
|
|
44
|
+
return `${fingerprint(value)}_NL${newlines}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// allow/deny 数组结构指纹:各数组的项数 + 内容 sha1(只对数组结构,不含父级配置)。
|
|
48
|
+
function toolFilterFingerprint(toolFilter) {
|
|
49
|
+
const list = (arr) => (Array.isArray(arr) ? arr : []);
|
|
50
|
+
const allow = list(toolFilter?.allow);
|
|
51
|
+
const deny = list(toolFilter?.deny);
|
|
52
|
+
const part = (label, arr) => `${label}:N${arr.length}_${fingerprint(JSON.stringify(arr))}`;
|
|
53
|
+
return `${part('allow', allow)};${part('deny', deny)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// cfg(requested/effective 双栏共用)。pending 为 merged(continuable 时部分值会被
|
|
57
|
+
// 丢弃);opts 覆盖生效栏(continuable 生效 preset 恒 'inherit'、reasoningEffort 被
|
|
58
|
+
// 宿主忽略应空置)。双栏拆分正是为捕捉「请求了但被宿主丢弃」的失真。空字段统一
|
|
59
|
+
// 归一为 null(不写 undefined,保持 JSONL 行形状稳定)。
|
|
60
|
+
function cfgOf(merged, opts = {}) {
|
|
61
|
+
const persona = typeof merged?.persona === 'string' ? merged.persona : '';
|
|
62
|
+
const hasPersona = persona.length > 0;
|
|
63
|
+
const toolFilterSet = merged?.toolFilter !== undefined && merged?.toolFilter !== null;
|
|
64
|
+
const str = (value) => (typeof value === 'string' && value.length > 0 ? value : null);
|
|
65
|
+
return {
|
|
66
|
+
preset: opts.preset ?? (typeof merged?.preset === 'string' && merged.preset !== '' ? merged.preset : 'inherit'),
|
|
67
|
+
provider: str(merged?.provider),
|
|
68
|
+
model: str(merged?.model),
|
|
69
|
+
reasoningEffort: opts.reasoningEffort !== undefined ? opts.reasoningEffort : str(merged?.reasoningEffort),
|
|
70
|
+
maxTokens: typeof merged?.maxTokens === 'number' ? merged.maxTokens : null,
|
|
71
|
+
maxDepth: typeof merged?.maxDepth === 'number' ? merged.maxDepth : null,
|
|
72
|
+
persona_present: hasPersona,
|
|
73
|
+
persona_fp: hasPersona ? fingerprint(persona) : null,
|
|
74
|
+
toolFilter_present: toolFilterSet,
|
|
75
|
+
toolFilter_fp: toolFilterSet ? toolFilterFingerprint(merged.toolFilter) : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 记录静态元数据(写入即知的共享字段,不含 ts/v——由 record 落盘时补)。三分支各自
|
|
80
|
+
// 在此基础上补 child_id 与 outcome 后调用 record。pluginVersion 由工厂闭包注入。
|
|
81
|
+
function buildBase({ parent, args, merged, mode, pluginVersion }) {
|
|
82
|
+
const prompt = typeof args?.prompt === 'string' ? args.prompt : '';
|
|
83
|
+
const effectivePreset = mode === 'continuable' ? 'inherit' : (merged?.preset ?? 'inherit');
|
|
84
|
+
// continuable 下宿主忽略 reasoningEffort(决策轨迹侧将其列入 ignored),生效栏
|
|
85
|
+
// 空置以保持「生效值」语义与聚合层读值不失真。
|
|
86
|
+
const effectiveReasoningEffort = mode === 'continuable' ? null : (merged?.reasoningEffort ?? null);
|
|
87
|
+
return {
|
|
88
|
+
// session_id 空值归一 null:它是未来聚合层惰性 join 的键,静默缺失会破坏
|
|
89
|
+
// join 且难察觉,显式 null 至少保持 JSONL 行形状稳定。
|
|
90
|
+
session_id: parent?.session?.header?.id ?? null,
|
|
91
|
+
parent_model: parent?.options?.model ?? null,
|
|
92
|
+
parent_provider: parent?.options?.provider ?? null,
|
|
93
|
+
mode,
|
|
94
|
+
origin: ORIGIN,
|
|
95
|
+
requested: cfgOf(merged),
|
|
96
|
+
effective: cfgOf(merged, { preset: effectivePreset, reasoningEffort: effectiveReasoningEffort }),
|
|
97
|
+
task: {
|
|
98
|
+
len: prompt.length,
|
|
99
|
+
structure: structureFingerprint(prompt),
|
|
100
|
+
},
|
|
101
|
+
source: SOURCE,
|
|
102
|
+
provenance: `dispatch:v${pluginVersion}`,
|
|
103
|
+
plugin_version: pluginVersion,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- outcome 组装(前台/后台结算时携带;continuable 无结算 → 不产 outcome)--------
|
|
108
|
+
|
|
109
|
+
// tool_count 为「携带 usage 的 assistant/message 事件数」(collectChildCalls 的
|
|
110
|
+
// totalCalls),是工具调用次数的可测代理而非字面计数值——宿主会话面无更直接的
|
|
111
|
+
// 工具调用计数字段;拿不到记 null。
|
|
112
|
+
function foregroundOutcome({ result, elapsedMs, prunedOutputLen, toolCount, childTotalTokens, childUsage }) {
|
|
113
|
+
const stopReason = result?.stopReason ?? 'completed';
|
|
114
|
+
const completed = stopReason === 'completed';
|
|
115
|
+
return {
|
|
116
|
+
status: completed ? 'completed' : (stopReason === 'aborted' ? 'killed' : 'failed'),
|
|
117
|
+
stop_reason: stopReason,
|
|
118
|
+
elapsed_ms: elapsedMs,
|
|
119
|
+
output_len: prunedOutputLen,
|
|
120
|
+
tool_count: completed ? toolCount : null,
|
|
121
|
+
...(typeof childTotalTokens === 'number' && Number.isFinite(childTotalTokens) ? { tokens: childTotalTokens } : {}),
|
|
122
|
+
...(childUsage !== null && typeof childUsage === 'object' ? { usage: childUsage } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function backgroundOutcome(settled) {
|
|
127
|
+
const s = settled !== null && typeof settled === 'object' ? settled : {};
|
|
128
|
+
const totalCalls = s.calls !== null && typeof s.calls === 'object' && typeof s.calls.totalCalls === 'number'
|
|
129
|
+
? s.calls.totalCalls
|
|
130
|
+
: null;
|
|
131
|
+
return {
|
|
132
|
+
status: s.status ?? 'failed',
|
|
133
|
+
stop_reason: s.stopReason ?? 'error',
|
|
134
|
+
elapsed_ms: s.elapsedMs,
|
|
135
|
+
output_len: typeof s.output === 'string' ? s.output.length : 0,
|
|
136
|
+
tool_count: totalCalls,
|
|
137
|
+
...(typeof s.childTotalTokens === 'number' && Number.isFinite(s.childTotalTokens) ? { tokens: s.childTotalTokens } : {}),
|
|
138
|
+
...(s.childUsage !== null && typeof s.childUsage === 'object' ? { usage: s.childUsage } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- 三分支 finish 助手:每条派发只加一行调用(守 dispatch-tool 行门)------------
|
|
143
|
+
|
|
144
|
+
export function finishForegroundLedger(ledger, base, childId, result, prunedBlocks, elapsedMs, childCalls, childTotalTokens, childUsage) {
|
|
145
|
+
const calls = childCalls ?? {};
|
|
146
|
+
ledger.record({
|
|
147
|
+
...base,
|
|
148
|
+
child_id: childId,
|
|
149
|
+
mode: 'foreground',
|
|
150
|
+
outcome: foregroundOutcome({
|
|
151
|
+
result,
|
|
152
|
+
elapsedMs,
|
|
153
|
+
prunedOutputLen: textFrom(prunedBlocks).length,
|
|
154
|
+
toolCount: typeof calls.totalCalls === 'number' ? calls.totalCalls : null,
|
|
155
|
+
childTotalTokens,
|
|
156
|
+
childUsage,
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function finishBackgroundLedger(ledger, base, childId, settled) {
|
|
162
|
+
ledger.record({ ...base, child_id: childId, mode: 'background', outcome: backgroundOutcome(settled) });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function finishContinuableLedger(ledger, base, childId) {
|
|
166
|
+
ledger.record({ ...base, child_id: childId, mode: 'continuable' });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- 审计 meta(ledger.meta.json)----------------------------------------------
|
|
170
|
+
|
|
171
|
+
// meta 默认态(缺失/损坏/形状不符均回退此值 → fail-soft 从零)。last_write_ts 为
|
|
172
|
+
// 最近一次成功落盘时间(旧文件无该字段时经合并回退 0)。
|
|
173
|
+
const META_DEFAULTS = { v: 1, lostTelemetry: 0, lostGovernance: 0, health: 'ok', last_write_ts: 0 };
|
|
174
|
+
|
|
175
|
+
// 读取并校验 meta.json;缺失/损坏/形状不符回退默认并 warn(fail-soft 从零)。
|
|
176
|
+
// 只接受 v:1 与必填三字段的数字/字符串形态,防止被手改的坏元数据冒充。
|
|
177
|
+
function loadAuditMeta(metaFile, warn) {
|
|
178
|
+
if (!existsSync(metaFile)) return { ...META_DEFAULTS };
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(readFileSync(metaFile, 'utf8'));
|
|
181
|
+
if (
|
|
182
|
+
parsed !== null && typeof parsed === 'object' && parsed.v === 1
|
|
183
|
+
&& typeof parsed.lostTelemetry === 'number' && Number.isFinite(parsed.lostTelemetry)
|
|
184
|
+
&& typeof parsed.lostGovernance === 'number' && Number.isFinite(parsed.lostGovernance)
|
|
185
|
+
&& (parsed.health === 'ok' || parsed.health === 'degraded')
|
|
186
|
+
) {
|
|
187
|
+
return { ...META_DEFAULTS, ...parsed };
|
|
188
|
+
}
|
|
189
|
+
warn(`ledger meta: 审计元数据形状不符,已从零重建(${metaFile})`);
|
|
190
|
+
return { ...META_DEFAULTS };
|
|
191
|
+
} catch {
|
|
192
|
+
warn(`ledger meta: 审计元数据损坏,已从零重建(${metaFile})`);
|
|
193
|
+
return { ...META_DEFAULTS };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// --- 工厂 -----------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
// 原子写 meta.json(tmp+rename)。metaState 为可写闭包状态 {dir, metaFile, lost,
|
|
200
|
+
// governanceLost, health};写成功以 healthValue 为准(恢复 ok 即在此触发)。写失败
|
|
201
|
+
// fail-soft:清理 tmp + warn、返回 false。warnOnFailure 供派发遥测失败路径关闭——
|
|
202
|
+
// 该路径已就台账写失败 warn 一次,避免重复告警。
|
|
203
|
+
function persistAuditMeta(metaState, healthValue, warn, { warnOnFailure = true } = {}) {
|
|
204
|
+
const payload = { v: 1, lostTelemetry: metaState.lost, lostGovernance: metaState.governanceLost, health: healthValue, last_write_ts: Date.now() };
|
|
205
|
+
let reported = false;
|
|
206
|
+
try {
|
|
207
|
+
mkdirSync(metaState.dir, { recursive: true });
|
|
208
|
+
const tmp = `${metaState.metaFile}.tmp`;
|
|
209
|
+
writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
210
|
+
renameSync(tmp, metaState.metaFile);
|
|
211
|
+
metaState.health = healthValue;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
try { rmSync(`${metaState.metaFile}.tmp`, { force: true }); } catch { /* best effort */ }
|
|
214
|
+
reported = true;
|
|
215
|
+
if (warnOnFailure) {
|
|
216
|
+
warn(`ledger meta: 审计元数据写入失败(${error instanceof Error ? error.message : String(error)})`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return !reported;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- 治理审计(append-only)-------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
// 治理失败统一接线:计 lostGovernance + health 置 degraded,meta 尽力落盘保证
|
|
225
|
+
// 重启仍 degraded;内存 health 无论落盘成败一律降级(写失败只 warn,不降回 ok)。
|
|
226
|
+
// ok → degraded 转变时经 onAlert 通知提醒系统(全局审计异常源)。
|
|
227
|
+
function markGovernanceFailureMeta(meta, persistMeta, onAlert) {
|
|
228
|
+
const wasOk = meta.health === 'ok';
|
|
229
|
+
meta.governanceLost += 1;
|
|
230
|
+
meta.health = 'degraded';
|
|
231
|
+
persistMeta('degraded');
|
|
232
|
+
if (wasOk && typeof onAlert === 'function') onAlert(meta.governanceLost);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 治理审计事件(放行审计等)追加一行到 governance-audit.jsonl(append-only 不可变)。
|
|
236
|
+
// 写失败绝不静默:warn + markGovernanceFailureMeta(计 lostGovernance + degraded);
|
|
237
|
+
// 写成功若正处 degraded,即为一次成功文件写 → 恢复 ok(与派发台账同口径)。
|
|
238
|
+
function appendGovernanceAudit(meta, dir, auditFile, persistMeta, warn, onAlert, entry) {
|
|
239
|
+
const line = JSON.stringify({ v: GOVERNANCE_AUDIT_VERSION, ts: Date.now(), ...entry });
|
|
240
|
+
try {
|
|
241
|
+
mkdirSync(dir, { recursive: true });
|
|
242
|
+
appendFileSync(auditFile, `${line}\n`, 'utf8');
|
|
243
|
+
if (meta.health === 'degraded') persistMeta('ok');
|
|
244
|
+
} catch (error) {
|
|
245
|
+
warn(`dispatch ledger: 治理审计事件写入失败(${error instanceof Error ? error.message : String(error)})`);
|
|
246
|
+
markGovernanceFailureMeta(meta, persistMeta, onAlert);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function createEvolutionLedger({ dshHome, pluginVersion, warn = () => {}, onAlert } = {}) {
|
|
251
|
+
if (typeof dshHome !== 'string' || dshHome.length === 0) {
|
|
252
|
+
throw new Error('dispatch ledger: 派发台账需要 dshHome 目录');
|
|
253
|
+
}
|
|
254
|
+
const dir = join(dshHome, 'subagent-evolution');
|
|
255
|
+
const file = join(dir, 'dispatch.jsonl');
|
|
256
|
+
const auditFile = join(dir, 'governance-audit.jsonl');
|
|
257
|
+
const persisted = loadAuditMeta(join(dir, 'ledger.meta.json'), warn);
|
|
258
|
+
// 可写审计状态:lost(遥测)/governanceLost/health,初始为持久化值。
|
|
259
|
+
const meta = {
|
|
260
|
+
dir,
|
|
261
|
+
metaFile: join(dir, 'ledger.meta.json'),
|
|
262
|
+
lost: persisted.lostTelemetry,
|
|
263
|
+
governanceLost: persisted.lostGovernance,
|
|
264
|
+
health: persisted.health,
|
|
265
|
+
};
|
|
266
|
+
const persistMeta = (healthValue, opts) => persistAuditMeta(meta, healthValue, warn, opts);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
baseEntry: (input) => buildBase({ ...input, pluginVersion }),
|
|
270
|
+
// 追加不可变:mkdir recursive + appendFileSync。写失败只 warn + 丢失计数 ++,
|
|
271
|
+
// 绝不抛错、绝不阻断派发。写失败计 lostTelemetry 并落盘 meta;写成功恢复 ok。
|
|
272
|
+
record(entry) {
|
|
273
|
+
try {
|
|
274
|
+
// 序列化也在 try 内(循环引用/BigInt 等理论异常会绕过 fail-soft 外泄;
|
|
275
|
+
// 实际 entry 为自产标量,防御性收口)。
|
|
276
|
+
const line = JSON.stringify({ v: LEDGER_VERSION, ts: Date.now(), ...entry });
|
|
277
|
+
mkdirSync(dir, { recursive: true });
|
|
278
|
+
appendFileSync(file, `${line}\n`, 'utf8');
|
|
279
|
+
if (meta.health === 'degraded') persistMeta('ok');
|
|
280
|
+
} catch (error) {
|
|
281
|
+
meta.lost += 1;
|
|
282
|
+
warn(`dispatch ledger: 派发台账写入失败,已丢失 ${meta.lost} 条(${error instanceof Error ? error.message : String(error)})`);
|
|
283
|
+
persistMeta(meta.health, { warnOnFailure: false });
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
recordGovernanceAudit(entry) {
|
|
287
|
+
appendGovernanceAudit(meta, dir, auditFile, persistMeta, warn, onAlert, entry);
|
|
288
|
+
},
|
|
289
|
+
// 治理审计失败(profile 变更写失败等):计 lostGovernance + health 置 degraded,
|
|
290
|
+
// 不抛、不阻断调用方;meta 尽力落盘保证重启仍 degraded。
|
|
291
|
+
markGovernanceFailure() {
|
|
292
|
+
markGovernanceFailureMeta(meta, persistMeta, onAlert);
|
|
293
|
+
},
|
|
294
|
+
// 进程内 health('ok' | 'degraded')。
|
|
295
|
+
metaHealth: () => meta.health,
|
|
296
|
+
// 设置页 summary 的 audit 字段来源:{lostTelemetry, lostGovernance, health}。
|
|
297
|
+
auditState: () => ({ lostTelemetry: meta.lost, lostGovernance: meta.governanceLost, health: meta.health }),
|
|
298
|
+
lostCount: () => meta.lost,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// lib/core/evolution-summary.mjs — 派发台账(dispatch.jsonl)的确定性双层聚合。
|
|
2
|
+
// computeSummaries 纯函数不读文件;writeSummaries 才碰 IO(派发优化建议链 readSummaries /
|
|
3
|
+
// refreshSummaries / computeAdvice / buildAdviceText 已拆至 evolution-advice.mjs 守行门)。
|
|
4
|
+
// 不变量:同输入恒同输出;聚合只读台账不写回;parent_adopted 由聚合层惰性 join。
|
|
5
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import { summarizeCosts } from './cost-evidence.mjs';
|
|
8
|
+
|
|
9
|
+
// 公式:weighted_success = (completed − 0.3·confirmedFalse) / total,下限 0,小数点 3
|
|
10
|
+
// 位。每例「已确认未被采纳」的派发(parentAdoptedConfirmedFalse)从分子扣 0.3;
|
|
11
|
+
// unknown(未决)计 0 不惩罚——只有确认 false 才有惩罚,避免窗口期未判决的派发过早
|
|
12
|
+
// 拉低分数。total = completed+failed+killed。
|
|
13
|
+
export function weightedSuccess({ completed, failed, killed, parentAdoptedConfirmedFalse = 0 }) {
|
|
14
|
+
const total = completed + failed + killed;
|
|
15
|
+
if (total <= 0) return 0;
|
|
16
|
+
const falseCount = Number.isFinite(parentAdoptedConfirmedFalse) ? parentAdoptedConfirmedFalse : 0;
|
|
17
|
+
const numerator = Math.max(0, completed - 0.3 * Math.max(0, falseCount));
|
|
18
|
+
return Number((numerator / total).toFixed(3));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// --- 采纳判定窗口(三态)------------------------------------------------------
|
|
22
|
+
// 参数化判定窗口:窗口内(尚未超过 N 轮 或 T ms)未引用一律 unknown;窗口外已确认未
|
|
23
|
+
// 引用(adoptedAt 为 null)落 false;adoptedAt 有值恒 true。N/T 待实测后校准,故均为
|
|
24
|
+
// 参数而非常量。
|
|
25
|
+
export function adoptStatus({ windowN = null, windowMs = null, dispatchedAt, adoptedAt, now, roundsElapsed = null }) {
|
|
26
|
+
if (adoptedAt !== null && adoptedAt !== undefined) return 'true';
|
|
27
|
+
const base = dispatchedAt !== null && dispatchedAt !== undefined ? dispatchedAt : now;
|
|
28
|
+
const withinMs = windowMs === null || (now - base) < windowMs;
|
|
29
|
+
const withinRounds = windowN === null || (roundsElapsed !== null ? roundsElapsed : 0) < windowN;
|
|
30
|
+
return withinMs && withinRounds ? 'unknown' : 'false';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- 置信度级别 ---------------------------------------------------------------
|
|
34
|
+
// 阈值(3/10)与 min_n_required 为建议初值,待实测后校准。
|
|
35
|
+
export function confidenceLevel(n, minN = 3, highN = 10) {
|
|
36
|
+
if (n >= highN) return 'high';
|
|
37
|
+
if (n >= minN) return 'medium';
|
|
38
|
+
return 'low';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- profile 身份与轴提取(确定性)-------------------------------------------
|
|
42
|
+
// L1 key = 生效配置重建的 profile 身份(台账不存 profileId,故从 effective cfg 重建);
|
|
43
|
+
// 无区分性配置时记 '(inline)'。能力轴(preset/provider/model/persona/toolFilter)构成
|
|
44
|
+
// 身份;预算轴 effort/maxTokens 不并入 L1 键(聚合语义修正:同一方案改预算不得被当成
|
|
45
|
+
// 不同身份——恒判「降」的混合键已废弃)。纯 effort 组与无配置组都落 '(inline)',
|
|
46
|
+
// 升/降方向改由组内实际配置轴(summarizeGroup 的 axes 字段)判定。
|
|
47
|
+
function cfgSet(cfg) {
|
|
48
|
+
return cfg !== null && typeof cfg === 'object' ? cfg : {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function profileKeyOf(cfg) {
|
|
52
|
+
const c = cfgSet(cfg);
|
|
53
|
+
const parts = [];
|
|
54
|
+
if (c.preset !== undefined && c.preset !== null && c.preset !== 'inherit') parts.push(`preset:${c.preset}`);
|
|
55
|
+
if (c.provider !== undefined && c.provider !== null) parts.push(`provider:${c.provider}`);
|
|
56
|
+
if (c.model !== undefined && c.model !== null) parts.push(`model:${c.model}`);
|
|
57
|
+
if (c.persona_present === true) parts.push('persona:1');
|
|
58
|
+
if (c.toolFilter_present === true) parts.push('toolFilter:1');
|
|
59
|
+
return parts.length > 0 ? parts.join('|') : '(inline)';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 供 AdoptionTracker 等聚合 join 复用:与 computeSummaries 的 L1 身份键完全一致。
|
|
63
|
+
export { profileKeyOf };
|
|
64
|
+
|
|
65
|
+
// 能力轴取值:preset/model/provider 取 effective 值(实数命中);persona 只存 present
|
|
66
|
+
// 布尔(隐私不落原文);toolFilter_from 取 toolFilter_present 的来源标记(requested/
|
|
67
|
+
// effective 在台账内同源,记 'requested'/'effective'/absent)。
|
|
68
|
+
function capabilityAxisKeys(profileKey, record) {
|
|
69
|
+
const eff = cfgSet(record?.effective);
|
|
70
|
+
const req = cfgSet(record?.requested);
|
|
71
|
+
const keys = [];
|
|
72
|
+
if (eff.preset !== undefined && eff.preset !== null) keys.push(`${profileKey}:preset:${eff.preset}`);
|
|
73
|
+
if (eff.model !== undefined && eff.model !== null) keys.push(`${profileKey}:model:${eff.model}`);
|
|
74
|
+
if (eff.provider !== undefined && eff.provider !== null) keys.push(`${profileKey}:provider:${eff.provider}`);
|
|
75
|
+
keys.push(`${profileKey}:persona:${eff.persona_present === true ? 'present' : 'absent'}`);
|
|
76
|
+
const from = req.toolFilter_present === true && eff.toolFilter_present === true
|
|
77
|
+
? 'requested-and-effective'
|
|
78
|
+
: (eff.toolFilter_present === true ? 'effective' : 'absent');
|
|
79
|
+
keys.push(`${profileKey}:toolFilter_from:${from}`);
|
|
80
|
+
return keys;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function budgetAxisKeys(profileKey, record) {
|
|
84
|
+
const eff = cfgSet(record?.effective);
|
|
85
|
+
const keys = [];
|
|
86
|
+
if (eff.maxTokens !== undefined && eff.maxTokens !== null) keys.push(`${profileKey}:maxTokens:${eff.maxTokens}`);
|
|
87
|
+
if (eff.reasoningEffort !== undefined && eff.reasoningEffort !== null) {
|
|
88
|
+
keys.push(`${profileKey}:reasoningEffort:${eff.reasoningEffort}`);
|
|
89
|
+
}
|
|
90
|
+
return keys;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- 单组聚合 -----------------------------------------------------------------
|
|
94
|
+
function outcomeCounts(records) {
|
|
95
|
+
const counts = { completed: 0, failed: 0, killed: 0 };
|
|
96
|
+
for (const r of records) {
|
|
97
|
+
if (r !== null && typeof r === 'object' && r.outcome && r.outcome.status === 'completed') counts.completed += 1;
|
|
98
|
+
else if (r !== null && typeof r === 'object' && r.outcome && r.outcome.status === 'failed') counts.failed += 1;
|
|
99
|
+
else if (r !== null && typeof r === 'object' && r.outcome && r.outcome.status === 'killed') counts.killed += 1;
|
|
100
|
+
}
|
|
101
|
+
return counts;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function avg(existing, value) {
|
|
105
|
+
if (value === undefined || value === null || !Number.isFinite(value)) return existing;
|
|
106
|
+
if (existing === null) return { sum: value, count: 1 };
|
|
107
|
+
return { sum: existing.sum + value, count: existing.count + 1 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function mean(acc) {
|
|
111
|
+
return acc === null || acc.count === 0 ? null : Number((acc.sum / acc.count).toFixed(2));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 组内轴存在性:能力轴(preset/provider/model/persona/toolFilter)与预算轴
|
|
115
|
+
// (reasoningEffort/maxTokens)。L1 键去 effort 后建议的升/降方向不再只依赖键文本
|
|
116
|
+
// (纯 effort 组键为 '(inline)'),改为读组内实际配置轴。
|
|
117
|
+
function groupAxes(records) {
|
|
118
|
+
let capability = false;
|
|
119
|
+
let budget = false;
|
|
120
|
+
for (const r of records) {
|
|
121
|
+
if (r === null || typeof r !== 'object') continue;
|
|
122
|
+
const eff = cfgSet(r.effective);
|
|
123
|
+
if ((eff.preset !== undefined && eff.preset !== null && eff.preset !== 'inherit')
|
|
124
|
+
|| (eff.provider !== undefined && eff.provider !== null)
|
|
125
|
+
|| (eff.model !== undefined && eff.model !== null)
|
|
126
|
+
|| eff.persona_present === true || eff.toolFilter_present === true) capability = true;
|
|
127
|
+
if ((eff.reasoningEffort !== undefined && eff.reasoningEffort !== null)
|
|
128
|
+
|| (eff.maxTokens !== undefined && eff.maxTokens !== null)) budget = true;
|
|
129
|
+
}
|
|
130
|
+
return { capability, budget };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 组装一层聚合子集(L1 与 L2 复用):outcome/score/perf/confidence/cooldown/deployments_total。
|
|
134
|
+
function summarizeGroup(records, opts = {}) {
|
|
135
|
+
const counts = outcomeCounts(records);
|
|
136
|
+
const deploymentsTotal = counts.completed + counts.failed + counts.killed;
|
|
137
|
+
const parentAdoptedConfirmedFalse = opts.parentAdoptedConfirmedFalse ?? 0;
|
|
138
|
+
const score = {
|
|
139
|
+
weighted_success: weightedSuccess({ ...counts, parentAdoptedConfirmedFalse }),
|
|
140
|
+
win_rate: deploymentsTotal > 0 ? Number((counts.completed / deploymentsTotal).toFixed(3)) : 0,
|
|
141
|
+
};
|
|
142
|
+
let elapsedAcc = null;
|
|
143
|
+
let outputAcc = null;
|
|
144
|
+
let toolAcc = null;
|
|
145
|
+
for (const r of records) {
|
|
146
|
+
if (!r || typeof r !== 'object' || !r.outcome) continue;
|
|
147
|
+
elapsedAcc = avg(elapsedAcc, r.outcome.elapsed_ms);
|
|
148
|
+
outputAcc = avg(outputAcc, r.outcome.output_len);
|
|
149
|
+
// tool_count 只对 completed 结算(见台账),且无 usage 事件时空值不计。
|
|
150
|
+
if (r.outcome.status === 'completed') toolAcc = avg(toolAcc, r.outcome.tool_count);
|
|
151
|
+
}
|
|
152
|
+
const cost = summarizeCosts(records);
|
|
153
|
+
return {
|
|
154
|
+
deployments_total: deploymentsTotal,
|
|
155
|
+
outcome: counts,
|
|
156
|
+
score,
|
|
157
|
+
perf: {
|
|
158
|
+
avg_elapsed_ms: mean(elapsedAcc),
|
|
159
|
+
avg_output_len: mean(outputAcc),
|
|
160
|
+
avg_tool_count: mean(toolAcc),
|
|
161
|
+
},
|
|
162
|
+
confidence: {
|
|
163
|
+
n: deploymentsTotal,
|
|
164
|
+
min_n_required: 3,
|
|
165
|
+
level: confidenceLevel(deploymentsTotal),
|
|
166
|
+
},
|
|
167
|
+
// 省 token 证据(38c):avg_cost 供模型侧 section 与设置页成本预览同源 join;
|
|
168
|
+
// 无价格/无 usage 的样本不进 priced,avg_cost 为 null 时调用方省略显示。
|
|
169
|
+
avg_cost: cost.priced > 0 ? Number((cost.estimated_cost / cost.priced).toFixed(4)) : null,
|
|
170
|
+
cost: {
|
|
171
|
+
estimated_cost: cost.estimated_cost,
|
|
172
|
+
estimated_inherit_cost: cost.estimated_inherit_cost,
|
|
173
|
+
estimated_saving: cost.estimated_saving,
|
|
174
|
+
priced: cost.priced,
|
|
175
|
+
inherit_count: cost.inherit_count,
|
|
176
|
+
inherit_ratio: cost.inherit_ratio,
|
|
177
|
+
},
|
|
178
|
+
// 冷却期:until_ts/last_apply_ts 由应用层(建议采纳)回填;聚合层尚无来源,置 null。
|
|
179
|
+
cooldown: { until_ts: null, last_apply_ts: null },
|
|
180
|
+
// 组内轴存在性:L1 键去 effort 后的方向判定来源(legacy 无此字段时回退键文本)。
|
|
181
|
+
axes: groupAxes(records),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- 顶层聚合(纯函数,无 IO)---------------------------------------------------
|
|
186
|
+
// records:dispatch.jsonl 逐行解析结果(调用方可先用 parseDispatchRecords 过滤损坏行)。
|
|
187
|
+
// opts.parentAdoptedConfirmedFalse:可选按聚合 key 的「已确认未采纳」计数 map,供外部
|
|
188
|
+
// 惰性 join 注入;缺省视为 0(台账内无该信号)。返回 l1/l2 普通对象。
|
|
189
|
+
export function computeSummaries(records, opts = {}) {
|
|
190
|
+
const l1Map = new Map();
|
|
191
|
+
const l2Map = new Map();
|
|
192
|
+
for (const record of records) {
|
|
193
|
+
if (record === null || typeof record !== 'object') continue;
|
|
194
|
+
const eff = cfgSet(record.effective);
|
|
195
|
+
const profileKey = profileKeyOf(eff);
|
|
196
|
+
if (!l1Map.has(profileKey)) l1Map.set(profileKey, []);
|
|
197
|
+
l1Map.get(profileKey).push(record);
|
|
198
|
+
for (const axisKey of capabilityAxisKeys(profileKey, record).concat(budgetAxisKeys(profileKey, record))) {
|
|
199
|
+
if (!l2Map.has(axisKey)) l2Map.set(axisKey, []);
|
|
200
|
+
l2Map.get(axisKey).push(record);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const falseCounts = opts.parentAdoptedConfirmedFalse ?? {};
|
|
204
|
+
const build = (map) => {
|
|
205
|
+
const out = {};
|
|
206
|
+
for (const [key, list] of map) {
|
|
207
|
+
out[key] = summarizeGroup(list, { parentAdoptedConfirmedFalse: falseCounts[key] ?? 0 });
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
};
|
|
211
|
+
return { l1: build(l1Map), l2: build(l2Map) };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- 损坏行解析(纯函数,fail-soft 跳过 + 计数)-------------------------------
|
|
215
|
+
// 逐行 JSON.parse;损坏/空行跳过并计数,不影响有效记录。
|
|
216
|
+
export function parseDispatchRecords(lines) {
|
|
217
|
+
let skipped = 0;
|
|
218
|
+
const records = [];
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
if (typeof line !== 'string' || line.trim().length === 0) {
|
|
221
|
+
skipped += 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(line);
|
|
226
|
+
records.push(parsed);
|
|
227
|
+
} catch {
|
|
228
|
+
skipped += 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { records, skipped };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --- 原子写入(tmp+rename;profiles-store 模式)------------------------------
|
|
235
|
+
// 写 <file>.tmp 后 rename 覆盖,崩溃不产生截断文件。失败不 throw,返回 {persisted,
|
|
236
|
+
// error?} 由调用方按 fail-soft 处理。何时重算由调度层(审计分层/手动触发)决定,本
|
|
237
|
+
// 模块只提供写入原语。目录缺失时 mkdir recursive 补齐。
|
|
238
|
+
export function writeSummaries(file, data) {
|
|
239
|
+
const tmp = `${file}.tmp`;
|
|
240
|
+
try {
|
|
241
|
+
// 顶层带 v 版本号(台账按行带 v 同源),改 schema 时递增。
|
|
242
|
+
const payload = { v: 1, ...data, _computed_at: Date.now() };
|
|
243
|
+
const dir = dirname(file);
|
|
244
|
+
mkdirSync(dir, { recursive: true });
|
|
245
|
+
writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
246
|
+
renameSync(tmp, file);
|
|
247
|
+
return { persisted: true };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// 尽力清理未 rename 的残留 tmp。
|
|
250
|
+
try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
|
|
251
|
+
return { persisted: false, error: error instanceof Error ? error.message : String(error) };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|