dsh-loop-engine 0.1.5-rc4 → 0.1.7-rc1

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/index.js CHANGED
@@ -395,21 +395,30 @@ function renderAssistantBlocks(blocks) {
395
395
  }
396
396
  return sections.join("\n\n");
397
397
  }
398
- function renderToolResult(message) {
399
- const block = message.content[0];
400
- const body = block.content.map((child) => {
401
- switch (child.type) {
398
+ function renderTextBlocks(blocks) {
399
+ return blocks.map((block) => {
400
+ switch (block.type) {
402
401
  case "text":
403
- return child.text;
402
+ return block.text;
404
403
  case "image":
405
404
  return OMITTED_IMAGE_TEXT;
406
405
  default:
407
406
  return "";
408
407
  }
409
408
  }).filter((section) => section !== "").join("\n\n");
410
- const tag = block.isError === true ? "tool-result-error" : "tool-result";
409
+ }
410
+ function renderToolResult(blocks, isError) {
411
+ const body = renderTextBlocks(blocks);
412
+ const tag = isError === true ? "tool-result-error" : "tool-result";
411
413
  return frame(tag, body || "(no content)");
412
414
  }
415
+ function renderModernToolResult(message) {
416
+ return renderToolResult(message.content, message.isError);
417
+ }
418
+ function renderLegacyToolResult(message) {
419
+ const block = message.content[0];
420
+ return renderToolResult(block?.content ?? [], block?.isError);
421
+ }
413
422
  var ENGINE_SLASH_LINE = /^\/[^\s/]+(?:[ \t]+.*)?$/;
414
423
  function engineSlashPrompt(messages) {
415
424
  const last = messages.at(-1);
@@ -433,24 +442,18 @@ function serializeHistory(messages) {
433
442
  break;
434
443
  }
435
444
  case "user": {
436
- const user = message;
437
- if (user.source.kind === "tool") {
438
- sections.push(renderToolResult(user));
445
+ if (message.source.kind === "tool") {
446
+ sections.push(renderLegacyToolResult(message));
439
447
  } else {
440
- const body = user.content.map((block) => {
441
- switch (block.type) {
442
- case "text":
443
- return block.text;
444
- case "image":
445
- return OMITTED_IMAGE_TEXT;
446
- default:
447
- return "";
448
- }
449
- }).filter((section) => section !== "").join("\n\n");
448
+ const body = renderTextBlocks(message.content);
450
449
  sections.push(frame("user", body || "(no content)"));
451
450
  }
452
451
  break;
453
452
  }
453
+ case "tool": {
454
+ sections.push(renderModernToolResult(message));
455
+ break;
456
+ }
454
457
  default:
455
458
  break;
456
459
  }
@@ -624,6 +627,29 @@ var DriverInbox = class {
624
627
  }
625
628
  };
626
629
 
630
+ // src/driver-core/system-head.ts
631
+ import { createSystemMessage } from "@deepseek-ai/dsh-llm";
632
+
633
+ // src/compat.ts
634
+ import * as dshSettings from "@deepseek-ai/dsh-settings";
635
+ var LEGACY_HARNESS = "SettingsProvider" in dshSettings;
636
+
637
+ // src/driver-core/system-head.ts
638
+ var SURFACE_TYPES = /* @__PURE__ */ new Set([
639
+ "system/message",
640
+ "user/message",
641
+ "developer/message",
642
+ "assistant/message",
643
+ "tool/result"
644
+ ]);
645
+ function appendSystemHeadIfMissing(session, turn, step) {
646
+ if (LEGACY_HARNESS) return;
647
+ for (const event of session.snapshotEvents()) {
648
+ if (SURFACE_TYPES.has(event.type) && event.surfaceOp !== void 0) return;
649
+ }
650
+ session.append("system/message", { turn, step, message: createSystemMessage("") }, { surfaceOp: "append" });
651
+ }
652
+
627
653
  // src/provider-route.ts
628
654
  import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
629
655
  function hostedRouteLabelOf(_engine) {
@@ -685,6 +711,9 @@ function sessionModelOverrideOf(ctx, session) {
685
711
  }
686
712
 
687
713
  // src/driver-core/model-handover.ts
714
+ var SHIPPED_ROUTE_APIS = {
715
+ "deepseek-official": "openai-completions"
716
+ };
688
717
  var warned = /* @__PURE__ */ new WeakMap();
689
718
  function warnOnce(ctx, key, message) {
690
719
  let seen = warned.get(ctx);
@@ -711,10 +740,9 @@ function readProviderProfile(ctx, address) {
711
740
  const record = node;
712
741
  const { baseURL, api, apiKeyEnv } = record;
713
742
  if (typeof baseURL !== "string" || baseURL.length === 0) return void 0;
714
- if (typeof api !== "string" || api.length === 0) return void 0;
715
743
  return {
716
744
  baseURL,
717
- api,
745
+ api: typeof api === "string" && api.length > 0 ? api : void 0,
718
746
  apiKeyEnv: typeof apiKeyEnv === "string" && apiKeyEnv.length > 0 ? apiKeyEnv : void 0
719
747
  };
720
748
  }
@@ -734,7 +762,7 @@ async function resolveModelHandover(ctx, override) {
734
762
  }
735
763
  const profile = readProviderProfile(ctx, address);
736
764
  if (profile === void 0) {
737
- return refuse(ctx, override, `the "${address.settingsNs}" settings section names no baseURL and wire protocol for it`);
765
+ return refuse(ctx, override, `the "${address.settingsNs}" settings section names no baseURL for it`);
738
766
  }
739
767
  const apiKey = await readApiKey(ctx, profile.apiKeyEnv);
740
768
  if (apiKey === void 0) {
@@ -744,7 +772,7 @@ async function resolveModelHandover(ctx, override) {
744
772
  provider: override.provider,
745
773
  model: override.model,
746
774
  baseURL: profile.baseURL,
747
- api: profile.api,
775
+ api: profile.api ?? SHIPPED_ROUTE_APIS[override.provider],
748
776
  apiKey
749
777
  };
750
778
  }
@@ -1506,6 +1534,7 @@ var ClaudeCodeAgent = class {
1506
1534
  this.session.append("step/start", { turn, step });
1507
1535
  phase.step = step;
1508
1536
  try {
1537
+ appendSystemHeadIfMissing(this.session, turn, step);
1509
1538
  for (const message of decision.messages) {
1510
1539
  this.session.append("user/message", message, { surfaceOp: "append" });
1511
1540
  }
@@ -2035,16 +2064,21 @@ var HostedEngineRuntime = class {
2035
2064
  return {
2036
2065
  agent,
2037
2066
  signal: abort.signal,
2038
- publish: (source) => {
2067
+ publish: async (source) => {
2039
2068
  assertLive();
2040
2069
  const joining = lifetime.entered;
2041
2070
  if (!joining) lifetime.bind(agent.ctx.sessions.enter(session));
2042
2071
  detachAgent = loopCtx.agents.enter(agent, parentAgent);
2043
2072
  if (!joining) agent.ctx.sessions.announce(session);
2044
2073
  assertLive();
2045
- loopCtx.agents.announce(agent);
2046
- assertLive();
2047
- emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
2074
+ if (LEGACY_HARNESS) {
2075
+ ;
2076
+ loopCtx.agents.announce(agent);
2077
+ assertLive();
2078
+ emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
2079
+ } else {
2080
+ await loopCtx.agents.announce(agent, source, abort.signal);
2081
+ }
2048
2082
  assertLive();
2049
2083
  return { agent, dispose, retire, lifetime };
2050
2084
  },
@@ -2333,15 +2367,15 @@ import { canonicalHeader as canonicalHeader2 } from "@deepseek-ai/dsh-session";
2333
2367
  var CODEX_DSH_PROVIDER = "dsh";
2334
2368
  var CODEX_DSH_API_KEY_ENV = "DSH_LOOP_ENGINE_API_KEY";
2335
2369
  var CODEX_WIRE_APIS = {
2336
- "openai-responses": "responses",
2337
- "openai-completions": "chat"
2370
+ "openai-responses": "responses"
2338
2371
  };
2339
2372
  function tomlString(value) {
2340
2373
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
2341
2374
  }
2342
2375
  function codexModelConfig(handover) {
2343
- const wire = CODEX_WIRE_APIS[handover.api];
2376
+ const wire = handover.api === void 0 ? void 0 : CODEX_WIRE_APIS[handover.api];
2344
2377
  const profile = [
2378
+ `name=${tomlString(CODEX_DSH_PROVIDER)}`,
2345
2379
  `base_url=${tomlString(handover.baseURL)}`,
2346
2380
  ...wire === void 0 ? [] : [`wire_api=${tomlString(wire)}`],
2347
2381
  `env_key=${tomlString(CODEX_DSH_API_KEY_ENV)}`
@@ -2911,6 +2945,7 @@ var CodexAgent = class {
2911
2945
  this.session.append("step/end", { turn: phase.turn, step: phase.step });
2912
2946
  phase.step += 1;
2913
2947
  this.session.append("step/start", { turn: phase.turn, step: phase.step });
2948
+ appendSystemHeadIfMissing(this.session, phase.turn, phase.step);
2914
2949
  this.stepSettledTools = 0;
2915
2950
  }
2916
2951
  /** Lazily created app-server client, reused across steps and released on scope teardown. */
@@ -3962,6 +3997,7 @@ var PiAgent = class {
3962
3997
  this.session.append("step/end", { turn: phase.turn, step: phase.step });
3963
3998
  phase.step += 1;
3964
3999
  this.session.append("step/start", { turn: phase.turn, step: phase.step });
4000
+ appendSystemHeadIfMissing(this.session, phase.turn, phase.step);
3965
4001
  this.stepSettledTools = 0;
3966
4002
  }
3967
4003
  /**
@@ -4325,10 +4361,11 @@ var PiAgent = class {
4325
4361
  }
4326
4362
  this.assertRequestHeader();
4327
4363
  signal.throwIfAborted();
4328
- const handover = await resolveModelHandover(
4364
+ const resolved = await resolveModelHandover(
4329
4365
  this.loopCtx,
4330
4366
  sessionModelOverrideOf(this.loopCtx, this.session)
4331
4367
  );
4368
+ const handover = resolved?.api === void 0 ? void 0 : resolved;
4332
4369
  const controller = new AbortController();
4333
4370
  const cancel = () => {
4334
4371
  if (!controller.signal.aborted) {
@@ -4685,7 +4722,7 @@ var KIMI_PROVIDER_TYPES = {
4685
4722
  "openai-responses": "openai"
4686
4723
  };
4687
4724
  function kimiModelEnv(handover) {
4688
- const type = KIMI_PROVIDER_TYPES[handover.api];
4725
+ const type = handover.api === void 0 ? void 0 : KIMI_PROVIDER_TYPES[handover.api];
4689
4726
  return {
4690
4727
  KIMI_MODEL_NAME: handover.model,
4691
4728
  KIMI_MODEL_API_KEY: handover.apiKey,
@@ -5296,6 +5333,7 @@ var KimiAgent = class {
5296
5333
  this.session.append("step/start", { turn, step });
5297
5334
  phase.step = step;
5298
5335
  try {
5336
+ appendSystemHeadIfMissing(this.session, turn, step);
5299
5337
  for (const message of decision.messages) {
5300
5338
  this.session.append("user/message", message, { surfaceOp: "append" });
5301
5339
  }
@@ -5690,7 +5728,7 @@ var KimiLoop = class extends HostedEngineRuntime {
5690
5728
  import z5 from "@deepseek-ai/schemastery";
5691
5729
 
5692
5730
  // src/namespace.ts
5693
- var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
5731
+ var LEGACY_LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
5694
5732
 
5695
5733
  // src/settings.ts
5696
5734
  var LOOP_ENGINE_SETTINGS_SCHEMA = z5.object({
@@ -5698,8 +5736,16 @@ var LOOP_ENGINE_SETTINGS_SCHEMA = z5.object({
5698
5736
  showInComposer: z5.boolean().default(true)
5699
5737
  });
5700
5738
  function loopEngineSettingsNamespace() {
5701
- return LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL;
5739
+ return LEGACY_LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL;
5740
+ }
5741
+ function withVolatile(schema) {
5742
+ const volatile = schema.volatile;
5743
+ return volatile === void 0 ? schema : volatile.call(schema);
5702
5744
  }
5745
+ var LOOP_ENGINE_ENGINE_SCHEMA = withVolatile(
5746
+ z5.union(LOOP_ENGINE_IDS.map((id) => z5.const(id))).default("in-process")
5747
+ );
5748
+ var LOOP_ENGINE_SHOW_IN_COMPOSER_SCHEMA = withVolatile(z5.boolean().default(true));
5703
5749
 
5704
5750
  // src/patch-manager.ts
5705
5751
  var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block --";
@@ -5838,8 +5884,13 @@ async function writeIfDifferent(path, text) {
5838
5884
  await rename(tmp, path);
5839
5885
  return true;
5840
5886
  }
5887
+ async function readComposition(source, id) {
5888
+ if (source.readDocument !== void 0) return (await source.readDocument(id)).content;
5889
+ if (source.read !== void 0) return source.read(id);
5890
+ throw new Error("loop-engine: the preset roster exposes neither readDocument() nor read()");
5891
+ }
5841
5892
  async function ensureEnginePresets(dshHome, source) {
5842
- const composition = await source.read(SOURCE_PRESET_ID);
5893
+ const composition = await readComposition(source, SOURCE_PRESET_ID);
5843
5894
  const stripped = `${MANAGED_HEADER}
5844
5895
  ${stripPresetRows(composition)}`;
5845
5896
  let changed = false;
@@ -5853,7 +5904,7 @@ ${stripPresetRows(composition)}`;
5853
5904
  }
5854
5905
 
5855
5906
  // src/router-loop.ts
5856
- import AgentLoop from "@deepseek-ai/dsh-agent-loop";
5907
+ import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from "@deepseek-ai/dsh-agent-loop";
5857
5908
 
5858
5909
  // src/model-selection-reset.ts
5859
5910
  var AGENT_DEFAULT_MODEL_NS = "agent-default-model";
@@ -6754,7 +6805,10 @@ var RouterLoop = class extends AgentLoop {
6754
6805
  * @param warn - diagnostic sink for a skipped engine command.
6755
6806
  */
6756
6807
  constructor(ctx, build, records, warn) {
6757
- super(ctx, { agents: [] });
6808
+ super(ctx, LEGACY_HARNESS ? { agents: [] } : {
6809
+ agents: [],
6810
+ maxParallelToolCalls: { get: () => DEFAULT_MAX_PARALLEL_TOOL_CALLS }
6811
+ });
6758
6812
  this.build = build;
6759
6813
  this.records = records;
6760
6814
  this.warn = warn;
@@ -7338,7 +7392,13 @@ var Config5 = z6.object({
7338
7392
  approvalPolicy: z6.union(CODEX_APPROVAL_POLICIES.map((policy) => z6.const(policy))),
7339
7393
  piProvider: z6.string(),
7340
7394
  piThinking: z6.string(),
7341
- kimiBin: z6.string()
7395
+ kimiBin: z6.string(),
7396
+ // The live fields exist only on the 0.1.7 line; the 0.1.5 line carries the
7397
+ // selection in a settings section instead. The empty arm never runs when the
7398
+ // coverage job is on 0.1.7; the 0.1.5 dep set is exercised by
7399
+ // vitest.config.compat015.ts, which takes it.
7400
+ /* v8 ignore next -- legacy arm, exercised by vitest.config.compat015.ts */
7401
+ ...LEGACY_HARNESS ? {} : { engine: LOOP_ENGINE_ENGINE_SCHEMA, showInComposer: LOOP_ENGINE_SHOW_IN_COMPOSER_SCHEMA }
7342
7402
  });
7343
7403
  function resolvePatchPath(config) {
7344
7404
  if (config.patchPath !== void 0 && config.patchPath !== "") return config.patchPath;
@@ -7538,9 +7598,7 @@ function apply(ctx, config) {
7538
7598
  });
7539
7599
  };
7540
7600
  let savedPresetDefault;
7541
- let steeredEngine;
7542
7601
  const steerPresetDefault = (engine) => {
7543
- steeredEngine = engine;
7544
7602
  if (engine === "in-process") {
7545
7603
  const saved = savedPresetDefault;
7546
7604
  savedPresetDefault = void 0;
@@ -7559,7 +7617,6 @@ function apply(ctx, config) {
7559
7617
  mutatePresetDefault({ op: "set", path: ["default"], value: target });
7560
7618
  });
7561
7619
  };
7562
- const seedEngine = legacyEngine ?? "in-process";
7563
7620
  const pluginWarn = (message) => {
7564
7621
  ctx.logger.warn(message);
7565
7622
  };
@@ -7620,25 +7677,47 @@ function apply(ctx, config) {
7620
7677
  CLEAR_ROUTER_RETRY();
7621
7678
  releaseRoutes();
7622
7679
  }, "loop-engine: cleanup");
7623
- let source;
7624
- ctx.inject(["settings"], (settingsCtx) => {
7625
- settingsCtx.settings.installSection(
7626
- ctx,
7627
- loopEngineSettingsNamespace(),
7628
- LOOP_ENGINE_SETTINGS_SCHEMA,
7629
- { engine: seedEngine, showInComposer: true },
7630
- {
7631
- setSource: (current) => {
7632
- source = current;
7633
- },
7634
- onChange: () => {
7635
- const next = source().engine;
7636
- if (next === steeredEngine) return;
7637
- steerPresetDefault(next);
7680
+ if (LEGACY_HARNESS) {
7681
+ let source;
7682
+ let steered;
7683
+ ctx.inject(["settings"], (settingsCtx) => {
7684
+ const settings = settingsCtx.settings;
7685
+ settings.installSection(
7686
+ ctx,
7687
+ loopEngineSettingsNamespace(),
7688
+ LOOP_ENGINE_SETTINGS_SCHEMA,
7689
+ // The managed block's engine survives the upgrade as the section's seed;
7690
+ // a deployment with no block keeps the in-process default.
7691
+ { engine: legacyEngine ?? "in-process", showInComposer: true },
7692
+ {
7693
+ setSource: (current) => {
7694
+ source = current;
7695
+ },
7696
+ onChange: () => {
7697
+ const next = source().engine;
7698
+ if (next === steered) return;
7699
+ steered = next;
7700
+ steerPresetDefault(next);
7701
+ }
7638
7702
  }
7639
- }
7703
+ );
7704
+ });
7705
+ return;
7706
+ }
7707
+ ctx.inject(["settings"], (settingsCtx) => {
7708
+ settingsCtx.effect(
7709
+ () => settingsCtx.settings.configure({ auto: false }, ctx.fiber),
7710
+ "loop-engine: settings page policy"
7640
7711
  );
7641
7712
  });
7713
+ let observedEngine = config.engine.get();
7714
+ ctx.on("settings/document-updated", () => {
7715
+ const next = config.engine.get();
7716
+ if (next === observedEngine) return;
7717
+ observedEngine = next;
7718
+ steerPresetDefault(next);
7719
+ });
7720
+ steerPresetDefault(legacyEngine ?? observedEngine);
7642
7721
  }
7643
7722
  export {
7644
7723
  Config5 as Config,
package/lib/invariant.js CHANGED
@@ -12,6 +12,14 @@ var LOOP_ENGINE_SETTINGS_SCHEMA = z.object({
12
12
  engine: z.union(LOOP_ENGINE_IDS.map((id) => z.const(id))).default("in-process"),
13
13
  showInComposer: z.boolean().default(true)
14
14
  });
15
+ function withVolatile(schema) {
16
+ const volatile = schema.volatile;
17
+ return volatile === void 0 ? schema : volatile.call(schema);
18
+ }
19
+ var LOOP_ENGINE_ENGINE_SCHEMA = withVolatile(
20
+ z.union(LOOP_ENGINE_IDS.map((id) => z.const(id))).default("in-process")
21
+ );
22
+ var LOOP_ENGINE_SHOW_IN_COMPOSER_SCHEMA = withVolatile(z.boolean().default(true));
15
23
 
16
24
  // src/patch-manager.ts
17
25
  var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block --";
@@ -1,7 +1,12 @@
1
1
  /**
2
- * Loop engine settings plugin, browser half. Registers the "Loop engine"
3
- * page under the settings section slot once the settings shell declares it,
4
- * binding one store to the duplicated `agent-loop-engine` settings scope.
2
+ * Loop engine settings plugin, browser half. Registers the "Loop engine" page
3
+ * under the settings section slot and binds one store to whichever settings
4
+ * client the running harness serves: the 0.1.7 `configForms` form for the
5
+ * plugin's own `loop-engine` profile entry, or the 0.1.5 `settingsScope` for its
6
+ * `agent-loop-engine` settings section. Two inject callbacks register the page,
7
+ * one per generation — whichever service exists fires, and the client bundle
8
+ * never imports a host package, so it cannot probe the generation like the node
9
+ * half does.
5
10
  * Export discipline: packages/client/AGENTS.md.
6
11
  * @module dsh-loop-engine/client
7
12
  */
@@ -17,12 +22,17 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
17
22
  'settings.loop-engine': LoopEngineKey;
18
23
  }
19
24
  }
20
- /** Required services (cordis fiber inject). The target slot is declared by
21
- * ui-settings' apply; registration depends on it through `slots.inject()`. */
25
+ /**
26
+ * Required services (cordis fiber inject). Only the services BOTH generations
27
+ * provide are listed statically: `configForms` (0.1.7) and `settingsScope`
28
+ * (0.1.5) are mutually exclusive, so each is injected inside its own callback
29
+ * rather than named here. The target slot is declared by ui-settings' apply;
30
+ * registration depends on it through `slots.inject()`.
31
+ */
22
32
  export declare const inject: string[];
23
33
  /**
24
34
  * Register the Loop engine section once the `settings.section` declaration is
25
- * on the ledger and bind its store to the duplicated settings scope.
35
+ * on the ledger and bind its store to whichever settings client is present.
26
36
  * @param ctx - client root context.
27
37
  */
28
38
  export declare function apply(ctx: ClientContext): void;
@@ -82,8 +82,9 @@ export type { SessionEngineReport } from '../agent-preset-ids.ts';
82
82
  /**
83
83
  * The standard seat members this half reads off a session-scope slot.
84
84
  *
85
- * Only the identity: the engine itself is no longer read from a framework hook,
86
- * it is asked of the host through this plugin's own Remote.
85
+ * The identity, plus the one live fact the turn-status row needs: whether the
86
+ * session is mid-turn. The engine itself is no longer read from a framework
87
+ * hook, it is asked of the host through this plugin's own Remote.
87
88
  */
88
89
  export interface SessionSeat {
89
90
  /**
@@ -92,6 +93,23 @@ export interface SessionSeat {
92
93
  * (which only ever renders with a session) shows nothing.
93
94
  */
94
95
  sessionId?: string;
96
+ /**
97
+ * Selector hook over the current Session's snapshot (`SessionStandardProps` of
98
+ * every session-scoped seat). Optional for the same reason as the identity: a
99
+ * caller with no such seat — or a test rendering one directly — has none, and
100
+ * the turn-status gate is then left alone rather than guessed.
101
+ */
102
+ useSession?: SnapshotSelector;
103
+ }
104
+ /**
105
+ * The selector-hook shape this half needs from `useSession`: a read of one field
106
+ * of the Session snapshot, with no equality function of its own.
107
+ */
108
+ export type SnapshotSelector = <S>(select: (snapshot: SessionRunningState) => S) => S;
109
+ /** The one field of the Session snapshot the turn-status row reads. */
110
+ export interface SessionRunningState {
111
+ /** Whether a turn is in flight for this session. */
112
+ readonly running: boolean;
95
113
  }
96
114
  /** Cordis service key AND wire namespace of the plugin's own Remote. */
97
115
  export declare const LOOP_ENGINE_REMOTE_NAMESPACE = "loopEngine";
@@ -239,14 +257,30 @@ export declare function engineSwitchReady(report: SessionEngineReport | undefine
239
257
  * this page.
240
258
  */
241
259
  export declare function switchNeedsReload(current: SessionEngine, target: LoopEngineId): boolean;
242
- /** A codec of one wire field (harness: `TypertCodec`, strict branch). */
260
+ /** One boundary schema of a wire field (a zod schema, as the Gateway holds it). */
261
+ interface RemoteSchema {
262
+ parse(value: unknown): unknown;
263
+ }
264
+ /**
265
+ * A codec of one wire field (harness: `TypertCodec`, strict branch).
266
+ *
267
+ * The two harness generations read DIFFERENT members of this object and each
268
+ * validates only its own: the 0.1.5 line's registry and client parse through
269
+ * `schema`, while the 0.1.7 line's registry requires a `create()` factory and
270
+ * parses through `codec.create()` (`typert: … strict codec has no create()
271
+ * factory`). One build serves both, so the codec carries both fields.
272
+ */
243
273
  interface RemoteCodec {
244
274
  readonly mode: 'strict';
245
275
  /** Stable identity of the field's declared type, for diagnostics. */
246
276
  readonly typeSymbol: string;
247
- readonly schema: {
248
- parse(value: unknown): unknown;
249
- };
277
+ /** The 0.1.5 line's read: the boundary schema itself. */
278
+ readonly schema: RemoteSchema;
279
+ /**
280
+ * The 0.1.7 line's read: a factory returning the boundary schema.
281
+ * @returns the same boundary schema.
282
+ */
283
+ readonly create: () => RemoteSchema;
250
284
  }
251
285
  /** One ordered business parameter (harness: `InvocationParameterDescriptor`). */
252
286
  interface RemoteParameter {
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Loop engine selection store: the durable settings scope is the transport,
3
- * and the store publishes a render-safe snapshot plus the write path.
2
+ * Loop engine selection store: a settings transport is the channel, and the
3
+ * store publishes a render-safe snapshot plus the write path. The transport is
4
+ * abstracted so one store follows either generation's client service — the
5
+ * 0.1.7 `ConfigForm` for the plugin's own profile entry, or the 0.1.5
6
+ * `SettingsScope` for its `agent-loop-engine` settings section.
4
7
  * @module dsh-loop-engine/client/store
5
8
  */
6
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
7
9
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
8
10
  import type { LoopEngineId } from '../agent-preset-ids.ts';
11
+ import type { LoopEngineSettings } from '../settings.ts';
9
12
  /** State rendered by the loop engine section. */
10
13
  export interface LoopEngineState {
11
14
  status: 'loading' | 'ready' | 'unavailable' | 'saving';
@@ -14,44 +17,76 @@ export interface LoopEngineState {
14
17
  writable: boolean;
15
18
  error: string | null;
16
19
  }
17
- /** Narrow a wire section to the stored engine id and display toggle; an invalid one reads default. */
20
+ /** The render-relevant slice of a settings snapshot, shared by both generations' transports. */
21
+ export interface LoopEngineSettingsSnapshot {
22
+ status: 'loading' | 'ready' | 'unavailable';
23
+ value: LoopEngineSettings | undefined;
24
+ writable: boolean;
25
+ }
26
+ /**
27
+ * The settings channel the store follows. Satisfied by the 0.1.7 `ConfigForm`
28
+ * and by the 0.1.5 `SettingsScope` (through a thin adapter in `./index.ts`); the
29
+ * write's boolean is optional because the 0.1.5 scope reports acceptance only
30
+ * through the snapshot it leaves behind.
31
+ */
32
+ export interface LoopEngineSettingsTransport {
33
+ /** @returns the current sync snapshot. */
34
+ getSnapshot(): LoopEngineSettingsSnapshot;
35
+ /**
36
+ * Observe snapshot replacements.
37
+ * @param listener - invoked after each snapshot change.
38
+ * @returns the disposer removing the listener.
39
+ */
40
+ subscribe(listener: () => void): () => void;
41
+ /**
42
+ * Persist one field.
43
+ * @param field - the field to write.
44
+ * @param value - the value to write.
45
+ * @returns whether the write was accepted, when the transport reports it.
46
+ */
47
+ set(field: 'engine' | 'showInComposer', value: unknown): Promise<boolean | void>;
48
+ }
49
+ /**
50
+ * Narrow a wire section to the stored engine id and display toggle; an invalid
51
+ * one reads default. Used by the 0.1.5 `settingsScope.bind` decoder, whose scope
52
+ * receives raw wire sections.
53
+ * @param section - the wire section value.
54
+ * @returns the decoded selection, or `undefined` when the section is unusable.
55
+ */
18
56
  export declare function decodeLoopEngine(section: unknown): {
19
57
  engine: LoopEngineId;
20
58
  showInComposer: boolean;
21
59
  } | undefined;
22
60
  /** Coordinates the settings-backed loop engine selection. */
23
61
  export declare class LoopEngineStore {
24
- private readonly scope;
62
+ private readonly transport;
25
63
  /** uSES-safe state source shared by the registered settings section. */
26
64
  readonly store: SnapshotStore<LoopEngineState>;
27
65
  private following;
28
66
  private saving;
29
67
  /**
30
- * @param scope - the loop engine settings namespace scope.
68
+ * @param transport - the settings channel the store follows.
31
69
  */
32
- constructor(scope: SettingsScope<{
33
- engine: LoopEngineId;
34
- showInComposer: boolean;
35
- }>);
36
- /** Begin following the bound scope and publish its current answer. */
70
+ constructor(transport: LoopEngineSettingsTransport);
71
+ /** Begin following the transport and publish its current answer. */
37
72
  load(): void;
38
73
  /**
39
- * Persist the selected engine. Success is judged against the snapshot the
40
- * write left behind, so a refused write reports error after its recovery.
74
+ * Persist the selected engine. Success is the transport's accepted answer
75
+ * (when it reports one) checked against the snapshot the write left behind,
76
+ * so a refused write reports error after its recovery.
41
77
  * @param engine - the engine to select for future Agent turns.
42
78
  * @returns whether the write landed.
43
79
  */
44
80
  setEngine(engine: LoopEngineId): Promise<boolean>;
45
81
  /**
46
- * Persist whether the composer shows the engine picker. Success is judged
47
- * against the snapshot the write left behind, so a refused write reports
48
- * error after its recovery. Unlike {@link setEngine}, landing does not reload
49
- * the page — the toggle only changes composer visibility.
82
+ * Persist whether the composer shows the engine picker. Unlike
83
+ * {@link setEngine}, landing does not reload the page — the toggle only
84
+ * changes composer visibility.
50
85
  * @param show - whether the chat page composer reveals the engine picker.
51
86
  * @returns whether the write landed.
52
87
  */
53
88
  setShowInComposer(show: boolean): Promise<boolean>;
54
- /** Stop following the scope. */
89
+ /** Stop following the transport. */
55
90
  dispose(): void;
56
91
  private derive;
57
92
  }