dsh-model-switch 0.4.27 → 0.4.28

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## v0.4.28
2
+
3
+ Two fixes for the same DSH `0.2.1-rc.2` line: re-enabling the plugin on 0.2.1 made **every tool call fail before its body ran**, and the Subagent routing adapter that 0.4.26 had to fail closed is adapted to the release's activation API.
4
+
5
+ - **P0: every tool call failed under the Auto preset on DSH 0.2.1 (`src/auto-review.ts`).** With the plugin enabled and the approval policy on Auto, each tool call was rejected with `Auto review of tool "…" failed; its body was not executed: cannot get property "workingDirectory" without inject` — read, grep, pwsh, even exiting plan mode. Root cause: `model-switch-auto-review` called `official.apply(scope)` **directly inside its own fiber**, so every service the official gate reads had to be mirrored in this row's `inject`; 0.2.1's gate started reading `ctx.workingDirectory`, and the mirror list had gone stale. Fix: the official gate is now **mounted as a plugin** (`autoReviewGate` → `scope.plugin(...)`), so cordis resolves the official dependency list — including `workingDirectory` on 0.2.1 and its absence on 0.2.0 — and this row declares only the services its own code reads (`llm`, `modelSwitch`). The session-correlation listener is registered only after the gate's fiber reports that its `apply` actually ran, so it still lands ahead of the gate's own prepended listener; an already-active official row still fails loudly (single `registerAuto`), and a gate still waiting for host services is reported instead of silently mis-ordering.
6
+ - **Subagent routing adapted to the 0.2.1 activation API (`src/subagent-runtime.ts`).** 0.2.1 replaced `start()` / `startContinuable()` with `startActivation(spec)`. The routed runtime now recognizes **either published line** (`subagentSurfaceOf`), keeps both legacy overrides unchanged, and adds a `startActivation` override that rewrites `spec.request.agentOptions` through the same `provider group → flat subagent card → session model` resolver (a caller-named route still wins; a provider whose capability is not `agentOptions` is still never reinterpreted). Only a Host publishing neither surface falls back to the untouched official runtime with `routedRuntimeUnavailableNotice`, so the 0.2.1 fallback path is now unreachable in practice.
7
+ - **Correction to the v0.4.26 entry.** That entry claimed the official auto-review `apply` "runs unchanged under this row's scoped `llm`" on 0.2.1. The live desktop showed otherwise: it ran unchanged only until the reviewer read a service the mirrored `inject` did not carry. 0.4.26 and 0.4.27 therefore shipped that failure on 0.2.1; 0.4.28 is the first release where the gate actually runs there.
8
+ - **Verification.**
9
+ - `pnpm run check` green (52 test files / 460 tests + pack gate). New coverage: the inject trap reproduced with a real cordis `Service` (an unwrapped gate fails with the exact live message, the wrapped gate passes), the official dependency list surviving the mount, gate-failure reporting, `subagentSurfaceOf` for the activation line, the start line, and neither, activation-spec routing (session group, flat fixed pair, follow-main, explicit caller route, external provider), spec passthrough, and mounting the routed runtime on an activation-only Host.
10
+ - **Real published packages, `0.2.1-rc.2`** (lab CLI `node_modules`, a real `Service` registration so the trap is live, the real official gate): the pre-fix shape reproduces exactly `Auto review of tool "pwsh" failed; its body was not executed: cannot get property "workingDirectory" without inject`; the shipped shape reaches `workingDirectory.ensure` (one call), registers the Auto gate once, and never fails on inject. The real 0.2.1 `SubagentRuntime` classifies as the activation line and drops `start`/`startContinuable`; the adapter delegates to that exact method with `request.agentOptions` rewritten to the configured subagent route while `provider` / `label` / `delivery` / `signal` / `parent` identity pass through.
11
+ - **Real published packages, `0.2.0-rc.2`**: the row registers the Auto gate **without** a `workingDirectory` service, the gate never hits an inject failure, the runtime classifies as the `start` line, and `start()` delegation still rewrites the route and preserves parent identity.
12
+ - **Lab plane** (`DSH_HOME=~/.dsh-lab`, npm-installed `0.2.1-rc.2` runtime, packed `dsh-model-switch-0.4.28.tgz` installed in the `labh` profile): a headless turn ran a real tool (directory listing) with no inject failure and no `{{cwd}}` error; a second turn spawned a real subagent, and the child session's own `subagent/descriptor` records `mode: continuable`, `provider: spawn`, `agentProvider: apigoto`, `agentModel: mimo-v2.6-flash` — the fixed subagent route configured for that profile, not the session default `apigoto/deepseek-flash`. That is the end-to-end proof that the activation-line override reaches the child's route on 0.2.1.
13
+ - **Residual (honest).** The live desktop is the only place the Auto preset gate runs end to end with a real tool body: after updating, re-enable the plugin, switch the approval policy back from full-access mode to Auto, and run one tool call. The harness above exercises the real 0.2.1 packages and the real cordis `Service` trap, but not a full session snapshot — the fake session stops the reviewer right after `workingDirectory.ensure`, by design. 0.1.7 / 0.2.0 keep the same entries, the same route precedence, and the same fail-closed behavior.
14
+
1
15
  ## v0.4.27
2
16
 
3
17
  A fix for a regression v0.4.26 introduced on DSH `0.2.1-rc.2`, plus the architecture change that removes its cause: the plugin no longer ships static copies of DSH's agent presets.
@@ -16,7 +30,7 @@ Two coordinated changes: automatic session naming now survives a failing title r
16
30
  - **DSH 0.2.1 naming line (`executeSessionTitleLlm`).** `0.2.1-rc.2` removed `generateSessionTitleWithLlm` / `registerSessionTitleLlmProvider` and split the shared policy: the new `executeSessionTitleLlm(ctx, config, request, id, prepared)` returns assembled blocks plus the terminal finish reason, each provider owns its system instruction, input framing, reasoning-effort selection, and output interpretation, and `targetWords` / `targetCjkCharacters` moved into the provider's own config. The row detects the installed line at runtime and mirrors the shipped 0.2.1 provider exactly: same system instruction (including the best-effort line and, for all-prompts, the current-title stability rules anchored on a provider-sourced `currentTitle`), same JSON framing, the route's least supported reasoning effort, and the same finish-reason / whole-line-emphasis / empty-output acceptance rules. `cordis.patch.yml` also disables the `optional-session-title-all-prompts` row that 0.2.1's experimental session-titles bundle inserts (unknown-row warning on profiles without it, like the optional auto-review row).
17
31
  - **DSH 0.2.1 compatibility review (0.2.0-rc.2 → 0.2.1-rc.2, public API only).**
18
32
  - **Naming:** the service keeps `register` / `ensureFallback` / the `first-prompt` gate and only adds an optional `currentTitle` to the provider request; `onMainRequest` dropped one boundary-freshness condition. The provider request shape this row returns is unchanged.
19
- - **Auto review:** the official gate now resolves the reviewing agent's directory through `ctx.workingDirectory.ensure(...)` and injects `workingDirectory`; `dsh-base` 0.2.1 mounts the `working-directory` row, so the official `apply` runs unchanged under this row's scoped `llm`.
33
+ - **Auto review:** the official gate now resolves the reviewing agent's directory through `ctx.workingDirectory.ensure(...)` and injects `workingDirectory`; `dsh-base` 0.2.1 mounts the `working-directory` row. **[Corrected in v0.4.28]** This entry originally claimed the official `apply` "runs unchanged under this row's scoped `llm`" on 0.2.1. It did not: the row called `apply` directly and mirrored the gate's services in its own `inject`, so the new `workingDirectory` read failed for every tool call. v0.4.28 mounts the gate as a plugin instead.
20
34
  - **Compaction and presets:** `dsh-compaction-basic` and `dsh-agent-preset` are byte-identical to 0.2.0, so the wholesale preset copies and the compaction engine need no re-sync.
21
35
  - **Unchanged surfaces this plugin calls:** `llm.stream(GenerateOptions)` (with `prepareCall` added beside it), `web.registerSearchProvider`, `tools/pre-execute` (`exec.agent` kept), session `snapshotEvents` / `requestHeader` / `requestContext` / `append`, and the composer's `conversation.input.model` seat.
22
36
  - **Subagents:** `@deepseek-ai/dsh-subagent` replaced `start()` / `startContinuable()` with `startActivation(spec)` plus a provider registry. The routed runtime therefore cannot override the new entry point; this row mounts the untouched official runtime (fail-closed) and logs one explicit notice. Recorded in `docs/seam-gap.md` for a later upstream proposal.
package/PRODUCT.md CHANGED
@@ -33,4 +33,4 @@ Official `settings.section` has no icon field. The Settings nav glyph is a label
33
33
 
34
34
  ## Delivery architecture
35
35
 
36
- The package foundation and pure deep modules remain the policy/test surface. Runtime delivery disables the independent Subagent Loader row with its official name and inserts a uniquely identified replacement; the experimental auto-review row is disabled the same way and replaced by `dsh-model-switch/auto-review`, which runs the official gate's `apply` unchanged under a scoped `llm` shadow (one `registerAuto`, and only when the profile started with the optional auto-review bundle). Official AgentPresets stay mounted so they share the process `dsh-scope` identity. Search uses the released `ctx.web.registerSearchProvider()` seam so official tool ownership is preserved. Image uses a unique tool name and provider-owned authenticated adapters. Self-maintained Codex/Grok plugins register optional adapters through the public Model Switch service when present; standalone provider behavior is unchanged. Vision remains disabled. Replacement modules preserve the official public interfaces and fail closed when compatibility or capability validation fails.
36
+ The package foundation and pure deep modules remain the policy/test surface. Runtime delivery disables the independent Subagent Loader row with its official name and inserts a uniquely identified replacement; the experimental auto-review row is disabled the same way and replaced by `dsh-model-switch/auto-review`, which mounts the official gate's `apply` unchanged as a plugin under a scoped `llm` shadow — the Host resolves the gate's own `inject` list (including 0.2.1's `workingDirectory`), one `registerAuto` runs, and only when the profile started with the optional auto-review bundle. Official AgentPresets stay mounted so they share the process `dsh-scope` identity. Search uses the released `ctx.web.registerSearchProvider()` seam so official tool ownership is preserved. Image uses a unique tool name and provider-owned authenticated adapters. Self-maintained Codex/Grok plugins register optional adapters through the public Model Switch service when present; standalone provider behavior is unchanged. Vision remains disabled. Replacement modules preserve the official public interfaces and fail closed when compatibility or capability validation fails.
package/README.zh.md CHANGED
@@ -180,7 +180,7 @@ DeepSeek 薄适配器调用官方公开 `DeepSeekSearchProvider`,复用用户
180
180
 
181
181
  DSH Host peer 和开发依赖接受 `>=0.1.7-alpha.2`,包括 rc.1 和后续版本。开发锁文件解析到 rc.2。Cordis 接受 `>=4.0.4 <5.0.0`。
182
182
 
183
- 已验证版本:`0.1.7-alpha.2`、`0.1.7-rc.1`、`0.1.7-rc.2`、`0.2.0-rc.2`、`0.2.1-rc.2`。0.2.0 的声明覆盖三处差异:Host 改名的提问撤下动词(`cancel()` → `dismiss()`)、`session.selectModel` 不 await 的部署默认值写入、以返回值报出的 `ModelDirectory.select` 拒绝;三者均已在 lab(3082/3085)与真实桌面会话中逐一验证。0.2.1 的声明覆盖两处被替换的 seam:共享命名调用改为 provider 自备提示词与解析的 `executeSessionTitleLlm`(本插件按安装行在运行时适配,并镜像官方 0.2.1 的提示词、取最低 reasoning effort 与 `currentTitle` 锚定);Subagent 运行时以 `startActivation` 取代 `start()`/`startContinuable()`,路由版运行时在新 Host 上不可用,本插件改为挂载未经改动的官方运行时并输出一条明确提示(记录于 `docs/seam-gap.md`)。0.2.1 的 lab 证据为:npm 安装的 0.2.1 运行时 + 打包后的 0.4.26 组合 `exit 0`、命名/压缩/自动审查/子代理四行均来自本插件、Web Host 在 3082 启动并服务需要鉴权的页面;该次未经浏览器调用 3082 能力 RPC,真实桌面的 0.2.1 命名差分在应用升级后复验。0.2.1 还暴露出一处预设层面的差异(0.2.1 校验提示词变量、只有 `provider`/`model`,且把 `time-context`/`tool-schedule` 移进预设):0.4.26 的静态预设副本因此让每一轮都报 `unknown prompt variable "{{cwd}}"`,0.4.27 删除副本改为启动期运行时桥接,并在 lab 的 0.2.1 headless 真实回合中验证通过(展开见 CHANGELOG 0.4.27)。
183
+ 已验证版本:`0.1.7-alpha.2`、`0.1.7-rc.1`、`0.1.7-rc.2`、`0.2.0-rc.2`、`0.2.1-rc.2`。0.2.0 的声明覆盖三处差异:Host 改名的提问撤下动词(`cancel()` → `dismiss()`)、`session.selectModel` 不 await 的部署默认值写入、以返回值报出的 `ModelDirectory.select` 拒绝;三者均已在 lab(3082/3085)与真实桌面会话中逐一验证。0.2.1 的声明覆盖两处被替换的 seam:共享命名调用改为 provider 自备提示词与解析的 `executeSessionTitleLlm`(本插件按安装行在运行时适配,并镜像官方 0.2.1 的提示词、取最低 reasoning effort 与 `currentTitle` 锚定);Subagent 运行时以 `startActivation` 取代 `start()`/`startContinuable()`,本插件同时识别两条已发布线,并为激活入口覆写 `spec.request.agentOptions`(显式路由仍优先、非 agentOptions 能力的外部适配器不改写),路由语义与旧线一致(0.4.28,记录于 `docs/seam-gap.md`);0.2.1 还新增了自动审查门禁对 `ctx.workingDirectory` 的读取,旧实现因在自己的 fiber 里直调 `official.apply` 而需要手工镜像门禁的 `inject`,于是每个工具调用都被拒(`cannot get property "workingDirectory" without inject`),0.4.28 改为把官方门禁以插件方式挂载、由官方 `inject` 解析依赖,本行只声明自身需要的服务。0.4.28 的真实包证据:0.2.1 上旧形态精确复现该注入错误、新形态到达 `workingDirectory.ensure` 且成功注册门禁、激活入口的路由改写与 spec 透传通过;0.2.0 上无需 `workingDirectory` 服务即可注册门禁,`start()` 路由改写与父身份保持不变。0.2.1 的 lab 证据为:npm 安装的 0.2.1 运行时 + 打包后的 0.4.26 组合 `exit 0`、命名/压缩/自动审查/子代理四行均来自本插件、Web Host 在 3082 启动并服务需要鉴权的页面;该次未经浏览器调用 3082 能力 RPC,真实桌面的 0.2.1 命名差分在应用升级后复验。0.2.1 还暴露出一处预设层面的差异(0.2.1 校验提示词变量、只有 `provider`/`model`,且把 `time-context`/`tool-schedule` 移进预设):0.4.26 的静态预设副本因此让每一轮都报 `unknown prompt variable "{{cwd}}"`,0.4.27 删除副本改为启动期运行时桥接,并在 lab 的 0.2.1 headless 真实回合中验证通过(展开见 CHANGELOG 0.4.27)。
184
184
 
185
185
  `package.json#dsh.compatibility.dshReleases` 中的已验证版本是证据,不是允许列表。新增验证声明前要审查公开 API 变化并测试新 Host 版本。
186
186
 
@@ -32,15 +32,18 @@ import { AsyncLocalStorage } from "node:async_hooks";
32
32
  */
33
33
  /** Cordis plugin name used by loader diagnostics. */
34
34
  const name = "model-switch-auto-review";
35
- /** The official gate's host services plus Model Switch's settings owner. */
36
- const inject = [
37
- "modelSwitch",
38
- "approval",
39
- "llm",
40
- "permissionPresets",
41
- "sessions",
42
- "tools"
43
- ];
35
+ /**
36
+ * Only the services this row's own code touches.
37
+ *
38
+ * The official gate used to run inside this row's fiber, so this list had to
39
+ * mirror every service the official `apply` reads. DSH 0.2.1 added
40
+ * `workingDirectory` to the official gate and the mirror went stale, which made
41
+ * every tool call fail with `cannot get property "workingDirectory" without
42
+ * inject`. The gate is now mounted as its own plugin (see {@link autoReviewGate}),
43
+ * so cordis resolves the official dependency list — including future additions —
44
+ * and this row declares only what it reads itself.
45
+ */
46
+ const inject = ["llm", "modelSwitch"];
44
47
  /** Optional bundle whose composition opts a profile into Auto review. */
45
48
  const AUTO_REVIEW_BUNDLE = "@deepseek-ai/dsh-experimental-auto-review";
46
49
  /**
@@ -138,6 +141,35 @@ function autoReviewBundleActive(ctx) {
138
141
  return Array.isArray(bundles) && bundles.includes(AUTO_REVIEW_BUNDLE);
139
142
  }
140
143
  /**
144
+ * Wrap the official gate as a plugin that declares its own dependencies.
145
+ *
146
+ * Directly calling `official.apply(scope)` ran the gate's code on this row's
147
+ * fiber, so every service the gate reads had to be mirrored in this row's
148
+ * `inject`. Mounting it instead lets cordis resolve the official list, which is
149
+ * what keeps a released Host's new dependency (0.2.1's `workingDirectory`) from
150
+ * breaking every tool call. The hooks let the caller observe whether the gate
151
+ * actually ran before it binds the session-correlation listener.
152
+ *
153
+ * @param gate - the official module (or an equivalent structural gate).
154
+ * @param hooks - applied/failure observers; the failure is rethrown.
155
+ * @returns A cordis plugin mounting the official gate unchanged.
156
+ */
157
+ function autoReviewGate(gate, hooks = {}) {
158
+ return {
159
+ name: gate.name ?? "experimental-auto-review",
160
+ ...gate.inject === void 0 ? {} : { inject: [...gate.inject] },
161
+ apply(gateCtx) {
162
+ try {
163
+ gate.apply(gateCtx);
164
+ } catch (error) {
165
+ hooks.onFailure?.(error);
166
+ throw error;
167
+ }
168
+ hooks.onApplied?.();
169
+ }
170
+ };
171
+ }
172
+ /**
141
173
  * Mount the official gate under the reviewer-route scope.
142
174
  *
143
175
  * A profile that never enabled the optional bundle keeps no Auto preset and no
@@ -159,13 +191,31 @@ async function apply(ctx) {
159
191
  }
160
192
  const activeSession = new AsyncLocalStorage();
161
193
  const scope = reviewScope(ctx, activeSession);
194
+ let failure;
195
+ let applied = false;
196
+ const fiber = scope.plugin(autoReviewGate(official, {
197
+ onApplied: () => {
198
+ applied = true;
199
+ },
200
+ onFailure: (error) => {
201
+ failure = error;
202
+ }
203
+ }));
162
204
  try {
163
- official.apply(scope);
164
- bindAutoReviewSessionCorrelation(scope, activeSession);
205
+ await fiber;
165
206
  } catch (cause) {
207
+ failure ??= cause;
208
+ }
209
+ if (failure !== void 0) {
166
210
  ctx.logger.error("model-switch: the official auto-review row is already active; the reviewer route override is unavailable");
167
- ctx.logger.error(cause);
211
+ ctx.logger.error(failure);
212
+ return;
213
+ }
214
+ if (!applied) {
215
+ ctx.logger.info("model-switch: the official auto-review gate is waiting for its host services; the reviewer route override is not bound yet");
216
+ return;
168
217
  }
218
+ bindAutoReviewSessionCorrelation(scope, activeSession);
169
219
  }
170
220
  //#endregion
171
- export { apply, autoReviewRouteOf, bindAutoReviewSessionCorrelation, inject, name, resolveSessionAutoReviewRoute, reviewLlm, reviewScope };
221
+ export { apply, autoReviewGate, autoReviewRouteOf, bindAutoReviewSessionCorrelation, inject, name, resolveSessionAutoReviewRoute, reviewLlm, reviewScope };
@@ -49,6 +49,30 @@ function routeSubagentRequest(request, route) {
49
49
  function assertPublicMethod(value, surface, method) {
50
50
  if (value === null || typeof value !== "object" && typeof value !== "function" || typeof Reflect.get(value, method) !== "function") throw new StartupIncompatibilityError(surface, surface + " must expose public " + method + "()");
51
51
  }
52
+ /**
53
+ * Classify the runtime surface this Host publishes.
54
+ *
55
+ * DSH 0.2.1 replaced `start()` / `startContinuable()` with the activation API
56
+ * (`startActivation`), so a version-agnostic adapter has to recognize both
57
+ * lines instead of hard-coding one.
58
+ *
59
+ * @param value - a runtime class's prototype or a mounted runtime instance.
60
+ * @returns `activation` (0.2.1+), `start` (0.1.7/0.2.0), or undefined when neither is published.
61
+ */
62
+ function subagentSurfaceOf(value) {
63
+ if (value === null || typeof value !== "object" && typeof value !== "function") return void 0;
64
+ if (typeof Reflect.get(value, "startActivation") === "function") return "activation";
65
+ const hasStart = typeof Reflect.get(value, "start") === "function";
66
+ const hasContinuable = typeof Reflect.get(value, "startContinuable") === "function";
67
+ return hasStart && hasContinuable ? "start" : void 0;
68
+ }
69
+ /** Read one public method, failing with the typed startup error when it is absent. */
70
+ function publicMethod(prototype, surface, method) {
71
+ if (prototype === null || typeof prototype !== "object" && typeof prototype !== "function") throw new StartupIncompatibilityError(surface, surface + " must expose public " + method + "()");
72
+ const value = Reflect.get(prototype, method);
73
+ if (typeof value !== "function") throw new StartupIncompatibilityError(surface, surface + " must expose public " + method + "()");
74
+ return value;
75
+ }
52
76
  function constructible(value) {
53
77
  if (typeof value !== "function") return false;
54
78
  try {
@@ -62,9 +86,9 @@ function constructible(value) {
62
86
  function assertOfficialRuntimeSurface() {
63
87
  const runtime = OfficialSubagentRuntime;
64
88
  if (!constructible(runtime)) throw new StartupIncompatibilityError("OfficialSubagentRuntime", "OfficialSubagentRuntime must be constructible");
65
- const prototype = Reflect.get(runtime, "prototype");
66
- assertPublicMethod(prototype, "OfficialSubagentRuntime", "start");
67
- assertPublicMethod(prototype, "OfficialSubagentRuntime", "startContinuable");
89
+ const line = subagentSurfaceOf(Reflect.get(runtime, "prototype"));
90
+ if (line === void 0) throw new StartupIncompatibilityError("OfficialSubagentRuntime", "OfficialSubagentRuntime must expose public startActivation() (0.2.1+) or start()/startContinuable()");
91
+ return line;
68
92
  }
69
93
  function assertRoutingSurface(ctx) {
70
94
  const modelSwitch = ctx.modelSwitch;
@@ -74,9 +98,9 @@ function routingSurface(ctx) {
74
98
  return ctx.modelSwitch;
75
99
  }
76
100
  function assertMountedSurface(ctx) {
77
- assertPublicMethod(ctx.subagents, "mounted Subagent runtime", "start");
78
- assertPublicMethod(ctx.subagents, "mounted Subagent runtime", "startContinuable");
79
- assertPublicMethod(ctx.subagents, "mounted Subagent runtime", "resolveMaxDepth");
101
+ const service = ctx.subagents;
102
+ assertPublicMethod(service, "mounted Subagent runtime", "resolveMaxDepth");
103
+ if (subagentSurfaceOf(service) === void 0) throw new StartupIncompatibilityError("mounted Subagent runtime", "mounted Subagent runtime must expose public startActivation() (0.2.1+) or start()/startContinuable()");
80
104
  }
81
105
  function once(dispose) {
82
106
  let pending;
@@ -199,14 +223,29 @@ var ModelSwitchSubagentRuntime = class extends OfficialSubagentRuntime {
199
223
  return routeSubagentRequest(request, sessionSubagentRouteOf(this.ctx, request.parent.session, modelSwitch.currentSettings()));
200
224
  }
201
225
  start(name, request) {
202
- return super.start(name, this.routed(name, request));
226
+ return publicMethod(OfficialSubagentRuntime.prototype, "OfficialSubagentRuntime", "start").call(this, name, this.routed(name, request));
203
227
  }
204
228
  startContinuable(spec) {
205
- return super.startContinuable({
229
+ return publicMethod(OfficialSubagentRuntime.prototype, "OfficialSubagentRuntime", "startContinuable").call(this, {
230
+ ...spec,
231
+ request: this.routed(spec.provider, spec.request)
232
+ });
233
+ }
234
+ /**
235
+ * 0.2.1+ activation entry: delegation tools call this instead of
236
+ * `start()`/`startContinuable()`, and its spec carries the same
237
+ * `request.agentOptions` this adapter already rewrites. Every other spec
238
+ * field (label, childId, signal, delivery) passes through untouched.
239
+ *
240
+ * @param spec - the activation spec published by the installed Host.
241
+ * @returns The official activation result.
242
+ */
243
+ startActivation(spec) {
244
+ return publicMethod(OfficialSubagentRuntime.prototype, "OfficialSubagentRuntime", "startActivation").call(this, {
206
245
  ...spec,
207
246
  request: this.routed(spec.provider, spec.request)
208
247
  });
209
248
  }
210
249
  };
211
250
  //#endregion
212
- export { ModelSwitchSubagentRuntime, StartupIncompatibilityError, profileSubagentRuntime as default, profileSubagentRuntime, mountWithStartupFallback, routeSubagentRequest, routedRuntimeUnavailableNotice };
251
+ export { ModelSwitchSubagentRuntime, StartupIncompatibilityError, profileSubagentRuntime as default, profileSubagentRuntime, mountWithStartupFallback, routeSubagentRequest, routedRuntimeUnavailableNotice, subagentSurfaceOf };
@@ -33,7 +33,17 @@ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
33
33
  import type { Session } from '@deepseek-ai/dsh-session';
34
34
  /** Cordis plugin name used by loader diagnostics. */
35
35
  export declare const name = "model-switch-auto-review";
36
- /** The official gate's host services plus Model Switch's settings owner. */
36
+ /**
37
+ * Only the services this row's own code touches.
38
+ *
39
+ * The official gate used to run inside this row's fiber, so this list had to
40
+ * mirror every service the official `apply` reads. DSH 0.2.1 added
41
+ * `workingDirectory` to the official gate and the mirror went stale, which made
42
+ * every tool call fail with `cannot get property "workingDirectory" without
43
+ * inject`. The gate is now mounted as its own plugin (see {@link autoReviewGate}),
44
+ * so cordis resolves the official dependency list — including future additions —
45
+ * and this row declares only what it reads itself.
46
+ */
37
47
  export declare const inject: string[];
38
48
  /** Pinned reviewer route; both identifiers are non-empty. */
39
49
  export interface AutoReviewRoute {
@@ -93,6 +103,42 @@ export declare function bindAutoReviewSessionCorrelation(scope: Context, activeS
93
103
  * @returns A child context shadowing only the llm service.
94
104
  */
95
105
  export declare function reviewScope(ctx: Context, activeSession?: AsyncLocalStorage<Session | undefined>): Context;
106
+ /**
107
+ * Structural view of the official gate's plugin face.
108
+ *
109
+ * `name` and `inject` are part of the module's released plugin contract; the
110
+ * gate is mounted through cordis rather than called directly so its own
111
+ * dependency list is what the fiber resolves.
112
+ */
113
+ export interface OfficialAutoReviewGate {
114
+ readonly apply: (ctx: Context) => void;
115
+ readonly inject?: readonly string[];
116
+ readonly name?: string;
117
+ }
118
+ /** The plugin face handed to cordis, with the official dependency list kept. */
119
+ export interface MountedAutoReviewGate {
120
+ readonly name: string;
121
+ readonly inject?: string[];
122
+ readonly apply: (gateCtx: Context) => void;
123
+ }
124
+ /**
125
+ * Wrap the official gate as a plugin that declares its own dependencies.
126
+ *
127
+ * Directly calling `official.apply(scope)` ran the gate's code on this row's
128
+ * fiber, so every service the gate reads had to be mirrored in this row's
129
+ * `inject`. Mounting it instead lets cordis resolve the official list, which is
130
+ * what keeps a released Host's new dependency (0.2.1's `workingDirectory`) from
131
+ * breaking every tool call. The hooks let the caller observe whether the gate
132
+ * actually ran before it binds the session-correlation listener.
133
+ *
134
+ * @param gate - the official module (or an equivalent structural gate).
135
+ * @param hooks - applied/failure observers; the failure is rethrown.
136
+ * @returns A cordis plugin mounting the official gate unchanged.
137
+ */
138
+ export declare function autoReviewGate(gate: OfficialAutoReviewGate, hooks?: {
139
+ readonly onApplied?: () => void;
140
+ readonly onFailure?: (error: unknown) => void;
141
+ }): MountedAutoReviewGate;
96
142
  /**
97
143
  * Mount the official gate under the reviewer-route scope.
98
144
  *
@@ -21,6 +21,19 @@ export interface MountedStartup<T> {
21
21
  readonly dispose: () => Promise<void>;
22
22
  }
23
23
  type RoutableSubagentRequest = Pick<SubagentStartRequest, 'parent' | 'agentOptions'>;
24
+ /**
25
+ * The 0.2.1+ activation spec, declared structurally.
26
+ *
27
+ * The pinned dependency typings predate the activation API, so the modern entry
28
+ * is described here and delegated through the base prototype. `request` carries
29
+ * the same `parent`/`agentOptions` pair the legacy start request does, which is
30
+ * why one route adapter serves both lines.
31
+ */
32
+ export interface SubagentActivationSpecLike {
33
+ readonly provider: string;
34
+ readonly request: Pick<SubagentStartRequest, 'parent' | 'agentOptions'> & Record<string, unknown>;
35
+ readonly [field: string]: unknown;
36
+ }
24
37
  /**
25
38
  * Inject one resolved route into a spawn that names none.
26
39
  *
@@ -34,6 +47,19 @@ type RoutableSubagentRequest = Pick<SubagentStartRequest, 'parent' | 'agentOptio
34
47
  * @returns The request to hand the official runtime.
35
48
  */
36
49
  export declare function routeSubagentRequest<T extends RoutableSubagentRequest>(request: T, route: SubagentRoute | undefined): T;
50
+ /** The public start surface one installed Subagent runtime publishes. */
51
+ export type SubagentSurfaceLine = 'activation' | 'start';
52
+ /**
53
+ * Classify the runtime surface this Host publishes.
54
+ *
55
+ * DSH 0.2.1 replaced `start()` / `startContinuable()` with the activation API
56
+ * (`startActivation`), so a version-agnostic adapter has to recognize both
57
+ * lines instead of hard-coding one.
58
+ *
59
+ * @param value - a runtime class's prototype or a mounted runtime instance.
60
+ * @returns `activation` (0.2.1+), `start` (0.1.7/0.2.0), or undefined when neither is published.
61
+ */
62
+ export declare function subagentSurfaceOf(value: unknown): SubagentSurfaceLine | undefined;
37
63
  /**
38
64
  * Mount a candidate and use the fallback only for typed startup incompatibility.
39
65
  *
@@ -68,5 +94,15 @@ export declare class ModelSwitchSubagentRuntime extends OfficialSubagentRuntime
68
94
  private routed;
69
95
  start(name: string, request: SubagentStartRequest): Promise<SubagentRun>;
70
96
  startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>;
97
+ /**
98
+ * 0.2.1+ activation entry: delegation tools call this instead of
99
+ * `start()`/`startContinuable()`, and its spec carries the same
100
+ * `request.agentOptions` this adapter already rewrites. Every other spec
101
+ * field (label, childId, signal, delivery) passes through untouched.
102
+ *
103
+ * @param spec - the activation spec published by the installed Host.
104
+ * @returns The official activation result.
105
+ */
106
+ startActivation(spec: SubagentActivationSpecLike): Promise<unknown>;
71
107
  }
72
108
  export default profileSubagentRuntime;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-switch",
3
- "version": "0.4.27",
3
+ "version": "0.4.28",
4
4
  "description": "Explicit model routing plus composer/Plan picker for DeepSeek Harness",
5
5
  "repository": {
6
6
  "type": "git",