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,54 +1,38 @@
1
1
  /**
2
- * Virtual `FallbacksChain/Auto` LLM adapter (plan fallbacks-virtual-chain
3
- * Task 1, technical pins P1–P3; PR #62 feedback round): a mount-only
4
- * catalog row that makes the configured effective chain selectable as the
5
- * session **primary** in the host model picker, without patching dsh or
6
- * dsh-TUI (web and TUI share the adapter catalog).
2
+ * Virtual `FallbacksChain` LLM adapter (named virtual chains, plan
3
+ * named-virtual-chains T2): one provider carrying N model rows — ONE per
4
+ * named chain, the row id BEING the chain id (picker selection is
5
+ * `FallbacksChain/<chain id>`), without patching dsh or dsh-TUI.
7
6
  *
8
- * Wiring (P2): `installFallbacksAdapter` installs ONE conditional
7
+ * Wiring: `installFallbacksAdapter` installs ONE conditional
9
8
  * `ctx.inject(['llm'])` child — absent `llm` service (test harness) is a
10
9
  * clean no-op, and fiber unload ⇒ the child's disposer unregisters the
11
10
  * route. Registration is an idempotent transition-reconcile on COMMITTED
12
- * config snapshots: register when nothing is configured → something is
13
- * (`isFallbackActive` — content presence, plan fallbacks-web-ux-alignment
14
- * T2), unregister on the reverse, driven by the settings `onChange` hook
15
- * (the returned reconcile thunk, wired by `apply()`) plus child activation.
16
- * The row is visible whenever something is configured — a non-conforming
17
- * all-day chain does NOT hide it (PR #62 feedback); conformance still gates
18
- * a successful delegate (`effectiveHeadOf` below refuses a non-conforming
19
- * all-day). The registration gate is `isFallbackActive` content presence
20
- * (`rootChain` / `timeSlots` / declared roles): a slot-row edit that flips
21
- * that gate (e.g. removing the only slot while `rootChain` and roles are
22
- * empty) intentionally churns registration — the content-presence contract
23
- * (plan fallbacks-web-ux-alignment T2), not churn to design away; do NOT
24
- * re-narrow the condition to ignore `timeSlots`. Conformance-only chain
25
- * edits still never churn (conformance gates the delegate, not the row).
11
+ * config snapshots: register when `chains` goes empty → non-empty
12
+ * (`isFallbackActive` = `chains.length > 0`), unregister on the reverse,
13
+ * driven by the settings `onChange` hook (the returned reconcile thunk,
14
+ * wired by `apply()`) plus child activation. A chain with empty or
15
+ * non-conforming `entries` still shows its row (conformance gates the
16
+ * delegate via {@link effectiveHeadOf}, not the row). One
17
+ * `llm.registerAdapter([FALLBACKS_PROVIDER], adapter)` call serves all rows.
26
18
  *
27
- * Adapter behavior (P1/P3, direction C): `listModels` advertises exactly the
28
- * one virtual row; `stream()` WALKS the session's chain (cooldown / failed-set
29
- * / switch cap / recovery bookkeeping) in `src/chain-walk.ts` — the walk is
30
- * injected at install and owns the pre-attempt guards, so this module stays
31
- * the catalog + metadata surface. `resolveModel` proxies the current effective
32
- * head's metadata when resolvable (modalities/context-window/reasoning follow
33
- * the head) with a permissive default otherwise — never throws.
34
- * `providerRetryPolicy` and `imageRequestPricing` mirror the same head for the
35
- * host-captured retry policy and the token meter — also never throwing.
19
+ * Adapter behavior: `listModels` advertises one row per chain;
20
+ * `stream()` WALKS the selected chain (cooldown / failed-set / switch cap /
21
+ * recovery bookkeeping) in `src/chain-walk.ts` — the walk is injected at
22
+ * install and owns the pre-attempt guards. `resolveModel` /
23
+ * `imageRequestPricing` resolve the chain FROM the `model` argument and
24
+ * follow its effective head (permissive default otherwise — never throw).
25
+ * `providerRetryPolicy` returns `undefined` (documented degradation): the
26
+ * host captures the policy ONCE per provider at registration, so the virtual
27
+ * route cannot mirror a per-chain head's policy — the host default applies.
36
28
  *
37
- * @module dsh-llm-fallbacks/virtual-adapter
29
+ @module dsh-llm-fallbacks/virtual-adapter
38
30
  */
39
31
  import type { Context } from '@deepseek-ai/cordis';
40
32
  import { LlmAdapter, type GenerateOptions, type LlmImageRequestPricing, type LlmModelInfo, type LlmResolvedModelInfo, type LlmRuntime, type ResolvedRetryPolicy, type StreamChunk } from '@deepseek-ai/dsh-llm';
41
- import { type FallbacksConfig } from './config.ts';
42
- import type { PolicyRoute } from './subagent-policy.ts';
33
+ import { type FallbacksConfig, type NamedChain } from './config.ts';
43
34
  /** Provider route of the virtual adapter (exact string, spec lock). */
44
35
  export declare const FALLBACKS_PROVIDER = "FallbacksChain";
45
- /**
46
- * Model id of the virtual catalog row (exact string, spec lock). "Auto" is
47
- * the hardcoded picker id (not i18n — user decision 2026-08-18). The
48
- * catalog `name` the host picker renders is dynamic — see
49
- * {@link pickerDisplayName} (`Auto: <model>[<slot>]`).
50
- */
51
- export declare const FALLBACKS_CHAIN_MODEL = "Auto";
52
36
  /** One dispatchable exact head: `provider/model`. Wildcards are never seeds (P3). */
53
37
  export interface EffectiveHead {
54
38
  provider: string;
@@ -57,50 +41,32 @@ export interface EffectiveHead {
57
41
  /**
58
42
  * The FIRST DISPATCHABLE exact head of a chain — the single definition of
59
43
  * "effective head" (F-001) shared by this module's metadata paths
60
- * (`pickerDisplayName` / `resolveModel` / `providerRetryPolicy` /
61
- * `imageRequestPricing`), the walk's head and served-route resolution
62
- * (`servedRouteOf` in `src/chain-walk.ts`), and the seam's mount gate
63
- * (`injectRoute` in `src/subagents-seam.ts`). Walks the SAME chain
64
- * `resolveEffectiveChain` produces, skipping entries that can never
44
+ * (`pickerDisplayName` / `resolveModel` / `imageRequestPricing`) and the
45
+ * walk's head resolution (`resolveWalk` in `src/chain-walk.ts`). Walks the
46
+ * SAME chain `resolveEffectiveChain` produces, skipping entries that can never
65
47
  * be dispatched: malformed selectors (config-warning path), `provider/*`
66
48
  * `provider/*` wildcards (no real pair), and self-routes back to
67
49
  * `FallbacksChain/*` (the P1 recursion guard). `undefined` when the chain
68
50
  * is empty or no entry is dispatchable.
69
- *
70
- * `allowedModels` (spec D1) additionally restricts the result to the host
71
- * subagent allowlist: a PLUGIN-originated route is only ever attempted on a
72
- * route the host authorizes, so the seam's mount gate and a plugin-originated
73
- * walk's start pass the enabled policy's list and an entry it excludes is
74
- * skipped like any other non-dispatchable entry. An empty intersection is
75
- * `undefined` — the caller declines the mount / refuses the walk.
76
51
  */
77
- export declare function firstDispatchableExactHead(chain: readonly string[], allowedModels?: readonly PolicyRoute[]): EffectiveHead | undefined;
52
+ export declare function firstDispatchableExactHead(chain: readonly string[]): EffectiveHead | undefined;
78
53
  /**
79
- * The effective chain head at `now` (P1): the first DISPATCHABLE exact head
80
- * of the SAME `resolveEffectiveChain` the routing engine uses — thin
81
- * config+clock entry into the shared {@link firstDispatchableExactHead} (no
82
- * divergent skip/walk rules). `undefined` when the chain is empty or no
83
- * entry is dispatchable (wildcard, self-route to `FallbacksChain/*`,
84
- * malformed selector).
85
- *
86
- * Conformance gate (PR #62 feedback): the all-day `rootChain` must be
87
- * conforming (exactly one official model — Flash or Pro)
88
- * for a successful delegate —
89
- * a legacy multi-model (or empty) all-day chain earns no primary
90
- * semantics even though the picker row is visible. `resolveModel` then
91
- * falls back to the permissive default. The delegate side of that gate is the
92
- * chain walk's ROOT-origin rule (`UNDISPATCHABLE_EFFECTIVE_HEAD`); a subagent
93
- * walks its role chain instead, so the all-day shape never gates a child.
54
+ * The effective chain head of ONE named chain at `now`: the first
55
+ * DISPATCHABLE exact head of the SAME `resolveEffectiveChain` the walk uses —
56
+ * thin chain+clock entry into the shared {@link firstDispatchableExactHead}.
57
+ * `undefined` when the entries are empty or non-conforming (the tail must be
58
+ * exactly one official model), or no entry is dispatchable (wildcard,
59
+ * self-route to `FallbacksChain/*`, malformed selector). `resolveModel` then
60
+ * falls back to the permissive default; the delegate side of the conformance
61
+ * gate is the chain walk's `UNDISPATCHABLE_EFFECTIVE_HEAD`.
94
62
  */
95
- export declare function effectiveHeadOf(config: FallbacksConfig, now: Date): EffectiveHead | undefined;
63
+ export declare function effectiveHeadOf(chain: NamedChain, now: Date, tz?: string): EffectiveHead | undefined;
96
64
  /**
97
- * Host picker label: `Auto: <displayName>[<slot>]`.
98
- * Display name first so the trigger stays readable and platforms stay
99
- * distinguishable (catalog `name`, not the model id). Bare `Auto` when
100
- * there is no dispatchable head. Slot label from {@link resolveSlotState}.
101
- * @param modelDisplayName - catalog/resolved name; falls back to the id.
65
+ * Host picker label for one chain: `` `${chain.label}: <head>[<slot>]` ``.
66
+ * Bare `chain.label` when there is no dispatchable head. The label is
67
+ * user-editable and display-only; the model id is the chain id.
102
68
  */
103
- export declare function pickerDisplayName(config: FallbacksConfig, now?: Date, modelDisplayName?: string): string;
69
+ export declare function pickerDisplayName(chain: NamedChain, now?: Date, modelDisplayName?: string, tz?: string): string;
104
70
  /**
105
71
  * The virtual adapter (P1/P3). `stream()` hands the call to the injected walk
106
72
  * (see `src/chain-walk.ts`) — registration metadata stays here.
@@ -115,50 +81,39 @@ export declare class FallbacksChainAdapter extends LlmAdapter {
115
81
  id: string;
116
82
  name: string;
117
83
  };
118
- /** Advisory catalog: one virtual row; `name` is the live picker label. */
84
+ /** Advisory catalog: ONE row per named chain; `name` is the live picker label. */
119
85
  listModels(provider: string): Promise<readonly LlmModelInfo[]>;
120
86
  /**
121
- * Proxy the current effective head's model metadata when resolvable (P3):
122
- * modalities/context-window/reasoning follow the head — the truthful
123
- * answer for the CURRENT head. Permissive default otherwise; never throws.
87
+ * Model-scoped metadata: resolve the chain FROM the `model` argument (the
88
+ * chain id) and follow its effective head — modalities/context-window/
89
+ * reasoning follow the head, the truthful answer for the CURRENT head.
90
+ * Permissive default for an unknown chain id or an unresolvable head;
91
+ * never throws.
124
92
  */
125
93
  resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
126
94
  /**
127
- * Route-accurate retry attribution: the virtual row declares no policy of
128
- * its own — the virtual `provider` argument is intentionally ignored. The
129
- * host captures `adapter.providerRetryPolicy(provider) ??
130
- * resolveRetryPolicy(void 0, …)` ONCE, when a route is registered, so
131
- * answering here with the SAME policy the concrete head route declared
132
- * (e.g. a user's `llm-deepseek.retryPolicy`, including `mode: 'always'`)
133
- * keeps retries behaving exactly as they do on the head `stream()`
134
- * dispatches. Never throws: an unresolvable head, a vanished `llm`, or an
135
- * unregistered head provider degrades to `undefined` — the host's own
136
- * default. That last arm matters: the runtime's lookup THROWS `NO_ADAPTER`
137
- * for an unknown provider, and this call happens inside the plugin's
138
- * `registerAdapter`, so an escaping throw would take the whole virtual
139
- * route down instead of just degrading its policy.
95
+ * DOCUMENTED DEGRADATION (named virtual chains): the adapter stays ONE
96
+ * `FallbacksChain` provider carrying N chain rows, but the host captures
97
+ * `adapter.providerRetryPolicy(provider)` ONCE per provider at
98
+ * registration, so the virtual route can no longer mirror its head's
99
+ * retry policy. It returns `undefined` — the host's own default applies.
140
100
  */
141
101
  providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined;
142
102
  /**
143
- * Route-accurate image pricing: the virtual row has no
144
- * pricing of its own — the virtual `provider`/`model` arguments are
145
- * intentionally ignored. Resolves the SAME effective head `stream()`
146
- * dispatches and delegates through the runtime lookup, so the token
147
- * meter gets exactly the answer the concrete route's adapter would give
148
- * (including `undefined`). Never throws: an unresolvable head, a
149
- * vanished `llm`, or a throwing delegate degrades to `undefined` — the
150
- * token meter's neutral estimate.
103
+ * Image pricing, model-scoped: resolve the chain FROM the `model`
104
+ * argument and delegate through the runtime lookup for its effective head
105
+ * (including `undefined`). Never throws: an unknown chain id, an
106
+ * unresolvable head, a vanished `llm`, or a throwing delegate degrades to
107
+ * `undefined` — the token meter's neutral estimate.
151
108
  */
152
- imageRequestPricing(_provider: string, _model: string): LlmImageRequestPricing | undefined;
109
+ imageRequestPricing(_provider: string, model: string): LlmImageRequestPricing | undefined;
153
110
  /**
154
- * The virtual route's entry point (P1, direction C): the session's chain is
155
- * WALKED (`src/chain-walk.ts`) — the walk owns the pre-attempt guards
156
- * (`EMPTY_EFFECTIVE_CHAIN` / `UNDISPATCHABLE_EFFECTIVE_HEAD` /
157
- * `LLM_UNAVAILABLE`, thrown synchronously before any chunk, so the runtime
158
- * normalizes them exactly like an adapter throw) because only it can resolve
159
- * WHICH chain the calling session walks: a subagent's role chain is not the
160
- * root's all-day chain, so a root-only gate here would refuse a child that
161
- * has a perfectly walkable role chain.
111
+ * The virtual route's entry point: `options.model` IS the chain id and the
112
+ * selected chain is WALKED (`src/chain-walk.ts`) — the walk owns the
113
+ * pre-attempt guards (`UNKNOWN_CHAIN` / `EMPTY_EFFECTIVE_CHAIN` /
114
+ * `UNDISPATCHABLE_EFFECTIVE_HEAD` / `LLM_UNAVAILABLE`, thrown synchronously
115
+ * before any chunk, so the runtime normalizes them exactly like an adapter
116
+ * throw).
162
117
  */
163
118
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
164
119
  }
package/locale/en.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Fallbacks",
4
- "description": "Automatic provider/model fallback chains that keep agent steps running when LLM requests keep failing (auth, quota, rate limit).",
4
+ "description": "Named fallback chains for model requests: on failure the walk switches to the next model in the chain so agent steps keep running; the main agent must select a virtual chain in the model picker for fallback to apply.",
5
5
  "detail": "docs/DETAIL.md"
6
6
  }
7
7
  }
package/locale/zh.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "故障降级",
4
- "description": "模型请求持续失败(认证、配额、限流)时自动切换备用模型链,保持 agent 步骤继续运行。",
4
+ "description": "为模型请求配置命名降级链:失败时按链内顺序切换到备用模型,保持 agent 步骤继续运行;主代理需在模型选择器中选择一条虚拟链才有降级效果。",
5
5
  "detail": "docs/DETAIL.zh.md"
6
6
  }
7
7
  }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "dsh",
6
6
  "dsh-plugin"
7
7
  ],
8
- "version": "0.7.0",
8
+ "version": "0.8.0",
9
9
  "type": "module",
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
@@ -1,19 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
- import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
4
- import type { FallbacksSettingsController, FallbacksSettingsState } from './fallbacks-store.ts';
5
- /** Injected dependencies of {@link GeneralFallbacksRow} (slot `inject`). */
6
- export interface GeneralFallbacksRowInjected {
7
- /** The shared controller (loaded on first mount, refreshed on pushed invalidations). */
8
- controller: FallbacksSettingsController;
9
- /** uSES subscription hook bound to the store (inject face — advisor pattern). */
10
- useSnapshot: SnapshotSelectorHook<FallbacksSettingsState>;
11
- }
12
- /** Props delivered by the slot outlet: runtime share + locale seat + inject face. */
13
- export type GeneralFallbacksRowProps = PropsRuntime<'settings.general.item'> & PropsLocale<'fallbacks'> & GeneralFallbacksRowInjected;
14
- /**
15
- * Render the Fallbacks status row.
16
- * @param props - composed slot props.
17
- * @returns the row element tree.
18
- */
19
- export declare function GeneralFallbacksRow({ controller, useSnapshot, t }: GeneralFallbacksRowProps): ReactNode;
package/dist/roles.d.ts DELETED
@@ -1,89 +0,0 @@
1
- /**
2
- * Role resolution for fallback chains (spec §7.1; plan fallbacks-role-runtime
3
- * Task 1).
4
- *
5
- * This module is the RULES stage. Dispatch-time role resolution
6
- * (plan fallbacks-role-automatch; plan dsh-148 added the dispatch stage) is
7
- * the three-stage resolver in `role-resolution.ts` — explicit
8
- * (`session.header.agentPreset`) → dispatch-declared role (the seam record
9
- * resolved from `**Execute as**`) → this rules stage; `AgentLike` carries the
10
- * additive `session.header.agentPreset?` carrier for that explicit stage.
11
- *
12
- * Precedence (first hit wins):
13
- * 1. the first `rules` entry whose specified provider/model patterns match
14
- * the agent — rules are SUBAGENT-ONLY (PR #62 feedback): a root-origin
15
- * agent never matches rules and resolves straight to the built-in
16
- * `'inherit'` (→ `rootChain`). The legacy per-rule `origin` field (a
17
- * pre-feedback config may still carry `origin: root|subagent`) is
18
- * IGNORED at match time — every rule applies to subagents regardless
19
- * of the stored origin value;
20
- * 2. the built-in `'inherit'` role (no-rule-match default, spec §7.1 / D4).
21
- *
22
- * A matched rule must target a declared role id (`roleIds`, derived from
23
- * `config.roles.list`) or the built-in `'inherit'` — an undeclared target
24
- * warns and falls back to `'inherit'` (defensive; startup validation already
25
- * flagged the reference, spec §7.1 / AC-4).
26
- *
27
- * Role ids are canonicalized by trim (qc2 F-001, client-canonical trim
28
- * alignment — `validateFallbacksConfig` trims ids and rule references, so
29
- * the runtime must too): `roleIds` maps a TRIMMED id to the DECLARED RAW
30
- * id, membership compares `rule.role.trim()`, and a matched rule returns
31
- * the declared raw id — so roleDef lookups in `chains.ts` / `commands.ts`
32
- * match the stored `roles.list[].id` exactly and a padded YAML id
33
- * (`' coder '`) with a trimmed rule reference (`'coder'`) resolves to the
34
- * same role instead of silently degrading to `'inherit'`.
35
- *
36
- * A missing agent origin is treated as `'root'` — and root agents never
37
- * match rules (rules are subagent-only, PR #62 feedback). Origin is read
38
- * from `session.header.origin` — a native `SessionHeader` field the store folds
39
- * from `CreateSessionOptions.meta.origin` (`packages/core/session/src/
40
- * index.ts:884`); subagent children set it via `childSessionMeta`
41
- * (`packages/subagent/subagent/src/child-agent.ts:115`), root agents carry
42
- * none. A subagent `persona` is NOT readable at the decision point:
43
- * `AgentOptions` is provider/model/maxTokens only, and persona is installed
44
- * as a scoped system-prompt section in the child's creation window — see
45
- * `.mstar/iterations/iter-20260811-fallbacks-mount-only/guides/
46
- * role-and-model-selection-exploration.md` (Role section).
47
- *
48
- * @module dsh-llm-fallbacks/roles
49
- */
50
- import { type FallbacksRoleRule } from './config.ts';
51
- export type { FallbacksRoleRule } from './config.ts';
52
- /** Agent origins (spec §3) — an agent property, not a rule constraint:
53
- * rules are subagent-only (PR #62 feedback). */
54
- export type Origin = 'root' | 'subagent';
55
- /** Loose agent shape sufficient for role resolution (spec §3 / brief). */
56
- export interface AgentLike {
57
- options?: {
58
- provider?: string;
59
- model?: string;
60
- };
61
- session?: {
62
- header?: {
63
- origin?: Origin;
64
- /** Dispatch-time explicit role carrier (plan fallbacks-role-automatch Task 2). */
65
- agentPreset?: string;
66
- };
67
- };
68
- }
69
- /**
70
- * Resolve the fallback-chain role for an agent: first matching rule (in
71
- * listed order) → the built-in `'inherit'` role. A rule targeting an id
72
- * outside the declared-id set ∪ {'inherit'} warns once and resolves to
73
- * `'inherit'` (the defensive path — `validateFallbacksConfig` already
74
- * warned at startup, spec §7.1).
75
- *
76
- * `roleIds` is the canonical trimmed-id map built by the wiring
77
- * (`trimmed id → declared raw id`, see `src/index.ts` apply()): a matched
78
- * rule returns the declared RAW id so the roleDef lookups in
79
- * `chains.ts` / `commands.ts` find the stored `roles.list[]` entry exactly
80
- * (qc2 F-001 — no silent rule inertness on padded YAML ids).
81
- *
82
- * `warn` defaults to `console.warn` (direct-call compatibility); the
83
- * decision path injects the cordis `logger.warn` so every plugin warning
84
- * flows through the plugin log namespace (qc2 F-002 / qc3 S-3).
85
- */
86
- export declare function resolveRole(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, warn?: (message: string) => void, servedRoute?: {
87
- provider: string;
88
- model: string;
89
- }): string;
@@ -1,43 +0,0 @@
1
- /**
2
- * Route identity and allowlist intersection on resolved candidates (plan
3
- * dsh-012-subagent-routing T2; spec D1 ∩ D2).
4
- *
5
- * Pure, host-free module: plain route data in, results out. Route identity is
6
- * the EXACT provider string plus the EXACT model string — no aliasing, no
7
- * case-folding, no catalog-id substitution (spec Terms). Intersections run on
8
- * RESOLVED candidates (concrete provider+model after the chain candidate
9
- * pipeline); this module never resolves chains or expands wildcards — the
10
- * caller passes already-resolved candidates in.
11
- *
12
- * Fail-closed direction: an empty allowlist contains nothing and an empty
13
- * intersection yields `undefined` — the caller must skip the
14
- * plugin-originated route rather than fall back to an unproven one (spec D1).
15
- * Never throws.
16
- *
17
- * @module dsh-llm-fallbacks/route-allowlist
18
- */
19
- import type { PolicyRoute } from './subagent-policy.ts';
20
- /** One resolved chain candidate: a selector anchored to a concrete model id. */
21
- export type ResolvedCandidate = {
22
- readonly provider: string;
23
- readonly model?: string;
24
- };
25
- /**
26
- * Normalize resolved chain candidates into detached plain routes. Candidates
27
- * without a concrete model id are dropped (they are not routes); every
28
- * surviving entry is a fresh object, so the result never aliases the
29
- * caller's candidate list.
30
- */
31
- export declare function resolvedRoutes(candidates: readonly ResolvedCandidate[]): PolicyRoute[];
32
- /**
33
- * Whether the route is on the allowlist under exact route identity: both the
34
- * provider and the model strings must equal an allowed route exactly.
35
- */
36
- export declare function routeOnAllowlist(route: PolicyRoute, allowedModels: readonly PolicyRoute[]): boolean;
37
- /**
38
- * The first resolved candidate that is on the allowlist, preserving the
39
- * candidate order (the chain's first in-allowlist entry wins), or `undefined`
40
- * when the intersection is empty — the caller then skips the inject / switch
41
- * and the host seed stands (spec D1 ∩ D2).
42
- */
43
- export declare function firstAllowedCandidate(resolved: readonly PolicyRoute[], allowedModels: readonly PolicyRoute[]): PolicyRoute | undefined;
@@ -1,137 +0,0 @@
1
- /**
2
- * Subagent routing policy reader (plan dsh-012-subagent-routing T1; spec D1).
3
- *
4
- * Pure, host-free module: plain data in, results out. It resolves the
5
- * effective subagent model-selection policy from two snapshots the wiring
6
- * layer extracts from the host:
7
- *
8
- * 1. the session's `subagent/model-selection-policy` event payload, read
9
- * STRUCTURALLY — the upstream reader is not published from
10
- * `@deepseek-ai/dsh-tool-subagent` and throws on malformed payloads,
11
- * which is incompatible with the fail-closed no-throw requirement;
12
- * 2. the host's live subagent model-selection settings value (the
13
- * `subagent-model-selection-settings` settings namespace), typed via a
14
- * type-only peer import that is erased at emit. rc.1 made the settings
15
- * service's own snapshot reader private, so the wiring reads the settings
16
- * form value and narrows it structurally here.
17
- *
18
- * The session event wins over settings (ADR read order). Anything present but
19
- * unreadable yields `'unprovable'` (fail-closed): callers must not emit a
20
- * plugin-originated route they cannot prove is allowed. This module never
21
- * throws.
22
- *
23
- * @module dsh-llm-fallbacks/subagent-policy
24
- */
25
- import type { SubagentModelSelectionSettings } from '@deepseek-ai/dsh-tool-subagent/model-selection-settings';
26
- declare module '@deepseek-ai/dsh-session/types' {
27
- interface SessionEventMap {
28
- /**
29
- * Host-recorded route policy for a model-selectable subagent session.
30
- * The payload shape mirrors upstream exactly, so a future published
31
- * upstream augmentation merges identically. Read structurally by this
32
- * plugin; never written here.
33
- */
34
- 'subagent/model-selection-policy': {
35
- /** Exact routes this session may select explicitly for a child. */
36
- allowedModels: {
37
- provider: string;
38
- model: string;
39
- }[];
40
- };
41
- }
42
- }
43
- /** One exact child LLM route (structural twin of upstream `AllowedModelRoute`). */
44
- export type PolicyRoute = {
45
- readonly provider: string;
46
- readonly model: string;
47
- };
48
- /**
49
- * Effective subagent routing policy for one session.
50
- *
51
- * - `disabled`: host policy off or absent — selection matches pre-policy
52
- * behavior.
53
- * - `enabled`: an allowlist is proven; plugin-originated routes must stay
54
- * inside it.
55
- * - `unprovable`: the policy is on but unreadable (malformed event payload,
56
- * or enabled settings with a malformed route list) — fail-closed; no
57
- * plugin-originated route may be emitted.
58
- */
59
- export type EffectivePolicy = {
60
- readonly state: 'disabled';
61
- } | {
62
- readonly state: 'enabled';
63
- readonly allowedModels: readonly PolicyRoute[];
64
- } | {
65
- readonly state: 'unprovable';
66
- };
67
- /** Outcome of the structural session-event read. */
68
- export type SessionPolicyEventRead = {
69
- readonly ok: true;
70
- readonly allowedModels: PolicyRoute[];
71
- } | {
72
- readonly ok: false;
73
- readonly present: true;
74
- } | {
75
- readonly ok: false;
76
- readonly present: false;
77
- };
78
- /** Settings snapshot consumed by the reader — plain structural data, no host objects. */
79
- export type PolicySettings = {
80
- readonly enabled: boolean;
81
- readonly allowedModels: readonly PolicyRoute[];
82
- };
83
- /** Assignability guard: fails typecheck if the peer's settings shape drifts from the reader's snapshot contract. */
84
- type AssertSettingsShape<T extends PolicySettings> = T;
85
- /**
86
- * The host settings value's shape, re-exported from the published peer
87
- * subpath (type-only). Wiring passes the settings form's namespace value in as
88
- * plain data; if upstream drifts, this alias fails typecheck instead of
89
- * drifting silently at runtime (plan ADR).
90
- */
91
- export type PeerSettingsSnapshot = AssertSettingsShape<SubagentModelSelectionSettings>;
92
- /**
93
- * Narrow a host settings value into the reader's snapshot contract.
94
- * Reads STRUCTURALLY like {@link readSessionPolicyEvent} — rc.1 publishes no
95
- * settings value reader — so a malformed value is reported, never raised; the
96
- * wiring decides its failure face (warn + fail-closed).
97
- *
98
- * @param value - untrusted settings-form value for the policy namespace.
99
- * @returns a detached snapshot, or undefined when the value is not one.
100
- */
101
- export declare function policySettingsFrom(value: unknown): PolicySettings | undefined;
102
- /**
103
- * Structurally read the session's `subagent/model-selection-policy` event.
104
- * Never throws: a present-but-malformed payload is reported, not raised.
105
- *
106
- * Recency (qc fix wave S-asym — deliberate asymmetry, both sides mirror
107
- * upstream): this read takes the FIRST occurrence, exactly like the upstream
108
- * reader's `.find` — the host writes the policy event at most once per
109
- * session (its ensure-guard checks the reader before appending), so a second
110
- * occurrence would already be off-contract. The re-emittable
111
- * `model/selection` log is the opposite: `authorized-route.ts`
112
- * `lastModelSelection` scans backwards because upstream's selection
113
- * projection folds to the newest entry.
114
- *
115
- * @param events - plain event descriptors (`type` + optional `data`) captured
116
- * from the session by the wiring layer.
117
- * @returns the detached route list when the event is present and valid;
118
- * otherwise a presence flag distinguishing "no policy event" (pure
119
- * inheritance path) from "policy event unreadable" (fail-closed).
120
- */
121
- export declare function readSessionPolicyEvent(events: readonly {
122
- type: string;
123
- data?: unknown;
124
- }[]): SessionPolicyEventRead;
125
- /**
126
- * Resolve the effective policy from the session-event read and the settings
127
- * snapshot. The session event wins over settings (ADR read order).
128
- * Fail-closed: a present-but-malformed event, or enabled settings with a
129
- * malformed route list, yields `'unprovable'`. Never throws.
130
- *
131
- * @param event - result of {@link readSessionPolicyEvent}.
132
- * @param settings - host settings value narrowed by
133
- * {@link policySettingsFrom}, or undefined when the host composes no such
134
- * settings entry.
135
- */
136
- export declare function effectivePolicy(event: SessionPolicyEventRead, settings: PolicySettings | undefined): EffectivePolicy;
137
- export {};