@twinsearth/dsh-master 0.0.0-stage → 3.2.9
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/LICENSE +661 -0
- package/README.md +93 -2
- package/client.js +334 -0
- package/cordis.patch.yml +71 -0
- package/dist-host/contract/client.js +82 -0
- package/dist-host/contract/constants.js +70 -0
- package/dist-host/contract/envelope.js +84 -0
- package/dist-host/contract/index.js +17 -0
- package/dist-host/contract/negotiate.js +87 -0
- package/dist-host/contract/server.js +71 -0
- package/dist-host/contract/transports.js +360 -0
- package/dist-host/index.js +260 -0
- package/dist-host/kernel.js +186 -0
- package/dist-host/plugins/cost-optimizer.js +147 -0
- package/dist-host/plugins/dual-manager.js +251 -0
- package/dist-host/plugins/superagent-router.js +499 -0
- package/dist-host/plugins/superagent-worker.js +73 -0
- package/dist-host/superagent/bridge-tokens.js +236 -0
- package/dist-host/superagent/dispatch.js +198 -0
- package/dist-host/superagent/feishu.js +115 -0
- package/dist-host/superagent/github.js +119 -0
- package/dist-host/superagent/pr-delivery.js +351 -0
- package/dist-host/superagent/router.js +219 -0
- package/dist-host/superagent/stream-adapter.js +238 -0
- package/dist-host/superagent/stream-tokens.js +141 -0
- package/dist-host/superagent/ticket-store.js +108 -0
- package/dist-host/superagent/trust.js +145 -0
- package/dist-host/superagent/types.js +144 -0
- package/dist-host/superagent/worker.js +95 -0
- package/dist-host/superagent/writeback.js +162 -0
- package/dist-worker/agent-loop.js +493 -0
- package/dist-worker/errors.js +41 -0
- package/dist-worker/github-pr.js +44 -0
- package/dist-worker/index.js +323 -0
- package/dist-worker/llm-http.js +351 -0
- package/dist-worker/main.js +45 -0
- package/dist-worker/prompt.js +35 -0
- package/dist-worker/sink-http.js +135 -0
- package/dist-worker/tools.js +542 -0
- package/dist-worker/types.js +14 -0
- package/docs/ARCHITECTURE.md +114 -0
- package/docs/CHANGELOG.md +389 -0
- package/docs/INTEROP.md +60 -0
- package/docs/RELEASE-v3.2.8.md +27 -0
- package/docs/RELEASE-v3.2.9.md +29 -0
- package/docs/REVIEW-v3.2.7.md +121 -0
- package/docs/ROADMAP.md +106 -0
- package/docs/SECURITY.md +153 -0
- package/docs/SMOKE-3.2.5.md +77 -0
- package/docs/SMOKE-3.2.7.md +73 -0
- package/docs/SUPERAGENT.md +141 -0
- package/docs/WORKER-AGENT-LOOP.md +139 -0
- package/icon.svg +9 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +79 -4
- package/src/contract/client.js +86 -0
- package/src/contract/constants.js +79 -0
- package/src/contract/envelope.js +83 -0
- package/src/contract/index.js +19 -0
- package/src/contract/negotiate.js +106 -0
- package/src/contract/server.js +77 -0
- package/src/contract/transports.js +319 -0
- package/src/index.ts +265 -0
- package/src/kernel.ts +190 -0
- package/src/plugins/cost-optimizer.ts +155 -0
- package/src/plugins/dual-manager.ts +252 -0
- package/src/plugins/superagent-router.ts +465 -0
- package/src/plugins/superagent-worker.ts +85 -0
- package/src/superagent/bridge-tokens.ts +236 -0
- package/src/superagent/dispatch.ts +197 -0
- package/src/superagent/feishu.ts +104 -0
- package/src/superagent/github.ts +111 -0
- package/src/superagent/pr-delivery.ts +308 -0
- package/src/superagent/router.ts +201 -0
- package/src/superagent/stream-adapter.ts +184 -0
- package/src/superagent/stream-tokens.ts +142 -0
- package/src/superagent/ticket-store.ts +104 -0
- package/src/superagent/trust.ts +139 -0
- package/src/superagent/types.ts +135 -0
- package/src/superagent/worker.ts +100 -0
- package/src/superagent/writeback.ts +154 -0
- package/tsconfig.host.build.json +12 -0
- package/tsconfig.host.json +21 -0
- package/tsconfig.worker.build.json +13 -0
- package/tsconfig.worker.json +17 -0
- package/types/superagent.d.ts +99 -0
- package/worker/agent-loop.ts +524 -0
- package/worker/errors.ts +47 -0
- package/worker/github-pr.ts +65 -0
- package/worker/index.ts +334 -0
- package/worker/llm-http.ts +359 -0
- package/worker/main.ts +45 -0
- package/worker/prompt.ts +39 -0
- package/worker/sink-http.ts +159 -0
- package/worker/tools.ts +531 -0
- package/worker/types.ts +247 -0
package/src/kernel.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
// @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Dsh-Master(房主)共享内核(v3.2.3)。
|
|
7
|
+
* 只放纯函数与常量;Node 侧工具用到的 crypto 是唯一内置模块依赖。
|
|
8
|
+
* 与瓮城 dsh-Fang 的协作只走 DSH 服务键,不 import fang 代码。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import crypto from 'node:crypto'
|
|
12
|
+
|
|
13
|
+
export const MASTER_VERSION = '3.2.9'
|
|
14
|
+
export const FANG_CONTRACT_MIN = '3.1.14'
|
|
15
|
+
|
|
16
|
+
export const SERVICE = Object.freeze({
|
|
17
|
+
master: 'dshMaster',
|
|
18
|
+
superagentRouter: 'dshMasterSuperagentRouter',
|
|
19
|
+
superagentWorker: 'dshMasterSuperagentWorker',
|
|
20
|
+
cost: 'dshMasterCost',
|
|
21
|
+
dual: 'dshMasterDual',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const FANG_SERVICE = Object.freeze({
|
|
25
|
+
orchestrator: 'fang',
|
|
26
|
+
forker: 'fangForker',
|
|
27
|
+
decisionCenter: 'fangDecisionCenter',
|
|
28
|
+
sessionCatalog: 'fangSessionCatalog',
|
|
29
|
+
sessionBridge: 'fangSessionBridge',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export const ROW = Object.freeze({
|
|
33
|
+
core: 'master-core',
|
|
34
|
+
superagentRouter: 'master-superagent-router',
|
|
35
|
+
superagentWorker: 'master-superagent-worker',
|
|
36
|
+
superagentLoop: 'master-superagent-loop',
|
|
37
|
+
cost: 'master-cost-optimizer',
|
|
38
|
+
dual: 'master-dual-manager',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export const ROLE = Object.freeze({ mother: 'mother', essence: 'essence', fork: 'fork' })
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 角色矩阵(v3.2.3):
|
|
45
|
+
* * Router 常驻原身(essence):只评估/建票/派发,不执行代码;母体不接纳。
|
|
46
|
+
* * Worker 是研究原型,只在 fork:编译后的 Worker preset 行 master-superagent-loop 也只 fork。
|
|
47
|
+
* * 成本优化 fork;双主管 mother/essence。
|
|
48
|
+
*/
|
|
49
|
+
export const ROLE_MATRIX = Object.freeze({
|
|
50
|
+
'master-core': Object.freeze(Object.values(ROLE)),
|
|
51
|
+
'master-superagent-router': Object.freeze([ROLE.essence]),
|
|
52
|
+
'master-superagent-worker': Object.freeze([ROLE.fork]),
|
|
53
|
+
'master-superagent-loop': Object.freeze([ROLE.fork]),
|
|
54
|
+
'master-cost-optimizer': Object.freeze([ROLE.fork]),
|
|
55
|
+
'master-dual-manager': Object.freeze([ROLE.mother, ROLE.essence]),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export const SUBPLUGIN_CATALOG = Object.freeze([
|
|
59
|
+
Object.freeze({
|
|
60
|
+
id: 'superagent-router', row: ROW.superagentRouter, name: '超级智能体 Router', nameEn: 'SuperAgent Router',
|
|
61
|
+
roles: ROLE_MATRIX[ROW.superagentRouter], roadmap: '3.2.1',
|
|
62
|
+
description: '常驻原身:Webhook → 评估 → Ticket/Mandate → 派发;不执行代码改动。',
|
|
63
|
+
}),
|
|
64
|
+
Object.freeze({
|
|
65
|
+
id: 'superagent-worker', row: ROW.superagentWorker, name: '超级智能体 Worker', nameEn: 'SuperAgent Worker',
|
|
66
|
+
roles: ROLE_MATRIX[ROW.superagentWorker], roadmap: '3.2.1',
|
|
67
|
+
description: '每个工单一个隔离分身:在 Mandate 作用域内执行;研究原型,仅 fork。',
|
|
68
|
+
}),
|
|
69
|
+
Object.freeze({
|
|
70
|
+
id: 'superagent-loop', row: ROW.superagentLoop, name: 'Worker Agent Loop', nameEn: 'SuperAgent Worker Loop',
|
|
71
|
+
roles: ROLE_MATRIX[ROW.superagentLoop], roadmap: '3.2.3',
|
|
72
|
+
description: '编译后的 Worker preset:有 Ticket 的 fork 自动跑多轮工具循环并流式回传。',
|
|
73
|
+
}),
|
|
74
|
+
Object.freeze({
|
|
75
|
+
id: 'cost-optimizer', row: ROW.cost, name: '成本优化', nameEn: 'HarnessMM',
|
|
76
|
+
roles: ROLE_MATRIX[ROW.cost], roadmap: '3.3.0',
|
|
77
|
+
description: 'HarnessMM observe 层;仅 fork,默认 observe。',
|
|
78
|
+
}),
|
|
79
|
+
Object.freeze({
|
|
80
|
+
id: 'dual-manager', row: ROW.dual, name: '双主管', nameEn: 'PM + PJM',
|
|
81
|
+
roles: ROLE_MATRIX[ROW.dual], roadmap: '3.4.0',
|
|
82
|
+
description: 'PM 产 PRD、PJM 调度分身;只允许 mother / essence。',
|
|
83
|
+
}),
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 从环境变量探测当前角色。
|
|
88
|
+
* 瓮城注入:母体 DSH_FANG_ROLE=mother;元宝运行态 fork+DSH_FANG_ESSENCE=1;
|
|
89
|
+
* 普通分身 DSH_FANG_ROLE=fork + DSH_FANG_TASK_ID=task_/rt-。
|
|
90
|
+
*/
|
|
91
|
+
export function detectRole(env = process.env) {
|
|
92
|
+
const raw = String(env?.DSH_FANG_ROLE ?? '').trim().toLowerCase()
|
|
93
|
+
const taskId = String(env?.DSH_FANG_TASK_ID ?? '')
|
|
94
|
+
const essence = String(env?.DSH_FANG_ESSENCE ?? '') === '1' || taskId.startsWith('rt-')
|
|
95
|
+
if (raw === 'mother') return ROLE.mother
|
|
96
|
+
if (raw === 'fork') return essence ? ROLE.essence : ROLE.fork
|
|
97
|
+
return essence ? ROLE.essence : ROLE.mother
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function roleAllowed(rowId, role) {
|
|
101
|
+
const allowed = ROLE_MATRIX[rowId]
|
|
102
|
+
return Array.isArray(allowed) ? allowed.includes(role) : true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function enabledOf(config) {
|
|
106
|
+
return !(config && typeof config === 'object' && config.enabled === false)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function safeGet(ctx, name) {
|
|
110
|
+
try {
|
|
111
|
+
if (!ctx || typeof ctx !== 'object') return null
|
|
112
|
+
if (typeof ctx.get === 'function') return ctx.get(name) ?? null
|
|
113
|
+
return ctx[name] ?? null
|
|
114
|
+
} catch {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function jsonResponse(res, status, body) {
|
|
120
|
+
try {
|
|
121
|
+
if (res && typeof res.statusCode === 'number') res.statusCode = status
|
|
122
|
+
res?.setHeader?.('content-type', 'application/json; charset=utf-8')
|
|
123
|
+
res?.setHeader?.('cache-control', 'no-store')
|
|
124
|
+
res?.end?.(JSON.stringify(body))
|
|
125
|
+
} catch {
|
|
126
|
+
/* ignore */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 读取请求 JSON body(大小上限;非法/超限返回 {ok:false,code})。 */
|
|
131
|
+
export async function readJsonBody(req, limit = 512 * 1024) {
|
|
132
|
+
try {
|
|
133
|
+
const chunks = []
|
|
134
|
+
let total = 0
|
|
135
|
+
if (req && typeof req[Symbol.asyncIterator] === 'function') {
|
|
136
|
+
for await (const chunk of req) {
|
|
137
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))
|
|
138
|
+
total += buffer.length
|
|
139
|
+
if (total > limit) return { ok: false, code: 'body-too-large' }
|
|
140
|
+
chunks.push(buffer)
|
|
141
|
+
}
|
|
142
|
+
} else if (req && typeof req.on === 'function') {
|
|
143
|
+
await new Promise((resolve, reject) => {
|
|
144
|
+
req.on('data', (chunk) => {
|
|
145
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))
|
|
146
|
+
total += buffer.length
|
|
147
|
+
if (total > limit) reject(new Error('body-too-large'))
|
|
148
|
+
else chunks.push(buffer)
|
|
149
|
+
})
|
|
150
|
+
req.on('end', resolve)
|
|
151
|
+
req.on('error', reject)
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
const raw = Buffer.concat(chunks).toString('utf8')
|
|
155
|
+
if (!raw) return { ok: true, body: {} }
|
|
156
|
+
return { ok: true, body: JSON.parse(raw) }
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return { ok: false, code: String(error?.message ?? error) === 'body-too-large' ? 'body-too-large' : 'invalid-json' }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 常量时间比较两个 token(长度不同直接 false)。 */
|
|
163
|
+
export function tokenEquals(received, expected) {
|
|
164
|
+
const a = Buffer.from(String(received ?? ''), 'utf8')
|
|
165
|
+
const b = Buffer.from(String(expected ?? ''), 'utf8')
|
|
166
|
+
if (a.length === 0 || a.length !== b.length) return false
|
|
167
|
+
return crypto.timingSafeEqual(a, b)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function bearerOf(headers) {
|
|
171
|
+
const raw = headers && typeof headers === 'object'
|
|
172
|
+
? (headers.authorization ?? headers.Authorization ?? '')
|
|
173
|
+
: ''
|
|
174
|
+
const match = /^Bearer\s+(\S+)$/i.exec(String(raw ?? '').trim())
|
|
175
|
+
return match ? match[1] : ''
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function masterError(code, message, detail) {
|
|
179
|
+
const out = { ok: false, code: String(code), error: String(message ?? code) }
|
|
180
|
+
if (detail !== undefined) out.detail = detail
|
|
181
|
+
return out
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function createIdFactory(prefix) {
|
|
185
|
+
let seq = 0
|
|
186
|
+
return () => {
|
|
187
|
+
seq += 1
|
|
188
|
+
return `${prefix}_${Date.now().toString(36)}_${seq.toString(36)}`
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
// @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 子插件 2:成本优化(HarnessMM 观察层)—— 为分身装“燃油表”。
|
|
7
|
+
*
|
|
8
|
+
* 3.2.0 先交付可观测/可估算的骨架,不做推理拦截:
|
|
9
|
+
* * `observe` 模式只统计/估算,不改写任何模型调用;
|
|
10
|
+
* * HarnessMM 的 EGD/GDR/CCS 真实接入在 3.3.0 里程碑(见 docs/ROADMAP.md);
|
|
11
|
+
* * 角色:只允许 fork(分身生产盘),母体/元主不加载。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
ROW,
|
|
16
|
+
ROLE,
|
|
17
|
+
SERVICE,
|
|
18
|
+
detectRole,
|
|
19
|
+
enabledOf,
|
|
20
|
+
masterError,
|
|
21
|
+
roleAllowed,
|
|
22
|
+
safeGet,
|
|
23
|
+
} from '../kernel.ts'
|
|
24
|
+
|
|
25
|
+
export const name = 'dsh-master-cost-optimizer'
|
|
26
|
+
export const inject = ['dshMaster']
|
|
27
|
+
export const ROW_ID = ROW.cost
|
|
28
|
+
|
|
29
|
+
/** HarnessMM 官方基准(3.2.0 文档引用;3.3.0 接入后应被实测值覆盖)。 */
|
|
30
|
+
export const HARNESSMM_BENCHMARK = Object.freeze({
|
|
31
|
+
baselineTokens: 96280,
|
|
32
|
+
optimizedTokens: 11620,
|
|
33
|
+
savingsRatio: 0.8793,
|
|
34
|
+
successRate: { baseline: 0.7143, optimized: 0.8571 },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function finiteNonNegative(value) {
|
|
38
|
+
const n = Number(value)
|
|
39
|
+
return Number.isFinite(n) && n >= 0 ? n : null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {object} ctx
|
|
44
|
+
* @param {{enabled?:boolean, mode?:string, targetSavingsRatio?:number}} [config]
|
|
45
|
+
*/
|
|
46
|
+
export function apply(ctx, config = {}) {
|
|
47
|
+
if (!enabledOf(config)) return { skipped: true, reason: 'disabled-by-config' }
|
|
48
|
+
const role = detectRole()
|
|
49
|
+
if (!roleAllowed(ROW_ID, role)) {
|
|
50
|
+
try {
|
|
51
|
+
ctx?.logger?.info?.(`[dsh-master] cost-optimizer skipped: role=${role} not in ${ROLE.fork}`)
|
|
52
|
+
} catch { /* ignore */ }
|
|
53
|
+
return { skipped: true, reason: `role-not-allowed:${role}` }
|
|
54
|
+
}
|
|
55
|
+
const master = safeGet(ctx, SERVICE.master)
|
|
56
|
+
if (!master || typeof master.registerSubplugin !== 'function') {
|
|
57
|
+
return { skipped: true, reason: 'master-core-unavailable' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
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,
|
|
65
|
+
})
|
|
66
|
+
const metrics = {
|
|
67
|
+
tasks: 0,
|
|
68
|
+
baselineTokens: 0,
|
|
69
|
+
optimizedTokens: 0,
|
|
70
|
+
savedTokens: 0,
|
|
71
|
+
observedSuccess: 0,
|
|
72
|
+
observedTotal: 0,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const service = {
|
|
76
|
+
id: SERVICE.cost,
|
|
77
|
+
version: '3.2.5',
|
|
78
|
+
role,
|
|
79
|
+
mode: policy.mode,
|
|
80
|
+
/** 基于 HarnessMM 基准 + 目标比例做派生前预估。 */
|
|
81
|
+
estimate({ baselineTokens } = {}) {
|
|
82
|
+
const baseline = finiteNonNegative(baselineTokens)
|
|
83
|
+
if (baseline === null) return masterError('baseline-tokens-invalid', 'baselineTokens must be a non-negative number')
|
|
84
|
+
const projectedOptimized = Math.round(baseline * (1 - policy.targetSavingsRatio))
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
mode: policy.mode,
|
|
88
|
+
baselineTokens: baseline,
|
|
89
|
+
targetSavingsRatio: policy.targetSavingsRatio,
|
|
90
|
+
projectedOptimizedTokens: projectedOptimized,
|
|
91
|
+
projectedSavingsTokens: baseline - projectedOptimized,
|
|
92
|
+
basis: {
|
|
93
|
+
source: 'HarnessMM benchmark',
|
|
94
|
+
baselineTokens: HARNESSMM_BENCHMARK.baselineTokens,
|
|
95
|
+
optimizedTokens: HARNESSMM_BENCHMARK.optimizedTokens,
|
|
96
|
+
savingsRatio: HARNESSMM_BENCHMARK.savingsRatio,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
/** 归档后记录实测;3.3.0 起由 HarnessMM 的 EGD/GDR/CCS 自动上报。 */
|
|
101
|
+
record({ taskId, baselineTokens, optimizedTokens, success } = {}) {
|
|
102
|
+
const baseline = finiteNonNegative(baselineTokens)
|
|
103
|
+
const optimized = finiteNonNegative(optimizedTokens)
|
|
104
|
+
if (baseline === null) return masterError('baseline-tokens-invalid', 'baselineTokens must be a non-negative number')
|
|
105
|
+
if (optimized === null) return masterError('optimized-tokens-invalid', 'optimizedTokens must be a non-negative number')
|
|
106
|
+
if (optimized > baseline) return masterError('optimized-exceeds-baseline', 'optimizedTokens must not exceed baselineTokens')
|
|
107
|
+
metrics.tasks += 1
|
|
108
|
+
metrics.baselineTokens += baseline
|
|
109
|
+
metrics.optimizedTokens += optimized
|
|
110
|
+
metrics.savedTokens += baseline - optimized
|
|
111
|
+
if (typeof success === 'boolean') {
|
|
112
|
+
metrics.observedTotal += 1
|
|
113
|
+
if (success) metrics.observedSuccess += 1
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
taskId: taskId === undefined ? null : String(taskId),
|
|
118
|
+
savedTokens: baseline - optimized,
|
|
119
|
+
summary: service.summary(),
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
summary() {
|
|
123
|
+
const ratio = metrics.baselineTokens > 0 ? metrics.savedTokens / metrics.baselineTokens : 0
|
|
124
|
+
const successRate = metrics.observedTotal > 0 ? metrics.observedSuccess / metrics.observedTotal : null
|
|
125
|
+
return { ...metrics, savingsRatio: ratio, successRate }
|
|
126
|
+
},
|
|
127
|
+
reset() {
|
|
128
|
+
Object.assign(metrics, { tasks: 0, baselineTokens: 0, optimizedTokens: 0, savedTokens: 0, observedSuccess: 0, observedTotal: 0 })
|
|
129
|
+
return { ok: true }
|
|
130
|
+
},
|
|
131
|
+
status() {
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
id: 'cost-optimizer',
|
|
135
|
+
version: '3.2.5',
|
|
136
|
+
role,
|
|
137
|
+
mode: policy.mode,
|
|
138
|
+
requestedMode: policy.requestedMode,
|
|
139
|
+
targetSavingsRatio: policy.targetSavingsRatio,
|
|
140
|
+
benchmark: HARNESSMM_BENCHMARK,
|
|
141
|
+
summary: service.summary(),
|
|
142
|
+
note: '3.2.0 observe-only:不拦截/不改写模型调用;EGD/GDR/CCS 在 3.3.0 接入。',
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try { ctx.effect(() => ctx.provide(SERVICE.cost, service)) } catch { /* fake ctx */ }
|
|
148
|
+
master.registerSubplugin({
|
|
149
|
+
id: 'cost-optimizer',
|
|
150
|
+
name: 'dsh-master-cost-optimizer',
|
|
151
|
+
capabilities: ['token-estimate', 'savings-record', 'cost-status'],
|
|
152
|
+
description: 'HarnessMM observe 层(仅 fork)',
|
|
153
|
+
})
|
|
154
|
+
return service
|
|
155
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
|
|
3
|
+
// @ts-nocheck — host JS→TS 迁移阶段:运行时行为优先,类型注解逐步补齐(worker/ 仍严格检查)。
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 子插件 3:双主管(PM + PJM)。
|
|
7
|
+
*
|
|
8
|
+
* 定位:运行在母体/元主内的元 Agent;PM 把模糊需求变成结构化 PRD,PJM 把 PRD
|
|
9
|
+
* 拆成步骤并通过瓮城的 fangForker 调度分身执行。本子插件不直接运行生产任务:
|
|
10
|
+
* * 没有 fangForker.spawn 时 dispatch fail-closed;
|
|
11
|
+
* * 歧义澄清复用瓮城决策中心(fangDecisionCenter),不另造一套审批;
|
|
12
|
+
* * 角色:只允许 mother / essence(分身里没有“管理层”)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
FANG_SERVICE,
|
|
17
|
+
ROW,
|
|
18
|
+
ROLE,
|
|
19
|
+
SERVICE,
|
|
20
|
+
createIdFactory,
|
|
21
|
+
detectRole,
|
|
22
|
+
enabledOf,
|
|
23
|
+
masterError,
|
|
24
|
+
roleAllowed,
|
|
25
|
+
safeGet,
|
|
26
|
+
} from '../kernel.ts'
|
|
27
|
+
|
|
28
|
+
export const name = 'dsh-master-dual-manager'
|
|
29
|
+
export const inject = ['dshMaster']
|
|
30
|
+
export const ROW_ID = ROW.dual
|
|
31
|
+
|
|
32
|
+
const PRIORITIES = Object.freeze(['low', 'normal', 'high', 'critical'])
|
|
33
|
+
const DEFAULT_STEP_KIND = 'development'
|
|
34
|
+
|
|
35
|
+
function stringList(value) {
|
|
36
|
+
if (!Array.isArray(value)) return []
|
|
37
|
+
return value.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function missing(name) {
|
|
41
|
+
return masterError(`${name}-required`, `${name} is required`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {object} ctx
|
|
46
|
+
* @param {{enabled?:boolean}} [config]
|
|
47
|
+
*/
|
|
48
|
+
export function apply(ctx, config = {}) {
|
|
49
|
+
if (!enabledOf(config)) return { skipped: true, reason: 'disabled-by-config' }
|
|
50
|
+
const role = detectRole()
|
|
51
|
+
if (!roleAllowed(ROW_ID, role)) {
|
|
52
|
+
try {
|
|
53
|
+
ctx?.logger?.info?.(`[dsh-master] dual-manager skipped: role=${role} not in mother/essence`)
|
|
54
|
+
} catch { /* ignore */ }
|
|
55
|
+
return { skipped: true, reason: `role-not-allowed:${role}` }
|
|
56
|
+
}
|
|
57
|
+
const master = safeGet(ctx, SERVICE.master)
|
|
58
|
+
if (!master || typeof master.registerSubplugin !== 'function') {
|
|
59
|
+
return { skipped: true, reason: 'master-core-unavailable' }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const newPrdId = createIdFactory('prd')
|
|
63
|
+
const newPlanId = createIdFactory('plan')
|
|
64
|
+
const prds = new Map()
|
|
65
|
+
const plans = new Map()
|
|
66
|
+
|
|
67
|
+
const decisionCenter = () => safeGet(ctx, FANG_SERVICE.decisionCenter)
|
|
68
|
+
const forker = () => {
|
|
69
|
+
const service = safeGet(ctx, FANG_SERVICE.forker)
|
|
70
|
+
return service && typeof service.spawn === 'function' ? service : null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pm = {
|
|
74
|
+
/** 需求 → 结构化 PRD(不调用模型;3.2.x 接入 PM agent 后由模型填充字段)。 */
|
|
75
|
+
createPrd(input = {}) {
|
|
76
|
+
const title = String(input.title ?? '').trim()
|
|
77
|
+
const goal = String(input.goal ?? '').trim()
|
|
78
|
+
if (!title) return missing('title')
|
|
79
|
+
if (!goal) return missing('goal')
|
|
80
|
+
const priority = PRIORITIES.includes(String(input.priority ?? 'normal')) ? String(input.priority ?? 'normal') : 'normal'
|
|
81
|
+
const prd = Object.freeze({
|
|
82
|
+
id: newPrdId(),
|
|
83
|
+
title,
|
|
84
|
+
goal,
|
|
85
|
+
priority,
|
|
86
|
+
constraints: Object.freeze(stringList(input.constraints)),
|
|
87
|
+
acceptance: Object.freeze(stringList(input.acceptance)),
|
|
88
|
+
requester: String(input.requester ?? 'user'),
|
|
89
|
+
status: 'draft',
|
|
90
|
+
createdAt: Date.now(),
|
|
91
|
+
})
|
|
92
|
+
prds.set(prd.id, prd)
|
|
93
|
+
return { ok: true, prd }
|
|
94
|
+
},
|
|
95
|
+
getPrd(id) {
|
|
96
|
+
const prd = prds.get(String(id ?? ''))
|
|
97
|
+
return prd ? { ok: true, prd } : masterError('prd-not-found', 'PRD not found', { id: String(id ?? '') })
|
|
98
|
+
},
|
|
99
|
+
listPrds() {
|
|
100
|
+
return { ok: true, items: [...prds.values()] }
|
|
101
|
+
},
|
|
102
|
+
/** 歧义澄清:复用瓮城决策中心,返回 ticket 或明确的 unavailable。 */
|
|
103
|
+
async askUser({ question, context = '', options = [], level = 'yellow' } = {}) {
|
|
104
|
+
const q = String(question ?? '').trim()
|
|
105
|
+
if (!q) return missing('question')
|
|
106
|
+
const center = decisionCenter()
|
|
107
|
+
if (!center || typeof center.create !== 'function') {
|
|
108
|
+
return masterError('fang-decision-center-unavailable', 'dsh-fang decision center is required to ask the user')
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const out = await center.create({
|
|
112
|
+
level: ['red', 'yellow', 'blue', 'green'].includes(String(level)) ? String(level) : 'yellow',
|
|
113
|
+
title: q,
|
|
114
|
+
context: String(context),
|
|
115
|
+
options: Array.isArray(options) ? options : [],
|
|
116
|
+
source: { role: 'pm', kind: 'prd-clarification' },
|
|
117
|
+
})
|
|
118
|
+
if (out && out.ok === false) return { ...masterError('decision-create-failed', 'decision center rejected the ticket', out) }
|
|
119
|
+
return { ok: true, ticket: out?.ticket ?? out }
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return masterError('decision-create-threw', String(error?.message ?? error))
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const pjm = {
|
|
127
|
+
/** PRD → 可调度步骤计划。 */
|
|
128
|
+
plan(prdId, { steps = [] } = {}) {
|
|
129
|
+
const prd = prds.get(String(prdId ?? ''))
|
|
130
|
+
if (!prd) return masterError('prd-not-found', 'PRD not found', { id: String(prdId ?? '') })
|
|
131
|
+
if (!Array.isArray(steps) || steps.length === 0) return missing('steps')
|
|
132
|
+
const normalized = []
|
|
133
|
+
for (const [index, step] of steps.entries()) {
|
|
134
|
+
const title = String(step?.title ?? '').trim()
|
|
135
|
+
const prompt = String(step?.prompt ?? '').trim()
|
|
136
|
+
if (!title) return missing(`steps[${index}].title`)
|
|
137
|
+
if (!prompt) return missing(`steps[${index}].prompt`)
|
|
138
|
+
normalized.push({
|
|
139
|
+
id: String(step?.id ?? `step-${index + 1}`),
|
|
140
|
+
title,
|
|
141
|
+
prompt,
|
|
142
|
+
kind: String(step?.kind ?? DEFAULT_STEP_KIND),
|
|
143
|
+
status: 'pending',
|
|
144
|
+
fork: null,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
const plan = {
|
|
148
|
+
id: newPlanId(),
|
|
149
|
+
prdId: prd.id,
|
|
150
|
+
status: 'planned',
|
|
151
|
+
steps: normalized,
|
|
152
|
+
createdAt: Date.now(),
|
|
153
|
+
}
|
|
154
|
+
plans.set(plan.id, plan)
|
|
155
|
+
return { ok: true, plan }
|
|
156
|
+
},
|
|
157
|
+
getPlan(id) {
|
|
158
|
+
const plan = plans.get(String(id ?? ''))
|
|
159
|
+
return plan ? { ok: true, plan } : masterError('plan-not-found', 'Plan not found', { id: String(id ?? '') })
|
|
160
|
+
},
|
|
161
|
+
/** 调瓮城 fangForker.spawn 派生执行分身;无 forker 时 fail-closed。 */
|
|
162
|
+
async dispatch(planId, { stepId } = {}) {
|
|
163
|
+
const plan = plans.get(String(planId ?? ''))
|
|
164
|
+
if (!plan) return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') })
|
|
165
|
+
const step = plan.steps.find((item) => item.id === String(stepId ?? ''))
|
|
166
|
+
if (!step) return masterError('step-not-found', 'Step not found', { id: String(stepId ?? '') })
|
|
167
|
+
if (step.fork) return masterError('step-already-dispatched', 'Step already dispatched', { stepId: step.id, fork: step.fork })
|
|
168
|
+
const fangForker = forker()
|
|
169
|
+
if (!fangForker) {
|
|
170
|
+
return masterError('fang-forker-unavailable', 'dsh-fang fangForker.spawn is required to dispatch a step')
|
|
171
|
+
}
|
|
172
|
+
// 先占位再 await,避免两个并发 dispatch 同时看到 step.fork=null 而重复派生。
|
|
173
|
+
step.status = 'dispatching'
|
|
174
|
+
step.fork = { pending: true }
|
|
175
|
+
try {
|
|
176
|
+
const spawnConfig = config.dispatch && typeof config.dispatch === 'object' ? config.dispatch : {}
|
|
177
|
+
const fork = await fangForker.spawn({
|
|
178
|
+
homeDir: String(spawnConfig.homeDir ?? '') || undefined,
|
|
179
|
+
cliPath: String(spawnConfig.cliPath ?? '') || undefined,
|
|
180
|
+
profile: String(spawnConfig.profile ?? 'default') || 'default',
|
|
181
|
+
taskTitle: `PJM ${plan.id}/${step.id} ${step.title}`,
|
|
182
|
+
env: {
|
|
183
|
+
DSH_PJM_PRD_ID: String(plan.prdId),
|
|
184
|
+
DSH_PJM_PLAN_ID: String(plan.id),
|
|
185
|
+
DSH_PJM_STEP_ID: String(step.id),
|
|
186
|
+
DSH_PJM_KIND: String(step.kind),
|
|
187
|
+
DSH_PJM_PROMPT: String(step.prompt),
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
if (fork && fork.ok === false) {
|
|
191
|
+
step.status = 'pending'; step.fork = null
|
|
192
|
+
return { ...masterError('fang-spawn-failed', 'fangForker.spawn returned an error', fork), step }
|
|
193
|
+
}
|
|
194
|
+
step.status = 'running'
|
|
195
|
+
step.fork = fork ?? null
|
|
196
|
+
plan.status = 'running'
|
|
197
|
+
return { ok: true, planId: plan.id, stepId: step.id, step, fork }
|
|
198
|
+
} catch (error) {
|
|
199
|
+
step.status = 'pending'; step.fork = null
|
|
200
|
+
return masterError('fang-spawn-threw', String(error?.message ?? error))
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
completeStep(planId, stepId, { status = 'done', summary = '' } = {}) {
|
|
204
|
+
const plan = plans.get(String(planId ?? ''))
|
|
205
|
+
if (!plan) return masterError('plan-not-found', 'Plan not found', { id: String(planId ?? '') })
|
|
206
|
+
const step = plan.steps.find((item) => item.id === String(stepId ?? ''))
|
|
207
|
+
if (!step) return masterError('step-not-found', 'Step not found', { id: String(stepId ?? '') })
|
|
208
|
+
const next = status === 'failed' ? 'failed' : 'done'
|
|
209
|
+
step.status = next
|
|
210
|
+
step.summary = String(summary)
|
|
211
|
+
const states = plan.steps.map((item) => item.status)
|
|
212
|
+
plan.status = states.every((state) => state === 'done')
|
|
213
|
+
? 'done'
|
|
214
|
+
: (states.some((state) => state === 'running') ? 'running' : 'planned')
|
|
215
|
+
return { ok: true, plan }
|
|
216
|
+
},
|
|
217
|
+
listPlans() {
|
|
218
|
+
return { ok: true, items: [...plans.values()] }
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const service = {
|
|
223
|
+
id: SERVICE.dual,
|
|
224
|
+
version: '3.2.5',
|
|
225
|
+
role,
|
|
226
|
+
pm,
|
|
227
|
+
pjm,
|
|
228
|
+
status() {
|
|
229
|
+
return {
|
|
230
|
+
ok: true,
|
|
231
|
+
id: 'dual-manager',
|
|
232
|
+
version: '3.2.5',
|
|
233
|
+
role,
|
|
234
|
+
pm: { prds: prds.size },
|
|
235
|
+
pjm: { plans: plans.size },
|
|
236
|
+
cooperation: {
|
|
237
|
+
decisionCenter: Boolean(decisionCenter()),
|
|
238
|
+
forker: Boolean(forker()),
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try { ctx.effect(() => ctx.provide(SERVICE.dual, service)) } catch { /* fake ctx */ }
|
|
245
|
+
master.registerSubplugin({
|
|
246
|
+
id: 'dual-manager',
|
|
247
|
+
name: 'dsh-master-dual-manager',
|
|
248
|
+
capabilities: ['pm-prd', 'pm-ask-user', 'pjm-plan', 'pjm-dispatch'],
|
|
249
|
+
description: 'PM + PJM 双主管(仅 mother/essence)',
|
|
250
|
+
})
|
|
251
|
+
return service
|
|
252
|
+
}
|