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.
@@ -1,38 +1,22 @@
1
1
  /**
2
- * Dispatch-time role resolution (plan fallbacks-role-automatch Task 2; the
3
- * dispatch-role stage added by plan dsh-148-agent-team-role-delivery Task 1).
4
- *
5
- * Precedence (first hit wins) across three ordered stages:
2
+ * Dispatch-time role resolution (named virtual chains, plan
3
+ * named-virtual-chains T3): narrowed to TWO stages —
6
4
  * 1. explicit — `session.header.agentPreset` trimmed matches a declared role
7
- * id → that role's DECLARED RAW id (via the `roleIds` trimmed-id map, the
8
- * same canonicalization the rules path uses); the reserved `'inherit'`
9
- * and undeclared presets are NEVER dispatchable explicit roles and fall
10
- * through to the next stage;
5
+ * id → that role's DECLARED RAW id (via the `roleIds` trimmed-id map); the
6
+ * reserved `'inherit'` and undeclared presets are NEVER dispatchable
7
+ * explicit roles;
11
8
  * 2. dispatch — `opts.dispatchRole`: the role the dispatch seam resolved from
12
9
  * the Assignment/teammate prompt (`**Execute as**: <id>`, the single
13
10
  * parser in `src/subagents-seam.ts`), canonicalized by the same trim +
14
11
  * `roleIds` lookup. An absent, empty, `'inherit'` or no-longer-declared
15
- * reference falls through silently — a role removed after the dispatch is
16
- * not an error;
17
- * 3. rules — the existing `resolveRole` passthrough, unchanged (a declared
18
- * role wins; no-rule-match / an explicit rule targeting `'inherit'`
19
- * resolve to `'inherit'`).
20
- *
21
- * `'inherit'` is the single "no specific role" outcome — reached by
22
- * no-rule-match OR an explicit rule targeting `'inherit'`.
12
+ * reference resolves to `undefined` — no role, no error.
23
13
  *
24
- * There is deliberately NO auto-match stage: the only host point it ever had
25
- * was the deleted `agent/request` inject, and running an LLM call inside the
26
- * walk's `stream()` would be a nested request per failure (design
27
- * dsh-022-rc1-upgrade §2.2). Resolution is therefore pure and synchronous —
28
- * the walk must throw its pre-attempt errors before the first chunk.
14
+ * The rules stage is DELETED with `roles.rules` (v2). `undefined` is the
15
+ * single "no specific role" outcome — the plugin does not intervene and the
16
+ * child inherits the delegating agent's route.
29
17
  *
30
18
  * @module dsh-llm-fallbacks/role-resolution
31
19
  */
32
- import { type FallbacksRoleRule } from './config.ts';
33
- import { type AgentLike } from './roles.ts';
34
- export type { FallbacksRoleRule } from './config.ts';
35
- /** Options for {@link resolveDeclaredRole}. */
36
20
  export interface DeclaredRoleOptions {
37
21
  /**
38
22
  * Dispatch-declared role: the DECLARED RAW id the seam resolved from the
@@ -40,27 +24,14 @@ export interface DeclaredRoleOptions {
40
24
  * live `roleIds` map; absent, `inherit`, or no-longer-declared → skipped.
41
25
  */
42
26
  dispatchRole?: string;
43
- /** Warning sink — the decision path injects the plugin logger. */
44
- warn: (message: string) => void;
45
- /**
46
- * The route the agent's request was actually SERVED by, when that differs
47
- * from the pair recorded on the agent (the virtual `FallbacksChain/Auto`
48
- * row). Rule matching accepts either, so a rule keyed on the real chain head
49
- * keeps matching a child whose recorded route is the virtual pair (QC1 I-1).
50
- * `undefined` for every real route.
51
- */
52
- servedRoute?: {
53
- provider: string;
54
- model: string;
55
- };
56
27
  }
57
28
  /**
58
- * The dispatch role: explicit → dispatch-declared → rules.
29
+ * The dispatch role: explicit preset → dispatch-declared. Returns
30
+ * `undefined` when neither stage resolves (NOT SPECIFIED — no plugin
31
+ * intervention).
59
32
  *
60
33
  * `roleIds` is the canonical trimmed-id map (`trimmed id → declared raw id`,
61
34
  * built in `src/index.ts` apply()): every stage canonicalizes its reference
62
- * by trim and returns the DECLARED RAW id, so roleDef lookups in
63
- * `chains.ts` / `commands.ts` find the stored `roles.list[]` entry exactly
64
- * (qc2 F-001 — no silent role inertness on padded YAML ids).
35
+ * by trim and returns the DECLARED RAW id.
65
36
  */
66
- export declare function resolveDeclaredRole(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, opts: DeclaredRoleOptions): string;
37
+ export declare function resolveDeclaredRole(agentPreset: string | undefined, roleIds: ReadonlyMap<string, string>, opts: DeclaredRoleOptions): string | undefined;
package/dist/schema.d.ts CHANGED
@@ -15,9 +15,10 @@
15
15
  * Object fields are optional by default in schemastery; `.default()` fills
16
16
  * the spec defaults, `.required()` keeps mandatory fields. Unknown keys are
17
17
  * RETAINED by the composition (verified plan Task 1 Step 1) — that is what
18
- * lets `detectLegacyKeys` flag two-block-era leftovers (`chains` /
19
- * `roles.default`) on the composed object at startup (warn + gateway
20
- * `legacyKeys`, see `src/index.ts` apply()).
18
+ * lets `detectLegacyKeys` flag v1 two-block leftovers (`rootChain` /
19
+ * `roles.rules` / …) on the composed object at startup (warn-only
20
+ * migration; the gateway strips them via LEGACY_KEYS, see
21
+ * `src/gateway.ts`).
21
22
  *
22
23
  * Current host contract: the whole schema is marked `volatile` (the host
23
24
  * retired `installSection` — the Loader entry Config IS the settings
@@ -34,7 +35,7 @@
34
35
  * @module dsh-llm-fallbacks/schema
35
36
  */
36
37
  import z from '@deepseek-ai/schemastery';
37
- import type { FallbacksConfig } from './config.ts';
38
+ import type { FallbacksConfigSource } from './config.ts';
38
39
  declare module '@deepseek-ai/schemastery' {
39
40
  namespace Schemastery {
40
41
  interface Meta {
@@ -43,4 +44,4 @@ declare module '@deepseek-ai/schemastery' {
43
44
  }
44
45
  }
45
46
  }
46
- export declare const Config: z<FallbacksConfig>;
47
+ export declare const Config: z<FallbacksConfigSource>;
package/dist/seeds.d.ts CHANGED
@@ -42,7 +42,7 @@
42
42
  *
43
43
  * @module dsh-llm-fallbacks/seeds
44
44
  */
45
- import { type FallbackStrategy, type FallbacksConfig, type FallbacksConfigLogger, type FallbacksRoles } from './config.ts';
45
+ import { type FallbacksConfig, type FallbacksConfigLogger, type FallbacksRoles } from './config.ts';
46
46
  /** A seed declaration from a companion plugin (spec §9.1). */
47
47
  export interface SeedDeclaration {
48
48
  id: string;
@@ -112,10 +112,8 @@ export interface EffectiveRole {
112
112
  id: string;
113
113
  /** Effective row persona. */
114
114
  persona: string;
115
- /** Passthrough — never touched by seeds (R4). */
116
- chain?: string[];
117
- /** Passthrough — never touched by seeds (R4). */
118
- fallback?: FallbackStrategy;
115
+ /** Passthrough — never touched by seeds (R4): the named-chain POINTER. */
116
+ chain?: string;
119
117
  /** Id is in the live declaration set (trimmed row-id match). */
120
118
  seeded: boolean;
121
119
  /** `seeded` && row persona !== current seed default. */
@@ -154,7 +152,7 @@ export interface SeedsWireStatus {
154
152
  export interface SeedsIo {
155
153
  /** Fresh composed config read (the same source the gateway reads). */
156
154
  read(): FallbacksConfig;
157
- /** Persist a full `{ list, rules }` to the settings user layer. */
155
+ /** Persist the `{ list }` to the settings user layer. */
158
156
  writeRoles(roles: FallbacksRoles): Promise<void>;
159
157
  }
160
158
  /**
@@ -192,7 +190,7 @@ export declare class FallbacksSeedManager {
192
190
  * RESOLVED effective default before/after the candidate slice commit
193
191
  * (at-default tracking follows the resolution winner — a producer that
194
192
  * is not the winner for an id it declares never rewrites the row),
195
- * writes only when the computed `{ list, rules }` differs from the
193
+ * writes only when the computed `{ list }` differs from the
196
194
  * current composed roles (idempotent, AC-1), and commits the slice only
197
195
  * after a successful write (compute → write → commit; retry-safe). An
198
196
  * empty batch drops the producer's slice.
package/dist/state.d.ts CHANGED
@@ -16,51 +16,9 @@
16
16
  *
17
17
  * @module dsh-llm-fallbacks/state
18
18
  */
19
- import type { FallbackSwitchReason } from './events.ts';
20
19
  import { CooldownStore } from './cooldown.ts';
21
20
  import { RecoveryStore } from './recovery.ts';
22
21
  import type { RecoveryPolicy } from './config.ts';
23
- /**
24
- * One plugin-originated subagent switch the host allowlist blocked (plan
25
- * dsh-012-subagent-routing T3; spec D1). The intersection of the resolved
26
- * candidates with the effective allowlist was empty while the unconstrained walk
27
- * would have switched — the attempt is recorded in-memory only (issue #52:
28
- * no durable session write) for the Subagents card warning (spec D4/T5).
29
- */
30
- export interface BlockedSwitchAttempt {
31
- /** Epoch ms when the switch was blocked. */
32
- at: number;
33
- /** The route the unconstrained walk would have switched to (exact provider+model). */
34
- route: {
35
- provider: string;
36
- model: string;
37
- };
38
- /** The failure reason that triggered the would-be switch. */
39
- reason: FallbackSwitchReason;
40
- }
41
- /**
42
- * How the effective subagent chain head was chosen (plan dsh-012-subagent-routing
43
- * T5 / T2 M3). Lives on a per-agent map (mirrors {@link BlockedSwitchAttempt}),
44
- * not on {@link AgentFallbackState}. The write point is the dispatch seam
45
- * (direction C): `authorized` = the spawn request carried an explicit route,
46
- * `injected` = the seam mounted the child on the virtual chain row.
47
- */
48
- export type ChainHeadSource = 'authorized' | 'injected';
49
- /**
50
- * The effective chain head for one subagent, plus the source label the
51
- * Subagents card renders (spec D4). In-memory only (issue #52).
52
- */
53
- export interface EffectiveChainHead {
54
- /** Epoch ms when this head was recorded (recency for the card snapshot). */
55
- at: number;
56
- /** Exact provider+model of the effective chain head. */
57
- route: {
58
- provider: string;
59
- model: string;
60
- };
61
- /** `authorized` = host spawn preserved; `injected` = plugin role-inject. */
62
- source: ChainHeadSource;
63
- }
64
22
  /** One agent's fallback runtime state. */
65
23
  export interface AgentFallbackState {
66
24
  /** `provider/model → expiry epoch ms`; lazily expired on read. */
@@ -61,22 +61,25 @@
61
61
  * before the child's first pre-step. This module reads no provider persona
62
62
  * capability and injects no agent preset.
63
63
  *
64
- * Virtual chain mount (plan dsh-022-rc1-upgrade §1.3; direction C): the SAME
65
- * resolution point is where a dispatch child is mounted on the virtual
66
- * `FallbacksChain/Auto` row — `request.agentOptions` is stamped (rc.1 merges
67
- * it over the parent's route when `manager.startLocal` creates the child) when
68
- * all five conditions hold: a resolved role, no explicit `agentOptions` route
69
- * (explicit = authorized, untouched), a LOCAL backend, an active config whose
70
- * resolved role chain has a dispatchable exact head ON the host allowlist when
71
- * the subagent policy is `enabled`, and a policy that is not `unprovable`. The
72
- * record carries `virtualRouteInjected` (sticky) and the effective chain head
73
- * is labelled in `chainHeads` (`injected` / `authorized`). Injection is
64
+ * Role chain stamp (named virtual chains §3, `injectRoleChain`): the SAME
65
+ * resolution point is where a dispatch child is pointed at a named chain —
66
+ * `request.agentOptions` is stamped with `{ provider: 'FallbacksChain',
67
+ * model: <role.chain> }` (rc.1 merges it over the parent's route when
68
+ * `manager.startLocal` creates the child) when all FOUR conditions hold: a
69
+ * declared role resolved, its `chain` is non-empty AND declared, the request
70
+ * carries no explicit `agentOptions` (explicit = authorized, untouched), and
71
+ * the backend is LOCAL. No host-policy consultation: the mapping is the
72
+ * user's STATIC config, never a model-authored route — no allowlist, no
73
+ * `unprovable` branch, no `virtualRouteInjected` flag. The seam does NOT
74
+ * check conformance: a declared-but-non-conforming chain is still stamped and
75
+ * the walk's own tail gate refuses it — one gate, in the walk. Injection is
74
76
  * creation-time only BY CONSTRUCTION: rc.1's `startActivation` rejects any
75
- * child id that already exists (`DUPLICATE_CHILD`), so this path never attaches
76
- * to an existing child and cannot overwrite its durable selection — a
77
- * caller-reserved `childId` is a pre-chosen identity for a NEW child. A
78
- * non-injectable dispatch forwards the caller's spec untouched (external
79
- * backends keep pure inheritance).
77
+ * child id that already exists (`DUPLICATE_CHILD`): a caller-reserved
78
+ * `childId` the seam already recorded is a RE-ACTIVATION of that child (its
79
+ * durable selection is already fixed), so it stamps nothing — that activation
80
+ * writes the role record only (I-000081). A non-stamped dispatch forwards the
81
+ * caller's spec untouched (external backends keep pure inheritance; not
82
+ * specified ⇒ the child inherits the delegating agent's route).
80
83
  *
81
84
  * In-session notice row (plan Task 3): the SAME install point registers the
82
85
  * `agent/pre-step` emitter (`./role-notice.ts`) over the record map it owns, so
@@ -107,8 +110,6 @@
107
110
  */
108
111
  import type { Context } from '@deepseek-ai/cordis';
109
112
  import { type FallbacksConfig, type FallbacksRole } from './config.ts';
110
- import type { EffectiveChainHead } from './state.ts';
111
- import type { EffectivePolicy } from './subagent-policy.ts';
112
113
  /**
113
114
  * The cordis service-read waterfall event the seam registers on. Referenced
114
115
  * ONCE (registration) so a future cordis rename is a one-line change here.
@@ -168,18 +169,6 @@ export interface SubagentSeamRecord {
168
169
  * `true` survives.
169
170
  */
170
171
  personaNotApplied?: boolean;
171
- /**
172
- * `true` when this dispatch MOUNTED the child on the virtual
173
- * `FallbacksChain/Auto` row (design §1.3): the plugin stamped the creation
174
- * options, so the child's chain is plugin-originated and its switches are
175
- * allowlist-filtered. Read by the chain walk (`ChainWalkDeps.seamRecord`)
176
- * and never by a host surface.
177
- *
178
- * STICKY, like `personaNotApplied`: the mount happened at spawn and a record
179
- * rewrite (a resume reusing the `childId`) carries no verdict of its own, so
180
- * the first `true` survives.
181
- */
182
- virtualRouteInjected?: true;
183
172
  }
184
173
  /** One consumed prompt content block (`@deepseek-ai/dsh-llm` `ContentBlock` text members). */
185
174
  export interface SubagentPromptBlockView {
@@ -243,21 +232,10 @@ export interface SubagentSeamOptions {
243
232
  roles?: () => readonly FallbacksRole[];
244
233
  /**
245
234
  * Live config reader — the SAME `source()` the runtime and the adapter walk
246
- * read. Absent ⇒ the seam never mounts a child on the virtual route (a
247
- * partial wiring keeps the native dispatch unchanged).
235
+ * read. Absent ⇒ the seam never stamps a role chain (a partial wiring keeps
236
+ * the native dispatch unchanged).
248
237
  */
249
238
  source?: () => FallbacksConfig;
250
- /**
251
- * The host subagent model-selection policy for one agent (guarded read,
252
- * fail-closed). Absent ⇒ treated as `'disabled'` (no policy to fail closed
253
- * on); an `'unprovable'` answer skips the mount.
254
- */
255
- policyFor?: (agentId: string) => EffectivePolicy;
256
- /**
257
- * Per-agent effective chain head, keyed by child session id (the Subagents
258
- * card's read). Absent ⇒ the seam records no head label.
259
- */
260
- chainHeads?: Map<string, EffectiveChainHead>;
261
239
  /** Contained debug sink (at most one line per contained degrade). */
262
240
  debug?: (message: string) => void;
263
241
  }
@@ -331,15 +309,15 @@ export declare function resolveDeclaredRoleFromAssignment(prompt: string, roleId
331
309
  * DECLARED RAW id the seam already resolved, so this is an exact id match, not
332
310
  * a second canonicalization pass.
333
311
  *
334
- * Only `id` and `persona` are read: no `chain`, no `rootChain`, no routing
335
- * state — a `chain: []` role's persona resolves exactly like a chained one's.
312
+ * Only `id` and `persona` are read: no `chain` pointer, no routing state — a
313
+ * role that specifies a chain names its persona exactly like one that does not.
336
314
  *
337
315
  * @param roles - the live declared roles (`roles.list`).
338
316
  * @param roleId - the DECLARED RAW role id resolved at the seam.
339
317
  */
340
318
  export declare function personaForRole(roles: readonly FallbacksRole[], roleId: string): string | undefined;
341
319
  /**
342
- * @internal Test seam (mirrors `chainHeads`): the seam installed on
320
+ * @internal Test seam: the seam installed on
343
321
  * `ctx`, if any. Not part of the plugin's public surface — lets tests read and
344
322
  * seed the per-child record map without reaching into the installation
345
323
  * closure. `undefined` when no seam is installed.
@@ -9,12 +9,12 @@
9
9
  * wires these helpers at request time (Task 2). Malformed rows NEVER
10
10
  * throw: they warn ONCE per row instance (through `console.warn` — the
11
11
  * resolver's 3-argument contract has no logger parameter) and are
12
- * skipped; a legacy non-empty non-conforming `rootChain` keeps the
13
- * legacy failure walk verbatim (P6).
12
+ * skipped; a non-empty non-conforming `entries` tail keeps the failure walk
13
+ * verbatim (the chain's delegate stays inert).
14
14
  *
15
15
  * @module dsh-llm-fallbacks/time-slots
16
16
  */
17
- import type { FallbacksConfig } from './config.ts';
17
+ import type { NamedChain } from './config.ts';
18
18
  /** Official all-day selectors — the ONLY legal tails (length 1, XOR):
19
19
  * Flash (the current official default) and Pro, both selectable. The Pro
20
20
  * tail id is the catalog id verbatim: the official default catalog
@@ -87,32 +87,29 @@ export declare const PRESETS: Record<PresetId, PresetDefinition>;
87
87
  */
88
88
  export declare function isAllDayConforming(chain: readonly string[]): boolean;
89
89
  /**
90
- * Effective chain for a root request at `now` (P5): the FIRST extra row
91
- * whose descriptor contains `now` (stored order) — that row's `chain`
92
- * REPLACES the all-day chain (never concatenated); no match ⇒ `rootChain`
93
- * (the all-day row, always last and required). Malformed rows warn once
94
- * and are skipped; never throws.
90
+ * Effective chain for a request on ONE named chain at `now`: the FIRST
91
+ * matching row of {@link NamedChain.timeSlots} (stored order) — that row's
92
+ * `chain` REPLACES {@link NamedChain.entries} (never concatenated); no match
93
+ * ⇒ the chain's own `entries`. Malformed rows warn once and are skipped;
94
+ * never throws.
95
95
  *
96
- * P6 (qc1 F-001): without a conforming all-day the slot rows are inert, so
97
- * this IS the raw `rootChain` — the gate lives in {@link resolveSlotState},
98
- * the single source every slot surface (this, the 分时切换 log, the
99
- * `/fallbacks` strip, the virtual adapter delegate, its served-route anchor)
100
- * reads.
96
+ * Without a conforming entries tail the slot rows are inert, so this IS the
97
+ * raw `entries` — the gate lives in {@link resolveSlotState}, the single
98
+ * source every slot surface reads.
101
99
  */
102
- export declare function resolveEffectiveChain(config: FallbacksConfig, now: Date, tz: string): string[];
100
+ export declare function resolveEffectiveChain(chain: NamedChain, now: Date, tz: string): string[];
103
101
  /**
104
- * Slot winner + display label (P5): drives 分时切换 detection (per-root-agent
105
- * last-winner marker, in-process only) and the card / `/fallbacks` status
106
- * strip. `winner` is the matching row or `'all-day'`; `label` names the
107
- * slot (frozen preset label or `custom HH:mm-HH:mm`).
102
+ * Slot winner + display label for one named chain: drives 分时切换 detection
103
+ * (per-(agent, chain) last-winner marker, in-process only) and the card /
104
+ * `/fallbacks` status strip. `winner` is the matching row or `'all-day'`;
105
+ * `label` names the slot (frozen preset label or `custom HH:mm-HH:mm`).
108
106
  *
109
- * P6 gate (qc1 F-001): without a conforming all-day
110
- * (`isAllDayConforming(config.rootChain)`) the winner is ALWAYS `'all-day'`
111
- * — a legacy multi-model (or empty) chain earns no slot rows, so every
112
- * surface fed by this resolver reports the inert state and routing stays on
113
- * the raw `rootChain` (the legacy walk verbatim).
107
+ * Gate: without a conforming entries tail (`isAllDayConforming(chain.entries)`)
108
+ * the winner is ALWAYS `'all-day'` — a multi-model (or empty) chain earns no
109
+ * slot rows, so every surface fed by this resolver reports the inert state and
110
+ * routing stays on the raw `entries`.
114
111
  */
115
- export declare function resolveSlotState(config: FallbacksConfig, now: Date, tz: string): {
112
+ export declare function resolveSlotState(chain: NamedChain, now: Date, tz: string): {
116
113
  winner: SlotRowConfig | 'all-day';
117
114
  label: string;
118
115
  };
@@ -114,14 +114,11 @@ export declare const FALLBACKS_TUI_SECTION_NS = "fallbacks";
114
114
  */
115
115
  export declare const JSON_FIELD_MAX_DRAFT_BYTES: number;
116
116
  /**
117
- * The `fallbacks` /settings section: 10 fields covering the web-card
118
- * capabilities (the default-model choice rides `rootChain`'s last entry and
119
- * the per-role fallback strategy rides `roles.list` JSON; the config-level
120
- * `enabled` switch is REMOVED — plan fallbacks-web-ux-alignment T2, the
121
- * Plugins-page row toggle is the master switch and the runtime gates read
122
- * content presence; the always-mode retry cap and the dispatch role
123
- * auto-match switch are REMOVED with their mechanisms — direction C, only a
124
- * `FallbacksChain/Auto` session walks a chain). Scalar
117
+ * The `fallbacks` /settings section covering the web-card capabilities
118
+ * (named virtual chains v2: ONE `Chains (JSON)` field over `chains` on the
119
+ * same bounded-parse machinery; `Roles (JSON)` keeps the chain POINTER; the
120
+ * v1 `rootChain` / top-level `timeSlots` / `roles.rules` fields are REMOVED
121
+ * with their mechanisms). Scalar
125
122
  * capabilities use native kinds (boolean/number/select); complex structures
126
123
  * use `text` fields with custom `format`/`parse` that mirror the gateway's
127
124
  * validation. Built fresh per call — the host deep-freezes whatever it