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.
- package/README.md +26 -2
- package/README.zh-CN.md +26 -2
- package/dist/authorized-route.d.ts +68 -0
- package/dist/client/ConversationFallbackSwitch.d.ts +18 -14
- package/dist/client/fallbacks-store.d.ts +120 -18
- package/dist/client/index.d.ts +9 -8
- package/dist/client/index.js +176 -41
- package/dist/client/locales.d.ts +14 -0
- package/dist/commands.d.ts +4 -0
- package/dist/config.d.ts +14 -0
- package/dist/cooldown.d.ts +9 -0
- package/dist/gateway.d.ts +79 -3
- package/dist/index.d.ts +25 -5
- package/dist/index.js +470 -50
- package/dist/override.d.ts +39 -0
- package/dist/recovery.d.ts +92 -0
- package/dist/route-allowlist.d.ts +43 -0
- package/dist/state.d.ts +67 -2
- package/dist/subagent-policy.d.ts +123 -0
- package/dist/virtual-adapter.d.ts +49 -0
- package/package.json +25 -21
- package/scripts/build-client.ts +10 -6
|
@@ -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
|
-
/**
|
|
111
|
-
|
|
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.
|
|
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.
|
|
64
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
65
|
-
"@deepseek-ai/dsh-api-gateway": "^0.1.
|
|
66
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.
|
|
67
|
-
"@deepseek-ai/dsh-
|
|
68
|
-
"@deepseek-ai/dsh-client-
|
|
69
|
-
"@deepseek-ai/dsh-client-
|
|
70
|
-
"@deepseek-ai/dsh-client-
|
|
71
|
-
"@deepseek-ai/dsh-client-ui-
|
|
72
|
-
"@deepseek-ai/dsh-client-ui-
|
|
73
|
-
"@deepseek-ai/dsh-client-ui-
|
|
74
|
-
"@deepseek-ai/dsh-client-ui-
|
|
75
|
-
"@deepseek-ai/dsh-
|
|
76
|
-
"@deepseek-ai/dsh-
|
|
77
|
-
"@deepseek-ai/dsh-
|
|
78
|
-
"@deepseek-ai/dsh-
|
|
79
|
-
"@deepseek-ai/dsh-
|
|
80
|
-
"@deepseek-ai/dsh-
|
|
81
|
-
"@deepseek-ai/
|
|
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": {
|
package/scripts/build-client.ts
CHANGED
|
@@ -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
|
|
6
|
-
* `@deepseek-ai/dsh-client-
|
|
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 (
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
/**
|