dsh-llm-fallbacks 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,281 @@
1
+ import type { FallbackSwitchReason } from '../events.ts';
2
+ /** Simplified Chinese dictionary (the key-set source of truth). */
3
+ export declare const zh: {
4
+ title: string;
5
+ intro: string;
6
+ collapse: string;
7
+ expand: string;
8
+ unsaved: string;
9
+ discard: string;
10
+ retry: string;
11
+ readOnly: string;
12
+ 'enabled.label': string;
13
+ 'enabled.hint': string;
14
+ 'enabled.tooltip': string;
15
+ 'enabled.off': string;
16
+ 'triggerCodes.label': string;
17
+ 'triggerCodes.hint': string;
18
+ 'triggerCodes.tooltip': string;
19
+ 'triggerCodes.RATE_LIMIT': string;
20
+ 'triggerCodes.QUOTA': string;
21
+ 'triggerCodes.AUTH': string;
22
+ 'triggerCodes.extra': string;
23
+ 'revertPolicy.label': string;
24
+ 'revertPolicy.cooldown-expiry': string;
25
+ 'revertPolicy.never': string;
26
+ 'revertPolicy.hint': string;
27
+ 'revertPolicy.tooltip': string;
28
+ 'cooldownMs.label': string;
29
+ 'cooldownMs.hint': string;
30
+ 'cooldownMs.tooltip': string;
31
+ 'maxSwitchesPerStep.label': string;
32
+ 'maxSwitchesPerStep.hint': string;
33
+ 'maxSwitchesPerStep.tooltip': string;
34
+ 'alwaysModeRetryCap.label': string;
35
+ 'alwaysModeRetryCap.hint': string;
36
+ 'alwaysModeRetryCap.tooltip': string;
37
+ 'rootChain.label': string;
38
+ 'rootChain.hint': string;
39
+ 'rootChain.tooltip': string;
40
+ 'rootChain.selector.add': string;
41
+ 'chains.selector.remove': string;
42
+ 'chains.selector.providerPlaceholder': string;
43
+ 'chains.selector.modelPlaceholder': string;
44
+ 'chains.selector.wildcard': string;
45
+ 'chains.selector.noModels': string;
46
+ 'roles.list.label': string;
47
+ 'roles.list.hint': string;
48
+ 'roles.list.tooltip': string;
49
+ 'roles.id': string;
50
+ 'roles.id.hint': string;
51
+ 'roles.idPlaceholder': string;
52
+ 'roles.label': string;
53
+ 'roles.description': string;
54
+ 'roles.fallback': string;
55
+ 'roles.fallback.inherit-root': string;
56
+ 'roles.fallback.none': string;
57
+ 'roles.add': string;
58
+ 'roles.remove': string;
59
+ 'roles.selector.add': string;
60
+ 'roles.rules': string;
61
+ 'roles.rules.hint': string;
62
+ 'roles.rules.tooltip': string;
63
+ 'roles.rule.origin': string;
64
+ 'roles.rule.origin.any': string;
65
+ 'roles.rule.origin.root': string;
66
+ 'roles.rule.origin.subagent': string;
67
+ 'roles.rule.provider': string;
68
+ 'roles.rule.provider.any': string;
69
+ 'roles.rule.model': string;
70
+ 'roles.rule.model.any': string;
71
+ 'roles.rule.role': string;
72
+ 'roles.rule.role.inherit': string;
73
+ 'roles.rule.roleSelectPlaceholder': string;
74
+ 'roles.rule.roleUndeclared.short': string;
75
+ 'roles.addRule': string;
76
+ 'roles.removeRule': string;
77
+ 'validation.blocked': string;
78
+ 'validation.roleIdFormat': string;
79
+ 'validation.roleIdReserved': string;
80
+ 'validation.roleIdDuplicate': string;
81
+ 'validation.ruleRoleUndeclared': string;
82
+ 'validation.ruleRoleRequired': string;
83
+ 'validation.selector': string;
84
+ 'legacy.banner': string;
85
+ 'catalog.empty': string;
86
+ 'catalog.error': string;
87
+ 'catalog.partial': string;
88
+ 'catalog.outside.hint': string;
89
+ 'catalog.outside.tooltip': string;
90
+ 'catalog.outside.short': string;
91
+ 'catalog.unconfigured.short': string;
92
+ 'status.title': string;
93
+ 'status.effectiveModel.label': string;
94
+ 'status.effectiveModel.unavailable': string;
95
+ 'status.effectiveModel.note': string;
96
+ 'status.switches.label': string;
97
+ 'status.switches.empty': string;
98
+ 'status.switches.error': string;
99
+ 'status.switches.compact': string;
100
+ 'status.switches.reason.trigger-code': string;
101
+ 'status.switches.reason.always-cap': string;
102
+ 'status.selectionNote': string;
103
+ 'general.title': string;
104
+ 'general.enabled': string;
105
+ 'general.disabled': string;
106
+ 'general.unknown': string;
107
+ 'general.unavailable': string;
108
+ 'general.switch': string;
109
+ 'general.switch.empty': string;
110
+ 'general.error': string;
111
+ 'chat.switch.title': string;
112
+ 'chat.switch.summary': string;
113
+ 'defaults.prefix': string;
114
+ save: string;
115
+ 'save.saving': string;
116
+ 'save.error': string;
117
+ close: string;
118
+ reset: string;
119
+ 'reset.confirmTitle': string;
120
+ 'reset.confirm': string;
121
+ 'reset.confirm.cancel': string;
122
+ 'reset.confirm.action': string;
123
+ 'reset.saving': string;
124
+ loading: string;
125
+ unavailable: string;
126
+ 'error.generic': string;
127
+ };
128
+ /** The fallbacks dictionary key union. */
129
+ export type FallbacksKey = keyof typeof zh;
130
+ /** English dictionary, checked complete against the zh key set. */
131
+ export declare const en: {
132
+ title: string;
133
+ intro: string;
134
+ collapse: string;
135
+ expand: string;
136
+ unsaved: string;
137
+ discard: string;
138
+ retry: string;
139
+ readOnly: string;
140
+ 'enabled.label': string;
141
+ 'enabled.hint': string;
142
+ 'enabled.tooltip': string;
143
+ 'enabled.off': string;
144
+ 'triggerCodes.label': string;
145
+ 'triggerCodes.hint': string;
146
+ 'triggerCodes.tooltip': string;
147
+ 'triggerCodes.RATE_LIMIT': string;
148
+ 'triggerCodes.QUOTA': string;
149
+ 'triggerCodes.AUTH': string;
150
+ 'triggerCodes.extra': string;
151
+ 'revertPolicy.label': string;
152
+ 'revertPolicy.cooldown-expiry': string;
153
+ 'revertPolicy.never': string;
154
+ 'revertPolicy.hint': string;
155
+ 'revertPolicy.tooltip': string;
156
+ 'cooldownMs.label': string;
157
+ 'cooldownMs.hint': string;
158
+ 'cooldownMs.tooltip': string;
159
+ 'maxSwitchesPerStep.label': string;
160
+ 'maxSwitchesPerStep.hint': string;
161
+ 'maxSwitchesPerStep.tooltip': string;
162
+ 'alwaysModeRetryCap.label': string;
163
+ 'alwaysModeRetryCap.hint': string;
164
+ 'alwaysModeRetryCap.tooltip': string;
165
+ 'rootChain.label': string;
166
+ 'rootChain.hint': string;
167
+ 'rootChain.tooltip': string;
168
+ 'rootChain.selector.add': string;
169
+ 'chains.selector.remove': string;
170
+ 'chains.selector.providerPlaceholder': string;
171
+ 'chains.selector.modelPlaceholder': string;
172
+ 'chains.selector.wildcard': string;
173
+ 'chains.selector.noModels': string;
174
+ 'roles.list.label': string;
175
+ 'roles.list.hint': string;
176
+ 'roles.list.tooltip': string;
177
+ 'roles.id': string;
178
+ 'roles.id.hint': string;
179
+ 'roles.idPlaceholder': string;
180
+ 'roles.label': string;
181
+ 'roles.description': string;
182
+ 'roles.fallback': string;
183
+ 'roles.fallback.inherit-root': string;
184
+ 'roles.fallback.none': string;
185
+ 'roles.add': string;
186
+ 'roles.remove': string;
187
+ 'roles.selector.add': string;
188
+ 'roles.rules': string;
189
+ 'roles.rules.hint': string;
190
+ 'roles.rules.tooltip': string;
191
+ 'roles.rule.origin': string;
192
+ 'roles.rule.origin.any': string;
193
+ 'roles.rule.origin.root': string;
194
+ 'roles.rule.origin.subagent': string;
195
+ 'roles.rule.provider': string;
196
+ 'roles.rule.provider.any': string;
197
+ 'roles.rule.model': string;
198
+ 'roles.rule.model.any': string;
199
+ 'roles.rule.role': string;
200
+ 'roles.rule.role.inherit': string;
201
+ 'roles.rule.roleSelectPlaceholder': string;
202
+ 'roles.rule.roleUndeclared.short': string;
203
+ 'roles.addRule': string;
204
+ 'roles.removeRule': string;
205
+ 'validation.blocked': string;
206
+ 'validation.roleIdFormat': string;
207
+ 'validation.roleIdReserved': string;
208
+ 'validation.roleIdDuplicate': string;
209
+ 'validation.ruleRoleUndeclared': string;
210
+ 'validation.ruleRoleRequired': string;
211
+ 'validation.selector': string;
212
+ 'legacy.banner': string;
213
+ 'catalog.empty': string;
214
+ 'catalog.error': string;
215
+ 'catalog.partial': string;
216
+ 'catalog.outside.hint': string;
217
+ 'catalog.outside.tooltip': string;
218
+ 'catalog.outside.short': string;
219
+ 'catalog.unconfigured.short': string;
220
+ 'status.title': string;
221
+ 'status.effectiveModel.label': string;
222
+ 'status.effectiveModel.unavailable': string;
223
+ 'status.effectiveModel.note': string;
224
+ 'status.switches.label': string;
225
+ 'status.switches.empty': string;
226
+ 'status.switches.error': string;
227
+ 'status.switches.compact': string;
228
+ 'status.switches.reason.trigger-code': string;
229
+ 'status.switches.reason.always-cap': string;
230
+ 'status.selectionNote': string;
231
+ 'general.title': string;
232
+ 'general.enabled': string;
233
+ 'general.disabled': string;
234
+ 'general.unknown': string;
235
+ 'general.unavailable': string;
236
+ 'general.switch': string;
237
+ 'general.switch.empty': string;
238
+ 'general.error': string;
239
+ 'chat.switch.title': string;
240
+ 'chat.switch.summary': string;
241
+ 'defaults.prefix': string;
242
+ save: string;
243
+ 'save.saving': string;
244
+ 'save.error': string;
245
+ close: string;
246
+ reset: string;
247
+ 'reset.confirmTitle': string;
248
+ 'reset.confirm': string;
249
+ 'reset.confirm.cancel': string;
250
+ 'reset.confirm.action': string;
251
+ 'reset.saving': string;
252
+ loading: string;
253
+ unavailable: string;
254
+ 'error.generic': string;
255
+ };
256
+ /** The settings section's dictionary namespace. */
257
+ export declare const NS = "fallbacks";
258
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
259
+ interface LocaleNamespaceMap {
260
+ /** This feature's settings-section copy. */
261
+ fallbacks: FallbacksKey;
262
+ }
263
+ }
264
+ /**
265
+ * Reason → locale key map for switch summaries (S-c; shared by the card's
266
+ * status block and the General page status row). The session log is durable
267
+ * and forward-compatible: a reason value outside the current union (a newer
268
+ * plugin wrote it) renders raw instead of falling into a binary else branch.
269
+ */
270
+ export declare const SWITCH_REASON_KEYS: Readonly<Partial<Record<FallbackSwitchReason, FallbacksKey>>>;
271
+ /** Human-readable trigger-code labels (spec §4 用户直观性). */
272
+ export declare const TRIGGER_CODE_LABELS: Readonly<Record<string, FallbacksKey>>;
273
+ /**
274
+ * The known trigger codes the form toggles; unknown codes are preserved.
275
+ * M-04: derived from the host defaults so the toggle set can never drift from
276
+ * the decision set (`defaultFallbacksConfig.triggerCodes` is the single
277
+ * source of truth; the labels mapping above stays keyed by code).
278
+ */
279
+ export declare const KNOWN_TRIGGER_CODES: readonly string[];
280
+ /** Toggle one known code's membership in `codes` (used by the form; pure). */
281
+ export declare function withTriggerCode(codes: readonly string[], code: string, present: boolean): string[];
@@ -0,0 +1,17 @@
1
+ import type { FallbacksSwitchEventData } from '../events.ts';
2
+ /**
3
+ * True when `value` is a well-formed `fallbacks/switch` payload — the ONE
4
+ * client-side shape guard, shared by the conversation node definition's
5
+ * `match`/`start` and the renderer's degrade check (both in
6
+ * `src/client/ConversationFallbackSwitch.tsx`).
7
+ *
8
+ * The durable session log is append-only and survives plugin/host upgrades,
9
+ * so a `fallbacks/switch` event or node payload may carry a stale or
10
+ * corrupted shape — version skew must degrade the transcript line (a
11
+ * title-only notice), never crash the session assembly or the renderer.
12
+ *
13
+ * The HOST-side mirror (`src/commands.ts` `isFallbacksSwitchData`) lives in
14
+ * a DIFFERENT bundle (host vs client) — it intentionally stays separate; do
15
+ * not merge the two guards across the bundle boundary.
16
+ */
17
+ export declare function isFallbacksSwitchData(value: unknown): value is FallbacksSwitchEventData;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * `/fallbacks` slash command (plan fallbacks-role-runtime T3, AC-5).
3
+ *
4
+ * Session-scoped, read-only diagnostic: current session origin → resolved
5
+ * role → resolved chain (role chain, else rootChain — an `inherit: true`
6
+ * tail is annotated 「(inherit-root)」) → recent `fallbacks/switch` events
7
+ * (newest first, capped) → cooldown status. Mirrors dsh-advisor's
8
+ * `/advisor` command pattern: a conditional `ctx.inject(['commands'])` child
9
+ * in `src/index.ts` calls {@link registerFallbacksCommands} with a
10
+ * factory-bound handler; `commands` never joins the top-level inject list, so
11
+ * the command is silently absent when no command registry is composed (no
12
+ * top-level inject pollution — advisor T1 fix).
13
+ *
14
+ * The handler is **read-only**: it never mutates fallback state (no cooldown
15
+ * reset, no pending-switch writes). zh/en copy lives in this file — the
16
+ * client half's `src/client/locales.ts` is a separate client-side dictionary.
17
+ * The host carries no per-session locale signal (the `locale` service is
18
+ * client-side), so the wiring picks a deterministic default (`zh`, this
19
+ * repo's primary language); both dictionaries are unit-tested.
20
+ *
21
+ * @module dsh-llm-fallbacks/commands
22
+ */
23
+ import type { CommandDefinition } from '@deepseek-ai/dsh-commands';
24
+ import { type FallbacksRole } from './config.ts';
25
+ import type { Origin } from './roles.ts';
26
+ import type { FallbacksSwitchEventData } from './events.ts';
27
+ /** How many recent `fallbacks/switch` events `/fallbacks` shows (newest first). */
28
+ export declare const RECENT_SWITCHES_LIMIT = 5;
29
+ /** Minimal command registry surface (satisfied by the dsh `CommandService`). */
30
+ export interface FallbacksCommandRegistry {
31
+ register(definition: CommandDefinition): () => void;
32
+ }
33
+ /** Minimal agent/session surface the command reads (satisfied by the real `Agent`). */
34
+ export interface FallbacksCommandAgent {
35
+ readonly id: string;
36
+ readonly options?: {
37
+ readonly provider?: string;
38
+ readonly model?: string;
39
+ };
40
+ readonly session: {
41
+ readonly header?: {
42
+ readonly origin?: Origin;
43
+ };
44
+ readonly events: readonly unknown[];
45
+ };
46
+ }
47
+ /** One active cooldown entry displayed by `/fallbacks`. */
48
+ export interface FallbacksCooldownEntry {
49
+ /** `${provider}/${model}` key. */
50
+ readonly key: string;
51
+ /** Expiry epoch ms; `Infinity` for `revertPolicy: 'never'`. */
52
+ readonly untilEpochMs: number;
53
+ }
54
+ /** The read-only diagnostic snapshot the `/fallbacks` handler renders. */
55
+ export interface FallbacksCommandSnapshot {
56
+ /** Session origin: `'root'` when the agent carries no header origin. */
57
+ readonly origin: Origin;
58
+ /** Resolved role (first matching `roles.rules` entry → built-in `'inherit'`, spec §7.1). */
59
+ readonly role: string;
60
+ /** True when the role's own chain is non-empty and shown; false when rootChain (or none) is shown. */
61
+ readonly chainRole: boolean;
62
+ /** The displayed chain entries: the role's own chain when non-empty, else
63
+ * rootChain — except `fallback: 'none'` with an empty own chain, which
64
+ * yields `[]` even when rootChain is non-empty (nothing appended, mirroring
65
+ * resolveChainViews' `[...[], ...[]]`); empty = not configured. */
66
+ readonly chain: readonly string[];
67
+ /** True when rootChain is appended as the inherit fallback tail (role's
68
+ * `fallback` is `'inherit-root'` — or the role is unknown — and rootChain
69
+ * is non-empty; the diagnostic annotation source, spec §7.4). */
70
+ readonly inherit: boolean;
71
+ /** Recent `fallbacks/switch` events, newest first, capped at {@link RECENT_SWITCHES_LIMIT}. */
72
+ readonly switches: readonly FallbacksSwitchEventData[];
73
+ /** Active cooldown entries for the agent. */
74
+ readonly cooldown: readonly FallbacksCooldownEntry[];
75
+ }
76
+ /**
77
+ * The session-scoped read-only operations the `/fallbacks` handler drives.
78
+ * Implemented by the wiring (`src/index.ts`) against the live config source
79
+ * (`roles.list` / `rootChain` — no chain map anymore) and the per-agent
80
+ * state store; faked in unit tests.
81
+ */
82
+ export interface FallbacksCommandController {
83
+ /** Snapshot the session's fallback diagnostics. Never mutates state. */
84
+ getSnapshot(agent: FallbacksCommandAgent): FallbacksCommandSnapshot;
85
+ }
86
+ /** zh/en dictionaries for the `/fallbacks` output. */
87
+ export declare const FALLBACKS_COMMAND_LOCALES: {
88
+ readonly zh: {
89
+ readonly title: "当前会话 fallback 诊断(只读)";
90
+ readonly description: "查看当前会话的降级链、最近切换与冷却状态(只读)";
91
+ readonly origin: "会话来源";
92
+ readonly role: "角色";
93
+ readonly chain: "链";
94
+ readonly inheritRoot: "(inherit-root)";
95
+ readonly chainNone: "未配置";
96
+ readonly switches: "最近切换";
97
+ readonly switchesNone: "本会话暂无 fallback 切换";
98
+ readonly switchLine: "{from} → {to}(role={role},reason={reason})";
99
+ readonly cooldown: "冷却";
100
+ readonly cooldownNone: "无活跃冷却";
101
+ readonly cooldownLine: "{key} 冷却至 {time}";
102
+ readonly cooldownNever: "{key} 会话内不再回主";
103
+ readonly reason: {
104
+ readonly 'trigger-code': "触发码";
105
+ readonly 'always-cap': "always 上限";
106
+ };
107
+ };
108
+ readonly en: {
109
+ readonly title: "Session fallback diagnostics (read-only)";
110
+ readonly description: "Inspect fallback chain, recent switches, and cooldown for this session (read-only)";
111
+ readonly origin: "Session origin";
112
+ readonly role: "Role";
113
+ readonly chain: "Chain";
114
+ readonly inheritRoot: " (inherit-root)";
115
+ readonly chainNone: "not configured";
116
+ readonly switches: "Recent switches";
117
+ readonly switchesNone: "No fallback switches in this session";
118
+ readonly switchLine: "{from} → {to} (role={role}, reason={reason})";
119
+ readonly cooldown: "Cooldown";
120
+ readonly cooldownNone: "none active";
121
+ readonly cooldownLine: "{key} suppressed until {time}";
122
+ readonly cooldownNever: "{key} not reverting this session";
123
+ readonly reason: {
124
+ readonly 'trigger-code': "trigger-code";
125
+ readonly 'always-cap': "always-cap";
126
+ };
127
+ };
128
+ };
129
+ /** A locale id supported by {@link FALLBACKS_COMMAND_LOCALES}. */
130
+ export type FallbacksCommandLocale = keyof typeof FALLBACKS_COMMAND_LOCALES;
131
+ /**
132
+ * The newest `limit` `fallbacks/switch` events from a session's raw event
133
+ * log, newest first. Unknown event shapes and malformed `fallbacks/switch`
134
+ * payloads are skipped defensively (a session log may carry any
135
+ * `SessionEventMap` type, and the durable log can outlive schema versions).
136
+ */
137
+ export declare function recentFallbacksSwitches(events: readonly unknown[], limit: number): FallbacksSwitchEventData[];
138
+ /**
139
+ * The chain entries `/fallbacks` shows for a role (spec §7.4): the declared
140
+ * role's own chain when non-empty (`chainRole: true`); an empty own chain
141
+ * defers to `rootChain` unless `fallback: 'none'` — then nothing is appended
142
+ * and the display chain is empty, mirroring `resolveChainViews`'s
143
+ * `[...[], ...[]]` exactly; undeclared ids and the built-in `'inherit'`
144
+ * role resolve to `rootChain`. `inherit: true` marks the append-not-replace
145
+ * tail — the role's `fallback` is `'inherit-root'` (the default) or the role
146
+ * is unknown/built-in `'inherit'`, and `rootChain` is non-empty. Mirrors
147
+ * `resolveChainViews`'s concatenation (see {@link buildRoleEntries} —
148
+ * `src/chains.ts`; the diagnostic keeps its display semantics: the role's
149
+ * own chain renders in full, `rootChain` only when the role has no own
150
+ * chain, with the inherit tail as an annotation) without a failing model to
151
+ * resolve against (the diagnostic is model-independent).
152
+ *
153
+ * `warn` mirrors {@link resolveChainViews}' defensive unknown-role warn
154
+ * (qc2 F-002 — routed through the injected logger; the `/fallbacks` path
155
+ * never reaches here unsanitized, as {@link resolveRole} resolves to a
156
+ * declared id or `'inherit'` first, so this is direct-caller parity).
157
+ */
158
+ export declare function resolveChainForDiagnostic(roles: readonly FallbacksRole[], rootChain: readonly string[], role: string, warn?: (message: string) => void): {
159
+ readonly chainRole: boolean;
160
+ readonly chain: readonly string[];
161
+ readonly inherit: boolean;
162
+ };
163
+ /**
164
+ * Render the `/fallbacks` status surface for one snapshot. Kept minimal and
165
+ * truthful: origin → role → chain (+ inherit tail) → recent switches →
166
+ * cooldown.
167
+ */
168
+ export declare function fallbacksCommandText(snapshot: FallbacksCommandSnapshot, locale?: FallbacksCommandLocale): string;
169
+ /**
170
+ * Register the `/fallbacks` command with a command registry (the dsh
171
+ * `CommandService`, or a fake in tests). Called from the plugin's conditional
172
+ * `ctx.inject(['commands'], ...)` child — the command exists only when a
173
+ * registry is composed.
174
+ * @returns the registry disposer (the inject child owns its lifetime).
175
+ */
176
+ export declare function registerFallbacksCommands(registry: FallbacksCommandRegistry, controller: FallbacksCommandController, locale?: FallbacksCommandLocale): () => void;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The `fallbacks` settings namespace: plugin config schema + defaults.
3
+ *
4
+ * Two-block config model (plan fallbacks-role-config-model): block 1
5
+ * `rootChain` — the root agent's single fallback chain (empty = no
6
+ * degradation) — plus block 2 declared role entities: `roles.list`
7
+ * (id/label/description/prompt?/permissions?/chain?/fallback) and
8
+ * `roles.rules` enum references into the declared ids (or the built-in
9
+ * `'inherit'` role). The legacy `chains` / `roles.default` keys are gone
10
+ * from the schema and type (zero residual, migration table excepted); the
11
+ * runtime consumes the new shape directly and flags surviving legacy keys
12
+ * at startup via `detectLegacyKeys` (see `src/index.ts` apply()).
13
+ *
14
+ * Spec §4 is authoritative for field names and default values — notably
15
+ * `triggerCodes` defaults to dsh's stable failure codes `['AUTH', 'QUOTA',
16
+ * 'RATE_LIMIT']` (there is no `QUOTA_EXCEEDED` code in dsh), and an
17
+ * unconfigured install (`enabled: false`, empty `rootChain`, empty roles)
18
+ * is a no-op pass-through exactly like an uninstalled plugin (AC-8).
19
+ *
20
+ * This module is pure logic: it must not import any `@deepseek-ai/*` package
21
+ * (types included) — `FallbacksConfig` is the plugin's own type. Task 3
22
+ * registers this schema with `installSettingsSection` under the `fallbacks`
23
+ * settings namespace.
24
+ *
25
+ * @module dsh-llm-fallbacks/config
26
+ */
27
+ import z from 'schemastery';
28
+ /** How a cooled-down model comes back (spec §4). */
29
+ export type RevertPolicy = 'cooldown-expiry' | 'never';
30
+ /** A single role rule: match on origin/provider/model patterns (spec §3). */
31
+ export interface FallbacksRoleRule {
32
+ origin?: 'root' | 'subagent';
33
+ provider?: string;
34
+ model?: string;
35
+ role: string;
36
+ }
37
+ /**
38
+ * Chain-append strategy of a declared role entity: `inherit-root` (the
39
+ * default) runs the role's own chain and then appends `rootChain`;
40
+ * `none` uses only the role's own chain.
41
+ */
42
+ export type FallbackStrategy = 'inherit-root' | 'none';
43
+ /**
44
+ * A declared role entity (plan fallbacks-role-config-model Task 1).
45
+ *
46
+ * `prompt` / `permissions` are schema-reserved for the next iteration
47
+ * (fallbacks-explicit-role-tool) — no consumer this round, and writing them
48
+ * does NOT change this round's degradation behavior.
49
+ */
50
+ export interface FallbacksRole {
51
+ id: string;
52
+ label: string;
53
+ description: string;
54
+ /** Reserved for next iteration — no consumer this round. */
55
+ prompt?: string;
56
+ /** Reserved for next iteration — no consumer this round. */
57
+ permissions?: {
58
+ allow?: string[];
59
+ deny?: string[];
60
+ };
61
+ chain?: string[];
62
+ fallback?: FallbackStrategy;
63
+ }
64
+ /** Role grouping for fallback chains: declared entities + enum references. */
65
+ export interface FallbacksRoles {
66
+ list: FallbacksRole[];
67
+ rules: FallbacksRoleRule[];
68
+ }
69
+ /**
70
+ * The full `fallbacks` settings shape (two-block config model, verbatim
71
+ * field names).
72
+ */
73
+ export interface FallbacksConfig {
74
+ enabled: boolean;
75
+ triggerCodes: string[];
76
+ rootChain: string[];
77
+ roles: FallbacksRoles;
78
+ cooldownMs: number;
79
+ revertPolicy: RevertPolicy;
80
+ maxSwitchesPerStep: number;
81
+ alwaysModeRetryCap: number;
82
+ }
83
+ /**
84
+ * Spec §4 defaults — `Config({})` must equal this (no-op install).
85
+ * `enabled` defaults to `false` (readme-settings spec §1.2): the feature
86
+ * switch is off until the user turns it on in the settings page; an
87
+ * unconfigured install (`enabled: false`, empty rootChain, empty roles)
88
+ * behaves exactly like an uninstalled plugin (AC-3 / no-op invariant).
89
+ */
90
+ export declare const defaultFallbacksConfig: FallbacksConfig;
91
+ /**
92
+ * Reserved role id: legal as a rule target (`roles.rules[].role`) and as
93
+ * the no-rule-match fallback, but FORBIDDEN in `roles.list[].id`.
94
+ */
95
+ export declare const INHERIT_ROLE_ID = "inherit";
96
+ /** Role id format (aligned with yet-another-subagent `isValidProfileId`). */
97
+ export declare const ROLE_ID_PATTERN: RegExp;
98
+ /**
99
+ * Minimal logger surface {@link validateFallbacksConfig} warns through —
100
+ * keeps this module free of `@deepseek-ai/*` imports (a cordis Logger is
101
+ * structurally compatible).
102
+ */
103
+ export interface FallbacksConfigLogger {
104
+ warn(message: string): void;
105
+ }
106
+ /**
107
+ * Validate a fallbacks config (pure, warn-only — never throws, never
108
+ * mutates): role id format/uniqueness/reserved word, rule role references
109
+ * (declared ids + the built-in `'inherit'`), the `fallback` enum, and
110
+ * `rootChain`/role-chain selector legality. `label`/`description` are free
111
+ * text and are deliberately not validated. Each violation emits one
112
+ * `llm-fallbacks: ...` warn and "does not take effect" — the config stays
113
+ * usable (spec §4 / AC-4 warn-not-crash semantics).
114
+ */
115
+ export declare function validateFallbacksConfig(config: FallbacksConfig, logger: FallbacksConfigLogger): void;
116
+ /**
117
+ * Detect legacy (two-block-era) leftovers in a config SOURCE — the composed
118
+ * object `source()` returns, or a raw settings document. Recognizes the
119
+ * removed `chains` key, the removed `roles.default` field, and
120
+ * `roles.rules[].role` values that reference no declared `roles.list` id
121
+ * and are not the built-in `'inherit'`. Returns descriptive key/role names;
122
+ * the gateway attaches them as `get().legacyKeys` so the client can show a
123
+ * migration banner (spec §9 — the source is read directly because
124
+ * schemastery retains unknown keys, verified plan Task 1 Step 1).
125
+ */
126
+ export declare function detectLegacyKeys(source: Record<string, unknown>): string[];
127
+ /**
128
+ * Plugin Config schema (schemastery), mirroring {@link FallbacksConfig}.
129
+ * Object fields are optional by default in schemastery; `.default()` fills
130
+ * the spec defaults, `.required()` keeps mandatory fields. Unknown keys are
131
+ * RETAINED by the composition (verified plan Task 1 Step 1) — that is what
132
+ * lets `detectLegacyKeys` flag two-block-era leftovers (`chains` /
133
+ * `roles.default`) on the composed object at startup (warn + gateway
134
+ * `legacyKeys`, see `src/index.ts` apply()).
135
+ */
136
+ export declare const Config: z<FallbacksConfig>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Cooldown store + per-step failure set (spec §5.1; plan Task 2).
3
+ *
4
+ * `CooldownStore` maps `provider/model` keys to an expiry epoch ms with lazy
5
+ * evaluation: expired entries are dropped on read. `revertPolicy: 'never'`
6
+ * is expressed by suppressing with `Infinity` — the entry never expires
7
+ * within the session.
8
+ *
9
+ * `StepFailureSet` is the step's failed-model set (spec §5.1 `failed`).
10
+ * `switchCount` / `maxSwitchesPerStep` judgement belongs to the caller
11
+ * (Task 3) — this task only provides the data structure.
12
+ *
13
+ * @module dsh-llm-fallbacks/cooldown
14
+ */
15
+ /**
16
+ * Per-agent model cooldown: `suppress(key, untilEpochMs)` then
17
+ * `isSuppressed(key, now?)` with lazy expiry.
18
+ */
19
+ export declare class CooldownStore {
20
+ private entries;
21
+ /** Number of tracked keys (expired entries are dropped lazily on read). */
22
+ get size(): number;
23
+ /** Suppress `key` until `untilEpochMs` (use `Infinity` for `revertPolicy: 'never'`). */
24
+ suppress(key: string, untilEpochMs: number): void;
25
+ /** True while `key` is suppressed at time `now` (default `Date.now()`); expired entries are removed. */
26
+ isSuppressed(key: string, now?: number): boolean;
27
+ /**
28
+ * Read-only diagnostic accessor: the active (non-expired) entries, in
29
+ * insertion order. This is a pure read — expired entries are FILTERED OUT
30
+ * but left in the store; the lazy delete happens only in
31
+ * {@link isSuppressed} (the decision path). `Infinity` entries are always
32
+ * active. Consumed by the `/fallbacks` command's cooldown display.
33
+ */
34
+ snapshot(now?: number): Array<{
35
+ key: string;
36
+ untilEpochMs: number;
37
+ }>;
38
+ }
39
+ /**
40
+ * The current step's failed-model set (`${provider}/${model}` keys).
41
+ */
42
+ export declare class StepFailureSet {
43
+ private keys;
44
+ get size(): number;
45
+ add(key: string): void;
46
+ has(key: string): boolean;
47
+ reset(): void;
48
+ }