@kasenri/dsh-orbit 0.5.9 → 0.6.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.
- package/README.md +60 -6
- package/lib/activation.js +7 -0
- package/lib/client.js +347 -25
- package/lib/decisions.js +4 -0
- package/lib/dsh-host.js +47 -5
- package/lib/index.js +53 -12
- package/lib/kernel.js +93 -5
- package/lib/moa-adapter.js +434 -0
- package/lib/routes.js +28 -0
- package/lib/service.js +34 -3
- package/lib/session-state.js +151 -1
- package/lib/state-store.js +13 -6
- package/lib/supervisor.js +174 -16
- package/lib/types.js +8 -1
- package/package.json +7 -3
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { copyFile, lstat, mkdir, readFile, readdir, realpath, stat, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { redactText, truncateSafe } from "./sanitize.js";
|
|
15
|
+
const SUPPORTED_MOA_VERSION = '0.2.19';
|
|
16
|
+
const MAX_CONTEXT_CHARS = 16_000;
|
|
17
|
+
const MAX_CANDIDATE_TEXT = 12_000;
|
|
18
|
+
const MAX_JUDGE_TEXT = 8_000;
|
|
19
|
+
const MAX_PROMOTED_FILES = 64;
|
|
20
|
+
const MAX_PROMOTED_FILE_BYTES = 2 * 1024 * 1024;
|
|
21
|
+
const CODE_FENCE = String.fromCharCode(96, 96, 96);
|
|
22
|
+
function sha256(value) {
|
|
23
|
+
return createHash('sha256').update(value).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
function priceFor(route, prices) {
|
|
26
|
+
if (!prices)
|
|
27
|
+
return undefined;
|
|
28
|
+
const provider = route.provider.trim().toLowerCase();
|
|
29
|
+
const model = route.model.trim().toLowerCase();
|
|
30
|
+
const row = prices[`${provider}/${model}`] ?? prices[model] ?? prices[`${provider}/*`] ?? prices['*'];
|
|
31
|
+
if (!row)
|
|
32
|
+
return undefined;
|
|
33
|
+
return { input: row.input, output: row.output, cacheHit: row.cacheHit ?? row.input };
|
|
34
|
+
}
|
|
35
|
+
function toMoaUsage(route, usage, prices) {
|
|
36
|
+
if (!usage)
|
|
37
|
+
return undefined;
|
|
38
|
+
const cacheRead = usage.cacheReadTokens ?? 0;
|
|
39
|
+
const cacheWrite = usage.cacheWriteTokens ?? 0;
|
|
40
|
+
const total = usage.totalTokens ?? usage.inputTokens + usage.outputTokens + cacheRead + cacheWrite;
|
|
41
|
+
const rates = priceFor(route, prices);
|
|
42
|
+
const cost = rates
|
|
43
|
+
? ((usage.inputTokens + cacheWrite) / 1_000_000) * rates.input + (cacheRead / 1_000_000) * rates.cacheHit + (usage.outputTokens / 1_000_000) * rates.output
|
|
44
|
+
: undefined;
|
|
45
|
+
return {
|
|
46
|
+
input_tokens: usage.inputTokens,
|
|
47
|
+
output_tokens: usage.outputTokens,
|
|
48
|
+
total_tokens: total,
|
|
49
|
+
...(cacheRead > 0 ? { cache_read_tokens: cacheRead } : {}),
|
|
50
|
+
...(cacheWrite > 0 ? { cache_write_tokens: cacheWrite } : {}),
|
|
51
|
+
...(cost === undefined ? {} : { cost_usd: Number(cost.toFixed(6)) }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function combineUsage(first, second) {
|
|
55
|
+
if (!first)
|
|
56
|
+
return second;
|
|
57
|
+
if (!second)
|
|
58
|
+
return first;
|
|
59
|
+
const cost = (first.cost_usd ?? 0) + (second.cost_usd ?? 0);
|
|
60
|
+
const hasCost = first.cost_usd !== undefined || second.cost_usd !== undefined;
|
|
61
|
+
return {
|
|
62
|
+
input_tokens: first.input_tokens + second.input_tokens,
|
|
63
|
+
output_tokens: first.output_tokens + second.output_tokens,
|
|
64
|
+
total_tokens: first.total_tokens + second.total_tokens,
|
|
65
|
+
...((first.cache_read_tokens ?? 0) + (second.cache_read_tokens ?? 0) > 0 ? { cache_read_tokens: (first.cache_read_tokens ?? 0) + (second.cache_read_tokens ?? 0) } : {}),
|
|
66
|
+
...((first.cache_write_tokens ?? 0) + (second.cache_write_tokens ?? 0) > 0 ? { cache_write_tokens: (first.cache_write_tokens ?? 0) + (second.cache_write_tokens ?? 0) } : {}),
|
|
67
|
+
...(hasCost ? { cost_usd: Number(cost.toFixed(6)) } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function safeSegment(value) {
|
|
71
|
+
return value.replace(/[^A-Za-z0-9_.-]/gu, '_').slice(0, 120);
|
|
72
|
+
}
|
|
73
|
+
function stepRoot(workspace, runId, stepId) {
|
|
74
|
+
return join(workspace, '.cx', 'moa', safeSegment(runId), safeSegment(stepId));
|
|
75
|
+
}
|
|
76
|
+
function candidateDir(workspace, runId, stepId, index) {
|
|
77
|
+
return join(stepRoot(workspace, runId, stepId), 'candidate-' + index);
|
|
78
|
+
}
|
|
79
|
+
function candidateMetaPath(workspace, runId, stepId, index) {
|
|
80
|
+
return join(stepRoot(workspace, runId, stepId), 'candidate-' + index + '.json');
|
|
81
|
+
}
|
|
82
|
+
function judgePath(workspace, runId, stepId) {
|
|
83
|
+
return join(stepRoot(workspace, runId, stepId), 'decision.json');
|
|
84
|
+
}
|
|
85
|
+
function sanitizeRelativePath(value) {
|
|
86
|
+
const trimmed = value.trim().replace(/\\/gu, '/').replace(/^\/+/, '');
|
|
87
|
+
if (trimmed === '')
|
|
88
|
+
return undefined;
|
|
89
|
+
const first = trimmed.split('/')[0]?.toLowerCase();
|
|
90
|
+
if (first === '.cx' || first === '.git' || first === '.moa')
|
|
91
|
+
return undefined;
|
|
92
|
+
const normalized = trimmed.split('/').filter((part) => part !== '' && part !== '.').join('/');
|
|
93
|
+
if (normalized.split('/').some((part) => part === '..'))
|
|
94
|
+
return undefined;
|
|
95
|
+
return normalized;
|
|
96
|
+
}
|
|
97
|
+
function extractFileBlocks(text) {
|
|
98
|
+
const files = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const fenced = /\x60{3}[\w-]*\s+(?:file|path|filepath)=["']?([^\s"'\n\r]+)["']?[\r\n]+([\s\S]*?)\x60{3}/giu;
|
|
101
|
+
for (const match of text.matchAll(fenced)) {
|
|
102
|
+
const path = sanitizeRelativePath(match[1] ?? '');
|
|
103
|
+
if (!path || seen.has(path))
|
|
104
|
+
continue;
|
|
105
|
+
seen.add(path);
|
|
106
|
+
files.push({ path, content: match[2] ?? '' });
|
|
107
|
+
}
|
|
108
|
+
const headed = /(?:###?\s*File:\s*|File:\s*)["']?([A-Za-z0-9_.\-/\\]+)["']?\s*[\r\n]+\x60{3}[\w-]*[\r\n]+([\s\S]*?)\x60{3}/giu;
|
|
109
|
+
for (const match of text.matchAll(headed)) {
|
|
110
|
+
const path = sanitizeRelativePath(match[1] ?? '');
|
|
111
|
+
if (!path || seen.has(path))
|
|
112
|
+
continue;
|
|
113
|
+
seen.add(path);
|
|
114
|
+
files.push({ path, content: match[2] ?? '' });
|
|
115
|
+
}
|
|
116
|
+
return files;
|
|
117
|
+
}
|
|
118
|
+
async function assertNoSymlinkTraversal(root, relativePath) {
|
|
119
|
+
let current = root;
|
|
120
|
+
for (const part of relativePath.split('/')) {
|
|
121
|
+
current = join(current, part);
|
|
122
|
+
try {
|
|
123
|
+
const info = await lstat(current);
|
|
124
|
+
if (info.isSymbolicLink())
|
|
125
|
+
throw new Error('ORBIT_MOA_PROMOTION_SYMLINK: ' + relativePath);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error.code === 'ENOENT')
|
|
129
|
+
return;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function writeCandidateFiles(root, files) {
|
|
135
|
+
const paths = [];
|
|
136
|
+
for (const file of files.slice(0, MAX_PROMOTED_FILES)) {
|
|
137
|
+
if (Buffer.byteLength(file.content, 'utf8') > MAX_PROMOTED_FILE_BYTES)
|
|
138
|
+
continue;
|
|
139
|
+
const target = resolve(root, file.path);
|
|
140
|
+
if (relative(root, target).startsWith('..'))
|
|
141
|
+
continue;
|
|
142
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
143
|
+
await writeFile(target, file.content, { encoding: 'utf8', mode: 0o600 });
|
|
144
|
+
paths.push(file.path);
|
|
145
|
+
}
|
|
146
|
+
return paths;
|
|
147
|
+
}
|
|
148
|
+
function modelPrompt(step, context, index, total) {
|
|
149
|
+
return [
|
|
150
|
+
'你是 Orbit 的 MoA 独立候选模型。',
|
|
151
|
+
'你正在处理候选 ' + index + '/' + total + '。请独立解决当前步骤,不要假设其他候选会补救你的方案。',
|
|
152
|
+
'你没有工具调用权限,也不能声称已经实际修改或测试项目。',
|
|
153
|
+
'如果方案需要修改文件,请输出完整文件内容,并使用严格格式:' + CODE_FENCE + '语言 file="相对路径"' + '。不要写入 .cx。',
|
|
154
|
+
'优先给出可以直接应用、最小且完整的实现;说明关键取舍与预期验证方式。',
|
|
155
|
+
'自然语言默认使用简体中文;代码、命令、路径、provider/model ID 保持原样。',
|
|
156
|
+
'当前步骤:' + step.goal,
|
|
157
|
+
context ? '当前项目上下文(只读):\n' + context : '当前项目上下文:无可读文本文件。',
|
|
158
|
+
].join('\n');
|
|
159
|
+
}
|
|
160
|
+
function critiquePrompt(step, own, others) {
|
|
161
|
+
return [
|
|
162
|
+
'你是 Orbit 的 MoA 候选模型,现在进入一次有界的同伴互评修订。',
|
|
163
|
+
'请保留自己方案中正确的部分,吸收其他候选的优点,修复明确缺陷,然后重新输出完整候选。',
|
|
164
|
+
'不要把多个方案简单拼接;最终仍必须是一份可独立应用的完整方案。',
|
|
165
|
+
'如果修改文件,继续使用带 file="相对路径" 的完整文件代码块格式。',
|
|
166
|
+
'当前步骤:' + step.goal,
|
|
167
|
+
'你上一轮的方案:\n' + truncateSafe(own, 6_000),
|
|
168
|
+
'其他候选摘要:\n' + truncateSafe(others, 8_000),
|
|
169
|
+
].join('\n');
|
|
170
|
+
}
|
|
171
|
+
function judgePrompt(step, candidates, texts) {
|
|
172
|
+
const body = candidates
|
|
173
|
+
.filter((candidate) => candidate.ok)
|
|
174
|
+
.map((candidate) => [
|
|
175
|
+
'候选 ' + candidate.index + '(' + candidate.provider + '/' + candidate.model + ')',
|
|
176
|
+
'文件:' + (candidate.files.join(', ') || '无'),
|
|
177
|
+
truncateSafe(texts[candidate.index - 1] ?? candidate.summary, 4_000),
|
|
178
|
+
].join('\n'))
|
|
179
|
+
.join('\n\n---\n\n');
|
|
180
|
+
return [
|
|
181
|
+
'你是 Orbit 的 MoA Judge。',
|
|
182
|
+
'你的职责只有相对比较:从现有成功候选中选择最适合当前步骤的一份。不要生成新的综合代码,不要决定 Orbit Step 是否通过。',
|
|
183
|
+
'必须考虑正确性、最小改动、可验证性、回归风险和用户原始步骤目标。',
|
|
184
|
+
'最终必须单独输出机器标记:WINNER_CANDIDATE_INDEX: <数字>。',
|
|
185
|
+
'自然语言默认使用简体中文。',
|
|
186
|
+
'当前步骤:' + step.goal,
|
|
187
|
+
body,
|
|
188
|
+
].join('\n\n');
|
|
189
|
+
}
|
|
190
|
+
function winnerIndex(text, allowed) {
|
|
191
|
+
const match = text.match(/WINNER_CANDIDATE_INDEX\s*:\s*(\d+)/iu);
|
|
192
|
+
if (!match)
|
|
193
|
+
return undefined;
|
|
194
|
+
const index = Number(match[1]);
|
|
195
|
+
return allowed.has(index) ? index : undefined;
|
|
196
|
+
}
|
|
197
|
+
async function readJson(path) {
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export class OrbitMoaAdapter {
|
|
206
|
+
host;
|
|
207
|
+
modulePromise;
|
|
208
|
+
constructor(host) {
|
|
209
|
+
this.host = host;
|
|
210
|
+
}
|
|
211
|
+
async load() {
|
|
212
|
+
if (this.modulePromise)
|
|
213
|
+
return this.modulePromise;
|
|
214
|
+
this.modulePromise = (async () => {
|
|
215
|
+
const require = createRequire(import.meta.url);
|
|
216
|
+
let packageJsonPath;
|
|
217
|
+
try {
|
|
218
|
+
packageJsonPath = require.resolve('@goodandready/dsh-moa/package.json');
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
throw new Error('ORBIT_MOA_UNAVAILABLE: 未安装 @goodandready/dsh-moa。');
|
|
222
|
+
}
|
|
223
|
+
const pkg = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
224
|
+
const version = String(pkg.version ?? '');
|
|
225
|
+
if (version !== SUPPORTED_MOA_VERSION) {
|
|
226
|
+
throw new Error('ORBIT_MOA_VERSION_UNSUPPORTED: 当前支持 ' + SUPPORTED_MOA_VERSION + ',检测到 ' + (version || 'unknown') + '。');
|
|
227
|
+
}
|
|
228
|
+
const entry = require.resolve('@goodandready/dsh-moa');
|
|
229
|
+
const api = await import(__rewriteRelativeImportExtension(pathToFileURL(entry).href));
|
|
230
|
+
if (typeof api.collectProjectContext !== 'function' || typeof api.formatProjectContext !== 'function') {
|
|
231
|
+
throw new Error('ORBIT_MOA_API_UNAVAILABLE: dsh-moa 缺少项目上下文接口。');
|
|
232
|
+
}
|
|
233
|
+
return { version, api };
|
|
234
|
+
})();
|
|
235
|
+
return this.modulePromise;
|
|
236
|
+
}
|
|
237
|
+
async availability() {
|
|
238
|
+
if (!this.host.runModel)
|
|
239
|
+
return { available: false, reason: 'ORBIT_MOA_MODEL_RUNNER_UNAVAILABLE' };
|
|
240
|
+
try {
|
|
241
|
+
const loaded = await this.load();
|
|
242
|
+
return { available: true, version: loaded.version };
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return { available: false, reason: error instanceof Error ? error.message : String(error) };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async call(route, label, prompt, signal) {
|
|
249
|
+
if (!this.host.runModel)
|
|
250
|
+
throw new Error('ORBIT_MOA_MODEL_RUNNER_UNAVAILABLE');
|
|
251
|
+
const request = { label, prompt, route, ...(signal ? { signal } : {}) };
|
|
252
|
+
const result = await this.host.runModel(request);
|
|
253
|
+
if (result.interrupted)
|
|
254
|
+
throw new Error(result.reason ?? 'ORBIT_MOA_MODEL_INTERRUPTED');
|
|
255
|
+
return { text: result.output, ...(result.usage ? { usage: result.usage } : {}) };
|
|
256
|
+
}
|
|
257
|
+
async fanout(input) {
|
|
258
|
+
const loaded = await this.load();
|
|
259
|
+
const collected = await loaded.api.collectProjectContext(input.workspace, MAX_CONTEXT_CHARS);
|
|
260
|
+
const context = loaded.api.formatProjectContext(collected.files ?? [], {
|
|
261
|
+
skippedFiles: collected.skippedFiles ?? 0,
|
|
262
|
+
skippedList: collected.skippedList ?? [],
|
|
263
|
+
});
|
|
264
|
+
const count = input.policy.candidate_count;
|
|
265
|
+
await mkdir(stepRoot(input.workspace, input.runId, input.step.id), { recursive: true, mode: 0o700 });
|
|
266
|
+
const texts = new Array(count).fill('');
|
|
267
|
+
const firstPass = await Promise.all(input.policy.candidates.slice(0, count).map(async (route, offset) => {
|
|
268
|
+
const index = offset + 1;
|
|
269
|
+
const metaPath = candidateMetaPath(input.workspace, input.runId, input.step.id, index);
|
|
270
|
+
const routeHash = sha256(JSON.stringify(route));
|
|
271
|
+
const cached = await readJson(metaPath);
|
|
272
|
+
if (cached?.route_hash === routeHash && typeof cached.text === 'string') {
|
|
273
|
+
texts[offset] = cached.text;
|
|
274
|
+
return cached;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const call = await this.call(route, 'moa-candidate-' + input.step.id + '-' + index, modelPrompt(input.step, context, index, count), input.signal);
|
|
278
|
+
const text = call.text;
|
|
279
|
+
texts[offset] = text;
|
|
280
|
+
const written = await writeCandidateFiles(candidateDir(input.workspace, input.runId, input.step.id, index), extractFileBlocks(text));
|
|
281
|
+
const result = {
|
|
282
|
+
index,
|
|
283
|
+
provider: route.provider,
|
|
284
|
+
model: route.model,
|
|
285
|
+
ok: true,
|
|
286
|
+
summary: redactText(truncateSafe(text, 2_000)),
|
|
287
|
+
files: written,
|
|
288
|
+
...(toMoaUsage(route, call.usage, input.policy.prices) ? { usage: toMoaUsage(route, call.usage, input.policy.prices) } : {}),
|
|
289
|
+
text: truncateSafe(text, MAX_CANDIDATE_TEXT),
|
|
290
|
+
route_hash: routeHash,
|
|
291
|
+
};
|
|
292
|
+
await writeFile(metaPath, JSON.stringify(result, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
const message = redactText(truncateSafe(error instanceof Error ? error.message : String(error), 500));
|
|
297
|
+
const result = {
|
|
298
|
+
index,
|
|
299
|
+
provider: route.provider,
|
|
300
|
+
model: route.model,
|
|
301
|
+
ok: false,
|
|
302
|
+
summary: message,
|
|
303
|
+
files: [],
|
|
304
|
+
error: message,
|
|
305
|
+
route_hash: routeHash,
|
|
306
|
+
};
|
|
307
|
+
await writeFile(metaPath, JSON.stringify(result, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
}));
|
|
311
|
+
if (input.policy.peer_critique) {
|
|
312
|
+
await Promise.all(firstPass.map(async (candidate, offset) => {
|
|
313
|
+
if (!candidate.ok)
|
|
314
|
+
return;
|
|
315
|
+
const route = input.policy.candidates[offset];
|
|
316
|
+
const others = firstPass.filter((other) => other.ok && other.index !== candidate.index).map((other) => '候选 ' + other.index + ': ' + other.summary).join('\n\n');
|
|
317
|
+
try {
|
|
318
|
+
const revisedCall = await this.call(route, 'moa-critique-' + input.step.id + '-' + candidate.index, critiquePrompt(input.step, texts[offset] ?? candidate.summary, others), input.signal);
|
|
319
|
+
const revised = revisedCall.text;
|
|
320
|
+
texts[offset] = revised;
|
|
321
|
+
candidate.summary = redactText(truncateSafe(revised, 2_000));
|
|
322
|
+
candidate.files = await writeCandidateFiles(candidateDir(input.workspace, input.runId, input.step.id, candidate.index), extractFileBlocks(revised));
|
|
323
|
+
candidate.usage = combineUsage(candidate.usage, toMoaUsage(route, revisedCall.usage, input.policy.prices));
|
|
324
|
+
await writeFile(candidateMetaPath(input.workspace, input.runId, input.step.id, candidate.index), JSON.stringify({
|
|
325
|
+
...candidate,
|
|
326
|
+
text: truncateSafe(revised, MAX_CANDIDATE_TEXT),
|
|
327
|
+
route_hash: sha256(JSON.stringify(route)),
|
|
328
|
+
peer_critique: true,
|
|
329
|
+
}, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
candidate.ok = false;
|
|
333
|
+
candidate.error = redactText(truncateSafe(error instanceof Error ? error.message : String(error), 500));
|
|
334
|
+
}
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
const candidates = firstPass.map((candidate) => {
|
|
338
|
+
const copy = { ...candidate };
|
|
339
|
+
delete copy.text;
|
|
340
|
+
delete copy.route_hash;
|
|
341
|
+
return copy;
|
|
342
|
+
});
|
|
343
|
+
const successful = candidates.filter((candidate) => candidate.ok).length;
|
|
344
|
+
return { adapterVersion: loaded.version, candidates, successful, failed: candidates.length - successful };
|
|
345
|
+
}
|
|
346
|
+
async judge(input) {
|
|
347
|
+
const cached = await readJson(judgePath(input.workspace, input.runId, input.step.id));
|
|
348
|
+
if (cached?.winningCandidate && cached.summary)
|
|
349
|
+
return cached;
|
|
350
|
+
const successful = input.candidates.filter((candidate) => candidate.ok);
|
|
351
|
+
if (successful.length < 2)
|
|
352
|
+
throw new Error('ORBIT_MOA_QUORUM_FAILED: 至少需要 2 个成功候选。');
|
|
353
|
+
const texts = [];
|
|
354
|
+
for (const candidate of input.candidates) {
|
|
355
|
+
const meta = await readJson(candidateMetaPath(input.workspace, input.runId, input.step.id, candidate.index));
|
|
356
|
+
texts[candidate.index - 1] = meta?.text ?? candidate.summary;
|
|
357
|
+
}
|
|
358
|
+
const judgeCall = await this.call(input.policy.judge, 'moa-judge-' + input.step.id, judgePrompt(input.step, input.candidates, texts), input.signal);
|
|
359
|
+
const output = judgeCall.text;
|
|
360
|
+
const allowed = new Set(successful.map((candidate) => candidate.index));
|
|
361
|
+
const selected = winnerIndex(output, allowed);
|
|
362
|
+
if (selected === undefined)
|
|
363
|
+
throw new Error('ORBIT_MOA_JUDGE_INVALID: Judge 未返回有效 WINNER_CANDIDATE_INDEX。');
|
|
364
|
+
const winner = input.candidates[selected - 1];
|
|
365
|
+
if (!winner)
|
|
366
|
+
throw new Error('ORBIT_MOA_JUDGE_INVALID: winner 不存在。');
|
|
367
|
+
const result = {
|
|
368
|
+
winningCandidate: selected,
|
|
369
|
+
winnerModel: winner.provider + '/' + winner.model,
|
|
370
|
+
summary: redactText(truncateSafe(output, MAX_JUDGE_TEXT)),
|
|
371
|
+
...(toMoaUsage(input.policy.judge, judgeCall.usage, input.policy.prices) ? { usage: toMoaUsage(input.policy.judge, judgeCall.usage, input.policy.prices) } : {}),
|
|
372
|
+
};
|
|
373
|
+
await writeFile(judgePath(input.workspace, input.runId, input.step.id), JSON.stringify(result, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
async promote(input) {
|
|
377
|
+
const sourceRoot = candidateDir(input.workspace, input.runId, input.stepId, input.winningCandidate);
|
|
378
|
+
const workspaceRoot = await realpath(input.workspace).catch(() => resolve(input.workspace));
|
|
379
|
+
const receiptPath = join(stepRoot(input.workspace, input.runId, input.stepId), 'promotion.json');
|
|
380
|
+
const cached = await readJson(receiptPath);
|
|
381
|
+
if (cached && Array.isArray(cached.files)) {
|
|
382
|
+
if (cached.files.length === 0)
|
|
383
|
+
return cached;
|
|
384
|
+
const matches = await Promise.all(cached.files.map(async (entry) => {
|
|
385
|
+
try {
|
|
386
|
+
return sha256(await readFile(join(workspaceRoot, entry.path))) === entry.promoted_sha256;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}));
|
|
392
|
+
if (matches.every(Boolean))
|
|
393
|
+
return cached;
|
|
394
|
+
}
|
|
395
|
+
const files = [];
|
|
396
|
+
const walk = async (dir) => {
|
|
397
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
398
|
+
const absolute = join(dir, entry.name);
|
|
399
|
+
if (entry.isDirectory())
|
|
400
|
+
await walk(absolute);
|
|
401
|
+
else if (entry.isFile())
|
|
402
|
+
files.push(relative(sourceRoot, absolute).replace(/\\/gu, '/'));
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
await walk(sourceRoot);
|
|
406
|
+
if (files.length > MAX_PROMOTED_FILES)
|
|
407
|
+
throw new Error('ORBIT_MOA_PROMOTION_TOO_LARGE: 文件数量超过安全上限。');
|
|
408
|
+
const receipt = { files: [], promoted_at: new Date().toISOString() };
|
|
409
|
+
if (files.length === 0) {
|
|
410
|
+
await writeFile(receiptPath, JSON.stringify(receipt, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
411
|
+
return receipt;
|
|
412
|
+
}
|
|
413
|
+
for (const rel of files.sort()) {
|
|
414
|
+
const safe = sanitizeRelativePath(rel);
|
|
415
|
+
if (!safe)
|
|
416
|
+
throw new Error('ORBIT_MOA_PROMOTION_PATH_INVALID: ' + rel);
|
|
417
|
+
const source = resolve(sourceRoot, safe);
|
|
418
|
+
const info = await stat(source);
|
|
419
|
+
if (!info.isFile() || info.size > MAX_PROMOTED_FILE_BYTES)
|
|
420
|
+
throw new Error('ORBIT_MOA_PROMOTION_FILE_INVALID: ' + safe);
|
|
421
|
+
const destination = resolve(workspaceRoot, safe);
|
|
422
|
+
if (relative(workspaceRoot, destination).startsWith('..'))
|
|
423
|
+
throw new Error('ORBIT_MOA_PROMOTION_PATH_INVALID: ' + safe);
|
|
424
|
+
await assertNoSymlinkTraversal(workspaceRoot, safe);
|
|
425
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
426
|
+
const candidateBytes = await readFile(source);
|
|
427
|
+
await copyFile(source, destination);
|
|
428
|
+
const promotedBytes = await readFile(destination);
|
|
429
|
+
receipt.files.push({ path: safe, candidate_sha256: sha256(candidateBytes), promoted_sha256: sha256(promotedBytes) });
|
|
430
|
+
}
|
|
431
|
+
await writeFile(receiptPath, JSON.stringify(receipt, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
432
|
+
return receipt;
|
|
433
|
+
}
|
|
434
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Effective route resolution for a new Orbit run. */
|
|
2
|
+
import { DEFAULT_MAX_MOA_STEPS, DEFAULT_MOA_CANDIDATES } from "./types.js";
|
|
2
3
|
export function projectionRegistryOf(ctx) {
|
|
3
4
|
const reflect = ctx?.reflect;
|
|
4
5
|
const registry = reflect?.get('sessionProjections');
|
|
@@ -56,6 +57,33 @@ export function routeFromSelection(selection) {
|
|
|
56
57
|
...(typeof effort === 'string' && effort.trim() !== '' ? { reasoningEffort: effort.trim() } : {}),
|
|
57
58
|
};
|
|
58
59
|
}
|
|
60
|
+
/** Resolve the optional MoA policy for a NEW run. No model is guessed or inherited. */
|
|
61
|
+
export function resolveMoaPolicy(input) {
|
|
62
|
+
const enabled = input.settings?.enabled ?? input.config?.enabled ?? false;
|
|
63
|
+
if (!enabled)
|
|
64
|
+
return undefined;
|
|
65
|
+
const candidateCount = input.settings?.candidateCount ?? input.config?.candidateCount ?? DEFAULT_MOA_CANDIDATES;
|
|
66
|
+
const peerCritique = input.settings?.peerCritique ?? input.config?.peerCritique ?? false;
|
|
67
|
+
const maxMoaSteps = input.settings?.maxMoaSteps ?? input.config?.maxMoaSteps ?? DEFAULT_MAX_MOA_STEPS;
|
|
68
|
+
const rawCandidates = input.settings?.candidates?.length ? input.settings.candidates : input.config?.candidates ?? [];
|
|
69
|
+
const candidates = rawCandidates.map((route) => routeFromSelection(route)).filter((route) => route !== undefined);
|
|
70
|
+
const judge = routeFromSelection(input.settings?.judge) ?? routeFromSelection(input.config?.judge);
|
|
71
|
+
if (!Number.isSafeInteger(candidateCount) || candidateCount < 2 || candidateCount > 4) {
|
|
72
|
+
throw new Error('ORBIT_MOA_CANDIDATE_COUNT_INVALID: 候选数量必须为 2-4。');
|
|
73
|
+
}
|
|
74
|
+
if (candidates.length < candidateCount || judge === undefined) {
|
|
75
|
+
throw new Error('ORBIT_MOA_MODEL_CONFIGURATION_REQUIRED: 请为所有 MoA 候选和 Judge 选择模型。');
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
enabled: true,
|
|
79
|
+
candidate_count: candidateCount,
|
|
80
|
+
peer_critique: peerCritique,
|
|
81
|
+
max_moa_steps: maxMoaSteps,
|
|
82
|
+
candidates: candidates.slice(0, candidateCount),
|
|
83
|
+
judge,
|
|
84
|
+
...(input.prices && Object.keys(input.prices).length > 0 ? { prices: structuredClone(input.prices) } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
59
87
|
const ROLE_LABELS = {
|
|
60
88
|
commander: 'Commander',
|
|
61
89
|
executor: 'Executor',
|
package/lib/service.js
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { Service } from '@deepseek-ai/cordis';
|
|
4
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
4
5
|
import { DshOrbitHost } from "./dsh-host.js";
|
|
6
|
+
import { OrbitMoaAdapter } from "./moa-adapter.js";
|
|
5
7
|
import { OrbitStateStore } from "./state-store.js";
|
|
6
8
|
import { OrbitSupervisor } from "./supervisor.js";
|
|
9
|
+
import { orbitRuntimeFromState } from "./session-state.js";
|
|
7
10
|
export class OrbitService extends Service {
|
|
11
|
+
root;
|
|
8
12
|
host;
|
|
9
13
|
config;
|
|
10
14
|
executions = new Map();
|
|
11
15
|
constructor(ctx, config) {
|
|
12
16
|
super(ctx, 'orbit');
|
|
17
|
+
this.root = ctx;
|
|
13
18
|
this.host = new DshOrbitHost(ctx);
|
|
14
19
|
this.config = config;
|
|
15
20
|
}
|
|
21
|
+
publishRuntime(state) {
|
|
22
|
+
const owner = state.owner_session_id;
|
|
23
|
+
if (!owner)
|
|
24
|
+
return;
|
|
25
|
+
const session = this.root.sessions.get(SessionId(owner));
|
|
26
|
+
if (!session)
|
|
27
|
+
return;
|
|
28
|
+
session.append('orbit/runtime', orbitRuntimeFromState(state));
|
|
29
|
+
}
|
|
16
30
|
supervisorFor(projectDir) {
|
|
17
|
-
return new OrbitSupervisor(new OrbitStateStore(projectDir), this.host, {
|
|
31
|
+
return new OrbitSupervisor(new OrbitStateStore(projectDir, (state) => this.publishRuntime(state)), this.host, {
|
|
18
32
|
defaultRoutes: this.config.routes,
|
|
19
33
|
...(this.config.resolveRoutes ? { resolveRoutes: this.config.resolveRoutes } : {}),
|
|
34
|
+
...(this.config.resolveMoaPolicy ? { resolveMoaPolicy: this.config.resolveMoaPolicy } : {}),
|
|
20
35
|
...(this.config.resolveOwnerSessionId ? { resolveOwnerSessionId: this.config.resolveOwnerSessionId } : {}),
|
|
21
36
|
browserTools: this.config.browserTools,
|
|
22
37
|
commanderReadOnlyTools: this.config.commanderReadOnlyTools,
|
|
@@ -148,10 +163,26 @@ export class OrbitService extends Service {
|
|
|
148
163
|
});
|
|
149
164
|
const reflect = this.ctx.reflect;
|
|
150
165
|
checks.push({ name: 'model-registry', status: reflect.get('llm') ? 'pass' : 'fail', detail: '使用 DSH 当前 LLM registry 校验模型,不内置角色模型。' });
|
|
151
|
-
checks.push({ name: 'orbit-settings', status: reflect.get('settings') ? 'pass' : 'warn', detail: 'Commander / Watchdog 使用用户 Orbit 设置或显式 profile routes。' });
|
|
166
|
+
checks.push({ name: 'orbit-settings', status: reflect.get('settings') ? 'pass' : 'warn', detail: 'Commander / Watchdog / MoA 使用用户 Orbit 设置或显式 profile routes。' });
|
|
167
|
+
try {
|
|
168
|
+
const policy = this.config.resolveMoaPolicy?.();
|
|
169
|
+
const availability = await new OrbitMoaAdapter(this.host).availability();
|
|
170
|
+
checks.push({
|
|
171
|
+
name: 'moa-integration',
|
|
172
|
+
status: policy && !availability.available ? 'warn' : 'pass',
|
|
173
|
+
detail: availability.available
|
|
174
|
+
? `@goodandready/dsh-moa ${availability.version ?? ''} 可用;Orbit 使用隔离候选、Judge 选优和受控 Promotion。`
|
|
175
|
+
: policy
|
|
176
|
+
? availability.reason ?? 'MoA 不可用'
|
|
177
|
+
: 'MoA 未启用;Orbit 单 Executor 模式不受影响。',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
checks.push({ name: 'moa-integration', status: 'warn', detail: String(error) });
|
|
182
|
+
}
|
|
152
183
|
try {
|
|
153
184
|
const routes = this.config.resolveRoutes?.();
|
|
154
|
-
const issues = routes ? await this.host.validateRoutes(routes) : ['未配置模型解析来源'];
|
|
185
|
+
const issues = routes ? await this.host.validateRoutes({ ...routes }) : ['未配置模型解析来源'];
|
|
155
186
|
checks.push({ name: 'role-model-configuration', status: issues.length ? 'warn' : 'pass', detail: issues.join(';') || '三角色用户模型配置可用。' });
|
|
156
187
|
}
|
|
157
188
|
catch (error) {
|