@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
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 3.3.0 模块注册表 + 内置模块适配器(设计 §6 SPI)。
|
|
6
|
+
*
|
|
7
|
+
* 注册表是 Worker 与模块层之间唯一的中介:Agent Loop 只调注册表,
|
|
8
|
+
* 注册表负责顺序应用、异常隔离(fail-open)、模式约束(shadow 不改执行)与归因汇总。
|
|
9
|
+
* A1 由 `tool-compact.ts` 的纯格式化函数适配成 ToolModule;A2/A3 与 B1–B4 后续按同一 SPI 接入。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
CostModule,
|
|
14
|
+
DecisionModule,
|
|
15
|
+
Edg,
|
|
16
|
+
EdgDraft,
|
|
17
|
+
EdgEdge,
|
|
18
|
+
GuardVerdict,
|
|
19
|
+
ModelCallInfo,
|
|
20
|
+
ModelResponseInfo,
|
|
21
|
+
ModuleAttribution,
|
|
22
|
+
ModuleDegraded,
|
|
23
|
+
ModuleMode,
|
|
24
|
+
ModuleRegistry,
|
|
25
|
+
SubstitutionCertDraft,
|
|
26
|
+
ToolCallView,
|
|
27
|
+
ToolCtx,
|
|
28
|
+
ToolModule,
|
|
29
|
+
ToolReceipt,
|
|
30
|
+
ToolTransformInput,
|
|
31
|
+
TurnSnapshot,
|
|
32
|
+
} from './cost-spi.ts'
|
|
33
|
+
import { createToolCompactor } from './tool-compact.ts'
|
|
34
|
+
import { normalizeCompactMode } from './tool-compact.ts'
|
|
35
|
+
import { createSpillStore } from './spill.ts'
|
|
36
|
+
|
|
37
|
+
const estimateTokens = (chars: number): number => (chars > 0 ? Math.ceil(chars / 4) : 0)
|
|
38
|
+
|
|
39
|
+
export interface ModuleRegistryOptions {
|
|
40
|
+
/** 启动时附加的模块(顺序即应用顺序)。 */
|
|
41
|
+
modules?: CostModule[]
|
|
42
|
+
/** 模块异常的留痕出口(每次「模块+阶段」首次与每 20 次上报一次)。 */
|
|
43
|
+
onDegraded?(event: ModuleDegraded): void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createModuleRegistry(options: ModuleRegistryOptions = {}): ModuleRegistry {
|
|
47
|
+
const toolModules: ToolModule[] = []
|
|
48
|
+
const decisionModules: DecisionModule[] = []
|
|
49
|
+
const ids = new Set<string>()
|
|
50
|
+
const errors: string[] = []
|
|
51
|
+
const degradedList: ModuleDegraded[] = []
|
|
52
|
+
const degradedCounts = new Map<string, ModuleDegraded>()
|
|
53
|
+
let stats = { evaluated: 0, applied: 0, savedChars: 0 }
|
|
54
|
+
const byModule: Record<string, number> = {}
|
|
55
|
+
|
|
56
|
+
const noteError = (message: string): void => {
|
|
57
|
+
if (errors.length < 20) errors.push(message)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 模块异常:fail-open 留痕(不改写本次输出、不阻断 Agent Loop)。 */
|
|
61
|
+
const noteDegraded = (module: string, phase: ModuleDegraded['phase'], error: unknown): void => {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
63
|
+
noteError(`${module}: ${message}`)
|
|
64
|
+
const key = `${module}|${phase}`
|
|
65
|
+
const previous = degradedCounts.get(key)
|
|
66
|
+
const count = (previous?.count ?? 0) + 1
|
|
67
|
+
const event: ModuleDegraded = { module, phase, message, count, at: Date.now() }
|
|
68
|
+
degradedCounts.set(key, event)
|
|
69
|
+
const existing = degradedList.findIndex((item) => item.module === module && item.phase === phase)
|
|
70
|
+
if (existing >= 0) degradedList[existing] = event
|
|
71
|
+
else degradedList.push(event)
|
|
72
|
+
if (count === 1 || count % 20 === 0) {
|
|
73
|
+
try { options.onDegraded?.(event) } catch { /* 留痕失败不得影响执行 */ }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function register(module: CostModule): boolean {
|
|
78
|
+
if (!module || typeof module !== 'object') return false
|
|
79
|
+
const id = String((module as { id?: unknown }).id ?? '')
|
|
80
|
+
if (!id || ids.has(id)) return false
|
|
81
|
+
ids.add(id)
|
|
82
|
+
if (module.kind === 'tool') toolModules.push(module as ToolModule)
|
|
83
|
+
else if (module.kind === 'decision') decisionModules.push(module as DecisionModule)
|
|
84
|
+
else return false
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const module of options.modules ?? []) register(module)
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* ③ 派发前:顺序应用 `beforeDispatch`。
|
|
92
|
+
* **只有 enforce 档才真的改写/屏蔽**;shadow 只记录「本可…」,observe/off 跳过。
|
|
93
|
+
*/
|
|
94
|
+
function beforeToolDispatch(input: { ticketId: string; stepIndex: number; call: ToolCallView }) {
|
|
95
|
+
const attributions: ModuleAttribution[] = []
|
|
96
|
+
let call = input.call
|
|
97
|
+
for (const module of toolModules) {
|
|
98
|
+
if (module.mode === 'off' || typeof module.beforeDispatch !== 'function') continue
|
|
99
|
+
try {
|
|
100
|
+
if (!module.appliesTo(call.name)) continue
|
|
101
|
+
const ctx: ToolCtx = { ticketId: input.ticketId, stepIndex: input.stepIndex, mode: module.mode, originalToolName: input.call.name }
|
|
102
|
+
const next = module.beforeDispatch(call, ctx)
|
|
103
|
+
const wouldChange = next === null || next.name !== call.name || JSON.stringify(next.args ?? {}) !== JSON.stringify(call.args ?? {})
|
|
104
|
+
if (!wouldChange) continue
|
|
105
|
+
const attribution: ModuleAttribution = {
|
|
106
|
+
module: module.id,
|
|
107
|
+
mode: module.mode,
|
|
108
|
+
applied: module.mode === 'enforce',
|
|
109
|
+
originalChars: 0,
|
|
110
|
+
resultChars: 0,
|
|
111
|
+
savedChars: 0,
|
|
112
|
+
estimatedTokens: 0,
|
|
113
|
+
meta: { point: 'beforeDispatch', hidden: next === null, from: call.name, to: next === null ? null : next.name },
|
|
114
|
+
}
|
|
115
|
+
attributions.push(attribution)
|
|
116
|
+
if (module.mode !== 'enforce') continue
|
|
117
|
+
if (next === null) {
|
|
118
|
+
return { call, hidden: true as const, hiddenBy: module.id, reason: 'hidden-by-module', attributions, errors: errors.slice() }
|
|
119
|
+
}
|
|
120
|
+
call = next
|
|
121
|
+
} catch (error) {
|
|
122
|
+
noteDegraded(module.id, 'beforeDispatch', error)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { call, hidden: false as const, attributions, errors: errors.slice() }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** ④ 回执:顺序应用 `afterReceipt`(shadow 只算不换)。 */
|
|
129
|
+
function afterToolReceipt(input: ToolTransformInput): { text: string; attributions: ModuleAttribution[]; errors: string[] } {
|
|
130
|
+
const attributions: ModuleAttribution[] = []
|
|
131
|
+
let text = String(input.text ?? '')
|
|
132
|
+
for (const module of toolModules) {
|
|
133
|
+
if (module.mode === 'off' || typeof module.afterReceipt !== 'function') continue
|
|
134
|
+
try {
|
|
135
|
+
if (!module.appliesTo(input.toolName)) continue
|
|
136
|
+
const ctx: ToolCtx = { ticketId: input.ticketId, stepIndex: input.stepIndex, mode: module.mode, originalToolName: input.toolName }
|
|
137
|
+
const out = module.afterReceipt({ text }, ctx)
|
|
138
|
+
if (!out || typeof out.text !== 'string') continue
|
|
139
|
+
const originalChars = text.length
|
|
140
|
+
const resultChars = out.text.length
|
|
141
|
+
const savedChars = Math.max(0, originalChars - resultChars)
|
|
142
|
+
// §5.4:模块可给出拟合式算出的 token 归因;否则退回字符/4 估算。
|
|
143
|
+
const moduleTokens = Math.max(0, Number(out.savedTokens ?? 0) || 0)
|
|
144
|
+
const estimatedTokens = moduleTokens > 0 ? moduleTokens : estimateTokens(savedChars)
|
|
145
|
+
// 是否「真的替换」由模式决定:模块在 shadow 下不得改变执行;
|
|
146
|
+
// A1 自己在「不更短」时返回原文,因此长度纪律不放在注册表里。
|
|
147
|
+
const applied = module.mode === 'enforce' && out.text !== text
|
|
148
|
+
attributions.push({
|
|
149
|
+
module: module.id,
|
|
150
|
+
mode: module.mode,
|
|
151
|
+
applied,
|
|
152
|
+
originalChars,
|
|
153
|
+
resultChars: applied ? resultChars : originalChars,
|
|
154
|
+
savedChars: applied ? savedChars : 0,
|
|
155
|
+
estimatedTokens: applied ? estimatedTokens : 0,
|
|
156
|
+
meta: { point: 'afterReceipt', toolName: input.toolName, wouldSaveChars: savedChars },
|
|
157
|
+
})
|
|
158
|
+
stats.evaluated += 1
|
|
159
|
+
if (applied) {
|
|
160
|
+
stats.applied += 1
|
|
161
|
+
stats.savedChars += savedChars
|
|
162
|
+
byModule[module.id] = (byModule[module.id] ?? 0) + estimatedTokens
|
|
163
|
+
text = out.text
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
noteDegraded(module.id, 'afterReceipt', error)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { text, attributions, errors: errors.slice() }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* ---------------- B 类:决策复用(§7 SPI) ---------------- */
|
|
173
|
+
|
|
174
|
+
/** ② 作者步骤:每个 B 模块产出一张候选图(异常 fail-open 留痕)。 */
|
|
175
|
+
async function authorGraphs(turn: TurnSnapshot): Promise<{ drafts: Array<{ module: string; draft: EdgDraft }>; errors: string[] }> {
|
|
176
|
+
const drafts: Array<{ module: string; draft: EdgDraft }> = []
|
|
177
|
+
for (const module of decisionModules) {
|
|
178
|
+
if (module.mode === 'off' || typeof module.author !== 'function') continue
|
|
179
|
+
try {
|
|
180
|
+
const draft = await module.author(turn)
|
|
181
|
+
if (draft && Array.isArray(draft.edges) && draft.edges.length > 0) drafts.push({ module: module.id, draft })
|
|
182
|
+
} catch (error) {
|
|
183
|
+
noteDegraded(module.id, 'author', error)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { drafts, errors: errors.slice() }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** ④ 守卫:收集所有命中(observe 只计数,shadow 落证书,enforce 可替换)。 */
|
|
190
|
+
function guardReceipt(input: { receipt: ToolReceipt; graph: Edg }): {
|
|
191
|
+
verdicts: Array<{ module: string; mode: ModuleMode; verdict: GuardVerdict }>
|
|
192
|
+
hit: { module: string; mode: ModuleMode; verdict: GuardVerdict } | null
|
|
193
|
+
errors: string[]
|
|
194
|
+
} {
|
|
195
|
+
const verdicts: Array<{ module: string; mode: ModuleMode; verdict: GuardVerdict }> = []
|
|
196
|
+
for (const module of decisionModules) {
|
|
197
|
+
if (module.mode === 'off' || typeof module.guard !== 'function') continue
|
|
198
|
+
try {
|
|
199
|
+
const verdict = module.guard(input.receipt, input.graph)
|
|
200
|
+
if (!verdict || typeof verdict !== 'object') continue
|
|
201
|
+
verdicts.push({ module: module.id, mode: module.mode, verdict })
|
|
202
|
+
} catch (error) {
|
|
203
|
+
noteDegraded(module.id, 'guard', error)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const hit = verdicts.find((item) => item.verdict.hit === true) ?? null
|
|
207
|
+
return { verdicts, hit, errors: errors.slice() }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 命中后的替换:只有 `enforce` 真的执行(且需策略层白名单放行,由调用方判定);
|
|
212
|
+
* `shadow` 只产出「本可替换」的证书草案,不执行任何工具。
|
|
213
|
+
*/
|
|
214
|
+
async function substitute(input: {
|
|
215
|
+
edge: EdgEdge
|
|
216
|
+
receipt: ToolReceipt
|
|
217
|
+
ctx: ToolCtx
|
|
218
|
+
mode: ModuleMode
|
|
219
|
+
}): Promise<{ executed: boolean; certificate: SubstitutionCertDraft | null; module: string | null; errors: string[] }> {
|
|
220
|
+
for (const module of decisionModules) {
|
|
221
|
+
if (module.mode === 'off' || typeof module.substitute !== 'function') continue
|
|
222
|
+
if (module.id !== input.ctx.ticketId && false) continue
|
|
223
|
+
try {
|
|
224
|
+
const certificate = await module.substitute(input.edge, input.receipt, input.ctx)
|
|
225
|
+
if (!certificate) continue
|
|
226
|
+
// 非原生派发的证书一律拒绝(§5.5 硬约束)。
|
|
227
|
+
if (certificate.viaNativeDispatcher !== true) {
|
|
228
|
+
noteDegraded(module.id, 'substitute', new Error('substitute must go through the native dispatcher'))
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
const executed = input.mode === 'enforce'
|
|
232
|
+
return { executed, certificate, module: module.id, errors: errors.slice() }
|
|
233
|
+
} catch (error) {
|
|
234
|
+
noteDegraded(module.id, 'substitute', error)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { executed: false, certificate: null, module: null, errors: errors.slice() }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function beforeModelCall(info: ModelCallInfo): void {
|
|
241
|
+
for (const module of decisionModules) {
|
|
242
|
+
try { module.beforeModelCall?.(info) } catch (error) {
|
|
243
|
+
noteDegraded(module.id, 'beforeModelCall', error)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function afterModelResponse(info: ModelResponseInfo): void {
|
|
249
|
+
for (const module of decisionModules) {
|
|
250
|
+
try { module.afterModelResponse?.(info) } catch (error) {
|
|
251
|
+
noteDegraded(module.id, 'afterModelResponse', error)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function describe(): Array<{ id: string; kind: string; mode: string; surface: string }> {
|
|
257
|
+
return [
|
|
258
|
+
...toolModules.map((module) => ({ id: module.id, kind: module.kind, mode: module.mode, surface: module.surface })),
|
|
259
|
+
...decisionModules.map((module) => ({ id: module.id, kind: module.kind, mode: module.mode, surface: 'worker' })),
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 工具面常驻成本:各 A 模块自报之和(供 prompt 成本报表)。 */
|
|
264
|
+
function promptCost(): { toolTokens: number; schemaCount: number; byModule: Record<string, { toolTokens: number; schemaCount: number }> } {
|
|
265
|
+
const byModuleCost: Record<string, { toolTokens: number; schemaCount: number }> = {}
|
|
266
|
+
let toolTokens = 0
|
|
267
|
+
let schemaCount = 0
|
|
268
|
+
for (const module of toolModules) {
|
|
269
|
+
if (typeof module.promptCost !== 'function') continue
|
|
270
|
+
try {
|
|
271
|
+
const cost = module.promptCost()
|
|
272
|
+
const tokens = Math.max(0, Number(cost?.toolTokens ?? 0) || 0)
|
|
273
|
+
const count = Math.max(0, Number(cost?.schemaCount ?? 0) || 0)
|
|
274
|
+
byModuleCost[module.id] = { toolTokens: tokens, schemaCount: count }
|
|
275
|
+
toolTokens += tokens
|
|
276
|
+
schemaCount += count
|
|
277
|
+
} catch (error) {
|
|
278
|
+
noteDegraded(module.id, 'afterReceipt', error)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { toolTokens, schemaCount, byModule: byModuleCost }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function summary(): { evaluated: number; applied: number; savedChars: number; estimatedTokens: number; byModule: Record<string, number> } {
|
|
285
|
+
return {
|
|
286
|
+
evaluated: stats.evaluated,
|
|
287
|
+
applied: stats.applied,
|
|
288
|
+
savedChars: stats.savedChars,
|
|
289
|
+
estimatedTokens: estimateTokens(stats.savedChars),
|
|
290
|
+
byModule: { ...byModule },
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function degraded(): ModuleDegraded[] {
|
|
295
|
+
return degradedList.map((item) => ({ ...item }))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resetTask(): void {
|
|
299
|
+
stats = { evaluated: 0, applied: 0, savedChars: 0 }
|
|
300
|
+
for (const key of Object.keys(byModule)) delete byModule[key]
|
|
301
|
+
errors.length = 0
|
|
302
|
+
degradedList.length = 0
|
|
303
|
+
degradedCounts.clear()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return Object.freeze({
|
|
307
|
+
register,
|
|
308
|
+
beforeToolDispatch,
|
|
309
|
+
afterToolReceipt,
|
|
310
|
+
transformToolResult: afterToolReceipt,
|
|
311
|
+
authorGraphs,
|
|
312
|
+
guardReceipt,
|
|
313
|
+
substitute,
|
|
314
|
+
beforeModelCall,
|
|
315
|
+
afterModelResponse,
|
|
316
|
+
describe,
|
|
317
|
+
promptCost,
|
|
318
|
+
summary,
|
|
319
|
+
degraded,
|
|
320
|
+
resetTask,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** A1:把 tool-compact 的纯格式化函数适配成 ToolModule(SPI §6)。 */
|
|
325
|
+
export function createCompactModule(mode?: string, options: { workspace?: string; spill?: boolean; spillThresholdChars?: number } = {}): ToolModule {
|
|
326
|
+
const resolved = normalizeCompactMode(mode) as ModuleMode
|
|
327
|
+
// spill:大输出落盘 + 摘要(原文仍可回读)。只在 enforce 生效,且必须有 workspace。
|
|
328
|
+
const spill = options.spill !== false && options.workspace
|
|
329
|
+
? createSpillStore({ workspace: options.workspace })
|
|
330
|
+
: null
|
|
331
|
+
const compactor = createToolCompactor({
|
|
332
|
+
mode: 'enforce',
|
|
333
|
+
spill: spill ? { save: (input) => spill.save({ ticketId: 'worker', stepIndex: 0, toolName: 'tool', text: input.text }) } : null,
|
|
334
|
+
spillThresholdChars: options.spillThresholdChars,
|
|
335
|
+
})
|
|
336
|
+
const handled = new Set(['fs_read_file', 'fs_list_dir', 'fs_write_file', 'exec_shell_in_vm'])
|
|
337
|
+
return Object.freeze({
|
|
338
|
+
id: 'a1-compact' as const,
|
|
339
|
+
kind: 'tool' as const,
|
|
340
|
+
surface: 'worker' as const,
|
|
341
|
+
mode: resolved,
|
|
342
|
+
appliesTo(toolName: string): boolean {
|
|
343
|
+
return handled.has(String(toolName ?? ''))
|
|
344
|
+
},
|
|
345
|
+
afterReceipt(result: { text: string; truncated?: boolean }, ctx: ToolCtx) {
|
|
346
|
+
// 候选压缩结果一律算出;**由注册表按模式决定是否真的替换**(shadow 不改变执行)。
|
|
347
|
+
const candidate = compactor.compact({ toolName: ctx.originalToolName, text: result.text })
|
|
348
|
+
if (!candidate.evaluated || !candidate.applied) return null
|
|
349
|
+
return { text: candidate.text, truncated: result.truncated, savedTokens: candidate.estimatedTokens }
|
|
350
|
+
},
|
|
351
|
+
/** A1 不新增常驻工具:prompt 成本为 0(真实 schema 成本由 Agent Loop 另计)。 */
|
|
352
|
+
promptCost() {
|
|
353
|
+
return { toolTokens: 0, schemaCount: 0 }
|
|
354
|
+
},
|
|
355
|
+
})
|
|
356
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 3.3.0 模块层 SPI(ToolModule / DecisionModule,设计 §6 / §7)。
|
|
6
|
+
*
|
|
7
|
+
* 这是「系统层 ↔ 模块层」之间唯一的契约面:A 类模块(工具面)实现 ToolModule,
|
|
8
|
+
* B 类模块(决策复用)实现 DecisionModule;Worker 的四个切点只与注册表交互,
|
|
9
|
+
* 不认识任何具体模块。新增模块不需要改动 agent-loop.ts。
|
|
10
|
+
*
|
|
11
|
+
* 硬性约束(设计 §9):
|
|
12
|
+
* * 模块异常一律 fail-open 并留痕,不得中断 Agent Loop;
|
|
13
|
+
* * 模块只能在**自己的**职责面内改写(A 类改工具调用/回执,B 类改决策复用),
|
|
14
|
+
* 不得触碰权限、Mandate、预算与状态机;
|
|
15
|
+
* * `shadow` 档只计算、**不改变执行**(beforeDispatch 的改写/屏蔽只在 enforce 生效);
|
|
16
|
+
* * 每次生效必须产出归因(shadow 只报「本可节省」)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type ToolModuleId = 'a1-compact' | 'a2-disclosure' | 'a3-batch'
|
|
20
|
+
export type DecisionModuleId = 'b1-edg' | 'b2-gdr' | 'b3-ccs' | 'b4-rsi'
|
|
21
|
+
export type ModuleMode = 'off' | 'shadow' | 'enforce'
|
|
22
|
+
|
|
23
|
+
export interface ModuleAttribution {
|
|
24
|
+
module: ToolModuleId | DecisionModuleId
|
|
25
|
+
mode: ModuleMode
|
|
26
|
+
/** 是否真的改写了本次执行。 */
|
|
27
|
+
applied: boolean
|
|
28
|
+
originalChars: number
|
|
29
|
+
resultChars: number
|
|
30
|
+
savedChars: number
|
|
31
|
+
estimatedTokens: number
|
|
32
|
+
/** 附加信息(命中率、守卫名等),仅用于报表。 */
|
|
33
|
+
meta?: Record<string, unknown>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ------------------------------------------------------------------ *
|
|
37
|
+
* A 类:工具面模块(设计 §6 SPI)
|
|
38
|
+
* ------------------------------------------------------------------ */
|
|
39
|
+
|
|
40
|
+
/** ③ 派发前的工具调用视图(只含路由所需字段,不含任何权限信息)。 */
|
|
41
|
+
export interface ToolCallView {
|
|
42
|
+
id: string
|
|
43
|
+
name: string
|
|
44
|
+
/** 已解析的参数对象(原始 JSON 字符串由 Agent Loop 保留)。 */
|
|
45
|
+
args: Record<string, unknown>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** ④ 工具回执视图。 */
|
|
49
|
+
export interface ToolResultView {
|
|
50
|
+
text: string
|
|
51
|
+
/** Agent Loop 侧是否已按长度截断(模块不得反向「补长」)。 */
|
|
52
|
+
truncated?: boolean
|
|
53
|
+
/**
|
|
54
|
+
* 模块自算的归因节省(token)。给出时注册表优先采用它
|
|
55
|
+
* (例如 A1 的固定拟合式 §5.4.1),否则按「字符差/4」估算。
|
|
56
|
+
*/
|
|
57
|
+
savedTokens?: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ToolCtx {
|
|
61
|
+
ticketId: string
|
|
62
|
+
stepIndex: number
|
|
63
|
+
/** 本模块当前生效档位(由注册表按模块自身 mode 注入)。 */
|
|
64
|
+
mode: ModuleMode
|
|
65
|
+
/** 原始工具名(beforeDispatch 改写后仍保留,便于模块识别自己)。 */
|
|
66
|
+
originalToolName: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A 类:工具面模块。
|
|
71
|
+
*
|
|
72
|
+
* * `beforeDispatch`(③):工具名/描述映射,可隐藏官方工具(返回 `null`);
|
|
73
|
+
* * `afterReceipt`(④):回执压缩,必须保留可用性(行号、可定位、错误可读);
|
|
74
|
+
* * `promptCost`:工具面常驻成本(供 prompt 成本报表)。
|
|
75
|
+
*/
|
|
76
|
+
export interface ToolModule {
|
|
77
|
+
readonly id: ToolModuleId
|
|
78
|
+
readonly kind: 'tool'
|
|
79
|
+
/**
|
|
80
|
+
* 作用面(设计 §3.3):Worker 内注册的模块恒为 `worker`。
|
|
81
|
+
* 宿主会话工具面(dsh-ops 一类宿主插件)不在本 SPI 内,由宿主侧适配器单独归因。
|
|
82
|
+
*/
|
|
83
|
+
readonly surface: 'worker'
|
|
84
|
+
readonly mode: ModuleMode
|
|
85
|
+
appliesTo(toolName: string): boolean
|
|
86
|
+
/**
|
|
87
|
+
* ③ 派发前:返回改写后的调用(可改 name/args);
|
|
88
|
+
* 返回 `null` 表示**本模块屏蔽该工具**(不执行,回一条结构化拒绝给模型)。
|
|
89
|
+
* 仅在 `enforce` 档生效;shadow 只记录「本可屏蔽/改写」。
|
|
90
|
+
*/
|
|
91
|
+
beforeDispatch?(call: ToolCallView, ctx: ToolCtx): ToolCallView | null
|
|
92
|
+
/** ④ 回执:返回压缩后的结果;`null` 表示不改。 */
|
|
93
|
+
afterReceipt?(result: ToolResultView, ctx: ToolCtx): ToolResultView | null
|
|
94
|
+
/** 工具面常驻成本(模块自报;为空表示不新增常驻工具)。 */
|
|
95
|
+
promptCost?(): { toolTokens: number; schemaCount: number }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 兼容别名:旧调用点用的 transformToolResult 输入/输出形态。 */
|
|
99
|
+
export interface ToolTransformInput {
|
|
100
|
+
ticketId: string
|
|
101
|
+
stepIndex: number
|
|
102
|
+
toolName: string
|
|
103
|
+
args?: Record<string, unknown>
|
|
104
|
+
text: string
|
|
105
|
+
mode?: ModuleMode
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ToolTransformOutput {
|
|
109
|
+
text: string
|
|
110
|
+
applied: boolean
|
|
111
|
+
originalChars: number
|
|
112
|
+
resultChars: number
|
|
113
|
+
savedChars: number
|
|
114
|
+
estimatedTokens: number
|
|
115
|
+
meta?: Record<string, unknown>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ------------------------------------------------------------------ *
|
|
119
|
+
* B 类:决策复用模块(设计 §7 SPI)
|
|
120
|
+
* ------------------------------------------------------------------ */
|
|
121
|
+
|
|
122
|
+
/** 工具回执(守卫判据的输入)。`digest` 是回执内容的稳定摘要。 */
|
|
123
|
+
export interface ToolReceipt {
|
|
124
|
+
toolName: string
|
|
125
|
+
args: Record<string, unknown>
|
|
126
|
+
text: string
|
|
127
|
+
/** 回执摘要(schemaHash + 内容摘要),守卫与证书都用它做关联。 */
|
|
128
|
+
digest: string
|
|
129
|
+
stepIndex: number
|
|
130
|
+
ticketId: string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 可执行决策图的边/图/判定以 `worker/edg.ts`(B1)为唯一权威定义,这里只做再导出。 */
|
|
134
|
+
export type { EdgGraph as Edg, EdgEdge, EdgGuard, EdgArgSource, EdgGraph, EdgState, EdgSource } from './edg.ts'
|
|
135
|
+
import type { EdgGraph as EdgGraphType, EdgEdge as EdgEdgeType } from './edg.ts'
|
|
136
|
+
|
|
137
|
+
/** 慢模型输出里抽取出的候选决策图(作者步骤产物;成本入 governance)。 */
|
|
138
|
+
export interface EdgDraft {
|
|
139
|
+
edges: Array<Omit<EdgEdgeType, 'id'> & { id?: string }>
|
|
140
|
+
model?: string
|
|
141
|
+
note?: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 守卫判定结果(GDR 的 `GdrMatch` 是它的超集)。 */
|
|
145
|
+
export interface GuardVerdict {
|
|
146
|
+
hit: boolean
|
|
147
|
+
edgeId?: string
|
|
148
|
+
/** 未命中原因(可读、可审计)。 */
|
|
149
|
+
reason?: string
|
|
150
|
+
receiptDigest: string
|
|
151
|
+
/** 命中时预估「本可节省」的 token(shadow 用来算对照收益)。 */
|
|
152
|
+
wouldSaveTokens?: number
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** ② 作者步骤的输入快照。 */
|
|
156
|
+
export interface TurnSnapshot {
|
|
157
|
+
ticketId: string
|
|
158
|
+
taskId?: string
|
|
159
|
+
turn: number
|
|
160
|
+
stepIndex: number
|
|
161
|
+
model?: string
|
|
162
|
+
/** 慢模型本轮文本输出。 */
|
|
163
|
+
assistant: string
|
|
164
|
+
toolCalls: Array<{ name: string; args: Record<string, unknown> }>
|
|
165
|
+
receipts: ToolReceipt[]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 替换证书草案(Worker 侧形态;host 侧同名类型用于审计链)。 */
|
|
169
|
+
export interface SubstitutionCertDraft {
|
|
170
|
+
edgeId: string
|
|
171
|
+
toolName: string
|
|
172
|
+
schemaHash: string
|
|
173
|
+
guardExpr: string
|
|
174
|
+
receiptDigest: string
|
|
175
|
+
replacedCallbackId: string
|
|
176
|
+
viaNativeDispatcher: true
|
|
177
|
+
taskId: string
|
|
178
|
+
step: number
|
|
179
|
+
savedTokens?: number
|
|
180
|
+
moduleId?: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface ModelCallInfo {
|
|
184
|
+
ticketId: string
|
|
185
|
+
stepIndex: number
|
|
186
|
+
model?: string
|
|
187
|
+
historyChars: number
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface ModelResponseInfo {
|
|
191
|
+
ticketId: string
|
|
192
|
+
stepIndex: number
|
|
193
|
+
usage: { input: number; output: number; total: number }
|
|
194
|
+
latencyMs: number
|
|
195
|
+
/** provider 未返回 usage、由 harness 兜底估算(不得当实测上报)。 */
|
|
196
|
+
estimated?: boolean
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* B 类:决策复用模块(设计 §7)。
|
|
201
|
+
*
|
|
202
|
+
* * `author`(②):从慢模型输出抽取候选决策图,作者成本入 **governance**;
|
|
203
|
+
* * `guard`(④):拿回执判断是否命中已存边;
|
|
204
|
+
* * `substitute`:命中后**经原生派发器**执行并返回证书。
|
|
205
|
+
*
|
|
206
|
+
* 三档语义(§5.3)由注册表强制:observe 只计数、shadow 只算并落证书、enforce 才真的替换。
|
|
207
|
+
*/
|
|
208
|
+
export interface DecisionModule {
|
|
209
|
+
readonly id: DecisionModuleId
|
|
210
|
+
readonly kind: 'decision'
|
|
211
|
+
readonly mode: ModuleMode
|
|
212
|
+
/** ② 作者步骤:产出候选 EDG(可为 null)。 */
|
|
213
|
+
author?(turn: TurnSnapshot): Promise<EdgDraft | null>
|
|
214
|
+
/** ④ 守卫:拿回执判断是否命中某条已存边。 */
|
|
215
|
+
guard?(receipt: ToolReceipt, graph: EdgGraphType): GuardVerdict
|
|
216
|
+
/** 命中后:经原生派发器执行并返回证书。 */
|
|
217
|
+
substitute?(edge: EdgEdgeType, receipt: ToolReceipt, ctx: ToolCtx): Promise<SubstitutionCertDraft>
|
|
218
|
+
/** 可选:模型调用前后(统计/RSI 反馈用)。 */
|
|
219
|
+
beforeModelCall?(info: ModelCallInfo): void
|
|
220
|
+
afterModelResponse?(info: ModelResponseInfo): void
|
|
221
|
+
/** 本模块累计的归因(命中/跳过次数等)。 */
|
|
222
|
+
attribution?(): ModuleAttribution | null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type CostModule = ToolModule | DecisionModule
|
|
226
|
+
|
|
227
|
+
/** 复用异常时的降级信号(目标 2.1-5:降级本身必须留痕,不伪装成节省)。 */
|
|
228
|
+
export interface ModuleDegraded {
|
|
229
|
+
module: string
|
|
230
|
+
phase: 'beforeDispatch' | 'afterReceipt' | 'author' | 'guard' | 'substitute' | 'beforeModelCall' | 'afterModelResponse'
|
|
231
|
+
message: string
|
|
232
|
+
/** 同一模块+阶段累计发生次数(去重后上报,避免刷爆事件流)。 */
|
|
233
|
+
count: number
|
|
234
|
+
at: number
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface ModuleRegistry {
|
|
238
|
+
/** 注册模块;重复 id 会被忽略并返回 false(先注册先得)。 */
|
|
239
|
+
register(module: CostModule): boolean
|
|
240
|
+
/** ③ 派发前:顺序应用 beforeDispatch;返回最终调用与是否被屏蔽。 */
|
|
241
|
+
beforeToolDispatch(input: {
|
|
242
|
+
ticketId: string
|
|
243
|
+
stepIndex: number
|
|
244
|
+
call: ToolCallView
|
|
245
|
+
}): { call: ToolCallView; hidden: false; attributions: ModuleAttribution[]; errors: string[] } | {
|
|
246
|
+
call: ToolCallView
|
|
247
|
+
hidden: true
|
|
248
|
+
hiddenBy: string
|
|
249
|
+
reason: string
|
|
250
|
+
attributions: ModuleAttribution[]
|
|
251
|
+
errors: string[]
|
|
252
|
+
}
|
|
253
|
+
/** ④ 回执:顺序应用 afterReceipt;返回最终文本与全部归因。 */
|
|
254
|
+
afterToolReceipt(input: ToolTransformInput): { text: string; attributions: ModuleAttribution[]; errors: string[] }
|
|
255
|
+
/** 兼容别名(等同 afterToolReceipt)。 */
|
|
256
|
+
transformToolResult(input: ToolTransformInput): { text: string; attributions: ModuleAttribution[]; errors: string[] }
|
|
257
|
+
/** 广播给 B 类模块。 */
|
|
258
|
+
beforeModelCall(info: ModelCallInfo): void
|
|
259
|
+
afterModelResponse(info: ModelResponseInfo): void
|
|
260
|
+
/** ② 作者步骤:收集候选决策图(成本入 governance,由宿主记账)。 */
|
|
261
|
+
authorGraphs(turn: TurnSnapshot): Promise<{ drafts: Array<{ module: string; draft: EdgDraft }>; errors: string[] }>
|
|
262
|
+
/** ④ 守卫:收集命中(observe 只计数 / shadow 落证书 / enforce 可替换)。 */
|
|
263
|
+
guardReceipt(input: { receipt: ToolReceipt; graph: EdgGraphType }): {
|
|
264
|
+
verdicts: Array<{ module: string; mode: ModuleMode; verdict: GuardVerdict }>
|
|
265
|
+
hit: { module: string; mode: ModuleMode; verdict: GuardVerdict } | null
|
|
266
|
+
errors: string[]
|
|
267
|
+
}
|
|
268
|
+
/** 命中后替换:仅 enforce 真正执行;shadow 只产出「本可替换」证书。 */
|
|
269
|
+
substitute(input: { edge: EdgEdgeType; receipt: ToolReceipt; ctx: ToolCtx; mode: ModuleMode }): Promise<{
|
|
270
|
+
executed: boolean
|
|
271
|
+
certificate: SubstitutionCertDraft | null
|
|
272
|
+
module: string | null
|
|
273
|
+
errors: string[]
|
|
274
|
+
}>
|
|
275
|
+
describe(): Array<{ id: string; kind: string; mode: string; surface: string }>
|
|
276
|
+
/** 工具面常驻成本(各 A 模块自报之和)。 */
|
|
277
|
+
promptCost(): { toolTokens: number; schemaCount: number; byModule: Record<string, { toolTokens: number; schemaCount: number }> }
|
|
278
|
+
/** 本次任务累计归因汇总(按模块)。 */
|
|
279
|
+
summary(): { evaluated: number; applied: number; savedChars: number; estimatedTokens: number; byModule: Record<string, number> }
|
|
280
|
+
/** 降级记录(模块异常留痕)。 */
|
|
281
|
+
degraded(): ModuleDegraded[]
|
|
282
|
+
resetTask(): void
|
|
283
|
+
}
|