dsh-llm-fallbacks 0.3.5 → 0.4.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/README.zh-CN.md +23 -1
- package/dist/authorized-route.d.ts +68 -0
- package/dist/client/ConversationFallbackSwitch.d.ts +18 -14
- package/dist/client/fallbacks-store.d.ts +120 -18
- package/dist/client/index.d.ts +9 -8
- package/dist/client/index.js +170 -39
- package/dist/client/locales.d.ts +14 -0
- package/dist/gateway.d.ts +79 -3
- package/dist/index.d.ts +24 -4
- package/dist/index.js +341 -39
- package/dist/override.d.ts +39 -0
- package/dist/route-allowlist.d.ts +43 -0
- package/dist/state.d.ts +40 -0
- package/dist/subagent-policy.d.ts +123 -0
- package/dist/virtual-adapter.d.ts +49 -0
- package/package.json +25 -21
- package/scripts/build-client.ts +10 -6
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|

|
|
10
|
-

|
|
11
11
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
12
12
|
|
|
13
13
|
Automatic provider/model fallback chains for dsh (DeepSeek Harness): when an agent's LLM requests keep failing — retries exhausted, auth errors, quota exceeded, rate limiting (429) — the plugin switches provider/model along the fallback chain for the current role, and the current step/turn continues on the target model: tasks are not interrupted by model problems.
|
|
@@ -131,6 +131,7 @@ Save the config and restart the session, then type `/fallbacks` — the read-onl
|
|
|
131
131
|
- **Time slots**: optional `fallbacks.timeSlots` rows rotate the effective root chain by wall-clock windows in the config-level `tz` timezone (default `Asia/Shanghai`) — four frozen UTC+8 presets (`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`, windows are code constants, models-only edits) or custom `start`/`end`/`days` windows. The first matching row wins; the all-day row is always last. A slot change applies on the **next** root request and is logged as a **time-slot switch** — a routing seed, never a failure decision: it consumes no cooldown and does not count against `maxSwitchesPerStep`. Failure walks keep the **fallback switch** copy (see [Time-slot presets](#time-slot-presets)).
|
|
132
132
|
- **Dispatch-time role resolution**: on a subagent's first request its role is resolved in three stages — explicit (`agentPreset` matches a declared role id) → deterministic rules (unchanged) → LLM auto-match from the declared role taxonomy (`fallbacks.roleAutoMatch`, default `true`). The resolved role's chain-head model is injected into the first request and recorded via an explicit `role → model` log line (no durable `fallbacks/switch` event is written — issue #52 stop-write); set `roleAutoMatch: false` to disable the LLM auto-match stage (the explicit `agentPreset` stage still applies — with no explicit role this reproduces the previous rules-only behavior). The settings card always renders an **Enable role auto-match** switch (default `true`) to toggle it — the schema default applies even to legacy configs that never declared the key.
|
|
133
133
|
- **Cooldown and revert**: failed / switched-away models are not re-selected during cooldown; `revertPolicy: cooldown-expiry` returns to the primary model automatically.
|
|
134
|
+
- **Host subagent model policy (dsh 0.1.2)**: when the host `subagent-model-selection` policy is enabled, its allowlist is a hard constraint on every plugin-originated subagent route — an explicit authorized spawn route stays the chain head (role-inject skipped), inheritance inject heads and failure-switch targets are intersected with the effective allowlist, and an empty intersection skips the inject/switch (warn log + read-only card warning; no out-of-allowlist request is ever sent). A present-but-unreadable policy fails closed. Policy off/absent → inject and failure-switch selection exactly as 0.3.5. Override `reasoningEffort` follows the upstream routeChanged rule on every path (same route → keep; route change → drop unless explicit). See [Host subagent model policy](#host-subagent-model-policy-dsh-012).
|
|
134
135
|
- **Half-open recovery (opt-in)**: `recovery: half-open` makes recovery evidence-driven — an expired cooldown leaves the route half-open for one logged probe instead of restoring the preference; consecutive failures escalate the suppression duration (×2 per failure, capped at 1 h); an observed completion closes the circuit and fully restores the preference. `revertPolicy: 'never'` keeps the mechanism inert; state is session-scoped in-memory (a restart resets). YAML-only — the default `timer` keeps every existing behavior byte-identical (see [docs/configuration.md](docs/configuration.md#recovery-mode-recovery-key)).
|
|
135
136
|
- **Visible behavior**: every switch is recorded in an info-level log line (from/to/role/reason) — no silent model switching. The plugin deliberately writes **no** durable `fallbacks/switch` session events (issue #52: the apply()-time event-type registration was proven ineffective, and a session containing the event refused to load after a dsh restart). Sessions written by older plugin versions that contain such events are repaired by `scripts/repair-fallbacks-switch-logs.ts`, which marks legacy events ignorable so affected sessions load again.
|
|
136
137
|
- **Safety valves**: `maxSwitchesPerStep` caps switches per step and `alwaysModeRetryCap` caps always-mode retries — chain loops cannot amplify latency.
|
|
@@ -181,6 +182,27 @@ The plugin ships **7 bundled generic subagent roles** out of the box — `design
|
|
|
181
182
|
- **Switch**: `fallbacks.presets` — `'bundled'` (default) declares the preset roles on apply; `'none'` disables the automatic declaration (already-materialized rows stay).
|
|
182
183
|
- Full semantics (upgrade behavior, conflict handling, library reuse of `presetRoles`) → [docs/configuration.md](docs/configuration.md).
|
|
183
184
|
|
|
185
|
+
## Host subagent model policy (dsh 0.1.2)
|
|
186
|
+
|
|
187
|
+
dsh 0.1.2 adds host-side child-model selection for subagents: a `subagent-model-selection` settings allowlist, a per-session `subagent/model-selection-policy` event, and spawn-time `provider/model/reasoning_effort` routing. The plugin reconciles with it under one runtime arbiter — plugin roles/chains remain the failure-recovery layer the host does not provide.
|
|
188
|
+
|
|
189
|
+
**Policy read (per session)**: the session `subagent/model-selection-policy` event wins; otherwise the `subagent-model-selection` settings service when `enabled`. Missing service / `enabled: false` / no event → policy off.
|
|
190
|
+
|
|
191
|
+
When the policy is **enabled**:
|
|
192
|
+
|
|
193
|
+
- **Authorized head preserved (skip inject)**: a subagent spawned with an explicit `provider`+`model` (spawn options, durable `request/header`, or a `model/selection` selection) keeps that route as its chain head — role-inject is skipped and the authorized route is never overwritten at the first request; the plugin chain applies only from failure time onward. Pure inheritance (no explicit selection fields) is **not** an authorized route.
|
|
194
|
+
- **Allowlist-constrained inject**: pure inheritance still resolves a role in the three stages above, but the injected chain head is plugin-originated and must be on the effective allowlist — the first resolved in-allowlist candidate wins. Empty intersection → inject is skipped and the host seed stands (warn log).
|
|
195
|
+
- **Allowlist-constrained failure switching**: after a triggering failure, the resolved candidates (wildcards already expanded; cooldown / step-failed / same-as-current filters already applied) are intersected with the allowlist in walk order. Empty intersection → **no switch**, no out-of-allowlist request, a warn log plus an in-memory blocked-attempt record (no durable session write — issue #52 stands).
|
|
196
|
+
- **Fail-closed**: a present-but-malformed policy event, or enabled settings with an unreadable route list, disables plugin-originated injects and switches for that session (warn log); the host seed and non-switch behavior are untouched.
|
|
197
|
+
- **Card status area (read-only)**: the Fallbacks card Subagents section shows the effective allowlist, the effective chain head with its source (`authorized` / `injected`), and an empty-intersection warning when a switch was blocked — the same numbers the runtime uses, never a second write-face.
|
|
198
|
+
- **Virtual-route carve-out**: a subagent explicitly spawned on the plugin's virtual provider (`FallbacksChain` / `Auto`) is user-authorized for that route (spec D2 — explicit selection). The virtual adapter's internal delegation of its requests to the configured effective chain head is that route's documented purpose and is outside D1's selection semantics — D1 governs provider/model selection the plugin makes, not delegation inside an explicitly chosen virtual route — so the delegated head is not allowlist-filtered.
|
|
199
|
+
|
|
200
|
+
When the policy is **disabled or absent**, inject and failure-switch **selection** are unchanged from 0.3.5 (no allowlist filter, no authorized-route skip).
|
|
201
|
+
|
|
202
|
+
**Effort rule (policy-independent)**: `reasoningEffort` on every override path (role-inject, failure switch, always-cap switch, slot/picker overrides) follows the upstream 0.1.2 `routeChanged` rule — an unchanged provider+model route keeps the seed effort; a route change drops it unless an effort is explicitly named for the override (explicit always survives). A stale effort is never carried into a different provider.
|
|
203
|
+
|
|
204
|
+
Full semantics → [docs/configuration.md](docs/configuration.md#host-subagent-model-selection-dsh-012).
|
|
205
|
+
|
|
184
206
|
## Mount-only (no dsh modification)
|
|
185
207
|
|
|
186
208
|
The plugin installs as a **pure mount**: bundle insert + client inject + its own gateway channel (`/api/fallbacks/get|set|reset`) — no dsh patches, no postinstall step, and dsh upgrades never require re-patching. Stale leftover patches from an older patched install are harmless.
|
package/README.zh-CN.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|

|
|
10
|
-

|
|
11
11
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
12
12
|
|
|
13
13
|
dsh(DeepSeek Harness)的自动模型降级插件:当 root agent 或 subagent 的模型请求持续失败(重试耗尽、权限、配额超限、限流 429)时,按角色/模型 fallback 链自动切换 provider/model,当前 step/turn 在目标模型上继续完成——任务不因模型问题中断。
|
|
@@ -131,6 +131,7 @@ fallbacks:
|
|
|
131
131
|
- **峰谷无忧(分时切换)**:可选的 `fallbacks.timeSlots` 行按墙钟窗口(配置级 `tz` 时区,默认 `Asia/Shanghai`)轮换 root 生效链——四个冻结的 UTC+8 预设(`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`,窗口为代码常量、仅模型链可编辑),或自定义 `start`/`end`/`days` 窗口。第一条命中的行生效;全时段行固定最后。时段切换在**下一个** root 请求生效,日志记为**分时切换**——路由种子而非失败决策:不消耗冷却、不计入 `maxSwitchesPerStep`。失败降级保留**降级切换**文案(见 [分时槽预设(分时切换)](#分时槽预设分时切换))。
|
|
132
132
|
- **派发时角色解析**:在 subagent 的首次请求上,其角色按三个阶段解析——显式(`agentPreset` 匹配已声明角色 id)→ 确定性规则(不变)→ LLM 自动匹配(从已声明角色体系中选择,`fallbacks.roleAutoMatch` 默认 `true`)。解析出的角色的链头模型注入首次请求,并以显式 `role → model` 日志行记录(不写 durable `fallbacks/switch` 事件——issue #52 停写);设 `roleAutoMatch: false` 仅关闭 LLM 自动匹配阶段(显式 `agentPreset` 阶段仍生效——无显式角色时即复现原有仅规则行为)。设置卡总是渲染「启用角色自动匹配」开关(默认 `true`)以切换之——即使是从未声明过该键的旧配置,schema 默认值同样生效。
|
|
133
133
|
- **冷却与回主**:被切离/失败的模型在冷却期内不再入选;`revertPolicy: cooldown-expiry` 冷却到期后自动回主模型。
|
|
134
|
+
- **宿主子代理模型策略(dsh 0.1.2)**:当宿主 `subagent-model-selection` 策略启用时,其允许列表对每个插件发起的 subagent 路由都是硬约束——显式授权的派发路由保持为链头(跳过角色注入),继承注入的链头与失败切换目标都与生效允许列表求交集,交集为空则跳过注入/切换(warn 日志 + 只读卡片警告;绝不发送允许列表之外的请求)。策略存在但不可读时 fail-closed。策略关闭/缺省时,注入与失败切换的选择与 0.3.5 完全一致。覆盖路径上的 `reasoningEffort` 遵循上游 routeChanged 规则(同路由 → 保留;跨路由 → 除非显式指定否则丢弃)。见 [宿主子代理模型策略](#宿主子代理模型策略dsh-012)。
|
|
134
135
|
- **半开恢复(可选)**:`recovery: half-open` 让恢复以证据驱动——冷却到期后路由进入 **half-open**,以一次记录探针(logged probe)放行,而不是直接恢复首选;连续失败使抑制时长按 **×2** 逐次升级、**1 小时**封顶;观察到完成即闭合回路、完全恢复首选。`revertPolicy: 'never'` 使该机制完全失效;状态为会话级内存态(重启即重置)。仅 YAML 配置——默认 `timer` 保持所有既有行为逐字节一致(见 [docs/configuration.md](docs/configuration.md#recovery-mode-recovery-key))。
|
|
135
136
|
- **行为可见**:每次切换以 info 级日志行(from/to/role/reason)记录——无静默换模型。插件**刻意不写** durable `fallbacks/switch` 会话事件(issue #52——apply() 时的事件类型注册被证伪无效,含该事件的会话在 dsh 重启后拒绝加载)。由旧版插件写入、含此类事件的会话由 `scripts/repair-fallbacks-switch-logs.ts` 修复——旧事件被标记 ignorable 后,受影响会话可重新加载。
|
|
136
137
|
- **安全阀**:`maxSwitchesPerStep` 限制每 step 切换次数、`alwaysModeRetryCap` 限制 always 模式重试——链循环不会放大延迟。
|
|
@@ -181,6 +182,27 @@ fallbacks:
|
|
|
181
182
|
- **开关**:`fallbacks.presets`——`'bundled'`(默认)在 apply 时声明预设角色;`'none'` 关闭自动声明(已物化行保留)。
|
|
182
183
|
- 完整语义(升级行为、冲突处理、`presetRoles` 库复用)→ [docs/configuration.md](docs/configuration.md)。
|
|
183
184
|
|
|
185
|
+
## 宿主子代理模型策略(dsh 0.1.2)
|
|
186
|
+
|
|
187
|
+
dsh 0.1.2 为 subagent 增加了宿主侧的子模型选择:`subagent-model-selection` 设置允许列表、每会话的 `subagent/model-selection-policy` 事件、派发时的 `provider/model/reasoning_effort` 路由。插件在单一运行时仲裁点下与之协调——插件的角色/链仍是宿主不提供的失败恢复层。
|
|
188
|
+
|
|
189
|
+
**策略读取(按会话)**:会话 `subagent/model-selection-policy` 事件优先;否则读取 `subagent-model-selection` 设置服务(`enabled` 时)。服务缺失 / `enabled: false` / 无事件 → 策略关闭。
|
|
190
|
+
|
|
191
|
+
策略**启用**时:
|
|
192
|
+
|
|
193
|
+
- **授权链头保留(跳过注入)**:以显式 `provider`+`model` 派发的 subagent(派发 options、durable `request/header`、或 `model/selection` 选择)将该路由保持为链头——跳过角色注入,授权路由在首次请求上绝不被覆盖;插件链仅从失败时刻起生效。纯继承(无显式选择字段)**不是**授权路由。
|
|
194
|
+
- **允许列表约束注入**:纯继承仍走三阶段角色解析,但注入的链头由插件发起、必须在生效允许列表内——按序取第一个已解析且在列表内的候选。交集为空 → 跳过注入,保留宿主种子路由(warn 日志)。
|
|
195
|
+
- **允许列表约束失败切换**:触发码失败后,已解析候选(通配符已展开;冷却 / 本步已失败 / 与当前相同等过滤已应用)按走链顺序与允许列表求交集。交集为空 → **不切换**、不发送允许列表之外的请求,记 warn 日志 + 内存态 blocked-attempt 记录(不写 durable 会话事件——issue #52 不变)。
|
|
196
|
+
- **Fail-closed**:策略事件存在但载荷畸形,或设置启用但路由列表不可读 → 该会话的插件注入与切换全部禁用(warn 日志);宿主种子与非切换行为不受影响。
|
|
197
|
+
- **卡片状态区(只读)**:Fallbacks 卡片的子代理区块显示生效允许列表、生效链头及其来源(`宿主授权` / `插件注入`),以及切换被阻止时的空交集警告——与运行时使用的是同一份数据,绝不是第二个写入面。
|
|
198
|
+
- **虚拟路由例外**:显式指定插件虚拟 provider(`FallbacksChain` / `Auto`)派发的 subagent,即为该路由的用户授权(spec D2——显式选择)。虚拟 adapter 将其请求内部委托给配置的生效链头,是该路由的既定用途,不属于 D1 的选择语义——D1 约束的是插件做出的 provider/model 选择,而不是显式所选虚拟路由内部的委托——被委托的链头不经允许列表过滤。
|
|
199
|
+
|
|
200
|
+
策略**关闭或缺省**时,注入与失败切换的**选择**与 0.3.5 一致(无允许列表过滤、无授权路由跳过)。
|
|
201
|
+
|
|
202
|
+
**Effort 规则(与策略无关)**:所有覆盖路径(角色注入、失败切换、always 上限切换、分时/选择器覆盖)上的 `reasoningEffort` 遵循上游 0.1.2 的 `routeChanged` 规则——provider+model 路由不变则保留种子 effort;路由变化且未显式指定 effort 则丢弃(显式指定的 effort 永远保留)。陈旧的 effort 绝不会被带入另一个 provider。
|
|
203
|
+
|
|
204
|
+
完整语义 → [docs/configuration.md](docs/configuration.md#host-subagent-model-selection-dsh-012)。
|
|
205
|
+
|
|
184
206
|
## 纯挂载(零 dsh 修改)
|
|
185
207
|
|
|
186
208
|
插件以**纯挂载**方式安装:bundle 行插入 + client inject + 自有 gateway 通道(`/api/fallbacks/get|set|reset`)——无 dsh 补丁、无 postinstall 步骤,dsh 升级永不需重打。旧版打补丁安装遗留的补丁无害。
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorized-route detection from durable child state (plan
|
|
3
|
+
* dsh-012-subagent-routing T2; spec D2).
|
|
4
|
+
*
|
|
5
|
+
* Pure, host-free module: a plain-data session view in, one route out. The
|
|
6
|
+
* wiring captures the view at a subagent's first request from the detection
|
|
7
|
+
* sources the plan names (shapes verified against upstream
|
|
8
|
+
* `packages/subagent/subagent/src/child-agent.ts:68-85` and
|
|
9
|
+
* `packages/api/session-controller/src/types.ts:284`):
|
|
10
|
+
*
|
|
11
|
+
* - the session's `model/selection` log entries (plain descriptors; payload
|
|
12
|
+
* is the `ModelSelection` shape — provider, model, optional
|
|
13
|
+
* reasoningEffort), an explicit host-recorded selection for this session;
|
|
14
|
+
* - the durable `request/header` config fold (`requestHeader()?.config`);
|
|
15
|
+
* - the resolved creation options (`agent.options`) as the pre-first-request
|
|
16
|
+
* fallback — upstream precedence: the durable header wins, options
|
|
17
|
+
* fallback (`parentAgentOptionsForDelegation`);
|
|
18
|
+
* - the pure-inheritance baseline: the delegating parent's current route,
|
|
19
|
+
* resolved by the wiring the same way (`requestHeader()?.config` else
|
|
20
|
+
* creation options).
|
|
21
|
+
*
|
|
22
|
+
* A source yields a route only when BOTH provider and model are present
|
|
23
|
+
* non-empty strings — an effort alone is not a route (spec Terms). A
|
|
24
|
+
* `model/selection` entry is definitionally an explicit selection, so it is
|
|
25
|
+
* authorized as-is. A route read from the header or the creation options is
|
|
26
|
+
* explicit only when it is NOT the pure-inheritance continuation: when the
|
|
27
|
+
* baseline is provable (complete) and equals the candidate exactly, the
|
|
28
|
+
* candidate is the parent's inherited route, not a selection — pure
|
|
29
|
+
* inheritance is NOT an authorized route (spec Terms; upstream
|
|
30
|
+
* `resolveChildAgentOptions` materializes the parent route into child
|
|
31
|
+
* options even without an explicit selection, so equality with the baseline
|
|
32
|
+
* is the only child-side inheritance signal). With the baseline unprovable
|
|
33
|
+
* the candidate stands: D2 keeps authorized chain heads unoverwritten, and
|
|
34
|
+
* the allowlist-constrained inject below this decision holds D1 either way.
|
|
35
|
+
*
|
|
36
|
+
* Never throws; malformed source shapes yield `undefined`, not routes.
|
|
37
|
+
*
|
|
38
|
+
* @module dsh-llm-fallbacks/authorized-route
|
|
39
|
+
*/
|
|
40
|
+
import type { PolicyRoute } from './subagent-policy.ts';
|
|
41
|
+
/** One plain route-bearing source shape (header config, options, selection payload, baseline). */
|
|
42
|
+
export type RouteSource = {
|
|
43
|
+
readonly provider?: unknown;
|
|
44
|
+
readonly model?: unknown;
|
|
45
|
+
readonly reasoningEffort?: unknown;
|
|
46
|
+
};
|
|
47
|
+
/** Structural session view consumed by {@link detectAuthorizedRoute} — plain data only, no live host objects. */
|
|
48
|
+
export interface AuthorizedRouteSession {
|
|
49
|
+
/** Durable request-header config fold captured from the child session (absent before the first logged header). */
|
|
50
|
+
readonly requestHeader?: RouteSource | undefined;
|
|
51
|
+
/** Plain event descriptors (`type` + optional `data`) captured from the child session's log. */
|
|
52
|
+
readonly events?: readonly {
|
|
53
|
+
readonly type: string;
|
|
54
|
+
readonly data?: unknown;
|
|
55
|
+
}[] | undefined;
|
|
56
|
+
/** Resolved creation options captured from the agent (pre-first-request fallback). */
|
|
57
|
+
readonly options?: RouteSource | undefined;
|
|
58
|
+
/** Pure-inheritance baseline: the delegating parent's current route (absent when unprovable). */
|
|
59
|
+
readonly inherited?: RouteSource | undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Detect the subagent's explicit authorized route at its first request, or
|
|
63
|
+
* `undefined` for pure inheritance (semantics in the module doc). The wiring
|
|
64
|
+
* skips role-inject on a defined result — the authorized route is the chain
|
|
65
|
+
* head (spec D2) — and runs the allowlist-constrained three-stage inject on
|
|
66
|
+
* `undefined`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function detectAuthorizedRoute(session: AuthorizedRouteSession): PolicyRoute | undefined;
|
|
@@ -10,20 +10,24 @@
|
|
|
10
10
|
* so the `unknown-surface` fallback never picked it up and the transcript
|
|
11
11
|
* showed nothing).
|
|
12
12
|
*
|
|
13
|
-
* Contract notes (dsh-private, verified 2026-08-
|
|
14
|
-
* - D1 registry: `ConversationEventRegistry.register(definition)` —
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Contract notes (dsh-private, 0.1.2 homes, verified 2026-08-28):
|
|
14
|
+
* - D1 registry: `ConversationEventRegistry.register(definition)` — the
|
|
15
|
+
* client Context's `uiConversation.events` service seat
|
|
16
|
+
* (`ui-conversation conversation/event-registry.ts:17-25`); external
|
|
17
|
+
* registration precedent `ui-workflow-run/src/client/index.ts:22-26`
|
|
18
|
+
* (its inject list declares `uiConversation` for the same direct read).
|
|
17
19
|
* The engine feeds EVERY session event to each definition's `match`
|
|
18
|
-
* (`
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* (`ui-conversation conversation/assembler.ts:406-414`) — non-surface
|
|
21
|
+
* plugin events included — and live events reach the engine through the
|
|
22
|
+
* assembly window pump (`ui-conversation conversation/assembly.ts:97-105`
|
|
23
|
+
* `assembler.append`).
|
|
21
24
|
* - D2 seat: `conversation.chat.node` is a keyed seat dispatched by
|
|
22
|
-
* `ChatConversationViewNode.kind` (`ui-
|
|
23
|
-
* `chat/ChatNodeSeat.tsx:
|
|
24
|
-
* `{ name, key, locale }` (precedents: ui-tool `tool-call
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
25
|
+
* `ChatConversationViewNode.kind` (`ui-chat contract/slots.ts:171-180`;
|
|
26
|
+
* `chat/ChatNodeSeat.tsx:212` renderSlot dispatch), externally registrable
|
|
27
|
+
* as `{ name, key, locale }` (precedents: ui-tool `tool-call`
|
|
28
|
+
* `apply.ts:28-31`, ui-goal `command-input` `index.ts:60-63`,
|
|
29
|
+
* ui-workflow-run `workflow-run`).
|
|
30
|
+
* - Purity: this file only type-imports `@deepseek-ai/dsh-client-ui-chat/client`
|
|
27
31
|
* and `@deepseek-ai/dsh-client-ui-conversation/client` (both erased at
|
|
28
32
|
* build); the renderer self-draws on `--dsw-alias-*` tokens. Render-only:
|
|
29
33
|
* the Definition is a pure view contribution — no message construction,
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
*/
|
|
32
36
|
import type { ReactNode } from 'react';
|
|
33
37
|
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
34
|
-
import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-
|
|
38
|
+
import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
35
39
|
import type { FallbackSwitchReason } from '../events.ts';
|
|
36
40
|
/** Final chat payload of one decided fallback switch (snapshot of the event). */
|
|
37
41
|
export interface FallbacksSwitchChatData {
|
|
@@ -53,7 +57,7 @@ export interface FallbacksSwitchChatData {
|
|
|
53
57
|
readonly role: string;
|
|
54
58
|
readonly reason: FallbackSwitchReason;
|
|
55
59
|
}
|
|
56
|
-
declare module '@deepseek-ai/dsh-client-ui-
|
|
60
|
+
declare module '@deepseek-ai/dsh-client-ui-chat/client' {
|
|
57
61
|
interface ChatNodeDataMap {
|
|
58
62
|
/** One decided fallback provider/model switch, rendered at its event seq. */
|
|
59
63
|
'fallbacks-switch': FallbacksSwitchChatData;
|
|
@@ -24,8 +24,11 @@
|
|
|
24
24
|
* message in `state.error` for the section's error banner (KD-G3 — the old
|
|
25
25
|
* `settings-conflict` branch is gone).
|
|
26
26
|
*/
|
|
27
|
-
import type {
|
|
28
|
-
import {
|
|
27
|
+
import type { LlmConfigurableProvider, ModelCatalog, ModelProviderGroup, SessionFollowFrame, SessionFollowRequest, SessionId, SettingsDescribeValue, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client';
|
|
28
|
+
import type { SessionEventLikeEntry } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
29
|
+
import type { ClientConnectionRpc } from '@deepseek-ai/dsh-client-connection/client';
|
|
30
|
+
import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
31
|
+
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
|
|
29
32
|
import { type FallbackStrategy, type FallbacksConfig, type FallbacksRole, type FallbacksRoleRule, type FallbacksRoles } from '../config.ts';
|
|
30
33
|
import type { SlotRowConfig } from '../time-slots.ts';
|
|
31
34
|
import type { FallbacksSwitchEventData } from '../events.ts';
|
|
@@ -105,17 +108,17 @@ export interface FallbacksSettingsState {
|
|
|
105
108
|
catalogStatus: 'idle' | 'loading' | 'ready' | 'error';
|
|
106
109
|
/** Catalog read diagnostic: whole-load failure or per-provider lookups. */
|
|
107
110
|
catalogError: string | null;
|
|
108
|
-
/** Configurable-provider directory (`llm
|
|
109
|
-
providers:
|
|
111
|
+
/** Configurable-provider directory (`llm/listConfigurableProviders`). */
|
|
112
|
+
providers: LlmConfigurableProvider[];
|
|
110
113
|
/**
|
|
111
114
|
* The provider dropdown's offer set: catalog providers whose settings
|
|
112
115
|
* profile resolves, with the Models page's `configured` join semantics
|
|
113
116
|
* (spec §2.5 — see {@link configuredProvidersOf}). Unconfigured directory
|
|
114
117
|
* providers never appear as options.
|
|
115
118
|
*/
|
|
116
|
-
configuredProviders:
|
|
117
|
-
/** Model catalog groups (`
|
|
118
|
-
groups: ModelProviderGroup[];
|
|
119
|
+
configuredProviders: LlmConfigurableProvider[];
|
|
120
|
+
/** Model catalog groups (`session/modelCatalog`). */
|
|
121
|
+
groups: readonly ModelProviderGroup[];
|
|
119
122
|
/** Bumped on every accepted catalog read; drives row re-classification. */
|
|
120
123
|
catalogEpoch: number;
|
|
121
124
|
/** Recent-switch summary (spec §2.4 R-4a / §2.5 D-5). */
|
|
@@ -124,7 +127,48 @@ export interface FallbacksSettingsState {
|
|
|
124
127
|
switchesError: string | null;
|
|
125
128
|
/** Most recent `fallbacks/switch` events of the current session, newest first. */
|
|
126
129
|
switches: FallbacksSwitchSnapshot[];
|
|
130
|
+
/**
|
|
131
|
+
* Host subagent-model-selection policy (spec D4 / T5). `undefined` when
|
|
132
|
+
* the gateway omitted the additive field (old payload / policy off) —
|
|
133
|
+
* the card hides the status area and never renders an active allowlist.
|
|
134
|
+
*/
|
|
135
|
+
subagentPolicy: SubagentPolicyView | undefined;
|
|
127
136
|
}
|
|
137
|
+
/** Source label of the effective subagent chain head (spec D4). */
|
|
138
|
+
export type SubagentPolicyHeadSource = 'authorized' | 'injected';
|
|
139
|
+
/** One exact route on the additive `subagentPolicy` wire field. */
|
|
140
|
+
export type SubagentPolicyRoute = {
|
|
141
|
+
provider: string;
|
|
142
|
+
model: string;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Client-side view of the additive host-policy payload. Structural twin of
|
|
146
|
+
* the gateway wire type — parsed from unknown, never imported from host
|
|
147
|
+
* modules (`src/client/**` consumes policy only via the gateway channel).
|
|
148
|
+
*/
|
|
149
|
+
export type SubagentPolicyView = {
|
|
150
|
+
state: 'enabled';
|
|
151
|
+
allowedModels: SubagentPolicyRoute[];
|
|
152
|
+
head?: {
|
|
153
|
+
route: SubagentPolicyRoute;
|
|
154
|
+
source: SubagentPolicyHeadSource;
|
|
155
|
+
};
|
|
156
|
+
blockedAttempt?: {
|
|
157
|
+
at: number;
|
|
158
|
+
route: SubagentPolicyRoute;
|
|
159
|
+
reason: string;
|
|
160
|
+
};
|
|
161
|
+
} | {
|
|
162
|
+
state: 'disabled';
|
|
163
|
+
} | {
|
|
164
|
+
state: 'unprovable';
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Shape-guard the additive `subagentPolicy` wire field (T5). Malformed
|
|
168
|
+
* payloads resolve to `undefined` (hide the status area) — the store never
|
|
169
|
+
* throws on an old or misshapen field (old-payload tolerance).
|
|
170
|
+
*/
|
|
171
|
+
export declare function parseSubagentPolicyWire(value: unknown): SubagentPolicyView | undefined;
|
|
128
172
|
/**
|
|
129
173
|
* The provider dropdown's offer set (spec §2.5 D-4): catalog providers whose
|
|
130
174
|
* settings profile resolves in the describe namespaces — the Models page's
|
|
@@ -135,7 +179,7 @@ export interface FallbacksSettingsState {
|
|
|
135
179
|
* section still renders existing values for them (read-back + annotation) so
|
|
136
180
|
* nothing is lost on save.
|
|
137
181
|
*/
|
|
138
|
-
export declare function configuredProvidersOf(providers: readonly
|
|
182
|
+
export declare function configuredProvidersOf(providers: readonly LlmConfigurableProvider[], namespaces: ReadonlyMap<string, SettingsNamespaceView>): LlmConfigurableProvider[];
|
|
139
183
|
/**
|
|
140
184
|
* Fold the redacted descriptor value into a complete {@link FallbacksConfig}:
|
|
141
185
|
* missing optional fields take spec §4 defaults; gross type mismatches throw
|
|
@@ -157,7 +201,7 @@ export type CatalogSelection = {
|
|
|
157
201
|
} | null;
|
|
158
202
|
/** The catalog faces row conversions classify raw values against (D-4). */
|
|
159
203
|
export interface CatalogLookup {
|
|
160
|
-
providers: readonly
|
|
204
|
+
providers: readonly LlmConfigurableProvider[];
|
|
161
205
|
groups: readonly ModelProviderGroup[];
|
|
162
206
|
}
|
|
163
207
|
/** The raw selector string a selection serializes to ('' when empty). */
|
|
@@ -175,11 +219,11 @@ export declare function classifyProvider(raw: string, catalog: CatalogLookup | u
|
|
|
175
219
|
export declare function classifyModel(provider: string, raw: string, catalog: CatalogLookup | undefined): CatalogSelection;
|
|
176
220
|
/**
|
|
177
221
|
* Extract the most recent `fallbacks/switch` events from one history page
|
|
178
|
-
* (spec §2.5 D-5): filter by event type, order by
|
|
179
|
-
* most `limit`. Single-page read — fewer than
|
|
180
|
-
* multi-page backfill (Non-Goal).
|
|
222
|
+
* (spec §2.5 D-5): drop packed chunk runs, filter by event type, order by
|
|
223
|
+
* `seq` descending, take at most `limit`. Single-page read — fewer than
|
|
224
|
+
* `limit` events show as-is; no multi-page backfill (Non-Goal).
|
|
181
225
|
*/
|
|
182
|
-
export declare function extractRecentSwitches(entries: readonly
|
|
226
|
+
export declare function extractRecentSwitches(entries: readonly SessionEventLikeEntry[], limit?: number): FallbacksSwitchSnapshot[];
|
|
183
227
|
/**
|
|
184
228
|
* Derive the "current effective model" (spec §2.5 D-6): ① disabled / empty
|
|
185
229
|
* rootChain → unavailable; ② a recent switch exists → the latest one's `to`;
|
|
@@ -309,6 +353,50 @@ export interface RoleRuleRow {
|
|
|
309
353
|
export declare function rulesToRows(rules: readonly FallbacksRoleRule[], catalog?: CatalogLookup): RoleRuleRow[];
|
|
310
354
|
/** Rebuild the role rules from edited rows; empty provider/model drop out. */
|
|
311
355
|
export declare function rowsToRules(rows: readonly RoleRuleRow[]): FallbacksRoleRule[];
|
|
356
|
+
/**
|
|
357
|
+
* The settings Remote namespace face the fallbacks store reads
|
|
358
|
+
* (0.1.2 `ctx.remote.settings`; api-settings-controller `@Remote` methods).
|
|
359
|
+
*/
|
|
360
|
+
export interface FallbacksSettingsRemote {
|
|
361
|
+
/** Redacted describe: provider writability plus the namespace directory. */
|
|
362
|
+
describe(): Promise<RemoteResult<SettingsDescribeValue>>;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The llm Remote namespace face the fallbacks store reads
|
|
366
|
+
* (0.1.2 `ctx.remote.llm`; dsh-llm `@Remote` methods).
|
|
367
|
+
*/
|
|
368
|
+
export interface FallbacksLlmRemote {
|
|
369
|
+
/** Declared configurable-provider directory, registered or dormant. */
|
|
370
|
+
listConfigurableProviders(): Promise<RemoteResult<LlmConfigurableProvider[]>>;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The session Remote namespace face the fallbacks store reads
|
|
374
|
+
* (0.1.2 `ctx.remote.session`; api-session-controller `@Remote` methods).
|
|
375
|
+
*/
|
|
376
|
+
export interface FallbacksSessionRemote {
|
|
377
|
+
/** Host-generation model catalog: provider groups plus per-provider failures. */
|
|
378
|
+
modelCatalog(): Promise<RemoteResult<ModelCatalog>>;
|
|
379
|
+
/**
|
|
380
|
+
* Live event stream for one durable session address; the opening snapshot
|
|
381
|
+
* frame carries the tail page this store reads, then the stream is
|
|
382
|
+
* cancelled (0.1.2 successor of the apiproxy `sessions.history` unary —
|
|
383
|
+
* `session/page` needs the follow opening frame's cursor, so a cold tail
|
|
384
|
+
* read goes through the follow opening).
|
|
385
|
+
*/
|
|
386
|
+
follow(request: SessionFollowRequest, signal?: AbortSignal): AsyncIterable<SessionFollowFrame>;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The generated Remote namespaces the fallbacks store consumes, as one
|
|
390
|
+
* structural face. The mount boundary casts `ctx.remote` once — the same
|
|
391
|
+
* pattern as the session-controller client's `ctx.remote as unknown as
|
|
392
|
+
* SessionRemotes` — because the generated `TypertRemoteNamespaceMap` merge
|
|
393
|
+
* ships with the host's build, not with the linked package types.
|
|
394
|
+
*/
|
|
395
|
+
export interface FallbacksRemote {
|
|
396
|
+
readonly settings: FallbacksSettingsRemote;
|
|
397
|
+
readonly llm: FallbacksLlmRemote;
|
|
398
|
+
readonly session: FallbacksSessionRemote;
|
|
399
|
+
}
|
|
312
400
|
/** Controller joining Settings reads, writes, and pushed invalidations. */
|
|
313
401
|
export declare class FallbacksSettingsController {
|
|
314
402
|
private readonly api;
|
|
@@ -329,12 +417,22 @@ export declare class FallbacksSettingsController {
|
|
|
329
417
|
private namespaces;
|
|
330
418
|
private currentSession;
|
|
331
419
|
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
420
|
+
* The store's dispose signal (qc3 F-003): created alongside the store
|
|
421
|
+
* lifecycle, threaded into every `session.follow` call, aborted once in
|
|
422
|
+
* {@link dispose} — a follow opening still mid-await at plugin unload
|
|
423
|
+
* (HMR / fiber dispose, or a wedged gateway stream) is cancelled promptly
|
|
424
|
+
* instead of staying open until the mount abort or socket failure.
|
|
425
|
+
*/
|
|
426
|
+
private readonly followAbort;
|
|
427
|
+
/**
|
|
428
|
+
* @param api - the Remote namespace faces the store reads (settings
|
|
429
|
+
* describe `writable` + namespace directory, llm provider directory,
|
|
430
|
+
* session model catalog + follow tail page) — `ctx.remote` cast once at
|
|
431
|
+
* the mount boundary.
|
|
334
432
|
* @param rpc - the connection's generic RPC caller for the host gateway
|
|
335
433
|
* channel (`/api`), injected from the connection handle.
|
|
336
434
|
*/
|
|
337
|
-
constructor(api:
|
|
435
|
+
constructor(api: FallbacksRemote, rpc: ClientConnectionRpc);
|
|
338
436
|
/**
|
|
339
437
|
* Refresh the page snapshot. Latest request wins. `settings.describe`
|
|
340
438
|
* still runs — it supplies the top-level `writable` flag (host read-only
|
|
@@ -351,7 +449,8 @@ export declare class FallbacksSettingsController {
|
|
|
351
449
|
*/
|
|
352
450
|
load(): Promise<void>;
|
|
353
451
|
/**
|
|
354
|
-
* Refresh the provider/model catalog (`llm
|
|
452
|
+
* Refresh the provider/model catalog (`llm/listConfigurableProviders` +
|
|
453
|
+
* `session/modelCatalog`), an
|
|
355
454
|
* independent read path with its own generation guard so it can run
|
|
356
455
|
* parallel to {@link load} without clobbering it (spec §2.5 D-4).
|
|
357
456
|
* Per-provider lookup failures ride `catalogError` as a diagnostic without
|
|
@@ -371,7 +470,10 @@ export declare class FallbacksSettingsController {
|
|
|
371
470
|
setCurrentSession(sessionId: SessionId | undefined): void;
|
|
372
471
|
/**
|
|
373
472
|
* Read the recent-switch summary for the current session (spec §2.5 D-5):
|
|
374
|
-
* one
|
|
473
|
+
* one tail page (`maxMessages` = {@link SWITCHES_HISTORY_PAGE}) read from
|
|
474
|
+
* the `session/follow` opening snapshot — 0.1.2 has no unary tail-page
|
|
475
|
+
* read (`session/page` needs the follow opening frame's cursor), so the
|
|
476
|
+
* stream is cancelled right after its first frame —
|
|
375
477
|
* `fallbacks/switch` events extracted newest-first capped at
|
|
376
478
|
* {@link RECENT_SWITCH_LIMIT}. No current session → honest empty ready
|
|
377
479
|
* state (no RPC); a read failure lands `switchesStatus: 'error'` and never
|
package/dist/client/index.d.ts
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* - Registers the `fallbacks` locale dictionaries (zh/en).
|
|
10
10
|
* - Constructs the card's own store over the connection: the fallbacks
|
|
11
11
|
* config rides the plugin's gateway channel (`connection.rpc` →
|
|
12
|
-
* `/api/fallbacks/get|set|reset`), while `settings
|
|
13
|
-
* namespace directory)
|
|
14
|
-
* `
|
|
12
|
+
* `/api/fallbacks/get|set|reset`), while `settings/describe` (writable +
|
|
13
|
+
* namespace directory), the provider/model catalog, and the switch-history
|
|
14
|
+
* tail page ride the typed `ctx.remote` namespaces (0.1.2:
|
|
15
|
+
* `ConnectionHandle` dropped `api`; see `fallbacks-store.ts`).
|
|
15
16
|
* - Registers the `settings.plugin.item` card `key: 'fallbacks'` with a
|
|
16
17
|
* matching `id: 'fallbacks'` (the rc.7 keyed slot — no `order`; the id
|
|
17
18
|
* keeps the card mountable on hosts that still declare the slot as a list,
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
*
|
|
32
33
|
* @module dsh-llm-fallbacks/client
|
|
33
34
|
*/
|
|
34
|
-
import type { ClientContext } from '@deepseek-ai/
|
|
35
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
35
36
|
export type { FallbacksCardInjected, FallbacksCardProps } from './FallbacksCard.tsx';
|
|
36
37
|
export type { GeneralFallbacksRowInjected, GeneralFallbacksRowProps } from './GeneralFallbacksRow.tsx';
|
|
37
38
|
export type { ConversationFallbackSwitchProps, FallbacksSwitchChatData, } from './ConversationFallbackSwitch.tsx';
|
|
@@ -39,12 +40,12 @@ export type { FallbacksSettingsState } from './fallbacks-store.ts';
|
|
|
39
40
|
export { FallbacksSettingsController, FALLBACKS_SETTINGS_NS } from './fallbacks-store.ts';
|
|
40
41
|
/**
|
|
41
42
|
* Required services (cordis fiber inject); registrations wait on the slot
|
|
42
|
-
* declaration. `
|
|
43
|
-
* registration reads the service directly (`ctx.
|
|
43
|
+
* declaration. `uiConversation` is declared because the D1 Definition
|
|
44
|
+
* registration reads the service directly (`ctx.uiConversation.events.register`
|
|
44
45
|
* at the bottom of `apply` — explicit fiber-ordering parity with the
|
|
45
46
|
* ui-workflow-run precedent, whose inject list includes it for the same
|
|
46
|
-
* direct read).
|
|
47
|
-
*
|
|
47
|
+
* direct read). Cordis would still provide the service synchronously on
|
|
48
|
+
* apply, but the declaration makes the dependency honest. `sessions` is
|
|
48
49
|
* deliberately NOT injected (S-g): a non-web host without the dsh-session
|
|
49
50
|
* client service must not hang the fiber waiting for it — the wiring reads
|
|
50
51
|
* it reflectively and degrades to the switches empty state when absent
|