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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/README.zh-CN.md +119 -0
- package/bundle/cordis.patch.yml +19 -0
- package/dist/chains.d.ts +167 -0
- package/dist/client/ConversationFallbackSwitch.d.ts +87 -0
- package/dist/client/FallbacksCard.d.ts +86 -0
- package/dist/client/GeneralFallbacksRow.d.ts +19 -0
- package/dist/client/fallbacks-store.d.ts +379 -0
- package/dist/client/index.d.ts +56 -0
- package/dist/client/index.js +3916 -0
- package/dist/client/locales.d.ts +281 -0
- package/dist/client/switch-guard.d.ts +17 -0
- package/dist/commands.d.ts +176 -0
- package/dist/config.d.ts +136 -0
- package/dist/cooldown.d.ts +48 -0
- package/dist/events.d.ts +36 -0
- package/dist/gateway.d.ts +176 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +1853 -0
- package/dist/roles.d.ts +70 -0
- package/dist/selectors.d.ts +35 -0
- package/dist/state.d.ts +112 -0
- package/package.json +91 -0
- package/scripts/build-client.ts +374 -0
- package/scripts/verify-dist.mjs +68 -0
|
@@ -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;
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
}
|