dsh-llm-fallbacks 0.7.0 → 0.8.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 CHANGED
@@ -12,7 +12,7 @@
12
12
  ![dsh](https://img.shields.io/badge/DSH-0.2.2--rc.1-4B32C3.svg)
13
13
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-llm-fallbacks?lang=en)](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
14
14
 
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.
15
+ Automatic provider/model fallback chains for dsh (DeepSeek Harness): the plugin registers N **named virtual chains**, each selectable in the model picker as **`FallbacksChain / <chain id>`** — a session routed to one walks that chain, so an LLM request that keeps failing on a listed code (auth, quota, rate limiting 429, …) is retried on the next available provider/model and tasks are not interrupted by model problems. A session on any **real** provider/model has **no plugin fallback**: `llm-retry` alone handles it. Create a chain in the Fallbacks settings card, then select its `FallbacksChain / <id>` row to get it.
16
16
 
17
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).
18
18
 
@@ -20,9 +20,9 @@ Works in both dsh front ends: the **web** profile (Settings → Plugins → Fall
20
20
 
21
21
  ### Automatic model failover
22
22
 
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.
23
+ A session routed to `FallbacksChain / <chain id>` — the root agent by selection, or a subagent whose role points at that chain (「指定降级链」) — walks it: 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 a chain (empty `chains` is the only first-run state).
24
24
 
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.
25
+ - **Who gets a chain**: only sessions whose model selection routes to `FallbacksChain / <chain id>`. 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
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
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.
28
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.
@@ -33,7 +33,7 @@ A session routed to `FallbacksChain / Auto` — the root agent, or an inheriting
33
33
 
34
34
  ### Peak/valley time slots
35
35
 
36
- Time slots rotate the **effective root chain** by wall-clock windows: each row carries its own fallback chain, and the first row whose window contains the current moment replaces the all-day chain for the next root request — the all-day chain stays the last resort when no row matches. Rotation is a routing seed, not a failure decision: it consumes no cooldown and leaves the failure walk (fallback switch) untouched.
36
+ Time slots are configured **per chain** and rotate that chain's effective entries by wall-clock windows: the first row whose window contains the current moment replaces the chain's default entries for the next request — the chain's own entries stay the last resort when no row matches. Rotation is a routing seed, not a failure decision: it consumes no cooldown and leaves the failure walk (fallback switch) untouched.
37
37
 
38
38
  ![Time slots](docs/assets/screenshot-1-en.png)
39
39
 
@@ -54,7 +54,7 @@ GLM Peak and GLM Valley are offered in the card picker only when `zai-coding-cn`
54
54
 
55
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.
56
56
 
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)).
57
+ Role **identity** is resolved at dispatch in two ordered stages, first hit winning — explicit `agentPreset` → the **dispatch-declared** `**Execute as**: <id>` header. If the child's dispatch prompt declares `**Execute as**: <id>`, that role's persona, `[role: <id>]` notice and badge are delivered. A child with **no declared dispatch header** is NOT SPECIFIED: no persona, no notice row, no role half of the badge — and the plugin does not intervene. The role can also carry a **manual chain pointer** (「指定降级链」): at dispatch the seam stamps a local, non-explicit-route child onto `FallbacksChain / <role.chain>` — an explicit route is **authorized** and never touched, and an external-backend child keeps pure inheritance. There is no rule matching and no host-policy consultation: the mapping is your static config.
58
58
 
59
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.
60
60
 
@@ -89,30 +89,30 @@ The plugin's settings live in a shared `fallbacks:` namespace, editable from thr
89
89
 
90
90
  ```yaml
91
91
  fallbacks:
92
- rootChain: # all-day chain: leading entries = fallback walk, last = Default model (official)
93
- - anthropic/claude-3-5-sonnet
94
- - deepseek-official/deepseek-flash
95
- timeSlots: # optional: rotate the effective root chain by wall-clock windows
96
- - kind: preset
97
- preset: liang-peak
98
- chain:
92
+ chains: # named virtual chains: one picker row each (FallbacksChain/<id>)
93
+ - id: Auto # the picker model id (case-sensitive, unique)
94
+ label: Default chain # tab title + picker prefix (display only)
95
+ entries: # 默认降级链 …then the 默认模型 tail (exactly one official model)
99
96
  - anthropic/claude-3-5-sonnet
100
- roles: # optional: declare role entities, then reference them from rules
97
+ - deepseek-official/deepseek-v4-pro
98
+ timeSlots: [] # optional: this chain's own wall-clock rows
99
+ - id: Coding
100
+ label: Coding
101
+ entries:
102
+ - openai/gpt-4o-mini
103
+ - deepseek-official/deepseek-flash
104
+ roles: # optional: role identities + an optional chain POINTER
101
105
  list:
102
106
  - id: reviewer
103
107
  persona: Code-review subagents
104
- chain:
105
- - openai/gpt-4o-mini
106
- fallback: inherit-root
107
- rules: # subagent-only: rules never match root requests
108
- - role: reviewer
108
+ chain: Coding # 指定降级链: children under this role mount FallbacksChain/Coding
109
109
  ```
110
110
 
111
111
  Three rules of thumb:
112
112
 
113
- - **There is no feature switch**: the plugin intervenes exactly when content is configured — a non-empty `rootChain`, at least one `timeSlots` row, or one declared role. An empty section is a complete no-op, and the Plugins-page row toggle is the master switch.
114
- - **The all-day `rootChain` tail must be exactly one official model** — `deepseek-official/deepseek-flash` **or** `deepseek-official/deepseek-v4-pro` (XOR); the leading entries are the fallback walk, walked first.
115
- - **Slots and roles are optional**: the first time-slot row whose window matches replaces the all-day chain for the next root request; rules map subagents to declared roles and never match root requests.
113
+ - **There is no feature switch**: the plugin intervenes exactly when a chain is configured (`chains` non-empty). An empty section is a complete no-op, and the Plugins-page row toggle is the master switch.
114
+ - **A chain's LAST entry (默认模型) must be exactly one official model** — `deepseek-official/deepseek-flash` **or** `deepseek-official/deepseek-v4-pro` (XOR); the leading 默认降级链 entries are walked first.
115
+ - **Slots and roles are optional**: each chain's first matching time-slot row replaces that chain's entries; a role's `chain` pointer (or nothing) decides which chain a dispatched child mounts.
116
116
 
117
117
  Full reference (keys, entry syntax, selector rules, migration) → [docs/configuration.md](docs/configuration.md).
118
118
 
@@ -124,12 +124,12 @@ Save the config and restart the session, then type `/fallbacks` — the read-onl
124
124
 
125
125
  ## Feature highlights
126
126
 
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)
127
+ - **Automatic failover** on a virtual route — a `FallbacksChain / <id>` session (root or role-stamped 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)
128
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)
129
- - **Peak/valley time slots** — four frozen UTC+8 presets or custom windows rotate the effective root chain. → [details](docs/features.md#time-slots)
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)
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
+ - **Peak/valley time slots** — four frozen UTC+8 presets or custom windows rotate a chain's own effective entries. → [details](docs/features.md#time-slots)
130
+ - **Role-based subagents** — a dispatch-declared role brings persona, notice row and badge to the child, and its optional `chain` pointer puts the child on `FallbacksChain/<id>`. → [details](docs/features.md#role-based-subagents)
131
+ - **FallbacksChain in the picker** — select `FallbacksChain / <chain id>` 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)
132
+ - **Host subagent model policy** — dsh enforces its 0.1.2 child-model allowlist on the dispatching side; this plugin does not consult it. An explicit child selection is limited to the parent route unless the host allowlist admits it. → [details](docs/features.md#role-based-subagents)
133
133
  - **Five bundled preset roles** — `reviewer` / `scout` / `security-reviewer` / `sonic` / `task`, declared on apply. → [details](docs/features.md#preset-roles)
134
134
  - **dsh-tui surfaces** — `/fallbacks`, `/fallbacks config`, and a `/settings` section with web-card parity. → [details](docs/features.md#dsh-tui-surfaces)
135
135
 
@@ -143,7 +143,7 @@ The plugin installs as a **pure mount**: bundle insert + client inject + its own
143
143
  |---|---|
144
144
  | [docs/install.md](docs/install.md) | desktop/web plugin page / profile install (web + dsh-tui) / registry / git / local variants / uninstall / `--dump-config` verification |
145
145
  | [docs/configuration.md](docs/configuration.md) | full `fallbacks` namespace reference, selector syntax, example YAML, plugin-config card usage, TUI readback, behavior notes, preset roles |
146
- | [docs/features.md](docs/features.md) | capability reference: failover, context-window fallback, time slots, role-based subagents, picker, host policy, preset roles, agent presets, dsh-tui |
146
+ | [docs/features.md](docs/features.md) | capability reference: failover, context-window fallback, time slots, role-based subagents, picker, preset roles, agent presets, dsh-tui |
147
147
  | [docs/repair.md](docs/repair.md) | session-log repair: refusal classes, `--apply` publishing, flags, exit codes, lossy `--drop-legacy-events` |
148
148
  | [docs/consumer-api.md](docs/consumer-api.md) | developer consumption contract: library API + named `llm-fallbacks` service + role seeds, export inventory, lifecycle, typing |
149
149
  | [docs/release.md](docs/release.md) | release process: Trusted Publishing setup, Release prep SOP, fragment format, rollback |
package/README.zh-CN.md CHANGED
@@ -12,7 +12,7 @@
12
12
  ![dsh](https://img.shields.io/badge/DSH-0.2.2--rc.1-4B32C3.svg)
13
13
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-llm-fallbacks?lang=zh)](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
14
14
 
15
- dsh(DeepSeek Harness)的自动模型降级插件:只有模型选择走插件虚拟行 **`FallbacksChain / Auto`** 的会话才沿配置的降级链行走——llm 请求在列出的失败码上持续失败(权限错误、配额超限、限流 429 等)时,请求会在下一个可用 provider/model 上重试,任务不会因模型问题中断。选择**真实** provider/model 的会话**没有插件降级**,只由 `llm-retry` 处理。想要降级链,请在模型选择器中选择 `FallbacksChain / Auto`。
15
+ dsh(DeepSeek Harness)的自动模型降级插件:插件注册 N 条**命名虚拟链**,每条在模型选择器中都是一行 **`FallbacksChain / <链 id>`**——选择某条链的会话沿该链行走,llm 请求在列出的失败码上持续失败(权限错误、配额超限、限流 429 等)时,请求会在下一个可用 provider/model 上重试,任务不会因模型问题中断。选择**真实** provider/model 的会话**没有插件降级**,只由 `llm-retry` 处理。在 Fallbacks 设置卡中创建链,然后选择它的 `FallbacksChain / <id>` 行即可。
16
16
 
17
17
  两个 dsh 前端均可用:**web** profile(设置 → 插件配置 → Fallbacks 卡片)与 **dsh-tui** 终端 profile(`/fallbacks` 会话诊断、`/fallbacks config` 回读、`/settings` 的 fallbacks 分区用于编辑)。
18
18
 
@@ -20,9 +20,9 @@ dsh(DeepSeek Harness)的自动模型降级插件:只有模型选择走插
20
20
 
21
21
  ### 自动模型降级
22
22
 
23
- 会话模型选择走 `FallbacksChain / Auto` 时——root agent,或被分发 seam 挂到该行的继承型 subagent——沿自身链行走:请求在**输出任何内容之前**命中触发码,就在同一个请求内改到下一个可用 provider/model,step 继续运行。选择真实 provider/model 的会话不获得任何插件降级。无需手动切换模型;在配置了内容之前,插件不做任何事。
23
+ 会话模型选择走 `FallbacksChain / <链 id>` 时——root agent 选中该行,或子代理的角色以「指定降级链」指向该链——沿该链行走:请求在**输出任何内容之前**命中触发码,就在同一个请求内改到下一个可用 provider/model,step 继续运行。选择真实 provider/model 的会话不获得任何插件降级。无需手动切换模型;在配置链之前(`chains` 为空是唯一初始状态)插件不做任何事。
24
24
 
25
- - **谁能拿到链**:只有模型选择走 `FallbacksChain / Auto` 的会话。包括选中该行的 root agent,以及分发时被 seam 挂到该行的 subagent(本地 backend、请求上没有显式路由)。真实 provider/model 选择——无论 root 还是 subagent——完全交给 llm-retry。
25
+ - **谁能拿到链**:只有模型选择走 `FallbacksChain / <链 id>` 的会话。包括选中该行的 root agent,以及角色「指定降级链」指向该链、分发时被盖章到该行的 subagent(本地 backend、请求上没有显式路由)。真实 provider/model 选择——无论 root 还是 subagent——完全交给 llm-retry。
26
26
  - **触发**:`triggerCodes`(默认 `AUTH` / `QUOTA` / `RATE_LIMIT`)——**任何** dsh 失败码都可用,包括 `CONTEXT_WINDOW_EXCEEDED`(会降级到上下文窗口更大的候选)。可重试失败(5xx、`TRANSPORT`、`TIMEOUT` 等)先交给 llm-retry;它的每次重试都会重新进入 walk,并从刚进入冷却的链头之后开始。
27
27
  - **已输出即提交**:某个尝试一旦吐出 chunk,之后的失败**不会**在该请求内切换——已产出的输出无法撤回。失败上抛给 loop;恢复路径是 llm-retry 重入(新的一次 walk)或终止失败。
28
28
  - **恢复**:失败或被切走的模型在 `cooldownMs` 内不再被选中,`revertPolicy: cooldown-expiry` 在冷却到期后回到主模型;可选的 `recovery: half-open` 模式(YAML-only)把这次回归改为证据驱动,而非定时器驱动。
@@ -54,7 +54,7 @@ GLM Peak / GLM Valley 仅在配置了 `zai-coding-cn` 时才会出现在卡片
54
54
 
55
55
  subagent 的角色由分发提示词声明——Assignment 头部字段 `**Execute as**: <id>`;它与 `roles.list` 中已声明的角色 id 匹配。只有这个**已声明头部**才投递子代理的身份:`persona` 以 system-prompt 分区安装到子代理,子会话恰好写入一行 `[role: <id>]` 通知行,会话头部显示角色徽章。
56
56
 
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))。
57
+ 角色**身份**在分发时按两个有序阶段解析、先命中者生效——显式 `agentPreset` → **派发声明** `**Execute as**: <id>` 头部。只要子代理的分发提示词声明了 `**Execute as**: <id>`,该角色的 persona、`[role: <id>]` 通知行与徽章就会投递。完全没有声明分发头部的子代理是**未指定**:没有 persona、通知行与徽章角色半边——插件也不干预。角色还可携带**手动链指针**(「指定降级链」):分发时 seam 把本地、无显式路由的子代理盖到 `FallbacksChain / <role.chain>` 上——带显式路由的请求属于**已授权**,插件永不改写;外部 backend 的子代理保持纯继承。没有规则匹配,也没有宿主策略咨询:映射就是你的静态配置。
58
58
 
59
59
  这一套同时覆盖**普通 subagent 分发与 Agent Team 队友**(`spawn_teammate`):两者都是带着同一头部(由模型写入)经同一分发 seam 创建的子代理,不需要任何按机制区分的配置。
60
60
 
@@ -89,29 +89,29 @@ dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui 终端 profile
89
89
 
90
90
  ```yaml
91
91
  fallbacks:
92
- rootChain: # 全时段链:前置项 = 降级链,末项 = 默认模型(官方模型)
93
- - anthropic/claude-3-5-sonnet
94
- - deepseek-official/deepseek-flash
95
- timeSlots: # 可选:按墙钟窗口轮换生效 root 链
96
- - kind: preset
97
- preset: liang-peak
98
- chain:
92
+ chains: # 命名虚拟链:每条 = picker 一行(FallbacksChain/<id>)
93
+ - id: Auto # picker 模型 id(大小写敏感、唯一)
94
+ label: 默认链 # tab 标题 + picker 前缀(仅展示)
95
+ entries: # 默认降级链 …末尾是默认模型(恰好一个官方模型)
99
96
  - anthropic/claude-3-5-sonnet
100
- roles: # 可选:先声明角色实体,再由规则引用
97
+ - deepseek-official/deepseek-v4-pro
98
+ timeSlots: [] # 可选:该链自己的分时行
99
+ - id: Coding
100
+ label: 编码
101
+ entries:
102
+ - openai/gpt-4o-mini
103
+ - deepseek-official/deepseek-flash
104
+ roles: # 可选:角色身份 + 可选链指针
101
105
  list:
102
106
  - id: reviewer
103
107
  persona: Code-review subagents
104
- chain:
105
- - openai/gpt-4o-mini
106
- fallback: inherit-root
107
- rules: # 仅 subagent:规则永不匹配 root 请求
108
- - role: reviewer
108
+ chain: Coding # 指定降级链:该角色派发的子代理挂到 FallbacksChain/Coding
109
109
  ```
110
110
 
111
111
  三条要点:
112
112
 
113
- - **没有功能开关**:只要配置了内容——非空 `rootChain`、至少一行 `timeSlots`、或一个已声明角色——插件才介入;空配置是完全的 no-op,Plugins 页的行开关是总开关。
114
- - **全时段 `rootChain` 的末项必须恰好是一个官方模型**——`deepseek-official/deepseek-flash` **或** `deepseek-official/deepseek-v4-pro`(XOR);前置项是降级链,优先进走。
113
+ - **没有功能开关**:只要配置了链(`chains` 非空)插件才介入;空配置是完全的 no-op,Plugins 页的行开关是总开关。
114
+ - **每条链的末项(默认模型)必须恰好是一个官方模型**——`deepseek-official/deepseek-flash` **或** `deepseek-official/deepseek-v4-pro`(XOR);前部「默认降级链」条目优先进走。
115
115
  - **分时槽与角色都是可选的**:第一个窗口命中的分时行会在下一个 root 请求取代全时段链;规则把 subagent 映射到已声明角色,且永不匹配 root 请求。
116
116
 
117
117
  完整参考(字段、条目语法、选择器规则、迁移)→ [docs/configuration.md](docs/configuration.md)。
@@ -124,12 +124,12 @@ fallbacks:
124
124
 
125
125
  ## 能力一览
126
126
 
127
- - **自动降级**(仅虚拟路由)——`FallbacksChain / Auto` 会话(root 或 seam 挂载的 subagent)命中触发码时沿链切换,step 继续运行;真实模型会话没有插件降级。 → [详情](docs/features.md#automatic-model-failover)
127
+ - **自动降级**(仅虚拟路由)——`FallbacksChain / <id>` 会话(root 或角色盖章的 subagent)命中触发码时沿链切换,step 继续运行;真实模型会话没有插件降级。 → [详情](docs/features.md#automatic-model-failover)
128
128
  - **上下文窗口感知降级**——`CONTEXT_WINDOW_EXCEEDED` 会切到窗口更大的候选;该切换是请求级的(不消耗冷却)。 → [详情](docs/features.md#context-window-aware-fallback)
129
- - **峰谷无忧(分时槽)**——四个冻结的 UTC+8 预设或自定义窗口轮换生效 root 链。 → [详情](docs/features.md#time-slots)
130
- - **角色化 subagent**——派发声明的角色为子代理带来 persona、通知行与徽章;继承型子代理被挂到虚拟链路由,受宿主策略约束。 → [详情](docs/features.md#role-based-subagents)
131
- - **模型选择器中的 FallbacksChain**——选择 `FallbacksChain / Auto` 即由配置链承载该会话;选择真实模型则只由 llm-retry 处理,没有插件降级。 → [详情](docs/features.md#fallbackschain-in-the-model-picker)
132
- - **宿主子代理模型策略**——dsh 0.1.2 的 allowlist 是插件发起的所有 subagent 路由的硬约束。 → [详情](docs/features.md#host-subagent-model-selection-policy-dsh-012)
129
+ - **峰谷无忧(分时槽)**——四个冻结的 UTC+8 预设或自定义窗口轮换该链自己的生效条目。 → [详情](docs/features.md#time-slots)
130
+ - **角色化 subagent**——派发声明的角色为子代理带来 persona、通知行与徽章,其可选的 `chain` 指针把子代理挂到 `FallbacksChain/<id>`。 → [详情](docs/features.md#role-based-subagents)
131
+ - **模型选择器中的 FallbacksChain**——选择 `FallbacksChain / <链 id>` 即由配置链承载该会话;选择真实模型则只由 llm-retry 处理,没有插件降级。 → [详情](docs/features.md#fallbackschain-in-the-model-picker)
132
+ - **宿主子代理模型策略**——dsh 在**派发侧**执行自己的 0.1.2 子模型 allowlist;本插件不读取它。显式子代理选择只允许父路由,除非该路由在宿主 allowlist 内。 → [详情](docs/features.md#role-based-subagents)
133
133
  - **五个内置预设角色**——`reviewer` / `scout` / `security-reviewer` / `sonic` / `task`,随 apply 自动声明。 → [详情](docs/features.md#preset-roles)
134
134
  - **dsh-tui 操作面**——`/fallbacks`、`/fallbacks config`,以及与 web 卡片等价的 `/settings` 分区。 → [详情](docs/features.md#dsh-tui-surfaces)
135
135
 
@@ -143,7 +143,7 @@ fallbacks:
143
143
  |---|---|
144
144
  | [docs/install.md](docs/install.md) | 桌面/Web 插件页 / profile 安装(web + dsh-tui)/ registry / git / 本地目录 / 卸载 / `--dump-config` 校验 |
145
145
  | [docs/configuration.md](docs/configuration.md) | `fallbacks` 命名空间完整参考、选择器语法、示例 YAML、插件配置卡片用法、TUI 回读、行为说明、预设角色 |
146
- | [docs/features.md](docs/features.md) | 能力参考:降级、上下文窗口降级、分时槽、角色化 subagent、选择器、宿主策略、预设角色、agent 预设、dsh-tui |
146
+ | [docs/features.md](docs/features.md) | 能力参考:降级、上下文窗口降级、分时槽、角色化 subagent、选择器、预设角色、agent 预设、dsh-tui |
147
147
  | [docs/repair.md](docs/repair.md) | 会话日志修复:拒载类别、`--apply` 发布、参数、退出码、有损 `--drop-legacy-events` |
148
148
  | [docs/consumer-api.md](docs/consumer-api.md) | 开发者消费契约:库 API + 具名 `llm-fallbacks` 服务 + 角色种子、导出清单、生命周期、类型 |
149
149
  | [docs/release.md](docs/release.md) | 发布流程:Trusted Publishing 配置、Release prep SOP、fragment 格式、回滚 |
@@ -4,13 +4,20 @@
4
4
  * `stream()` instead of on the `agent/request` / `agent/request-error`
5
5
  * waterfalls.
6
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
7
+ * One walk per `stream()` call: resolve the selected named chain and its
8
+ * slot-effective tail, then attempt the chain's heads in order. Root and
9
+ * subagent walks are the SAME shape — every walk is the selected chain. A
9
10
  * pre-output terminal `finish` whose failure code is a trigger code is
10
11
  * swallowed, booked (cooldown / recovery) and the walk advances; anything
11
12
  * 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.
13
+ * failure after the attempt already yielded output — is forwarded verbatim
14
+ * and ends the walk. A `usage` chunk is metadata, so it neither commits the
15
+ * attempt nor stops the advance.
16
+ *
17
+ * A pre-output parameter rejection (`UNSUPPORTED_REASONING_EFFORT`, or a
18
+ * `max_tokens`-shaped `INVALID_REQUEST`) is repaired first: the SAME candidate
19
+ * is retried once with the offending control dropped, because a route that
20
+ * discloses no capability only reveals its real cap through that rejection.
14
21
  *
15
22
  * The COMMITTED rule (§3.1 / §7.5) is why a post-output failure cannot be
16
23
  * replayed: yielded chunks cannot be unsent, so a second provider's output
@@ -26,33 +33,39 @@
26
33
  * the per-call failed set and the switch cap live only for this walk
27
34
  * (`maxSwitchesPerStep` = switches per walk, attempts ≤ cap + 1).
28
35
  *
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.
36
+ * The pre-attempt guards (`UNKNOWN_CHAIN` / `EMPTY_EFFECTIVE_CHAIN` /
37
+ * `UNDISPATCHABLE_EFFECTIVE_HEAD` / `LLM_UNAVAILABLE`) are owned here, and
38
+ * `slotWinners` carries the per-(agent, chain) 分时切换 rotation log.
33
39
  *
34
40
  * @module dsh-llm-fallbacks/chain-walk
35
41
  */
36
42
  import { type GenerateOptions, type LlmRuntime, type StreamChunk } from '@deepseek-ai/dsh-llm';
37
43
  import { type FailingModel } from './chains.ts';
38
44
  import type { FallbacksConfig } from './config.ts';
39
- import type { FallbackSwitchReason } from './events.ts';
40
45
  import type { FallbackStateStore } from './state.ts';
41
- import type { EffectivePolicy } from './subagent-policy.ts';
42
46
  /**
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
+ * `LlmError` code: the effective chain is empty — the selected named chain has
48
+ * no entries to walk (the pre-attempt guard; the walk is the only point that
49
+ * knows WHICH chain the session selected, `options.model` being the chain id).
47
50
  */
48
51
  export declare const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
49
52
  /**
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).
53
+ * `LlmError` code: the chain cannot be walked — its entries tail is not
54
+ * conforming (the last entry must be exactly one official model), or no entry
55
+ * is a dispatchable real pair (wildcard selector, malformed selector, or a
56
+ * self-route back to `FallbacksChain/*` — the recursion guard).
54
57
  */
55
58
  export declare const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
59
+ /**
60
+ * `LlmError` code: `options.model` names no declared chain — reachable only
61
+ * once a chain the session selected is deleted or renamed (the migrated
62
+ * default id `Auto` is unchanged, so an upgrade alone never hits this).
63
+ * Thrown synchronously before any chunk; the host surfaces a terminal error
64
+ * finish and the user re-selects in the picker. No silent fallback to
65
+ * another chain — picking the wrong named chain silently would be a
66
+ * data-routing surprise.
67
+ */
68
+ export declare const UNKNOWN_CHAIN_CODE = "UNKNOWN_CHAIN";
56
69
  /** `LlmError` code (defensive): the `llm` runtime disappeared mid-flight. */
57
70
  export declare const LLM_UNAVAILABLE_CODE = "LLM_UNAVAILABLE";
58
71
  /**
@@ -83,6 +96,24 @@ export declare const CONTEXT_WINDOW_EXCEEDED_CODE = "CONTEXT_WINDOW_EXCEEDED";
83
96
  * send the next SHORT prompt to the fallback for no reason.
84
97
  */
85
98
  export declare const REQUEST_SCOPED_CODES: ReadonlySet<string>;
99
+ /**
100
+ * The exact-route capabilities a walk reads from `resolveModelInfo`: the
101
+ * context window (wildcard / overflow expansions) plus the two request
102
+ * controls parameter adaptation compares against. Every field is optional — a
103
+ * host that discloses nothing leaves every request untouched.
104
+ */
105
+ interface ModelInfoLike {
106
+ context?: {
107
+ contextWindow?: number;
108
+ };
109
+ defaultMaxTokens?: number;
110
+ reasoning?: {
111
+ efforts: readonly {
112
+ id: string;
113
+ name: string;
114
+ }[];
115
+ };
116
+ }
86
117
  /**
87
118
  * Model-catalog view the wildcard / context-window probes read. `contextWindow`
88
119
  * is a host-disclosed extra on the advisory catalog row (the dsh `LlmModelInfo`
@@ -95,11 +126,7 @@ export interface ModelCatalogLike {
95
126
  id: string;
96
127
  contextWindow?: number;
97
128
  }[]>;
98
- resolveModelInfo?(provider: string, model: string): Promise<{
99
- context?: {
100
- contextWindow?: number;
101
- };
102
- }>;
129
+ resolveModelInfo?(provider: string, model: string): Promise<ModelInfoLike>;
103
130
  }
104
131
  /**
105
132
  * The `provider/*`-entry existence probe: the target provider's advertised
@@ -129,48 +156,23 @@ export declare function makeModelExists(catalog: ModelCatalogLike | undefined, p
129
156
  */
130
157
  export declare function makeContextWindowOf(catalog: ModelCatalogLike | undefined, routes: readonly FailingModel[]): Promise<(provider: string, model: string) => number | undefined>;
131
158
  /**
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).
159
+ * Key of one (agent, chain) 分时切换 marker. Both the walk's write and the
160
+ * wiring's `agent/disposed` cleanup build keys through here, so the two halves
161
+ * of the contract cannot drift apart.
137
162
  */
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). */
163
+ export declare function slotMarkerKey(agentId: string, chainId: string): string;
164
+ /** Key prefix covering every chain marker of one agent (the cleanup sweep). */
165
+ export declare function slotMarkerPrefix(agentId: string): string;
166
+ /** Everything one walk reads from the plugin wiring. */
151
167
  export interface ChainWalkDeps {
152
168
  readConfig(): FallbacksConfig;
153
169
  getLlm(): LlmRuntime | undefined;
154
- resolveAgentView(sessionId: GenerateOptions['sessionId']): AgentChainView | undefined;
155
170
  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
  /**
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.
172
+ * Per-(agent, chain id) last time-slot winner (P7 分时切换 marker): the
173
+ * caller owns the map (re-baselined on a settings change, cleared on
174
+ * dispose), the walk reads and writes it — the log only, never a failure
175
+ * decision. Absent ⇒ no log.
174
176
  */
175
177
  slotWinners?: Map<string, {
176
178
  key: string;
@@ -179,26 +181,10 @@ export interface ChainWalkDeps {
179
181
  log: {
180
182
  info(message: string, ...args: unknown[]): void;
181
183
  warn(message: string, ...args: unknown[]): void;
184
+ /** Optional: the request-rewriting lines (parameter adaptation) are diagnostics only. */
185
+ debug?(message: string, ...args: unknown[]): void;
182
186
  };
183
187
  }
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
188
  /**
203
189
  * Walk one virtual call's chain (design §6.1): an async iterable of the
204
190
  * attempts' chunks, ending in exactly one terminal `finish`.