dsh-llm-fallbacks 0.1.6 → 0.2.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 +30 -109
- package/README.zh-CN.md +27 -101
- package/dist/client/index.js +1 -1
- package/dist/commands.d.ts +87 -0
- package/dist/index.js +140 -16
- package/dist/tui.d.ts +55 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,73 +5,24 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|

|
|
7
7
|

|
|
8
|
-

|
|
9
|
+

|
|
9
10
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
10
11
|
|
|
11
12
|
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.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
Works in both dsh front ends: the **web** profile (Settings → 插件配置 → Fallbacks card) and the **dsh-tui** terminal profile (`/fallbacks` + `/fallbacks config`).
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
dsh plugin --profile web add dsh-llm-fallbacks # pin a version with @<version>
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Features
|
|
20
|
-
|
|
21
|
-
- **Automatic fallback for root and subagents**: any agent switches down the chain to the next available provider/model on model failure — no manual model switching.
|
|
22
|
-
- **Two-block config**: block 1 `rootChain` — the root agent's single fallback chain (empty = root does not fall back); block 2 declared roles — `roles.list` role entities (id/persona/chain/fallback) that `roles.rules` reference by id (or the built-in `inherit`); no rule match → `inherit` → `rootChain`.
|
|
23
|
-
- **Entry syntax**: chain entries are `provider/model` (exact switch) or `provider/*` (keep the failed model id, switch provider only) — the old chain-key namespace (provider/model keys, role-name keys) is gone.
|
|
24
|
-
- **Cooldown and revert**: models that were switched away from / failed are not re-selected during the cooldown period; `revertPolicy: cooldown-expiry` automatically returns to the primary model when the cooldown expires, while `never` does not return within the session.
|
|
25
|
-
- **Visible behavior**: every switch appends a persisted session event `fallbacks/switch` (from/to/role/reason), alongside info-level logs (candidate attempt order and skip reasons) and the read-only status block on the Settings → 插件配置 → Fallbacks card — no silent model switching.
|
|
26
|
-
- **Safety valves**: switching stops and the original error semantics are kept once `maxSwitchesPerStep` is exceeded for a step, preventing chain loops from amplifying latency; `mode: 'always'` providers additionally have a retry cap (`alwaysModeRetryCap`).
|
|
27
|
-
- **No-config no-op**: `enabled` defaults to off (`false`); with no `rootChain`/role chains, unmatched trigger codes, or unresolved roles the plugin is a complete no-op — identical to not being installed, and no events are emitted.
|
|
28
|
-
|
|
29
|
-
## Install
|
|
30
|
-
|
|
31
|
-
### One-line install
|
|
32
|
-
|
|
33
|
-
```sh
|
|
34
|
-
dsh plugin --profile web add dsh-llm-fallbacks # pin a version with @<version>
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
A registry install fetches the **built package** (`dist/`) — nothing is built on the target machine. The plugin is **mount-only**: it never modifies the dsh source tree, and no patch / postinstall step exists — dsh upgrades never require re-patching. Versioning follows npm dist-tags (`latest` by default); pin an exact version with `dsh plugin --profile web add dsh-llm-fallbacks@<version>`.
|
|
38
|
-
|
|
39
|
-
> **Release status**: published as `dsh-llm-fallbacks@0.1.3` (latest). The first publish used a one-time `NODE_AUTH_TOKEN` bootstrap secret; Trusted Publishing is configured afterwards for tokenless releases.
|
|
40
|
-
|
|
41
|
-
### Registry package (npm / pnpm)
|
|
42
|
-
|
|
43
|
-
```sh
|
|
44
|
-
npm install dsh-llm-fallbacks # or: pnpm add dsh-llm-fallbacks
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
> Release process → [docs/release.md](docs/release.md).
|
|
48
|
-
|
|
49
|
-
### Git install (works today)
|
|
50
|
-
|
|
51
|
-
```sh
|
|
52
|
-
dsh plugin --profile web add github:omdsh-dev/dsh-llm-fallbacks # pin a commit with #<sha>
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
A git install fetches **sources, not built artifacts** — the bundle builds itself on install (`prepare` self-build), so the target machine needs node + pnpm. pnpm ≥ 10 blocks a git dependency's `prepare` by default (`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`); allow the build in the profile's `pnpm-workspace.yaml` and re-run the `add` (details in [docs/install.md](docs/install.md)).
|
|
16
|
+
## Quick start
|
|
56
17
|
|
|
57
|
-
###
|
|
18
|
+
### Install
|
|
58
19
|
|
|
59
20
|
```sh
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# 2) Add to the target profile (example: web)
|
|
63
|
-
dsh plugin --profile web add .
|
|
21
|
+
dsh plugin --profile web add dsh-llm-fallbacks # web profile (Settings → Fallbacks card)
|
|
22
|
+
dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui terminal profile
|
|
64
23
|
```
|
|
65
24
|
|
|
66
|
-
|
|
67
|
-
> `@deepseek-ai/*` packages (peer deps, host-provided at runtime) from the npm
|
|
68
|
-
> registry at `0.1.0-rc.6` — `autoInstallPeers` + the registry auth token in
|
|
69
|
-
> the user-level `~/.npmrc` (pnpm 11 no longer expands `${NPM_TOKEN}` from a
|
|
70
|
-
> project `.npmrc`), no local link farm.
|
|
71
|
-
|
|
72
|
-
> Both methods, uninstall, and `--dump-config` verification — including the bundle-layer ordering requirements — are covered in [docs/install.md](docs/install.md).
|
|
73
|
-
|
|
74
|
-
## Quick start
|
|
25
|
+
Same plugin, either front end — the only difference is the `--profile` flag. Pin a version with `@<version>`. A registry install fetches the **built package** (`dist/`), nothing builds on the target machine. Registry / git / local-directory variants, uninstall, and `--dump-config` verification → [docs/install.md](docs/install.md).
|
|
75
26
|
|
|
76
27
|
### Minimal configuration
|
|
77
28
|
|
|
@@ -80,12 +31,12 @@ Add a `fallbacks:` section to the dsh settings document (default `$DSH_HOME/sett
|
|
|
80
31
|
```yaml
|
|
81
32
|
fallbacks:
|
|
82
33
|
enabled: true # feature switch; defaults to false — set explicitly to enable
|
|
83
|
-
rootChain: # block 1: root agent's chain
|
|
34
|
+
rootChain: # block 1: root agent's chain, tried in order after the primary model fails
|
|
84
35
|
- anthropic/claude-3-5-sonnet
|
|
85
36
|
- openai/*
|
|
86
|
-
roles: # block 2: declare
|
|
37
|
+
roles: # block 2: declare role entities first, then let rules reference them
|
|
87
38
|
list:
|
|
88
|
-
- id: reviewer #
|
|
39
|
+
- id: reviewer # unique id (/^[a-z0-9-]{1,32}$/); "inherit" is reserved
|
|
89
40
|
persona: Code-review subagents
|
|
90
41
|
chain:
|
|
91
42
|
- openai/gpt-4o-mini
|
|
@@ -95,72 +46,42 @@ fallbacks:
|
|
|
95
46
|
role: reviewer
|
|
96
47
|
```
|
|
97
48
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
Save and restart the web session for the changes to take effect. The feature switch `fallbacks.enabled` **defaults to off (`false`)** — the plugin only engages once it is turned on; `triggerCodes` defaults to `AUTH` / `QUOTA` / `RATE_LIMIT`; and with **no `rootChain`/role chains configured the behavior is identical to not having the plugin installed**. More examples (role entities, fallback strategies, rules referencing `inherit`) → [docs/configuration.md](docs/configuration.md).
|
|
101
|
-
|
|
102
|
-
> **Upgrade note (behavior change)**: an existing `fallbacks:` section **without an explicit `enabled` key** now resolves to `false` after upgrading — add `enabled: true` to keep the plugin active.
|
|
103
|
-
|
|
104
|
-
## `/fallbacks` command (in-session diagnostics)
|
|
49
|
+
No rule match → the built-in `inherit` → `rootChain`. `enabled` defaults to **off** — with no chains configured the plugin is a complete no-op. Full reference (role entities, fallback strategies, rules, selectors, preset roles) → [docs/configuration.md](docs/configuration.md).
|
|
105
50
|
|
|
106
|
-
|
|
51
|
+
> **Upgrade note (behavior change)**: an existing `fallbacks:` section **without an explicit `enabled` key** resolves to `false` after upgrading — add `enabled: true` to keep the plugin active.
|
|
107
52
|
|
|
108
|
-
|
|
109
|
-
- the **resolved chain** for that role (the role's own chain entries, annotated `(inherit-root)` when `rootChain` is appended — `rootChain` entries render in full only when the role has no own chain; `fallback: none` with an empty own chain, or no chain at all, → `not configured`);
|
|
110
|
-
- the **recent switches** (`fallbacks/switch` events, newest first, up to 5): from/to provider/model, role, reason;
|
|
111
|
-
- the **cooldown status**: which `provider/model` keys are currently suppressed and until when.
|
|
53
|
+
### Verify
|
|
112
54
|
|
|
113
|
-
|
|
55
|
+
Save and restart the session, then type `/fallbacks` — the read-only in-session diagnostics (origin, resolved role, chain, recent `fallbacks/switch` events, cooldown status). In a dsh-tui profile, `/fallbacks config` additionally reads back the composed configuration (the TUI has no settings page — config is file-only; see [docs/configuration.md](docs/configuration.md)).
|
|
114
56
|
|
|
115
|
-
##
|
|
116
|
-
|
|
117
|
-
The plugin installs as a **pure mount** — it never modifies the dsh source tree:
|
|
118
|
-
|
|
119
|
-
- **Install = bundle insert + client inject + own gateway**: `bundle/cordis.patch.yml`
|
|
120
|
-
inserts the plugin row over the profile bundle stack, `dsh.client.inject` mounts
|
|
121
|
-
the Fallbacks card on the Settings → 插件配置 page, and settings read/write/reset
|
|
122
|
-
go through the plugin's own gateway channel (`/api/fallbacks/get|set|reset`).
|
|
123
|
-
- **No patches, no auto-apply step**: there are no dsh-body patch files and no install
|
|
124
|
-
lifecycle step that applies one. A one-line registry install works as-is.
|
|
125
|
-
- **dsh upgrades never require re-patching**: a dsh upgrade that resets the source
|
|
126
|
-
tree changes nothing for this plugin — it keeps working without any re-apply step.
|
|
127
|
-
- **Stale leftover patches are harmless**: the plugin never depends on a patch
|
|
128
|
-
export (role resolution is rules-only; the model-selection marker coordination
|
|
129
|
-
was removed), so a previously patched dsh tree can be left as-is or manually
|
|
130
|
-
reverted — neither is required.
|
|
131
|
-
|
|
132
|
-
## Developer consumption
|
|
133
|
-
|
|
134
|
-
Beyond the dsh plugin mount, `dsh-llm-fallbacks` exposes a programmable consumer surface — both faces share the same function implementations (single point of truth, no copied logic):
|
|
135
|
-
|
|
136
|
-
- **Library API**: import the runtime functions and types from the package root — `import { resolveRole, resolveChain, validateFallbacksConfig } from 'dsh-llm-fallbacks'`.
|
|
137
|
-
- **Named cordis service**: while the plugin is applied, `ctx.get('llm-fallbacks')` returns a pure-function service (`{ name, version, resolveRole, resolveChain, validateFallbacksConfig, detectLegacyKeys, declareSeeds, getEffectiveRoles, revertSeededPersona }`); after the plugin is disposed it is `undefined`. Runtime state stays observable via `fallbacks/switch` events, not through the service.
|
|
138
|
-
- **Role seeds**: companion plugins auto-provision role rows with zero operator hand-edit — `declareSeeds([{ id, persona }])` declares the full current seed set (re-declaring is a no-op; ids failing `/^[a-z0-9-]{1,32}$/` or equal to `inherit` are skipped per-id with a warn, never coerced), `getEffectiveRoles()` reads back which roles are seeded and whether the persona is overridden, and `revertSeededPersona(id)` restores the **currently declared** seed default. The settings card shows a seed-default / override badge with a revert button; seeds never write `chain` / `fallback`.
|
|
57
|
+
## Features
|
|
139
58
|
|
|
140
|
-
|
|
59
|
+
- **Automatic fallback for root and subagents**: any agent switches down the chain to the next available provider/model on model failure — no manual model switching.
|
|
60
|
+
- **Two-block config**: `rootChain` for the root agent; declared role entities (`roles.list`) referenced by `roles.rules` (or the built-in `inherit`).
|
|
61
|
+
- **Cooldown and revert**: failed / switched-away models are not re-selected during cooldown; `revertPolicy: cooldown-expiry` returns to the primary model automatically.
|
|
62
|
+
- **Visible behavior**: every switch appends a persisted `fallbacks/switch` session event (from/to/role/reason) with info-level logs — no silent model switching.
|
|
63
|
+
- **Safety valves**: `maxSwitchesPerStep` caps switches per step and `alwaysModeRetryCap` caps always-mode retries — chain loops cannot amplify latency.
|
|
64
|
+
- **No-config no-op**: `enabled` defaults to off; with no chains configured the plugin behaves exactly like not being installed.
|
|
141
65
|
|
|
142
66
|
## Preset roles
|
|
143
67
|
|
|
144
|
-
The plugin ships
|
|
68
|
+
The plugin ships **7 bundled generic subagent roles** out of the box — `designer` / `librarian` / `reviewer` / `scout` / `security-reviewer` / `sonic` / `task` — declared automatically on `apply` as seeded `roles.list` rows (`{ id, persona }`): idempotent, and never overwriting an operator persona. They appear in the Settings card (seed badge, id immutable) and in the `/fallbacks config` role summary, ready for `roles.rules` to reference.
|
|
145
69
|
|
|
146
|
-
- **
|
|
147
|
-
-
|
|
70
|
+
- **Switch**: `fallbacks.presets` — `'bundled'` (default) declares the preset roles on apply; `'none'` disables the automatic declaration (already-materialized rows stay).
|
|
71
|
+
- Full semantics (upgrade behavior, conflict handling, library reuse of `presetRoles`) → [docs/configuration.md](docs/configuration.md).
|
|
148
72
|
|
|
149
|
-
|
|
150
|
-
import { presetRoles } from 'dsh-llm-fallbacks'
|
|
73
|
+
## Mount-only (no dsh modification)
|
|
151
74
|
|
|
152
|
-
|
|
153
|
-
if (fb !== undefined) await fb.declareSeeds(presetRoles) // or a FallbacksSeedManager
|
|
154
|
-
```
|
|
75
|
+
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.
|
|
155
76
|
|
|
156
77
|
## Documentation
|
|
157
78
|
|
|
158
79
|
| Doc | Content |
|
|
159
80
|
|---|---|
|
|
160
|
-
| [docs/install.md](docs/install.md) | profile install / registry
|
|
81
|
+
| [docs/install.md](docs/install.md) | profile install (web + dsh-tui) / registry / git / local variants / uninstall / `--dump-config` verification |
|
|
82
|
+
| [docs/configuration.md](docs/configuration.md) | full `fallbacks` namespace reference, selector syntax, example YAML, plugin-config card usage, TUI readback, behavior notes, preset roles |
|
|
83
|
+
| [docs/consumer-api.md](docs/consumer-api.md) | developer consumption contract: library API + named `llm-fallbacks` service + role seeds, export inventory, lifecycle, typing |
|
|
161
84
|
| [docs/release.md](docs/release.md) | release process: Trusted Publishing setup, Release prep SOP, fragment format, rollback |
|
|
162
|
-
| [docs/configuration.md](docs/configuration.md) | full `fallbacks` namespace reference, selector syntax, example YAML, plugin-config card usage, behavior notes |
|
|
163
|
-
| [docs/consumer-api.md](docs/consumer-api.md) | developer consumption contract: export inventory, minimal examples, lifecycle, typing |
|
|
164
85
|
| [docs/verification.md](docs/verification.md) | verification records (test matrix, bundle layer order, runtime contracts, QA gate script) |
|
|
165
86
|
|
|
166
87
|
## License
|
package/README.zh-CN.md
CHANGED
|
@@ -5,71 +5,24 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|

|
|
7
7
|

|
|
8
|
-

|
|
9
|
+

|
|
10
|
+
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
10
11
|
|
|
11
12
|
dsh(DeepSeek Harness)的自动模型降级插件:当 root agent 或 subagent 的模型请求持续失败(重试耗尽、权限、配额超限、限流 429)时,按角色/模型 fallback 链自动切换 provider/model,当前 step/turn 在目标模型上继续完成——任务不因模型问题中断。
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
两个 dsh 前端均可用:**web** profile(设置 → 插件配置 → Fallbacks 卡片)与 **dsh-tui** 终端 profile(`/fallbacks` + `/fallbacks config`)。
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
dsh plugin --profile web add dsh-llm-fallbacks # 钉版本:加 @<version>
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## 能力一览
|
|
20
|
-
|
|
21
|
-
- **root / subagent 自动降级**:任意 agent 在模型故障下按链切换到下一个可用 provider/model,无需手动换模型。
|
|
22
|
-
- **两块制配置**:块 1 `rootChain`——root 主代理唯一降级链(空 = root 不降级);块 2 声明式角色——`roles.list` 角色实体(id/persona/chain/fallback)供 `roles.rules` 按 id 引用(或内置 `inherit`);未命中规则 → `inherit` → `rootChain`。
|
|
23
|
-
- **条目语法**:链条目为 `provider/model`(精确切换)或 `provider/*`(保留失败模型 id 仅换 provider)——旧链键命名空间(provider/model 键、角色名键)已删除。
|
|
24
|
-
- **冷却与回主**:被切离/失败的模型在冷却期内不再入选;`revertPolicy: cooldown-expiry` 冷却到期后自动回主模型,`never` 会话内不回。
|
|
25
|
-
- **行为可见**:每次切换追加持久化会话事件 `fallbacks/switch`(from/to/role/reason),配合 info 级日志(候选尝试顺序与跳过原因)与设置 → 插件配置 → Fallbacks 卡片上的只读状态块,无静默换模型。
|
|
26
|
-
- **安全阀**:每 step 的 `maxSwitchesPerStep` 超限后停止切换、保持原错误语义,防止链循环放大延迟;`mode: 'always'` 的 provider 另有重试上限(`alwaysModeRetryCap`)。
|
|
27
|
-
- **无配置回归(no-op)**:`enabled` 默认关闭(`false`),无 `rootChain`/角色链 / 未命中触发码 / 角色解析失败时插件完全 no-op——行为与未安装时一致,不产生任何事件。
|
|
28
|
-
|
|
29
|
-
## 安装
|
|
30
|
-
|
|
31
|
-
### 一句话安装
|
|
32
|
-
|
|
33
|
-
```sh
|
|
34
|
-
dsh plugin --profile web add dsh-llm-fallbacks # 钉版本:加 @<version>
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
registry 安装拉取的是**已构建产物**(`dist/`),目标机无需构建。插件为**纯挂载**:对 dsh 源码树零修改,无任何补丁 / postinstall 步骤——dsh 升级无需重打。版本跟随 npm dist-tag(默认 `latest`);钉精确版本:`dsh plugin --profile web add dsh-llm-fallbacks@<version>`。
|
|
38
|
-
|
|
39
|
-
> **发布状态**:已发布 `dsh-llm-fallbacks@0.1.3`(latest)。首次发布用一次性 `NODE_AUTH_TOKEN` bootstrap;Trusted Publishing 后续配置为无 token 发布。
|
|
40
|
-
|
|
41
|
-
### registry 包安装(npm / pnpm)
|
|
42
|
-
|
|
43
|
-
```sh
|
|
44
|
-
npm install dsh-llm-fallbacks # 或:pnpm add dsh-llm-fallbacks
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
> 发布流程见 [docs/release.md](docs/release.md)。
|
|
48
|
-
|
|
49
|
-
### git 安装(当前可用)
|
|
50
|
-
|
|
51
|
-
```sh
|
|
52
|
-
dsh plugin --profile web add github:omdsh-dev/dsh-llm-fallbacks # 钉 commit:加 #<sha>
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
git 安装拉取的是**源码而非构建产物**——安装时由 `prepare` 自构建,目标机需要 node 与 pnpm。pnpm ≥ 10 默认拦截 git 依赖的 `prepare`(`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`);在 profile 的 `pnpm-workspace.yaml` 中放行构建后重跑 `add`(详见 [docs/install.md](docs/install.md))。
|
|
16
|
+
## 快速开始
|
|
56
17
|
|
|
57
|
-
###
|
|
18
|
+
### 安装
|
|
58
19
|
|
|
59
20
|
```sh
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# 2) 装入目标 profile(示例为 web)
|
|
63
|
-
dsh plugin --profile web add .
|
|
21
|
+
dsh plugin --profile web add dsh-llm-fallbacks # web profile(设置 → Fallbacks 卡片)
|
|
22
|
+
dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui 终端 profile
|
|
64
23
|
```
|
|
65
24
|
|
|
66
|
-
|
|
67
|
-
> 来源为 npm registry 的 `0.1.0-rc.6`(`autoInstallPeers` + 用户级 `~/.npmrc` 的 registry
|
|
68
|
-
> 认证令牌——pnpm 11 起项目级 `.npmrc` 不再展开 `${NPM_TOKEN}`,无本地 link farm)。
|
|
69
|
-
|
|
70
|
-
> 两种安装方式、卸载与 `--dump-config` 验证(含 bundle 层顺序要求)详见 [docs/install.md](docs/install.md)。
|
|
71
|
-
|
|
72
|
-
## 快速开始
|
|
25
|
+
同一个插件、两个前端——区别只在 `--profile` 参数。钉版本:加 `@<version>`。registry 安装拉取的是**已构建产物**(`dist/`),目标机无需构建。registry / git / 本地目录变体、卸载与 `--dump-config` 验证 → [docs/install.md](docs/install.md)。
|
|
73
26
|
|
|
74
27
|
### 最小配置
|
|
75
28
|
|
|
@@ -93,69 +46,42 @@ fallbacks:
|
|
|
93
46
|
role: reviewer
|
|
94
47
|
```
|
|
95
48
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
保存并重启 web 会话后生效。功能级开关 `fallbacks.enabled` **默认关闭(`false`)**——打开开关后插件才会介入;`triggerCodes` 默认覆盖 `AUTH` / `QUOTA` / `RATE_LIMIT`;**未配置任何 `rootChain`/角色链时行为与未安装插件完全一致**。更多示例(角色实体、fallback 策略、引用 `inherit` 的规则)见 [docs/configuration.md](docs/configuration.md)。
|
|
49
|
+
未命中规则 → 内置 `inherit` → `rootChain`。`enabled` **默认关闭(`false`)**——未配置任何链时插件完全 no-op。完整参考(角色实体、fallback 策略、规则、selector、预设角色)→ [docs/configuration.md](docs/configuration.md)。
|
|
99
50
|
|
|
100
51
|
> **升级提示(行为变更)**:已有 `fallbacks:` 配置若**未显式写 `enabled` 键**,升级后解析为 `false`——请补上 `enabled: true` 以保持插件继续生效。
|
|
101
52
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
在任意会话中键入 `/fallbacks` 即可查看当前会话的 fallback 状态,无需打开设置页:
|
|
105
|
-
|
|
106
|
-
- **会话来源**(`root` / `subagent`)与**解析角色**(首个命中规则的 `role`,未命中则内置 `inherit`);
|
|
107
|
-
- 该角色的**解析链**(角色自身链 + 继承的 `rootChain`,`fallback: none` 时仅角色链)——无链时显示「未配置」;
|
|
108
|
-
- **最近切换**(`fallbacks/switch` 事件,最新在前,至多 5 条):from/to provider/model、role、reason;
|
|
109
|
-
- **冷却状态**:哪些 `provider/model` 处于冷却、冷却至何时(`revertPolicy: 'never'` 显示「会话内不再回主」)。
|
|
53
|
+
### 验证
|
|
110
54
|
|
|
111
|
-
|
|
55
|
+
保存并重启会话后,键入 `/fallbacks`——只读的会话内诊断(来源、解析角色、链、最近的 `fallbacks/switch` 事件、冷却状态)。在 dsh-tui profile 中,`/fallbacks config` 额外回读组合配置(TUI 无设置页——配置仅文件,见 [docs/configuration.md](docs/configuration.md))。
|
|
112
56
|
|
|
113
|
-
##
|
|
114
|
-
|
|
115
|
-
插件以**纯挂载**方式安装,**从不修改 dsh 源码树**:
|
|
116
|
-
|
|
117
|
-
- **安装 = bundle 行插入 + client inject + 自有 gateway**:`bundle/cordis.patch.yml`
|
|
118
|
-
把插件行插入 profile bundle 栈,`dsh.client.inject` 在设置 → 插件配置页挂载
|
|
119
|
-
Fallbacks 卡片,设置读写/重置
|
|
120
|
-
走插件自有 gateway 通道(`/api/fallbacks/get|set|reset`)。
|
|
121
|
-
- **无补丁、无自动打补丁**:没有 dsh 本体 patch 文件,也没有任何安装期 apply 步骤;
|
|
122
|
-
一句话 registry 安装即可用。
|
|
123
|
-
- **dsh 升级永不需重打**:dsh 升级重置源码树对本插件无影响——无需任何重打步骤。
|
|
124
|
-
- **残留旧补丁无害**:插件不依赖任何补丁导出(角色解析 rules-only;model-selection
|
|
125
|
-
标记协调已移除)——已打过旧补丁的 dsh 树可原样保留或手动回滚,均非必需。
|
|
126
|
-
|
|
127
|
-
## 开发者消费面
|
|
128
|
-
|
|
129
|
-
除插件挂载外,`dsh-llm-fallbacks` 还暴露可编程消费面——两个入口共享同一函数实现(单点真相,无复制逻辑):
|
|
130
|
-
|
|
131
|
-
- **库 API**:从包根直接 import 运行时函数与类型——`import { resolveRole, resolveChain, validateFallbacksConfig } from 'dsh-llm-fallbacks'`。
|
|
132
|
-
- **具名 cordis service**:插件 apply 期间 `ctx.get('llm-fallbacks')` 返回纯函数服务(`{ name, version, resolveRole, resolveChain, validateFallbacksConfig, detectLegacyKeys, declareSeeds, getEffectiveRoles, revertSeededPersona }`),插件 dispose 后为 `undefined`。运行态(冷却、最近切换)请读 `fallbacks/switch` 事件,不走服务对象。
|
|
133
|
-
- **角色 seeds(种子角色)**:伴随插件可**零手工配置**自动补全角色行——`declareSeeds([{ id, persona }])` 声明完整当前种子集(重复声明为 no-op;不匹配 `/^[a-z0-9-]{1,32}$/` 或等于保留字 `inherit` 的 id 按条跳过并告警,绝不改写),`getEffectiveRoles()` 回读哪些角色为 seeded、persona 是否被覆盖,`revertSeededPersona(id)` 恢复**当前声明**的 seed 默认 persona。设置卡显示 seed 默认 / override 徽标并带 revert 按钮;seeds 永不写 `chain` / `fallback`。
|
|
57
|
+
## 能力一览
|
|
134
58
|
|
|
135
|
-
|
|
59
|
+
- **root / subagent 自动降级**:任意 agent 在模型故障下按链切换到下一个可用 provider/model,无需手动换模型。
|
|
60
|
+
- **两块制配置**:`rootChain` 管 root 代理;声明式角色实体(`roles.list`)供 `roles.rules` 引用(或内置 `inherit`)。
|
|
61
|
+
- **冷却与回主**:被切离/失败的模型在冷却期内不再入选;`revertPolicy: cooldown-expiry` 冷却到期后自动回主模型。
|
|
62
|
+
- **行为可见**:每次切换追加持久化会话事件 `fallbacks/switch`(from/to/role/reason),配合 info 级日志——无静默换模型。
|
|
63
|
+
- **安全阀**:`maxSwitchesPerStep` 限制每 step 切换次数、`alwaysModeRetryCap` 限制 always 模式重试——链循环不会放大延迟。
|
|
64
|
+
- **无配置回归(no-op)**:`enabled` 默认关闭;未配置任何链时行为与未安装插件完全一致。
|
|
136
65
|
|
|
137
66
|
## 预设角色(Preset roles)
|
|
138
67
|
|
|
139
|
-
插件内置 **7
|
|
68
|
+
插件内置 **7 个通用子代理角色**,开箱即用——`designer` / `librarian` / `reviewer` / `scout` / `security-reviewer` / `sonic` / `task`——`apply` 时自动以 seeded `roles.list` 行(`{ id, persona }`)声明:幂等,且绝不覆盖 operator 同名 persona。它们出现在设置卡的 seed 徽标(id 不可改)与 `/fallbacks config` 的角色摘要中,可直接被 `roles.rules` 引用。
|
|
140
69
|
|
|
141
|
-
-
|
|
142
|
-
-
|
|
70
|
+
- **开关**:`fallbacks.presets`——`'bundled'`(默认)在 apply 时声明预设角色;`'none'` 关闭自动声明(已物化行保留)。
|
|
71
|
+
- 完整语义(升级行为、冲突处理、`presetRoles` 库复用)→ [docs/configuration.md](docs/configuration.md)。
|
|
143
72
|
|
|
144
|
-
|
|
145
|
-
import { presetRoles } from 'dsh-llm-fallbacks'
|
|
73
|
+
## 纯挂载(零 dsh 修改)
|
|
146
74
|
|
|
147
|
-
|
|
148
|
-
if (fb !== undefined) await fb.declareSeeds(presetRoles) // 或直接使用 FallbacksSeedManager
|
|
149
|
-
```
|
|
75
|
+
插件以**纯挂载**方式安装:bundle 行插入 + client inject + 自有 gateway 通道(`/api/fallbacks/get|set|reset`)——无 dsh 补丁、无 postinstall 步骤,dsh 升级永不需重打。旧版打补丁安装遗留的补丁无害。
|
|
150
76
|
|
|
151
77
|
## 文档
|
|
152
78
|
|
|
153
79
|
| 文档 | 内容 |
|
|
154
80
|
|---|---|
|
|
155
|
-
| [docs/install.md](docs/install.md) | profile
|
|
81
|
+
| [docs/install.md](docs/install.md) | profile 安装(web + dsh-tui)/ registry / git / 本地目录变体 / 卸载 / `--dump-config` 验证 |
|
|
82
|
+
| [docs/configuration.md](docs/configuration.md) | `fallbacks` 命名空间全字段、selector 语法、示例 YAML、插件配置卡使用、TUI 回读、行为说明、预设角色 |
|
|
83
|
+
| [docs/consumer-api.md](docs/consumer-api.md) | 开发者消费契约:库 API + 具名 `llm-fallbacks` service + 角色 seeds、导出清单、生命周期、类型说明 |
|
|
156
84
|
| [docs/release.md](docs/release.md) | 发布流程:Trusted Publishing 前置、Release prep SOP、fragment 格式、回滚 |
|
|
157
|
-
| [docs/configuration.md](docs/configuration.md) | `fallbacks` 命名空间全字段、selector 语法、示例 YAML、插件配置卡使用、行为说明 |
|
|
158
|
-
| [docs/consumer-api.md](docs/consumer-api.md) | 开发者消费契约:导出清单、最小示例、生命周期、类型说明 |
|
|
159
85
|
| [docs/verification.md](docs/verification.md) | 验证记录(测试矩阵、bundle 层序、运行契约、QA gate 剧本) |
|
|
160
86
|
|
|
161
87
|
## 许可
|
package/dist/client/index.js
CHANGED
|
@@ -1966,7 +1966,7 @@ window.__ModuleLoader__.load({
|
|
|
1966
1966
|
placeholder: t("roles.idPlaceholder"),
|
|
1967
1967
|
"aria-label": t("roles.id"),
|
|
1968
1968
|
"aria-invalid": invalid ? true : void 0,
|
|
1969
|
-
disabled: !writable,
|
|
1969
|
+
disabled: !writable || seed !== void 0,
|
|
1970
1970
|
onChange: (event) => {
|
|
1971
1971
|
updateRoleRow(index, { id: event.target.value });
|
|
1972
1972
|
}
|
package/dist/commands.d.ts
CHANGED
|
@@ -73,6 +73,29 @@ export interface FallbacksCommandSnapshot {
|
|
|
73
73
|
/** Active cooldown entries for the agent. */
|
|
74
74
|
readonly cooldown: readonly FallbacksCooldownEntry[];
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The composed-config surface `/fallbacks config` renders (plan
|
|
78
|
+
* fallbacks-tui-client T2, AC-2): the composed `fallbacks` namespace as the
|
|
79
|
+
* runtime sees it (settings user layer included). Roles are summarized from
|
|
80
|
+
* `roles.list` as `{ id, chainCount }` — the two-block model: `roles.list`
|
|
81
|
+
* entities carry id/persona/chain/fallback (NO per-role `model`;
|
|
82
|
+
* `provider`/`model` live on `roles.rules`), so the readback line is id +
|
|
83
|
+
* chain count, never a rules dump.
|
|
84
|
+
*/
|
|
85
|
+
export interface FallbacksConfigSummary {
|
|
86
|
+
readonly enabled: boolean;
|
|
87
|
+
readonly triggerCodes: readonly string[];
|
|
88
|
+
readonly rootChain: readonly string[];
|
|
89
|
+
readonly roles: readonly {
|
|
90
|
+
id: string;
|
|
91
|
+
chainCount: number;
|
|
92
|
+
}[];
|
|
93
|
+
readonly cooldownMs: number;
|
|
94
|
+
readonly revertPolicy: string;
|
|
95
|
+
readonly maxSwitchesPerStep: number;
|
|
96
|
+
readonly alwaysModeRetryCap: number;
|
|
97
|
+
readonly presets: 'bundled' | 'none';
|
|
98
|
+
}
|
|
76
99
|
/**
|
|
77
100
|
* The session-scoped read-only operations the `/fallbacks` handler drives.
|
|
78
101
|
* Implemented by the wiring (`src/index.ts`) against the live config source
|
|
@@ -82,12 +105,20 @@ export interface FallbacksCommandSnapshot {
|
|
|
82
105
|
export interface FallbacksCommandController {
|
|
83
106
|
/** Snapshot the session's fallback diagnostics. Never mutates state. */
|
|
84
107
|
getSnapshot(agent: FallbacksCommandAgent): FallbacksCommandSnapshot;
|
|
108
|
+
/**
|
|
109
|
+
* Snapshot the composed fallbacks config (settings readback). Not
|
|
110
|
+
* agent-scoped — the composed config is session-independent; reads the
|
|
111
|
+
* same live config source the runtime reads. Never mutates state.
|
|
112
|
+
*/
|
|
113
|
+
getConfig(): FallbacksConfigSummary;
|
|
85
114
|
}
|
|
86
115
|
/** zh/en dictionaries for the `/fallbacks` output. */
|
|
87
116
|
export declare const FALLBACKS_COMMAND_LOCALES: {
|
|
88
117
|
readonly zh: {
|
|
89
118
|
readonly title: "当前会话 fallback 诊断(只读)";
|
|
90
119
|
readonly description: "查看当前会话的降级链、最近切换与冷却状态(只读)";
|
|
120
|
+
readonly usageConfig: "查看组合后的 fallbacks 配置(设置回读)";
|
|
121
|
+
readonly usage: " /fallbacks config 查看组合后的 fallbacks 配置(设置回读)";
|
|
91
122
|
readonly origin: "会话来源";
|
|
92
123
|
readonly role: "角色";
|
|
93
124
|
readonly chain: "链";
|
|
@@ -104,10 +135,27 @@ export declare const FALLBACKS_COMMAND_LOCALES: {
|
|
|
104
135
|
readonly 'trigger-code': "触发码";
|
|
105
136
|
readonly 'always-cap': "always 上限";
|
|
106
137
|
};
|
|
138
|
+
readonly configTitle: "Fallbacks 配置";
|
|
139
|
+
readonly configEnabled: "已启用";
|
|
140
|
+
readonly configDisabled: "未启用";
|
|
141
|
+
readonly configTriggerCodes: "触发码";
|
|
142
|
+
readonly configRootChain: "根链";
|
|
143
|
+
readonly configEmpty: "(空)";
|
|
144
|
+
readonly configRoles: "角色";
|
|
145
|
+
readonly configRoleItem: "{id}(chain: {n})";
|
|
146
|
+
readonly configCooldown: "冷却";
|
|
147
|
+
readonly configRevert: "回主策略";
|
|
148
|
+
readonly configMaxSwitches: "单步最大切换";
|
|
149
|
+
readonly configAlwaysCap: "always 上限";
|
|
150
|
+
readonly configPresets: "预置";
|
|
151
|
+
readonly configEdit: "编辑:~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)或 $DSH_HOME/settings.yaml(fallbacks: 分节)";
|
|
152
|
+
readonly configEditHint: "TUI 无法修改配置——只能编辑文件";
|
|
107
153
|
};
|
|
108
154
|
readonly en: {
|
|
109
155
|
readonly title: "Session fallback diagnostics (read-only)";
|
|
110
156
|
readonly description: "Inspect fallback chain, recent switches, and cooldown for this session (read-only)";
|
|
157
|
+
readonly usageConfig: "show the composed fallbacks config (settings readback)";
|
|
158
|
+
readonly usage: " /fallbacks config show the composed fallbacks config (settings readback)";
|
|
111
159
|
readonly origin: "Session origin";
|
|
112
160
|
readonly role: "Role";
|
|
113
161
|
readonly chain: "Chain";
|
|
@@ -124,10 +172,34 @@ export declare const FALLBACKS_COMMAND_LOCALES: {
|
|
|
124
172
|
readonly 'trigger-code': "trigger-code";
|
|
125
173
|
readonly 'always-cap': "always-cap";
|
|
126
174
|
};
|
|
175
|
+
readonly configTitle: "Fallbacks config";
|
|
176
|
+
readonly configEnabled: "enabled";
|
|
177
|
+
readonly configDisabled: "disabled";
|
|
178
|
+
readonly configTriggerCodes: "Trigger codes";
|
|
179
|
+
readonly configRootChain: "Root chain";
|
|
180
|
+
readonly configEmpty: "(empty)";
|
|
181
|
+
readonly configRoles: "Roles";
|
|
182
|
+
readonly configRoleItem: "{id} (chain: {n})";
|
|
183
|
+
readonly configCooldown: "Cooldown";
|
|
184
|
+
readonly configRevert: "Revert";
|
|
185
|
+
readonly configMaxSwitches: "Max switches/step";
|
|
186
|
+
readonly configAlwaysCap: "Always-mode cap";
|
|
187
|
+
readonly configPresets: "Presets";
|
|
188
|
+
readonly configEdit: "Edit: ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) or $DSH_HOME/settings.yaml (fallbacks: section)";
|
|
189
|
+
readonly configEditHint: "TUI cannot change config — edit files only";
|
|
127
190
|
};
|
|
128
191
|
};
|
|
129
192
|
/** A locale id supported by {@link FALLBACKS_COMMAND_LOCALES}. */
|
|
130
193
|
export type FallbacksCommandLocale = keyof typeof FALLBACKS_COMMAND_LOCALES;
|
|
194
|
+
/** The `/fallbacks` subcommands: `'config'` (composed-config readback) or
|
|
195
|
+
* `''` (the bare session snapshot). */
|
|
196
|
+
export type FallbacksSubcommand = '' | 'config';
|
|
197
|
+
/**
|
|
198
|
+
* Map an invocation's rawInput to a subcommand: trimmed `'config'` →
|
|
199
|
+
* `'config'`; everything else (incl. empty) → `''` (bare snapshot). Lenient
|
|
200
|
+
* by design — unknown input keeps today's bare behavior, never errors.
|
|
201
|
+
*/
|
|
202
|
+
export declare function parseFallbacksSubcommand(rawInput: string): FallbacksSubcommand;
|
|
131
203
|
/**
|
|
132
204
|
* The newest `limit` `fallbacks/switch` events from a session's raw event
|
|
133
205
|
* log, newest first. Unknown event shapes and malformed `fallbacks/switch`
|
|
@@ -160,6 +232,21 @@ export declare function resolveChainForDiagnostic(roles: readonly FallbacksRole[
|
|
|
160
232
|
readonly chain: readonly string[];
|
|
161
233
|
readonly inherit: boolean;
|
|
162
234
|
};
|
|
235
|
+
/**
|
|
236
|
+
* Cap for long list lines in the composed-config readback: beyond this many
|
|
237
|
+
* items a line truncates with `…` (the `Roles:` count always stays the FULL
|
|
238
|
+
* count). Same sanity scale as {@link RECENT_SWITCHES_LIMIT}.
|
|
239
|
+
*/
|
|
240
|
+
export declare const FALLBACKS_CONFIG_LIST_CAP = 5;
|
|
241
|
+
/**
|
|
242
|
+
* Render the `/fallbacks config` surface (plan fallbacks-tui-client T2,
|
|
243
|
+
* AC-2): the composed `fallbacks` namespace as the runtime reads it + file-only
|
|
244
|
+
* edit hints (TUI has no write surface). The FIRST LINE marks the
|
|
245
|
+
* composed-config readback — distinct from the diagnostic title and never
|
|
246
|
+
* merged into {@link fallbacksCommandText} (two operator surfaces, product
|
|
247
|
+
* lock). Locale defaults to `zh` (the command default); en dictionary tested.
|
|
248
|
+
*/
|
|
249
|
+
export declare function fallbacksConfigText(summary: FallbacksConfigSummary, locale?: FallbacksCommandLocale): string;
|
|
163
250
|
/**
|
|
164
251
|
* Render the `/fallbacks` status surface for one snapshot. Kept minimal and
|
|
165
252
|
* truthful: origin → role → chain (+ inherit tail) → recent switches →
|
package/dist/index.js
CHANGED
|
@@ -559,10 +559,16 @@ function fallbacksTypertContribution() {
|
|
|
559
559
|
}
|
|
560
560
|
//#endregion
|
|
561
561
|
//#region .build/host/commands.js
|
|
562
|
+
const CONFIG_SUBCOMMAND_DESCRIPTION = {
|
|
563
|
+
zh: "查看组合后的 fallbacks 配置(设置回读)",
|
|
564
|
+
en: "show the composed fallbacks config (settings readback)"
|
|
565
|
+
};
|
|
562
566
|
const FALLBACKS_COMMAND_LOCALES = {
|
|
563
567
|
zh: {
|
|
564
568
|
title: "当前会话 fallback 诊断(只读)",
|
|
565
569
|
description: "查看当前会话的降级链、最近切换与冷却状态(只读)",
|
|
570
|
+
usageConfig: CONFIG_SUBCOMMAND_DESCRIPTION.zh,
|
|
571
|
+
usage: ` /fallbacks config ${CONFIG_SUBCOMMAND_DESCRIPTION.zh}`,
|
|
566
572
|
origin: "会话来源",
|
|
567
573
|
role: "角色",
|
|
568
574
|
chain: "链",
|
|
@@ -578,11 +584,28 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
578
584
|
reason: {
|
|
579
585
|
"trigger-code": "触发码",
|
|
580
586
|
"always-cap": "always 上限"
|
|
581
|
-
}
|
|
587
|
+
},
|
|
588
|
+
configTitle: "Fallbacks 配置",
|
|
589
|
+
configEnabled: "已启用",
|
|
590
|
+
configDisabled: "未启用",
|
|
591
|
+
configTriggerCodes: "触发码",
|
|
592
|
+
configRootChain: "根链",
|
|
593
|
+
configEmpty: "(空)",
|
|
594
|
+
configRoles: "角色",
|
|
595
|
+
configRoleItem: "{id}(chain: {n})",
|
|
596
|
+
configCooldown: "冷却",
|
|
597
|
+
configRevert: "回主策略",
|
|
598
|
+
configMaxSwitches: "单步最大切换",
|
|
599
|
+
configAlwaysCap: "always 上限",
|
|
600
|
+
configPresets: "预置",
|
|
601
|
+
configEdit: "编辑:~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)或 $DSH_HOME/settings.yaml(fallbacks: 分节)",
|
|
602
|
+
configEditHint: "TUI 无法修改配置——只能编辑文件"
|
|
582
603
|
},
|
|
583
604
|
en: {
|
|
584
605
|
title: "Session fallback diagnostics (read-only)",
|
|
585
606
|
description: "Inspect fallback chain, recent switches, and cooldown for this session (read-only)",
|
|
607
|
+
usageConfig: CONFIG_SUBCOMMAND_DESCRIPTION.en,
|
|
608
|
+
usage: ` /fallbacks config ${CONFIG_SUBCOMMAND_DESCRIPTION.en}`,
|
|
586
609
|
origin: "Session origin",
|
|
587
610
|
role: "Role",
|
|
588
611
|
chain: "Chain",
|
|
@@ -598,9 +621,27 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
598
621
|
reason: {
|
|
599
622
|
"trigger-code": "trigger-code",
|
|
600
623
|
"always-cap": "always-cap"
|
|
601
|
-
}
|
|
624
|
+
},
|
|
625
|
+
configTitle: "Fallbacks config",
|
|
626
|
+
configEnabled: "enabled",
|
|
627
|
+
configDisabled: "disabled",
|
|
628
|
+
configTriggerCodes: "Trigger codes",
|
|
629
|
+
configRootChain: "Root chain",
|
|
630
|
+
configEmpty: "(empty)",
|
|
631
|
+
configRoles: "Roles",
|
|
632
|
+
configRoleItem: "{id} (chain: {n})",
|
|
633
|
+
configCooldown: "Cooldown",
|
|
634
|
+
configRevert: "Revert",
|
|
635
|
+
configMaxSwitches: "Max switches/step",
|
|
636
|
+
configAlwaysCap: "Always-mode cap",
|
|
637
|
+
configPresets: "Presets",
|
|
638
|
+
configEdit: "Edit: ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) or $DSH_HOME/settings.yaml (fallbacks: section)",
|
|
639
|
+
configEditHint: "TUI cannot change config — edit files only"
|
|
602
640
|
}
|
|
603
641
|
};
|
|
642
|
+
function parseFallbacksSubcommand(rawInput) {
|
|
643
|
+
return rawInput.trim() === "config" ? "config" : "";
|
|
644
|
+
}
|
|
604
645
|
function isFallbacksSwitchData(data) {
|
|
605
646
|
if (typeof data !== "object" || data === null) return false;
|
|
606
647
|
const payload = data;
|
|
@@ -646,6 +687,32 @@ function formatCooldown(entry, t) {
|
|
|
646
687
|
if (!Number.isFinite(entry.untilEpochMs)) return t.cooldownNever.replace("{key}", entry.key);
|
|
647
688
|
return t.cooldownLine.replace("{key}", entry.key).replace("{time}", new Date(entry.untilEpochMs).toISOString());
|
|
648
689
|
}
|
|
690
|
+
function formatConfigList(items) {
|
|
691
|
+
if (items.length <= 5) return items.join(", ");
|
|
692
|
+
return [...items.slice(0, 5), "…"].join(", ");
|
|
693
|
+
}
|
|
694
|
+
function formatConfigRoles(roles, t) {
|
|
695
|
+
const items = roles.slice(0, 5).map((role) => t.configRoleItem.replace("{id}", role.id).replace("{n}", String(role.chainCount)));
|
|
696
|
+
const list = items.length === 0 ? "" : `${items.join(", ")}${roles.length > 5 ? ", …" : ""}`;
|
|
697
|
+
return `${roles.length}${list.length === 0 ? "" : ` — ${list}`}`;
|
|
698
|
+
}
|
|
699
|
+
function fallbacksConfigText(summary, locale = "zh") {
|
|
700
|
+
const t = FALLBACKS_COMMAND_LOCALES[locale];
|
|
701
|
+
return [
|
|
702
|
+
`${t.configTitle}: ${summary.enabled ? t.configEnabled : t.configDisabled}`,
|
|
703
|
+
`${t.configTriggerCodes}: ${summary.triggerCodes.length === 0 ? t.configEmpty : formatConfigList(summary.triggerCodes)}`,
|
|
704
|
+
`${t.configRootChain}: ${summary.rootChain.length === 0 ? t.configEmpty : formatConfigList(summary.rootChain)}`,
|
|
705
|
+
`${t.configRoles}: ${formatConfigRoles(summary.roles, t)}`,
|
|
706
|
+
`${t.configCooldown}: ${summary.cooldownMs} ms`,
|
|
707
|
+
`${t.configRevert}: ${summary.revertPolicy}`,
|
|
708
|
+
`${t.configMaxSwitches}: ${summary.maxSwitchesPerStep}`,
|
|
709
|
+
`${t.configAlwaysCap}: ${summary.alwaysModeRetryCap}`,
|
|
710
|
+
`${t.configPresets}: ${summary.presets}`,
|
|
711
|
+
"",
|
|
712
|
+
t.configEdit,
|
|
713
|
+
t.configEditHint
|
|
714
|
+
].join("\n");
|
|
715
|
+
}
|
|
649
716
|
function fallbacksCommandText(snapshot, locale = "zh") {
|
|
650
717
|
const t = FALLBACKS_COMMAND_LOCALES[locale];
|
|
651
718
|
const lines = [t.title];
|
|
@@ -671,14 +738,13 @@ function fallbacksCommandText(snapshot, locale = "zh") {
|
|
|
671
738
|
function createFallbacksCommandHandler(controller, locale = "zh") {
|
|
672
739
|
return (invocation) => ({
|
|
673
740
|
kind: "success",
|
|
674
|
-
text: fallbacksCommandText(controller.getSnapshot(invocation.agent), locale)
|
|
741
|
+
text: parseFallbacksSubcommand(invocation.rawInput ?? "") === "config" ? fallbacksConfigText(controller.getConfig(), locale) : fallbacksCommandText(controller.getSnapshot(invocation.agent), locale)
|
|
675
742
|
});
|
|
676
743
|
}
|
|
677
744
|
function registerFallbacksCommands(registry, controller, locale = "zh") {
|
|
678
745
|
return registry.register({
|
|
679
746
|
name: "fallbacks",
|
|
680
747
|
description: FALLBACKS_COMMAND_LOCALES[locale].description,
|
|
681
|
-
input: { hint: "" },
|
|
682
748
|
handler: createFallbacksCommandHandler(controller, locale)
|
|
683
749
|
});
|
|
684
750
|
}
|
|
@@ -897,6 +963,44 @@ const presetRoles = [
|
|
|
897
963
|
}
|
|
898
964
|
];
|
|
899
965
|
//#endregion
|
|
966
|
+
//#region .build/host/tui.js
|
|
967
|
+
const FALLBACKS_TUI_ROOT = "fallbacks";
|
|
968
|
+
const FALLBACKS_CONFIG_NODE = {
|
|
969
|
+
name: "config",
|
|
970
|
+
description: FALLBACKS_COMMAND_LOCALES.zh.usageConfig,
|
|
971
|
+
descriptions: {
|
|
972
|
+
zh: FALLBACKS_COMMAND_LOCALES.zh.usageConfig,
|
|
973
|
+
en: FALLBACKS_COMMAND_LOCALES.en.usageConfig
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
function fallbacksChildren(canonicalPath) {
|
|
977
|
+
if (canonicalPath[0] !== "fallbacks") return [];
|
|
978
|
+
if (canonicalPath.length === 1) return [FALLBACKS_CONFIG_NODE];
|
|
979
|
+
return [];
|
|
980
|
+
}
|
|
981
|
+
const FALLBACKS_PROVIDER = {
|
|
982
|
+
root: FALLBACKS_TUI_ROOT,
|
|
983
|
+
descriptions: {
|
|
984
|
+
zh: FALLBACKS_COMMAND_LOCALES.zh.description,
|
|
985
|
+
en: FALLBACKS_COMMAND_LOCALES.en.description
|
|
986
|
+
},
|
|
987
|
+
children: fallbacksChildren
|
|
988
|
+
};
|
|
989
|
+
function installTuiClient(ctx, opts) {
|
|
990
|
+
if (!opts.serviceOwned) return;
|
|
991
|
+
ctx.inject(["tuiCommandTrees"], (tctx) => {
|
|
992
|
+
const trees = tctx.tuiCommandTrees;
|
|
993
|
+
if (trees === void 0) return;
|
|
994
|
+
try {
|
|
995
|
+
return trees.register(FALLBACKS_PROVIDER);
|
|
996
|
+
} catch (error) {
|
|
997
|
+
if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
|
|
998
|
+
tctx.logger("llm-fallbacks").debug("llm-fallbacks: tui command tree already registered — no provider on this fiber");
|
|
999
|
+
return () => {};
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
//#endregion
|
|
900
1004
|
//#region .build/host/index.js
|
|
901
1005
|
const name = "llm-fallbacks";
|
|
902
1006
|
const provide = ["llm-fallbacks"];
|
|
@@ -1120,21 +1224,41 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
1120
1224
|
ctx.effect(() => () => {
|
|
1121
1225
|
states.clear();
|
|
1122
1226
|
}, "llm-fallbacks: clear per-agent state");
|
|
1123
|
-
const fallbacksCommandController = {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1227
|
+
const fallbacksCommandController = {
|
|
1228
|
+
getSnapshot(agent) {
|
|
1229
|
+
const config = source();
|
|
1230
|
+
const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn);
|
|
1231
|
+
const state = states.peek(agent.id);
|
|
1232
|
+
return {
|
|
1233
|
+
origin: agent.session.header?.origin ?? "root",
|
|
1234
|
+
role,
|
|
1235
|
+
...resolveChainForDiagnostic(config.roles.list, config.rootChain, role, logger.warn),
|
|
1236
|
+
switches: recentFallbacksSwitches(agent.session.events, 5),
|
|
1237
|
+
cooldown: state === void 0 ? [] : state.cooldown.snapshot()
|
|
1238
|
+
};
|
|
1239
|
+
},
|
|
1240
|
+
getConfig() {
|
|
1241
|
+
const config = source();
|
|
1242
|
+
return {
|
|
1243
|
+
enabled: config.enabled,
|
|
1244
|
+
triggerCodes: config.triggerCodes,
|
|
1245
|
+
rootChain: config.rootChain,
|
|
1246
|
+
roles: config.roles.list.map((role) => ({
|
|
1247
|
+
id: role.id,
|
|
1248
|
+
chainCount: role.chain?.length ?? 0
|
|
1249
|
+
})),
|
|
1250
|
+
cooldownMs: config.cooldownMs,
|
|
1251
|
+
revertPolicy: config.revertPolicy,
|
|
1252
|
+
maxSwitchesPerStep: config.maxSwitchesPerStep,
|
|
1253
|
+
alwaysModeRetryCap: config.alwaysModeRetryCap,
|
|
1254
|
+
presets: config.presets ?? "bundled"
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1135
1258
|
ctx.inject(["commands"], (commandCtx) => {
|
|
1136
1259
|
return registerFallbacksCommands(commandCtx.commands, fallbacksCommandController);
|
|
1137
1260
|
});
|
|
1261
|
+
installTuiClient(ctx, { serviceOwned });
|
|
1138
1262
|
ctx.inject(["settings"], () => {
|
|
1139
1263
|
if (!serviceOwned) return;
|
|
1140
1264
|
if (seedsIo.read().presets === "none") return;
|
package/dist/tui.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-tui client surface (plan fallbacks-tui-client Task 1, AC-1): registers
|
|
3
|
+
* a `tuiCommandTrees` provider for the `/fallbacks` command so the dsh-tui
|
|
4
|
+
* profile's `/` menu shows the command with localized descriptions and
|
|
5
|
+
* `config` subcommand completion — with ZERO dsh-TUI changes.
|
|
6
|
+
*
|
|
7
|
+
* The service and its shapes are consumed structurally (read-only reference:
|
|
8
|
+
* dsh-TUI @ 557a27a, `src/dsh-adapter/command-trees.ts` +
|
|
9
|
+
* `src/commands.ts`): the three types below are minimal local copies of the
|
|
10
|
+
* host's `TuiCommandTreeProvider` / `LocalizedDescriptions` /
|
|
11
|
+
* `CommandCompletionNode`, so no `@deepseek-harness-tui/dsh-tui` peer is
|
|
12
|
+
* needed (plan constraint: zero new peer/dependency).
|
|
13
|
+
*
|
|
14
|
+
* Copy comes from the single command copy source: root descriptions reuse
|
|
15
|
+
* `FALLBACKS_COMMAND_LOCALES.*.description`; the `config` completion node
|
|
16
|
+
* reuses the `usageConfig` key (the same key Task 2's USAGE line consumes).
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
/** Localized copy map for a tree row (host `LocalizedDescriptions` shape). */
|
|
20
|
+
export type TuiLocalizedDescriptions = Readonly<Partial<Record<'zh' | 'en', string>>>;
|
|
21
|
+
/** One child in a slash-command tree (host `CommandCompletionNode` shape). */
|
|
22
|
+
export interface TuiCommandCompletionNode {
|
|
23
|
+
name: string;
|
|
24
|
+
aliases?: readonly string[];
|
|
25
|
+
description: string;
|
|
26
|
+
descriptions?: TuiLocalizedDescriptions;
|
|
27
|
+
tag?: string;
|
|
28
|
+
/** Optional i18n key; plugin nodes normally rely on fallback text. */
|
|
29
|
+
descriptionKey?: string;
|
|
30
|
+
}
|
|
31
|
+
/** A `tuiCommandTrees` provider (host `TuiCommandTreeProvider` shape). */
|
|
32
|
+
export interface TuiCommandTreeProvider {
|
|
33
|
+
/** Root command name without `/`. Must match the command registry entry. */
|
|
34
|
+
root: string;
|
|
35
|
+
/** Provider-owned translations for the root command row. */
|
|
36
|
+
descriptions?: TuiLocalizedDescriptions;
|
|
37
|
+
/** Children for the full canonical path, including `root` at index zero. */
|
|
38
|
+
children(canonicalPath: readonly string[]): readonly TuiCommandCompletionNode[];
|
|
39
|
+
}
|
|
40
|
+
/** The provider's root — matches the command registry entry name `fallbacks`. */
|
|
41
|
+
export declare const FALLBACKS_TUI_ROOT = "fallbacks";
|
|
42
|
+
/**
|
|
43
|
+
* Register the `/fallbacks` provider on the optional `tuiCommandTrees`
|
|
44
|
+
* service. First-fiber-only (`serviceOwned === true` — mirrors the
|
|
45
|
+
* gateway/typert multi-fiber dedupe; the host registry throws on duplicate
|
|
46
|
+
* roots, so a deduped later fiber must never register). The service is
|
|
47
|
+
* optional: a composition without `dsh-tui-command-trees` keeps the plugin
|
|
48
|
+
* working and simply omits the TUI surface.
|
|
49
|
+
*
|
|
50
|
+
* The inject child returns the registry disposer so cordis withdraws the
|
|
51
|
+
* registration when this fiber (or the service) goes away.
|
|
52
|
+
*/
|
|
53
|
+
export declare function installTuiClient(ctx: Context, opts: {
|
|
54
|
+
serviceOwned: boolean;
|
|
55
|
+
}): void;
|