@kasenri/dsh-orbit 0.5.10 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -1
- package/lib/activation.js +7 -0
- package/lib/client.js +413 -41
- package/lib/decisions.js +4 -0
- package/lib/dsh-host.js +47 -5
- package/lib/index.js +52 -3
- package/lib/kernel.js +67 -3
- package/lib/moa-adapter.js +449 -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 +173 -16
- package/lib/types.js +5 -1
- package/package.json +6 -2
package/lib/session-state.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
export const ORBIT_TOGGLE_COMMAND = 'orbit-toggle';
|
|
16
16
|
/** The projection key carrying the per-Session enable state. */
|
|
17
17
|
export const ORBIT_SESSION_KEY = 'orbitSession';
|
|
18
|
+
/** Projection key carrying a bounded live/durable Orbit execution snapshot. */
|
|
19
|
+
export const ORBIT_RUNTIME_KEY = 'orbitRuntime';
|
|
18
20
|
/**
|
|
19
21
|
* Parse one `/orbit-toggle` argument; `undefined` when it names neither state,
|
|
20
22
|
* so a malformed toggle never flips the switch as a side effect.
|
|
@@ -58,7 +60,144 @@ function orbitSessionSchema() {
|
|
|
58
60
|
},
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
|
-
|
|
63
|
+
function finiteNumber(value) {
|
|
64
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
65
|
+
}
|
|
66
|
+
function usageFrom(value) {
|
|
67
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
68
|
+
return undefined;
|
|
69
|
+
const record = value;
|
|
70
|
+
const input = finiteNumber(record['input_tokens']);
|
|
71
|
+
const output = finiteNumber(record['output_tokens']);
|
|
72
|
+
const total = finiteNumber(record['total_tokens']);
|
|
73
|
+
if (input === undefined || output === undefined || total === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const cacheRead = finiteNumber(record['cache_read_tokens']);
|
|
76
|
+
const cacheWrite = finiteNumber(record['cache_write_tokens']);
|
|
77
|
+
const cost = finiteNumber(record['cost_usd']);
|
|
78
|
+
return {
|
|
79
|
+
input_tokens: input,
|
|
80
|
+
output_tokens: output,
|
|
81
|
+
total_tokens: total,
|
|
82
|
+
...(cacheRead === undefined ? {} : { cache_read_tokens: cacheRead }),
|
|
83
|
+
...(cacheWrite === undefined ? {} : { cache_write_tokens: cacheWrite }),
|
|
84
|
+
...(cost === undefined ? {} : { cost_usd: cost }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function orbitRuntimeSchema() {
|
|
88
|
+
return {
|
|
89
|
+
parse(value) {
|
|
90
|
+
if (value === null)
|
|
91
|
+
return null;
|
|
92
|
+
if (typeof value !== 'object' || Array.isArray(value))
|
|
93
|
+
throw new Error('orbitRuntime projection value must be an object or null');
|
|
94
|
+
const record = value;
|
|
95
|
+
const runId = typeof record['runId'] === 'string' ? record['runId'] : undefined;
|
|
96
|
+
const phase = typeof record['phase'] === 'string' ? record['phase'] : undefined;
|
|
97
|
+
const status = typeof record['status'] === 'string' ? record['status'] : undefined;
|
|
98
|
+
const updatedAt = typeof record['updatedAt'] === 'string' ? record['updatedAt'] : undefined;
|
|
99
|
+
const loopRaw = record['loop'];
|
|
100
|
+
const used = finiteNumber(loopRaw?.['used']);
|
|
101
|
+
const max = finiteNumber(loopRaw?.['max']);
|
|
102
|
+
if (!runId || !phase || !status || !updatedAt || used === undefined || max === undefined) {
|
|
103
|
+
throw new Error('orbitRuntime projection value is incomplete');
|
|
104
|
+
}
|
|
105
|
+
const allowedPhases = new Set(['PLAN', 'EXECUTE', 'EVALUATE', 'SUCCESS', 'NEEDS_USER', 'BUDGET_EXHAUSTED', 'STOPPED']);
|
|
106
|
+
const allowedStatuses = new Set(['running', 'success', 'stopped', 'needs_user', 'budget_exhausted']);
|
|
107
|
+
if (!allowedPhases.has(phase) || !allowedStatuses.has(status))
|
|
108
|
+
throw new Error('orbitRuntime projection value has invalid phase/status');
|
|
109
|
+
const currentRaw = record['currentStep'];
|
|
110
|
+
const currentId = typeof currentRaw?.['id'] === 'string' ? currentRaw['id'] : undefined;
|
|
111
|
+
const currentAttempt = finiteNumber(currentRaw?.['attempt']);
|
|
112
|
+
const currentMode = currentRaw?.['executionMode'] === 'MOA' ? 'MOA' : currentRaw?.['executionMode'] === 'SINGLE' ? 'SINGLE' : undefined;
|
|
113
|
+
const moaRaw = record['moa'];
|
|
114
|
+
let moa;
|
|
115
|
+
if (moaRaw) {
|
|
116
|
+
const moaPhase = typeof moaRaw['phase'] === 'string' ? moaRaw['phase'] : undefined;
|
|
117
|
+
const allowedMoa = new Set(['FANOUT', 'JUDGE', 'SELECTED', 'PROMOTING', 'PROMOTED', 'FAILED']);
|
|
118
|
+
if (!moaPhase || !allowedMoa.has(moaPhase))
|
|
119
|
+
throw new Error('orbitRuntime MoA phase is invalid');
|
|
120
|
+
const candidatesRaw = Array.isArray(moaRaw['candidates']) ? moaRaw['candidates'] : [];
|
|
121
|
+
const candidates = candidatesRaw.flatMap((entry) => {
|
|
122
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
|
|
123
|
+
return [];
|
|
124
|
+
const item = entry;
|
|
125
|
+
const index = finiteNumber(item['index']);
|
|
126
|
+
const provider = typeof item['provider'] === 'string' ? item['provider'] : undefined;
|
|
127
|
+
const model = typeof item['model'] === 'string' ? item['model'] : undefined;
|
|
128
|
+
const files = finiteNumber(item['files']);
|
|
129
|
+
if (index === undefined || !provider || !model || typeof item['ok'] !== 'boolean' || files === undefined)
|
|
130
|
+
return [];
|
|
131
|
+
return [{ index, provider, model, ok: item['ok'], files, ...(usageFrom(item['usage']) ? { usage: usageFrom(item['usage']) } : {}) }];
|
|
132
|
+
});
|
|
133
|
+
moa = {
|
|
134
|
+
phase: moaPhase,
|
|
135
|
+
candidates,
|
|
136
|
+
...(typeof moaRaw['judgeModel'] === 'string' ? { judgeModel: moaRaw['judgeModel'] } : {}),
|
|
137
|
+
...(finiteNumber(moaRaw['winningCandidate']) === undefined ? {} : { winningCandidate: finiteNumber(moaRaw['winningCandidate']) }),
|
|
138
|
+
...(typeof moaRaw['winnerModel'] === 'string' ? { winnerModel: moaRaw['winnerModel'] } : {}),
|
|
139
|
+
...(usageFrom(moaRaw['totalUsage']) ? { totalUsage: usageFrom(moaRaw['totalUsage']) } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
runId,
|
|
144
|
+
phase: phase,
|
|
145
|
+
status: status,
|
|
146
|
+
loop: { used, max },
|
|
147
|
+
...(currentId && currentAttempt !== undefined && currentMode ? { currentStep: { id: currentId, attempt: currentAttempt, executionMode: currentMode } } : {}),
|
|
148
|
+
...(moa ? { moa } : {}),
|
|
149
|
+
updatedAt,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Build the bounded runtime view persisted into the owning Session. */
|
|
155
|
+
export function orbitRuntimeFromState(state) {
|
|
156
|
+
const step = state.current_step === undefined ? undefined : state.plan.steps.find((candidate) => candidate.id === state.current_step?.id);
|
|
157
|
+
const moa = state.moa_step;
|
|
158
|
+
return {
|
|
159
|
+
runId: state.run_id,
|
|
160
|
+
phase: state.phase,
|
|
161
|
+
status: state.status,
|
|
162
|
+
loop: { used: state.loop.used, max: state.loop.max },
|
|
163
|
+
...(state.current_step && step ? {
|
|
164
|
+
currentStep: {
|
|
165
|
+
id: state.current_step.id,
|
|
166
|
+
attempt: state.current_step.attempt,
|
|
167
|
+
executionMode: step.execution_mode === 'MOA' ? 'MOA' : 'SINGLE',
|
|
168
|
+
},
|
|
169
|
+
} : {}),
|
|
170
|
+
...(moa ? {
|
|
171
|
+
moa: {
|
|
172
|
+
phase: moa.phase,
|
|
173
|
+
candidates: moa.candidates.slice(0, 4).map((candidate) => ({
|
|
174
|
+
index: candidate.index,
|
|
175
|
+
provider: candidate.provider,
|
|
176
|
+
model: candidate.model,
|
|
177
|
+
ok: candidate.ok,
|
|
178
|
+
files: candidate.files.length,
|
|
179
|
+
...(candidate.usage ? { usage: candidate.usage } : {}),
|
|
180
|
+
})),
|
|
181
|
+
...(state.moa_policy?.judge ? { judgeModel: `${state.moa_policy.judge.provider}/${state.moa_policy.judge.model}` } : {}),
|
|
182
|
+
...(moa.winning_candidate === undefined ? {} : { winningCandidate: moa.winning_candidate }),
|
|
183
|
+
...(moa.winner_model ? { winnerModel: moa.winner_model } : {}),
|
|
184
|
+
...(moa.total_usage ? { totalUsage: moa.total_usage } : {}),
|
|
185
|
+
},
|
|
186
|
+
} : {}),
|
|
187
|
+
updatedAt: state.updated_at,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export function foldOrbitRuntime(state, event) {
|
|
191
|
+
if (event.type !== 'orbit/runtime')
|
|
192
|
+
return state;
|
|
193
|
+
try {
|
|
194
|
+
return orbitRuntimeSchema().parse(event.data);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return state;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** Register the per-Session enable and runtime projections on the session projection registry. */
|
|
62
201
|
export function installOrbitSessionProjection(ctx) {
|
|
63
202
|
ctx.sessionProjections.register({
|
|
64
203
|
key: ORBIT_SESSION_KEY,
|
|
@@ -71,6 +210,17 @@ export function installOrbitSessionProjection(ctx) {
|
|
|
71
210
|
},
|
|
72
211
|
stateVersion: 1,
|
|
73
212
|
});
|
|
213
|
+
ctx.sessionProjections.register({
|
|
214
|
+
key: ORBIT_RUNTIME_KEY,
|
|
215
|
+
stateSchema: orbitRuntimeSchema(),
|
|
216
|
+
init: () => null,
|
|
217
|
+
apply: (state, event) => foldOrbitRuntime(state, event),
|
|
218
|
+
wire: {
|
|
219
|
+
viewSchema: orbitRuntimeSchema(),
|
|
220
|
+
view: (state) => state,
|
|
221
|
+
},
|
|
222
|
+
stateVersion: 1,
|
|
223
|
+
});
|
|
74
224
|
}
|
|
75
225
|
/** Whether the Session currently defaults ordinary messages into Orbit. */
|
|
76
226
|
export function orbitEnabledOf(ctx, session) {
|
package/lib/state-store.js
CHANGED
|
@@ -23,8 +23,10 @@ function nowIso() {
|
|
|
23
23
|
export class OrbitStateStore {
|
|
24
24
|
stateDir;
|
|
25
25
|
lockDepth = 0;
|
|
26
|
-
|
|
26
|
+
onWrite;
|
|
27
|
+
constructor(projectDir, onWrite) {
|
|
27
28
|
this.stateDir = join(projectDir, '.cx');
|
|
29
|
+
this.onWrite = onWrite;
|
|
28
30
|
}
|
|
29
31
|
get statePath() {
|
|
30
32
|
return join(this.stateDir, 'state.json');
|
|
@@ -88,20 +90,25 @@ export class OrbitStateStore {
|
|
|
88
90
|
return raw;
|
|
89
91
|
}
|
|
90
92
|
writeState(state) {
|
|
91
|
-
|
|
93
|
+
const next = this.transact(() => {
|
|
92
94
|
const current = this.readRawState();
|
|
93
95
|
const revision = Number(current?.['state_revision'] ?? 0) + 1;
|
|
94
|
-
const
|
|
96
|
+
const updated = {
|
|
95
97
|
...state,
|
|
96
98
|
schema_version: ORBIT_SCHEMA_VERSION,
|
|
97
99
|
state_revision: revision,
|
|
98
100
|
driver_ownership: driverOwnershipFor(state.phase, state.status),
|
|
99
101
|
updated_at: nowIso(),
|
|
100
102
|
};
|
|
101
|
-
Object.assign(state,
|
|
102
|
-
this.writeJson(this.statePath,
|
|
103
|
-
return
|
|
103
|
+
Object.assign(state, updated);
|
|
104
|
+
this.writeJson(this.statePath, updated);
|
|
105
|
+
return updated;
|
|
104
106
|
});
|
|
107
|
+
try {
|
|
108
|
+
this.onWrite?.(next);
|
|
109
|
+
}
|
|
110
|
+
catch { /* UI projection must never veto durable state. */ }
|
|
111
|
+
return next;
|
|
105
112
|
}
|
|
106
113
|
writeJson(filePath, value) {
|
|
107
114
|
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
package/lib/supervisor.js
CHANGED
|
@@ -2,15 +2,17 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { buildEvidenceBundle, buildStepResult, formatStepResults, formatEvidenceBundle } from "./evidence.js";
|
|
4
4
|
import { assertCommanderDecision, assertStrategyDecision, assertTimeoutDecision, assertWatchdogDecision, assertGuardWatchdogDecision, COMMANDER_FINAL_EVALUATE_SCHEMA, COMMANDER_PLAN_SCHEMA, COMMANDER_STEP_EVALUATE_SCHEMA, COMMANDER_STRATEGY_SCHEMA, WATCHDOG_GUARD_SCHEMA, WATCHDOG_RUNTIME_SCHEMA, WATCHDOG_STRATEGY_SCHEMA, WATCHDOG_TIMEOUT_SCHEMA, } from "./decisions.js";
|
|
5
|
-
import {
|
|
5
|
+
import { OrbitMoaAdapter } from "./moa-adapter.js";
|
|
6
|
+
import { applyCommanderNeedsUser, applyCorrectionStep, applyExecutorCapabilityUnavailable, applyExecutorInterrupted, applyExecutorResume, applyExecutorSuccess, applyFinalAppend, applyFinalSuccess, applyPlan, applyStepPass, baseStepIdOf, beginStep, clearExecutorChild, clearGuardRecovery, correctionBlockCode, correctionDepthOf, createInitialState, enterBudgetExhausted, enterNeedsUser, ensureAutomaticLoopBudgetForPlan, markStrategyChallengeUsed, normalizePlan, assertMoaPlanWithinPolicy, normalizeExecutionMode, openWatchdogAttempt, recordGuardRecovery, recordPlanFailure, recordWatchdogDecision, restoreEvaluationState, resumeFromNeedsUser, stopRun, upsertStepResult, normalizeCapabilities, } from "./kernel.js";
|
|
6
7
|
import { truncateSafe } from "./sanitize.js";
|
|
7
8
|
import { OrbitStateStore } from "./state-store.js";
|
|
8
9
|
import { executorToolsFor, EXECUTOR_READ_ONLY_TOOLS, READ_ONLY_ROLE_TOOLS } from "./capabilities.js";
|
|
9
10
|
import { COMMANDER_EXTENSION_MS, COMMANDER_HARD_CEILING_MS, COMMANDER_SOFT_DEADLINE_MS, DEFAULT_CAPABILITIES, EXECUTOR_TIMEOUT_MS, GUARD_ESCALATION_THRESHOLD, GUARD_FIRST_INSTRUCTION, GUARD_NEEDS_USER_INSTRUCTION, GUARD_RECOVERY_CAP, GUARD_REPEAT_INSTRUCTION, GUARD_RETRY_INSTRUCTION, MAX_EXECUTOR_INTERRUPT_RETRIES, MAX_WATCHDOG_CALLS_PER_STEP, WATCHDOG_TIMEOUT_MS, } from "./types.js";
|
|
10
11
|
import { resolveEffectiveRoutes } from "./routes.js";
|
|
11
|
-
const COMMANDER_PLAN_PROMPT = (goal, constraints, userReply) => `你是 Orbit 指挥官(Commander),当前阶段:PLAN。
|
|
12
|
-
|
|
12
|
+
const COMMANDER_PLAN_PROMPT = (goal, constraints, userReply, moaPolicy) => `你是 Orbit 指挥官(Commander),当前阶段:PLAN。
|
|
13
|
+
请为下述目标制定最小化的 1-5 个逻辑工程步骤。
|
|
13
14
|
规则:基础读取不声明 capabilities;修改文件使用 "filesystem",执行命令使用 "shell",访问网页/API 使用 "web",驱动真实浏览器使用 "browser"。只选择当前步骤真正需要的能力,可组合,保持最小化。
|
|
15
|
+
execution_mode 只能是 SINGLE 或 MOA。普通、确定性步骤使用 SINGLE;只有存在明显多解、高不确定性且独立候选比较能提高质量时才使用 MOA。${moaPolicy ? `当前 Run 已启用 MoA,最多 ${moaPolicy.max_moa_steps} 个 MOA 步骤,候选数固定为 ${moaPolicy.candidate_count}。` : '当前 Run 未启用 MoA,所有步骤必须使用 SINGLE。'}
|
|
14
16
|
请通过结构化结果协议提交最终计划。
|
|
15
17
|
你的自然语言输出、推理说明和总结默认全部使用简体中文;decision 枚举、capability id、代码、命令、路径、provider/model ID 等机器标识保持原样。
|
|
16
18
|
目标:${goal}
|
|
@@ -23,6 +25,7 @@ const COMMANDER_STEP_PROMPT = (goal, step, evidence, state) => `你是 Orbit 指
|
|
|
23
25
|
你的自然语言输出、推理说明和总结默认全部使用简体中文;decision 枚举、代码、命令、路径、provider/model ID 等机器标识保持原样。
|
|
24
26
|
原始目标:${goal}
|
|
25
27
|
当前步骤 ${step.id}:${step.goal}
|
|
28
|
+
执行模式:${normalizeExecutionMode(step.execution_mode)}
|
|
26
29
|
迭代计数:loop ${state.loop.used}/${state.loop.max}${userReplyLine(state)}
|
|
27
30
|
执行员证据:
|
|
28
31
|
${evidence}`;
|
|
@@ -34,7 +37,7 @@ const COMMANDER_FINAL_PROMPT = (goal, plan, evidence, state) => `你是 Orbit
|
|
|
34
37
|
你的自然语言输出、推理说明和总结默认全部使用简体中文;decision 枚举、代码、命令、路径、provider/model ID 等机器标识保持原样。
|
|
35
38
|
原始目标:${goal}
|
|
36
39
|
计划摘要:${plan.summary}
|
|
37
|
-
步骤:${plan.steps.map((step) => `${step.id}:${step.goal}[${step.status}]`).join('; ')}
|
|
40
|
+
步骤:${plan.steps.map((step) => `${step.id}:${step.goal}[${step.status}/${normalizeExecutionMode(step.execution_mode)}]`).join('; ')}
|
|
38
41
|
Loop:${state.loop.used}/${state.loop.max}${userReplyLine(state)}
|
|
39
42
|
执行员证据:
|
|
40
43
|
${evidence}`;
|
|
@@ -42,6 +45,21 @@ ${evidence}`;
|
|
|
42
45
|
* The durable user reply line for role prompts. The reply is the user's answer
|
|
43
46
|
* to a NEEDS_USER question and never replaces the original goal.
|
|
44
47
|
*/
|
|
48
|
+
function sumMoaUsage(items) {
|
|
49
|
+
const present = items.filter((item) => item !== undefined);
|
|
50
|
+
if (present.length === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
const costItems = present.filter((item) => item.cost_usd !== undefined);
|
|
53
|
+
const completeCost = costItems.length === present.length;
|
|
54
|
+
return {
|
|
55
|
+
input_tokens: present.reduce((sum, item) => sum + item.input_tokens, 0),
|
|
56
|
+
output_tokens: present.reduce((sum, item) => sum + item.output_tokens, 0),
|
|
57
|
+
total_tokens: present.reduce((sum, item) => sum + item.total_tokens, 0),
|
|
58
|
+
...(present.some((item) => item.cache_read_tokens !== undefined) ? { cache_read_tokens: present.reduce((sum, item) => sum + (item.cache_read_tokens ?? 0), 0) } : {}),
|
|
59
|
+
...(present.some((item) => item.cache_write_tokens !== undefined) ? { cache_write_tokens: present.reduce((sum, item) => sum + (item.cache_write_tokens ?? 0), 0) } : {}),
|
|
60
|
+
...(completeCost ? { cost_usd: Number(costItems.reduce((sum, item) => sum + (item.cost_usd ?? 0), 0).toFixed(6)) } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
45
63
|
function userReplyLine(state) {
|
|
46
64
|
return state.pending_user_reply ? `\n用户回复(对上一个问题的回答):${state.pending_user_reply}` : '';
|
|
47
65
|
}
|
|
@@ -87,24 +105,28 @@ export class OrbitSupervisor {
|
|
|
87
105
|
store;
|
|
88
106
|
host;
|
|
89
107
|
config;
|
|
108
|
+
moa;
|
|
90
109
|
/** Evidence for the step that just settled; never persisted into state.json. */
|
|
91
110
|
stepEvidence;
|
|
92
111
|
constructor(store, host, config) {
|
|
93
112
|
this.store = store;
|
|
94
113
|
this.host = host;
|
|
95
114
|
this.config = config;
|
|
115
|
+
this.moa = config.moaAdapter ?? new OrbitMoaAdapter(host);
|
|
96
116
|
}
|
|
97
117
|
now() {
|
|
98
118
|
return this.host.now();
|
|
99
119
|
}
|
|
100
120
|
createState(input) {
|
|
101
121
|
const ownerSessionId = this.config.resolveOwnerSessionId?.();
|
|
122
|
+
const moaPolicy = this.config.resolveMoaPolicy?.();
|
|
102
123
|
return createInitialState({
|
|
103
124
|
runId: typeof input.run_id === 'string' && input.run_id.length > 0 ? input.run_id : randomUUID(),
|
|
104
125
|
now: this.now(),
|
|
105
126
|
goal: (input.goal ?? '').trim(),
|
|
106
127
|
...(input.preset !== undefined ? { preset: input.preset } : {}),
|
|
107
128
|
routes: this.resolveNewRoutes(),
|
|
129
|
+
...(moaPolicy ? { moaPolicy } : {}),
|
|
108
130
|
...(input.approved_loop_count !== undefined ? { approvedLoopCount: input.approved_loop_count } : {}),
|
|
109
131
|
...(input.max_loops !== undefined ? { maxLoops: input.max_loops } : {}),
|
|
110
132
|
...(input.user_hard_constraints ? { userHardConstraints: input.user_hard_constraints } : {}),
|
|
@@ -117,9 +139,22 @@ export class OrbitSupervisor {
|
|
|
117
139
|
return this.config.resolveRoutes();
|
|
118
140
|
return resolveEffectiveRoutes({ configRoutes: this.config.defaultRoutes });
|
|
119
141
|
}
|
|
120
|
-
async preflightRoutes(
|
|
142
|
+
async preflightRoutes(state, signal) {
|
|
143
|
+
const routes = { ...state.routes };
|
|
144
|
+
const policy = state.moa_policy;
|
|
145
|
+
if (policy) {
|
|
146
|
+
policy.candidates.forEach((route, index) => { routes['moa_candidate_' + (index + 1)] = route; });
|
|
147
|
+
routes.moa_judge = policy.judge;
|
|
148
|
+
}
|
|
121
149
|
const issues = await this.host.validateRoutes(routes, signal);
|
|
122
|
-
|
|
150
|
+
if (issues.length > 0)
|
|
151
|
+
return `ORBIT_ROLE_MODEL_UNAVAILABLE: ${issues.join(';')}。请重新选择可用模型。`;
|
|
152
|
+
if (policy) {
|
|
153
|
+
const availability = await this.moa.availability();
|
|
154
|
+
if (!availability.available)
|
|
155
|
+
return availability.reason ?? 'ORBIT_MOA_UNAVAILABLE';
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
123
158
|
}
|
|
124
159
|
async bootstrap(input, signal) {
|
|
125
160
|
if (signal?.aborted)
|
|
@@ -132,7 +167,7 @@ export class OrbitSupervisor {
|
|
|
132
167
|
const requestedGoal = (input.goal ?? '').trim();
|
|
133
168
|
let state = this.store.readState();
|
|
134
169
|
const raw = this.store.readRawState();
|
|
135
|
-
const legacy = raw !== null && raw['schema_version']
|
|
170
|
+
const legacy = raw !== null && Number(raw['schema_version'] ?? 0) < 2;
|
|
136
171
|
if (legacy && requestedGoal) {
|
|
137
172
|
let created;
|
|
138
173
|
try {
|
|
@@ -141,7 +176,7 @@ export class OrbitSupervisor {
|
|
|
141
176
|
catch (error) {
|
|
142
177
|
return { ok: false, action: 'run', message: error instanceof Error ? error.message : String(error) };
|
|
143
178
|
}
|
|
144
|
-
const invalid = await this.preflightRoutes(created
|
|
179
|
+
const invalid = await this.preflightRoutes(created, signal);
|
|
145
180
|
if (invalid)
|
|
146
181
|
return { ok: false, action: 'run', message: invalid };
|
|
147
182
|
state = this.store.writeState(created);
|
|
@@ -156,7 +191,7 @@ export class OrbitSupervisor {
|
|
|
156
191
|
catch (error) {
|
|
157
192
|
return { ok: false, action: 'run', message: error instanceof Error ? error.message : String(error) };
|
|
158
193
|
}
|
|
159
|
-
const invalid = await this.preflightRoutes(created
|
|
194
|
+
const invalid = await this.preflightRoutes(created, signal);
|
|
160
195
|
if (invalid)
|
|
161
196
|
return { ok: false, action: 'run', message: invalid };
|
|
162
197
|
state = this.store.writeState(created);
|
|
@@ -172,7 +207,7 @@ export class OrbitSupervisor {
|
|
|
172
207
|
catch (error) {
|
|
173
208
|
return { ok: false, action: 'run', message: error instanceof Error ? error.message : String(error) };
|
|
174
209
|
}
|
|
175
|
-
const invalid = await this.preflightRoutes(created
|
|
210
|
+
const invalid = await this.preflightRoutes(created, signal);
|
|
176
211
|
if (invalid)
|
|
177
212
|
return { ok: false, action: 'run', message: invalid };
|
|
178
213
|
state = this.store.writeState(created);
|
|
@@ -237,7 +272,7 @@ export class OrbitSupervisor {
|
|
|
237
272
|
if (state.phase === 'BUDGET_EXHAUSTED')
|
|
238
273
|
return this.result(state, true);
|
|
239
274
|
if (preflight) {
|
|
240
|
-
const routeIssue = await this.preflightRoutes(state
|
|
275
|
+
const routeIssue = await this.preflightRoutes(state, signal);
|
|
241
276
|
if (routeIssue)
|
|
242
277
|
return this.result(state, false, routeIssue);
|
|
243
278
|
}
|
|
@@ -294,7 +329,7 @@ export class OrbitSupervisor {
|
|
|
294
329
|
}
|
|
295
330
|
// ── commander supervised path ──────────────────────────────────────────────
|
|
296
331
|
async makePlan(state, signal) {
|
|
297
|
-
const outcome = await this.runCommander(state, 'PLAN', COMMANDER_PLAN_PROMPT(state.goal, state.user_hard_constraints, userReplyLine(state)), COMMANDER_PLAN_SCHEMA, signal);
|
|
332
|
+
const outcome = await this.runCommander(state, 'PLAN', COMMANDER_PLAN_PROMPT(state.goal, state.user_hard_constraints, userReplyLine(state), state.moa_policy), COMMANDER_PLAN_SCHEMA, signal);
|
|
298
333
|
if (outcome.kind === 'needs_user')
|
|
299
334
|
return this.setNeedsUser(state, outcome.reason);
|
|
300
335
|
if (outcome.kind === 'interrupted') {
|
|
@@ -304,6 +339,7 @@ export class OrbitSupervisor {
|
|
|
304
339
|
}
|
|
305
340
|
try {
|
|
306
341
|
const plan = normalizePlan(outcome.structured);
|
|
342
|
+
assertMoaPlanWithinPolicy(plan, state.moa_policy);
|
|
307
343
|
applyPlan(state, plan);
|
|
308
344
|
ensureAutomaticLoopBudgetForPlan(state);
|
|
309
345
|
this.store.writeState(state);
|
|
@@ -435,6 +471,106 @@ export class OrbitSupervisor {
|
|
|
435
471
|
}
|
|
436
472
|
// ── executor runtime ───────────────────────────────────────────────────────
|
|
437
473
|
async executeStep(state, step, signal) {
|
|
474
|
+
if (normalizeExecutionMode(step.execution_mode) === 'MOA') {
|
|
475
|
+
const prepared = await this.prepareMoaStep(state, step, signal);
|
|
476
|
+
if (!prepared.ready)
|
|
477
|
+
return { done: prepared.done, ok: false, ...(prepared.message ? { message: prepared.message } : {}) };
|
|
478
|
+
return this.executeExecutorStep(state, step, signal, true);
|
|
479
|
+
}
|
|
480
|
+
return this.executeExecutorStep(state, step, signal, false);
|
|
481
|
+
}
|
|
482
|
+
async prepareMoaStep(state, step, signal) {
|
|
483
|
+
const policy = state.moa_policy;
|
|
484
|
+
if (!policy) {
|
|
485
|
+
enterNeedsUser(state, 'ORBIT_MOA_UNAVAILABLE: 当前 Run 没有冻结的 MoA 配置。');
|
|
486
|
+
this.store.writeState(state);
|
|
487
|
+
return { ready: false, done: true, message: state.last_error ?? undefined };
|
|
488
|
+
}
|
|
489
|
+
const availability = await this.moa.availability();
|
|
490
|
+
if (!availability.available) {
|
|
491
|
+
enterNeedsUser(state, availability.reason ?? 'ORBIT_MOA_UNAVAILABLE');
|
|
492
|
+
this.store.writeState(state);
|
|
493
|
+
return { ready: false, done: true, message: state.last_error ?? undefined };
|
|
494
|
+
}
|
|
495
|
+
const workspace = join(this.store.stateDir, '..');
|
|
496
|
+
if (state.moa_step?.step_id !== step.id) {
|
|
497
|
+
state.moa_step = {
|
|
498
|
+
step_id: step.id,
|
|
499
|
+
phase: 'FANOUT',
|
|
500
|
+
...(availability.version ? { adapter_version: availability.version } : {}),
|
|
501
|
+
candidates: [],
|
|
502
|
+
successful_candidates: 0,
|
|
503
|
+
failed_candidates: 0,
|
|
504
|
+
};
|
|
505
|
+
this.store.writeState(state);
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
if (state.moa_step.phase === 'FANOUT') {
|
|
509
|
+
const fanout = await this.moa.fanout({ workspace, runId: state.run_id, step, policy, ...(signal ? { signal } : {}) });
|
|
510
|
+
state.moa_step = {
|
|
511
|
+
...state.moa_step,
|
|
512
|
+
phase: fanout.successful >= 2 ? 'JUDGE' : 'FAILED',
|
|
513
|
+
adapter_version: fanout.adapterVersion,
|
|
514
|
+
candidates: fanout.candidates,
|
|
515
|
+
successful_candidates: fanout.successful,
|
|
516
|
+
failed_candidates: fanout.failed,
|
|
517
|
+
...(sumMoaUsage(fanout.candidates.map((candidate) => candidate.usage)) ? { total_usage: sumMoaUsage(fanout.candidates.map((candidate) => candidate.usage)) } : {}),
|
|
518
|
+
...(fanout.successful >= 2 ? {} : { last_error: 'ORBIT_MOA_QUORUM_FAILED: 至少需要 2 个成功候选。' }),
|
|
519
|
+
};
|
|
520
|
+
this.store.writeState(state);
|
|
521
|
+
}
|
|
522
|
+
if (state.moa_step.phase === 'FAILED') {
|
|
523
|
+
applyExecutorSuccess(state, {
|
|
524
|
+
summary: state.moa_step.last_error ?? 'ORBIT_MOA_FAILED',
|
|
525
|
+
changedFiles: this.host.changedFiles(workspace),
|
|
526
|
+
testSummary: [state.moa_step.last_error ?? 'ORBIT_MOA_FAILED'],
|
|
527
|
+
});
|
|
528
|
+
upsertStepResult(state, buildStepResult(step.id, state.current_step?.attempt ?? 1, buildEvidenceBundle({ executorOutput: state.moa_step.last_error ?? 'ORBIT_MOA_FAILED' }), state.test_summary));
|
|
529
|
+
this.store.writeState(state);
|
|
530
|
+
return { ready: false, done: false };
|
|
531
|
+
}
|
|
532
|
+
if (state.moa_step.phase === 'JUDGE') {
|
|
533
|
+
const judged = await this.moa.judge({ workspace, runId: state.run_id, step, policy, candidates: state.moa_step.candidates, ...(signal ? { signal } : {}) });
|
|
534
|
+
state.moa_step = {
|
|
535
|
+
...state.moa_step,
|
|
536
|
+
phase: 'SELECTED',
|
|
537
|
+
winning_candidate: judged.winningCandidate,
|
|
538
|
+
winner_model: judged.winnerModel,
|
|
539
|
+
judge_summary: judged.summary,
|
|
540
|
+
...(judged.usage ? { judge_usage: judged.usage } : {}),
|
|
541
|
+
...(sumMoaUsage([...state.moa_step.candidates.map((candidate) => candidate.usage), judged.usage]) ? { total_usage: sumMoaUsage([...state.moa_step.candidates.map((candidate) => candidate.usage), judged.usage]) } : {}),
|
|
542
|
+
};
|
|
543
|
+
this.store.writeState(state);
|
|
544
|
+
}
|
|
545
|
+
if (state.moa_step.phase === 'SELECTED') {
|
|
546
|
+
state.moa_step.phase = 'PROMOTING';
|
|
547
|
+
this.store.writeState(state);
|
|
548
|
+
}
|
|
549
|
+
if (state.moa_step.phase === 'PROMOTING') {
|
|
550
|
+
const winner = state.moa_step.winning_candidate;
|
|
551
|
+
if (!winner)
|
|
552
|
+
throw new Error('ORBIT_MOA_WINNER_MISSING');
|
|
553
|
+
const receipt = await this.moa.promote({ workspace, runId: state.run_id, stepId: step.id, winningCandidate: winner });
|
|
554
|
+
state.moa_step = { ...state.moa_step, phase: 'PROMOTED', promotion_receipt: receipt };
|
|
555
|
+
this.store.writeState(state);
|
|
556
|
+
}
|
|
557
|
+
return { ready: state.moa_step.phase === 'PROMOTED', done: false };
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
const reason = truncateSafe(error instanceof Error ? error.message : String(error), 500);
|
|
561
|
+
if (state.moa_step)
|
|
562
|
+
state.moa_step = { ...state.moa_step, phase: 'FAILED', last_error: reason };
|
|
563
|
+
applyExecutorSuccess(state, {
|
|
564
|
+
summary: reason,
|
|
565
|
+
changedFiles: this.host.changedFiles(workspace),
|
|
566
|
+
testSummary: [reason],
|
|
567
|
+
});
|
|
568
|
+
upsertStepResult(state, buildStepResult(step.id, state.current_step?.attempt ?? 1, buildEvidenceBundle({ executorOutput: reason }), state.test_summary));
|
|
569
|
+
this.store.writeState(state);
|
|
570
|
+
return { ready: false, done: false, message: reason };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async executeExecutorStep(state, step, signal, moaVerification) {
|
|
438
574
|
const capabilities = normalizeCapabilities(step.capabilities) ?? [];
|
|
439
575
|
if (capabilities.includes('browser') && !this.config.browserTools.some((tool) => this.host.hasTool(tool))) {
|
|
440
576
|
applyExecutorCapabilityUnavailable(state, step.id);
|
|
@@ -459,7 +595,7 @@ export class OrbitSupervisor {
|
|
|
459
595
|
handle = await this.host.startRole({
|
|
460
596
|
role: 'executor',
|
|
461
597
|
label: `executor-${step.id}`,
|
|
462
|
-
prompt: this.executorPrompt(state, step),
|
|
598
|
+
prompt: this.executorPrompt(state, step, moaVerification),
|
|
463
599
|
route: state.routes.executor,
|
|
464
600
|
workspace: join(this.store.stateDir, '..'),
|
|
465
601
|
toolFilter,
|
|
@@ -513,9 +649,12 @@ export class OrbitSupervisor {
|
|
|
513
649
|
}
|
|
514
650
|
return { done: false, ok: false };
|
|
515
651
|
}
|
|
652
|
+
const moaPrefix = moaVerification && state.moa_step
|
|
653
|
+
? `MoA Judge 选择:${state.moa_step.winner_model ?? 'unknown'}(候选 ${state.moa_step.winning_candidate ?? '?'})。\nJudge:${state.moa_step.judge_summary ?? ''}\n`
|
|
654
|
+
: '';
|
|
516
655
|
applyExecutorSuccess(state, {
|
|
517
656
|
...(result.childId ? { childId: result.childId } : {}),
|
|
518
|
-
summary: truncateSafe(result.output, 2000),
|
|
657
|
+
summary: truncateSafe(moaPrefix + result.output, 2000),
|
|
519
658
|
changedFiles: result.changedFiles ?? this.host.changedFiles(join(this.store.stateDir, '..')),
|
|
520
659
|
testSummary: result.testSummary ?? [],
|
|
521
660
|
});
|
|
@@ -523,7 +662,7 @@ export class OrbitSupervisor {
|
|
|
523
662
|
stepId: step.id,
|
|
524
663
|
bundle: buildEvidenceBundle({
|
|
525
664
|
settlement: result.settlement,
|
|
526
|
-
executorOutput: result.output,
|
|
665
|
+
executorOutput: moaPrefix + result.output,
|
|
527
666
|
changedFiles: state.changed_files,
|
|
528
667
|
tools: result.toolEvidence,
|
|
529
668
|
telemetry: result.telemetry,
|
|
@@ -550,7 +689,7 @@ export class OrbitSupervisor {
|
|
|
550
689
|
...(telemetry ? { telemetry } : {}),
|
|
551
690
|
};
|
|
552
691
|
}
|
|
553
|
-
executorPrompt(state, step) {
|
|
692
|
+
executorPrompt(state, step, moaVerification = false) {
|
|
554
693
|
const lines = [
|
|
555
694
|
'你是 Orbit 执行员(Executor)。',
|
|
556
695
|
'你只负责执行当前步骤:不要重新规划整个任务,也不要自行改变当前步骤的目标。',
|
|
@@ -558,6 +697,12 @@ export class OrbitSupervisor {
|
|
|
558
697
|
'完成后用简体中文提交简洁的执行证据:做了什么、运行了哪些命令/测试、验证结果以及仍存在的风险。',
|
|
559
698
|
'你的自然语言输出、执行说明和总结默认全部使用简体中文;代码、命令、路径、provider/model ID 等机器标识保持原样。',
|
|
560
699
|
`当前步骤 ${step.id}:${step.goal}`,
|
|
700
|
+
...(moaVerification ? [
|
|
701
|
+
'该步骤已由 MoA 生成多个候选并由 Judge 选出胜者,Supervisor 已确定性地把胜出文件提升到项目目录。',
|
|
702
|
+
'你的职责是对已应用结果做真实验证:检查 diff、运行必要测试,并只在验证发现明确小问题时做最小修正。不要重新运行 MoA,也不要自行选择另一个候选。',
|
|
703
|
+
`MoA 胜出:${state.moa_step?.winner_model ?? 'unknown'} / candidate-${state.moa_step?.winning_candidate ?? '?'}`,
|
|
704
|
+
`胜出候选摘要:${state.moa_step?.candidates.find((candidate) => candidate.index === state.moa_step?.winning_candidate)?.summary ?? '无'}`,
|
|
705
|
+
] : []),
|
|
561
706
|
`工作目录:${join(this.store.stateDir, '..')}`,
|
|
562
707
|
`硬性约束:${state.user_hard_constraints.join(';') || '无'}`,
|
|
563
708
|
];
|
|
@@ -638,9 +783,19 @@ export class OrbitSupervisor {
|
|
|
638
783
|
if (reconsider.kind === 'replace')
|
|
639
784
|
nextGoal = reconsider.replacementGoal;
|
|
640
785
|
}
|
|
786
|
+
const correctionMode = decision.next_step_execution_mode === undefined
|
|
787
|
+
? normalizeExecutionMode(step.execution_mode)
|
|
788
|
+
: normalizeExecutionMode(decision.next_step_execution_mode);
|
|
789
|
+
if (correctionMode === 'MOA') {
|
|
790
|
+
const currentMoa = state.plan.steps.filter((candidate) => normalizeExecutionMode(candidate.execution_mode) === 'MOA').length;
|
|
791
|
+
if (state.moa_policy === undefined || currentMoa >= state.moa_policy.max_moa_steps) {
|
|
792
|
+
return this.setNeedsUser(state, 'ORBIT_MOA_STEP_BUDGET_EXCEEDED');
|
|
793
|
+
}
|
|
794
|
+
}
|
|
641
795
|
applyCorrectionStep(state, step, {
|
|
642
796
|
nextGoal,
|
|
643
797
|
capabilities: decision.next_step_capabilities,
|
|
798
|
+
executionMode: correctionMode,
|
|
644
799
|
});
|
|
645
800
|
this.store.writeState(state);
|
|
646
801
|
return undefined;
|
|
@@ -892,6 +1047,8 @@ export class OrbitSupervisor {
|
|
|
892
1047
|
last_error: state.last_error,
|
|
893
1048
|
pending_user_reply: state.pending_user_reply,
|
|
894
1049
|
step_results: state.step_results,
|
|
1050
|
+
moa_policy: state.moa_policy,
|
|
1051
|
+
moa_step: state.moa_step,
|
|
895
1052
|
changed_files: state.changed_files,
|
|
896
1053
|
test_summary: state.test_summary,
|
|
897
1054
|
driver_ownership: state.driver_ownership,
|
package/lib/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Durable Orbit state and decision vocabulary. */
|
|
2
|
-
export const ORBIT_SCHEMA_VERSION =
|
|
2
|
+
export const ORBIT_SCHEMA_VERSION = 3;
|
|
3
3
|
export const COMMANDER_SOFT_DEADLINE_MS = 6 * 60_000;
|
|
4
4
|
export const COMMANDER_EXTENSION_MS = 4 * 60_000;
|
|
5
5
|
export const COMMANDER_HARD_CEILING_MS = 14 * 60_000;
|
|
@@ -12,6 +12,10 @@ export const MAX_WATCHDOG_CALLS_PER_STEP = 2;
|
|
|
12
12
|
export const MAX_EXECUTOR_INTERRUPT_RETRIES = 2;
|
|
13
13
|
export const MAX_PLAN_STEPS = 5;
|
|
14
14
|
export const MIN_PLAN_STEPS = 1;
|
|
15
|
+
export const MIN_MOA_CANDIDATES = 2;
|
|
16
|
+
export const MAX_MOA_CANDIDATES = 4;
|
|
17
|
+
export const DEFAULT_MOA_CANDIDATES = 3;
|
|
18
|
+
export const DEFAULT_MAX_MOA_STEPS = 2;
|
|
15
19
|
export const DEFAULT_LOOP_BUDGET = 5;
|
|
16
20
|
/** Automatic runs reserve two bounded execution slots beyond the accepted base plan. */
|
|
17
21
|
export const AUTOMATIC_LOOP_RECOVERY_RESERVE = 2;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kasenri/dsh-orbit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "让 AI 项目可以在无人监管下持续推进。Orbit 会自动规划任务、分工执行、逐步检查并继续完成后续工作;不同环节可以使用不同模型,让低成本模型承担执行任务、强模型负责规划和审核,从而降低整体 AI 使用成本。Watchdog 还会监控运行异常,在任务卡死或中断时协助恢复。",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"@deepseek-ai/dsh-commands": "^0.1.5-rc.2",
|
|
62
62
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
63
63
|
"@deepseek-ai/dsh-subagent": "^0.1.5-rc.2",
|
|
64
|
-
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2"
|
|
64
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
65
|
+
"@goodandready/dsh-moa": ">=0.2.19"
|
|
65
66
|
},
|
|
66
67
|
"peerDependenciesMeta": {
|
|
67
68
|
"@deepseek-ai/dsh-commands": {
|
|
@@ -69,6 +70,9 @@
|
|
|
69
70
|
},
|
|
70
71
|
"@deepseek-ai/dsh-llm": {
|
|
71
72
|
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@goodandready/dsh-moa": {
|
|
75
|
+
"optional": true
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
}
|