dsh-model-switch 0.4.27 → 0.4.29
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 +25 -1
- package/PRODUCT.md +1 -1
- package/README.zh.md +3 -1
- package/lib/auto-review.js +63 -13
- package/lib/session-title.js +230 -31
- package/lib/subagent-runtime.js +48 -9
- package/lib/types/auto-review.d.ts +47 -1
- package/lib/types/session-title.d.ts +12 -1
- package/lib/types/subagent-runtime.d.ts +36 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## v0.4.29
|
|
2
|
+
|
|
3
|
+
The experimental "随对话更新的标题" bundle (`dsh-experimental-session-titles-bundle`) no longer fails to enable while this plugin is active: the naming row now takes over the single provider slot the bundle's row competes for, mirrors that row's cadence, and keeps the configurable naming route working.
|
|
4
|
+
|
|
5
|
+
- **What broke.** Enabling the bundle produced `1 entry did not activate optional-session-title-all-prompts (@deepseek-ai/dsh-session-title-all-prompts-llm): session-title provider "session-title-first-prompt-llm" is already registered`. DSH 0.2.1's session-title service accepts exactly one registered provider, and this row already owned that slot with its mirrored first-prompt provider. The bundle's patch inserts its all-prompts row **after** this plugin's patch layer, so the static `- id: optional-session-title-all-prompts` disable this plugin ships can never reach a row that does not exist yet when that layer applies.
|
|
6
|
+
- **Fix: the row now takes the slot over instead of colliding (`src/session-title.ts`).** It mirrors the official row that exists in the tree — all-prompts winning whenever the experimental row is composed, first-prompt otherwise — and it watches `loader/patch-context`, which the loader runs inside the official entry before that entry's plugin `apply`. When an official naming row is about to register, this row yields the slot first (the service lets a registration be replaced once its disposer ran), so the official `register()` **succeeds** and the loader reports no failed activation; it then disables that row through the public `Entry.update({ disabled: true })` (which disposes the row's fiber and closes its registration) and claims the slot back under the row's cadence. A bounded retry absorbs the cases where the loader has not created the row's fiber yet, and an initial sweep handles the reverse composition order (an official row that activated before this row started). Only seams the loader already publishes are used: the `loader/patch-context` waterfall, `Entry.update`, and `sessionTitle.register` plus its disposer.
|
|
7
|
+
- **User-visible behavior.** Enabling the bundle: titles now update with every prompt **and** the configured naming model / provider-group route keeps applying (the provider id recorded with each title is `session-title-all-prompts-llm`, matching what the official row would record). Disabling the bundle: the first-prompt cadence comes back with the same route override. A Host that changes activation timing degrades to "the official provider serves naming, one explicit log line" rather than a crash or a wrong route.
|
|
8
|
+
- **Verification.** `pnpm run check` green (53 test files / 466 tests + pack gate). New coverage: the all-prompts row wins the mirror, the takeover yields before the official registration and reclaims the slot with the mirrored cadence, repeat patch-context events are idempotent, an already-active row is swept, and no official row keeps today's single first-prompt registration. Real published packages on `0.2.1-rc.2` (the real `dsh-session-title` service plus the real `dsh-session-title-all-prompts-llm` apply): the official row registers with **no** `already registered` error, the displaced row is disabled through `Entry.update`, the plugin owns the slot again, and the live provider is `session-title-all-prompts-llm` / `all-prompts`.
|
|
9
|
+
- **Residual (honest).** The takeover choreographs released loader seams rather than a dedicated "supersede this provider" API; `docs/seam-gap.md` records the missing seam and the upstream proposal (a decorate/replace registration or a multi-provider slot). The reverse direction — disabling the bundle at runtime without a profile restart — re-claims through `loader/partial-dispose` where the loader emits it, and otherwise needs the restart DSH already asks for on bundle changes.
|
|
10
|
+
|
|
11
|
+
## v0.4.28
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
- **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.
|
|
18
|
+
- **Verification.**
|
|
19
|
+
- `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.
|
|
20
|
+
- **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.
|
|
21
|
+
- **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.
|
|
22
|
+
- **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.
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
1
25
|
## v0.4.27
|
|
2
26
|
|
|
3
27
|
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 +40,7 @@ Two coordinated changes: automatic session naming now survives a failing title r
|
|
|
16
40
|
- **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
41
|
- **DSH 0.2.1 compatibility review (0.2.0-rc.2 → 0.2.1-rc.2, public API only).**
|
|
18
42
|
- **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
|
|
43
|
+
- **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
44
|
- **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
45
|
- **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
46
|
- **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
|
|
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. Session naming replaces the official `session-title-llm` row and owns the service's single provider slot; it mirrors the official cadence the profile declares — first-prompt by default, and the all-prompts cadence whenever the experimental session-titles bundle inserts its row — so enabling that bundle keeps both the per-prompt title refresh and this plugin's configurable naming route, while disabling it restores the first-prompt cadence. The takeover only uses released loader seams (the `loader/patch-context` waterfall, `Entry.update`, and `sessionTitle.register` plus its disposer) and degrades to the official provider with one explicit log line if a future Host changes activation timing. 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
|
@@ -119,6 +119,8 @@ DSH 会自动为新会话起名;命名的触发节奏由 profile 的官方命
|
|
|
119
119
|
|
|
120
120
|
路由在**每次命名请求时现读**:保存后对下一次起名立即生效、无需重启,且进行中的命名请求不会被保存打断。每次请求按顺序解析路由:该会话供应商组里的命名对,其次卡片的部署级对,其次会话自身的模型。请求经由官方命名实现发出,它会把实际使用的 provider/model 记录到该会话自己的 `session/title-llm-request` 事件里,因此持久记录与真实请求永远一致。本插件替换官方的 `session-title-llm` 行而不分叉命名逻辑(该接缝每个上下文只允许注册一个命名 provider),并镜像该行声明的节奏;profile 没有官方命名行时保持 DSH 的确定性兜底标题。
|
|
121
121
|
|
|
122
|
+
0.4.29 起与实验包「随对话更新的标题」(`dsh-experimental-session-titles-bundle`,插入 all-prompts 命名行)兼容:启用该实验包时,本插件接管同一个命名 provider 槽位并镜像 all-prompts 节奏,标题随对话更新,同时命名模型路由与供应商组路由继续生效(`session/title` 记录中的 provider id 为 `session-title-all-prompts-llm`,与官方行一致);禁用实验包即回到首条提示节奏与同样的路由覆写。实现只使用 loader 已发布的缝(`loader/patch-context` 瀑布、`Entry.update`、`sessionTitle.register` 及其 disposer),让官方行先成功注册、再被停用并由本行重占槽位,因此启用时不再出现 `already registered` 激活失败弹窗。
|
|
123
|
+
|
|
122
124
|
## 供应商配置组(Provider groups)
|
|
123
125
|
|
|
124
126
|
「供应商配置组」卡片为每个供应商保存一组路由——压缩、自动审查、会话命名与子代理四条路由一起存;为供应商 X 保存的组,作用于自身供应商就是 X 的会话。组是**推导出来的,而不是写进去的**:切换某个会话的供应商不写任何东西,因此不同供应商上的多个会话并行时再也不会互相覆盖、互相改写对方的辅助路由。
|
|
@@ -180,7 +182,7 @@ DeepSeek 薄适配器调用官方公开 `DeepSeekSearchProvider`,复用用户
|
|
|
180
182
|
|
|
181
183
|
DSH Host peer 和开发依赖接受 `>=0.1.7-alpha.2`,包括 rc.1 和后续版本。开发锁文件解析到 rc.2。Cordis 接受 `>=4.0.4 <5.0.0`。
|
|
182
184
|
|
|
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()
|
|
185
|
+
已验证版本:`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
186
|
|
|
185
187
|
`package.json#dsh.compatibility.dshReleases` 中的已验证版本是证据,不是允许列表。新增验证声明前要审查公开 API 变化并测试新 Host 版本。
|
|
186
188
|
|
package/lib/auto-review.js
CHANGED
|
@@ -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
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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(
|
|
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 };
|
package/lib/session-title.js
CHANGED
|
@@ -148,19 +148,32 @@ function selectTitleMessages(messages, firstPromptOnly) {
|
|
|
148
148
|
if (first === void 0) throw new Error("first-prompt title provider requires one human message");
|
|
149
149
|
return [first];
|
|
150
150
|
}
|
|
151
|
+
/** The id the experimental session-titles bundle inserts for all-prompts naming. */
|
|
152
|
+
const ALL_PROMPTS_ROW = "optional-session-title-all-prompts";
|
|
151
153
|
/**
|
|
152
154
|
* Locate the official session-title provider row in the loader tree.
|
|
155
|
+
*
|
|
153
156
|
* Disabled entries stay in the tree, so a name-matched disable still matches.
|
|
157
|
+
* The all-prompts row wins when it is present: that row only exists while the
|
|
158
|
+
* experimental session-titles bundle is composed, and its presence is exactly
|
|
159
|
+
* the user's choice to update titles with every prompt. The default
|
|
160
|
+
* first-prompt row is the fallback when that bundle is off.
|
|
154
161
|
* @param loader - the host loader, when one is reachable.
|
|
155
|
-
* @returns The official entry, or undefined when this profile has no such row.
|
|
162
|
+
* @returns The official entry to mirror, or undefined when this profile has no such row.
|
|
156
163
|
*/
|
|
157
164
|
function officialTitleEntry(loader) {
|
|
158
165
|
if (!loader) return void 0;
|
|
166
|
+
const allPrompts = TITLE_CADENCES[1];
|
|
167
|
+
const firstPrompt = TITLE_CADENCES[0];
|
|
168
|
+
let fallback;
|
|
159
169
|
for (const entry of loader.entries()) {
|
|
160
|
-
if (entry.id ===
|
|
170
|
+
if (entry.id === ALL_PROMPTS_ROW) return entry;
|
|
161
171
|
const pkg = entry.options?.name;
|
|
162
|
-
if (typeof pkg === "string" &&
|
|
172
|
+
if (typeof pkg === "string" && allPrompts !== void 0 && pkg === allPrompts.pkg) return entry;
|
|
173
|
+
if (fallback === void 0 && entry.id === OFFICIAL_TITLE_ROW) fallback = entry;
|
|
174
|
+
else if (fallback === void 0 && firstPrompt !== void 0 && pkg === firstPrompt.pkg) fallback = entry;
|
|
163
175
|
}
|
|
176
|
+
return fallback;
|
|
164
177
|
}
|
|
165
178
|
/**
|
|
166
179
|
* Copy the official row's stored config, ignoring non-object payloads.
|
|
@@ -517,6 +530,214 @@ function currentSettings(ctx) {
|
|
|
517
530
|
return;
|
|
518
531
|
}
|
|
519
532
|
}
|
|
533
|
+
/** Resolve the naming plan for one official row, or the shipped defaults when no row exists. */
|
|
534
|
+
function planFor(ctx, module, entry) {
|
|
535
|
+
const cadence = titleCadence(entry === void 0 ? void 0 : officialTitlePackage(entry)) ?? TITLE_CADENCES[0];
|
|
536
|
+
if (cadence === void 0) return void 0;
|
|
537
|
+
const forwarded = entry === void 0 ? {} : forwardedConfig(entry);
|
|
538
|
+
let policy;
|
|
539
|
+
try {
|
|
540
|
+
policy = resolveNamingPolicy(module, forwarded);
|
|
541
|
+
} catch (cause) {
|
|
542
|
+
ctx.logger.error("model-switch: the official session-title row has no valid naming policy; naming keeps the deterministic fallback");
|
|
543
|
+
ctx.logger.error(cause);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
cadence,
|
|
548
|
+
config: policy.config,
|
|
549
|
+
targets: policy.targets
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/** One naming plan bound to a live route resolver. */
|
|
553
|
+
function providerOf(ctx, module, plan) {
|
|
554
|
+
return {
|
|
555
|
+
id: plan.cadence.id,
|
|
556
|
+
automatic: plan.cadence.automatic,
|
|
557
|
+
generate: (request) => generateTitle(ctx, module, plan, request)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/** Timing knobs for the takeover choreography (exported for tests). */
|
|
561
|
+
const TAKEOVER_RETRY_DELAY_MS = 10;
|
|
562
|
+
/** How many times the takeover re-checks after disabling the official row. */
|
|
563
|
+
const TAKEOVER_MAX_ATTEMPTS = 20;
|
|
564
|
+
/**
|
|
565
|
+
* The single naming-provider slot this row serves.
|
|
566
|
+
*
|
|
567
|
+
* DSH's session-title service accepts exactly one registered provider, and the
|
|
568
|
+
* official rows (`session-title-llm`, and the experimental row the
|
|
569
|
+
* `dsh-experimental-session-titles-bundle` inserts) compete for the same slot.
|
|
570
|
+
* This row therefore mirrors one official row and owns the slot; when another
|
|
571
|
+
* official row activates anyway (the experimental bundle inserts its row after
|
|
572
|
+
* this plugin's patch layer, so a static `disabled` cannot reach it), it yields
|
|
573
|
+
* the slot so that row's own registration succeeds — no activation warning —
|
|
574
|
+
* and then disables that row and takes the slot back with its cadence mirrored.
|
|
575
|
+
* Every step uses released seams: the `loader/patch-context` waterfall, the
|
|
576
|
+
* public `Entry.update`, and `sessionTitle.register` plus its disposer.
|
|
577
|
+
*/
|
|
578
|
+
var NamingSeam = class {
|
|
579
|
+
ctx;
|
|
580
|
+
module;
|
|
581
|
+
disposer;
|
|
582
|
+
plan;
|
|
583
|
+
constructor(ctx, module, plan) {
|
|
584
|
+
this.ctx = ctx;
|
|
585
|
+
this.module = module;
|
|
586
|
+
this.plan = plan;
|
|
587
|
+
}
|
|
588
|
+
/** The cadence currently registered. */
|
|
589
|
+
get cadence() {
|
|
590
|
+
return this.plan.cadence;
|
|
591
|
+
}
|
|
592
|
+
/** Whether this row currently owns the slot. */
|
|
593
|
+
get claimed() {
|
|
594
|
+
return this.disposer !== void 0;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Register this row's provider with the given plan.
|
|
598
|
+
* @param plan - the naming plan to serve (defaults to the current one).
|
|
599
|
+
* @returns true when the slot is ours, false when another provider holds it.
|
|
600
|
+
*/
|
|
601
|
+
claim(plan = this.plan) {
|
|
602
|
+
this.plan = plan;
|
|
603
|
+
if (this.disposer !== void 0) return true;
|
|
604
|
+
try {
|
|
605
|
+
this.disposer = this.ctx.sessionTitle.register(providerOf(this.ctx, this.module, plan));
|
|
606
|
+
return true;
|
|
607
|
+
} catch {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/** Release the slot (the service lets the next registration replace a closing one). */
|
|
612
|
+
yieldSlot() {
|
|
613
|
+
const disposer = this.disposer;
|
|
614
|
+
this.disposer = void 0;
|
|
615
|
+
if (disposer === void 0) return;
|
|
616
|
+
try {
|
|
617
|
+
disposer();
|
|
618
|
+
} catch (cause) {
|
|
619
|
+
this.ctx.logger.error("model-switch: releasing the session-title provider failed");
|
|
620
|
+
this.ctx.logger.error(cause);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/** Swap the served plan, re-registering only when the slot is already ours. */
|
|
624
|
+
retarget(plan) {
|
|
625
|
+
if (!this.claimed) {
|
|
626
|
+
this.plan = plan;
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
this.yieldSlot();
|
|
630
|
+
this.claim(plan);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
/** Whether one loader entry declares an official naming provider this row supersedes. */
|
|
634
|
+
function isOfficialTitleRow(entry) {
|
|
635
|
+
if (entry === void 0) return false;
|
|
636
|
+
if (entry.id === OFFICIAL_TITLE_ROW || entry.id === ALL_PROMPTS_ROW) return true;
|
|
637
|
+
const pkg = entry.options?.name;
|
|
638
|
+
return typeof pkg === "string" && TITLE_CADENCES.some((cadence) => cadence.pkg === pkg);
|
|
639
|
+
}
|
|
640
|
+
/** The id one entry is known by (the loader's leaf id, not a nested path). */
|
|
641
|
+
function leafId(entry) {
|
|
642
|
+
const raw = entry.options?.id;
|
|
643
|
+
return typeof raw === "string" ? raw : void 0;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Install the takeover choreography for naming rows that activate after this one.
|
|
647
|
+
*
|
|
648
|
+
* `loader/patch-context` runs inside the official entry's own `_init`, before its
|
|
649
|
+
* plugin's `apply`, and it receives that entry. At that point the entry's
|
|
650
|
+
* `options` are final, so this row can yield the slot first — the official
|
|
651
|
+
* `register()` then succeeds instead of throwing the "already registered"
|
|
652
|
+
* activation warning — and afterwards disable that row through the public
|
|
653
|
+
* `Entry.update` and take the slot back with its cadence mirrored.
|
|
654
|
+
* @param ctx - this row's context.
|
|
655
|
+
* @param module - the auxiliary-LLM package.
|
|
656
|
+
* @param seam - the slot owner.
|
|
657
|
+
* @returns Nothing; the listeners are effects of this row's fiber.
|
|
658
|
+
*/
|
|
659
|
+
function installTakeover(ctx, module, seam) {
|
|
660
|
+
/** Entries already handled, so a repeated patch-context does not loop. */
|
|
661
|
+
const handled = /* @__PURE__ */ new Set();
|
|
662
|
+
/** Entries with a reclaim cycle in flight, so two triggers cannot race. */
|
|
663
|
+
const reclaiming = /* @__PURE__ */ new Set();
|
|
664
|
+
/** After the yield, disable the official row and take the slot back. */
|
|
665
|
+
const reclaim = async (entry, plan) => {
|
|
666
|
+
if (reclaiming.has(entry)) return;
|
|
667
|
+
reclaiming.add(entry);
|
|
668
|
+
try {
|
|
669
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
670
|
+
await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 0 : 10));
|
|
671
|
+
try {
|
|
672
|
+
await entry.update({ disabled: true });
|
|
673
|
+
} catch (cause) {
|
|
674
|
+
ctx.logger.info("model-switch: the displaced naming row could not be persisted as disabled; the takeover repeats next startup");
|
|
675
|
+
ctx.logger.info(String(cause));
|
|
676
|
+
}
|
|
677
|
+
if (seam.claim(plan)) {
|
|
678
|
+
ctx.logger.info(`model-switch: displaced the official naming row ${String(leafId(entry) ?? "unknown")}; its ${plan.cadence.automatic} cadence and policy are mirrored by the configurable naming route`);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
ctx.logger.error(`model-switch: the official naming row ${String(leafId(entry) ?? "unknown")} kept the session-title provider slot; the configurable naming route is unavailable this session`);
|
|
683
|
+
} finally {
|
|
684
|
+
reclaiming.delete(entry);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
/**
|
|
688
|
+
* The row an event names.
|
|
689
|
+
*
|
|
690
|
+
* The loader hands the emitting entry as the event's `this`, but cordis does
|
|
691
|
+
* not rebind `this` for listeners added through `ctx.on`, so a listener can
|
|
692
|
+
* also receive the payload as its first argument. Both shapes are accepted;
|
|
693
|
+
* the real Host always provides one of them.
|
|
694
|
+
*/
|
|
695
|
+
const entryOfEvent = (self, first) => {
|
|
696
|
+
const candidate = self ?? first;
|
|
697
|
+
return candidate !== null && typeof candidate === "object" && "options" in candidate ? candidate : void 0;
|
|
698
|
+
};
|
|
699
|
+
const listeners = [];
|
|
700
|
+
const listen = (event, listener) => {
|
|
701
|
+
const dispose = ctx.on?.(event, listener);
|
|
702
|
+
if (dispose !== void 0) listeners.push(dispose);
|
|
703
|
+
};
|
|
704
|
+
listen("loader/patch-context", function(first, second) {
|
|
705
|
+
const nextFn = typeof first === "function" ? first : second;
|
|
706
|
+
const entry = entryOfEvent(this, typeof first === "function" ? void 0 : first);
|
|
707
|
+
if (entry === void 0 || typeof nextFn !== "function" || !isOfficialTitleRow(entry) || handled.has(entry)) return nextFn === void 0 ? void 0 : nextFn();
|
|
708
|
+
handled.add(entry);
|
|
709
|
+
const plan = planFor(ctx, module, entry);
|
|
710
|
+
if (plan !== void 0) {
|
|
711
|
+
seam.yieldSlot();
|
|
712
|
+
reclaim(entry, plan);
|
|
713
|
+
}
|
|
714
|
+
return nextFn();
|
|
715
|
+
});
|
|
716
|
+
listen("loader/partial-dispose", function(first) {
|
|
717
|
+
const entry = entryOfEvent(this, first);
|
|
718
|
+
if (entry === void 0 || !isOfficialTitleRow(entry)) return;
|
|
719
|
+
handled.delete(entry);
|
|
720
|
+
if (seam.claimed) return;
|
|
721
|
+
const plan = planFor(ctx, module, officialTitleEntry(ctx.get("loader", false)));
|
|
722
|
+
if (plan !== void 0) seam.claim(plan);
|
|
723
|
+
});
|
|
724
|
+
const loader = ctx.get("loader", false);
|
|
725
|
+
if (typeof loader?.entries === "function") for (const candidate of loader.entries()) {
|
|
726
|
+
if (!isOfficialTitleRow(candidate)) continue;
|
|
727
|
+
if (candidate.disabled === true) continue;
|
|
728
|
+
const plan = planFor(ctx, module, candidate);
|
|
729
|
+
if (plan !== void 0) reclaim(candidate, plan);
|
|
730
|
+
}
|
|
731
|
+
const disposable = () => {
|
|
732
|
+
for (const dispose of listeners.splice(0)) dispose();
|
|
733
|
+
};
|
|
734
|
+
const effect = ctx.effect;
|
|
735
|
+
if (effect === void 0) return;
|
|
736
|
+
const registration = effect(function* () {
|
|
737
|
+
yield disposable;
|
|
738
|
+
}, "model-switch: session-title takeover");
|
|
739
|
+
if (typeof registration === "function") listeners.push(registration);
|
|
740
|
+
}
|
|
520
741
|
/**
|
|
521
742
|
* Mount the Model Switch session-naming row for this profile.
|
|
522
743
|
*
|
|
@@ -529,11 +750,7 @@ function currentSettings(ctx) {
|
|
|
529
750
|
*/
|
|
530
751
|
async function apply(ctx) {
|
|
531
752
|
const entry = officialTitleEntry(ctx.get("loader", false));
|
|
532
|
-
const cadence = titleCadence(entry === void 0 ? void 0 : officialTitlePackage(entry)) ?? TITLE_CADENCES[0];
|
|
533
|
-
if (cadence === void 0) return;
|
|
534
753
|
if (entry === void 0) ctx.logger.info("model-switch: no instantiated official session-title row to mirror; naming uses the first-prompt cadence");
|
|
535
|
-
/** The displaced row's naming policy, or the shipped defaults when it is absent. */
|
|
536
|
-
const forwarded = entry === void 0 ? {} : forwardedConfig(entry);
|
|
537
754
|
let module;
|
|
538
755
|
try {
|
|
539
756
|
module = await import(__rewriteRelativeImportExtension(TITLE_LLM_PACKAGE));
|
|
@@ -542,29 +759,11 @@ async function apply(ctx) {
|
|
|
542
759
|
ctx.logger.error(cause);
|
|
543
760
|
return;
|
|
544
761
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
ctx.logger.error(cause);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
const plan = {
|
|
554
|
-
cadence,
|
|
555
|
-
config: policy.config,
|
|
556
|
-
targets: policy.targets
|
|
557
|
-
};
|
|
558
|
-
try {
|
|
559
|
-
ctx.sessionTitle.register({
|
|
560
|
-
id: cadence.id,
|
|
561
|
-
automatic: cadence.automatic,
|
|
562
|
-
generate: (request) => generateTitle(ctx, module, plan, request)
|
|
563
|
-
});
|
|
564
|
-
} catch (cause) {
|
|
565
|
-
ctx.logger.error("model-switch: another session-title provider is already registered; the configurable naming route is unavailable");
|
|
566
|
-
ctx.logger.error(cause);
|
|
567
|
-
}
|
|
762
|
+
const plan = planFor(ctx, module, entry);
|
|
763
|
+
if (plan === void 0) return;
|
|
764
|
+
const seam = new NamingSeam(ctx, module, plan);
|
|
765
|
+
if (!seam.claim()) ctx.logger.info("model-switch: another session-title provider already owns the seam; naming follows it while it is active");
|
|
766
|
+
installTakeover(ctx, module, seam);
|
|
568
767
|
}
|
|
569
768
|
//#endregion
|
|
570
|
-
export { TITLE_CADENCES, apply, composeTitleRoute, forwardedConfig, frameTitleInput, inject, isMaxTokensFailure, isModernTitleModule, name, normalizeTitleText, officialTitleEntry, officialTitlePackage, resolveNamingPolicy, selectTitleMessages, selectTitleReasoningEffort, shouldTryNextCandidate, splitTitlePolicy, titleCadence, titleFromOutput, titleFromResponse, titleRouteCandidates, titleSystemPrompt, widerTitleBudget };
|
|
769
|
+
export { TAKEOVER_MAX_ATTEMPTS, TAKEOVER_RETRY_DELAY_MS, TITLE_CADENCES, apply, composeTitleRoute, forwardedConfig, frameTitleInput, inject, isMaxTokensFailure, isModernTitleModule, isOfficialTitleRow, name, normalizeTitleText, officialTitleEntry, officialTitlePackage, resolveNamingPolicy, selectTitleMessages, selectTitleReasoningEffort, shouldTryNextCandidate, splitTitlePolicy, titleCadence, titleFromOutput, titleFromResponse, titleRouteCandidates, titleSystemPrompt, widerTitleBudget };
|
package/lib/subagent-runtime.js
CHANGED
|
@@ -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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
78
|
-
assertPublicMethod(
|
|
79
|
-
|
|
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
|
|
226
|
+
return publicMethod(OfficialSubagentRuntime.prototype, "OfficialSubagentRuntime", "start").call(this, name, this.routed(name, request));
|
|
203
227
|
}
|
|
204
228
|
startContinuable(spec) {
|
|
205
|
-
return
|
|
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
|
-
/**
|
|
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
|
*
|
|
@@ -138,9 +138,14 @@ export declare function composeTitleRoute(config: TitleLlmPolicy, route: Capabil
|
|
|
138
138
|
export declare function selectTitleMessages(messages: readonly SessionTitleUserMessage[], firstPromptOnly: boolean): readonly SessionTitleUserMessage[];
|
|
139
139
|
/**
|
|
140
140
|
* Locate the official session-title provider row in the loader tree.
|
|
141
|
+
*
|
|
141
142
|
* Disabled entries stay in the tree, so a name-matched disable still matches.
|
|
143
|
+
* The all-prompts row wins when it is present: that row only exists while the
|
|
144
|
+
* experimental session-titles bundle is composed, and its presence is exactly
|
|
145
|
+
* the user's choice to update titles with every prompt. The default
|
|
146
|
+
* first-prompt row is the fallback when that bundle is off.
|
|
142
147
|
* @param loader - the host loader, when one is reachable.
|
|
143
|
-
* @returns The official entry, or undefined when this profile has no such row.
|
|
148
|
+
* @returns The official entry to mirror, or undefined when this profile has no such row.
|
|
144
149
|
*/
|
|
145
150
|
export declare function officialTitleEntry(loader: Loader | undefined): Entry | undefined;
|
|
146
151
|
/**
|
|
@@ -270,6 +275,12 @@ export declare function titleSystemPrompt(targets: TitleLengthTargets, hasCurren
|
|
|
270
275
|
* @returns The framed user input.
|
|
271
276
|
*/
|
|
272
277
|
export declare function frameTitleInput(messages: readonly SessionTitleUserMessage[], currentTitle: string | undefined): string;
|
|
278
|
+
/** Timing knobs for the takeover choreography (exported for tests). */
|
|
279
|
+
export declare const TAKEOVER_RETRY_DELAY_MS = 10;
|
|
280
|
+
/** How many times the takeover re-checks after disabling the official row. */
|
|
281
|
+
export declare const TAKEOVER_MAX_ATTEMPTS = 20;
|
|
282
|
+
/** Whether one loader entry declares an official naming provider this row supersedes. */
|
|
283
|
+
export declare function isOfficialTitleRow(entry: Entry | undefined): boolean;
|
|
273
284
|
/**
|
|
274
285
|
* Mount the Model Switch session-naming row for this profile.
|
|
275
286
|
*
|
|
@@ -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;
|