@kasenri/dsh-orbit 0.5.10 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,12 +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
9
  export const name = 'dsh-orbit';
10
- export const inject = ['tools', 'agents', 'subagents'];
10
+ export const inject = ['tools', 'agents', 'subagents', 'sessions'];
11
11
  const Route = z.object({
12
12
  provider: z.string(),
13
13
  model: z.string(),
@@ -19,9 +19,18 @@ const RoleRoute = z.object({
19
19
  reasoningEffort: z.string().default(''),
20
20
  });
21
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
+ });
22
30
  export const OrbitRouteSettingsSchema = z.object({
23
31
  commander: RoleRoute,
24
32
  watchdog: RoleRoute,
33
+ moa: OrbitMoaSettingsSchema,
25
34
  });
26
35
  export const Config = z.object({
27
36
  projectDir: z.string(),
@@ -32,6 +41,7 @@ export const Config = z.object({
32
41
  watchdog: Route,
33
42
  })
34
43
  .default({}),
44
+ moa: OrbitMoaSettingsSchema.default({}),
35
45
  browserTools: z.array(z.string()).default(['agent_browser']),
36
46
  commanderReadOnlyTools: z.array(z.string()).default(['read', 'read_image', 'glob', 'grep', 'web_search', 'web_fetch']),
37
47
  watchdogTools: z.array(z.string()).default(['read', 'read_image', 'glob', 'grep']),
@@ -47,15 +57,37 @@ export function apply(ctx, config) {
47
57
  // Orbit settings bridge. The package ships no model defaults; only explicit
48
58
  // profile routes can act as a headless compatibility fallback.
49
59
  let readRouteSettings = () => undefined;
60
+ let readMoaPrices = () => undefined;
50
61
  ctx.inject(['settings'], (settingsCtx) => {
51
62
  const scope = settingsCtx.settings.register('orbit', OrbitRouteSettingsSchema, {
52
63
  base: {
53
64
  commander: { provider: '', model: '', reasoningEffort: '' },
54
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
+ },
55
80
  },
56
81
  });
57
82
  readRouteSettings = () => scope.get();
58
- 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
+ });
59
91
  });
60
92
  // A NEW run resolves its three routes exactly once: Commander/Watchdog from
61
93
  // the Orbit settings (base = config), Executor from the initiating Session's
@@ -77,6 +109,11 @@ export function apply(ctx, config) {
77
109
  };
78
110
  // The DSH Session driving the current operation: a new run records it, and
79
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
+ });
80
117
  const resolveOwnerSessionId = () => {
81
118
  const agent = ctx.agents.currentInitiator();
82
119
  return agent === undefined ? undefined : String(agent.session.id);
@@ -84,6 +121,7 @@ export function apply(ctx, config) {
84
121
  const serviceConfig = {
85
122
  routes: config.routes,
86
123
  resolveRoutes,
124
+ resolveMoaPolicy: resolveMoaPolicyForRun,
87
125
  resolveOwnerSessionId,
88
126
  browserTools: config.browserTools,
89
127
  commanderReadOnlyTools: config.commanderReadOnlyTools,
@@ -115,6 +153,17 @@ export function apply(ctx, config) {
115
153
  });
116
154
  installOrbitGestureBoundary(ctx, {
117
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),
118
167
  activate: async (agent, goal, position, signal) => {
119
168
  const cwd = agent.session.header.cwd ?? process.cwd();
120
169
  let result;
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, AUTOMATIC_LOOP_RECOVERY_RESERVE, MAX_AUTOMATIC_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
  });
@@ -124,14 +175,16 @@ export function normalizeAppend(decision) {
124
175
  const goal = String(entry.goal ?? '').trim();
125
176
  if (goal) {
126
177
  const capabilities = normalizeCapabilities(entry.capabilities);
127
- items.push({ goal, ...(capabilities ? { capabilities } : {}) });
178
+ const execution_mode = normalizeExecutionMode(entry.execution_mode);
179
+ items.push({ goal, ...(capabilities ? { capabilities } : {}), ...(execution_mode === 'MOA' ? { execution_mode } : {}) });
128
180
  }
129
181
  }
130
182
  }
131
183
  }
132
184
  if (items.length === 0 && decision.next_step_goal?.trim()) {
133
185
  const capabilities = normalizeCapabilities(decision.next_step_capabilities);
134
- 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 } : {}) });
135
188
  }
136
189
  return items;
137
190
  }
@@ -148,6 +201,7 @@ export function correctionBlockCode(state, step) {
148
201
  export function createInitialState(input) {
149
202
  const explicit = explicitLoopBudget({ approved_loop_count: input.approvedLoopCount, max_loops: input.maxLoops });
150
203
  const max = explicit ?? DEFAULT_LOOP_BUDGET;
204
+ const moaPolicy = normalizeMoaPolicy(input.moaPolicy);
151
205
  return {
152
206
  schema_version: ORBIT_SCHEMA_VERSION,
153
207
  active_run_id: input.runId,
@@ -161,6 +215,7 @@ export function createInitialState(input) {
161
215
  goal_hash: hashGoal(input.goal),
162
216
  preset: input.preset ?? 'orbit-lite',
163
217
  routes: structuredClone(input.routes),
218
+ ...(moaPolicy ? { moa_policy: moaPolicy } : {}),
164
219
  loop: { used: 0, max },
165
220
  loop_budget_mode: explicit === undefined ? 'automatic' : 'explicit',
166
221
  approved_loop_count: max,
@@ -289,6 +344,10 @@ export function applyFinalAppend(state, decision) {
289
344
  const appended = normalizeAppend(decision);
290
345
  if (appended.length === 0)
291
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';
292
351
  if (remaining <= 0) {
293
352
  enterBudgetExhausted(state);
294
353
  return 'budget_exhausted';
@@ -301,6 +360,7 @@ export function applyFinalAppend(state, decision) {
301
360
  id: `P${index}`,
302
361
  goal: item.goal,
303
362
  ...(item.capabilities ? { capabilities: item.capabilities } : {}),
363
+ ...(item.execution_mode === 'MOA' ? { execution_mode: 'MOA' } : {}),
304
364
  status: 'pending',
305
365
  });
306
366
  }
@@ -318,10 +378,14 @@ export function applyCorrectionStep(state, step, input) {
318
378
  const correctionCapabilities = input.capabilities === undefined
319
379
  ? step.capabilities
320
380
  : normalizeCapabilities(input.capabilities);
381
+ const correctionMode = input.executionMode === undefined
382
+ ? normalizeExecutionMode(step.execution_mode)
383
+ : normalizeExecutionMode(input.executionMode);
321
384
  state.plan.steps.splice(insertAt, 0, {
322
385
  id: `${base}-${number}`,
323
386
  goal: input.nextGoal,
324
387
  ...(correctionCapabilities ? { capabilities: correctionCapabilities } : {}),
388
+ ...(correctionMode === 'MOA' ? { execution_mode: 'MOA' } : {}),
325
389
  status: 'pending',
326
390
  });
327
391
  state.phase = 'EXECUTE';