@kasenri/dsh-orbit 0.5.5 → 0.5.6

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/activation.js CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Deterministic Orbit activation.
3
3
  *
4
- * - `/agent-orbit` (host command or genuine-message gesture) keeps its explicit
5
- * directive; the existing `orbit_controller` tool + `OrbitService` remain the
6
- * runtime.
7
- * - A Session whose toggle is ON hands every ordinary user message straight to
8
- * `OrbitService`: the pre-step boundary consumes the turn (no parent model
9
- * call, no parent mutation) and the host starts or resumes the run. The model
10
- * never decides whether Orbit runs.
4
+ * Both entry points hand control to the host runtime directly:
5
+ * - an explicit `/agent-orbit <goal>` statement (host command or genuine
6
+ * message), whatever the Session toggle says, and
7
+ * - every ordinary user message in a Session whose toggle is ON.
8
+ *
9
+ * The pre-step boundary consumes the turn (no parent model call, no parent
10
+ * mutation) and starts or resumes the existing `OrbitService`; the same
11
+ * `orbit_controller` tool + Supervisor remain the only runtime.
11
12
  */
12
13
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
13
14
  import { ORBIT_TOGGLE_COMMAND, parseOrbitToggle } from "./session-state.js";
@@ -53,28 +54,13 @@ export function invokedOrbitMessage(messages) {
53
54
  }
54
55
  return undefined;
55
56
  }
56
- /**
57
- * The one deterministic directive injected for an explicit activation. It only
58
- * states the activation and carries the goal verbatim; every Orbit rule still
59
- * comes from the existing tool, service and protocol.
60
- */
61
- export function buildOrbitActivationDirective(goal) {
62
- const lines = [
63
- 'Orbit activation is explicit for this turn. Start or resume Orbit through the existing orbit_controller tool; do not ask the user to confirm the mode.',
64
- 'Treat the text following /agent-orbit as the requested goal, without summarizing or rewriting it.',
65
- ];
66
- lines.push(goal === ''
67
- ? 'No goal was provided — ask the user what Orbit should accomplish; do not start an empty run.'
68
- : `Goal: ${goal}`);
69
- return lines.join('\n');
70
- }
71
57
  /**
72
58
  * Register the closed-namespace `/agent-orbit` host command.
73
59
  *
74
- * The handler never injects a directive itself. It reposts the original
75
- * command line as a genuine user message, so the gesture boundary stays the
76
- * only activation point and a command-registered surface cannot double
77
- * activate through both routes.
60
+ * The handler never runs Orbit itself. It reposts the original command line as
61
+ * a genuine user message, so the gesture boundary stays the only activation
62
+ * point and a command-registered surface cannot double activate through both
63
+ * routes.
78
64
  */
79
65
  export function registerOrbitCommand(ctx) {
80
66
  ctx.effect(() => ctx.commands.register({
@@ -122,47 +108,33 @@ export function registerOrbitToggleCommand(ctx) {
122
108
  }), 'dsh-orbit: /orbit-toggle host command');
123
109
  }
124
110
  /**
125
- * Install the gesture boundary. It runs for every proposed step.
111
+ * Install the activation boundary. It runs for every proposed step.
126
112
  *
127
- * An explicit `/agent-orbit` always wins and always activates, whatever the
128
- * Session toggle says: it keeps the existing activation directive and the
129
- * `orbit_controller` tool. Otherwise, an enabled Session hands the ordinary
130
- * user message to Orbit directly — the step is consumed with no model call and
131
- * the message stays in the conversation as durable history.
113
+ * An explicit `/agent-orbit <goal>` always wins and always activates, whatever
114
+ * the Session toggle says. Otherwise, an enabled Session hands the ordinary
115
+ * user message to Orbit. Either way the step is consumed with no parent model
116
+ * call while the message stays in the conversation as durable history.
132
117
  */
133
118
  export function installOrbitGestureBoundary(ctx, options = {}) {
134
119
  ctx.on('agent/pre-step', async ({ agent, messages, signal }, next) => {
135
120
  const decision = await next();
136
121
  if (decision.kind === 'reject')
137
122
  return decision;
138
- if (decision.messages.some((message) => message.source.kind === 'orbit-command'))
139
- return decision;
140
123
  const explicit = invokedOrbitActivation(messages);
141
- if (explicit !== undefined) {
142
- signal.throwIfAborted();
143
- return {
144
- kind: 'enter',
145
- messages: [
146
- ...decision.messages,
147
- createUserMessage({
148
- content: [{ type: 'text', text: buildOrbitActivationDirective(explicit.goal) }],
149
- source: { kind: 'orbit-command', ...(explicit.goal === '' ? {} : { goal: explicit.goal }) },
150
- }),
151
- ],
152
- };
153
- }
154
- if (options.sessionEnabled?.(agent.session) !== true)
155
- return decision;
156
- const ordinary = invokedOrbitMessage(messages);
124
+ const goal = explicit !== undefined
125
+ ? explicit.goal
126
+ : options.sessionEnabled?.(agent.session) === true
127
+ ? invokedOrbitMessage(messages)?.goal
128
+ : undefined;
157
129
  const activate = options.activate;
158
- if (ordinary === undefined || activate === undefined)
130
+ if (goal === undefined || goal === '' || activate === undefined)
159
131
  return decision;
160
132
  signal.throwIfAborted();
161
133
  // The claimed batch enters the conversation exactly once: the loop commits
162
134
  // `decision.messages`, which this path leaves empty.
163
135
  for (const message of messages)
164
136
  agent.session.append('user/message', message, { surfaceOp: 'append' });
165
- await activate(agent, ordinary.goal, signal);
137
+ await activate(agent, goal, signal);
166
138
  return { kind: 'enter', messages: [] };
167
139
  });
168
140
  }
package/lib/client.js CHANGED
@@ -132,22 +132,22 @@ window.__ModuleLoader__.load({
132
132
  document.head.appendChild(tag);
133
133
  }
134
134
  var OrbitModelSelect_module_css_default = {
135
- "check": "f77nca_check",
136
- "switchControl": "f77nca_switchControl",
137
- "switchRow": "f77nca_switchRow",
138
135
  "rowDetail": "f77nca_rowDetail",
136
+ "switchControl": "f77nca_switchControl",
137
+ "rowValue": "f77nca_rowValue",
139
138
  "title": "f77nca_title",
140
- "panel": "f77nca_panel",
139
+ "chevron": "f77nca_chevron",
140
+ "switchRow": "f77nca_switchRow",
141
+ "group": "f77nca_group",
141
142
  "row": "f77nca_row",
143
+ "back": "f77nca_back",
144
+ "check": "f77nca_check",
142
145
  "rowLabel": "f77nca_rowLabel",
143
- "group": "f77nca_group",
144
- "error": "f77nca_error",
145
- "rowValueActive": "f77nca_rowValueActive",
146
146
  "separator": "f77nca_separator",
147
- "chevron": "f77nca_chevron",
148
- "rowValue": "f77nca_rowValue",
149
- "back": "f77nca_back",
150
- "hint": "f77nca_hint"
147
+ "error": "f77nca_error",
148
+ "hint": "f77nca_hint",
149
+ "panel": "f77nca_panel",
150
+ "rowValueActive": "f77nca_rowValueActive"
151
151
  };
152
152
  //#endregion
153
153
  //#region client/OrbitModelSelect.tsx
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm';
1
2
  import z from '@deepseek-ai/schemastery';
2
3
  import { installOrbitGestureBoundary, registerOrbitCommand, registerOrbitToggleCommand } from "./activation.js";
3
4
  import { estimateLoopCount } from "./kernel.js";
@@ -108,40 +109,41 @@ export function apply(ctx, config) {
108
109
  ctx.tools.register(createOrbitTool(ctx));
109
110
  ctx.tools.register(createOrbitTool(ctx, { legacy: true }));
110
111
  }
111
- // Deterministic activation surfaces: the closed-namespace `/agent-orbit`
112
- // host command (surfaces in the Web GUI slash menu through the Harness
113
- // commands client) and the genuine-user-message gesture boundary for
114
- // surfaces without command adjudication (headless CLI). Both default on.
115
- //
116
- // `commands` is registered lazily, not a required inject: every standard
117
- // profile mounts it, but a minimal composition that omits the command
118
- // registry keeps Orbit fully functional — the fiber never pends on it and
119
- // simply never gains the slash command, while the gesture boundary still
120
- // works.
121
- if (config.slashCommand) {
122
- ctx.inject(['commands'], (commandCtx) => {
112
+ // Slash-command registration is its own switch: the `/agent-orbit` command
113
+ // (surfaces in the Web GUI slash menu through the Harness commands client)
114
+ // rides `slashCommand`, while the per-chat toggle command and the host
115
+ // activation boundary below never depend on it. `commands` is registered
116
+ // lazily: a minimal composition without the command registry keeps Orbit
117
+ // fully functional — the fiber never pends on it, and the activation
118
+ // boundary (explicit `/agent-orbit` messages and enabled-Session ordinary
119
+ // messages) still runs.
120
+ ctx.inject(['commands'], (commandCtx) => {
121
+ registerOrbitToggleCommand(commandCtx);
122
+ if (config.slashCommand)
123
123
  registerOrbitCommand(commandCtx);
124
- registerOrbitToggleCommand(commandCtx);
125
- });
126
- installOrbitGestureBoundary(ctx, {
127
- sessionEnabled: (session) => orbitEnabledOf(ctx, session),
128
- activate: async (agent, goal, signal) => {
129
- const cwd = agent.session.header.cwd ?? process.cwd();
130
- try {
131
- // The initiator scope makes the run's children belong to this Agent;
132
- // the parent model is never asked to decide or to run the task.
133
- await ctx.agents.withInitiator(agent, () => service.run({
134
- goal,
135
- approved_loop_count: Math.max(estimateLoopCount(goal), HARD_ACTIVATION_MIN_LOOPS),
136
- }, cwd, signal));
137
- }
138
- catch {
139
- // Durable run state owns failure reporting; a takeover failure must
140
- // not fail the consumed parent turn.
141
- }
142
- },
143
- });
144
- }
124
+ });
125
+ installOrbitGestureBoundary(ctx, {
126
+ sessionEnabled: (session) => orbitEnabledOf(ctx, session),
127
+ activate: async (agent, goal, signal) => {
128
+ const cwd = agent.session.header.cwd ?? process.cwd();
129
+ let result;
130
+ try {
131
+ // The initiator scope makes the run's children belong to this Agent;
132
+ // the parent model is never asked to decide or to run the task.
133
+ result = await ctx.agents.withInitiator(agent, () => service.run({
134
+ goal,
135
+ approved_loop_count: Math.max(estimateLoopCount(goal), HARD_ACTIVATION_MIN_LOOPS),
136
+ }, cwd, signal));
137
+ }
138
+ catch (error) {
139
+ appendOrbitNotice(agent.session, `Orbit 启动失败:${error instanceof Error ? error.message : String(error)}`);
140
+ return;
141
+ }
142
+ const notice = activationNotice(result);
143
+ if (notice !== undefined)
144
+ appendOrbitNotice(agent.session, notice);
145
+ },
146
+ });
145
147
  // Per-Session Orbit enable state: natively durable through the Session log
146
148
  // and the projection registry. Minimal compositions without the registry
147
149
  // keep the feature inert (OFF) while `/agent-orbit` keeps working.
@@ -169,3 +171,36 @@ export function apply(ctx, config) {
169
171
  ctx.on('tools/pre-execute', (exec, next) => handler(exec, next));
170
172
  }
171
173
  }
174
+ /**
175
+ * One durable, user-visible Orbit notice. The DSH `plugin` + `form: 'notice'`
176
+ * source renders as a collapsed notice row instead of a user message, and the
177
+ * text stays part of the conversation so the next turn can read the outcome.
178
+ */
179
+ function appendOrbitNotice(session, text) {
180
+ session.append('user/message', createUserMessage({
181
+ content: [{ type: 'text', text }],
182
+ source: { kind: 'plugin', plugin: 'dsh-orbit', form: 'notice', summary: boundContextSummary(text) },
183
+ }), { surfaceOp: 'append' });
184
+ }
185
+ /**
186
+ * What the user must see when a hard-activated turn did not simply succeed:
187
+ * the waiting run's question, a refused start, or an unfinished run. The
188
+ * host stays the only mutation driver; the notice is reporting, never a
189
+ * hand-back of execution to the parent model.
190
+ */
191
+ function activationNotice(result) {
192
+ if (result.ok && result.phase !== 'NEEDS_USER')
193
+ return undefined;
194
+ const lastError = result.data?.['last_error'];
195
+ const reason = result.message ?? (typeof lastError === 'string' && lastError !== '' ? lastError : undefined);
196
+ if (result.phase === 'NEEDS_USER') {
197
+ return reason !== undefined && !reason.startsWith('COMMANDER_')
198
+ ? `Orbit 需要你的回复:${reason}`
199
+ : 'Orbit 需要你的回复:请回复当前运行需要的信息。';
200
+ }
201
+ if (result.message?.startsWith('ORBIT_ACTIVE_RUN_EXISTS') === true) {
202
+ return `Orbit 未接受这条新任务:${result.message}`;
203
+ }
204
+ const phase = result.phase ?? 'UNKNOWN';
205
+ return `Orbit 运行未完成(phase=${phase})${reason !== undefined ? `:${reason}` : ''}`;
206
+ }
package/lib/kernel.js CHANGED
@@ -151,15 +151,19 @@ export function createInitialState(input) {
151
151
  changed_files: [],
152
152
  test_summary: [],
153
153
  last_error: null,
154
+ pending_user_reply: null,
154
155
  user_hard_constraints: input.userHardConstraints ? [...input.userHardConstraints] : [],
155
156
  github_allowed: input.githubAllowed === true,
156
157
  interruption_retries: 0,
157
158
  };
158
159
  }
159
- /** A user answers a NEEDS_USER run with the same goal: execution continues. */
160
- export function resumeFromNeedsUser(state) {
160
+ /** A user answers a NEEDS_USER run: execution continues with the reply kept durable. */
161
+ export function resumeFromNeedsUser(state, userReply) {
161
162
  state.phase = 'EXECUTE';
162
163
  state.status = 'running';
164
+ const reply = (userReply ?? '').trim();
165
+ if (reply !== '')
166
+ state.pending_user_reply = reply;
163
167
  }
164
168
  /** Accept the Commander plan and move to execution. */
165
169
  export function applyPlan(state, plan) {
package/lib/supervisor.js CHANGED
@@ -6,18 +6,18 @@ import { applyCommanderNeedsUser, applyCorrectionStep, applyExecutorCapabilityUn
6
6
  import { truncateSafe } from "./sanitize.js";
7
7
  import { OrbitStateStore } from "./state-store.js";
8
8
  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";
9
- const COMMANDER_PLAN_PROMPT = (goal, constraints) => `You are the Orbit Commander. Produce the smallest set of 2-5 logical engineering steps for this goal.
9
+ const COMMANDER_PLAN_PROMPT = (goal, constraints, userReply) => `You are the Orbit Commander. Produce the smallest set of 2-5 logical engineering steps for this goal.
10
10
  Rules: ordinary engineering steps must omit capabilities. Add capability "browser" only when the step must drive a real web page, and "web-api-recon" when it must analyze captured network/API traffic. Keep it minimal.
11
11
  Submit your final plan through the structured result protocol.
12
12
  Goal: ${goal}
13
- Hard constraints: ${constraints.join('; ') || 'none'}`;
13
+ Hard constraints: ${constraints.join('; ') || 'none'}${userReply}`;
14
14
  const COMMANDER_STEP_PROMPT = (goal, step, evidence, state) => `You are the Orbit Commander doing STEP_EVALUATE. Verify the real project state; do not trust the Executor claim alone. Treat verified execution evidence as authoritative.
15
15
  Allowed decisions ONLY: PASS_CURRENT_STEP | CORRECT_CURRENT_STEP | NEEDS_USER.
16
16
  - CORRECT_CURRENT_STEP requires a concrete next step goal (optional capabilities).
17
17
  Submit your final judgment through the structured result protocol.
18
18
  Original goal: ${goal}
19
19
  Current step ${step.id}: ${step.goal}
20
- Iteration counters: loop ${state.loop.used}/${state.loop.max}
20
+ Iteration counters: loop ${state.loop.used}/${state.loop.max}${userReplyLine(state)}
21
21
  Executor claim:
22
22
  ${evidence}`;
23
23
  const COMMANDER_FINAL_PROMPT = (goal, plan, evidence, state) => `You are the Orbit Commander doing FINAL_EVALUATE. All planned steps are done. Decide whether the original goal is truly satisfied against the real project state.
@@ -27,9 +27,16 @@ Submit your final judgment through the structured result protocol.
27
27
  Original goal: ${goal}
28
28
  Plan summary: ${plan.summary}
29
29
  Steps: ${plan.steps.map((step) => `${step.id}:${step.goal}[${step.status}]`).join('; ')}
30
- Loop: ${state.loop.used}/${state.loop.max}
30
+ Loop: ${state.loop.used}/${state.loop.max}${userReplyLine(state)}
31
31
  Executor claim:
32
32
  ${evidence}`;
33
+ /**
34
+ * The durable user reply line for role prompts. The reply is the user's answer
35
+ * to a NEEDS_USER question and never replaces the original goal.
36
+ */
37
+ function userReplyLine(state) {
38
+ return state.pending_user_reply ? `\nUser reply (the user's answer to the previous question): ${state.pending_user_reply}` : '';
39
+ }
33
40
  const COMMANDER_STRATEGY_PROMPT = (goal, base, challenge, state) => `You are the Orbit Commander reconsidering strategy after a repeated correction on ${base} (STRATEGY_RECONSIDER).
34
41
  Allowed decisions ONLY: KEEP_APPROACH | REPLACE_CURRENT_STEP | NEEDS_USER.
35
42
  - REPLACE_CURRENT_STEP requires a replacement goal.
@@ -104,7 +111,13 @@ export class OrbitSupervisor {
104
111
  if (['SUCCESS', 'STOPPED', 'BUDGET_EXHAUSTED'].includes(state.phase) && requestedGoal) {
105
112
  state = this.store.writeState(this.createState(input));
106
113
  }
107
- if (!legacy && requestedGoal && state.goal && state.goal !== requestedGoal) {
114
+ if (state.phase === 'NEEDS_USER' && requestedGoal) {
115
+ // A reply to the Commander's question is not a new goal: keep the run,
116
+ // its original goal and its frozen routes, and carry the reply durably.
117
+ resumeFromNeedsUser(state, requestedGoal);
118
+ this.store.writeState(state);
119
+ }
120
+ else if (!legacy && requestedGoal && state.goal && state.goal !== requestedGoal) {
108
121
  return {
109
122
  ok: false,
110
123
  action: 'run',
@@ -112,10 +125,6 @@ export class OrbitSupervisor {
112
125
  message: 'ORBIT_ACTIVE_RUN_EXISTS: current Lite run owns this project; resume it or stop it before starting a different goal.',
113
126
  };
114
127
  }
115
- if (state.phase === 'NEEDS_USER' && requestedGoal) {
116
- resumeFromNeedsUser(state);
117
- this.store.writeState(state);
118
- }
119
128
  return this.run(state, signal);
120
129
  }
121
130
  async run(state, signal) {
@@ -194,7 +203,7 @@ export class OrbitSupervisor {
194
203
  }
195
204
  // ── commander supervised path ──────────────────────────────────────────────
196
205
  async makePlan(state, signal) {
197
- const outcome = await this.runCommander(state, 'PLAN', COMMANDER_PLAN_PROMPT(state.goal, state.user_hard_constraints), COMMANDER_PLAN_SCHEMA, signal);
206
+ const outcome = await this.runCommander(state, 'PLAN', COMMANDER_PLAN_PROMPT(state.goal, state.user_hard_constraints, userReplyLine(state)), COMMANDER_PLAN_SCHEMA, signal);
198
207
  if (outcome.kind === 'needs_user')
199
208
  return this.setNeedsUser(state, outcome.reason);
200
209
  if (outcome.kind === 'interrupted') {
@@ -448,6 +457,8 @@ export class OrbitSupervisor {
448
457
  `Working directory: ${join(this.store.stateDir, '..')}`,
449
458
  `Hard constraints: ${state.user_hard_constraints.join('; ') || 'none'}`,
450
459
  ];
460
+ if (state.pending_user_reply)
461
+ lines.push(`User reply (the user's answer to the previous question): ${state.pending_user_reply}`);
451
462
  if ((step.capabilities ?? []).length > 0)
452
463
  lines.push(`Capabilities: ${(step.capabilities ?? []).join(', ')}`);
453
464
  lines.push('Return a compact evidence summary: what changed, commands/tests run, and residual risks.');
@@ -773,6 +784,7 @@ export class OrbitSupervisor {
773
784
  strategy_challenge: state.strategy_challenge,
774
785
  guard_recovery: state.guard_recovery,
775
786
  last_error: state.last_error,
787
+ pending_user_reply: state.pending_user_reply,
776
788
  changed_files: state.changed_files,
777
789
  test_summary: state.test_summary,
778
790
  driver_ownership: state.driver_ownership,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kasenri/dsh-orbit",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "type": "module",
5
5
  "description": "Deterministic engineering orchestration for DeepSeek Harness with Commander, Executor, Smart Watchdog, bounded recovery and durable execution state.",
6
6
  "license": "MIT",