@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.
@@ -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
- /** Register the per-Session enable projection on the session projection registry. */
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) {
@@ -23,8 +23,10 @@ function nowIso() {
23
23
  export class OrbitStateStore {
24
24
  stateDir;
25
25
  lockDepth = 0;
26
- constructor(projectDir) {
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
- return this.transact(() => {
93
+ const next = this.transact(() => {
92
94
  const current = this.readRawState();
93
95
  const revision = Number(current?.['state_revision'] ?? 0) + 1;
94
- const next = {
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, next);
102
- this.writeJson(this.statePath, next);
103
- return next;
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 { applyCommanderNeedsUser, applyCorrectionStep, applyExecutorCapabilityUnavailable, applyExecutorInterrupted, applyExecutorResume, applyExecutorSuccess, applyFinalAppend, applyFinalSuccess, applyPlan, applyStepPass, baseStepIdOf, beginStep, clearExecutorChild, clearGuardRecovery, correctionBlockCode, correctionDepthOf, createInitialState, enterBudgetExhausted, enterNeedsUser, ensureAutomaticLoopBudgetForPlan, markStrategyChallengeUsed, normalizePlan, openWatchdogAttempt, recordGuardRecovery, recordPlanFailure, recordWatchdogDecision, restoreEvaluationState, resumeFromNeedsUser, stopRun, upsertStepResult, normalizeCapabilities, } from "./kernel.js";
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
- 请为下述目标制定最小化的 1-5 个逻辑工程步骤。
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(routes, signal) {
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
- return issues.length === 0 ? undefined : `ORBIT_ROLE_MODEL_UNAVAILABLE: ${issues.join(';')}。请重新选择可用模型。`;
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'] !== 2;
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.routes, signal);
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.routes, signal);
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.routes, signal);
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.routes, signal);
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;
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.5.10",
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
  }