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 CHANGED
@@ -5,73 +5,24 @@
5
5
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
7
7
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
8
- ![dsh](https://img.shields.io/badge/dsh-DeepSeek%20Harness%20compatible-4B32C3.svg)
8
+ ![dsh web](https://img.shields.io/badge/dsh%20web-compatible-4B32C3.svg)
9
+ ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
9
10
  [![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)
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
- Install with a single command (see [Install](#install)):
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
- ```sh
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
- ### Local directory install (recommended for development / verification)
18
+ ### Install
58
19
 
59
20
  ```sh
60
- # 1) Build in the plugin repo (the prepare self-build runs the pnpm toolchain: tsdown + tsc, no bun)
61
- pnpm install
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
- > **Development prerequisite**: type-checking and tests resolve the real
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; tried in order after the primary model fails
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 roles first, then let rules reference them
37
+ roles: # block 2: declare role entities first, then let rules reference them
87
38
  list:
88
- - id: reviewer # role entity: unique id (/^[a-z0-9-]{1,32}$/); "inherit" is reserved
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
- Roles are declared entities: `roles.list` holds role cards (id/persona + chain/fallback), and `roles.rules` match origin/provider/model in order to a declared role id or the built-in `inherit` (first match wins) — **no rule match → `inherit` → `rootChain`**. A declared role is only ever hit when a rule references it. A role without a chain is meaningless — the settings card blocks saving it and the host warns at startup; give every declared role a chain, or point its rules at the built-in `inherit`. (A hand-written YAML role with a missing/empty chain is not fatal: with `fallback: inherit-root` (the default) an empty chain still falls back to `rootChain`; with `fallback: none` an empty chain leaves no candidates and the request passes through untouched — either way it is only a startup warn.) The legacy chain-key namespace and role-default field are gone (migration table: [docs/configuration.md](docs/configuration.md)).
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
- Type `/fallbacks` in any session to inspect this session's fallback state — no need to open the settings page:
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
- - **Session origin** (`root` / `subagent`) and the **resolved role** (the `role` of the first matching `roles.rules` entry, otherwise the built-in `inherit`);
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
- The command is **read-only** — it never mutates fallback state (no cooldown reset, no pending-switch writes). It registers through a conditional `commands` child, so it appears only when the host composes the slash-command registry — with no registry the command is silently unavailable (no top-level inject pollution). Output is zh by default (the host carries no per-session locale signal); the en dictionary lives in the same copy table.
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
- ## Mount-only (no dsh modification)
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
- Full contract (export inventory, minimal examples, lifecycle, typing) → [docs/consumer-api.md](docs/consumer-api.md).
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 a **bundled taxonomy of 7 generic subagent roles** available out of the box — `designer` / `librarian` / `reviewer` / `scout` / `security-reviewer` / `sonic` / `task` — declared automatically on `apply` as seeded `roles.list` rows (`{ id, persona }`, two keys only): idempotent, and never overwriting an operator persona. Each persona is a concise instruction set distilled from the omp bundled agent prompts (`packages/coding-agent/src/prompts/agents/`, snapshot 2026-08-16).
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
- - **Config switch**: `fallbacks.presets` — `'bundled'` (default) declares the preset roles on apply; `'none'` disables the automatic declaration (zero declarations, zero writes from this switch; already-materialized rows stay). Full semantics (upgrade behavior, conflict handling, honest deletion limitation) → [docs/configuration.md](docs/configuration.md).
147
- - **Library reuse**: the same 7 declarations are exported from the package root — one line, the identical payload the plugin self-declares:
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
- ```ts
150
- import { presetRoles } from 'dsh-llm-fallbacks'
73
+ ## Mount-only (no dsh modification)
151
74
 
152
- const fb = ctx.get('llm-fallbacks') // plugin applied → service face
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 install / uninstall / `--dump-config` verification |
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](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
7
7
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
8
- ![dsh](https://img.shields.io/badge/dsh-DeepSeek%20Harness%20compatible-4B32C3.svg)
9
- [![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)
8
+ ![dsh web](https://img.shields.io/badge/dsh%20web-compatible-4B32C3.svg)
9
+ ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
10
+ [![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)
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
- ```sh
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
- # 1) 在插件仓库目录构建(prepare 自构建走 pnpm 工具链:tsdown + tsc,无 bun)
61
- pnpm install
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
- > **开发前置**:类型检查与测试解析的是**真实** `@deepseek-ai/*` 包(peer 依赖,运行时由宿主提供),
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
- 角色是声明式实体:`roles.list` 存放角色卡(id/persona + chain/fallback),`roles.rules` 按 origin/provider/model 顺序匹配到**已声明角色 id 或内置 `inherit`**(首个命中即停)——**未命中 → `inherit` → `rootChain`**。只声明不写规则的角色永不命中。角色无 chain 无意义——设置卡拦截保存、启动时告警;请为每个声明角色配置 chain,或让规则直接引用内置 `inherit`。(手写 YAML 角色链缺省/为空不致命:`fallback: inherit-root`(默认)时空链仍按防御语义回退 `rootChain`;`fallback: none` 时空链无候选 → no-op 透传;两种情况都仅启动告警。)旧链键命名空间与角色兜底字段已删除(迁移表见 [docs/configuration.md](docs/configuration.md))。
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
- ## `/fallbacks` 命令(会话内诊断)
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
- 命令**只读**——绝不修改 fallback 状态(不重置冷却、不写待应用切换)。它经条件 `commands` 子注入注册,仅在宿主组合了斜杠命令注册表时出现;无注册表时命令静默不可用(无顶层 inject 污染)。输出默认中文(宿主侧无会话级 locale 信号);英文词典在同一副本表中。
55
+ 保存并重启会话后,键入 `/fallbacks`——只读的会话内诊断(来源、解析角色、链、最近的 `fallbacks/switch` 事件、冷却状态)。在 dsh-tui profile 中,`/fallbacks config` 额外回读组合配置(TUI 无设置页——配置仅文件,见 [docs/configuration.md](docs/configuration.md))。
112
56
 
113
- ## 纯挂载(零 dsh 修改)
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
- 完整契约(导出清单、最小示例、生命周期、类型说明)见 [docs/consumer-api.md](docs/consumer-api.md)。
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 个通用子代理角色 taxonomy**,开箱即用——`designer` / `librarian` / `reviewer` / `scout` / `security-reviewer` / `sonic` / `task`——`apply` 时自动以 seeded `roles.list` 行(`{ id, persona }` 两键)声明:幂等,且绝不覆盖 operator 同名 persona。每个 persona 是从 omp bundled agent 提示词蒸馏的精简指令文(来源 `packages/coding-agent/src/prompts/agents/`,快照 2026-08-16)。
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
- - **配置开关**:`fallbacks.presets`——`'bundled'`(默认)在 apply 时声明预设角色;`'none'` 关闭自动声明(零声明零写,已物化行保留)。完整语义(升级行为、冲突处理、手工删行的诚实限制)见 [docs/configuration.md](docs/configuration.md)。
142
- - **库复用**:同一份 7 项声明从包根导出——一行复用与插件自声明完全相同的 payload:
70
+ - **开关**:`fallbacks.presets`——`'bundled'`(默认)在 apply 时声明预设角色;`'none'` 关闭自动声明(已物化行保留)。
71
+ - 完整语义(升级行为、冲突处理、`presetRoles` 库复用)→ [docs/configuration.md](docs/configuration.md)。
143
72
 
144
- ```ts
145
- import { presetRoles } from 'dsh-llm-fallbacks'
73
+ ## 纯挂载(零 dsh 修改)
146
74
 
147
- const fb = ctx.get('llm-fallbacks') // 插件已 apply → service 面
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 安装 / registry 安装 / 卸载 / `--dump-config` 验证 |
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
  ## 许可
@@ -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
  }
@@ -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 = { getSnapshot(agent) {
1124
- const config = source();
1125
- const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn);
1126
- const state = states.peek(agent.id);
1127
- return {
1128
- origin: agent.session.header?.origin ?? "root",
1129
- role,
1130
- ...resolveChainForDiagnostic(config.roles.list, config.rootChain, role, logger.warn),
1131
- switches: recentFallbacksSwitches(agent.session.events, 5),
1132
- cooldown: state === void 0 ? [] : state.cooldown.snapshot()
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;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "dsh",
6
6
  "dsh-plugin"
7
7
  ],
8
- "version": "0.1.6",
8
+ "version": "0.2.0",
9
9
  "type": "module",
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",