dsh-llm-fallbacks 0.3.4 → 0.4.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * RouteChanged effort rule for config overrides (plan dsh-012-subagent-routing
3
+ * T4; spec D3).
4
+ *
5
+ * Pure, host-free module: plain seed config plus a target route in, the
6
+ * overridden config out. The effort handling mirrors the upstream
7
+ * `resolveChildAgentOptions` rule (deepseek-harness
8
+ * `subagent/src/child-agent.ts:117`): a changed provider+model route drops the
9
+ * seed's route-owned `reasoningEffort` so the selected model resolves its own
10
+ * default, unless an effort is explicitly named for the override; an
11
+ * unchanged route keeps the seed effort. Policy-independent by design — the
12
+ * same rule applies on every override path whether the subagent routing
13
+ * policy is on or off (spec D3). Never throws.
14
+ *
15
+ * @module dsh-llm-fallbacks/override
16
+ */
17
+ import type { LlmCallConfig } from '@deepseek-ai/dsh-llm';
18
+ /** Reasoning-effort value carried by `LlmCallConfig.reasoningEffort`. */
19
+ export type LlmReasoningEffort = NonNullable<LlmCallConfig['reasoningEffort']>;
20
+ /**
21
+ * Override a request config with a target route under the upstream
22
+ * routeChanged effort rule (spec D3):
23
+ *
24
+ * - An explicitly named effort always survives — it wins over the seed effort
25
+ * on either route (upstream semantics: the request overrides the parent).
26
+ * - Same provider+model route → the seed `reasoningEffort` is preserved.
27
+ * - Route changed without an explicit effort → the seed effort is dropped
28
+ * (never carry a stale effort into a different provider/model route).
29
+ *
30
+ * @param seed - the caller's current request config.
31
+ * @param to - the effective target route.
32
+ * @param explicitEffort - an effort explicitly named for this override.
33
+ * @returns a fresh config routed to `to` with the effort rule applied; never
34
+ * mutates `seed`.
35
+ */
36
+ export declare function overrideConfigWithRouteRule(seed: LlmCallConfig, to: {
37
+ provider: string;
38
+ model: string;
39
+ }, explicitEffort?: LlmReasoningEffort): LlmCallConfig;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Half-open recovery state machine (plan fallbacks-half-open-recovery Task 2).
3
+ *
4
+ * `RecoveryStore` tracks per-route consecutive-failure counters and half-open
5
+ * episode flags, keyed `${provider}/${model}` via `selectorKey`. It is held
6
+ * per-agent on `AgentFallbackState` and is session-scoped in-memory (same
7
+ * lifetime as the cooldown store; a restart resets every route to a flat
8
+ * first cooldown).
9
+ *
10
+ * The store is pure logic — zero imports, the cooldown.ts purity precedent —
11
+ * so it is unit-testable without a host (recovery.spec.ts imports this module
12
+ * directly).
13
+ *
14
+ * State machine (plan P2):
15
+ * 1. `recordFailure` — a suppression write: increments the consecutive-failure
16
+ * counter and clears any half-open episode (rule 1).
17
+ * 2. `escalatedCooldownMs` — bounded geometric escalation of the suppression
18
+ * duration (rule 2).
19
+ * 3. `markHalfOpen` — an expired cooldown leaves the route half-open for one
20
+ * logged probe (rule 3).
21
+ * 4. `tryMarkProbeLogged` — the once-per-episode admission marker, not a gate
22
+ * (rule 4).
23
+ * 5. `isHalfOpen` — probe-failure gate for the no-surviving-target path
24
+ * (rule 5b).
25
+ * 6. `close` — an observed completion closes the circuit, only from half-open
26
+ * (rule 6).
27
+ *
28
+ * @module dsh-llm-fallbacks/recovery
29
+ */
30
+ /** Escalation multiplier applied per consecutive failure (rule 2). */
31
+ export declare const ESCALATION_MULTIPLIER = 2;
32
+ /** Escalation ceiling: 1 hour (rule 2). */
33
+ export declare const ESCALATION_CAP_MS = 3600000;
34
+ /**
35
+ * Bounded geometric escalation of the suppression duration (rule 2):
36
+ * `max(ms, min(ESCALATION_CAP_MS, ms * ESCALATION_MULTIPLIER ** (n - 1)))`
37
+ * for `n >= 1`. `n = 1` is flat `ms`; the `max(ms, ...)` floor keeps
38
+ * escalation from ever shortening below the configured flat cooldown when
39
+ * `ms >= ESCALATION_CAP_MS`; `ms = 0` degenerates to today's flat-0
40
+ * semantics.
41
+ */
42
+ export declare function escalatedCooldownMs(ms: number, n: number): number;
43
+ /**
44
+ * Per-route half-open recovery state (plan P2). Pure logic — no host
45
+ * dependencies, no `@deepseek-ai/*` imports.
46
+ */
47
+ export declare class RecoveryStore {
48
+ private entries;
49
+ /**
50
+ * Record a suppression write for `key` (rule 1): increments the
51
+ * consecutive-failure counter and clears any in-progress half-open episode
52
+ * (a failure while half-open re-suppresses escalated — rule 5). Returns the
53
+ * new counter value `n`; the caller computes the escalated duration with
54
+ * {@link escalatedCooldownMs}.
55
+ */
56
+ recordFailure(key: string): number;
57
+ /**
58
+ * Open a half-open episode for `key` (rule 3): the lapsed cooldown leaves
59
+ * the route half-open for one logged probe. Resets the per-episode probe
60
+ * marker; the consecutive-failure counter survives the episode so a probe
61
+ * failure escalates to `n + 1` (rule 5). Creates the entry on first sight
62
+ * (mid-session mode flips can surface a cooldown entry with no recovery
63
+ * entry — rule 8).
64
+ */
65
+ markHalfOpen(key: string, until: number): void;
66
+ /**
67
+ * Mark the episode's one logged admission (rule 4). Returns `true` only for
68
+ * the first admission of a half-open episode; later admissions while the
69
+ * episode is unresolved return `false` and route normally (the marker is
70
+ * not a gate — there is no admission limit).
71
+ */
72
+ tryMarkProbeLogged(key: string): boolean;
73
+ /** True while `key` has an in-progress half-open episode (rule 5b gate). */
74
+ isHalfOpen(key: string): boolean;
75
+ /**
76
+ * Close the circuit for `key` (rule 6): acts only when the entry exists and
77
+ * is half-open — the entry is deleted (counter reset by deletion, preference
78
+ * fully restored). A completion observed while the route is actively
79
+ * suppressed, or with no entry, is a no-op: a stale in-flight success must
80
+ * not cancel a fresher escalated re-suppression.
81
+ */
82
+ close(key: string): void;
83
+ /**
84
+ * The in-progress half-open episodes, in insertion order (display path —
85
+ * plan P4). `untilEpochMs` is the lapsed expiry epoch that drives the
86
+ * `/fallbacks` marker row.
87
+ */
88
+ halfOpenEntries(): Array<{
89
+ key: string;
90
+ untilEpochMs: number;
91
+ }>;
92
+ }
@@ -0,0 +1,43 @@
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;
package/dist/state.d.ts CHANGED
@@ -29,6 +29,8 @@
29
29
  */
30
30
  import type { FallbackSwitchReason } from './events.ts';
31
31
  import { CooldownStore, StepFailureSet } from './cooldown.ts';
32
+ import { RecoveryStore } from './recovery.ts';
33
+ import type { RecoveryPolicy } from './config.ts';
32
34
  /** Decision awaiting application at the next `agent/request` boundary (spec §5.1). */
33
35
  export interface PendingSwitch {
34
36
  from: {
@@ -42,6 +44,46 @@ export interface PendingSwitch {
42
44
  role: string;
43
45
  reason: FallbackSwitchReason;
44
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
+ }
45
87
  /** The current (turn, step)'s failed-model set and switch budget (spec §5.1). */
46
88
  export interface StepFailures {
47
89
  turn: number;
@@ -64,6 +106,8 @@ export interface AgentFallbackState {
64
106
  stepFailures: StepFailures;
65
107
  /** `provider/model → expiry epoch ms`; lazily expired on read. */
66
108
  cooldown: CooldownStore;
109
+ /** Half-open recovery bookkeeping (plan fallbacks-half-open-recovery P2). */
110
+ recovery: RecoveryStore;
67
111
  }
68
112
  /**
69
113
  * The `Map<agent.id, AgentFallbackState>` store plus the state-machine
@@ -107,6 +151,27 @@ export declare class FallbackStateStore {
107
151
  clearStepState(state: AgentFallbackState): void;
108
152
  /** Suppress `key` until `untilEpochMs` (`Infinity` for `revertPolicy: 'never'`). */
109
153
  suppress(state: AgentFallbackState, key: string, untilEpochMs: number): void;
110
- /** Lazy cooldown read: expired entries are dropped on read (spec §5.1). */
111
- isSuppressed(state: AgentFallbackState, key: string, now?: number): boolean;
154
+ /**
155
+ * Lazy cooldown read (spec §5.1; plan fallbacks-half-open-recovery P2
156
+ * rule 3). Under `'timer'` (the default) today's body runs verbatim:
157
+ * expired entries are dropped on read. Under `'half-open'` the raw `until`
158
+ * is read via `CooldownStore.peek` (no lazy drop): absent ⇒ false;
159
+ * `until > now` ⇒ true; `until <= now` ⇒ the entry is dropped via the
160
+ * existing `isSuppressed` and the route is marked half-open, returning
161
+ * false. `Infinity` (`revertPolicy: 'never'`) never satisfies `until <=
162
+ * now` ⇒ the never short-circuit is structural on the read side.
163
+ */
164
+ isSuppressed(state: AgentFallbackState, key: string, now?: number, recovery?: RecoveryPolicy): boolean;
165
+ /**
166
+ * Bulk half-open transition for the display path (plan P4): walks the
167
+ * cooldown keys applying rule 3 per key. A pure no-op under `'timer'` —
168
+ * internal state and output stay byte-identical in default mode.
169
+ */
170
+ syncRecovery(state: AgentFallbackState, now: number, mode: RecoveryPolicy): void;
171
+ /**
172
+ * Observed completion ⇒ close the circuit (plan P2 rule 6): acts only when
173
+ * the entry exists and is half-open; otherwise a no-op (a stale in-flight
174
+ * success must not cancel a fresher escalated re-suppression).
175
+ */
176
+ observeSuccess(state: AgentFallbackState, key: string): void;
112
177
  }
@@ -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.4",
8
+ "version": "0.4.0-alpha.1",
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-alpha.2",
64
+ "@deepseek-ai/dsh-api-gateway": "^0.1.2-alpha.2",
65
+ "@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.2",
66
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.2-alpha.2",
67
+ "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.2",
68
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
69
+ "@deepseek-ai/dsh-client-store": "^0.1.2-alpha.2",
70
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.2-alpha.2",
71
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2",
72
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
73
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.2",
74
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2",
75
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.2-alpha.2",
76
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
77
+ "@deepseek-ai/dsh-commands": "^0.1.2-alpha.2",
78
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
79
+ "@deepseek-ai/dsh-llm-retry": "^0.1.2-alpha.2",
80
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
81
+ "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
82
+ "@deepseek-ai/dsh-tool-subagent": "^0.1.2-alpha.2",
83
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.2",
84
+ "@deepseek-ai/dsh-typert-registry": "^0.1.2-alpha.2",
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
  /**