@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.
@@ -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, 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,7 +339,9 @@ 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);
344
+ ensureAutomaticLoopBudgetForPlan(state);
308
345
  this.store.writeState(state);
309
346
  return undefined;
310
347
  }
@@ -434,6 +471,106 @@ export class OrbitSupervisor {
434
471
  }
435
472
  // ── executor runtime ───────────────────────────────────────────────────────
436
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) {
437
574
  const capabilities = normalizeCapabilities(step.capabilities) ?? [];
438
575
  if (capabilities.includes('browser') && !this.config.browserTools.some((tool) => this.host.hasTool(tool))) {
439
576
  applyExecutorCapabilityUnavailable(state, step.id);
@@ -458,7 +595,7 @@ export class OrbitSupervisor {
458
595
  handle = await this.host.startRole({
459
596
  role: 'executor',
460
597
  label: `executor-${step.id}`,
461
- prompt: this.executorPrompt(state, step),
598
+ prompt: this.executorPrompt(state, step, moaVerification),
462
599
  route: state.routes.executor,
463
600
  workspace: join(this.store.stateDir, '..'),
464
601
  toolFilter,
@@ -512,9 +649,12 @@ export class OrbitSupervisor {
512
649
  }
513
650
  return { done: false, ok: false };
514
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
+ : '';
515
655
  applyExecutorSuccess(state, {
516
656
  ...(result.childId ? { childId: result.childId } : {}),
517
- summary: truncateSafe(result.output, 2000),
657
+ summary: truncateSafe(moaPrefix + result.output, 2000),
518
658
  changedFiles: result.changedFiles ?? this.host.changedFiles(join(this.store.stateDir, '..')),
519
659
  testSummary: result.testSummary ?? [],
520
660
  });
@@ -522,7 +662,7 @@ export class OrbitSupervisor {
522
662
  stepId: step.id,
523
663
  bundle: buildEvidenceBundle({
524
664
  settlement: result.settlement,
525
- executorOutput: result.output,
665
+ executorOutput: moaPrefix + result.output,
526
666
  changedFiles: state.changed_files,
527
667
  tools: result.toolEvidence,
528
668
  telemetry: result.telemetry,
@@ -549,7 +689,7 @@ export class OrbitSupervisor {
549
689
  ...(telemetry ? { telemetry } : {}),
550
690
  };
551
691
  }
552
- executorPrompt(state, step) {
692
+ executorPrompt(state, step, moaVerification = false) {
553
693
  const lines = [
554
694
  '你是 Orbit 执行员(Executor)。',
555
695
  '你只负责执行当前步骤:不要重新规划整个任务,也不要自行改变当前步骤的目标。',
@@ -557,6 +697,12 @@ export class OrbitSupervisor {
557
697
  '完成后用简体中文提交简洁的执行证据:做了什么、运行了哪些命令/测试、验证结果以及仍存在的风险。',
558
698
  '你的自然语言输出、执行说明和总结默认全部使用简体中文;代码、命令、路径、provider/model ID 等机器标识保持原样。',
559
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
+ ] : []),
560
706
  `工作目录:${join(this.store.stateDir, '..')}`,
561
707
  `硬性约束:${state.user_hard_constraints.join(';') || '无'}`,
562
708
  ];
@@ -637,9 +783,19 @@ export class OrbitSupervisor {
637
783
  if (reconsider.kind === 'replace')
638
784
  nextGoal = reconsider.replacementGoal;
639
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
+ }
640
795
  applyCorrectionStep(state, step, {
641
796
  nextGoal,
642
797
  capabilities: decision.next_step_capabilities,
798
+ executionMode: correctionMode,
643
799
  });
644
800
  this.store.writeState(state);
645
801
  return undefined;
@@ -891,6 +1047,8 @@ export class OrbitSupervisor {
891
1047
  last_error: state.last_error,
892
1048
  pending_user_reply: state.pending_user_reply,
893
1049
  step_results: state.step_results,
1050
+ moa_policy: state.moa_policy,
1051
+ moa_step: state.moa_step,
894
1052
  changed_files: state.changed_files,
895
1053
  test_summary: state.test_summary,
896
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,7 +12,14 @@ 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;
20
+ /** Automatic runs reserve two bounded execution slots beyond the accepted base plan. */
21
+ export const AUTOMATIC_LOOP_RECOVERY_RESERVE = 2;
22
+ export const MAX_AUTOMATIC_LOOP_BUDGET = MAX_PLAN_STEPS + AUTOMATIC_LOOP_RECOVERY_RESERVE;
16
23
  export const GUARD_FIRST_INSTRUCTION = '请改用更安全的方法继续当前任务,不要原样重试刚被阻断的操作。';
17
24
  export const GUARD_REPEAT_INSTRUCTION = '同一操作再次被阻断。请停止重复,并选择不同的安全方案。';
18
25
  export const GUARD_RETRY_INSTRUCTION = '之前的方案反复触发 Orbit 安全护栏。请改用不同的安全方案,不要重试被阻断的操作。';
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@kasenri/dsh-orbit",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
- "description": "Deterministic engineering orchestration for DeepSeek Harness with Commander, Executor, Smart Watchdog, bounded recovery and durable execution state.",
5
+ "description": "让 AI 项目可以在无人监管下持续推进。Orbit 会自动规划任务、分工执行、逐步检查并继续完成后续工作;不同环节可以使用不同模型,让低成本模型承担执行任务、强模型负责规划和审核,从而降低整体 AI 使用成本。Watchdog 还会监控运行异常,在任务卡死或中断时协助恢复。",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -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
  }