@kasenri/dsh-orbit 0.5.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/decisions.js CHANGED
@@ -26,6 +26,7 @@ export const COMMANDER_PLAN_SCHEMA = {
26
26
  id: { type: 'string', description: '稳定标识,例如 P0。' },
27
27
  goal: { type: 'string' },
28
28
  capabilities: { type: 'array', items: { type: 'string', enum: CAPABILITY_ENUM } },
29
+ execution_mode: { type: 'string', enum: ['SINGLE', 'MOA'] },
29
30
  },
30
31
  },
31
32
  },
@@ -40,6 +41,7 @@ export const COMMANDER_STEP_EVALUATE_SCHEMA = {
40
41
  reason: { type: 'string' },
41
42
  next_step_goal: { type: 'string' },
42
43
  next_step_capabilities: { type: 'array', items: { type: 'string', enum: CAPABILITY_ENUM } },
44
+ next_step_execution_mode: { type: 'string', enum: ['SINGLE', 'MOA'] },
43
45
  },
44
46
  };
45
47
  export const COMMANDER_FINAL_EVALUATE_SCHEMA = {
@@ -51,6 +53,7 @@ export const COMMANDER_FINAL_EVALUATE_SCHEMA = {
51
53
  summary: { type: 'string' },
52
54
  next_step_goal: { type: 'string' },
53
55
  next_step_capabilities: { type: 'array', items: { type: 'string', enum: CAPABILITY_ENUM } },
56
+ next_step_execution_mode: { type: 'string', enum: ['SINGLE', 'MOA'] },
54
57
  next_steps: {
55
58
  type: 'array',
56
59
  items: {
@@ -63,6 +66,7 @@ export const COMMANDER_FINAL_EVALUATE_SCHEMA = {
63
66
  properties: {
64
67
  goal: { type: 'string' },
65
68
  capabilities: { type: 'array', items: { type: 'string', enum: CAPABILITY_ENUM } },
69
+ execution_mode: { type: 'string', enum: ['SINGLE', 'MOA'] },
66
70
  },
67
71
  },
68
72
  ],
package/lib/dsh-host.js CHANGED
@@ -60,6 +60,28 @@ export class DshOrbitHost {
60
60
  }
61
61
  return this.startOneShot(parent, request, prompt, agentOptions);
62
62
  }
63
+ async runModel(request) {
64
+ const handle = await this.startRole({
65
+ role: 'commander',
66
+ label: request.label,
67
+ prompt: request.prompt,
68
+ route: request.route,
69
+ toolFilter: { allow: [] },
70
+ ...(request.signal ? { signal: request.signal } : {}),
71
+ });
72
+ try {
73
+ const result = await handle.result;
74
+ return {
75
+ output: result.visibleOutput ?? result.output,
76
+ interrupted: result.interrupted,
77
+ ...(result.reason ? { reason: result.reason } : {}),
78
+ ...(result.tokenUsage ? { usage: result.tokenUsage } : {}),
79
+ };
80
+ }
81
+ finally {
82
+ await this.releaseRole(handle);
83
+ }
84
+ }
63
85
  parent() {
64
86
  const initiator = this.ctx.agents.currentInitiator();
65
87
  if (initiator)
@@ -89,6 +111,7 @@ export class DshOrbitHost {
89
111
  ...(value.stopReason !== 'completed' ? { reason: value.stopReason } : {}),
90
112
  ...(value.diagnostic ? { testSummary: [value.diagnostic] } : {}),
91
113
  ...(value.structured !== undefined ? { structured: value.structured } : {}),
114
+ ...(this.readTokenUsage(run.localAgent) ? { tokenUsage: this.readTokenUsage(run.localAgent) } : {}),
92
115
  }))
93
116
  .catch((error) => ({
94
117
  childId: run.id,
@@ -262,6 +285,25 @@ export class DshOrbitHost {
262
285
  const events = agent.session?.snapshotEvents?.() ?? [];
263
286
  return events.filter((event) => event.type === 'turn/start').length > previousTurns;
264
287
  }
288
+ readTokenUsage(agent) {
289
+ const events = agent?.session?.snapshotEvents?.() ?? agent?.session?.ownEvents?.() ?? [];
290
+ for (let index = events.length - 1; index >= 0; index -= 1) {
291
+ const event = events[index];
292
+ if (!event || event.type !== 'assistant/message')
293
+ continue;
294
+ const usage = event.data?.usage;
295
+ if (typeof usage?.inputTokens !== 'number' || typeof usage.outputTokens !== 'number')
296
+ continue;
297
+ return {
298
+ inputTokens: usage.inputTokens,
299
+ outputTokens: usage.outputTokens,
300
+ ...(typeof usage.totalTokens === 'number' ? { totalTokens: usage.totalTokens } : {}),
301
+ ...(typeof usage.cacheReadTokens === 'number' ? { cacheReadTokens: usage.cacheReadTokens } : {}),
302
+ ...(typeof usage.cacheWriteTokens === 'number' ? { cacheWriteTokens: usage.cacheWriteTokens } : {}),
303
+ };
304
+ }
305
+ return undefined;
306
+ }
265
307
  readFinalOutput(agent) {
266
308
  const events = agent.session?.snapshotEvents?.() ?? agent.session?.ownEvents?.() ?? [];
267
309
  for (let index = events.length - 1; index >= 0; index -= 1) {
@@ -344,8 +386,8 @@ export class DshOrbitHost {
344
386
  async validateRoutes(routes, signal) {
345
387
  const issues = [];
346
388
  const labels = { commander: '指挥官', executor: '执行员', watchdog: '监控模型' };
347
- for (const role of ['commander', 'executor', 'watchdog']) {
348
- const route = routes[role];
389
+ for (const [role, route] of Object.entries(routes)) {
390
+ const label = labels[role] ?? role;
349
391
  try {
350
392
  const llm = this.ctx.reflect.get('llm');
351
393
  if (!llm)
@@ -355,18 +397,18 @@ export class DshOrbitHost {
355
397
  const info = await llm.resolveModelInfo(route.provider, route.model, activeSignal);
356
398
  const catalog = await llm.listModels(route.provider);
357
399
  if (catalog.length > 0 && !catalog.some((entry) => entry.id === route.model)) {
358
- issues.push(`${labels[role]}:ORBIT_MODEL_UNAVAILABLE (${route.provider}/${route.model}),当前模型目录中不存在,请重新选择。`);
400
+ issues.push(`${label}:ORBIT_MODEL_UNAVAILABLE (${route.provider}/${route.model}),当前模型目录中不存在,请重新选择。`);
359
401
  continue;
360
402
  }
361
403
  if (route.reasoningEffort !== undefined) {
362
404
  const efforts = info.reasoning?.efforts ?? [];
363
405
  if (!efforts.some((effort) => effort.id === route.reasoningEffort)) {
364
- issues.push(`${labels[role]}:ORBIT_REASONING_EFFORT_UNAVAILABLE (${route.provider}/${route.model}/${route.reasoningEffort}),当前模型未声明此推理等级。`);
406
+ issues.push(`${label}:ORBIT_REASONING_EFFORT_UNAVAILABLE (${route.provider}/${route.model}/${route.reasoningEffort}),当前模型未声明此推理等级。`);
365
407
  }
366
408
  }
367
409
  }
368
410
  catch (error) {
369
- issues.push(`${labels[role]}:ORBIT_MODEL_UNAVAILABLE (${route.provider}/${route.model}),当前不可用,请重新选择:${truncateSafe(error instanceof Error ? error.message : String(error), 200)}`);
411
+ issues.push(`${label}:ORBIT_MODEL_UNAVAILABLE (${route.provider}/${route.model}),当前不可用,请重新选择:${truncateSafe(error instanceof Error ? error.message : String(error), 200)}`);
370
412
  }
371
413
  }
372
414
  return issues;
package/lib/index.js CHANGED
@@ -2,17 +2,12 @@ import { AssistantStreamAccumulator, boundContextSummary, createAssistantMessage
2
2
  import z from '@deepseek-ai/schemastery';
3
3
  import { installOrbitGestureBoundary, registerOrbitCommand, registerOrbitToggleCommand } from "./activation.js";
4
4
  import { createOrbitPreExecuteHandler } from "./pipeline-guard.js";
5
- import { agentDefaultSelectionOf, resolveEffectiveRoutes, sessionModelSelectionOf, sessionModelStateOf } from "./routes.js";
5
+ import { agentDefaultSelectionOf, resolveEffectiveRoutes, resolveMoaPolicy, sessionModelSelectionOf, sessionModelStateOf } from "./routes.js";
6
6
  import { installOrbitSessionProjection, orbitEnabledOf } from "./session-state.js";
7
7
  import { OrbitService } from "./service.js";
8
8
  import { createOrbitTool } from "./tool.js";
9
- /**
10
- * Fixed hard-activation budget. Explicit tool calls may still supply a bounded
11
- * user-owned budget; ordinary activation never guesses complexity from words.
12
- */
13
- const HARD_ACTIVATION_LOOP_BUDGET = 5;
14
9
  export const name = 'dsh-orbit';
15
- export const inject = ['tools', 'agents', 'subagents'];
10
+ export const inject = ['tools', 'agents', 'subagents', 'sessions'];
16
11
  const Route = z.object({
17
12
  provider: z.string(),
18
13
  model: z.string(),
@@ -24,9 +19,18 @@ const RoleRoute = z.object({
24
19
  reasoningEffort: z.string().default(''),
25
20
  });
26
21
  /** `orbit` settings namespace: the two role routes Orbit persists itself. */
22
+ export const OrbitMoaSettingsSchema = z.object({
23
+ enabled: z.boolean().default(false),
24
+ candidateCount: z.natural().default(3),
25
+ peerCritique: z.boolean().default(false),
26
+ maxMoaSteps: z.natural().default(2),
27
+ candidates: z.array(RoleRoute).default([]),
28
+ judge: RoleRoute,
29
+ });
27
30
  export const OrbitRouteSettingsSchema = z.object({
28
31
  commander: RoleRoute,
29
32
  watchdog: RoleRoute,
33
+ moa: OrbitMoaSettingsSchema,
30
34
  });
31
35
  export const Config = z.object({
32
36
  projectDir: z.string(),
@@ -37,6 +41,7 @@ export const Config = z.object({
37
41
  watchdog: Route,
38
42
  })
39
43
  .default({}),
44
+ moa: OrbitMoaSettingsSchema.default({}),
40
45
  browserTools: z.array(z.string()).default(['agent_browser']),
41
46
  commanderReadOnlyTools: z.array(z.string()).default(['read', 'read_image', 'glob', 'grep', 'web_search', 'web_fetch']),
42
47
  watchdogTools: z.array(z.string()).default(['read', 'read_image', 'glob', 'grep']),
@@ -52,15 +57,37 @@ export function apply(ctx, config) {
52
57
  // Orbit settings bridge. The package ships no model defaults; only explicit
53
58
  // profile routes can act as a headless compatibility fallback.
54
59
  let readRouteSettings = () => undefined;
60
+ let readMoaPrices = () => undefined;
55
61
  ctx.inject(['settings'], (settingsCtx) => {
56
62
  const scope = settingsCtx.settings.register('orbit', OrbitRouteSettingsSchema, {
57
63
  base: {
58
64
  commander: { provider: '', model: '', reasoningEffort: '' },
59
65
  watchdog: { provider: '', model: '', reasoningEffort: '' },
66
+ moa: {
67
+ enabled: config.moa.enabled ?? false,
68
+ candidateCount: config.moa.candidateCount ?? 3,
69
+ peerCritique: config.moa.peerCritique ?? false,
70
+ maxMoaSteps: config.moa.maxMoaSteps ?? 2,
71
+ candidates: (config.moa.candidates ?? []).map((route) => ({
72
+ provider: route.provider,
73
+ model: route.model,
74
+ reasoningEffort: route.reasoningEffort ?? '',
75
+ })),
76
+ judge: config.moa.judge
77
+ ? { provider: config.moa.judge.provider, model: config.moa.judge.model, reasoningEffort: config.moa.judge.reasoningEffort ?? '' }
78
+ : { provider: '', model: '', reasoningEffort: '' },
79
+ },
60
80
  },
61
81
  });
62
82
  readRouteSettings = () => scope.get();
63
- settingsCtx.effect(() => () => { readRouteSettings = () => undefined; });
83
+ readMoaPrices = () => {
84
+ const value = settingsCtx.settings.get('dsh-moa');
85
+ return value?.prices;
86
+ };
87
+ settingsCtx.effect(() => () => {
88
+ readRouteSettings = () => undefined;
89
+ readMoaPrices = () => undefined;
90
+ });
64
91
  });
65
92
  // A NEW run resolves its three routes exactly once: Commander/Watchdog from
66
93
  // the Orbit settings (base = config), Executor from the initiating Session's
@@ -82,6 +109,11 @@ export function apply(ctx, config) {
82
109
  };
83
110
  // The DSH Session driving the current operation: a new run records it, and
84
111
  // only that Session may answer the run's NEEDS_USER question.
112
+ const resolveMoaPolicyForRun = () => resolveMoaPolicy({
113
+ settings: readRouteSettings()?.moa,
114
+ config: config.moa,
115
+ ...(readMoaPrices() ? { prices: readMoaPrices() } : {}),
116
+ });
85
117
  const resolveOwnerSessionId = () => {
86
118
  const agent = ctx.agents.currentInitiator();
87
119
  return agent === undefined ? undefined : String(agent.session.id);
@@ -89,6 +121,7 @@ export function apply(ctx, config) {
89
121
  const serviceConfig = {
90
122
  routes: config.routes,
91
123
  resolveRoutes,
124
+ resolveMoaPolicy: resolveMoaPolicyForRun,
92
125
  resolveOwnerSessionId,
93
126
  browserTools: config.browserTools,
94
127
  commanderReadOnlyTools: config.commanderReadOnlyTools,
@@ -120,16 +153,24 @@ export function apply(ctx, config) {
120
153
  });
121
154
  installOrbitGestureBoundary(ctx, {
122
155
  sessionEnabled: (session) => orbitEnabledOf(ctx, session),
156
+ competingMutationBlock: (agent, messages) => {
157
+ const cwd = agent.session.header.cwd ?? process.cwd();
158
+ if (!service.hasActiveRun(cwd))
159
+ return undefined;
160
+ const latest = [...messages].reverse().find((message) => message.source.kind === 'user');
161
+ const text = latest?.content.filter((block) => block.type === 'text').map((block) => block.text).join('\n').trimStart() ?? '';
162
+ return /^\/moa(?=$|[\t\n\r ])/u.test(text)
163
+ ? 'ORBIT_MUTATION_DRIVER_CONFLICT: 当前 workspace 已由 Orbit 持有,不能同时启动独立 /moa。请先完成或停止当前 Orbit Run。'
164
+ : undefined;
165
+ },
166
+ onBlocked: (agent, reason) => appendOrbitNotice(agent.session, reason),
123
167
  activate: async (agent, goal, position, signal) => {
124
168
  const cwd = agent.session.header.cwd ?? process.cwd();
125
169
  let result;
126
170
  try {
127
171
  // The initiator scope makes the run's children belong to this Agent;
128
172
  // the parent model is never asked to decide or to run the task.
129
- result = await ctx.agents.withInitiator(agent, () => service.run({
130
- goal,
131
- approved_loop_count: HARD_ACTIVATION_LOOP_BUDGET,
132
- }, cwd, signal));
173
+ result = await ctx.agents.withInitiator(agent, () => service.run({ goal }, cwd, signal));
133
174
  }
134
175
  catch (error) {
135
176
  appendOrbitNotice(agent.session, `Orbit 启动失败:${error instanceof Error ? error.message : String(error)}`);
package/lib/kernel.js CHANGED
@@ -5,11 +5,60 @@
5
5
  * read a clock, or use randomness. Runtime values (routes, timestamps, run ids)
6
6
  * are supplied by the supervisor, which owns orchestration and persistence.
7
7
  */
8
- import { MAX_CORRECTION_DEPTH, DEFAULT_LOOP_BUDGET, MAX_PLAN_STEPS, MAX_WATCHDOG_CALLS_PER_STEP, MIN_PLAN_STEPS, ORBIT_SCHEMA_VERSION, } from "./types.js";
8
+ import { MAX_CORRECTION_DEPTH, DEFAULT_LOOP_BUDGET, AUTOMATIC_LOOP_RECOVERY_RESERVE, MAX_AUTOMATIC_LOOP_BUDGET, MAX_PLAN_STEPS, MAX_WATCHDOG_CALLS_PER_STEP, MIN_PLAN_STEPS, MIN_MOA_CANDIDATES, MAX_MOA_CANDIDATES, DEFAULT_MAX_MOA_STEPS, ORBIT_SCHEMA_VERSION, } from "./types.js";
9
9
  /** Capabilities a plan step may request. */
10
10
  export const ORBIT_CAPABILITIES = ['filesystem', 'shell', 'web', 'browser'];
11
11
  const STEP_CAPABILITIES = new Set(ORBIT_CAPABILITIES);
12
12
  const PLAN_STEP_ID = /^P\d+$/u;
13
+ export function normalizeExecutionMode(value) {
14
+ return value === 'MOA' ? 'MOA' : 'SINGLE';
15
+ }
16
+ export function normalizeMoaPolicy(policy) {
17
+ if (policy === undefined || policy.enabled !== true)
18
+ return undefined;
19
+ if (!Number.isSafeInteger(policy.candidate_count) || policy.candidate_count < MIN_MOA_CANDIDATES || policy.candidate_count > MAX_MOA_CANDIDATES) {
20
+ throw new Error(`ORBIT_MOA_CANDIDATE_COUNT_INVALID: expected ${MIN_MOA_CANDIDATES}-${MAX_MOA_CANDIDATES}`);
21
+ }
22
+ if (!Number.isSafeInteger(policy.max_moa_steps) || policy.max_moa_steps < 1 || policy.max_moa_steps > MAX_PLAN_STEPS) {
23
+ throw new Error(`ORBIT_MOA_STEP_BUDGET_INVALID: expected 1-${MAX_PLAN_STEPS}`);
24
+ }
25
+ if (policy.candidates.length < policy.candidate_count) {
26
+ throw new Error('ORBIT_MOA_ROUTES_INCOMPLETE: 候选模型数量不足。');
27
+ }
28
+ const candidates = policy.candidates.slice(0, policy.candidate_count).map((route) => structuredClone(route));
29
+ if (candidates.some((route) => !route.provider || !route.model) || !policy.judge?.provider || !policy.judge.model) {
30
+ throw new Error('ORBIT_MOA_ROUTES_INCOMPLETE: 候选模型或 Judge 未完整配置。');
31
+ }
32
+ const prices = policy.prices === undefined
33
+ ? undefined
34
+ : Object.fromEntries(Object.entries(policy.prices).flatMap(([key, row]) => {
35
+ const input = Number(row?.input);
36
+ const output = Number(row?.output);
37
+ const cacheHit = row?.cacheHit === undefined ? undefined : Number(row.cacheHit);
38
+ if (!Number.isFinite(input) || input < 0 || !Number.isFinite(output) || output < 0 || (cacheHit !== undefined && (!Number.isFinite(cacheHit) || cacheHit < 0)))
39
+ return [];
40
+ return [[key, { input, output, ...(cacheHit === undefined ? {} : { cacheHit }) }]];
41
+ }));
42
+ return {
43
+ enabled: true,
44
+ candidate_count: policy.candidate_count,
45
+ peer_critique: policy.peer_critique === true,
46
+ max_moa_steps: policy.max_moa_steps || DEFAULT_MAX_MOA_STEPS,
47
+ candidates,
48
+ judge: structuredClone(policy.judge),
49
+ ...(prices && Object.keys(prices).length > 0 ? { prices } : {}),
50
+ };
51
+ }
52
+ export function assertMoaPlanWithinPolicy(plan, policy) {
53
+ const moaSteps = plan.steps.filter((step) => normalizeExecutionMode(step.execution_mode) === 'MOA').length;
54
+ if (moaSteps === 0)
55
+ return;
56
+ if (policy === undefined)
57
+ throw new Error('ORBIT_MOA_UNAVAILABLE: 当前 Run 未启用或未完整配置 MoA。');
58
+ if (moaSteps > policy.max_moa_steps) {
59
+ throw new Error(`ORBIT_MOA_STEP_BUDGET_EXCEEDED: plan requests ${moaSteps}, max is ${policy.max_moa_steps}`);
60
+ }
61
+ }
13
62
  export function normalizeCapabilities(value) {
14
63
  if (!Array.isArray(value))
15
64
  return undefined;
@@ -40,10 +89,12 @@ export function normalizePlan(plan) {
40
89
  const id = PLAN_STEP_ID.test(rawId) ? rawId : `P${index}`;
41
90
  const goal = String(record.goal ?? '').trim();
42
91
  const capabilities = normalizeCapabilities(record.capabilities);
92
+ const execution_mode = normalizeExecutionMode(record.execution_mode);
43
93
  return {
44
94
  id,
45
95
  goal,
46
96
  ...(capabilities ? { capabilities } : {}),
97
+ ...(execution_mode === 'MOA' ? { execution_mode } : {}),
47
98
  status: 'pending',
48
99
  };
49
100
  });
@@ -83,6 +134,29 @@ export function updateLoopBudget(state, budget) {
83
134
  state.loop_count = state.loop.used;
84
135
  state.remaining_budget = Math.max(0, budget - state.loop.used);
85
136
  }
137
+ /**
138
+ * Deterministic budget for an Orbit-owned run after PLAN is known.
139
+ * Small plans keep the historical floor of five; four/five-step plans gain two
140
+ * bounded recovery slots, with a hard automatic ceiling of seven.
141
+ */
142
+ export function automaticLoopBudgetForPlan(stepCount) {
143
+ if (!Number.isSafeInteger(stepCount) || stepCount < MIN_PLAN_STEPS || stepCount > MAX_PLAN_STEPS) {
144
+ throw new Error(`ORBIT_PLAN_STEP_COUNT_INVALID: expected ${MIN_PLAN_STEPS}-${MAX_PLAN_STEPS}, got ${stepCount}`);
145
+ }
146
+ return Math.min(MAX_AUTOMATIC_LOOP_BUDGET, Math.max(DEFAULT_LOOP_BUDGET, stepCount + AUTOMATIC_LOOP_RECOVERY_RESERVE));
147
+ }
148
+ /**
149
+ * Expand only Orbit-owned automatic budgets. Explicit user/tool budgets and
150
+ * old states without a recorded mode are never silently increased.
151
+ */
152
+ export function ensureAutomaticLoopBudgetForPlan(state) {
153
+ if (state.loop_budget_mode !== 'automatic')
154
+ return;
155
+ const baseSteps = state.plan.steps.filter((step) => isBaseStepId(step.id)).length;
156
+ const target = automaticLoopBudgetForPlan(baseSteps);
157
+ if (target > state.loop.max)
158
+ updateLoopBudget(state, target);
159
+ }
86
160
  export function hashGoal(goal) {
87
161
  let hash = 0;
88
162
  for (let index = 0; index < goal.length; index += 1) {
@@ -101,14 +175,16 @@ export function normalizeAppend(decision) {
101
175
  const goal = String(entry.goal ?? '').trim();
102
176
  if (goal) {
103
177
  const capabilities = normalizeCapabilities(entry.capabilities);
104
- items.push({ goal, ...(capabilities ? { capabilities } : {}) });
178
+ const execution_mode = normalizeExecutionMode(entry.execution_mode);
179
+ items.push({ goal, ...(capabilities ? { capabilities } : {}), ...(execution_mode === 'MOA' ? { execution_mode } : {}) });
105
180
  }
106
181
  }
107
182
  }
108
183
  }
109
184
  if (items.length === 0 && decision.next_step_goal?.trim()) {
110
185
  const capabilities = normalizeCapabilities(decision.next_step_capabilities);
111
- items.push({ goal: decision.next_step_goal.trim(), ...(capabilities ? { capabilities } : {}) });
186
+ const execution_mode = normalizeExecutionMode(decision.next_step_execution_mode);
187
+ items.push({ goal: decision.next_step_goal.trim(), ...(capabilities ? { capabilities } : {}), ...(execution_mode === 'MOA' ? { execution_mode } : {}) });
112
188
  }
113
189
  return items;
114
190
  }
@@ -123,8 +199,9 @@ export function correctionBlockCode(state, step) {
123
199
  return undefined;
124
200
  }
125
201
  export function createInitialState(input) {
126
- const max = explicitLoopBudget({ approved_loop_count: input.approvedLoopCount, max_loops: input.maxLoops }) ??
127
- DEFAULT_LOOP_BUDGET;
202
+ const explicit = explicitLoopBudget({ approved_loop_count: input.approvedLoopCount, max_loops: input.maxLoops });
203
+ const max = explicit ?? DEFAULT_LOOP_BUDGET;
204
+ const moaPolicy = normalizeMoaPolicy(input.moaPolicy);
128
205
  return {
129
206
  schema_version: ORBIT_SCHEMA_VERSION,
130
207
  active_run_id: input.runId,
@@ -138,7 +215,9 @@ export function createInitialState(input) {
138
215
  goal_hash: hashGoal(input.goal),
139
216
  preset: input.preset ?? 'orbit-lite',
140
217
  routes: structuredClone(input.routes),
218
+ ...(moaPolicy ? { moa_policy: moaPolicy } : {}),
141
219
  loop: { used: 0, max },
220
+ loop_budget_mode: explicit === undefined ? 'automatic' : 'explicit',
142
221
  approved_loop_count: max,
143
222
  remaining_budget: max,
144
223
  loop_count: 0,
@@ -265,6 +344,10 @@ export function applyFinalAppend(state, decision) {
265
344
  const appended = normalizeAppend(decision);
266
345
  if (appended.length === 0)
267
346
  return 'invalid';
347
+ const currentMoa = state.plan.steps.filter((step) => normalizeExecutionMode(step.execution_mode) === 'MOA').length;
348
+ const addedMoa = appended.filter((step) => normalizeExecutionMode(step.execution_mode) === 'MOA').length;
349
+ if (addedMoa > 0 && (state.moa_policy === undefined || currentMoa + addedMoa > state.moa_policy.max_moa_steps))
350
+ return 'invalid';
268
351
  if (remaining <= 0) {
269
352
  enterBudgetExhausted(state);
270
353
  return 'budget_exhausted';
@@ -277,6 +360,7 @@ export function applyFinalAppend(state, decision) {
277
360
  id: `P${index}`,
278
361
  goal: item.goal,
279
362
  ...(item.capabilities ? { capabilities: item.capabilities } : {}),
363
+ ...(item.execution_mode === 'MOA' ? { execution_mode: 'MOA' } : {}),
280
364
  status: 'pending',
281
365
  });
282
366
  }
@@ -294,10 +378,14 @@ export function applyCorrectionStep(state, step, input) {
294
378
  const correctionCapabilities = input.capabilities === undefined
295
379
  ? step.capabilities
296
380
  : normalizeCapabilities(input.capabilities);
381
+ const correctionMode = input.executionMode === undefined
382
+ ? normalizeExecutionMode(step.execution_mode)
383
+ : normalizeExecutionMode(input.executionMode);
297
384
  state.plan.steps.splice(insertAt, 0, {
298
385
  id: `${base}-${number}`,
299
386
  goal: input.nextGoal,
300
387
  ...(correctionCapabilities ? { capabilities: correctionCapabilities } : {}),
388
+ ...(correctionMode === 'MOA' ? { execution_mode: 'MOA' } : {}),
301
389
  status: 'pending',
302
390
  });
303
391
  state.phase = 'EXECUTE';