@twinsearth/dsh-master 3.2.9 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +39 -6
  2. package/client.js +51 -1
  3. package/dist-host/cost/attribution.js +183 -0
  4. package/dist-host/cost/audit.js +163 -0
  5. package/dist-host/cost/budget.js +211 -0
  6. package/dist-host/cost/bus.js +95 -0
  7. package/dist-host/cost/cert-chain.js +100 -0
  8. package/dist-host/cost/experiment.js +132 -0
  9. package/dist-host/cost/gate.js +189 -0
  10. package/dist-host/cost/host-surface.js +80 -0
  11. package/dist-host/cost/index.js +312 -0
  12. package/dist-host/cost/ledger-report.js +193 -0
  13. package/dist-host/cost/ledger.js +401 -0
  14. package/dist-host/cost/metrics.js +198 -0
  15. package/dist-host/cost/milestones.js +171 -0
  16. package/dist-host/cost/modules/index.js +5 -0
  17. package/dist-host/cost/modules/manifests.js +130 -0
  18. package/dist-host/cost/policy.js +276 -0
  19. package/dist-host/cost/report.js +156 -0
  20. package/dist-host/cost/reuse-report.js +128 -0
  21. package/dist-host/cost/rsi.js +367 -0
  22. package/dist-host/cost/toolface.js +147 -0
  23. package/dist-host/cost/types.js +31 -0
  24. package/dist-host/kernel.js +38 -11
  25. package/dist-host/plugins/cost-optimizer.js +277 -27
  26. package/dist-host/plugins/superagent-router.js +89 -2
  27. package/dist-host/plugins/superagent-worker.js +1 -1
  28. package/dist-host/superagent/router.js +1 -1
  29. package/dist-host/superagent/types.js +8 -0
  30. package/dist-worker/agent-loop.js +449 -7
  31. package/dist-worker/baseline-fit.js +66 -0
  32. package/dist-worker/budget-guard.js +93 -0
  33. package/dist-worker/ccs.js +99 -0
  34. package/dist-worker/cost-hooks.js +115 -0
  35. package/dist-worker/cost-modules.js +330 -0
  36. package/dist-worker/cost-spi.js +3 -0
  37. package/dist-worker/edg.js +372 -0
  38. package/dist-worker/gdr.js +159 -0
  39. package/dist-worker/index.js +80 -2
  40. package/dist-worker/llm-http.js +20 -0
  41. package/dist-worker/sink-http.js +1 -0
  42. package/dist-worker/spill.js +115 -0
  43. package/dist-worker/tool-batch.js +66 -0
  44. package/dist-worker/tool-compact.js +275 -0
  45. package/dist-worker/tool-disclosure.js +90 -0
  46. package/dist-worker/tools.js +16 -0
  47. package/docs/ARCHITECTURE.md +32 -11
  48. package/docs/CHANGELOG.md +60 -0
  49. package/docs/DESIGN-v3.3.0-cost-optimization.md +1053 -0
  50. package/docs/EXPERIMENT-M5-real-ab.md +82 -0
  51. package/docs/RELEASE-v3.2.10.md +32 -0
  52. package/docs/ROADMAP.md +3 -3
  53. package/docs/SECURITY.md +9 -9
  54. package/docs/SUPERAGENT.md +5 -5
  55. package/docs/THIRD-PARTY.md +87 -0
  56. package/docs/WORKER-AGENT-LOOP.md +1 -1
  57. package/package.json +15 -3
  58. package/src/cost/attribution.ts +186 -0
  59. package/src/cost/audit.ts +170 -0
  60. package/src/cost/budget.ts +226 -0
  61. package/src/cost/bus.ts +97 -0
  62. package/src/cost/cert-chain.ts +102 -0
  63. package/src/cost/experiment.ts +136 -0
  64. package/src/cost/gate.ts +191 -0
  65. package/src/cost/host-surface.ts +97 -0
  66. package/src/cost/index.ts +316 -0
  67. package/src/cost/ledger-report.ts +186 -0
  68. package/src/cost/ledger.ts +385 -0
  69. package/src/cost/metrics.ts +193 -0
  70. package/src/cost/milestones.ts +170 -0
  71. package/src/cost/modules/index.ts +13 -0
  72. package/src/cost/modules/manifests.ts +129 -0
  73. package/src/cost/policy.ts +276 -0
  74. package/src/cost/report.ts +158 -0
  75. package/src/cost/reuse-report.ts +131 -0
  76. package/src/cost/rsi.ts +364 -0
  77. package/src/cost/toolface.ts +151 -0
  78. package/src/cost/types.ts +298 -0
  79. package/src/kernel.ts +37 -11
  80. package/src/plugins/cost-optimizer.ts +271 -26
  81. package/src/plugins/superagent-router.ts +83 -2
  82. package/src/plugins/superagent-worker.ts +1 -1
  83. package/src/superagent/router.ts +1 -1
  84. package/src/superagent/types.ts +70 -0
  85. package/worker/agent-loop.ts +444 -7
  86. package/worker/baseline-fit.ts +80 -0
  87. package/worker/budget-guard.ts +156 -0
  88. package/worker/ccs.ts +202 -0
  89. package/worker/cost-hooks.ts +133 -0
  90. package/worker/cost-modules.ts +356 -0
  91. package/worker/cost-spi.ts +283 -0
  92. package/worker/edg.ts +424 -0
  93. package/worker/gdr.ts +233 -0
  94. package/worker/index.ts +84 -3
  95. package/worker/llm-http.ts +18 -0
  96. package/worker/sink-http.ts +5 -2
  97. package/worker/spill.ts +133 -0
  98. package/worker/tool-batch.ts +124 -0
  99. package/worker/tool-compact.ts +317 -0
  100. package/worker/tool-disclosure.ts +131 -0
  101. package/worker/tools.ts +553 -531
  102. package/worker/types.ts +191 -0
@@ -0,0 +1,93 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ const nonNegativeInt = (value) => {
4
+ const n = Number(value);
5
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
6
+ };
7
+ export function createBudgetGuard(options = {}) {
8
+ const enabled = options.enabled === true;
9
+ const limitTokens = options.limitTokens === undefined ? null : nonNegativeInt(options.limitTokens) || null;
10
+ const reserveTokens = options.reserveTokens === undefined ? null : nonNegativeInt(options.reserveTokens);
11
+ const onExhaust = options.onExhaust === 'degrade' ? 'degrade' : 'hard-stop';
12
+ const degradeModels = (Array.isArray(options.degradeModels) ? options.degradeModels : [])
13
+ .map((item) => String(item ?? '').trim())
14
+ .filter(Boolean);
15
+ const degradeTo = options.degradeTo === undefined ? null : String(options.degradeTo);
16
+ const degradeAllowed = onExhaust === 'degrade' && degradeTo !== null && degradeModels.includes(degradeTo);
17
+ let consumedTokens = 0;
18
+ /** 尚未释放的预留(settle/refund 用它避免重复记账)。 */
19
+ let reservedTokens = 0;
20
+ function decide(requested, at = Date.now()) {
21
+ const effective = degradeAllowed ? 'degrade' : 'hard-stop';
22
+ const base = {
23
+ consumedTokens,
24
+ limitTokens,
25
+ remainingTokens: limitTokens === null ? null : Math.max(0, limitTokens - consumedTokens),
26
+ requested,
27
+ onExhaust: effective,
28
+ at,
29
+ };
30
+ if (!enabled || limitTokens === null) {
31
+ return { ok: true, ...base };
32
+ }
33
+ const remaining = limitTokens - consumedTokens;
34
+ if (requested <= remaining) {
35
+ consumedTokens += requested;
36
+ return { ok: true, ...base, consumedTokens, remainingTokens: limitTokens - consumedTokens };
37
+ }
38
+ if (degradeAllowed && degradeTo !== null) {
39
+ // 降级也要付得起:预留额度放进额度内才放行,否则仍然 fail-closed。
40
+ if (requested <= remaining) {
41
+ consumedTokens += requested;
42
+ return { ok: true, degraded: true, model: degradeTo, ...base, consumedTokens, remainingTokens: limitTokens - consumedTokens };
43
+ }
44
+ return {
45
+ ok: false,
46
+ code: 'budget-exhausted',
47
+ message: `预算不足:剩余 ${Math.max(0, remaining)} token,需 ${requested},且降级模型 ${degradeTo} 也无法容纳`,
48
+ ...base,
49
+ };
50
+ }
51
+ return {
52
+ ok: false,
53
+ code: 'budget-exhausted',
54
+ message: `预算不足:剩余 ${Math.max(0, remaining)} token,需 ${requested} token(fail-closed,不启动模型调用)`,
55
+ ...base,
56
+ };
57
+ }
58
+ function beforeModelCall(input) {
59
+ // 预留值:显式配置优先;否则按「历史字符/4 + 固定输出余量」估算,宁高不低。
60
+ const estimate = reserveTokens !== null
61
+ ? reserveTokens
62
+ : Math.max(1, Math.ceil(Math.max(0, Number(input?.historyChars) || 0) / 4) + 256);
63
+ const decision = decide(estimate);
64
+ // 降级决策不消耗额外额度(预留已在 decide 内计入)。
65
+ if (decision.ok)
66
+ reservedTokens += estimate;
67
+ return decision;
68
+ }
69
+ function refund(tokens) {
70
+ const amount = nonNegativeInt(tokens);
71
+ consumedTokens = Math.max(0, consumedTokens - amount);
72
+ // 预留随之释放,否则后续 settle 会把同一份额度重复记账。
73
+ reservedTokens = Math.max(0, reservedTokens - amount);
74
+ }
75
+ function settle(tokens) {
76
+ const actual = nonNegativeInt(tokens);
77
+ // 结算:先退掉**尚未释放**的预留,再按真实用量记账(实际可能为 0)。
78
+ // 之前这里写的是 `- 0`(死代码),会把预留与真实用量重复计入,导致额度被提前耗尽。
79
+ consumedTokens = Math.max(0, consumedTokens - reservedTokens) + actual;
80
+ reservedTokens = 0;
81
+ }
82
+ function describe() {
83
+ return {
84
+ enabled,
85
+ limitTokens,
86
+ consumedTokens,
87
+ onExhaust: degradeAllowed ? onExhaust : 'hard-stop',
88
+ degradeTo: degradeAllowed ? degradeTo : null,
89
+ degradeModels: [...degradeModels],
90
+ };
91
+ }
92
+ return Object.freeze({ enabled, get consumedTokens() { return consumedTokens; }, beforeModelCall, refund, settle, describe });
93
+ }
@@ -0,0 +1,99 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ const tokensOf = (text) => (text.length > 0 ? Math.ceil(text.length / 4) : 0);
4
+ /** 派发结果是否算「失败」:抛错、系统拒绝、执行报错都算。 */
5
+ function resultFailed(text) {
6
+ const value = String(text ?? '');
7
+ if (value.startsWith('【系统拒绝】'))
8
+ return 'tool-denied';
9
+ if (value.startsWith('执行报错'))
10
+ return 'tool-error';
11
+ return null;
12
+ }
13
+ export function createCcs(options) {
14
+ if (!options?.dispatcher || typeof options.dispatcher.execute !== 'function') {
15
+ throw new Error('ccs requires the native dispatcher (worker/tools.ts execute)');
16
+ }
17
+ const clock = typeof options.clock === 'function' ? options.clock : () => Date.now();
18
+ const budget = options.budget ?? null;
19
+ let attempted = 0;
20
+ let dispatched = 0;
21
+ let failed = 0;
22
+ let fallbacks = 0;
23
+ function cert(input, fields) {
24
+ return {
25
+ edgeId: String(input.edge?.id ?? ''),
26
+ toolName: String(input.edge?.toolName ?? ''),
27
+ schemaHash: String(input.edge?.schemaHash ?? ''),
28
+ guardExpr: String(input.edge?.guard?.expr ?? ''),
29
+ receiptDigest: String(input.receipt?.digest ?? ''),
30
+ replacedCallbackId: String(input.replacedCallbackId ?? ''),
31
+ viaNativeDispatcher: true,
32
+ taskId: String(input.taskId ?? ''),
33
+ step: Number(input.step ?? 0),
34
+ dispatched: false,
35
+ moduleId: input.moduleId ?? 'b3-ccs',
36
+ savedTokens: Math.max(0, Number(input.savedTokens ?? 0) || 0),
37
+ at: clock(),
38
+ ...fields,
39
+ };
40
+ }
41
+ async function substitute(input) {
42
+ attempted += 1;
43
+ // ⓪ 取消信号:替换路径不得绕过取消(I8)。
44
+ if (input.signal?.aborted === true) {
45
+ failed += 1;
46
+ fallbacks += 1;
47
+ return { ok: false, text: null, fallback: true, certificate: cert(input, { dispatched: false, error: 'aborted' }) };
48
+ }
49
+ // ① 预算闸:替模型做的这次调用同样要付得起(禁止绕过 budget 检查)。
50
+ if (budget) {
51
+ const decision = budget.beforeModelCall({ historyChars: 0 });
52
+ if (!decision.ok) {
53
+ failed += 1;
54
+ fallbacks += 1;
55
+ return { ok: false, text: null, fallback: true, certificate: cert(input, { dispatched: false, error: decision.code ?? 'budget-exhausted' }) };
56
+ }
57
+ // 替模型省下的这次调用不产生模型 token:把刚扣的预留退回去。
58
+ budget.refund(decision.requested);
59
+ }
60
+ // ② 经**未改动的原生派发器**执行:权限/Mandate/取消/超时/审计全部照旧。
61
+ let text;
62
+ try {
63
+ text = await options.dispatcher.execute({ name: input.edge.toolName, arguments: JSON.stringify(input.args ?? {}) }, {
64
+ workspace: input.workspace,
65
+ signal: input.signal,
66
+ timeoutMs: Math.max(1, Number(input.timeoutMs) || 1),
67
+ mandate: input.mandate,
68
+ });
69
+ }
70
+ catch (error) {
71
+ failed += 1;
72
+ fallbacks += 1;
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ return { ok: false, text: null, fallback: true, certificate: cert(input, { dispatched: false, error: `dispatch-threw:${message}` }) };
75
+ }
76
+ // ③ 派发器返回的「拒绝/报错」也算失败:不吞错、不假装成功。
77
+ const failure = resultFailed(text);
78
+ if (failure) {
79
+ failed += 1;
80
+ fallbacks += 1;
81
+ return { ok: false, text: null, fallback: true, certificate: cert(input, { dispatched: false, error: failure }) };
82
+ }
83
+ dispatched += 1;
84
+ return {
85
+ ok: true,
86
+ text,
87
+ fallback: false,
88
+ certificate: cert(input, {
89
+ dispatched: true,
90
+ resultDigest: `sha256:${tokensOf(text).toString(36)}`,
91
+ resultChars: text.length,
92
+ }),
93
+ };
94
+ }
95
+ function stats() {
96
+ return { attempted, dispatched, failed, fallbacks };
97
+ }
98
+ return Object.freeze({ substitute, stats });
99
+ }
@@ -0,0 +1,115 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ const nonNegative = (value) => {
4
+ const n = Number(value);
5
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
6
+ };
7
+ /** 把 provider 的 usage 归一成 {input, output, total};缺 total 时用 input+output。 */
8
+ export function normalizeCostUsage(usage) {
9
+ const input = nonNegative(usage?.promptTokens);
10
+ const output = nonNegative(usage?.completionTokens);
11
+ const total = nonNegative(usage?.totalTokens) || input + output;
12
+ return { input, output, total };
13
+ }
14
+ export function createCostHooks(options = {}) {
15
+ const clock = typeof options.clock === 'function' ? options.clock : () => Date.now();
16
+ const emit = (event) => {
17
+ const sink = options.sink;
18
+ if (!sink || typeof sink.cost !== 'function')
19
+ return;
20
+ try {
21
+ sink.cost(event);
22
+ }
23
+ catch {
24
+ /* 成本观测失败不得影响控制流 */
25
+ }
26
+ };
27
+ const hooks = {
28
+ beforeModelCall(input) {
29
+ emit({
30
+ point: 'beforeModelCall',
31
+ at: clock(),
32
+ ticketId: String(input?.ticketId ?? ''),
33
+ stepIndex: Number(input?.stepIndex) || 0,
34
+ model: input?.model === undefined ? undefined : String(input.model),
35
+ historyChars: nonNegative(input?.historyChars),
36
+ });
37
+ },
38
+ afterModelResponse(input) {
39
+ emit({
40
+ point: 'afterModelResponse',
41
+ at: clock(),
42
+ ticketId: String(input?.ticketId ?? ''),
43
+ stepIndex: Number(input?.stepIndex) || 0,
44
+ usage: input?.usage ?? normalizeCostUsage(),
45
+ latencyMs: nonNegative(input?.latencyMs),
46
+ estimated: input?.estimated === true,
47
+ });
48
+ },
49
+ beforeToolDispatch(input) {
50
+ emit({
51
+ point: 'beforeToolDispatch',
52
+ at: clock(),
53
+ ticketId: String(input?.ticketId ?? ''),
54
+ stepIndex: Number(input?.stepIndex) || 0,
55
+ toolName: String(input?.toolName ?? ''),
56
+ argsChars: nonNegative(input?.argsChars),
57
+ });
58
+ },
59
+ afterToolReceipt(input) {
60
+ emit({
61
+ point: 'afterToolReceipt',
62
+ at: clock(),
63
+ ticketId: String(input?.ticketId ?? ''),
64
+ stepIndex: Number(input?.stepIndex) || 0,
65
+ toolName: String(input?.toolName ?? ''),
66
+ resultChars: nonNegative(input?.resultChars),
67
+ truncated: input?.truncated === true,
68
+ compaction: input?.compaction,
69
+ });
70
+ },
71
+ degraded(input) {
72
+ emit({
73
+ point: 'degraded',
74
+ at: clock(),
75
+ ticketId: String(input?.ticketId ?? ''),
76
+ stepIndex: Number(input?.stepIndex) || 0,
77
+ toolName: input?.module === undefined ? undefined : String(input.module),
78
+ degraded: {
79
+ module: String(input?.module ?? ''),
80
+ phase: input?.phase ?? 'transform',
81
+ message: String(input?.message ?? ''),
82
+ count: nonNegative(input?.count) || 1,
83
+ },
84
+ });
85
+ },
86
+ };
87
+ return Object.freeze(hooks);
88
+ }
89
+ /** 合并宿主注入的覆盖实现:先跑默认实现,再跑覆盖;任一失败都不影响另一个。 */
90
+ export function mergeCostHooks(base, override) {
91
+ if (!override)
92
+ return base;
93
+ const chain = (key) => {
94
+ const extra = override[key];
95
+ if (typeof extra !== 'function')
96
+ return base[key];
97
+ return ((...args) => {
98
+ try {
99
+ base[key](...args);
100
+ }
101
+ catch { /* ignore */ }
102
+ try {
103
+ extra(...args);
104
+ }
105
+ catch { /* ignore */ }
106
+ });
107
+ };
108
+ return Object.freeze({
109
+ beforeModelCall: chain('beforeModelCall'),
110
+ afterModelResponse: chain('afterModelResponse'),
111
+ beforeToolDispatch: chain('beforeToolDispatch'),
112
+ afterToolReceipt: chain('afterToolReceipt'),
113
+ degraded: chain('degraded'),
114
+ });
115
+ }
@@ -0,0 +1,330 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ import { createToolCompactor } from "./tool-compact.js";
4
+ import { normalizeCompactMode } from "./tool-compact.js";
5
+ import { createSpillStore } from "./spill.js";
6
+ const estimateTokens = (chars) => (chars > 0 ? Math.ceil(chars / 4) : 0);
7
+ export function createModuleRegistry(options = {}) {
8
+ const toolModules = [];
9
+ const decisionModules = [];
10
+ const ids = new Set();
11
+ const errors = [];
12
+ const degradedList = [];
13
+ const degradedCounts = new Map();
14
+ let stats = { evaluated: 0, applied: 0, savedChars: 0 };
15
+ const byModule = {};
16
+ const noteError = (message) => {
17
+ if (errors.length < 20)
18
+ errors.push(message);
19
+ };
20
+ /** 模块异常:fail-open 留痕(不改写本次输出、不阻断 Agent Loop)。 */
21
+ const noteDegraded = (module, phase, error) => {
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ noteError(`${module}: ${message}`);
24
+ const key = `${module}|${phase}`;
25
+ const previous = degradedCounts.get(key);
26
+ const count = (previous?.count ?? 0) + 1;
27
+ const event = { module, phase, message, count, at: Date.now() };
28
+ degradedCounts.set(key, event);
29
+ const existing = degradedList.findIndex((item) => item.module === module && item.phase === phase);
30
+ if (existing >= 0)
31
+ degradedList[existing] = event;
32
+ else
33
+ degradedList.push(event);
34
+ if (count === 1 || count % 20 === 0) {
35
+ try {
36
+ options.onDegraded?.(event);
37
+ }
38
+ catch { /* 留痕失败不得影响执行 */ }
39
+ }
40
+ };
41
+ function register(module) {
42
+ if (!module || typeof module !== 'object')
43
+ return false;
44
+ const id = String(module.id ?? '');
45
+ if (!id || ids.has(id))
46
+ return false;
47
+ ids.add(id);
48
+ if (module.kind === 'tool')
49
+ toolModules.push(module);
50
+ else if (module.kind === 'decision')
51
+ decisionModules.push(module);
52
+ else
53
+ return false;
54
+ return true;
55
+ }
56
+ for (const module of options.modules ?? [])
57
+ register(module);
58
+ /**
59
+ * ③ 派发前:顺序应用 `beforeDispatch`。
60
+ * **只有 enforce 档才真的改写/屏蔽**;shadow 只记录「本可…」,observe/off 跳过。
61
+ */
62
+ function beforeToolDispatch(input) {
63
+ const attributions = [];
64
+ let call = input.call;
65
+ for (const module of toolModules) {
66
+ if (module.mode === 'off' || typeof module.beforeDispatch !== 'function')
67
+ continue;
68
+ try {
69
+ if (!module.appliesTo(call.name))
70
+ continue;
71
+ const ctx = { ticketId: input.ticketId, stepIndex: input.stepIndex, mode: module.mode, originalToolName: input.call.name };
72
+ const next = module.beforeDispatch(call, ctx);
73
+ const wouldChange = next === null || next.name !== call.name || JSON.stringify(next.args ?? {}) !== JSON.stringify(call.args ?? {});
74
+ if (!wouldChange)
75
+ continue;
76
+ const attribution = {
77
+ module: module.id,
78
+ mode: module.mode,
79
+ applied: module.mode === 'enforce',
80
+ originalChars: 0,
81
+ resultChars: 0,
82
+ savedChars: 0,
83
+ estimatedTokens: 0,
84
+ meta: { point: 'beforeDispatch', hidden: next === null, from: call.name, to: next === null ? null : next.name },
85
+ };
86
+ attributions.push(attribution);
87
+ if (module.mode !== 'enforce')
88
+ continue;
89
+ if (next === null) {
90
+ return { call, hidden: true, hiddenBy: module.id, reason: 'hidden-by-module', attributions, errors: errors.slice() };
91
+ }
92
+ call = next;
93
+ }
94
+ catch (error) {
95
+ noteDegraded(module.id, 'beforeDispatch', error);
96
+ }
97
+ }
98
+ return { call, hidden: false, attributions, errors: errors.slice() };
99
+ }
100
+ /** ④ 回执:顺序应用 `afterReceipt`(shadow 只算不换)。 */
101
+ function afterToolReceipt(input) {
102
+ const attributions = [];
103
+ let text = String(input.text ?? '');
104
+ for (const module of toolModules) {
105
+ if (module.mode === 'off' || typeof module.afterReceipt !== 'function')
106
+ continue;
107
+ try {
108
+ if (!module.appliesTo(input.toolName))
109
+ continue;
110
+ const ctx = { ticketId: input.ticketId, stepIndex: input.stepIndex, mode: module.mode, originalToolName: input.toolName };
111
+ const out = module.afterReceipt({ text }, ctx);
112
+ if (!out || typeof out.text !== 'string')
113
+ continue;
114
+ const originalChars = text.length;
115
+ const resultChars = out.text.length;
116
+ const savedChars = Math.max(0, originalChars - resultChars);
117
+ // §5.4:模块可给出拟合式算出的 token 归因;否则退回字符/4 估算。
118
+ const moduleTokens = Math.max(0, Number(out.savedTokens ?? 0) || 0);
119
+ const estimatedTokens = moduleTokens > 0 ? moduleTokens : estimateTokens(savedChars);
120
+ // 是否「真的替换」由模式决定:模块在 shadow 下不得改变执行;
121
+ // A1 自己在「不更短」时返回原文,因此长度纪律不放在注册表里。
122
+ const applied = module.mode === 'enforce' && out.text !== text;
123
+ attributions.push({
124
+ module: module.id,
125
+ mode: module.mode,
126
+ applied,
127
+ originalChars,
128
+ resultChars: applied ? resultChars : originalChars,
129
+ savedChars: applied ? savedChars : 0,
130
+ estimatedTokens: applied ? estimatedTokens : 0,
131
+ meta: { point: 'afterReceipt', toolName: input.toolName, wouldSaveChars: savedChars },
132
+ });
133
+ stats.evaluated += 1;
134
+ if (applied) {
135
+ stats.applied += 1;
136
+ stats.savedChars += savedChars;
137
+ byModule[module.id] = (byModule[module.id] ?? 0) + estimatedTokens;
138
+ text = out.text;
139
+ }
140
+ }
141
+ catch (error) {
142
+ noteDegraded(module.id, 'afterReceipt', error);
143
+ }
144
+ }
145
+ return { text, attributions, errors: errors.slice() };
146
+ }
147
+ /* ---------------- B 类:决策复用(§7 SPI) ---------------- */
148
+ /** ② 作者步骤:每个 B 模块产出一张候选图(异常 fail-open 留痕)。 */
149
+ async function authorGraphs(turn) {
150
+ const drafts = [];
151
+ for (const module of decisionModules) {
152
+ if (module.mode === 'off' || typeof module.author !== 'function')
153
+ continue;
154
+ try {
155
+ const draft = await module.author(turn);
156
+ if (draft && Array.isArray(draft.edges) && draft.edges.length > 0)
157
+ drafts.push({ module: module.id, draft });
158
+ }
159
+ catch (error) {
160
+ noteDegraded(module.id, 'author', error);
161
+ }
162
+ }
163
+ return { drafts, errors: errors.slice() };
164
+ }
165
+ /** ④ 守卫:收集所有命中(observe 只计数,shadow 落证书,enforce 可替换)。 */
166
+ function guardReceipt(input) {
167
+ const verdicts = [];
168
+ for (const module of decisionModules) {
169
+ if (module.mode === 'off' || typeof module.guard !== 'function')
170
+ continue;
171
+ try {
172
+ const verdict = module.guard(input.receipt, input.graph);
173
+ if (!verdict || typeof verdict !== 'object')
174
+ continue;
175
+ verdicts.push({ module: module.id, mode: module.mode, verdict });
176
+ }
177
+ catch (error) {
178
+ noteDegraded(module.id, 'guard', error);
179
+ }
180
+ }
181
+ const hit = verdicts.find((item) => item.verdict.hit === true) ?? null;
182
+ return { verdicts, hit, errors: errors.slice() };
183
+ }
184
+ /**
185
+ * 命中后的替换:只有 `enforce` 真的执行(且需策略层白名单放行,由调用方判定);
186
+ * `shadow` 只产出「本可替换」的证书草案,不执行任何工具。
187
+ */
188
+ async function substitute(input) {
189
+ for (const module of decisionModules) {
190
+ if (module.mode === 'off' || typeof module.substitute !== 'function')
191
+ continue;
192
+ if (module.id !== input.ctx.ticketId && false)
193
+ continue;
194
+ try {
195
+ const certificate = await module.substitute(input.edge, input.receipt, input.ctx);
196
+ if (!certificate)
197
+ continue;
198
+ // 非原生派发的证书一律拒绝(§5.5 硬约束)。
199
+ if (certificate.viaNativeDispatcher !== true) {
200
+ noteDegraded(module.id, 'substitute', new Error('substitute must go through the native dispatcher'));
201
+ continue;
202
+ }
203
+ const executed = input.mode === 'enforce';
204
+ return { executed, certificate, module: module.id, errors: errors.slice() };
205
+ }
206
+ catch (error) {
207
+ noteDegraded(module.id, 'substitute', error);
208
+ }
209
+ }
210
+ return { executed: false, certificate: null, module: null, errors: errors.slice() };
211
+ }
212
+ function beforeModelCall(info) {
213
+ for (const module of decisionModules) {
214
+ try {
215
+ module.beforeModelCall?.(info);
216
+ }
217
+ catch (error) {
218
+ noteDegraded(module.id, 'beforeModelCall', error);
219
+ }
220
+ }
221
+ }
222
+ function afterModelResponse(info) {
223
+ for (const module of decisionModules) {
224
+ try {
225
+ module.afterModelResponse?.(info);
226
+ }
227
+ catch (error) {
228
+ noteDegraded(module.id, 'afterModelResponse', error);
229
+ }
230
+ }
231
+ }
232
+ function describe() {
233
+ return [
234
+ ...toolModules.map((module) => ({ id: module.id, kind: module.kind, mode: module.mode, surface: module.surface })),
235
+ ...decisionModules.map((module) => ({ id: module.id, kind: module.kind, mode: module.mode, surface: 'worker' })),
236
+ ];
237
+ }
238
+ /** 工具面常驻成本:各 A 模块自报之和(供 prompt 成本报表)。 */
239
+ function promptCost() {
240
+ const byModuleCost = {};
241
+ let toolTokens = 0;
242
+ let schemaCount = 0;
243
+ for (const module of toolModules) {
244
+ if (typeof module.promptCost !== 'function')
245
+ continue;
246
+ try {
247
+ const cost = module.promptCost();
248
+ const tokens = Math.max(0, Number(cost?.toolTokens ?? 0) || 0);
249
+ const count = Math.max(0, Number(cost?.schemaCount ?? 0) || 0);
250
+ byModuleCost[module.id] = { toolTokens: tokens, schemaCount: count };
251
+ toolTokens += tokens;
252
+ schemaCount += count;
253
+ }
254
+ catch (error) {
255
+ noteDegraded(module.id, 'afterReceipt', error);
256
+ }
257
+ }
258
+ return { toolTokens, schemaCount, byModule: byModuleCost };
259
+ }
260
+ function summary() {
261
+ return {
262
+ evaluated: stats.evaluated,
263
+ applied: stats.applied,
264
+ savedChars: stats.savedChars,
265
+ estimatedTokens: estimateTokens(stats.savedChars),
266
+ byModule: { ...byModule },
267
+ };
268
+ }
269
+ function degraded() {
270
+ return degradedList.map((item) => ({ ...item }));
271
+ }
272
+ function resetTask() {
273
+ stats = { evaluated: 0, applied: 0, savedChars: 0 };
274
+ for (const key of Object.keys(byModule))
275
+ delete byModule[key];
276
+ errors.length = 0;
277
+ degradedList.length = 0;
278
+ degradedCounts.clear();
279
+ }
280
+ return Object.freeze({
281
+ register,
282
+ beforeToolDispatch,
283
+ afterToolReceipt,
284
+ transformToolResult: afterToolReceipt,
285
+ authorGraphs,
286
+ guardReceipt,
287
+ substitute,
288
+ beforeModelCall,
289
+ afterModelResponse,
290
+ describe,
291
+ promptCost,
292
+ summary,
293
+ degraded,
294
+ resetTask,
295
+ });
296
+ }
297
+ /** A1:把 tool-compact 的纯格式化函数适配成 ToolModule(SPI §6)。 */
298
+ export function createCompactModule(mode, options = {}) {
299
+ const resolved = normalizeCompactMode(mode);
300
+ // spill:大输出落盘 + 摘要(原文仍可回读)。只在 enforce 生效,且必须有 workspace。
301
+ const spill = options.spill !== false && options.workspace
302
+ ? createSpillStore({ workspace: options.workspace })
303
+ : null;
304
+ const compactor = createToolCompactor({
305
+ mode: 'enforce',
306
+ spill: spill ? { save: (input) => spill.save({ ticketId: 'worker', stepIndex: 0, toolName: 'tool', text: input.text }) } : null,
307
+ spillThresholdChars: options.spillThresholdChars,
308
+ });
309
+ const handled = new Set(['fs_read_file', 'fs_list_dir', 'fs_write_file', 'exec_shell_in_vm']);
310
+ return Object.freeze({
311
+ id: 'a1-compact',
312
+ kind: 'tool',
313
+ surface: 'worker',
314
+ mode: resolved,
315
+ appliesTo(toolName) {
316
+ return handled.has(String(toolName ?? ''));
317
+ },
318
+ afterReceipt(result, ctx) {
319
+ // 候选压缩结果一律算出;**由注册表按模式决定是否真的替换**(shadow 不改变执行)。
320
+ const candidate = compactor.compact({ toolName: ctx.originalToolName, text: result.text });
321
+ if (!candidate.evaluated || !candidate.applied)
322
+ return null;
323
+ return { text: candidate.text, truncated: result.truncated, savedTokens: candidate.estimatedTokens };
324
+ },
325
+ /** A1 不新增常驻工具:prompt 成本为 0(真实 schema 成本由 Agent Loop 另计)。 */
326
+ promptCost() {
327
+ return { toolTokens: 0, schemaCount: 0 };
328
+ },
329
+ });
330
+ }
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ export {};