dsh-llm-fallbacks 0.6.13 → 0.7.0
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 +14 -10
- package/README.zh-CN.md +14 -10
- package/dist/chain-walk.d.ts +211 -0
- package/dist/chains.d.ts +28 -6
- package/dist/client/index.js +86 -194
- package/dist/client/locales.d.ts +2 -12
- package/dist/commands.d.ts +3 -10
- package/dist/config.d.ts +4 -16
- package/dist/cooldown.d.ts +1 -15
- package/dist/events.d.ts +8 -8
- package/dist/gateway.d.ts +5 -3
- package/dist/index.d.ts +19 -40
- package/dist/index.js +685 -875
- package/dist/role-resolution.d.ts +12 -51
- package/dist/roles.d.ts +3 -4
- package/dist/state.d.ts +18 -102
- package/dist/subagent-policy.d.ts +22 -8
- package/dist/subagents-seam.d.ts +70 -11
- package/dist/tui-settings.d.ts +4 -2
- package/dist/virtual-adapter.d.ts +42 -46
- package/docs/DETAIL.md +1 -0
- package/docs/DETAIL.zh.md +1 -0
- package/docs/preview.svg +47 -0
- package/docs/preview.zh.svg +47 -0
- package/locale/en.json +2 -1
- package/locale/zh.json +2 -1
- package/package.json +29 -25
- package/dist/authorized-route.d.ts +0 -68
- package/dist/automatch.d.ts +0 -98
- package/dist/override.d.ts +0 -39
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# dsh-llm-fallbacks
|
|
2
4
|
|
|
3
5
|
[English](README.md) | [Chinese](README.zh-CN.md)
|
|
@@ -7,10 +9,10 @@
|
|
|
7
9
|

|
|
8
10
|

|
|
9
11
|

|
|
10
|
-

|
|
11
13
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
12
14
|
|
|
13
|
-
Automatic provider/model fallback chains for dsh (DeepSeek Harness):
|
|
15
|
+
Automatic provider/model fallback chains for dsh (DeepSeek Harness): a session whose model selection routes to the plugin's virtual **`FallbacksChain / Auto`** row walks a configured fallback chain — an LLM request that keeps failing on a listed code (auth, quota, rate limiting 429, …) is retried on the next available provider/model, so tasks are not interrupted by model problems. A session on any **real** provider/model has **no plugin fallback**: `llm-retry` alone handles it. Select `FallbacksChain / Auto` in the model picker to get a chain.
|
|
14
16
|
|
|
15
17
|
Works in both dsh front ends: the **web** profile (Settings → Plugins → Fallbacks card) and the **dsh-tui** terminal profile (`/fallbacks` session diagnostics, `/fallbacks config` readback, and the `/settings` fallbacks section for editing).
|
|
16
18
|
|
|
@@ -18,12 +20,14 @@ Works in both dsh front ends: the **web** profile (Settings → Plugins → Fall
|
|
|
18
20
|
|
|
19
21
|
### Automatic model failover
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
A session routed to `FallbacksChain / Auto` — the root agent, or an inheriting subagent the dispatch seam mounted on that row — walks its chain: a request that fails on a listed code **before emitting any output** is retried on the next available provider/model inside the same request, and the step stays alive. Sessions on a real provider/model get no plugin fallback at all. No manual model switching, and nothing happens until you configure content.
|
|
22
24
|
|
|
23
|
-
- **
|
|
25
|
+
- **Who gets a chain**: only sessions whose model selection routes to `FallbacksChain / Auto`. That is the root agent when the row is selected, and a subagent the seam mounted there at dispatch (local backend, no explicit route on the request). A real provider/model selection — root or subagent — is left entirely to llm-retry.
|
|
26
|
+
- **Triggers**: `triggerCodes` (default `AUTH` / `QUOTA` / `RATE_LIMIT`) — **any** dsh failure code is accepted, including `CONTEXT_WINDOW_EXCEEDED`, which fails over to a larger-context candidate. Retryable failures (5xx, `TRANSPORT`, `TIMEOUT`, …) stay with llm-retry first; each of its retries re-enters the walk, which starts past the head that just went on cooldown.
|
|
27
|
+
- **Partial output is committed**: once an attempt has emitted a chunk, a later failure is **not** switched inside the request — yielded output cannot be unsent. The failure surfaces to the loop; recovery is llm-retry re-entry (a fresh walk) or the terminal error.
|
|
24
28
|
- **Recovery**: a failed / switched-away model is held out of selection for `cooldownMs`, and `revertPolicy: cooldown-expiry` returns to the primary model on expiry. The opt-in `recovery: half-open` mode (YAML-only) makes that return evidence-driven instead of timer-based.
|
|
25
|
-
- **Safety
|
|
26
|
-
- **Visibility**: every switch is recorded in an info-level log line (from/to/role/reason) — no silent model switching.
|
|
29
|
+
- **Safety valve**: `maxSwitchesPerStep` caps switches per request walk (attempts ≤ cap + 1) — chain loops cannot amplify latency.
|
|
30
|
+
- **Visibility**: every switch is recorded in an info-level log line (from/to/role/reason/candidates) — no silent model switching.
|
|
27
31
|
|
|
28
32
|
→ [Automatic model failover](docs/features.md#automatic-model-failover) · [Context-window-aware fallback](docs/features.md#context-window-aware-fallback)
|
|
29
33
|
|
|
@@ -50,7 +54,7 @@ GLM Peak and GLM Valley are offered in the card picker only when `zai-coding-cn`
|
|
|
50
54
|
|
|
51
55
|
A subagent's role is declared in its dispatch prompt — the Assignment header `**Execute as**: <id>` — and it is resolved against the role ids declared in `roles.list`. Only that **declared header** delivers the child's identity: the role's `persona` is installed on the child as a system-prompt section, the child's own session gets exactly one `[role: <id>]` notice row, and its session header shows a role badge.
|
|
52
56
|
|
|
53
|
-
Role **routing** is resolved separately, in
|
|
57
|
+
Role **routing** is resolved separately, in three ordered stages, first hit winning — explicit `agentPreset` → **dispatch-declared** `**Execute as**: <id>` header → rules. If the child's dispatch prompt declares `**Execute as**: <id>`, that role's persona, `[role: <id>]` notice and badge are delivered **regardless of which routing stage wins** — an earlier explicit `agentPreset` can route the child as another role while the identity carriers stay the declared header's. A child with **no declared dispatch header** gets no persona, no notice row and no role half of the badge (the model half still renders when the child has a recorded route), even when rules resolve a role for routing. Routing also decides whether the child gets a fallback chain: at dispatch the seam mounts an inheriting child on the virtual `FallbacksChain / Auto` row — local backends only, and only when the request carries no explicit `provider`/`model` route; a request that does carry one is **authorized** and never rewritten (card label `authorized`, a mounted child is labelled `injected`). A mounted child's candidates are intersected with the host allowlist when `subagent-model-selection` is enabled, and an external-backend child keeps pure inheritance — which means no chain ([host policy](docs/features.md#host-subagent-model-selection-policy-dsh-012)).
|
|
54
58
|
|
|
55
59
|
This covers an **ordinary subagent dispatch and an Agent Team teammate** (`spawn_teammate`) alike: both are children dispatched with the same header, which the model writes — the same dispatch seam, no mechanism-specific configuration.
|
|
56
60
|
|
|
@@ -120,11 +124,11 @@ Save the config and restart the session, then type `/fallbacks` — the read-onl
|
|
|
120
124
|
|
|
121
125
|
## Feature highlights
|
|
122
126
|
|
|
123
|
-
- **Automatic failover**
|
|
127
|
+
- **Automatic failover** on the virtual route — a `FallbacksChain / Auto` session (root or seam-mounted subagent) walks its chain on a listed failure code and the step continues; real-model sessions have no plugin fallback. → [details](docs/features.md#automatic-model-failover)
|
|
124
128
|
- **Context-window-aware fallback** — `CONTEXT_WINDOW_EXCEEDED` fails over to a larger-context candidate; the switch is request-scoped (no cooldown). → [details](docs/features.md#context-window-aware-fallback)
|
|
125
129
|
- **Peak/valley time slots** — four frozen UTC+8 presets or custom windows rotate the effective root chain. → [details](docs/features.md#time-slots)
|
|
126
|
-
- **Role-based subagents** — a dispatch-declared role brings persona, notice row and badge to the child;
|
|
127
|
-
- **FallbacksChain in the picker** — select `FallbacksChain / Auto` to make the chain the
|
|
130
|
+
- **Role-based subagents** — a dispatch-declared role brings persona, notice row and badge to the child; inheriting children are mounted on the virtual chain route subject to host policy. → [details](docs/features.md#role-based-subagents)
|
|
131
|
+
- **FallbacksChain in the picker** — select `FallbacksChain / Auto` to make the configured chain the session route; any real model is served by llm-retry alone, with no plugin fallback. → [details](docs/features.md#fallbackschain-in-the-model-picker)
|
|
128
132
|
- **Host subagent model policy** — the dsh 0.1.2 allowlist is a hard constraint on every plugin-originated subagent route. → [details](docs/features.md#host-subagent-model-selection-policy-dsh-012)
|
|
129
133
|
- **Five bundled preset roles** — `reviewer` / `scout` / `security-reviewer` / `sonic` / `task`, declared on apply. → [details](docs/features.md#preset-roles)
|
|
130
134
|
- **dsh-tui surfaces** — `/fallbacks`, `/fallbacks config`, and a `/settings` section with web-card parity. → [details](docs/features.md#dsh-tui-surfaces)
|
package/README.zh-CN.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# dsh-llm-fallbacks
|
|
2
4
|
|
|
3
5
|
[English](README.md) | [中文](README.zh-CN.md)
|
|
@@ -7,10 +9,10 @@
|
|
|
7
9
|

|
|
8
10
|

|
|
9
11
|

|
|
10
|
-

|
|
11
13
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
12
14
|
|
|
13
|
-
dsh(DeepSeek Harness
|
|
15
|
+
dsh(DeepSeek Harness)的自动模型降级插件:只有模型选择走插件虚拟行 **`FallbacksChain / Auto`** 的会话才沿配置的降级链行走——llm 请求在列出的失败码上持续失败(权限错误、配额超限、限流 429 等)时,请求会在下一个可用 provider/model 上重试,任务不会因模型问题中断。选择**真实** provider/model 的会话**没有插件降级**,只由 `llm-retry` 处理。想要降级链,请在模型选择器中选择 `FallbacksChain / Auto`。
|
|
14
16
|
|
|
15
17
|
两个 dsh 前端均可用:**web** profile(设置 → 插件配置 → Fallbacks 卡片)与 **dsh-tui** 终端 profile(`/fallbacks` 会话诊断、`/fallbacks config` 回读、`/settings` 的 fallbacks 分区用于编辑)。
|
|
16
18
|
|
|
@@ -18,12 +20,14 @@ dsh(DeepSeek Harness)的自动模型降级插件:当 agent 的模型请求
|
|
|
18
20
|
|
|
19
21
|
### 自动模型降级
|
|
20
22
|
|
|
21
|
-
root
|
|
23
|
+
会话模型选择走 `FallbacksChain / Auto` 时——root agent,或被分发 seam 挂到该行的继承型 subagent——沿自身链行走:请求在**输出任何内容之前**命中触发码,就在同一个请求内改到下一个可用 provider/model,step 继续运行。选择真实 provider/model 的会话不获得任何插件降级。无需手动切换模型;在配置了内容之前,插件不做任何事。
|
|
22
24
|
|
|
23
|
-
-
|
|
25
|
+
- **谁能拿到链**:只有模型选择走 `FallbacksChain / Auto` 的会话。包括选中该行的 root agent,以及分发时被 seam 挂到该行的 subagent(本地 backend、请求上没有显式路由)。真实 provider/model 选择——无论 root 还是 subagent——完全交给 llm-retry。
|
|
26
|
+
- **触发**:`triggerCodes`(默认 `AUTH` / `QUOTA` / `RATE_LIMIT`)——**任何** dsh 失败码都可用,包括 `CONTEXT_WINDOW_EXCEEDED`(会降级到上下文窗口更大的候选)。可重试失败(5xx、`TRANSPORT`、`TIMEOUT` 等)先交给 llm-retry;它的每次重试都会重新进入 walk,并从刚进入冷却的链头之后开始。
|
|
27
|
+
- **已输出即提交**:某个尝试一旦吐出 chunk,之后的失败**不会**在该请求内切换——已产出的输出无法撤回。失败上抛给 loop;恢复路径是 llm-retry 重入(新的一次 walk)或终止失败。
|
|
24
28
|
- **恢复**:失败或被切走的模型在 `cooldownMs` 内不再被选中,`revertPolicy: cooldown-expiry` 在冷却到期后回到主模型;可选的 `recovery: half-open` 模式(YAML-only)把这次回归改为证据驱动,而非定时器驱动。
|
|
25
|
-
- **保险丝**:`maxSwitchesPerStep`
|
|
26
|
-
- **可见性**:每次切换都写入 info 日志(from/to/role/reason),不存在静默切换。
|
|
29
|
+
- **保险丝**:`maxSwitchesPerStep` 限制单次请求 walk 的切换次数(尝试数 ≤ 上限 + 1)——链式循环不会放大延迟。
|
|
30
|
+
- **可见性**:每次切换都写入 info 日志(from/to/role/reason/candidates),不存在静默切换。
|
|
27
31
|
|
|
28
32
|
→ [自动模型降级](docs/features.md#automatic-model-failover) · [上下文窗口降级](docs/features.md#context-window-aware-fallback)
|
|
29
33
|
|
|
@@ -50,7 +54,7 @@ GLM Peak / GLM Valley 仅在配置了 `zai-coding-cn` 时才会出现在卡片
|
|
|
50
54
|
|
|
51
55
|
subagent 的角色由分发提示词声明——Assignment 头部字段 `**Execute as**: <id>`;它与 `roles.list` 中已声明的角色 id 匹配。只有这个**已声明头部**才投递子代理的身份:`persona` 以 system-prompt 分区安装到子代理,子会话恰好写入一行 `[role: <id>]` 通知行,会话头部显示角色徽章。
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
角色**路由**是另一套解析,按三个有序阶段、先命中者生效——显式 `agentPreset` → **派发声明** `**Execute as**: <id>` 头部 → 规则。只要子代理的分发提示词声明了 `**Execute as**: <id>`,该角色的 persona、`[role: <id>]` 通知行与徽章就会投递,**无论哪个路由阶段胜出**——更早的显式 `agentPreset` 可以把子代理路由成另一个角色,而身份载体仍属已声明头部。完全没有声明分发头部的子代理不会获得 persona、通知行与徽章,即使规则为路由解析出了角色。路由同时决定子代理是否拥有降级链:分发时 seam 把继承型子代理挂到虚拟 `FallbacksChain / Auto` 行上——仅限本地 backend,且仅当请求没有显式 `provider`/`model` 路由;带显式路由的请求属于**已授权**,插件永不改写(卡片标签 `authorized`,被挂载的子代理标签 `injected`)。宿主 `subagent-model-selection` 策略启用时,被挂载子代理的候选会与 allowlist 求交;外部 backend 的子代理保持纯继承——也就是没有降级链([宿主策略](docs/features.md#host-subagent-model-selection-policy-dsh-012))。
|
|
54
58
|
|
|
55
59
|
这一套同时覆盖**普通 subagent 分发与 Agent Team 队友**(`spawn_teammate`):两者都是带着同一头部(由模型写入)经同一分发 seam 创建的子代理,不需要任何按机制区分的配置。
|
|
56
60
|
|
|
@@ -120,11 +124,11 @@ fallbacks:
|
|
|
120
124
|
|
|
121
125
|
## 能力一览
|
|
122
126
|
|
|
123
|
-
-
|
|
127
|
+
- **自动降级**(仅虚拟路由)——`FallbacksChain / Auto` 会话(root 或 seam 挂载的 subagent)命中触发码时沿链切换,step 继续运行;真实模型会话没有插件降级。 → [详情](docs/features.md#automatic-model-failover)
|
|
124
128
|
- **上下文窗口感知降级**——`CONTEXT_WINDOW_EXCEEDED` 会切到窗口更大的候选;该切换是请求级的(不消耗冷却)。 → [详情](docs/features.md#context-window-aware-fallback)
|
|
125
129
|
- **峰谷无忧(分时槽)**——四个冻结的 UTC+8 预设或自定义窗口轮换生效 root 链。 → [详情](docs/features.md#time-slots)
|
|
126
|
-
- **角色化 subagent**——派发声明的角色为子代理带来 persona
|
|
127
|
-
- **模型选择器中的 FallbacksChain**——选择 `FallbacksChain / Auto`
|
|
130
|
+
- **角色化 subagent**——派发声明的角色为子代理带来 persona、通知行与徽章;继承型子代理被挂到虚拟链路由,受宿主策略约束。 → [详情](docs/features.md#role-based-subagents)
|
|
131
|
+
- **模型选择器中的 FallbacksChain**——选择 `FallbacksChain / Auto` 即由配置链承载该会话;选择真实模型则只由 llm-retry 处理,没有插件降级。 → [详情](docs/features.md#fallbackschain-in-the-model-picker)
|
|
128
132
|
- **宿主子代理模型策略**——dsh 0.1.2 的 allowlist 是插件发起的所有 subagent 路由的硬约束。 → [详情](docs/features.md#host-subagent-model-selection-policy-dsh-012)
|
|
129
133
|
- **五个内置预设角色**——`reviewer` / `scout` / `security-reviewer` / `sonic` / `task`,随 apply 自动声明。 → [详情](docs/features.md#preset-roles)
|
|
130
134
|
- **dsh-tui 操作面**——`/fallbacks`、`/fallbacks config`,以及与 web 卡片等价的 `/settings` 分区。 → [详情](docs/features.md#dsh-tui-surfaces)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chain walk (design dsh-022-rc1-upgrade §2/§3/§6.1): the fallback
|
|
3
|
+
* decision, executed inside the virtual `FallbacksChain/Auto` adapter's
|
|
4
|
+
* `stream()` instead of on the `agent/request` / `agent/request-error`
|
|
5
|
+
* waterfalls.
|
|
6
|
+
*
|
|
7
|
+
* One walk per `stream()` call: resolve the session's agent view, resolve the
|
|
8
|
+
* role and the chain tail, then attempt the chain's heads in order. A
|
|
9
|
+
* pre-output terminal `finish` whose failure code is a trigger code is
|
|
10
|
+
* swallowed, booked (cooldown / recovery) and the walk advances; anything
|
|
11
|
+
* else — a normal finish, an aborted finish, a non-trigger failure, or any
|
|
12
|
+
* failure after the attempt already yielded a chunk — is forwarded verbatim
|
|
13
|
+
* and ends the walk.
|
|
14
|
+
*
|
|
15
|
+
* The COMMITTED rule (§3.1 / §7.5) is why a post-output failure cannot be
|
|
16
|
+
* replayed: yielded chunks cannot be unsent, so a second provider's output
|
|
17
|
+
* cannot be concatenated behind a partial one. Such a failure is still BOOKED
|
|
18
|
+
* (cooldown / recovery) before it is forwarded — the attempt boundary is where
|
|
19
|
+
* both transactions happen — and then surfaces to the loop, which settles the
|
|
20
|
+
* partial attempt; llm-retry (or the terminal error) takes over from there, and
|
|
21
|
+
* a retry re-enters this walk, which resumes past the cooled head on its own.
|
|
22
|
+
*
|
|
23
|
+
* Bookkeeping survives a walk exactly as far as the state store does: cooldown
|
|
24
|
+
* (`cooldownMs` / `revertPolicy` / half-open escalation) is the cross-call
|
|
25
|
+
* memory keyed by session, so an llm-retry re-entry starts past a failed head;
|
|
26
|
+
* the per-call failed set and the switch cap live only for this walk
|
|
27
|
+
* (`maxSwitchesPerStep` = switches per walk, attempts ≤ cap + 1).
|
|
28
|
+
*
|
|
29
|
+
* The pre-attempt guards (`EMPTY_EFFECTIVE_CHAIN` / `UNDISPATCHABLE_EFFECTIVE_HEAD`
|
|
30
|
+
* / `LLM_UNAVAILABLE`) and the ROOT-only all-day conformance gate are owned
|
|
31
|
+
* here, and `slotWinners` carries the 分时切换 rotation log — all of it needs the
|
|
32
|
+
* session's resolved role/origin, which the adapter does not have.
|
|
33
|
+
*
|
|
34
|
+
* @module dsh-llm-fallbacks/chain-walk
|
|
35
|
+
*/
|
|
36
|
+
import { type GenerateOptions, type LlmRuntime, type StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
37
|
+
import { type FailingModel } from './chains.ts';
|
|
38
|
+
import type { FallbacksConfig } from './config.ts';
|
|
39
|
+
import type { FallbackSwitchReason } from './events.ts';
|
|
40
|
+
import type { FallbackStateStore } from './state.ts';
|
|
41
|
+
import type { EffectivePolicy } from './subagent-policy.ts';
|
|
42
|
+
/**
|
|
43
|
+
* `LlmError` code: the effective chain is empty — the virtual route has no
|
|
44
|
+
* head to walk (the pre-attempt guard, owned here because only the walk knows
|
|
45
|
+
* WHICH chain the calling session walks: a subagent's role chain, not the
|
|
46
|
+
* root's all-day chain).
|
|
47
|
+
*/
|
|
48
|
+
export declare const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
|
|
49
|
+
/**
|
|
50
|
+
* `LlmError` code: the chain head is not a dispatchable real pair — an
|
|
51
|
+
* undispatchable entry (wildcard selector, malformed selector, or a self-route
|
|
52
|
+
* back to `FallbacksChain/*` — the recursion guard), or, for a ROOT walk, a
|
|
53
|
+
* non-conforming all-day `rootChain` (the picker row's conformance gate).
|
|
54
|
+
*/
|
|
55
|
+
export declare const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
|
|
56
|
+
/** `LlmError` code (defensive): the `llm` runtime disappeared mid-flight. */
|
|
57
|
+
export declare const LLM_UNAVAILABLE_CODE = "LLM_UNAVAILABLE";
|
|
58
|
+
/**
|
|
59
|
+
* dsh core's canonical failure code for a request larger than the model's
|
|
60
|
+
* context window (`@deepseek-ai/dsh-llm` `CONTEXT_WINDOW_EXCEEDED_CODE`; the
|
|
61
|
+
* DeepSeek adapter's `httpErrorCode()` and llm-pi-ai's `mapStopReason()` both
|
|
62
|
+
* map the provider 400 "maximum context length" to it). Declared as a literal
|
|
63
|
+
* for the same reason `triggerCodes` is `string[]` — the plugin compares dsh
|
|
64
|
+
* failure codes as plain strings and never imports a peer value.
|
|
65
|
+
*
|
|
66
|
+
* It is NOT in llm-retry's retryable set, so the plugin sees it only when the
|
|
67
|
+
* operator lists it in `triggerCodes` — and once listed, this walk handles the
|
|
68
|
+
* rejection BEFORE the harness's compaction plugin compacts anything
|
|
69
|
+
* (measured 2026-09-09: the first overflow switches to the fallback, no
|
|
70
|
+
* compaction happens). Operators who want compaction tried first leave the
|
|
71
|
+
* code out of `triggerCodes`.
|
|
72
|
+
*
|
|
73
|
+
* A `CONTEXT_WINDOW_EXCEEDED` switch is REQUEST-scoped
|
|
74
|
+
* ({@link REQUEST_SCOPED_CODES}): the route is healthy and the request was too
|
|
75
|
+
* big, so the per-call failed key is recorded but no cooldown and no recovery
|
|
76
|
+
* counter are written.
|
|
77
|
+
*/
|
|
78
|
+
export declare const CONTEXT_WINDOW_EXCEEDED_CODE = "CONTEXT_WINDOW_EXCEEDED";
|
|
79
|
+
/**
|
|
80
|
+
* Failure codes whose switch is REQUEST-scoped: the route is healthy and the
|
|
81
|
+
* request was the problem, so a switch records the per-call failed key but
|
|
82
|
+
* writes no cooldown and no recovery counter. Cooling the route down would
|
|
83
|
+
* send the next SHORT prompt to the fallback for no reason.
|
|
84
|
+
*/
|
|
85
|
+
export declare const REQUEST_SCOPED_CODES: ReadonlySet<string>;
|
|
86
|
+
/**
|
|
87
|
+
* Model-catalog view the wildcard / context-window probes read. `contextWindow`
|
|
88
|
+
* is a host-disclosed extra on the advisory catalog row (the dsh `LlmModelInfo`
|
|
89
|
+
* does not type it), while `resolveModelInfo` is where the host actually
|
|
90
|
+
* carries it — both are optional and read structurally, so a host offering
|
|
91
|
+
* neither reports every window as unknown (never a skip).
|
|
92
|
+
*/
|
|
93
|
+
export interface ModelCatalogLike {
|
|
94
|
+
listModels(provider: string): Promise<readonly {
|
|
95
|
+
id: string;
|
|
96
|
+
contextWindow?: number;
|
|
97
|
+
}[]>;
|
|
98
|
+
resolveModelInfo?(provider: string, model: string): Promise<{
|
|
99
|
+
context?: {
|
|
100
|
+
contextWindow?: number;
|
|
101
|
+
};
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The `provider/*`-entry existence probe: the target provider's advertised
|
|
106
|
+
* catalog, fetched once per expansion and cached per provider. A
|
|
107
|
+
* missing/unknown provider or a failing catalog reads as "no such model", so
|
|
108
|
+
* wildcard candidates to it are skipped; without a catalog no filtering
|
|
109
|
+
* happens (`() => true`).
|
|
110
|
+
*
|
|
111
|
+
* simplify: catalog fetched per expansion, never cached across walks. Walks
|
|
112
|
+
* are failure-driven and rare; cache per provider in the plugin if they ever
|
|
113
|
+
* become hot.
|
|
114
|
+
*/
|
|
115
|
+
export declare function makeModelExists(catalog: ModelCatalogLike | undefined, providers: readonly string[]): Promise<(provider: string, model: string) => boolean>;
|
|
116
|
+
/**
|
|
117
|
+
* The context-window lookup for a `CONTEXT_WINDOW_EXCEEDED` expansion: a
|
|
118
|
+
* `provider/model → contextWindow` map over the failing route plus every
|
|
119
|
+
* resolved candidate route, fetched once per expansion (the
|
|
120
|
+
* {@link makeModelExists} pattern — failure-driven and rare, never cached).
|
|
121
|
+
*
|
|
122
|
+
* Two sources, cheapest first: the advertised catalog row when it discloses a
|
|
123
|
+
* window (one `listModels` per distinct provider), then — for routes still
|
|
124
|
+
* unknown — the exact-route `resolveModelInfo` metadata, which is where the
|
|
125
|
+
* dsh host puts `context.contextWindow`. A missing catalog, a throwing probe,
|
|
126
|
+
* or a route that discloses nothing all read as UNKNOWN (`undefined`), and
|
|
127
|
+
* unknown always KEEPS the candidate: an undisclosed window must never empty a
|
|
128
|
+
* chain.
|
|
129
|
+
*/
|
|
130
|
+
export declare function makeContextWindowOf(catalog: ModelCatalogLike | undefined, routes: readonly FailingModel[]): Promise<(provider: string, model: string) => number | undefined>;
|
|
131
|
+
/**
|
|
132
|
+
* The minimal agent view one walk needs (design §2.2): `GenerateOptions`
|
|
133
|
+
* carries only `sessionId`, so the session's agent is read opportunistically
|
|
134
|
+
* through the host `agents` registry. `undefined` when the id is absent or the
|
|
135
|
+
* agent left the registry — treated as a root agent with no role (the
|
|
136
|
+
* effective-chain walk still runs).
|
|
137
|
+
*/
|
|
138
|
+
export interface AgentChainView {
|
|
139
|
+
/** `agent.session.header.origin`; a missing header is a root agent. */
|
|
140
|
+
origin: 'root' | 'subagent';
|
|
141
|
+
/** `agent.session.header.agentPreset` — the explicit role stage. */
|
|
142
|
+
agentPreset?: string;
|
|
143
|
+
/** `agent.options` — the pair the rules stage matches (a virtual pair on this path). */
|
|
144
|
+
optionsRoute?: {
|
|
145
|
+
provider: string;
|
|
146
|
+
model: string;
|
|
147
|
+
};
|
|
148
|
+
sessionId: string;
|
|
149
|
+
}
|
|
150
|
+
/** Everything one walk reads from the plugin wiring (design §6.1). */
|
|
151
|
+
export interface ChainWalkDeps {
|
|
152
|
+
readConfig(): FallbacksConfig;
|
|
153
|
+
getLlm(): LlmRuntime | undefined;
|
|
154
|
+
resolveAgentView(sessionId: GenerateOptions['sessionId']): AgentChainView | undefined;
|
|
155
|
+
states: FallbackStateStore;
|
|
156
|
+
/** The live `trimmed id → declared raw id` map (re-derived on settings change). */
|
|
157
|
+
roleIds(): ReadonlyMap<string, string>;
|
|
158
|
+
/** The dispatch seam's record for one child — the dispatch-role stage. */
|
|
159
|
+
seamRecord(sessionId: SessionKey): {
|
|
160
|
+
role?: string;
|
|
161
|
+
virtualRouteInjected?: boolean;
|
|
162
|
+
} | undefined;
|
|
163
|
+
/** The host subagent policy for one session: guarded read + fail-closed. */
|
|
164
|
+
policyFor(sessionId: SessionKey): EffectivePolicy;
|
|
165
|
+
/** Record a plugin-originated switch the allowlist emptied (in-memory, card-only). */
|
|
166
|
+
recordBlocked?(sessionId: SessionKey, route: {
|
|
167
|
+
provider: string;
|
|
168
|
+
model: string;
|
|
169
|
+
}, reason: FallbackSwitchReason): void;
|
|
170
|
+
/**
|
|
171
|
+
* Per-agent last time-slot winner (P7 分时切换 marker): the caller owns the
|
|
172
|
+
* map (re-baselined on a settings change, cleared on dispose), the walk reads
|
|
173
|
+
* and writes it — the log only, never a failure decision. Absent ⇒ no log.
|
|
174
|
+
*/
|
|
175
|
+
slotWinners?: Map<string, {
|
|
176
|
+
key: string;
|
|
177
|
+
label: string;
|
|
178
|
+
}>;
|
|
179
|
+
log: {
|
|
180
|
+
info(message: string, ...args: unknown[]): void;
|
|
181
|
+
warn(message: string, ...args: unknown[]): void;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/** A non-null session id — the key the state store and cooldown memory are held under. */
|
|
185
|
+
type SessionKey = NonNullable<GenerateOptions['sessionId']>;
|
|
186
|
+
/**
|
|
187
|
+
* The route a `FallbacksChain/Auto` selection really SERVED, for the rules
|
|
188
|
+
* stage's pair-wise matching (QC1 I-1 in the pre-C path): a child whose
|
|
189
|
+
* recorded pair is the virtual row keeps matching a rule keyed on the real
|
|
190
|
+
* chain head. `effectiveHeadOf` is the same head the picker row advertises, and
|
|
191
|
+
* a non-conforming all-day chain yields `undefined` — no anchor, no widening.
|
|
192
|
+
* Shared by the walk (`resolveWalk`) and the `/fallbacks` snapshot, so the
|
|
193
|
+
* diagnostic surface resolves the role exactly as the run does.
|
|
194
|
+
*/
|
|
195
|
+
export declare function servedRouteOf(config: FallbacksConfig, route: {
|
|
196
|
+
readonly provider?: string;
|
|
197
|
+
readonly model?: string;
|
|
198
|
+
} | undefined, now: Date): {
|
|
199
|
+
provider: string;
|
|
200
|
+
model: string;
|
|
201
|
+
} | undefined;
|
|
202
|
+
/**
|
|
203
|
+
* Walk one virtual call's chain (design §6.1): an async iterable of the
|
|
204
|
+
* attempts' chunks, ending in exactly one terminal `finish`.
|
|
205
|
+
*
|
|
206
|
+
* Throws only for the pre-attempt conditions (empty chain, undispatchable
|
|
207
|
+
* head, vanished runtime) — every per-attempt failure is either swallowed into
|
|
208
|
+
* another attempt or forwarded as that attempt's own terminal `finish`.
|
|
209
|
+
*/
|
|
210
|
+
export declare function walkChain(deps: ChainWalkDeps, options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
211
|
+
export {};
|
package/dist/chains.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* such model id" skip is judged by {@link resolveCandidate} (via
|
|
18
18
|
* `modelExists`) or by the caller's filter.
|
|
19
19
|
*
|
|
20
|
-
* The concatenation itself lives in ONE helper — {@link
|
|
20
|
+
* The concatenation itself lives in ONE helper — {@link roleChainEntries}
|
|
21
21
|
* (qc1 S-3 SSOT) — shared by {@link resolveChainViews} and
|
|
22
22
|
* {@link hasWildcardEntry}, so the wildcard probe is exact (qc2 F-003:
|
|
23
23
|
* `fallback: 'none'` excludes `rootChain`) and can never diverge from the
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* @module dsh-llm-fallbacks/chains
|
|
37
37
|
*/
|
|
38
38
|
import { type FallbacksRole } from './config.ts';
|
|
39
|
-
import type { CooldownStore
|
|
39
|
+
import type { CooldownStore } from './cooldown.ts';
|
|
40
40
|
import { type Selector } from './selectors.ts';
|
|
41
41
|
/** The model that just failed — the anchor for chain resolution. */
|
|
42
42
|
export interface FailingModel {
|
|
@@ -55,6 +55,23 @@ export interface FailingModel {
|
|
|
55
55
|
* path is {@link parseSelector}, used by the Task 3 config-warning path.
|
|
56
56
|
*/
|
|
57
57
|
export declare function resolveCandidate(entry: string, failing: FailingModel, modelExists?: (provider: string, model: string) => boolean): Selector | null;
|
|
58
|
+
/**
|
|
59
|
+
* The role's effective concatenated chain entries (spec §7.2) — the SINGLE
|
|
60
|
+
* concatenation SSOT (qc1 S-3): the declared role's own `chain` followed by
|
|
61
|
+
* `rootChain` unless `fallback: 'none'` (append-not-replace — role entries
|
|
62
|
+
* first, rootChain as the tail); the built-in `inherit` id and any unknown
|
|
63
|
+
* role id resolve to `rootChain` alone. {@link resolveChainViews} (candidate
|
|
64
|
+
* resolution) and {@link hasWildcardEntry} (wildcard probe) both derive
|
|
65
|
+
* their candidate source here, so the probe never diverges from the
|
|
66
|
+
* resolution (grep-verifiable: one concatenation expression). Role ids are
|
|
67
|
+
* compared by trim (qc2 F-001 — the runtime canonicalizes padded declared
|
|
68
|
+
* ids, and `resolveRole` returns the declared raw id).
|
|
69
|
+
*
|
|
70
|
+
* Exported for the chain walk's head anchor (`src/chain-walk.ts`): the walk
|
|
71
|
+
* resolves the FIRST DISPATCHABLE exact entry over these same entries, so the
|
|
72
|
+
* head it starts at is the head the candidate walk below it starts from.
|
|
73
|
+
*/
|
|
74
|
+
export declare function roleChainEntries(roles: readonly FallbacksRole[], rootChain: readonly string[], role: string): readonly string[];
|
|
58
75
|
/**
|
|
59
76
|
* Single-pass chain resolution for the decision path (T1 review Important
|
|
60
77
|
* #2). Walks the role's concatenated entries ONCE and returns the unfiltered
|
|
@@ -91,7 +108,7 @@ export declare function selectCandidates(all: readonly Selector[], wildcard: rea
|
|
|
91
108
|
* Ordered fallback candidates for the failing (provider, model).
|
|
92
109
|
*
|
|
93
110
|
* Candidates are the role's concatenated entries (spec §7.2, see
|
|
94
|
-
* {@link
|
|
111
|
+
* {@link roleChainEntries} — the single concatenation SSOT): the declared
|
|
95
112
|
* role's own `chain` followed by `rootChain` unless `fallback: 'none'`
|
|
96
113
|
* (append-not-replace — role entries first, rootChain as the tail). The
|
|
97
114
|
* reserved built-in `inherit` id resolves to `rootChain` silently; a truly
|
|
@@ -118,7 +135,7 @@ export declare function resolveChain(roles: readonly FallbacksRole[], rootChain:
|
|
|
118
135
|
* `resolveChain` walks to decide whether the catalog existence probe is
|
|
119
136
|
* needed at all.
|
|
120
137
|
*
|
|
121
|
-
* The probe walks {@link
|
|
138
|
+
* The probe walks {@link roleChainEntries} — the SAME concatenation
|
|
122
139
|
* {@link resolveChainViews} resolves (qc1 S-3 single SSOT) — so it is
|
|
123
140
|
* exact, never an over-approximation (qc2 F-003 / qc3 S-1): for
|
|
124
141
|
* `fallback: 'none'` roles `rootChain` is excluded, and a wildcard that can
|
|
@@ -135,7 +152,9 @@ export interface CandidateFilterOptions {
|
|
|
135
152
|
/** Cooldown store: suppressed candidates are skipped (keyed `provider/model`). */
|
|
136
153
|
cooldown: Pick<CooldownStore, 'isSuppressed'>;
|
|
137
154
|
/** Failed-set for the current step: already-failed candidates are skipped. */
|
|
138
|
-
failed:
|
|
155
|
+
failed: {
|
|
156
|
+
has(key: string): boolean;
|
|
157
|
+
};
|
|
139
158
|
/** Optional existence probe: candidates the target provider lacks are skipped. */
|
|
140
159
|
modelExists?: (provider: string, model: string) => boolean;
|
|
141
160
|
/**
|
|
@@ -160,8 +179,11 @@ export declare function createCandidateFilter(options: CandidateFilterOptions):
|
|
|
160
179
|
* 行为可见性). `context-window` is the `CONTEXT_WINDOW_EXCEEDED` walk's
|
|
161
180
|
* exclusion: the candidate's disclosed window is not larger than the failing
|
|
162
181
|
* model's, so the request that just overflowed cannot fit there either.
|
|
182
|
+
* `allowlist` is the subagent-policy exclusion: the candidate survived every
|
|
183
|
+
* filter but sits outside the host allowlist, so a plugin-originated walk may
|
|
184
|
+
* not switch to it. The walk owns that label — the policy read lives there.
|
|
163
185
|
*/
|
|
164
|
-
export type CandidateSkipReason = 'same-as-current' | 'cooldown' | 'step-failed' | 'context-window' | 'missing-id';
|
|
186
|
+
export type CandidateSkipReason = 'same-as-current' | 'cooldown' | 'step-failed' | 'context-window' | 'missing-id' | 'allowlist';
|
|
165
187
|
/** One entry of the ordered, per-candidate annotation (T3 review Minor 1). */
|
|
166
188
|
export interface AnnotatedCandidate {
|
|
167
189
|
candidate: Selector;
|