@twinsearth/dsh-master 3.2.10 → 3.3.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.
- package/README.md +4 -0
- package/client.js +50 -0
- package/dist-host/contract/client.js +7 -0
- package/dist-host/contract/constants.js +16 -2
- package/dist-host/contract/index.js +1 -0
- package/dist-host/contract/relay.js +239 -0
- package/dist-host/cost/attribution.js +183 -0
- package/dist-host/cost/audit.js +163 -0
- package/dist-host/cost/budget.js +211 -0
- package/dist-host/cost/bus.js +95 -0
- package/dist-host/cost/cert-chain.js +100 -0
- package/dist-host/cost/experiment.js +132 -0
- package/dist-host/cost/fang-relay.js +329 -0
- package/dist-host/cost/gate.js +189 -0
- package/dist-host/cost/host-surface.js +80 -0
- package/dist-host/cost/index.js +345 -0
- package/dist-host/cost/ledger-report.js +193 -0
- package/dist-host/cost/ledger.js +401 -0
- package/dist-host/cost/metrics.js +198 -0
- package/dist-host/cost/milestones.js +209 -0
- package/dist-host/cost/modules/index.js +5 -0
- package/dist-host/cost/modules/manifests.js +130 -0
- package/dist-host/cost/policy.js +276 -0
- package/dist-host/cost/report.js +156 -0
- package/dist-host/cost/reuse-report.js +128 -0
- package/dist-host/cost/rsi.js +367 -0
- package/dist-host/cost/toolface.js +147 -0
- package/dist-host/cost/types.js +31 -0
- package/dist-host/kernel.js +28 -2
- package/dist-host/plugins/cost-optimizer.js +329 -27
- package/dist-host/plugins/superagent-router.js +104 -1
- package/dist-host/superagent/types.js +8 -0
- package/dist-worker/agent-loop.js +449 -7
- package/dist-worker/baseline-fit.js +66 -0
- package/dist-worker/budget-guard.js +93 -0
- package/dist-worker/ccs.js +99 -0
- package/dist-worker/cost-hooks.js +115 -0
- package/dist-worker/cost-modules.js +330 -0
- package/dist-worker/cost-spi.js +3 -0
- package/dist-worker/edg.js +372 -0
- package/dist-worker/gdr.js +159 -0
- package/dist-worker/index.js +80 -2
- package/dist-worker/llm-http.js +20 -0
- package/dist-worker/sink-http.js +1 -0
- package/dist-worker/spill.js +115 -0
- package/dist-worker/tool-batch.js +66 -0
- package/dist-worker/tool-compact.js +275 -0
- package/dist-worker/tool-disclosure.js +90 -0
- package/dist-worker/tools.js +16 -0
- package/docs/ARCHITECTURE.md +24 -0
- package/docs/CHANGELOG.md +66 -0
- package/docs/DESIGN-v3.3.0-cost-optimization.md +1058 -0
- package/docs/EXPERIMENT-M5-fang-relay.md +117 -0
- package/docs/EXPERIMENT-M5-real-ab.md +87 -0
- package/docs/THIRD-PARTY.md +87 -0
- package/package.json +2 -2
- package/src/contract/client.js +7 -0
- package/src/contract/constants.js +17 -2
- package/src/contract/index.js +1 -0
- package/src/contract/relay.js +223 -0
- package/src/cost/attribution.ts +186 -0
- package/src/cost/audit.ts +170 -0
- package/src/cost/budget.ts +226 -0
- package/src/cost/bus.ts +97 -0
- package/src/cost/cert-chain.ts +102 -0
- package/src/cost/experiment.ts +136 -0
- package/src/cost/fang-relay.ts +318 -0
- package/src/cost/gate.ts +191 -0
- package/src/cost/host-surface.ts +97 -0
- package/src/cost/index.ts +348 -0
- package/src/cost/ledger-report.ts +186 -0
- package/src/cost/ledger.ts +385 -0
- package/src/cost/metrics.ts +193 -0
- package/src/cost/milestones.ts +206 -0
- package/src/cost/modules/index.ts +13 -0
- package/src/cost/modules/manifests.ts +129 -0
- package/src/cost/policy.ts +276 -0
- package/src/cost/report.ts +158 -0
- package/src/cost/reuse-report.ts +131 -0
- package/src/cost/rsi.ts +364 -0
- package/src/cost/toolface.ts +151 -0
- package/src/cost/types.ts +298 -0
- package/src/kernel.ts +27 -2
- package/src/plugins/cost-optimizer.ts +313 -26
- package/src/plugins/superagent-router.ts +96 -1
- package/src/superagent/types.ts +70 -0
- package/worker/agent-loop.ts +444 -7
- package/worker/baseline-fit.ts +80 -0
- package/worker/budget-guard.ts +156 -0
- package/worker/ccs.ts +202 -0
- package/worker/cost-hooks.ts +133 -0
- package/worker/cost-modules.ts +356 -0
- package/worker/cost-spi.ts +283 -0
- package/worker/edg.ts +424 -0
- package/worker/gdr.ts +233 -0
- package/worker/index.ts +84 -3
- package/worker/llm-http.ts +18 -0
- package/worker/sink-http.ts +5 -2
- package/worker/spill.ts +133 -0
- package/worker/tool-batch.ts +124 -0
- package/worker/tool-compact.ts +317 -0
- package/worker/tool-disclosure.ts +131 -0
- package/worker/tools.ts +553 -531
- package/worker/types.ts +191 -0
|
@@ -4,6 +4,45 @@ import { DEFAULT_LIMITS } from "./types.js";
|
|
|
4
4
|
import { WorkerError, WorkerLimitError, WorkerTicketError, toFailure } from "./errors.js";
|
|
5
5
|
import { buildSystemPrompt, buildUserPrompt } from "./prompt.js";
|
|
6
6
|
import { createToolRegistry } from "./tools.js";
|
|
7
|
+
import { createCostHooks, mergeCostHooks, normalizeCostUsage } from "./cost-hooks.js";
|
|
8
|
+
import { createModuleRegistry, createCompactModule } from "./cost-modules.js";
|
|
9
|
+
import { createBudgetGuard } from "./budget-guard.js";
|
|
10
|
+
import { createDisclosureState, createLoadToolsTool } from "./tool-disclosure.js";
|
|
11
|
+
import { createBatchAdvisor } from "./tool-batch.js";
|
|
12
|
+
import { createCcs } from "./ccs.js";
|
|
13
|
+
import { createGdr } from "./gdr.js";
|
|
14
|
+
import { schemaHashOf } from "./edg.js";
|
|
15
|
+
/** §6 A2:从 deps/env 读取渐进式纰漏配置(默认关闭 ⇒ 全量工具面,fail-open)。 */
|
|
16
|
+
function disclosureOptions(deps) {
|
|
17
|
+
const config = deps.disclosure;
|
|
18
|
+
if (config && typeof config === 'object')
|
|
19
|
+
return config;
|
|
20
|
+
if (typeof process === 'undefined' || !process.env)
|
|
21
|
+
return { enabled: false };
|
|
22
|
+
if (process.env.DSH_MASTER_DISCLOSURE !== 'on')
|
|
23
|
+
return { enabled: false };
|
|
24
|
+
return {
|
|
25
|
+
enabled: true,
|
|
26
|
+
allowlist: String(process.env.DSH_MASTER_DISCLOSURE_ALLOWLIST ?? '').split(',').map((item) => item.trim()).filter(Boolean),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** §5.2:从环境变量读取 ① 预算闸配置(未配置 ⇒ 关闭,沿用 3.2.x 的事后硬上限)。 */
|
|
30
|
+
function budgetOptionsFromEnv() {
|
|
31
|
+
if (typeof process === 'undefined' || !process.env)
|
|
32
|
+
return {};
|
|
33
|
+
const env = process.env;
|
|
34
|
+
const limit = Number(env.DSH_MASTER_BUDGET_TOKENS);
|
|
35
|
+
if (!Number.isFinite(limit) || limit <= 0)
|
|
36
|
+
return {};
|
|
37
|
+
const list = String(env.DSH_MASTER_BUDGET_DEGRADE_MODELS ?? '').split(',').map((item) => item.trim()).filter(Boolean);
|
|
38
|
+
return {
|
|
39
|
+
enabled: true,
|
|
40
|
+
limitTokens: Math.floor(limit),
|
|
41
|
+
onExhaust: env.DSH_MASTER_BUDGET_ON_EXHAUST === 'degrade' ? 'degrade' : 'hard-stop',
|
|
42
|
+
degradeModels: list,
|
|
43
|
+
degradeTo: env.DSH_MASTER_BUDGET_DEGRADE_TO,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
7
46
|
/**
|
|
8
47
|
* 周期检测(v3.2.7 审计收口):识别「重复序列」而不是只看连续相同。
|
|
9
48
|
*
|
|
@@ -149,6 +188,50 @@ export class SuperAgentWorker {
|
|
|
149
188
|
/** 本步是否已收到 provider 的真实流式 delta(true 时不再分块模拟)。 */
|
|
150
189
|
streamedThisStep = false;
|
|
151
190
|
registry;
|
|
191
|
+
/** 3.3.0:四个成本切点(默认转发 sink.cost;可被 deps.costHooks 覆盖/叠加)。 */
|
|
192
|
+
costHooks;
|
|
193
|
+
/** 3.3.0 模块层:A/B 类模块的唯一中介(Agent Loop 不认识具体模块)。 */
|
|
194
|
+
modules;
|
|
195
|
+
/** 本次任务的 A1 归因汇总(保留原始字符口径,供 WorkerResult 上报)。 */
|
|
196
|
+
compactionStats = { evaluated: 0, applied: 0, originalChars: 0, compactChars: 0, savedChars: 0 };
|
|
197
|
+
/** 本次任务的降级留痕(模块 → 阶段 → 最近一次事件)。 */
|
|
198
|
+
degradedEvents = new Map(); /** 实测 / 估算 token 分开累计(设计 §5.1:报表不得混算)。 */
|
|
199
|
+
measuredUsageTokens = 0;
|
|
200
|
+
estimatedUsageTokens = 0;
|
|
201
|
+
/** 设计 §5.2:① 预算闸(默认关闭,opt-in 后从「超支后终止」变为「超支前拒绝」)。 */
|
|
202
|
+
budget;
|
|
203
|
+
/** §6 A2:渐进式纰漏状态(常驻集合 + 按需加载)。 */
|
|
204
|
+
disclosure;
|
|
205
|
+
/** §6 A3:批量调用引导与形状纠偏(只追加提示,不阻断不替换)。 */
|
|
206
|
+
batch;
|
|
207
|
+
/** §7 B3:替换执行器(只有配置了原生派发器时才存在)。 */
|
|
208
|
+
ccs = null;
|
|
209
|
+
/** §7 B2:守卫匹配器(始终存在,换成空图时不命中)。 */
|
|
210
|
+
gdr = createGdr();
|
|
211
|
+
/** §7 B3:本次任务的决策图(作者步骤/模板产出)。 */
|
|
212
|
+
edgGraph = null;
|
|
213
|
+
/** §7 B3:哪些边已完成(依赖检查用)。 */
|
|
214
|
+
completedEdges = new Set();
|
|
215
|
+
/** §7 B3:本次任务发生的替换证书。 */
|
|
216
|
+
substitutions = [];
|
|
217
|
+
/** §7 B3:被跳过的慢模型回调次数。 */
|
|
218
|
+
/** I8:外部取消信号(替换路径同样受其约束)。 */
|
|
219
|
+
cancelSignal;
|
|
220
|
+
skippedModelCalls = 0;
|
|
221
|
+
/** 各工具当前 schema 哈希(B2 第②环)。 */
|
|
222
|
+
schemaHashes = {};
|
|
223
|
+
/** 替换策略:off / shadow(算证据不替换)/ enforce(白名单)。 */
|
|
224
|
+
ccsPolicy = { mode: 'off', allowed: false, reason: 'disabled' };
|
|
225
|
+
/** M3:每次守卫匹配尝试(命中与未命中都记,作为命中率分母)。 */
|
|
226
|
+
reuseAttempts = [];
|
|
227
|
+
/** M3:shadow 下算出的「本可替换」证书(不派发、不进审计链)。 */
|
|
228
|
+
shadowCertificates = [];
|
|
229
|
+
/** 计划用于替换时「跳过一次模型调用」的预留 token(归因用)。 */
|
|
230
|
+
ccsReserveTokens = 0;
|
|
231
|
+
/** degrade 档选中的廉价模型(仅本次调用生效)。 */
|
|
232
|
+
callModel;
|
|
233
|
+
/** 当前循环步号(degraded 事件需要携带,注册表回调早于 stepIndex 提升)。 */
|
|
234
|
+
currentStepIndex = 0;
|
|
152
235
|
constructor(deps) {
|
|
153
236
|
if (!deps || !deps.llm || typeof deps.llm.chat !== 'function')
|
|
154
237
|
throw new Error('SuperAgentWorker requires deps.llm.chat');
|
|
@@ -165,7 +248,70 @@ export class SuperAgentWorker {
|
|
|
165
248
|
this.clock = typeof deps.clock === 'function' ? deps.clock : () => Date.now();
|
|
166
249
|
this.model = deps.model;
|
|
167
250
|
this.redactValues = Array.isArray(deps.redactValues) ? deps.redactValues.filter((value) => typeof value === 'string') : [];
|
|
168
|
-
this.
|
|
251
|
+
this.cancelSignal = deps.signal;
|
|
252
|
+
// §6 A2:渐进式纰漏——常驻工具面收敛,高级工具按需加载(默认关闭 ⇒ 全量工具面)。
|
|
253
|
+
this.disclosure = createDisclosureState(disclosureOptions(deps));
|
|
254
|
+
this.batch = createBatchAdvisor({
|
|
255
|
+
enabled: deps.batch?.enabled !== false,
|
|
256
|
+
mode: deps.batch?.mode,
|
|
257
|
+
maxBatchPerTurn: deps.batch?.maxBatchPerTurn ?? deps.limits?.maxToolCallsPerStep ?? this.limits.maxToolCallsPerStep,
|
|
258
|
+
});
|
|
259
|
+
const toolList = this.disclosure.enabled
|
|
260
|
+
? [...this.tools, createLoadToolsTool(this.disclosure, this.tools)]
|
|
261
|
+
: this.tools;
|
|
262
|
+
this.registry = createToolRegistry(toolList);
|
|
263
|
+
// §7 B3:只有显式给了决策图 + enforce 策略时才装替换器(默认不装 ⇒ 行为与 3.2.x 一致)。
|
|
264
|
+
const ccsConfig = deps.ccs;
|
|
265
|
+
if (ccsConfig?.graph && Array.isArray(ccsConfig.graph.edges) && ccsConfig.graph.edges.length > 0) {
|
|
266
|
+
this.edgGraph = ccsConfig.graph;
|
|
267
|
+
for (const tool of toolList) {
|
|
268
|
+
// 与 B1 的 schemaHashOf 同一算法:图里的 schemaHash 才能与运行时逐字对上。
|
|
269
|
+
this.schemaHashes[tool.name] = schemaHashOf(tool.name, tool.parameters, tool.description);
|
|
270
|
+
}
|
|
271
|
+
// I3:破坏性工具默认不入池 —— 图里属于破坏性工具的边在这里被剔除(除非显式放行)。
|
|
272
|
+
if (ccsConfig.allowDestructive !== true) {
|
|
273
|
+
const kept = this.edgGraph.edges.filter((edge) => !this.registry.destructive(edge.toolName));
|
|
274
|
+
if (kept.length !== this.edgGraph.edges.length) {
|
|
275
|
+
this.edgGraph = { ...this.edgGraph, edges: kept };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
this.ccsReserveTokens = Math.max(0, Number(ccsConfig.reserveTokens ?? 0) || 0);
|
|
279
|
+
const enforce = ccsConfig.mode === 'enforce';
|
|
280
|
+
const allowlist = Array.isArray(ccsConfig.allowlist) ? ccsConfig.allowlist.map((item) => String(item)) : [];
|
|
281
|
+
const allowlisted = allowlist.length === 0 ? true : allowlist.includes(String(ccsConfig.forkId ?? this.ticketId));
|
|
282
|
+
const shadow = ccsConfig.mode === 'shadow';
|
|
283
|
+
this.ccsPolicy = enforce && allowlisted
|
|
284
|
+
? { mode: 'enforce', allowed: true, reason: 'enforce' }
|
|
285
|
+
: shadow
|
|
286
|
+
? { mode: 'shadow', allowed: false, reason: 'shadow-only' }
|
|
287
|
+
: { mode: 'off', allowed: false, reason: enforce ? 'allowlist-miss' : 'not-enforce' };
|
|
288
|
+
}
|
|
289
|
+
this.costHooks = mergeCostHooks(createCostHooks({ sink: this.sink, clock: this.clock }), deps.costHooks);
|
|
290
|
+
const compactMode = deps.compactMode ?? (typeof process !== 'undefined' ? process.env.DSH_MASTER_COMPACT_MODE : undefined);
|
|
291
|
+
this.budget = createBudgetGuard(deps.budget ?? budgetOptionsFromEnv());
|
|
292
|
+
// §7 B3:替换器必须在 budget 就绪之后创建(它要在派发前过预算闸)。
|
|
293
|
+
if (this.ccsPolicy.allowed) {
|
|
294
|
+
this.ccs = createCcs({
|
|
295
|
+
dispatcher: { execute: (call, context) => this.registry.execute(call, context) },
|
|
296
|
+
budget: this.budget,
|
|
297
|
+
clock: this.clock,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
this.modules = createModuleRegistry({
|
|
301
|
+
modules: [createCompactModule(compactMode, { workspace: this.workspace }), ...(deps.costModules ?? [])],
|
|
302
|
+
// 目标 2.1-5:模块异常必须留痕(degraded 事件),且不中断 Agent Loop。
|
|
303
|
+
onDegraded: (event) => {
|
|
304
|
+
this.degradedEvents.set(`${event.module}|${event.phase}`, { ...event });
|
|
305
|
+
this.costHooks.degraded({
|
|
306
|
+
ticketId: this.ticketId,
|
|
307
|
+
stepIndex: this.currentStepIndex,
|
|
308
|
+
module: event.module,
|
|
309
|
+
phase: event.phase,
|
|
310
|
+
message: event.message,
|
|
311
|
+
count: event.count,
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
});
|
|
169
315
|
}
|
|
170
316
|
redact(text) {
|
|
171
317
|
return redactSecrets(text, this.redactValues);
|
|
@@ -177,6 +323,12 @@ export class SuperAgentWorker {
|
|
|
177
323
|
this.tokenUsage = 0;
|
|
178
324
|
this.finalAnswer = '';
|
|
179
325
|
this.prDelivery = undefined;
|
|
326
|
+
this.compactionStats = { evaluated: 0, applied: 0, originalChars: 0, compactChars: 0, savedChars: 0 };
|
|
327
|
+
this.degradedEvents.clear();
|
|
328
|
+
this.measuredUsageTokens = 0;
|
|
329
|
+
this.estimatedUsageTokens = 0;
|
|
330
|
+
this.callModel = undefined;
|
|
331
|
+
this.currentStepIndex = 0;
|
|
180
332
|
let ticket;
|
|
181
333
|
try {
|
|
182
334
|
ticket = normalizeTicket(input);
|
|
@@ -202,6 +354,11 @@ export class SuperAgentWorker {
|
|
|
202
354
|
{ role: 'system', content: buildSystemPrompt(ticket) },
|
|
203
355
|
{ role: 'user', content: buildUserPrompt(ticket) },
|
|
204
356
|
];
|
|
357
|
+
// §6 A3:首轮一次教学——写进**历史**(不是系统提示词),之后不再重复。
|
|
358
|
+
if (this.batch.status().state === 'active' && !this.batch.taught()) {
|
|
359
|
+
this.history.push({ role: 'user', content: this.batch.guidance() });
|
|
360
|
+
this.batch.markTaught();
|
|
361
|
+
}
|
|
205
362
|
try {
|
|
206
363
|
this.setPhase('initializing');
|
|
207
364
|
this.record({ type: 'phase', content: 'initializing' });
|
|
@@ -223,9 +380,33 @@ export class SuperAgentWorker {
|
|
|
223
380
|
throw new WorkerLimitError('max-steps', `Worker 超出最大循环次数 (${this.limits.maxSteps}),判定为死循环,强制终止。`);
|
|
224
381
|
}
|
|
225
382
|
stepIndex += 1;
|
|
383
|
+
this.currentStepIndex = stepIndex;
|
|
226
384
|
this.setPhase('thinking');
|
|
227
385
|
this.log(`[Worker] === 循环 Step ${stepIndex}/${this.limits.maxSteps} ===`);
|
|
228
386
|
this.assertContextBudget();
|
|
387
|
+
const modelStartedAt = this.clock();
|
|
388
|
+
// 设计 §5.2:预算检查点**只在 ①**,且 fail-closed(付不起就不启动调用)。
|
|
389
|
+
const budgetDecision = this.budget.beforeModelCall({
|
|
390
|
+
model: this.model,
|
|
391
|
+
historyChars: JSON.stringify(this.history).length,
|
|
392
|
+
stepIndex,
|
|
393
|
+
});
|
|
394
|
+
if (!budgetDecision.ok) {
|
|
395
|
+
throw new WorkerLimitError('budget-exhausted', budgetDecision.message ?? '预算不足,拒绝启动模型调用');
|
|
396
|
+
}
|
|
397
|
+
this.callModel = budgetDecision.degraded === true && budgetDecision.model ? budgetDecision.model : undefined;
|
|
398
|
+
this.costHooks.beforeModelCall({
|
|
399
|
+
ticketId: this.ticketId,
|
|
400
|
+
stepIndex,
|
|
401
|
+
model: this.callModel ?? this.model,
|
|
402
|
+
historyChars: JSON.stringify(this.history).length,
|
|
403
|
+
});
|
|
404
|
+
this.modules.beforeModelCall({
|
|
405
|
+
ticketId: this.ticketId,
|
|
406
|
+
stepIndex,
|
|
407
|
+
model: this.callModel ?? this.model,
|
|
408
|
+
historyChars: JSON.stringify(this.history).length,
|
|
409
|
+
});
|
|
229
410
|
const response = await this.callLLM();
|
|
230
411
|
const rawContent = String(response.message?.content ?? '');
|
|
231
412
|
if (rawContent.length > this.limits.maxContextChars) {
|
|
@@ -233,11 +414,32 @@ export class SuperAgentWorker {
|
|
|
233
414
|
}
|
|
234
415
|
const usage = response.usage;
|
|
235
416
|
let used = Number(usage?.totalTokens ?? ((usage?.promptTokens ?? 0) + (usage?.completionTokens ?? 0))) || 0;
|
|
417
|
+
let usedEstimated = false;
|
|
236
418
|
if (used <= 0) {
|
|
237
|
-
// provider 不返回 usage 时的兜底估算,防止 Token
|
|
419
|
+
// provider 不返回 usage 时的兜底估算,防止 Token 预算被绕过;报表里必须标 estimated。
|
|
238
420
|
used = Math.ceil((JSON.stringify(this.history).length + rawContent.length) / 4);
|
|
421
|
+
usedEstimated = true;
|
|
239
422
|
}
|
|
240
423
|
this.tokenUsage += used;
|
|
424
|
+
if (usedEstimated)
|
|
425
|
+
this.estimatedUsageTokens += used;
|
|
426
|
+
else
|
|
427
|
+
this.measuredUsageTokens += used;
|
|
428
|
+
const normalized = normalizeCostUsage(usage);
|
|
429
|
+
this.costHooks.afterModelResponse({
|
|
430
|
+
ticketId: this.ticketId,
|
|
431
|
+
stepIndex,
|
|
432
|
+
usage: normalized.total > 0 ? normalized : { input: 0, output: 0, total: used },
|
|
433
|
+
latencyMs: Math.max(0, this.clock() - modelStartedAt),
|
|
434
|
+
estimated: usedEstimated,
|
|
435
|
+
});
|
|
436
|
+
this.modules.afterModelResponse({
|
|
437
|
+
ticketId: this.ticketId,
|
|
438
|
+
stepIndex,
|
|
439
|
+
usage: normalized.total > 0 ? normalized : { input: 0, output: 0, total: used },
|
|
440
|
+
latencyMs: Math.max(0, this.clock() - modelStartedAt),
|
|
441
|
+
estimated: usedEstimated,
|
|
442
|
+
});
|
|
241
443
|
if (this.tokenUsage > this.limits.maxTokens) {
|
|
242
444
|
throw new WorkerLimitError('token-budget-exceeded', `超出最大 Token 预算 (${this.limits.maxTokens}),强制终止。`);
|
|
243
445
|
}
|
|
@@ -252,6 +454,12 @@ export class SuperAgentWorker {
|
|
|
252
454
|
this.record({ type: 'thought', content: assistantMessage.content });
|
|
253
455
|
}
|
|
254
456
|
const toolCalls = validateToolCalls(response);
|
|
457
|
+
// §6 A3:只做形状纠偏(追加提示),从不阻断、从不改写模型程序。
|
|
458
|
+
const batchReview = this.batch.review(toolCalls);
|
|
459
|
+
if (batchReview.correction) {
|
|
460
|
+
this.history.push({ role: 'user', content: batchReview.correction });
|
|
461
|
+
this.log(`[Worker] A3 批量纠偏: ${batchReview.reason}`);
|
|
462
|
+
}
|
|
255
463
|
if (toolCalls.length === 0) {
|
|
256
464
|
this.finalAnswer = String(assistantMessage.content ?? '').trim() || '任务执行完成,无返回内容。';
|
|
257
465
|
this.setPhase('reporting');
|
|
@@ -295,8 +503,71 @@ export class SuperAgentWorker {
|
|
|
295
503
|
this.record({ type: 'tool_call', content: call.args, toolName: call.name, toolArgs: safeParseArgs(call.args) });
|
|
296
504
|
this.setPhase('executing_tool');
|
|
297
505
|
this.stream(undefined, call.name);
|
|
298
|
-
|
|
299
|
-
|
|
506
|
+
this.costHooks.beforeToolDispatch({
|
|
507
|
+
ticketId: this.ticketId,
|
|
508
|
+
stepIndex,
|
|
509
|
+
toolName: call.name,
|
|
510
|
+
argsChars: String(call.args ?? '').length,
|
|
511
|
+
});
|
|
512
|
+
// ③ 派发前:A 类模块可改写工具名/参数,或在 enforce 档屏蔽该工具。
|
|
513
|
+
const dispatch = this.modules.beforeToolDispatch({
|
|
514
|
+
ticketId: this.ticketId,
|
|
515
|
+
stepIndex,
|
|
516
|
+
call: { id: call.id, name: call.name, args: safeParseArgs(call.args) ?? {} },
|
|
517
|
+
});
|
|
518
|
+
let result;
|
|
519
|
+
let dispatchedName = call.name;
|
|
520
|
+
if (dispatch.hidden) {
|
|
521
|
+
// 屏蔽:不执行工具,回一条结构化拒绝给模型(可读、可恢复、不进死循环判定)。
|
|
522
|
+
result = `【模块屏蔽】${dispatch.hiddenBy} 已隐藏工具 ${call.name}(${dispatch.reason});请改用其它可用工具或直接给出结论。`;
|
|
523
|
+
this.record({ type: 'tool_result', content: result, toolName: call.name });
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
dispatchedName = dispatch.call.name;
|
|
527
|
+
result = await this.executeTool(dispatchedName === call.name
|
|
528
|
+
? call
|
|
529
|
+
: { ...call, name: dispatchedName, args: JSON.stringify(dispatch.call.args ?? {}) });
|
|
530
|
+
}
|
|
531
|
+
const transformed = this.modules.afterToolReceipt({
|
|
532
|
+
ticketId: this.ticketId,
|
|
533
|
+
stepIndex,
|
|
534
|
+
toolName: dispatchedName,
|
|
535
|
+
args: safeParseArgs(call.args),
|
|
536
|
+
text: result,
|
|
537
|
+
});
|
|
538
|
+
const toolResultText = transformed.text;
|
|
539
|
+
const a1 = transformed.attributions.find((item) => item.module === 'a1-compact');
|
|
540
|
+
if (a1) {
|
|
541
|
+
const stats = this.compactionStats;
|
|
542
|
+
stats.evaluated += 1;
|
|
543
|
+
stats.originalChars += a1.originalChars;
|
|
544
|
+
stats.compactChars += a1.applied ? a1.resultChars : a1.originalChars;
|
|
545
|
+
stats.savedChars += a1.applied ? Math.max(0, a1.savedChars) : 0;
|
|
546
|
+
if (a1.applied)
|
|
547
|
+
stats.applied += 1;
|
|
548
|
+
}
|
|
549
|
+
{
|
|
550
|
+
const potentialChars = a1 ? Math.max(0, Number(a1.meta?.wouldSaveChars ?? a1.savedChars) || 0) : 0;
|
|
551
|
+
this.costHooks.afterToolReceipt({
|
|
552
|
+
ticketId: this.ticketId,
|
|
553
|
+
stepIndex,
|
|
554
|
+
toolName: call.name,
|
|
555
|
+
resultChars: toolResultText.length,
|
|
556
|
+
truncated: toolResultText.length > this.limits.maxToolResultChars,
|
|
557
|
+
compaction: {
|
|
558
|
+
module: 'a1-compact',
|
|
559
|
+
mode: a1?.mode ?? 'off',
|
|
560
|
+
applicable: Boolean(a1),
|
|
561
|
+
applied: a1?.applied === true,
|
|
562
|
+
originalChars: result.length,
|
|
563
|
+
compactChars: toolResultText.length,
|
|
564
|
+
// shadow 时这里是「本可节省」;是否真的省下由 applied 表示(账本只认 applied)。
|
|
565
|
+
savedChars: potentialChars,
|
|
566
|
+
estimatedTokens: potentialChars > 0 ? Math.ceil(potentialChars / 4) : 0,
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
const safeResult = this.redact(toolResultText);
|
|
300
571
|
const truncated = safeResult.length > this.limits.maxToolResultChars
|
|
301
572
|
? safeResult.slice(0, this.limits.maxToolResultChars) + `\n…[工具结果截断,原长 ${safeResult.length} 字符]`
|
|
302
573
|
: safeResult;
|
|
@@ -309,6 +580,13 @@ export class SuperAgentWorker {
|
|
|
309
580
|
});
|
|
310
581
|
this.setPhase('thinking');
|
|
311
582
|
}
|
|
583
|
+
// §7 B3:尝试用已存决策图替代「下一次慢模型回调」。
|
|
584
|
+
// 只有 enforce + 策略放行 + GDR 命中 + 派发成功才真的跳过;任何失败都回落慢模型。
|
|
585
|
+
const substituted = await this.trySubstitute(stepIndex);
|
|
586
|
+
if (substituted) {
|
|
587
|
+
this.setPhase('thinking');
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
312
590
|
// 循环继续:带着 tool 结果再次询问模型。
|
|
313
591
|
}
|
|
314
592
|
}
|
|
@@ -320,7 +598,109 @@ export class SuperAgentWorker {
|
|
|
320
598
|
return this.result(ticket.ticketId, false, failure);
|
|
321
599
|
}
|
|
322
600
|
}
|
|
323
|
-
/**
|
|
601
|
+
/** §7 B3:用决策图替代一次慢模型回调;成功返回 true(已注入历史),失败返回 false。 */
|
|
602
|
+
async trySubstitute(stepIndex) {
|
|
603
|
+
// M3:shadow 也做守卫匹配并产出「本可节省」证据,但**从不替换**。
|
|
604
|
+
if (!this.edgGraph || this.ccsPolicy.mode === 'off')
|
|
605
|
+
return false;
|
|
606
|
+
const lastTool = [...this.steps].reverse().find((step) => step.type === 'tool_result');
|
|
607
|
+
if (!lastTool || !lastTool.toolName)
|
|
608
|
+
return false;
|
|
609
|
+
const receipt = {
|
|
610
|
+
toolName: lastTool.toolName,
|
|
611
|
+
args: lastTool.toolArgs ?? {},
|
|
612
|
+
text: String(lastTool.content ?? ''),
|
|
613
|
+
digest: `sha256:${this.currentStepIndex.toString(36)}:${String(lastTool.content ?? '').length.toString(36)}`,
|
|
614
|
+
stepIndex,
|
|
615
|
+
ticketId: this.ticketId,
|
|
616
|
+
};
|
|
617
|
+
const guard = this.gdr.match({
|
|
618
|
+
receipt,
|
|
619
|
+
graph: this.edgGraph,
|
|
620
|
+
completed: [...this.completedEdges],
|
|
621
|
+
schemaHashOf: (toolName) => this.schemaHashes[toolName],
|
|
622
|
+
estimateSavedTokens: () => this.ccsReserveTokens,
|
|
623
|
+
});
|
|
624
|
+
// 每次复用尝试都留痕(命中与未命中都算分母)。
|
|
625
|
+
this.reuseAttempts.push({
|
|
626
|
+
toolName: receipt.toolName,
|
|
627
|
+
hit: guard.hit === true,
|
|
628
|
+
reason: guard.reason ?? null,
|
|
629
|
+
stage: guard.stage ?? null,
|
|
630
|
+
edgeId: guard.edgeId ?? null,
|
|
631
|
+
wouldSaveTokens: guard.hit === true ? Math.max(0, Number(guard.wouldSaveTokens ?? this.ccsReserveTokens) || 0) : 0,
|
|
632
|
+
stepIndex,
|
|
633
|
+
});
|
|
634
|
+
if (!guard.hit || !guard.edgeId || !guard.args)
|
|
635
|
+
return false;
|
|
636
|
+
const edge = this.edgGraph.edges.find((item) => item.id === guard.edgeId);
|
|
637
|
+
if (!edge)
|
|
638
|
+
return false;
|
|
639
|
+
const replacedCallbackId = `cb_${this.ticketId}_${stepIndex}_${guard.edgeId}`;
|
|
640
|
+
// M3 shadow:只产证书,不派发、不跳过模型调用(证书标 dispatched:false,不进审计链)。
|
|
641
|
+
if (this.ccsPolicy.mode !== 'enforce' || !this.ccs) {
|
|
642
|
+
this.shadowCertificates.push({
|
|
643
|
+
edgeId: edge.id,
|
|
644
|
+
toolName: edge.toolName,
|
|
645
|
+
schemaHash: edge.schemaHash,
|
|
646
|
+
guardExpr: edge.guard.expr,
|
|
647
|
+
receiptDigest: receipt.digest,
|
|
648
|
+
replacedCallbackId,
|
|
649
|
+
viaNativeDispatcher: true,
|
|
650
|
+
taskId: this.ticketId,
|
|
651
|
+
step: stepIndex,
|
|
652
|
+
dispatched: false,
|
|
653
|
+
error: 'shadow-not-dispatched',
|
|
654
|
+
savedTokens: Math.max(0, Number(guard.wouldSaveTokens ?? this.ccsReserveTokens) || 0),
|
|
655
|
+
moduleId: 'b3-ccs',
|
|
656
|
+
at: this.clock(),
|
|
657
|
+
});
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
const controller = new AbortController();
|
|
661
|
+
const abort = () => controller.abort();
|
|
662
|
+
if (this.cancelSignal) {
|
|
663
|
+
if (this.cancelSignal.aborted)
|
|
664
|
+
controller.abort();
|
|
665
|
+
else
|
|
666
|
+
this.cancelSignal.addEventListener('abort', abort, { once: true });
|
|
667
|
+
}
|
|
668
|
+
const result = await this.ccs.substitute({
|
|
669
|
+
edge,
|
|
670
|
+
args: guard.args,
|
|
671
|
+
receipt,
|
|
672
|
+
replacedCallbackId,
|
|
673
|
+
taskId: this.ticketId,
|
|
674
|
+
step: stepIndex,
|
|
675
|
+
workspace: this.workspace,
|
|
676
|
+
mandate: { ...this.effectiveMandate },
|
|
677
|
+
signal: controller.signal,
|
|
678
|
+
timeoutMs: this.limits.toolTimeoutMs,
|
|
679
|
+
savedTokens: this.ccsReserveTokens,
|
|
680
|
+
});
|
|
681
|
+
this.substitutions.push(result.certificate);
|
|
682
|
+
if (!result.ok || result.fallback || result.text === null) {
|
|
683
|
+
// 失败 ⇒ 记账留痕,然后**回落慢模型**(不吞错继续)。
|
|
684
|
+
this.log(`[Worker] B3 替换失败(${result.certificate.error ?? 'unknown'}),回落慢模型`);
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
this.completedEdges.add(edge.id);
|
|
688
|
+
this.skippedModelCalls += 1;
|
|
689
|
+
// 注入「这次调用本来会由模型发出」的两条消息,从而跳过这次模型推理。
|
|
690
|
+
this.history.push({
|
|
691
|
+
role: 'assistant',
|
|
692
|
+
content: '',
|
|
693
|
+
tool_calls: [{ id: replacedCallbackId, type: 'function', function: { name: edge.toolName, arguments: JSON.stringify(guard.args) } }],
|
|
694
|
+
});
|
|
695
|
+
const safe = this.redact(result.text);
|
|
696
|
+
this.history.push({ role: 'tool', tool_call_id: replacedCallbackId, name: edge.toolName, content: safe });
|
|
697
|
+
this.record({ type: 'tool_result', content: safe, toolName: edge.toolName });
|
|
698
|
+
try {
|
|
699
|
+
this.sink?.cost?.({ point: 'afterToolReceipt', at: this.clock(), ticketId: this.ticketId, stepIndex, toolName: edge.toolName, resultChars: safe.length, truncated: false });
|
|
700
|
+
}
|
|
701
|
+
catch { /* ignore */ }
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
324
704
|
getLatestStep() {
|
|
325
705
|
return this.steps.length > 0 ? this.steps[this.steps.length - 1] : null;
|
|
326
706
|
}
|
|
@@ -346,6 +726,67 @@ export class SuperAgentWorker {
|
|
|
346
726
|
tokenUsage: this.tokenUsage,
|
|
347
727
|
elapsedMs: Math.max(0, this.clock() - this.startedAt),
|
|
348
728
|
...(this.prDelivery ? { pr: this.prDelivery } : {}),
|
|
729
|
+
cost: {
|
|
730
|
+
compaction: {
|
|
731
|
+
mode: this.modules.describe().find((item) => item.id === 'a1-compact')?.mode ?? 'off',
|
|
732
|
+
...this.compactionStats,
|
|
733
|
+
estimatedTokens: this.compactionStats.savedChars > 0 ? Math.ceil(this.compactionStats.savedChars / 4) : 0,
|
|
734
|
+
},
|
|
735
|
+
...(this.degradedEvents.size > 0
|
|
736
|
+
? {
|
|
737
|
+
degraded: {
|
|
738
|
+
count: [...this.degradedEvents.values()].reduce((sum, event) => sum + event.count, 0),
|
|
739
|
+
events: [...this.degradedEvents.values()].map((event) => ({ ...event })),
|
|
740
|
+
},
|
|
741
|
+
}
|
|
742
|
+
: {}),
|
|
743
|
+
tokens: {
|
|
744
|
+
measured: this.measuredUsageTokens,
|
|
745
|
+
estimated: this.estimatedUsageTokens,
|
|
746
|
+
},
|
|
747
|
+
disclosure: (() => {
|
|
748
|
+
const stats = this.disclosure.stats();
|
|
749
|
+
const visible = this.disclosure.visible(this.registry.names());
|
|
750
|
+
return {
|
|
751
|
+
enabled: this.disclosure.enabled,
|
|
752
|
+
residentCount: this.disclosure.enabled ? visible.length : this.registry.names().length,
|
|
753
|
+
loaded: stats.loaded,
|
|
754
|
+
loadEvents: stats.loadEvents,
|
|
755
|
+
schemaTokens: stats.schemaTokens,
|
|
756
|
+
schemaCount: stats.schemaCount,
|
|
757
|
+
};
|
|
758
|
+
})(),
|
|
759
|
+
batch: (() => {
|
|
760
|
+
const status = this.batch.status();
|
|
761
|
+
const stats = this.batch.stats();
|
|
762
|
+
return { state: status.state, reason: status.reason, mode: status.mode, taught: stats.taught, corrections: stats.corrections };
|
|
763
|
+
})(),
|
|
764
|
+
ccs: (() => {
|
|
765
|
+
const guardStats = this.gdr.stats();
|
|
766
|
+
return {
|
|
767
|
+
mode: this.ccsPolicy.mode,
|
|
768
|
+
skippedModelCalls: this.skippedModelCalls,
|
|
769
|
+
attempts: this.reuseAttempts.map((attempt) => ({ ...attempt })),
|
|
770
|
+
shadowCertificates: this.shadowCertificates.map((cert) => ({ ...cert })),
|
|
771
|
+
substitutions: this.substitutions.map((cert) => ({ ...cert })),
|
|
772
|
+
guard: { matches: guardStats.matches, hits: guardStats.hits, hitRate: guardStats.hitRate, byReason: { ...guardStats.byReason } },
|
|
773
|
+
};
|
|
774
|
+
})(),
|
|
775
|
+
...(() => {
|
|
776
|
+
// §6:工具面常驻成本 = 模块自报 + 实际注入 schema(chars/4)。
|
|
777
|
+
const moduleCost = this.modules.promptCost();
|
|
778
|
+
const schemas = this.registry.schemas();
|
|
779
|
+
const schemaChars = JSON.stringify(schemas).length;
|
|
780
|
+
return {
|
|
781
|
+
toolPrompt: {
|
|
782
|
+
moduleTokens: moduleCost.toolTokens,
|
|
783
|
+
moduleSchemas: moduleCost.schemaCount,
|
|
784
|
+
schemaTokens: Math.ceil(schemaChars / 4),
|
|
785
|
+
schemaCount: schemas.length,
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
})(),
|
|
789
|
+
},
|
|
349
790
|
error,
|
|
350
791
|
};
|
|
351
792
|
}
|
|
@@ -372,9 +813,10 @@ export class SuperAgentWorker {
|
|
|
372
813
|
}, this.limits.llmTimeoutMs);
|
|
373
814
|
});
|
|
374
815
|
const request = {
|
|
375
|
-
model: this.model,
|
|
816
|
+
model: this.callModel ?? this.model,
|
|
376
817
|
messages: this.history,
|
|
377
|
-
|
|
818
|
+
// A2:只注入当前可见的工具 schema(常驻 + 已加载);关闭时不做任何过滤。
|
|
819
|
+
tools: this.registry.schemas().filter((schema) => this.disclosure.visible(this.registry.names()).includes(schema.name)),
|
|
378
820
|
signal: controller.signal,
|
|
379
821
|
};
|
|
380
822
|
try {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
/**
|
|
4
|
+
* 3.3.0 · A1 归因拟合式(设计 §5.4,**冻结参数**)。
|
|
5
|
+
*
|
|
6
|
+
* 归因算法(工具面):
|
|
7
|
+
*
|
|
8
|
+
* savedTokens = baselineOutputTokens(toolName, 原始长度) − actualOutputTokens(紧凑长度)
|
|
9
|
+
*
|
|
10
|
+
* `baselineOutputTokens` 由本文件的**固定拟合式**给出,参数在
|
|
11
|
+
* `docs/DESIGN-v3.3.0-cost-optimization.md` §5.4.1 冻结;**变更参数必须同步升版本号
|
|
12
|
+
* 并在 CHANGELOG 声明**,否则 `test/worker-baseline-fit.test.ts` 会失败。
|
|
13
|
+
*
|
|
14
|
+
* 公式(v1):
|
|
15
|
+
*
|
|
16
|
+
* baseline = ceil((chars + overheadChars[toolName]) / charsPerToken)
|
|
17
|
+
* actual = ceil(chars / charsPerToken)
|
|
18
|
+
*
|
|
19
|
+
* 其中 overhead 只补偿工具回执的**结构性固定开销**(如 shell 回执的 `STDOUT:/STDERR:` 头),
|
|
20
|
+
* 不是「估算魔法」:它必须能从工具的固定输出模板直接数出来。
|
|
21
|
+
*/
|
|
22
|
+
/** 拟合式版本;改参数必须同时改这里(测试会锁死这一约定)。 */
|
|
23
|
+
export const A1_FIT_VERSION = 'a1-fit-v1';
|
|
24
|
+
export const A1_FIT_PARAMS = Object.freeze({
|
|
25
|
+
version: A1_FIT_VERSION,
|
|
26
|
+
charsPerToken: 4,
|
|
27
|
+
overheadChars: Object.freeze({
|
|
28
|
+
fs_read_file: 0,
|
|
29
|
+
fs_list_dir: 0,
|
|
30
|
+
fs_write_file: 0,
|
|
31
|
+
// exec_shell_in_vm 的回执固定带 "STDOUT:\n"(8)+ "\nSTDERR:\n"(9)+ 末尾换行(1)
|
|
32
|
+
exec_shell_in_vm: 18,
|
|
33
|
+
}),
|
|
34
|
+
defaultOverheadChars: 0,
|
|
35
|
+
});
|
|
36
|
+
const nonNegative = (value) => {
|
|
37
|
+
const n = Number(value);
|
|
38
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
39
|
+
};
|
|
40
|
+
function overheadOf(toolName) {
|
|
41
|
+
const key = String(toolName ?? '');
|
|
42
|
+
const table = A1_FIT_PARAMS.overheadChars;
|
|
43
|
+
return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : A1_FIT_PARAMS.defaultOverheadChars;
|
|
44
|
+
}
|
|
45
|
+
/** baseline:不经 A1 时模型会看到的回执 token 数(拟合式,v1 冻结)。 */
|
|
46
|
+
export function baselineOutputTokens(toolName, rawChars) {
|
|
47
|
+
const chars = nonNegative(rawChars);
|
|
48
|
+
if (chars === 0)
|
|
49
|
+
return 0;
|
|
50
|
+
return Math.ceil((chars + overheadOf(toolName)) / A1_FIT_PARAMS.charsPerToken);
|
|
51
|
+
}
|
|
52
|
+
/** actual:紧凑后模型实际看到的回执 token 数。 */
|
|
53
|
+
export function actualOutputTokens(compactChars) {
|
|
54
|
+
const chars = nonNegative(compactChars);
|
|
55
|
+
if (chars === 0)
|
|
56
|
+
return 0;
|
|
57
|
+
return Math.ceil(chars / A1_FIT_PARAMS.charsPerToken);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 单次工具的归因节省(永不为负:紧凑后更长时记 0,并由调用方标 `applied: false`)。
|
|
61
|
+
*/
|
|
62
|
+
export function attributedSavedTokens(toolName, rawChars, compactChars) {
|
|
63
|
+
const baseline = baselineOutputTokens(toolName, rawChars);
|
|
64
|
+
const actual = actualOutputTokens(compactChars);
|
|
65
|
+
return Math.max(0, baseline - actual);
|
|
66
|
+
}
|