dsh-llm-fallbacks 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,49 +24,26 @@
24
24
  * message in `state.error` for the section's error banner (KD-G3 — the old
25
25
  * `settings-conflict` branch is gone).
26
26
  */
27
- import type { LlmConfigurableProvider, ModelCatalog, ModelProviderGroup, SessionFollowFrame, SessionFollowRequest, SessionId, SettingsDescribeValue, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client';
28
- import type { SessionEventLikeEntry } from '@deepseek-ai/dsh-api-session-controller/client';
27
+ import type { LlmConfigurableProvider, ModelCatalog, ModelProviderGroup, SessionId, SettingsDescribeValue, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client';
29
28
  import type { ClientConnectionRpc } from '@deepseek-ai/dsh-client-connection/client';
30
29
  import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
31
30
  import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
32
- import { type FallbackStrategy, type FallbacksConfig, type FallbacksRole, type FallbacksRoleRule, type FallbacksRoles } from '../config.ts';
31
+ import { type FallbacksConfig, type FallbacksRole, type NamedChain } from '../config.ts';
33
32
  import type { SlotRowConfig } from '../time-slots.ts';
34
- import type { FallbacksSwitchEventData } from '../events.ts';
35
33
  import type { SeedsWireStatus } from '../seeds.ts';
36
34
  /** The plugin's settings namespace on the host wire (settings/document-updated ns filter). */
37
35
  export declare const FALLBACKS_SETTINGS_NS = "fallbacks";
38
- /** Single-page history read for the status block (spec §2.5 D-5: `HISTORY_PAGE_MESSAGES`-sized). */
39
- export declare const SWITCHES_HISTORY_PAGE = 50;
40
- /** How many recent switches the status block renders (spec §2.5 D-5: N=5). */
41
- export declare const RECENT_SWITCH_LIMIT = 5;
42
36
  /**
43
- * One recent `fallbacks/switch` event as the status block renders it: the
44
- * durable payload plus the raw event's ordering key and time (the payload
45
- * itself carries no seq/time — spec §5 table).
46
- */
47
- export interface FallbacksSwitchSnapshot extends FallbacksSwitchEventData {
48
- /** Event seq within the session (newest-first ordering key). */
49
- seq: number;
50
- /** Event time, Unix epoch milliseconds. */
51
- time: number;
52
- }
53
- /**
54
- * The status block's derived "current effective model" (spec §2.5 D-6) — a
55
- * **display value** derived from configuration + recent switches, never a
56
- * live route probe.
37
+ * The per-chain "current effective model" (named virtual chains §4) — a
38
+ * **display value** derived from one chain's entries, never a live route
39
+ * probe.
57
40
  */
58
41
  export type EffectiveModelView =
59
- /** ① An inactive config (nothing configured) or an empty rootChain. */
42
+ /** ① An inactive config (no chains) or an empty entries list. */
60
43
  {
61
44
  kind: 'unavailable';
62
45
  }
63
- /** ② The most recent switch's target (`to`). */
64
- | {
65
- kind: 'switched';
66
- provider: string;
67
- model: string;
68
- }
69
- /** ③ No switches yet: the config's primary target (first chain entry). */
46
+ /** ② The chain's first entry (the dispatch head). */
70
47
  | {
71
48
  kind: 'config';
72
49
  provider: string;
@@ -121,54 +98,9 @@ export interface FallbacksSettingsState {
121
98
  groups: readonly ModelProviderGroup[];
122
99
  /** Bumped on every accepted catalog read; drives row re-classification. */
123
100
  catalogEpoch: number;
124
- /** Recent-switch summary (spec §2.4 R-4a / §2.5 D-5). */
125
- switchesStatus: 'idle' | 'loading' | 'ready' | 'error';
126
- /** Switch-read diagnostic (wire message); null when none. */
127
- switchesError: string | null;
128
- /** Most recent `fallbacks/switch` events of the current session, newest first. */
129
- switches: FallbacksSwitchSnapshot[];
130
- /**
131
- * Host subagent-model-selection policy (spec D4 / T5). `undefined` when
132
- * the gateway omitted the additive field (old payload / policy off) —
133
- * the card hides the status area and never renders an active allowlist.
134
- */
135
- subagentPolicy: SubagentPolicyView | undefined;
101
+ /** Current session id the conversation switch strip follows. */
102
+ currentSession: SessionId | undefined;
136
103
  }
137
- /** Source label of the effective subagent chain head (spec D4). */
138
- export type SubagentPolicyHeadSource = 'authorized' | 'injected';
139
- /** One exact route on the additive `subagentPolicy` wire field. */
140
- export type SubagentPolicyRoute = {
141
- provider: string;
142
- model: string;
143
- };
144
- /**
145
- * Client-side view of the additive host-policy payload. Structural twin of
146
- * the gateway wire type — parsed from unknown, never imported from host
147
- * modules (`src/client/**` consumes policy only via the gateway channel).
148
- */
149
- export type SubagentPolicyView = {
150
- state: 'enabled';
151
- allowedModels: SubagentPolicyRoute[];
152
- head?: {
153
- route: SubagentPolicyRoute;
154
- source: SubagentPolicyHeadSource;
155
- };
156
- blockedAttempt?: {
157
- at: number;
158
- route: SubagentPolicyRoute;
159
- reason: string;
160
- };
161
- } | {
162
- state: 'disabled';
163
- } | {
164
- state: 'unprovable';
165
- };
166
- /**
167
- * Shape-guard the additive `subagentPolicy` wire field (T5). Malformed
168
- * payloads resolve to `undefined` (hide the status area) — the store never
169
- * throws on an old or misshapen field (old-payload tolerance).
170
- */
171
- export declare function parseSubagentPolicyWire(value: unknown): SubagentPolicyView | undefined;
172
104
  /**
173
105
  * The provider dropdown's offer set (spec §2.5 D-4): catalog providers whose
174
106
  * settings profile resolves in the describe namespaces — the Models page's
@@ -218,28 +150,12 @@ export declare function classifyProvider(raw: string, catalog: CatalogLookup | u
218
150
  */
219
151
  export declare function classifyModel(provider: string, raw: string, catalog: CatalogLookup | undefined): CatalogSelection;
220
152
  /**
221
- * Extract the most recent `fallbacks/switch` events from one history page
222
- * (spec §2.5 D-5): drop packed chunk runs, filter by event type, order by
223
- * `seq` descending, take at most `limit`. Single-page read — fewer than
224
- * `limit` events show as-is; no multi-page backfill (Non-Goal).
225
- */
226
- export declare function extractRecentSwitches(entries: readonly SessionEventLikeEntry[], limit?: number): FallbacksSwitchSnapshot[];
227
- /**
228
- * Derive the "current effective model" (spec §2.5 D-6): ① inactive (nothing
229
- * configured — the removed `enabled` switch re-keyed to `isFallbackActive`)
230
- * / empty rootChain → unavailable; ② a recent switch exists → the latest
231
- * one's `to`; ③ otherwise → the config's primary target. A **display
232
- * value** — never a live route probe.
233
- *
234
- * INTENTIONAL D-6 CONTRACT RETENTION: after the AC-2 trim (plan
235
- * fallbacks-settings-visibility Task 2) the settings card's status block no
236
- * longer consumes this derivation, and no other production code imports it —
237
- * it is retained (NOT dead code to delete) as the spec §2.5 D-6 derived-value
238
- * surface, pinned by `tests/fallbacks-store.spec.ts` (D-6 display-value
239
- * contract). Keep both exports until the spec derivation is removed or gains
240
- * a real consumer.
153
+ * Derive the per-chain "current effective model" (named virtual chains §4):
154
+ * an inactive config (no chains) or empty entries → unavailable; otherwise
155
+ * the chain's first entry (the dispatch head). A **display value** — never a
156
+ * live route probe.
241
157
  */
242
- export declare function deriveEffectiveModel(config: FallbacksConfig, switches: readonly FallbacksSwitchSnapshot[]): EffectiveModelView;
158
+ export declare function deriveEffectiveModel(chain: NamedChain | undefined): EffectiveModelView;
243
159
  /** One chain selector row: provider + model (or wildcard). */
244
160
  export interface ChainSelectorRow {
245
161
  /** `provider/*` wildcard entry: the model part is absent. */
@@ -250,25 +166,14 @@ export interface ChainSelectorRow {
250
166
  }
251
167
  /** Serialize one selector row to its wire string (`provider/model` | `provider/*`). */
252
168
  export declare function selectorRowToRaw(row: ChainSelectorRow): string;
169
+ /** Parse one entry line into a selector row, classifying against the catalog. */
170
+ export declare function entryToSelectorRow(entry: string, catalog: CatalogLookup | undefined): ChainSelectorRow;
253
171
  /**
254
- * One rootChain row in the editor: the root agent's single fallback chain
255
- * (block 1 of the two-block model) as an ordered selector list. There is no
256
- * key input — the row IS the chain.
257
- */
258
- export interface RootChainRow {
259
- selectors: ChainSelectorRow[];
260
- }
261
- /** Project the rootChain entries into editable rows (one flat chain row). */
262
- export declare function rootChainToRows(rootChain: readonly string[], catalog?: CatalogLookup): RootChainRow[];
263
- /** Rebuild the rootChain from edited rows; rows with no usable selector drop out. */
264
- export declare function rowsToRootChain(rows: readonly RootChainRow[]): string[];
265
- /**
266
- * One extra time-slot row in the editor (plan fallbacks-timeslots Task 3):
267
- * preset rows freeze their windows (read-only summary, models-only edits);
268
- * custom rows edit start/end/days + chain. `kind` rides the wire VERBATIM —
269
- * a hand-written YAML row with an unknown kind reads back as a custom-shaped
270
- * row and serializes back unchanged, so the dirty check stays quiet (save
271
- * validation rejects it).
172
+ * One time-slot row in the editor: preset rows freeze their windows
173
+ * (read-only summary, models-only edits); custom rows edit start/end/days +
174
+ * chain. `kind` rides the wire VERBATIM — a hand-written YAML row with an
175
+ * unknown kind reads back as a custom-shaped row and serializes back
176
+ * unchanged, so the dirty check stays quiet (save validation rejects it).
272
177
  */
273
178
  export interface SlotEditorRow {
274
179
  kind: string;
@@ -290,12 +195,35 @@ export interface SlotEditorRow {
290
195
  export declare function timeSlotsToRows(timeSlots: readonly SlotRowConfig[], catalog?: CatalogLookup): SlotEditorRow[];
291
196
  /** Rebuild the time-slot rows from edited rows; blank selectors drop out.
292
197
  * `kind` rides verbatim (a hand-written unknown kind reads back unchanged;
293
- * save validation rejects it) — the cast asserts the trusted editor shape.
294
- * `days` is ALWAYS serialized ([] included): schemastery composes absent
295
- * array fields as `[]`, so the composed config every card load accepts
296
- * carries `days` on every row — the draft must too, or a clean card would
297
- * read back dirty. */
198
+ * save validation rejects it). `days` is ALWAYS serialized ([] included):
199
+ * schemastery composes absent array fields as `[]`, so the composed config
200
+ * every card load accepts carries `days` on every row — the draft must too,
201
+ * or a clean card would read back dirty. */
298
202
  export declare function rowsToTimeSlots(rows: readonly SlotEditorRow[]): SlotRowConfig[];
203
+ /**
204
+ * One named-chain draft in the editor (named virtual chains): the chain's
205
+ * id (immutable once created), display label, 默认降级链 selector rows, and
206
+ * its 分时 slot rows. `label`/entries/slots are editable; the id is not.
207
+ */
208
+ export interface ChainDraft {
209
+ id: string;
210
+ label: string;
211
+ selectors: ChainSelectorRow[];
212
+ slots: SlotEditorRow[];
213
+ }
214
+ /** Project the named chains into editable drafts (selectors classified). */
215
+ export declare function chainsToDrafts(chains: readonly NamedChain[], catalog?: CatalogLookup): ChainDraft[];
216
+ /**
217
+ * Generate a fresh unique chain id for the card's `+` affordance. Pass the
218
+ * DRAFT list, not the accepted config: two unsaved additions must not both
219
+ * claim `chain-N` (duplicate React keys, and a section the gateway rejects).
220
+ */
221
+ export declare function newChainId(existing: readonly {
222
+ id: string;
223
+ }[]): string;
224
+ /** Rebuild one named chain from its draft; blank selectors drop out. */
225
+ export declare function draftToChain(draft: ChainDraft): NamedChain;
226
+ export declare function chainToChainDraftOf(chain: NamedChain, catalog?: CatalogLookup): ChainDraft;
299
227
  /**
300
228
  * One declared-role row in the editor (block 2 `roles.list`): identity
301
229
  * fields + the role's own chain selector list + its append strategy.
@@ -305,62 +233,36 @@ export declare function rowsToTimeSlots(rows: readonly SlotEditorRow[]): SlotRow
305
233
  export interface RoleRow {
306
234
  id: string;
307
235
  persona: string;
308
- selectors: ChainSelectorRow[];
309
- fallback: FallbackStrategy;
236
+ /** The MANUALLY specified named-chain POINTER (「指定降级链」); '' = NOT SPECIFIED. */
237
+ chain: string;
310
238
  /** UI-only collapse state — never serialized (dropped by rowsToRoles). */
311
239
  collapsed: boolean;
312
240
  /**
313
- * UI-only seeded-persona brief disclosure (plan role-card-seeded-ux) —
314
- * never serialized (dropped by rowsToRoles). Only seeded rows render the
315
- * affordance (a read-only single-line brief + the expandable full text);
316
- * ordinary rows ignore the flag.
241
+ * UI-only seeded-persona brief disclosure — never serialized (dropped by
242
+ * rowsToRoles). Only seeded rows render the affordance; ordinary rows
243
+ * ignore the flag.
317
244
  */
318
245
  personaOpen: boolean;
319
246
  }
320
- /** Project the declared roles into editable rows (chain selectors classified). */
321
- export declare function rolesToRows(roles: readonly FallbacksRole[], catalog?: CatalogLookup): RoleRow[];
322
- /** Rebuild the declared roles from edited rows; empty selectors drop out. */
247
+ /** Project the declared roles into editable rows (chain pointer kept verbatim). */
248
+ export declare function rolesToRows(roles: readonly FallbacksRole[]): RoleRow[];
249
+ /** Rebuild the declared roles from edited rows; an empty pointer drops out. */
323
250
  export declare function rowsToRoles(rows: readonly RoleRow[]): FallbacksRole[];
324
251
  /**
325
252
  * Rebuild the declared roles from edited rows, re-attaching the
326
253
  * schema-reserved `prompt`/`permissions` fields from the last accepted
327
254
  * config by role id — they never round-trip through rows this round, so
328
- * without the merge a save would silently drop them (T2 reviewer minor
329
- * #2). The id trim matches {@link rowsToRoles}; a row whose id matches no
330
- * original role (a freshly added one) keeps no extras. Key order mirrors
331
- * `parseFallbacksConfig` so a clean draft's JSON dirty comparison never
332
- * flags it.
255
+ * without the merge a save would silently drop them. The id trim matches
256
+ * {@link rowsToRoles}; a row whose id matches no original role (a freshly
257
+ * added one) keeps no extras.
333
258
  */
334
259
  export declare function mergeRoleExtras(rows: readonly RoleRow[], originalRoles: readonly FallbacksRole[]): FallbacksRole[];
335
260
  /**
336
- * The `roles.rules` role dropdown's offer set — the ONLY data source for the
337
- * rule rows' role selector: the built-in `'inherit'` target plus every
338
- * declared `roles.list` id, in declaration order (a role added/removed on
339
- * the same page is reflected immediately).
340
- */
341
- export declare function ruleRoleOptions(roles: Pick<FallbacksRoles, 'list'>): string[];
342
- /**
343
- * Client-side legacy fallback detection (spec §9). The gateway's
344
- * `legacyKeys` wire field is authoritative for the migration banner; this is
345
- * a defensive fallback for configs that already passed wire normalization
346
- * yet still carry a `roles.rules` reference to an undeclared role id — the
347
- * only two-block-era leftover that can survive the wire (the removed
348
- * `chains`/`roles.default` keys never ride it).
261
+ * The 「指定降级链」 dropdown's offer set: the NOT-SPECIFIED empty value plus
262
+ * every declared chain, in declaration order (option label rendered by the
263
+ * card as `<label>(FallbacksChain/<id>)`).
349
264
  */
350
- export declare function detectLegacyClientKeys(config: FallbacksConfig): string[];
351
- /**
352
- * One role-rule row in the editor (PR #62 feedback: no origin control —
353
- * rules are subagent-only; a persisted wire `origin` is ignored).
354
- */
355
- export interface RoleRuleRow {
356
- provider: CatalogSelection;
357
- model: CatalogSelection;
358
- role: string;
359
- }
360
- /** Project the role rules into editable rows (provider/model classified). */
361
- export declare function rulesToRows(rules: readonly FallbacksRoleRule[], catalog?: CatalogLookup): RoleRuleRow[];
362
- /** Rebuild the role rules from edited rows; empty provider/model drop out. */
363
- export declare function rowsToRules(rows: readonly RoleRuleRow[]): FallbacksRoleRule[];
265
+ export declare function roleChainOptions(chains: readonly NamedChain[]): string[];
364
266
  /**
365
267
  * The settings Remote namespace face the fallbacks store reads
366
268
  * (`ctx.remote.settings`; api-settings-controller `@Remote` methods).
@@ -384,14 +286,6 @@ export interface FallbacksLlmRemote {
384
286
  export interface FallbacksSessionRemote {
385
287
  /** Host-generation model catalog: provider groups plus per-provider failures. */
386
288
  modelCatalog(): Promise<RemoteResult<ModelCatalog>>;
387
- /**
388
- * Live event stream for one durable session address; the opening snapshot
389
- * frame carries the tail page this store reads, then the stream is
390
- * cancelled (successor of the apiproxy `sessions.history` unary —
391
- * `session/page` needs the follow opening frame's cursor, so a cold tail
392
- * read goes through the follow opening).
393
- */
394
- follow(request: SessionFollowRequest, signal?: AbortSignal): AsyncIterable<SessionFollowFrame>;
395
289
  }
396
290
  /**
397
291
  * The generated Remote namespaces the fallbacks store consumes, as one
@@ -420,22 +314,12 @@ export declare class FallbacksSettingsController {
420
314
  */
421
315
  private writeGeneration;
422
316
  private catalogGeneration;
423
- private switchesGeneration;
424
317
  /** Every settings namespace from the last describe, keyed by ns — the configured-provider join's other input. */
425
318
  private namespaces;
426
- private currentSession;
427
- /**
428
- * The store's dispose signal (qc3 F-003): created alongside the store
429
- * lifecycle, threaded into every `session.follow` call, aborted once in
430
- * {@link dispose} — a follow opening still mid-await at plugin unload
431
- * (HMR / fiber dispose, or a wedged gateway stream) is cancelled promptly
432
- * instead of staying open until the mount abort or socket failure.
433
- */
434
- private readonly followAbort;
435
319
  /**
436
320
  * @param api - the Remote namespace faces the store reads (settings
437
321
  * describe `writable` + namespace directory, llm provider directory,
438
- * session model catalog + follow tail page) — `ctx.remote` cast once at
322
+ * session model catalog) — `ctx.remote` cast once at
439
323
  * the mount boundary.
440
324
  * @param rpc - the connection's generic RPC caller for the host gateway
441
325
  * channel (`/api`), injected from the connection handle.
@@ -468,27 +352,10 @@ export declare class FallbacksSettingsController {
468
352
  */
469
353
  loadCatalog(): Promise<void>;
470
354
  /**
471
- * Record the current session the status block reads (spec §2.5 D-5). Once
472
- * the block has been read once, its summary follows session switches
473
- * immediately; an idle block only records the id — the section's mount
474
- * effect performs the first read.
475
- * @param sessionId - the session whose history is summarized; undefined
476
- * (no current session) resolves to the empty state.
355
+ * Record the current session the conversation switch strip follows
356
+ * (switch events ride the session stream — no client history read).
477
357
  */
478
358
  setCurrentSession(sessionId: SessionId | undefined): void;
479
- /**
480
- * Read the recent-switch summary for the current session (spec §2.5 D-5):
481
- * one tail page (`maxMessages` = {@link SWITCHES_HISTORY_PAGE}) read from
482
- * the `session/follow` opening snapshot — the host has no unary tail-page
483
- * read (`session/page` needs the follow opening frame's cursor), so the
484
- * stream is cancelled right after its first frame —
485
- * `fallbacks/switch` events extracted newest-first capped at
486
- * {@link RECENT_SWITCH_LIMIT}. No current session → honest empty ready
487
- * state (no RPC); a read failure lands `switchesStatus: 'error'` and never
488
- * touches the settings state (the form keeps editing/saving normally).
489
- * @returns nothing; {@link store} carries success or failure.
490
- */
491
- loadSwitches(): Promise<void>;
492
359
  /**
493
360
  * Persist the full edited configuration through the gateway channel
494
361
  * (`/api/fallbacks/set`). The full config is sent as a MERGE patch (guide
@@ -501,19 +368,6 @@ export declare class FallbacksSettingsController {
501
368
  * @param next - the complete edited configuration.
502
369
  */
503
370
  save(next: FallbacksConfig): Promise<void>;
504
- /**
505
- * Explicit "sync to host allowlist" write (plan
506
- * role-chain-allowlist-visibility T2): the gateway endpoint merges the
507
- * CURRENT allowlist with this plugin's exact chain routes plus the virtual
508
- * `FallbacksChain/Auto` route and writes only `allowedModels`. The button
509
- * click is the ONLY trigger — the store never syncs on its own.
510
- * @returns true when the write landed (the card then reads the fresh
511
- * snapshot from the store); false on a refused/failed call, leaving the
512
- * last accepted state untouched. A response discarded by the write
513
- * generation guard is still a landed write — dispose() is the only
514
- * invalidator, so it must not report failure.
515
- */
516
- syncAllowlist(): Promise<boolean>;
517
371
  /**
518
372
  * Reset to composition defaults through the gateway channel
519
373
  * (`/api/fallbacks/reset` — the fallbacks-specific third method; the host
@@ -524,10 +378,10 @@ export declare class FallbacksSettingsController {
524
378
  /** Stop in-flight responses from publishing after plugin disposal. */
525
379
  dispose(): void;
526
380
  /**
527
- * Unpack one write response (set / reset / sync-allowlist) into the store
528
- * snapshot. The additive fields (legacyKeys §9, seeds §9.4, subagentPolicy
529
- * D4) follow the W-1/F-1 rule: absent on the wire keeps the last accepted
530
- * value — only a `get` may settle server truth.
381
+ * Unpack one write response (set / reset) into the store snapshot. The
382
+ * additive fields (legacyKeys §9, seeds §9.4) follow the W-1/F-1 rule:
383
+ * absent on the wire keeps the last accepted value — only a `get` may
384
+ * settle server truth.
531
385
  */
532
386
  private acceptWire;
533
387
  /**
@@ -561,11 +415,3 @@ export declare function refreshFallbacksIfLoaded(controller: FallbacksSettingsCo
561
415
  * @param controller - the fallbacks settings controller.
562
416
  */
563
417
  export declare function refreshCatalogIfLoaded(controller: FallbacksSettingsController): void;
564
- /**
565
- * Refetch the recent-switch summary after `settings/document-updated`
566
- * (fallbacks ns) / `connection/reset` only when the status block has already
567
- * been read once
568
- * (the switches twin of {@link refreshFallbacksIfLoaded}).
569
- * @param controller - the fallbacks settings controller.
570
- */
571
- export declare function refreshSwitchesIfLoaded(controller: FallbacksSettingsController): void;
@@ -29,7 +29,6 @@
29
29
  */
30
30
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
31
31
  export type { FallbacksCardInjected, FallbacksCardProps } from './FallbacksCard.tsx';
32
- export type { GeneralFallbacksRowInjected, GeneralFallbacksRowProps } from './GeneralFallbacksRow.tsx';
33
32
  export type { ConversationFallbackSwitchProps, FallbacksSwitchChatData, } from './ConversationFallbackSwitch.tsx';
34
33
  export type { SubagentRoleBadgeProps } from './SubagentRoleBadge.tsx';
35
34
  export type { FallbacksSettingsState } from './fallbacks-store.ts';