@kasenri/dsh-orbit 0.5.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -6
- package/lib/activation.js +7 -0
- package/lib/client.js +347 -25
- package/lib/decisions.js +4 -0
- package/lib/dsh-host.js +47 -5
- package/lib/index.js +53 -12
- package/lib/kernel.js +93 -5
- package/lib/moa-adapter.js +434 -0
- package/lib/routes.js +28 -0
- package/lib/service.js +34 -3
- package/lib/session-state.js +151 -1
- package/lib/state-store.js +13 -6
- package/lib/supervisor.js +174 -16
- package/lib/types.js +8 -1
- package/package.json +7 -3
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
|
|
348
|
-
const
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
|
|
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';
|