@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.
Files changed (104) hide show
  1. package/README.md +4 -0
  2. package/client.js +50 -0
  3. package/dist-host/contract/client.js +7 -0
  4. package/dist-host/contract/constants.js +16 -2
  5. package/dist-host/contract/index.js +1 -0
  6. package/dist-host/contract/relay.js +239 -0
  7. package/dist-host/cost/attribution.js +183 -0
  8. package/dist-host/cost/audit.js +163 -0
  9. package/dist-host/cost/budget.js +211 -0
  10. package/dist-host/cost/bus.js +95 -0
  11. package/dist-host/cost/cert-chain.js +100 -0
  12. package/dist-host/cost/experiment.js +132 -0
  13. package/dist-host/cost/fang-relay.js +329 -0
  14. package/dist-host/cost/gate.js +189 -0
  15. package/dist-host/cost/host-surface.js +80 -0
  16. package/dist-host/cost/index.js +345 -0
  17. package/dist-host/cost/ledger-report.js +193 -0
  18. package/dist-host/cost/ledger.js +401 -0
  19. package/dist-host/cost/metrics.js +198 -0
  20. package/dist-host/cost/milestones.js +209 -0
  21. package/dist-host/cost/modules/index.js +5 -0
  22. package/dist-host/cost/modules/manifests.js +130 -0
  23. package/dist-host/cost/policy.js +276 -0
  24. package/dist-host/cost/report.js +156 -0
  25. package/dist-host/cost/reuse-report.js +128 -0
  26. package/dist-host/cost/rsi.js +367 -0
  27. package/dist-host/cost/toolface.js +147 -0
  28. package/dist-host/cost/types.js +31 -0
  29. package/dist-host/kernel.js +28 -2
  30. package/dist-host/plugins/cost-optimizer.js +329 -27
  31. package/dist-host/plugins/superagent-router.js +104 -1
  32. package/dist-host/superagent/types.js +8 -0
  33. package/dist-worker/agent-loop.js +449 -7
  34. package/dist-worker/baseline-fit.js +66 -0
  35. package/dist-worker/budget-guard.js +93 -0
  36. package/dist-worker/ccs.js +99 -0
  37. package/dist-worker/cost-hooks.js +115 -0
  38. package/dist-worker/cost-modules.js +330 -0
  39. package/dist-worker/cost-spi.js +3 -0
  40. package/dist-worker/edg.js +372 -0
  41. package/dist-worker/gdr.js +159 -0
  42. package/dist-worker/index.js +80 -2
  43. package/dist-worker/llm-http.js +20 -0
  44. package/dist-worker/sink-http.js +1 -0
  45. package/dist-worker/spill.js +115 -0
  46. package/dist-worker/tool-batch.js +66 -0
  47. package/dist-worker/tool-compact.js +275 -0
  48. package/dist-worker/tool-disclosure.js +90 -0
  49. package/dist-worker/tools.js +16 -0
  50. package/docs/ARCHITECTURE.md +24 -0
  51. package/docs/CHANGELOG.md +66 -0
  52. package/docs/DESIGN-v3.3.0-cost-optimization.md +1058 -0
  53. package/docs/EXPERIMENT-M5-fang-relay.md +117 -0
  54. package/docs/EXPERIMENT-M5-real-ab.md +87 -0
  55. package/docs/THIRD-PARTY.md +87 -0
  56. package/package.json +2 -2
  57. package/src/contract/client.js +7 -0
  58. package/src/contract/constants.js +17 -2
  59. package/src/contract/index.js +1 -0
  60. package/src/contract/relay.js +223 -0
  61. package/src/cost/attribution.ts +186 -0
  62. package/src/cost/audit.ts +170 -0
  63. package/src/cost/budget.ts +226 -0
  64. package/src/cost/bus.ts +97 -0
  65. package/src/cost/cert-chain.ts +102 -0
  66. package/src/cost/experiment.ts +136 -0
  67. package/src/cost/fang-relay.ts +318 -0
  68. package/src/cost/gate.ts +191 -0
  69. package/src/cost/host-surface.ts +97 -0
  70. package/src/cost/index.ts +348 -0
  71. package/src/cost/ledger-report.ts +186 -0
  72. package/src/cost/ledger.ts +385 -0
  73. package/src/cost/metrics.ts +193 -0
  74. package/src/cost/milestones.ts +206 -0
  75. package/src/cost/modules/index.ts +13 -0
  76. package/src/cost/modules/manifests.ts +129 -0
  77. package/src/cost/policy.ts +276 -0
  78. package/src/cost/report.ts +158 -0
  79. package/src/cost/reuse-report.ts +131 -0
  80. package/src/cost/rsi.ts +364 -0
  81. package/src/cost/toolface.ts +151 -0
  82. package/src/cost/types.ts +298 -0
  83. package/src/kernel.ts +27 -2
  84. package/src/plugins/cost-optimizer.ts +313 -26
  85. package/src/plugins/superagent-router.ts +96 -1
  86. package/src/superagent/types.ts +70 -0
  87. package/worker/agent-loop.ts +444 -7
  88. package/worker/baseline-fit.ts +80 -0
  89. package/worker/budget-guard.ts +156 -0
  90. package/worker/ccs.ts +202 -0
  91. package/worker/cost-hooks.ts +133 -0
  92. package/worker/cost-modules.ts +356 -0
  93. package/worker/cost-spi.ts +283 -0
  94. package/worker/edg.ts +424 -0
  95. package/worker/gdr.ts +233 -0
  96. package/worker/index.ts +84 -3
  97. package/worker/llm-http.ts +18 -0
  98. package/worker/sink-http.ts +5 -2
  99. package/worker/spill.ts +133 -0
  100. package/worker/tool-batch.ts +124 -0
  101. package/worker/tool-compact.ts +317 -0
  102. package/worker/tool-disclosure.ts +131 -0
  103. package/worker/tools.ts +553 -531
  104. package/worker/types.ts +191 -0
@@ -3,15 +3,22 @@
3
3
  // @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
4
4
 
5
5
  /**
6
- * 子插件 2:成本优化(HarnessMM 观察层)—— 为分身装“燃油表”。
6
+ * 子插件 2:成本优化(3.3.0 M1:系统层接入,observe-only)。
7
7
  *
8
- * 3.2.0 先交付可观测/可估算的骨架,不做推理拦截:
9
- * * `observe` 模式只统计/估算,不改写任何模型调用;
10
- * * HarnessMM 的 EGD/GDR/CCS 真实接入在 3.3.0 里程碑(见 docs/ROADMAP.md);
11
- * * 角色:只允许 fork(分身生产盘),母体/元主不加载。
8
+ * 本行在 3.2.x 的 observe 骨架上扩展,**保持** `estimate/record/summary/reset/status`
9
+ * 兼容面,内部改为委托 3.3.0 系统层(`src/cost`):
10
+ * * ledger:两本账(execution / governance)+ 归因;
11
+ * * budget:task/fork 预算闸(fail-closed,附 remaining);
12
+ * * policy:observe → shadow → enforce 三档与 enforce 门禁;
13
+ * * audit:治理事件哈希链。
14
+ *
15
+ * M1 只记账:不拦截模型调用、不改写工具输出、不替换推理回调;
16
+ * 工具面(A1–A3)与决策复用(B1–B4)在 M2/M3/M4 接入。
17
+ * 角色:仍只允许 fork(分身生产盘)。
12
18
  */
13
19
 
14
20
  import {
21
+ OPTIONAL_SERVICE,
15
22
  ROW,
16
23
  ROLE,
17
24
  SERVICE,
@@ -21,12 +28,13 @@ import {
21
28
  roleAllowed,
22
29
  safeGet,
23
30
  } from '../kernel.ts'
31
+ import { createCostSystem, createFangRelayFromContext, DEFAULT_BUDGET_LIMITS } from '../cost/index.ts'
24
32
 
25
33
  export const name = 'dsh-master-cost-optimizer'
26
34
  export const inject = ['dshMaster']
27
35
  export const ROW_ID = ROW.cost
28
36
 
29
- /** HarnessMM 官方基准(3.2.0 文档引用;3.3.0 接入后应被实测值覆盖)。 */
37
+ /** HarnessMM 官方基准(3.2.0 文档引用;3.3.0 的实测值应覆盖它,而不是替代对标)。 */
30
38
  export const HARNESSMM_BENCHMARK = Object.freeze({
31
39
  baselineTokens: 96280,
32
40
  optimizedTokens: 11620,
@@ -41,7 +49,15 @@ function finiteNonNegative(value) {
41
49
 
42
50
  /**
43
51
  * @param {object} ctx
44
- * @param {{enabled?:boolean, mode?:string, targetSavingsRatio?:number}} [config]
52
+ * @param {{
53
+ * enabled?: boolean, mode?: string, targetSavingsRatio?: number,
54
+ * budget?: Array<{unit: string, limitTokens: number, onExhaust?: string}>,
55
+ * enforceAllowlist?: string[],
56
+ * relay?: {enabled?: boolean, httpBaseUrl?: string, httpToken?: string, fileDir?: string,
57
+ * timeoutMs?: number, maxBatch?: number, maxQueue?: number, probeTtlMs?: number,
58
+ * autoFlushMs?: number},
59
+ * dispatch?: {motherUrl?: string, motherToken?: string, contractFileDir?: string},
60
+ * }} [config]
45
61
  */
46
62
  export function apply(ctx, config = {}) {
47
63
  if (!enabledOf(config)) return { skipped: true, reason: 'disabled-by-config' }
@@ -58,11 +74,38 @@ export function apply(ctx, config = {}) {
58
74
  }
59
75
 
60
76
  const targetSavingsRatio = finiteNonNegative(config.targetSavingsRatio)
61
- const policy = Object.freeze({
62
- mode: 'observe', // 3.2.0 只允许 observe;replay 在 3.3.0 打开
63
- requestedMode: String(config.mode ?? 'observe'),
64
- targetSavingsRatio: targetSavingsRatio !== null && targetSavingsRatio <= 1 ? targetSavingsRatio : 0.8,
77
+ /**
78
+ * §5.6 / M5:瓮城回流出口。
79
+ * 传输按「插件配置 relay → dispatch 配置 → 环境变量 DSH_FANG_*」装配;
80
+ * 一个都没有时不造客户端,出口状态显式 `relay-client-absent`。
81
+ */
82
+ const relayWiring = createFangRelayFromContext(ctx, config, { logger: ctx?.logger })
83
+ const system = createCostSystem({
84
+ mode: config.mode,
85
+ allowlist: config.enforceAllowlist ?? config.allowlist,
86
+ policyScopes: config.scopes,
87
+ budgetLimits: Array.isArray(config.budget) && config.budget.length > 0 ? config.budget : DEFAULT_BUDGET_LIMITS,
88
+ degradeModels: config.degradeModels,
89
+ // 宿主会话工具面适配器:默认关闭;开启也只做外部收益登记,不重写任何宿主插件。
90
+ hostSurface: config.hostSurface && typeof config.hostSurface === 'object'
91
+ ? { enabled: config.hostSurface.enabled === true, source: config.hostSurface.source }
92
+ : undefined,
93
+ relay: relayWiring.relay,
94
+ logger: ctx?.logger,
65
95
  })
96
+ const policy = system.policy.describe()
97
+
98
+ // M5:加载后异步探测一次回流对端(不阻塞 apply;没有客户端时立即返回 absent)。
99
+ try { void Promise.resolve(system.relayProbe()).catch(() => {}) } catch { /* 探测失败不影响主流程 */ }
100
+ /** M5:回流自动投递;没有客户端或 autoFlushMs=0 时不开定时器。 */
101
+ const autoFlushMs = Math.max(0, Number(config.relay?.autoFlushMs ?? 2_000) || 0)
102
+ let relayTimer = null
103
+ if (relayWiring.client && autoFlushMs > 0) {
104
+ relayTimer = setInterval(() => { try { void system.relayFlush().catch(() => {}) } catch { /* ignore */ } }, autoFlushMs)
105
+ try { relayTimer.unref?.() } catch { /* ignore */ }
106
+ }
107
+
108
+ // 3.2.x 兼容计数器(任务级实测汇总;系统层另有账本,两者口径不同、互不覆盖)。
66
109
  const metrics = {
67
110
  tasks: 0,
68
111
  baselineTokens: 0,
@@ -74,19 +117,22 @@ export function apply(ctx, config = {}) {
74
117
 
75
118
  const service = {
76
119
  id: SERVICE.cost,
77
- version: '3.2.5',
120
+ version: '3.3.1',
78
121
  role,
79
- mode: policy.mode,
80
- /** 基于 HarnessMM 基准 + 目标比例做派生前预估。 */
122
+ get mode() {
123
+ return system.policy.describe().mode
124
+ },
125
+ /** 基于 HarnessMM 基准 + 目标比例做派生前预估(不承诺节省)。 */
81
126
  estimate({ baselineTokens } = {}) {
82
127
  const baseline = finiteNonNegative(baselineTokens)
83
128
  if (baseline === null) return masterError('baseline-tokens-invalid', 'baselineTokens must be a non-negative number')
84
- const projectedOptimized = Math.round(baseline * (1 - policy.targetSavingsRatio))
129
+ const current = system.policy.describe()
130
+ const projectedOptimized = Math.round(baseline * (1 - (targetSavingsRatio !== null && targetSavingsRatio <= 1 ? targetSavingsRatio : 0.8)))
85
131
  return {
86
132
  ok: true,
87
- mode: policy.mode,
133
+ mode: current.mode,
88
134
  baselineTokens: baseline,
89
- targetSavingsRatio: policy.targetSavingsRatio,
135
+ targetSavingsRatio: targetSavingsRatio !== null && targetSavingsRatio <= 1 ? targetSavingsRatio : 0.8,
90
136
  projectedOptimizedTokens: projectedOptimized,
91
137
  projectedSavingsTokens: baseline - projectedOptimized,
92
138
  basis: {
@@ -97,8 +143,8 @@ export function apply(ctx, config = {}) {
97
143
  },
98
144
  }
99
145
  },
100
- /** 归档后记录实测;3.3.0 起由 HarnessMM 的 EGD/GDR/CCS 自动上报。 */
101
- record({ taskId, baselineTokens, optimizedTokens, success } = {}) {
146
+ /** 归档后记录实测;3.3.0 起同时写系统层 execution 账本(baseline 给定时才算 saved)。 */
147
+ record({ taskId, baselineTokens, optimizedTokens, success, estimated = false, estimatedTokens = 0, forkId, sessionId } = {}) {
102
148
  const baseline = finiteNonNegative(baselineTokens)
103
149
  const optimized = finiteNonNegative(optimizedTokens)
104
150
  if (baseline === null) return masterError('baseline-tokens-invalid', 'baselineTokens must be a non-negative number')
@@ -112,13 +158,125 @@ export function apply(ctx, config = {}) {
112
158
  metrics.observedTotal += 1
113
159
  if (success) metrics.observedSuccess += 1
114
160
  }
161
+ const meta = { success: success === undefined ? null : Boolean(success) }
162
+ // §5.1:估算与实测分列,绝不混算。两部分各自入账(baseline 按比例拆分,保证 sum 一致)。
163
+ const estimatedPart = Math.min(optimized, Math.max(0, Number(estimatedTokens) || 0))
164
+ const measuredPart = optimized - estimatedPart
165
+ const entries = []
166
+ if (measuredPart > 0 || estimatedPart === 0) {
167
+ const measuredBaseline = estimatedPart === 0 ? baseline : Math.round(baseline * (measuredPart / optimized))
168
+ const measured = system.recordUsage({
169
+ account: 'execution',
170
+ module: 'unattributed',
171
+ source: 'model',
172
+ phase: 'completion',
173
+ taskId: taskId === undefined ? null : String(taskId),
174
+ forkId,
175
+ sessionId,
176
+ totalTokens: measuredPart,
177
+ baselineTokens: measuredBaseline,
178
+ estimated: estimated === true && estimatedPart === 0,
179
+ meta,
180
+ })
181
+ if (!measured.ok) return masterError(measured.code, 'ledger append failed')
182
+ entries.push(measured.entry.id)
183
+ }
184
+ if (estimatedPart > 0) {
185
+ const estimatedBaseline = Math.max(0, baseline - Math.round(baseline * (measuredPart / optimized)))
186
+ const measured = system.recordUsage({
187
+ account: 'execution',
188
+ module: 'unattributed',
189
+ source: 'model',
190
+ phase: 'completion',
191
+ taskId: taskId === undefined ? null : String(taskId),
192
+ forkId,
193
+ sessionId,
194
+ totalTokens: estimatedPart,
195
+ baselineTokens: estimatedBaseline,
196
+ estimated: true,
197
+ meta: { ...meta, estimatedFallback: true },
198
+ })
199
+ if (!measured.ok) return masterError(measured.code, 'ledger append failed')
200
+ entries.push(measured.entry.id)
201
+ }
115
202
  return {
116
203
  ok: true,
117
204
  taskId: taskId === undefined ? null : String(taskId),
118
205
  savedTokens: baseline - optimized,
206
+ ledger: { ids: entries, measuredTokens: measuredPart, estimatedTokens: estimatedPart },
119
207
  summary: service.summary(),
120
208
  }
121
209
  },
210
+ /** 3.3.0 M2:A1 归因。enforce 记入 execution 账本;shadow 只累计「本可节省」。 */
211
+ recordAttribution({ taskId, module, savedTokens, mode = 'shadow', meta, forkId, sessionId } = {}) {
212
+ return system.attribute({ taskId, forkId, sessionId, module, savedTokens, mode, meta })
213
+ },
214
+ /** 3.3.0 §5.5:写替换证书 + 对应账本事件(共用 taskId+step)。 */
215
+ recordSubstitution(cert = {}) {
216
+ return system.substitute(cert)
217
+ },
218
+ /** 3.3.0 §5.5:按 taskId(+step) 回放「这一省是怎么来的」。 */
219
+ trace({ taskId, step } = {}) {
220
+ return system.trace({ taskId, step })
221
+ },
222
+ /** 3.3.0 §6 A2:工具装载成本记 **governance**(明确归类,绝不算节省)。 */
223
+ recordGovernance({ taskId, module = 'a2-disclosure', tokens = 0, kind = 'tools.load', meta, forkId, sessionId } = {}) {
224
+ const amount = finiteNonNegative(tokens)
225
+ if (amount === null) return masterError('tokens-invalid', 'tokens must be a non-negative number')
226
+ const out = system.recordUsage({
227
+ account: 'governance',
228
+ module,
229
+ source: 'authoring',
230
+ phase: 'authoring',
231
+ taskId: taskId === undefined ? null : String(taskId),
232
+ forkId,
233
+ sessionId,
234
+ totalTokens: amount,
235
+ meta: { ...(meta && typeof meta === 'object' ? meta : {}), kind },
236
+ })
237
+ if (!out.ok) return masterError(out.code, out.message ?? 'governance ledger append failed')
238
+ return { ok: true, account: 'governance', module, tokens: amount, ledger: { id: out.entry.id } }
239
+ },
240
+ /** M1 交付物:记账报表(估算/实测分列、两本账不合并、外部面单列)。 */
241
+ ledgerReport(input = {}) {
242
+ return system.ledgerReport(input)
243
+ },
244
+ /** 3.3.0 §10.1:七项验收指标(必须给全;缺项返回 complete:false + missing[])。 */
245
+ metrics(input = {}) {
246
+ return system.metrics(input)
247
+ },
248
+ /** 3.3.0 §8:A(单价)/ B(跳过调用)分列报表 + 逐任务净额(净亏不隐藏)。 */
249
+ savingsReport(input = {}) {
250
+ return system.savingsReport(input)
251
+ },
252
+ /** 3.3.0 §5.4:总额结算,差额自动进 unattributed(不 blame 到任何模块)。 */
253
+ recordAttributionTotal({ taskId, totalSavedTokens, items = [], mode = 'shadow', forkId, sessionId } = {}) {
254
+ return system.attributeTotal({ taskId, totalSavedTokens, items, mode, forkId, sessionId })
255
+ },
256
+ /** 3.3.0 目标 2.1-5:模块异常降级留痕(审计 + 事件总线,不计入节省)。 */
257
+ recordDegraded({ taskId, module, phase = 'transform', message = '', count = 1 } = {}) {
258
+ return system.degraded({ taskId, module, phase, message, count })
259
+ },
260
+ /** 3.3.0:宿主会话工具面(外部插件)收益登记;只作对照,不计入本项目节省。 */
261
+ recordHostSurface({ taskId, savedTokens, meta } = {}) {
262
+ return system.hostSurface.record({ taskId, savedTokens, meta })
263
+ },
264
+ /** 3.3.0 §5.1:按维度查询账本(task/fork/session/module/account/phase/时间窗)。 */
265
+ query(filter = {}) {
266
+ return system.query(filter)
267
+ },
268
+ /** 3.3.0 §5.1:从 JSONL 回放。 */
269
+ replayLedger(direction = 'forward') {
270
+ return system.replay(direction)
271
+ },
272
+ /** 3.3.0:归因快照(按模块与任务聚合)。 */
273
+ attribution() {
274
+ return system.attribution.snapshot()
275
+ },
276
+ /** 3.3.0:订阅成本事件(宿主可转发给瓮城回流/信息栏);返回取消订阅函数。 */
277
+ subscribeCost(listener) {
278
+ return system.subscribe(listener)
279
+ },
122
280
  summary() {
123
281
  const ratio = metrics.baselineTokens > 0 ? metrics.savedTokens / metrics.baselineTokens : 0
124
282
  const successRate = metrics.observedTotal > 0 ? metrics.observedSuccess / metrics.observedTotal : null
@@ -126,30 +284,159 @@ export function apply(ctx, config = {}) {
126
284
  },
127
285
  reset() {
128
286
  Object.assign(metrics, { tasks: 0, baselineTokens: 0, optimizedTokens: 0, savedTokens: 0, observedSuccess: 0, observedTotal: 0 })
287
+ system.ledger.reset()
288
+ system.budget.reset()
289
+ system.attribution.reset()
290
+ system.bus.reset()
129
291
  return { ok: true }
130
292
  },
293
+ /** 3.3.0 系统层:账本快照(两本账 + 归因)。 */
294
+ ledger() {
295
+ return system.ledger.snapshot()
296
+ },
297
+ /** M4:账本事件查询(证书链核对需要原始事件,不只要快照)。 */
298
+ ledgerEntries(filter = {}) {
299
+ return system.query(filter)
300
+ },
301
+ /** 3.3.0 系统层:预算授权(① 唯一检查点;超限 masterError + 结构化 remaining)。 */
302
+ authorizeBudget({ unit, key = 'default', tokens = 0, cost = 0 } = {}) {
303
+ return system.authorize({ unit, key, tokens, cost })
304
+ },
305
+ /** 3.3.0 系统层:当前账单 + 预算 + 审计 + 节省口径。 */
306
+ cost() {
307
+ return system.status()
308
+ },
309
+ /** 3.3.0 §5.3:解析某上下文的生效档(global → fork → task,取最严格)。 */
310
+ resolvePolicy(context = {}) {
311
+ return system.policy.resolve(context)
312
+ },
313
+ /** 3.3.0 §5.3:守卫命中上报(observe 只计数 / shadow 落证书不替换 / enforce 才替换)。 */
314
+ recordGuardHit(input = {}) {
315
+ return system.policy.recordHit(input)
316
+ },
317
+ /** 3.3.0 §5.3:设置 fork/task 级档位。 */
318
+ setScope(scope, id, next) {
319
+ return system.policy.setScope(scope, id, next)
320
+ },
321
+ /** 3.3.0 §5.3:三档命中统计与证书。 */
322
+ policyStats() {
323
+ return system.policy.stats()
324
+ },
325
+ /** 3.3.0 系统层:灰度模式(enforce 需过门禁,见 policy.requestMode)。 */
326
+ requestMode(next, evidence = {}) {
327
+ return system.policy.requestMode(next, evidence)
328
+ },
329
+ /** 回滚:任何时刻可降级(observe 一行回滚)。 */
330
+ setMode(next) {
331
+ return system.policy.setMode(next)
332
+ },
333
+ audit() {
334
+ return system.audit.snapshot()
335
+ },
336
+ /** M4:证书列表(供证书链核对报表用;只读)。 */
337
+ substitutions(filter = {}) {
338
+ return system.audit.substitutions(filter)
339
+ },
340
+ /** M5:瓮城回流出口状态(只读;不可用时必须带具体原因)。 */
341
+ relayStatus() {
342
+ return system.relayStatus()
343
+ },
344
+ /** M5:立即探测回流对端(hello 协商 + relay 状态),返回最新状态。 */
345
+ async relayProbe(input = {}) {
346
+ return await system.relayProbe(input)
347
+ },
348
+ /** M5:立即投递队列中的回流事件;回执里的数字才是送达证据。 */
349
+ async relayFlush() {
350
+ return await system.relayFlush()
351
+ },
131
352
  status() {
353
+ const current = system.policy.describe()
132
354
  return {
133
355
  ok: true,
134
356
  id: 'cost-optimizer',
135
- version: '3.2.5',
357
+ version: '3.3.1',
136
358
  role,
137
- mode: policy.mode,
138
- requestedMode: policy.requestedMode,
139
- targetSavingsRatio: policy.targetSavingsRatio,
359
+ mode: current.mode,
360
+ requestedMode: current.requestedMode,
361
+ targetSavingsRatio: targetSavingsRatio !== null && targetSavingsRatio <= 1 ? targetSavingsRatio : 0.8,
140
362
  benchmark: HARNESSMM_BENCHMARK,
141
363
  summary: service.summary(),
142
- note: '3.2.0 observe-only:不拦截/不改写模型调用;EGD/GDR/CCS 在 3.3.0 接入。',
364
+ policy: current,
365
+ policyStats: system.policy.stats(),
366
+ budget: system.budget.state(),
367
+ ledger: system.ledger.snapshot(),
368
+ audit: system.audit.verify(),
369
+ shadow: Object.fromEntries(system.attribution.snapshot().modules.map((row) => [row.module, {
370
+ events: row.events,
371
+ projectedTokens: row.projectedTokens,
372
+ appliedTokens: row.enforceSavedTokens,
373
+ }])),
374
+ attribution: system.attribution.snapshot(),
375
+ attributionSummary: system.status().attributionSummary,
376
+ bus: system.bus.snapshot(1),
377
+ /** §5.6 / M5:瓮城回流出口(可达 ⇒ available:true + 对端自报 peer;否则带原因)。 */
378
+ relay: system.relayStatus(),
379
+ /** §5.6:内部事件类型(冒号命名)与对外出口状态。 */
380
+ events: { types: ['cost:event', 'cost:degraded', 'cost:substitution'], relays: system.bus.snapshot(1).relays },
381
+ degraded: system.status().degraded,
382
+ surfaces: system.status().surfaces,
383
+ hostSurface: system.hostSurface.status(),
384
+ tokens: system.status().tokens,
385
+ savings: system.status().savings,
386
+ /** §8:两项分列的总节省(单价项 + 跳过调用项),避免只报合计。 */
387
+ savingsColumns: (() => {
388
+ const report = system.savingsReport({ limit: 5000 })
389
+ return {
390
+ unitPriceSaved: report.columns.unitPrice.savedTokens,
391
+ skippedCallSaved: report.columns.skippedCalls.savedTokens,
392
+ skippedCalls: report.columns.skippedCalls.calls,
393
+ governanceTotal: report.governance.totalTokens,
394
+ netSaved: report.netSaved,
395
+ netLossTasks: report.netLossTasks.map((task) => task.taskId),
396
+ doubleCountGuard: report.doubleCountGuard,
397
+ }
398
+ })(),
399
+ note: '3.3.1:系统层记账/预算/灰度/审计 + A1 工具输出紧凑化(默认 shadow)+ 瓮城回流出口(contract 1.1 真实投递);工具面 A2–A3 与决策复用 B1–B4 已接入(shadow 默认)。',
143
400
  }
144
401
  },
145
402
  }
146
403
 
147
404
  try { ctx.effect(() => ctx.provide(SERVICE.cost, service)) } catch { /* fake ctx */ }
405
+ try {
406
+ ctx.effect(() => () => {
407
+ if (relayTimer) clearInterval(relayTimer)
408
+ relayTimer = null
409
+ })
410
+ } catch { /* fake ctx */ }
411
+ // 3.3.0 §11:可选子服务(主服务形态不变;消费方必须探测,不能假定存在)。
412
+ const optionalFaces = {
413
+ [OPTIONAL_SERVICE.costLedger]: Object.freeze({
414
+ query: (input) => system.query(input),
415
+ replay: (direction) => system.replay(direction),
416
+ snapshot: () => system.status().ledger,
417
+ }),
418
+ [OPTIONAL_SERVICE.costBudget]: Object.freeze({
419
+ authorize: (input) => system.authorize(input),
420
+ snapshot: () => system.status().budget,
421
+ }),
422
+ [OPTIONAL_SERVICE.costPolicy]: Object.freeze({
423
+ snapshot: () => system.status().policy,
424
+ stats: () => system.status().policyStats,
425
+ }),
426
+ [OPTIONAL_SERVICE.costAudit]: Object.freeze({
427
+ verify: () => system.audit().verify(),
428
+ substitutions: (filter) => system.audit().substitutions(filter),
429
+ trace: (input) => system.trace(input),
430
+ }),
431
+ }
432
+ for (const [name, face] of Object.entries(optionalFaces)) {
433
+ try { ctx.effect(() => ctx.provide(name, face)) } catch { /* 不支持可选子服务的运行时也无妨 */ }
434
+ }
148
435
  master.registerSubplugin({
149
436
  id: 'cost-optimizer',
150
437
  name: 'dsh-master-cost-optimizer',
151
- capabilities: ['token-estimate', 'savings-record', 'cost-status'],
152
- description: 'HarnessMM observe 层(仅 fork)',
438
+ capabilities: ['token-estimate', 'savings-record', 'cost-status', 'cost-ledger', 'cost-budget', 'cost-policy', 'cost-audit'],
439
+ description: 'HarnessMM observe 层(仅 fork;3.3.0 M1 系统层)',
153
440
  })
154
441
  return service
155
442
  }
@@ -49,6 +49,8 @@ const EVENTS_PATH = '/dsh-master/api/superagent/worker-events'
49
49
  const STREAM_PATH = '/dsh-master/api/superagent/stream'
50
50
  const STREAM_TOKEN_PATH = '/dsh-master/api/superagent/stream-token'
51
51
  const STATUS_PATH = '/dsh-master/api/superagent/status'
52
+ /** §5.6:成本事件摘要(状态页 / 分身信息栏读取)。 */
53
+ const COST_PATH = '/dsh-master/api/superagent/cost'
52
54
  const SESSION_PATH = '/dsh-master/api/superagent/session'
53
55
  const PR_PATH = '/dsh-master/api/superagent/pr'
54
56
  const GITHUB_PATH = '/dsh-master/api/superagent/github'
@@ -281,6 +283,87 @@ export function apply(ctx, config = {}) {
281
283
  if (token) tokenManager.revoke(token, 'ticket-done')
282
284
  }
283
285
 
286
+ /**
287
+ * §5.6:把 Worker 上报的 `cost` 事件按 ticket 汇成只读摘要,
288
+ * 供状态页与「分身信息栏」读取(不暴露任何 token)。
289
+ */
290
+ const costSummary = (ticketId) => {
291
+ const id = ticketId === undefined ? null : String(ticketId)
292
+ // §11:只读成本块的数据源——系统层 savings 两列(A 单价 / B 跳过调用)+ 命中率。
293
+ const serviceBlock = (() => {
294
+ try {
295
+ const cost = safeGet(ctx, SERVICE.cost)
296
+ if (!cost) return null
297
+ const status = typeof cost.status === 'function' ? cost.status() : null
298
+ const report = typeof cost.savingsReport === 'function' ? cost.savingsReport({ amortize: 'even', limit: 2000 }) : null
299
+ return {
300
+ savings: status?.savings ?? null,
301
+ columns: status?.savingsColumns ?? null,
302
+ policy: status?.policy ?? null,
303
+ ledger: status?.ledger?.entries !== undefined ? { entries: status.ledger.entries } : null,
304
+ unitPriceSaved: report?.columns?.unitPrice?.savedTokens ?? 0,
305
+ skippedCallSaved: report?.columns?.skippedCalls?.savedTokens ?? 0,
306
+ skippedCalls: report?.columns?.skippedCalls?.calls ?? 0,
307
+ netLossTasks: report?.netLossTasks?.map((task) => task.taskId) ?? [],
308
+ doubleCountGuard: report?.doubleCountGuard === true,
309
+ /** 守卫命中率需要各任务上报的 guard 统计聚合;未聚合前如实给 null,不猜。 */
310
+ hitRate: null,
311
+ }
312
+ } catch { return null }
313
+ })()
314
+ const tickets = id === null ? [...workerEvents.keys()] : [id]
315
+ /**
316
+ * §5.6 / M5:回流出口状态来自成本服务(3.3.1 起是真实探测,不再是硬编码)。
317
+ * 没有成本服务时如实给 `cost-service-unavailable`,不编造“未就绪”原因。
318
+ */
319
+ const relayBlock = (() => {
320
+ try {
321
+ const cost = safeGet(ctx, SERVICE.cost)
322
+ if (cost && typeof cost.relayStatus === 'function') {
323
+ const relay = cost.relayStatus()
324
+ if (relay && typeof relay === 'object') return relay
325
+ }
326
+ } catch { /* 回流状态读取失败不阻塞成本摘要 */ }
327
+ return { target: 'fang', available: false, reason: 'cost-service-unavailable' }
328
+ })()
329
+ const rows = []
330
+ const totals = { events: 0, degraded: 0, substitutions: 0, estimated: 0, savedTokens: 0 }
331
+ for (const key of tickets) {
332
+ const events = (workerEvents.get(key) ?? []).filter((event) => event && event.type === 'cost')
333
+ if (events.length === 0) continue
334
+ const row = { ticketId: key, events: 0, degraded: 0, substitutions: 0, estimated: 0, savedTokens: 0, points: {}, modules: {} }
335
+ for (const event of events) {
336
+ const payload = event.payload ?? {}
337
+ row.events += 1
338
+ row.points[String(payload.point ?? 'unknown')] = (row.points[String(payload.point ?? 'unknown')] ?? 0) + 1
339
+ if (payload.point === 'degraded') row.degraded += 1
340
+ if (payload.estimated === true) row.estimated += 1
341
+ const compaction = payload.compaction
342
+ if (compaction && compaction.applied === true) {
343
+ row.savedTokens += Math.max(0, Number(compaction.estimatedTokens ?? 0) || 0)
344
+ const moduleKey = String(compaction.module ?? 'a1-compact')
345
+ row.modules[moduleKey] = (row.modules[moduleKey] ?? 0) + Math.max(0, Number(compaction.estimatedTokens ?? 0) || 0)
346
+ }
347
+ }
348
+ totals.events += row.events
349
+ totals.degraded += row.degraded
350
+ totals.substitutions += row.substitutions
351
+ totals.estimated += row.estimated
352
+ totals.savedTokens += row.savedTokens
353
+ rows.push(row)
354
+ }
355
+ return {
356
+ ok: true,
357
+ ticketId: id,
358
+ tickets: rows,
359
+ totals,
360
+ /* §11:只读成本块(A 单价项 / B 跳过调用项分列 + 命中率),面板只读消费。 */
361
+ service: serviceBlock,
362
+ /* §5.6 / M5:瓮城回流出口真实状态(不可用必须显式带原因)。 */
363
+ relay: relayBlock,
364
+ }
365
+ }
366
+
284
367
  const service = {
285
368
  id: 'superagent-router',
286
369
  version: '3.2.5',
@@ -290,6 +373,8 @@ export function apply(ctx, config = {}) {
290
373
  forkerService: hasForker(),
291
374
  streaming: { configured: Boolean(origin), ...broadcaster.describe(), tokens: tokenManager.counts() },
292
375
  workerEvents: Object.fromEntries([...workerEvents.entries()].map(([k, v]) => [k, v.length])),
376
+ cost: costSummary().totals,
377
+ costRelay: costSummary().relay,
293
378
  writeback: writeback.describe(),
294
379
  quota: quotaStatus(),
295
380
  sessions: { count: sessionMap.size, last: [...sessionMap.values()].slice(-3) },
@@ -307,6 +392,8 @@ export function apply(ctx, config = {}) {
307
392
  workerEvents(ticketId) {
308
393
  return { ok: true, ticketId: String(ticketId ?? ''), events: workerEvents.get(String(ticketId ?? '')) ?? [] }
309
394
  },
395
+ /** §5.6:成本事件摘要(状态页/分身信息栏的数据源)。 */
396
+ cost(ticketId) { return costSummary(ticketId) },
310
397
  tokenAudit(limit) { return tokenManager.audit(limit) },
311
398
  rotateStreamToken(oldToken = '') { return tokenManager.rotate(oldToken, { kind: 'ui' }) },
312
399
  quota: () => quotaStatus(),
@@ -431,6 +518,14 @@ export function apply(ctx, config = {}) {
431
518
  return jsonResponse(res, 200, { ok: true, session: mapped })
432
519
  },
433
520
  })
521
+ register({
522
+ kind: 'exact', path: COST_PATH, method: 'GET',
523
+ handler: async (req, res) => {
524
+ const query = new URL(String(req?.url ?? '/'), 'http://x').searchParams
525
+ const ticketId = query.get('ticketId')
526
+ return jsonResponse(res, 200, service.cost(ticketId === null ? undefined : String(ticketId)))
527
+ },
528
+ })
434
529
  register({
435
530
  kind: 'exact', path: GITHUB_PATH, method: 'POST',
436
531
  handler: async (req, res) => {
@@ -462,4 +557,4 @@ export function apply(ctx, config = {}) {
462
557
  return service
463
558
  }
464
559
 
465
- export { EVENTS_PATH, STREAM_PATH, STREAM_TOKEN_PATH, STATUS_PATH, SESSION_PATH, readRawBody }
560
+ export { EVENTS_PATH, STREAM_PATH, STREAM_TOKEN_PATH, STATUS_PATH, SESSION_PATH, COST_PATH, readRawBody }
@@ -133,3 +133,73 @@ export function publicTicket(ticket, extra = {}) {
133
133
  ...extra,
134
134
  }
135
135
  }
136
+
137
+ /* ------------------------------------------------------------------ *
138
+ * 3.3.0 §11:成本域对外类型(宿主侧)
139
+ *
140
+ * 这些是**跨进程/跨插件边界的形状**:Worker 侧同名类型在 worker/*.ts,
141
+ * 宿主侧以本文件为准。字段名保持一致,便于证书与事件在两侧无损传递。
142
+ * ------------------------------------------------------------------ */
143
+
144
+ /** 成本事件类型(§5.6 事件总线)。 */
145
+ export const COST_EVENT_TYPE = Object.freeze(['cost:event', 'cost:degraded', 'cost:substitution'])
146
+
147
+ /** 单条成本事件(宿主 → 订阅者 / 瓮城回流)。 */
148
+ export interface CostEventEnvelope {
149
+ type: 'cost:event' | 'cost:degraded' | 'cost:substitution'
150
+ ticketId?: string
151
+ module?: string
152
+ phase?: string
153
+ account?: 'execution' | 'governance'
154
+ savedTokens?: number
155
+ totalTokens?: number
156
+ at: number
157
+ detail?: Record<string, unknown>
158
+ }
159
+
160
+ /** 可执行决策图的一条边(B1,宿主侧镜像)。 */
161
+ export interface EdgEdgeView {
162
+ id: string
163
+ toolName: string
164
+ schemaHash: string
165
+ argSource: Array<{ arg: string; from: 'literal' | 'regex' | 'jsonpath'; expr: string }>
166
+ guard: { kind: 'regex' | 'jsonpath' | 'equals'; expr: string; negate?: boolean }
167
+ destructive: boolean
168
+ dependsOn?: string[]
169
+ }
170
+
171
+ /** 替换证书(B3,宿主侧镜像;审计链里的记录也长这样)。 */
172
+ export interface SubstitutionCertView {
173
+ edgeId: string
174
+ toolName: string
175
+ schemaHash: string
176
+ guardExpr: string
177
+ receiptDigest: string
178
+ replacedCallbackId: string
179
+ viaNativeDispatcher: true
180
+ taskId: string
181
+ step: number
182
+ dispatched?: boolean
183
+ error?: string
184
+ savedTokens?: number
185
+ moduleId?: string
186
+ at?: number
187
+ }
188
+
189
+ /** 只读成本块(§11 client.js「房主」抽屉用;不含任何业务状态写入能力)。 */
190
+ export interface CostSummaryView {
191
+ enabled: boolean
192
+ mode?: string
193
+ savings: { executionSaved: number; governanceCost: number; netSaved: number }
194
+ savingsColumns?: {
195
+ unitPriceSaved: number
196
+ skippedCallSaved: number
197
+ skippedCalls: number
198
+ governanceTotal: number
199
+ netSaved: number
200
+ netLossTasks: string[]
201
+ doubleCountGuard: boolean
202
+ }
203
+ hitRate?: number | null
204
+ degraded?: number
205
+ }