dsh-llm-fallbacks 0.3.5 → 0.4.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.
package/dist/state.d.ts CHANGED
@@ -44,6 +44,46 @@ export interface PendingSwitch {
44
44
  role: string;
45
45
  reason: FallbackSwitchReason;
46
46
  }
47
+ /**
48
+ * One plugin-originated subagent switch the host allowlist blocked (plan
49
+ * dsh-012-subagent-routing T3; spec D1). The intersection of the resolved
50
+ * candidates with the effective allowlist was empty while the 0.3.5 walk
51
+ * would have switched — the attempt is recorded in-memory only (issue #52:
52
+ * no durable session write) for the Subagents card warning (spec D4/T5).
53
+ */
54
+ export interface BlockedSwitchAttempt {
55
+ /** Epoch ms when the switch was blocked. */
56
+ at: number;
57
+ /** The route the 0.3.5 walk would have switched to (exact provider+model). */
58
+ route: {
59
+ provider: string;
60
+ model: string;
61
+ };
62
+ /** The failure reason that triggered the would-be switch. */
63
+ reason: FallbackSwitchReason;
64
+ }
65
+ /**
66
+ * How the effective subagent chain head was chosen (plan dsh-012-subagent-routing
67
+ * T5 / T2 M3). Lives on a per-agent map (mirrors {@link BlockedSwitchAttempt}),
68
+ * not on {@link AgentFallbackState} — recording a head is not a switch intent
69
+ * (F-004: the fallback store grows only inside `commit()`).
70
+ */
71
+ export type ChainHeadSource = 'authorized' | 'injected';
72
+ /**
73
+ * The effective chain head for one subagent, plus the source label the
74
+ * Subagents card renders (spec D4). In-memory only (issue #52).
75
+ */
76
+ export interface EffectiveChainHead {
77
+ /** Epoch ms when this head was recorded (recency for the card snapshot). */
78
+ at: number;
79
+ /** Exact provider+model of the effective chain head. */
80
+ route: {
81
+ provider: string;
82
+ model: string;
83
+ };
84
+ /** `authorized` = host spawn preserved; `injected` = plugin role-inject. */
85
+ source: ChainHeadSource;
86
+ }
47
87
  /** The current (turn, step)'s failed-model set and switch budget (spec §5.1). */
48
88
  export interface StepFailures {
49
89
  turn: number;
@@ -0,0 +1,123 @@
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 settings service snapshot (`SubagentModelSelectionConfig`
13
+ * `.current()`), typed via a type-only peer import that is erased at emit.
14
+ *
15
+ * The session event wins over settings (ADR read order). Anything present but
16
+ * unreadable yields `'unprovable'` (fail-closed): callers must not emit a
17
+ * plugin-originated route they cannot prove is allowed. This module never
18
+ * throws.
19
+ *
20
+ * @module dsh-llm-fallbacks/subagent-policy
21
+ */
22
+ import type { SubagentModelSelectionSettings } from '@deepseek-ai/dsh-tool-subagent/model-selection-settings';
23
+ declare module '@deepseek-ai/dsh-session/types' {
24
+ interface SessionEventMap {
25
+ /**
26
+ * Host-recorded route policy for a model-selectable subagent session.
27
+ * The payload shape mirrors upstream exactly, so a future published
28
+ * upstream augmentation merges identically. Read structurally by this
29
+ * plugin; never written here.
30
+ */
31
+ 'subagent/model-selection-policy': {
32
+ /** Exact routes this session may select explicitly for a child. */
33
+ allowedModels: {
34
+ provider: string;
35
+ model: string;
36
+ }[];
37
+ };
38
+ }
39
+ }
40
+ /** One exact child LLM route (structural twin of upstream `AllowedModelRoute`). */
41
+ export type PolicyRoute = {
42
+ readonly provider: string;
43
+ readonly model: string;
44
+ };
45
+ /**
46
+ * Effective subagent routing policy for one session.
47
+ *
48
+ * - `disabled`: host policy off or absent — selection matches pre-policy
49
+ * (0.3.5) behavior.
50
+ * - `enabled`: an allowlist is proven; plugin-originated routes must stay
51
+ * inside it.
52
+ * - `unprovable`: the policy is on but unreadable (malformed event payload,
53
+ * or enabled settings with a malformed route list) — fail-closed; no
54
+ * plugin-originated route may be emitted.
55
+ */
56
+ export type EffectivePolicy = {
57
+ readonly state: 'disabled';
58
+ } | {
59
+ readonly state: 'enabled';
60
+ readonly allowedModels: readonly PolicyRoute[];
61
+ } | {
62
+ readonly state: 'unprovable';
63
+ };
64
+ /** Outcome of the structural session-event read. */
65
+ export type SessionPolicyEventRead = {
66
+ readonly ok: true;
67
+ readonly allowedModels: PolicyRoute[];
68
+ } | {
69
+ readonly ok: false;
70
+ readonly present: true;
71
+ } | {
72
+ readonly ok: false;
73
+ readonly present: false;
74
+ };
75
+ /** Settings snapshot consumed by the reader — plain structural data, no host objects. */
76
+ export type PolicySettings = {
77
+ readonly enabled: boolean;
78
+ readonly allowedModels: readonly PolicyRoute[];
79
+ };
80
+ /** Assignability guard: fails typecheck if the peer's settings shape drifts from the reader's snapshot contract. */
81
+ type AssertSettingsShape<T extends PolicySettings> = T;
82
+ /**
83
+ * The host settings service snapshot type, re-exported from the published
84
+ * peer subpath (type-only). Wiring reads the service's `current()` and passes
85
+ * the result in as plain data; if upstream drifts, this alias fails typecheck
86
+ * instead of drifting silently at runtime (plan ADR).
87
+ */
88
+ export type PeerSettingsSnapshot = AssertSettingsShape<SubagentModelSelectionSettings>;
89
+ /**
90
+ * Structurally read the session's `subagent/model-selection-policy` event.
91
+ * Never throws: a present-but-malformed payload is reported, not raised.
92
+ *
93
+ * Recency (qc fix wave S-asym — deliberate asymmetry, both sides mirror
94
+ * upstream): this read takes the FIRST occurrence, exactly like the upstream
95
+ * reader's `.find` — the host writes the policy event at most once per
96
+ * session (its ensure-guard checks the reader before appending), so a second
97
+ * occurrence would already be off-contract. The re-emittable
98
+ * `model/selection` log is the opposite: `authorized-route.ts`
99
+ * `lastModelSelection` scans backwards because upstream's selection
100
+ * projection folds to the newest entry.
101
+ *
102
+ * @param events - plain event descriptors (`type` + optional `data`) captured
103
+ * from the session by the wiring layer.
104
+ * @returns the detached route list when the event is present and valid;
105
+ * otherwise a presence flag distinguishing "no policy event" (pure
106
+ * inheritance path) from "policy event unreadable" (fail-closed).
107
+ */
108
+ export declare function readSessionPolicyEvent(events: readonly {
109
+ type: string;
110
+ data?: unknown;
111
+ }[]): SessionPolicyEventRead;
112
+ /**
113
+ * Resolve the effective policy from the session-event read and the settings
114
+ * snapshot. The session event wins over settings (ADR read order).
115
+ * Fail-closed: a present-but-malformed event, or enabled settings with a
116
+ * malformed route list, yields `'unprovable'`. Never throws.
117
+ *
118
+ * @param event - result of {@link readSessionPolicyEvent}.
119
+ * @param settings - settings service snapshot, or undefined when the host
120
+ * does not compose the service.
121
+ */
122
+ export declare function effectivePolicy(event: SessionPolicyEventRead, settings: PolicySettings | undefined): EffectivePolicy;
123
+ export {};
@@ -31,6 +31,7 @@
31
31
  * @module dsh-llm-fallbacks/virtual-adapter
32
32
  */
33
33
  import type { Context } from '@deepseek-ai/cordis';
34
+ import { LlmAdapter, type GenerateOptions, type LlmImageRequestPricing, type LlmModelInfo, type LlmResolvedModelInfo, type LlmRuntime, type StreamChunk } from '@deepseek-ai/dsh-llm';
34
35
  import type { FallbacksConfig } from './config.ts';
35
36
  /** Provider route of the virtual adapter (exact string, spec lock). */
36
37
  export declare const FALLBACKS_PROVIDER = "FallbacksChain";
@@ -78,6 +79,54 @@ export declare function firstDispatchableExactHead(chain: readonly string[]): Ef
78
79
  * @param modelDisplayName - catalog/resolved name; falls back to the id.
79
80
  */
80
81
  export declare function pickerDisplayName(config: FallbacksConfig, now?: Date, modelDisplayName?: string): string;
82
+ /**
83
+ * The virtual adapter (P1/P3). `stream()` is a thin head-delegate, never a
84
+ * second routing engine: no chain walk, cooldown, caps, revert bookkeeping,
85
+ * or state writes live here — those stay in the `agent/request` /
86
+ * `agent/request-error` listeners. Exported so the defensive guard paths
87
+ * the registration lifecycle cannot reach (a registered route whose `llm`
88
+ * vanished mid-teardown) stay unit-testable by direct construction.
89
+ */
90
+ export declare class FallbacksChainAdapter extends LlmAdapter {
91
+ private readonly readConfig;
92
+ private readonly getLlm;
93
+ constructor(readConfig: () => FallbacksConfig, getLlm: () => LlmRuntime | undefined);
94
+ /** Minimal honest descriptor (P3) — the provider route IS the display name. */
95
+ providerInfo(provider: string): {
96
+ id: string;
97
+ name: string;
98
+ };
99
+ /** Advisory catalog: one virtual row; `name` is the live picker label. */
100
+ listModels(provider: string): Promise<readonly LlmModelInfo[]>;
101
+ /**
102
+ * Proxy the current effective head's model metadata when resolvable (P3):
103
+ * modalities/context-window/reasoning follow the head — the truthful
104
+ * answer for the CURRENT head. Permissive default otherwise; never throws.
105
+ */
106
+ resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
107
+ /**
108
+ * Route-accurate image pricing (0.1.2 adoption): the virtual row has no
109
+ * pricing of its own — the virtual `provider`/`model` arguments are
110
+ * intentionally ignored. Resolves the SAME effective head `stream()`
111
+ * dispatches and delegates through the runtime lookup, so the token
112
+ * meter gets exactly the answer the concrete route's adapter would give
113
+ * (including `undefined`). Never throws: an unresolvable head, a
114
+ * vanished `llm`, or a throwing delegate degrades to `undefined` — the
115
+ * token meter's neutral estimate.
116
+ */
117
+ imageRequestPricing(_provider: string, _model: string): LlmImageRequestPricing | undefined;
118
+ /**
119
+ * Thin head-delegate (P1): resolve the effective chain head and dispatch
120
+ * that REAL pair through the host LLM runtime. Throws an explicit
121
+ * `LlmError` only when there is no real pair to delegate to: an empty
122
+ * effective chain, an undispatchable head (wildcard / malformed /
123
+ * self-route — the recursion guard), or a vanished `llm` runtime. The
124
+ * runtime normalizes the throw into a terminal error finish chunk, so the
125
+ * documented listener-order degradation stays graceful instead of a hard
126
+ * outage.
127
+ */
128
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
129
+ }
81
130
  /**
82
131
  * Install the virtual adapter registration lifecycle (P2).
83
132
  *
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "dsh",
6
6
  "dsh-plugin"
7
7
  ],
8
- "version": "0.3.5",
8
+ "version": "0.4.0",
9
9
  "type": "module",
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
@@ -32,7 +32,6 @@
32
32
  },
33
33
  "client": {
34
34
  "inject": [
35
- "@deepseek-ai/dsh-client-runtime",
36
35
  "@deepseek-ai/dsh-client-ui-settings-plugins",
37
36
  "@deepseek-ai/dsh-client-ui-settings",
38
37
  "@deepseek-ai/dsh-client-ui-conversation",
@@ -60,25 +59,30 @@
60
59
  "use-sync-external-store": "^1.6.0"
61
60
  },
62
61
  "peerDependencies": {
63
- "@deepseek-ai/cordis": "^4.0.1",
64
- "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
65
- "@deepseek-ai/dsh-api-gateway": "^0.1.1-rc.2",
66
- "@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.2",
67
- "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
68
- "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
69
- "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
70
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
71
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
72
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.2",
73
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.1-rc.2",
74
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
75
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
76
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
77
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
78
- "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
79
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2",
80
- "@deepseek-ai/dsh-typert-registry": "^0.1.1-rc.2",
81
- "@deepseek-ai/schemastery": "^3.18.1",
62
+ "@deepseek-ai/cordis": "^4.0.2",
63
+ "@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
64
+ "@deepseek-ai/dsh-api-gateway": "^0.1.2-rc.1",
65
+ "@deepseek-ai/dsh-api-remotes": "^0.1.2-rc.1",
66
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.2-rc.1",
67
+ "@deepseek-ai/dsh-client-connection": "^0.1.2-rc.1",
68
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
69
+ "@deepseek-ai/dsh-client-store": "^0.1.2-rc.1",
70
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.2-rc.1",
71
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1",
72
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1",
73
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-rc.1",
74
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-rc.1",
75
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.2-rc.1",
76
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
77
+ "@deepseek-ai/dsh-commands": "^0.1.2-rc.1",
78
+ "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
79
+ "@deepseek-ai/dsh-llm-retry": "^0.1.2-rc.1",
80
+ "@deepseek-ai/dsh-session": "^0.1.2-rc.1",
81
+ "@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
82
+ "@deepseek-ai/dsh-tool-subagent": "^0.1.2-rc.1",
83
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-rc.1",
84
+ "@deepseek-ai/dsh-typert-registry": "^0.1.2-rc.1",
85
+ "@deepseek-ai/schemastery": "^3.18.2",
82
86
  "react": "^18.2.0"
83
87
  },
84
88
  "devDependencies": {
@@ -2,8 +2,9 @@
2
2
  * Client bundle build: emits the closure-factory CJS artifact the dsh web
3
3
  * loader consumes — `window.__ModuleLoader__.load({ id: 'dsh-llm-fallbacks',
4
4
  * factory: (require) => { … return module.exports; } })`. Externals resolve
5
- * through the loader module table (platform seed entries + the documented
6
- * `@deepseek-ai/dsh-client-runtime/client` exemption); everything else
5
+ * through the loader module table (the platform seed entries — in 0.1.2 the
6
+ * former runtime exemption is folded into the `@deepseek-ai/dsh-client-store`
7
+ * platform module, the client half's one VALUE import); everything else
7
8
  * inlines.
8
9
  *
9
10
  * Build tool: `tsdown` (rolldown) — the same tool the dsh host itself uses
@@ -47,9 +48,12 @@ const OUT_FILE = 'index.js'
47
48
 
48
49
  /**
49
50
  * Frozen loader module table (dsh mechanism-guide): the plan-documented
50
- * composition = PLATFORM_MODULES (7 entries, `web/src/platform.ts:8-12`)
51
- * + the PRELOADED_CLIENT_EXTERNALS (`@deepseek-ai/dsh-client-runtime/client`,
52
- * `web/src/platform.ts:15-17`). rc.8 dropped `dsh-client-web-react` (deleted
51
+ * composition = PLATFORM_MODULES (8 entries in 0.1.2, `web/src/platform.ts:8-15`
52
+ * — the old runtime `./client` exemption became the
53
+ * `@deepseek-ai/dsh-client-store` platform module, so the store VALUE import
54
+ * stays external and resolves from the host loader table) + the
55
+ * PRELOADED_CLIENT_EXTERNALS (empty in 0.1.2, `web/src/platform.ts:16-17`).
56
+ * rc.8 dropped `dsh-client-web-react` (deleted
53
57
  * package — the uSES bridge is vendored in `src/client/use-snapshot.ts`),
54
58
  * `dsh-client-ui-attachment` and `dsh-client-schema-form` from the platform
55
59
  * table. `@deepseek-ai/cordis` is the in-box cordis framework (dsh-advisor
@@ -63,9 +67,9 @@ export const CLIENT_EXTERNALS: readonly string[] = [
63
67
  'react-dom',
64
68
  'react-dom/client',
65
69
  '@deepseek-ai/cordis',
70
+ '@deepseek-ai/dsh-client-store',
66
71
  '@deepseek-ai/dsh-client-ui-slots',
67
72
  '@deepseek-ai/dsh-client-ui-primitives',
68
- '@deepseek-ai/dsh-client-runtime/client',
69
73
  ]
70
74
 
71
75
  /**