@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,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
+ }