dsh-llm-fallbacks 0.5.2 → 0.6.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/README.md +5 -5
- package/README.zh-CN.md +5 -5
- package/dist/client/FallbacksCard.d.ts +10 -9
- package/dist/client/fallbacks-store.d.ts +1 -1
- package/dist/client/index.d.ts +7 -12
- package/dist/client/index.js +67 -60
- package/dist/client/locales.d.ts +0 -2
- package/dist/gateway.d.ts +49 -33
- package/dist/index.d.ts +18 -4
- package/dist/index.js +61 -32
- package/dist/role-notice.d.ts +48 -20
- package/dist/role-projection.d.ts +11 -10
- package/dist/schema.d.ts +20 -0
- package/dist/subagents-seam.d.ts +4 -4
- package/dist/time-slots.d.ts +13 -8
- package/package.json +25 -25
- package/scripts/build-client.ts +6 -5
package/dist/gateway.d.ts
CHANGED
|
@@ -25,20 +25,22 @@
|
|
|
25
25
|
*
|
|
26
26
|
* Data: `get` reads the `FallbacksSettingsBridge` source — the same live
|
|
27
27
|
* composed config the runtime reads (schema defaults → plugin-row base →
|
|
28
|
-
*
|
|
28
|
+
* profile patch). There is NO hard-gate resolver (unlike advisor's
|
|
29
29
|
* `resolveAdvisorConfig`): the fallbacks decision path runs at
|
|
30
30
|
* `agent/request` time in `src/index.ts`, so the gateway returns the raw
|
|
31
31
|
* composed config — `enabled` is a plain config field, not a gate output.
|
|
32
32
|
* `set` validates the patch against the `Config` schema first (unknown-key
|
|
33
33
|
* rejection unchanged — the settings service itself is non-strict and would
|
|
34
|
-
* merge the unknown key through), then writes the
|
|
35
|
-
* `ctx.settings.update` (
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* merge the unknown key through), then writes the plugin's profile-entry
|
|
35
|
+
* section in-process via `ctx.settings.update` (the `SettingsForms` form
|
|
36
|
+
* service; 0.1.7-rc.1 replaced the namespace registry — the write lands in
|
|
37
|
+
* the active profile's patch under {@link FALLBACKS_PROFILE_ENTRY} and the
|
|
38
|
+
* Loader commits it into the plugin's live config reference), and returns
|
|
39
|
+
* the new composed value. `reset` (fallbacks-specific third method —
|
|
40
|
+
* advisor has only get/set) clears the entry's patch section via
|
|
39
41
|
* `ctx.settings.replace(ns, {})`: `set` is merge-only and cannot express
|
|
40
42
|
* "reset to composition defaults" (sending default VALUES as a patch would
|
|
41
|
-
* pin stale defaults into the
|
|
43
|
+
* pin stale defaults into the patch). Every read response (get/set/reset)
|
|
42
44
|
* carries the additive `seeds: SeedsWireStatus[]` badge state (spec §9.4) and
|
|
43
45
|
* `revert-seed` exposes revert-to-current-seed-default for one id — both
|
|
44
46
|
* delegate to the per-apply `FallbacksSeedManager` passed into the
|
|
@@ -48,8 +50,8 @@
|
|
|
48
50
|
* stays the entry, `get` still works; `set`/`reset` fail with a clear
|
|
49
51
|
* error — KD-G5 fallback). The gateway captures the service through a
|
|
50
52
|
* conditional `ctx.inject(['settings'], ...)` child (the same activation
|
|
51
|
-
* pattern as
|
|
52
|
-
* resolvable from a fiber that declares it.
|
|
53
|
+
* pattern as the plugin apply's own settings child), because `ctx.settings`
|
|
54
|
+
* is only resolvable from a fiber that declares it.
|
|
53
55
|
*
|
|
54
56
|
* The returned config is normalized to the typertGateway JSON wire boundary:
|
|
55
57
|
* only schema-declared keys cross the wire, and absent values are OMITTED,
|
|
@@ -59,18 +61,31 @@
|
|
|
59
61
|
* @module dsh-llm-fallbacks/gateway
|
|
60
62
|
*/
|
|
61
63
|
import type { Context } from '@deepseek-ai/cordis';
|
|
62
|
-
import type
|
|
64
|
+
import { type SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
63
65
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
64
66
|
import type { TypertContribution } from '@deepseek-ai/dsh-typert-registry';
|
|
65
67
|
import type { FallbacksConfig } from './config';
|
|
66
68
|
import type { FallbacksSeedManager, SeedRevertOutcome, SeedsWireStatus } from './seeds';
|
|
67
69
|
/**
|
|
68
|
-
* The `fallbacks`
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
70
|
+
* The `fallbacks` service/typert namespace identity (`fallbacks`): the cordis
|
|
71
|
+
* service key, the typert namespace under `/api/fallbacks/*`, and the settings
|
|
72
|
+
* identity under the pre-0.1.6 dsh API. 0.1.7-rc.1 settings writes target the
|
|
73
|
+
* PROFILE ENTRY id instead (see {@link FALLBACKS_PROFILE_ENTRY}); the branded
|
|
74
|
+
* `SettingsNamespace` type the old service APIs constrained on survives as the
|
|
75
|
+
* type this constant carries.
|
|
72
76
|
*/
|
|
73
77
|
export declare const FALLBACKS_SETTINGS_NAMESPACE: SettingsNamespace;
|
|
78
|
+
/**
|
|
79
|
+
* The plugin's profile ENTRY id — the settings-write target on dsh 0.1.7-rc.1:
|
|
80
|
+
* `SettingsForms` keys every form and write by the Loader entry id
|
|
81
|
+
* (`entry.options.id`), and this plugin's bundle row ships `id: llm-fallbacks`
|
|
82
|
+
* (`bundle/cordis.patch.yml`), so `settings.update('llm-fallbacks', …)` edits
|
|
83
|
+
* exactly this plugin's section of the active profile patch. The section IS
|
|
84
|
+
* the Loader entry config now (schema defaults → bundle row base → profile
|
|
85
|
+
* patch), so a write is visible to the runtime through the same live config
|
|
86
|
+
* reference `apply()` reads — no separate registration step.
|
|
87
|
+
*/
|
|
88
|
+
export declare const FALLBACKS_PROFILE_ENTRY = "llm-fallbacks";
|
|
74
89
|
/** One exact child LLM route on the additive `subagentPolicy` wire field (T5). */
|
|
75
90
|
export type SubagentPolicyRoute = {
|
|
76
91
|
provider: string;
|
|
@@ -129,7 +144,7 @@ export type SubagentPolicySnapshotFn = () => {
|
|
|
129
144
|
/**
|
|
130
145
|
* The live configuration source for the gateway (guide §7 — the same bridge
|
|
131
146
|
* shape the runtime reads through). `source()` returns the live composed
|
|
132
|
-
* config (schema defaults → plugin-row base →
|
|
147
|
+
* config (schema defaults → plugin-row base → profile patch). The
|
|
133
148
|
* gateway reads it LIVE on every call, so no change notification is needed
|
|
134
149
|
* (the bridge stays minimal: source + the settings write channel; the dead
|
|
135
150
|
* `onChange` fan-out was removed in the QC fix wave — nothing subscribed).
|
|
@@ -196,8 +211,8 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
|
196
211
|
*/
|
|
197
212
|
constructor(ctx: Context, bridge: FallbacksSettingsBridge, seeds: FallbacksSeedManager, subagentPolicy?: SubagentPolicySnapshotFn);
|
|
198
213
|
/**
|
|
199
|
-
* Read the current composed config (schema defaults → entry base →
|
|
200
|
-
*
|
|
214
|
+
* Read the current composed config (schema defaults → entry base → profile
|
|
215
|
+
* patch). No hard-gate resolver (ADR-2): the raw composed config is
|
|
201
216
|
* the wire value — `enabled` is a plain field, not a gate output.
|
|
202
217
|
* @returns the wire-normalized composed config plus `legacyKeys` — legacy
|
|
203
218
|
* two-block-era fields (`chains` / `roles.default` / undeclared rule
|
|
@@ -209,13 +224,14 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
|
209
224
|
*/
|
|
210
225
|
get(): FallbacksReadResult;
|
|
211
226
|
/**
|
|
212
|
-
* Validate a config patch and write it to the
|
|
213
|
-
* the runtime
|
|
227
|
+
* Validate a config patch and write it to the plugin's profile-entry section
|
|
228
|
+
* (live — the runtime reads the same composed source through the Loader; no
|
|
229
|
+
* restart needed).
|
|
214
230
|
* @param patch - any subset of the config keys; unknown keys (top-level
|
|
215
231
|
* and nested under `roles`) are rejected before anything is written.
|
|
216
232
|
* @returns the NEW composed config plus `legacyKeys` detected on the
|
|
217
|
-
* POST-WRITE composed source (W-1/F-1): `set` is a
|
|
218
|
-
* legacy
|
|
233
|
+
* POST-WRITE composed source (W-1/F-1): `set` is a config MERGE, so a
|
|
234
|
+
* legacy patch section (`chains` / `roles.default`) survives a new-shape
|
|
219
235
|
* save — the response must keep reporting it, or the client banner
|
|
220
236
|
* would clear against server truth — plus the post-write `seeds` badge
|
|
221
237
|
* state (same W-1/F-1 rule as `legacyKeys`). Same shape as `get`.
|
|
@@ -224,13 +240,13 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
|
224
240
|
*/
|
|
225
241
|
set(patch: FallbacksConfigPatch): Promise<FallbacksReadResult>;
|
|
226
242
|
/**
|
|
227
|
-
* Clear the fallbacks
|
|
243
|
+
* Clear the fallbacks profile-entry section so the composition defaults
|
|
228
244
|
* reapply (`settings.replace(ns, {})` — the in-process removal path a
|
|
229
245
|
* merge-only `set` cannot express).
|
|
230
246
|
* @returns the new composed config plus `legacyKeys` on the post-write
|
|
231
|
-
* source — `replace` drops the
|
|
247
|
+
* source — `replace` drops the patch section, but legacy keys carried by
|
|
232
248
|
* the entry base survive and are correctly re-reported (W-1/F-1) — plus
|
|
233
|
-
* the post-write `seeds` badge state (clearing the
|
|
249
|
+
* the post-write `seeds` badge state (clearing the patch section also
|
|
234
250
|
* clears the materialized seed rows, so the honest response reports the
|
|
235
251
|
* emptied state; the registry survives and the next declare re-materializes).
|
|
236
252
|
* @throws when no settings service is composed (KD-G5: the write channel
|
|
@@ -255,7 +271,7 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
|
255
271
|
revertSeed(id: string): Promise<FallbacksRevertResult>;
|
|
256
272
|
/**
|
|
257
273
|
* Read the live composed config and normalize it to the typertGateway JSON
|
|
258
|
-
* wire boundary. Containment (guide §10): a malformed stored
|
|
274
|
+
* wire boundary. Containment (guide §10): a malformed stored patch section
|
|
259
275
|
* that the non-strict settings schema let through (e.g. an unknown key)
|
|
260
276
|
* must never fail the RPC — only schema-declared keys cross the wire, and
|
|
261
277
|
* absent values are omitted, never present-as-undefined (the result
|
|
@@ -271,7 +287,7 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
|
271
287
|
* The wire response of every read (get/set/reset — W-1/F-1): the
|
|
272
288
|
* normalized config plus `legacyKeys` detected on the live composed
|
|
273
289
|
* source plus the additive `seeds` badge state. set/reset must report the
|
|
274
|
-
* POST-WRITE source: the
|
|
290
|
+
* POST-WRITE source: the config merge retains legacy patch-section keys,
|
|
275
291
|
* so a save cannot clear them — the honest response keeps the migration
|
|
276
292
|
* banner until a get agrees; `seeds` follows the same rule (post-write
|
|
277
293
|
* badge state, W-1/F-1).
|
|
@@ -280,13 +296,13 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
|
280
296
|
/**
|
|
281
297
|
* The io seam the seed manager writes through (spec §9.1): `read` walks
|
|
282
298
|
* the same live bridge source the gateway reads; `writeRoles` persists a
|
|
283
|
-
* full `{ list, rules }` to the
|
|
284
|
-
* computed from a fresh composed read, so the write stays
|
|
285
|
-
*
|
|
286
|
-
* operator rules. The write channel fails with the same KD-G5
|
|
287
|
-
* set/reset when no settings service is composed. Built fresh
|
|
288
|
-
* the mutable settings capture is read at call time (the
|
|
289
|
-
* swaps it when the settings service appears/disappears).
|
|
299
|
+
* full `{ list, rules }` to the plugin's profile-entry section — both
|
|
300
|
+
* arrays always computed from a fresh composed read, so the write stays
|
|
301
|
+
* correct under the settings merge's array-replace semantics and never
|
|
302
|
+
* touches operator rules. The write channel fails with the same KD-G5
|
|
303
|
+
* message as set/reset when no settings service is composed. Built fresh
|
|
304
|
+
* per call so the mutable settings capture is read at call time (the
|
|
305
|
+
* inject child swaps it when the settings service appears/disappears).
|
|
290
306
|
*/
|
|
291
307
|
private seedsIo;
|
|
292
308
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* `llm-fallbacks` (see `bundle/cordis.patch.yml`), composed AFTER llm-retry.
|
|
7
7
|
*
|
|
8
8
|
* Wiring:
|
|
9
|
-
* - `fallbacks` settings
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - `fallbacks` settings via the Loader entry Config (0.1.7-rc.1: the
|
|
10
|
+
* volatile `Config` schema in `src/schema.ts` IS the settings section;
|
|
11
|
+
* `apply` reads the Loader's live config reference, and the
|
|
12
|
+
* `settings/document-updated` listener re-derives the runtime caches —
|
|
13
|
+
* the old `installSection` setSource/onChange contract, spec §4).
|
|
12
14
|
* - `agent/request-error` waterfall: `!enabled` / code ∉ `triggerCodes`
|
|
13
15
|
* (**always mode included**) → `next()`; otherwise resolve role + chain,
|
|
14
16
|
* and when a candidate survives the filter (current / cooldown /
|
|
@@ -138,4 +140,16 @@ export declare function chainHeads(ctx: Context): ReadonlyMap<string, EffectiveC
|
|
|
138
140
|
* Exported for direct unit testing of the counting + fast path.
|
|
139
141
|
*/
|
|
140
142
|
export declare function countRetryEvents(session: Session, turn: number, step: number, provider: string): number;
|
|
141
|
-
|
|
143
|
+
/**
|
|
144
|
+
* The live config reference the 0.1.7-rc.1 Loader hands `apply` for a
|
|
145
|
+
* volatile Config schema: an identity-stable `{ get }` view (cosmokit's
|
|
146
|
+
* `Volatile` protocol) whose snapshot the Loader re-commits on every form
|
|
147
|
+
* save, so reading `.get()` per call is the authoritative live composed
|
|
148
|
+
* config. Detected STRUCTURALLY on purpose — the protocol is duck-shaped and
|
|
149
|
+
* the dev-time schemastery resolved from the lockfile can predate it, in
|
|
150
|
+
* which case the plain composed object arrives exactly as before.
|
|
151
|
+
*/
|
|
152
|
+
interface LiveConfigRef {
|
|
153
|
+
get(): unknown;
|
|
154
|
+
}
|
|
155
|
+
export declare function apply(ctx: Context, config?: FallbacksConfig | LiveConfigRef): void;
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ function resolveWildcardEntry(failingModel, provider) {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
function isOfficialAllDayId(id) {
|
|
38
|
-
return id === "deepseek-official/deepseek-flash" || id === "deepseek-official/deepseek-pro";
|
|
38
|
+
return id === "deepseek-official/deepseek-flash" || id === "deepseek-official/deepseek-v4-pro";
|
|
39
39
|
}
|
|
40
40
|
const PRESETS = {
|
|
41
41
|
"liang-peak": {
|
|
@@ -362,7 +362,7 @@ function validateFallbacksConfig(config, logger) {
|
|
|
362
362
|
if ((role.chain ?? []).length === 0) logger.warn(`llm-fallbacks: role "${role.id}" has no model config — declare at least one chain entry, or use the built-in "inherit" rule target instead`);
|
|
363
363
|
if (role.fallback !== void 0 && role.fallback !== "inherit-root" && role.fallback !== "none") logger.warn(`llm-fallbacks: role "${role.id}" has invalid fallback "${String(role.fallback)}" — expected "inherit-root" or "none"`);
|
|
364
364
|
}
|
|
365
|
-
if (config.rootChain.length > 0 && !isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: rootChain must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-pro) — time-slot rows and the virtual picker row stay inert until the all-day chain tail conforms");
|
|
365
|
+
if (config.rootChain.length > 0 && !isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: rootChain must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro) — time-slot rows and the virtual picker row stay inert until the all-day chain tail conforms");
|
|
366
366
|
for (const entry of config.rootChain) try {
|
|
367
367
|
parseSelector(entry);
|
|
368
368
|
} catch (error) {
|
|
@@ -411,8 +411,6 @@ function detectLegacyKeys(source) {
|
|
|
411
411
|
function isRecordLike(value) {
|
|
412
412
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
413
413
|
}
|
|
414
|
-
//#endregion
|
|
415
|
-
//#region .build/host/schema.js
|
|
416
414
|
const Config = z.object({
|
|
417
415
|
enabled: z.boolean().default(false),
|
|
418
416
|
triggerCodes: z.array(z.string()).default([
|
|
@@ -459,7 +457,7 @@ const Config = z.object({
|
|
|
459
457
|
})).default([]),
|
|
460
458
|
tz: z.string().default("Asia/Shanghai"),
|
|
461
459
|
recovery: z.union([z.const("timer"), z.const("half-open")]).default("timer")
|
|
462
|
-
});
|
|
460
|
+
}).extra("volatile", true);
|
|
463
461
|
function buildAutomatchPrompt(roles, agent) {
|
|
464
462
|
const lines = [
|
|
465
463
|
"You are assigning a subagent to its best-fit role from a declared taxonomy.",
|
|
@@ -806,7 +804,8 @@ function effectivePolicy(event, settings) {
|
|
|
806
804
|
//#endregion
|
|
807
805
|
//#region .build/host/role-notice.js
|
|
808
806
|
const ROLE_NOTICE_PLUGIN = "dsh-llm-fallbacks";
|
|
809
|
-
const ROLE_NOTICE_SOURCE_KIND = "
|
|
807
|
+
const ROLE_NOTICE_SOURCE_KIND = "llm-fallbacks-role-notice";
|
|
808
|
+
const ROLE_NOTICE_LEGACY_SOURCE_KINDS = [`plugin:${ROLE_NOTICE_PLUGIN}`];
|
|
810
809
|
const ROLE_NOTICE_PERSONA_SKIPPED_SUFFIX = " (persona not applied)";
|
|
811
810
|
function markNoticeEmitted(emitted, childSessionId) {
|
|
812
811
|
if (!emitted.has(childSessionId) && emitted.size >= 256) {
|
|
@@ -823,7 +822,6 @@ function buildRoleNotice(role, personaNotApplied) {
|
|
|
823
822
|
}],
|
|
824
823
|
source: {
|
|
825
824
|
kind: ROLE_NOTICE_SOURCE_KIND,
|
|
826
|
-
plugin: ROLE_NOTICE_PLUGIN,
|
|
827
825
|
form: "notice",
|
|
828
826
|
summary: boundContextSummary(`role: ${role}`)
|
|
829
827
|
}
|
|
@@ -916,8 +914,9 @@ function roleFromEvent(event) {
|
|
|
916
914
|
if (typeof data !== "object" || data === null) return void 0;
|
|
917
915
|
const { content, source } = data;
|
|
918
916
|
if (typeof source !== "object" || source === null) return void 0;
|
|
919
|
-
const { kind
|
|
920
|
-
if (kind !== "
|
|
917
|
+
const { kind } = source;
|
|
918
|
+
if (typeof kind !== "string") return void 0;
|
|
919
|
+
if (kind !== "llm-fallbacks-role-notice" && !ROLE_NOTICE_LEGACY_SOURCE_KINDS.includes(kind)) return;
|
|
921
920
|
return roleFromContent(content);
|
|
922
921
|
}
|
|
923
922
|
const roleProjectionUnit = {
|
|
@@ -1514,7 +1513,7 @@ function overrideConfigWithRouteRule(seed, to, explicitEffort) {
|
|
|
1514
1513
|
}
|
|
1515
1514
|
//#endregion
|
|
1516
1515
|
//#region .build/host/gateway.js
|
|
1517
|
-
const
|
|
1516
|
+
const FALLBACKS_PROFILE_ENTRY = "llm-fallbacks";
|
|
1518
1517
|
const CONFIG_KEYS = {
|
|
1519
1518
|
enabled: true,
|
|
1520
1519
|
triggerCodes: true,
|
|
@@ -1571,13 +1570,13 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1571
1570
|
if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
1572
1571
|
const normalized = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== null));
|
|
1573
1572
|
if (Object.keys(normalized).length === 0) return this.readResult();
|
|
1574
|
-
await settings.update(
|
|
1573
|
+
await settings.update(FALLBACKS_PROFILE_ENTRY, normalized);
|
|
1575
1574
|
return this.readResult();
|
|
1576
1575
|
}
|
|
1577
1576
|
async reset() {
|
|
1578
1577
|
const settings = this.settings;
|
|
1579
1578
|
if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
1580
|
-
await settings.replace(
|
|
1579
|
+
await settings.replace(FALLBACKS_PROFILE_ENTRY, {});
|
|
1581
1580
|
return this.readResult();
|
|
1582
1581
|
}
|
|
1583
1582
|
async revertSeed(id) {
|
|
@@ -1614,7 +1613,7 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1614
1613
|
read: () => this.bridge.source(),
|
|
1615
1614
|
writeRoles: (roles) => {
|
|
1616
1615
|
if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
1617
|
-
return settings.update(
|
|
1616
|
+
return settings.update(FALLBACKS_PROFILE_ENTRY, { roles });
|
|
1618
1617
|
}
|
|
1619
1618
|
};
|
|
1620
1619
|
}
|
|
@@ -1679,7 +1678,7 @@ function validateConfigPatch(patch) {
|
|
|
1679
1678
|
}
|
|
1680
1679
|
if (key === "rootChain") {
|
|
1681
1680
|
const chain = patch[key];
|
|
1682
|
-
if (chain !== null && chain !== void 0 && (!Array.isArray(chain) || !isAllDayConforming(chain))) throw new Error("dsh-llm-fallbacks: rootChain must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-pro)");
|
|
1681
|
+
if (chain !== null && chain !== void 0 && (!Array.isArray(chain) || !isAllDayConforming(chain))) throw new Error("dsh-llm-fallbacks: rootChain must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro)");
|
|
1683
1682
|
}
|
|
1684
1683
|
}
|
|
1685
1684
|
Config(patch);
|
|
@@ -2599,10 +2598,10 @@ function buildFallbacksTuiSection() {
|
|
|
2599
2598
|
zh: "根降级链(JSON)",
|
|
2600
2599
|
en: "Root chain (JSON)"
|
|
2601
2600
|
},
|
|
2602
|
-
hint: "JSON array of model selectors; the last entry must be an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-pro). Pro is
|
|
2601
|
+
hint: "JSON array of model selectors; the last entry must be an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro). Pro is available in the catalog — the settings card no longer disables it.",
|
|
2603
2602
|
hintDescriptions: {
|
|
2604
|
-
zh: "模型选择器 JSON 数组;末项必须是官方模型(deepseek-official/deepseek-flash 或 deepseek-official/deepseek-pro)。Pro
|
|
2605
|
-
en: "JSON array of model selectors; the last entry must be an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-pro). Pro is
|
|
2603
|
+
zh: "模型选择器 JSON 数组;末项必须是官方模型(deepseek-official/deepseek-flash 或 deepseek-official/deepseek-v4-pro)。Pro 已在目录中提供——设置卡中不再禁用。",
|
|
2604
|
+
en: "JSON array of model selectors; the last entry must be an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro). Pro is available in the catalog — the settings card no longer disables it."
|
|
2606
2605
|
},
|
|
2607
2606
|
group: "chain",
|
|
2608
2607
|
kind: "text",
|
|
@@ -2784,7 +2783,7 @@ function restoreEnvelopeProvenance(messages) {
|
|
|
2784
2783
|
let changed = false;
|
|
2785
2784
|
const restored = messages.map((message) => {
|
|
2786
2785
|
const source = message.source;
|
|
2787
|
-
if (message.role !== "assistant" || source
|
|
2786
|
+
if (message.role !== "assistant" || source?.kind !== "model") return message;
|
|
2788
2787
|
if (source.provider !== "FallbacksChain" || source.model !== "Auto") return message;
|
|
2789
2788
|
const provenance = envelopeProvenance(source.replayState);
|
|
2790
2789
|
if (provenance === void 0) return message;
|
|
@@ -3060,16 +3059,34 @@ function authorizedRouteView(ctx, agent) {
|
|
|
3060
3059
|
} : void 0)
|
|
3061
3060
|
};
|
|
3062
3061
|
}
|
|
3062
|
+
function isLiveConfigRef(value) {
|
|
3063
|
+
return typeof value === "object" && value !== null && typeof value.get === "function";
|
|
3064
|
+
}
|
|
3065
|
+
function normalizeConfig(value) {
|
|
3066
|
+
const plain = isLiveConfigRef(value) ? value.get() : value;
|
|
3067
|
+
const resolved = Config(plain);
|
|
3068
|
+
return isLiveConfigRef(resolved) ? resolved.get() : resolved;
|
|
3069
|
+
}
|
|
3070
|
+
function mergeConfigLayer(under, over) {
|
|
3071
|
+
if (typeof over !== "object" || over === null || Array.isArray(over)) return under;
|
|
3072
|
+
const merged = { ...under };
|
|
3073
|
+
for (const [key, value] of Object.entries(over)) {
|
|
3074
|
+
if (value === void 0) continue;
|
|
3075
|
+
const current = merged[key];
|
|
3076
|
+
merged[key] = typeof current === "object" && current !== null && !Array.isArray(current) && typeof value === "object" && value !== null && !Array.isArray(value) ? mergeConfigLayer(current, value) : value;
|
|
3077
|
+
}
|
|
3078
|
+
return merged;
|
|
3079
|
+
}
|
|
3063
3080
|
function apply(ctx, config = defaultFallbacksConfig) {
|
|
3064
3081
|
const logger = ctx.logger("llm-fallbacks");
|
|
3065
|
-
const entry =
|
|
3082
|
+
const entry = normalizeConfig(config);
|
|
3066
3083
|
const seeds = new FallbacksSeedManager(logger);
|
|
3067
3084
|
const seedsSettingsUnavailable = "llm-fallbacks: seeds: settings service is unavailable — seed roles cannot be written";
|
|
3068
3085
|
let writeRoles = () => {
|
|
3069
3086
|
throw new Error(seedsSettingsUnavailable);
|
|
3070
3087
|
};
|
|
3071
3088
|
ctx.inject(["settings"], (sctx) => {
|
|
3072
|
-
writeRoles = (roles) => sctx.settings.update(
|
|
3089
|
+
writeRoles = (roles) => sctx.settings.update(FALLBACKS_PROFILE_ENTRY, { roles });
|
|
3073
3090
|
try {
|
|
3074
3091
|
sctx.provide("llm-fallbacks", {
|
|
3075
3092
|
name: "llm-fallbacks",
|
|
@@ -3099,7 +3116,12 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3099
3116
|
writeRoles: (roles) => writeRoles(roles)
|
|
3100
3117
|
};
|
|
3101
3118
|
let serviceOwned = ctx.get("llm-fallbacks") === void 0;
|
|
3102
|
-
let
|
|
3119
|
+
let settingsComposed;
|
|
3120
|
+
let source = () => {
|
|
3121
|
+
if (isLiveConfigRef(config)) return config.get();
|
|
3122
|
+
if (settingsComposed !== void 0) return settingsComposed;
|
|
3123
|
+
return entry;
|
|
3124
|
+
};
|
|
3103
3125
|
const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source());
|
|
3104
3126
|
validateFallbacksConfig(entry, logger);
|
|
3105
3127
|
const legacyKeys = detectLegacyKeys(source());
|
|
@@ -3107,18 +3129,25 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3107
3129
|
let roleIds = new Map(entry.roles.list.map((role) => [role.id.trim(), role.id]));
|
|
3108
3130
|
let hasChains = entry.rootChain.length > 0 || entry.roles.list.some((role) => (role.chain?.length ?? 0) > 0);
|
|
3109
3131
|
ctx.inject(["settings"], (sctx) => {
|
|
3110
|
-
sctx.settings
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3132
|
+
const settings = sctx.settings;
|
|
3133
|
+
const refreshComposed = () => {
|
|
3134
|
+
const descriptor = settings.describe().find((d) => d.ns === FALLBACKS_PROFILE_ENTRY);
|
|
3135
|
+
settingsComposed = descriptor === void 0 ? void 0 : normalizeConfig(mergeConfigLayer(entry, descriptor.value));
|
|
3136
|
+
};
|
|
3137
|
+
if (!isLiveConfigRef(config)) refreshComposed();
|
|
3138
|
+
const disposeDocumentUpdated = ctx.on("settings/document-updated", (ns) => {
|
|
3139
|
+
if (ns !== "llm-fallbacks") return;
|
|
3140
|
+
if (!isLiveConfigRef(config)) refreshComposed();
|
|
3141
|
+
const current = source();
|
|
3142
|
+
roleIds = new Map(current.roles.list.map((role) => [role.id.trim(), role.id]));
|
|
3143
|
+
hasChains = current.rootChain.length > 0 || current.roles.list.some((role) => (role.chain?.length ?? 0) > 0);
|
|
3144
|
+
reconcileFallbacksAdapter();
|
|
3145
|
+
slotWinners.clear();
|
|
3121
3146
|
});
|
|
3147
|
+
return () => {
|
|
3148
|
+
disposeDocumentUpdated();
|
|
3149
|
+
settingsComposed = void 0;
|
|
3150
|
+
};
|
|
3122
3151
|
});
|
|
3123
3152
|
const bridge = { source: () => source() };
|
|
3124
3153
|
const chainHeadMap = new Map();
|
package/dist/role-notice.d.ts
CHANGED
|
@@ -27,17 +27,24 @@
|
|
|
27
27
|
* decision has neither failure mode.
|
|
28
28
|
*
|
|
29
29
|
* Session-write contract (plan Global Constraints, hard): the row is a
|
|
30
|
-
* `user/message` built with `createUserMessage` and the
|
|
31
|
-
* `{ kind: '
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
30
|
+
* `user/message` built with `createUserMessage` and the producer-declared
|
|
31
|
+
* source `{ kind: 'llm-fallbacks-role-notice', form: 'notice', summary: … }`.
|
|
32
|
+
* 0.1.7-rc.1 removed the shared `plugin` kind: `MessageSourceMap` is
|
|
33
|
+
* producer-declared now ("each producer declares its own `kind` in its own
|
|
34
|
+
* module; there is no shared catch-all `plugin` kind" — dsh-llm message.d.ts),
|
|
35
|
+
* so this module merges its own kind and stamps `form: 'notice'` + the bound
|
|
36
|
+
* summary exactly like the host's own model-switch notice
|
|
37
|
+
* (`core/agent/src/model-selection.ts`). User-role messages carry any
|
|
38
|
+
* producer's kind, consumers fall through unknown kinds, and the V4 format
|
|
39
|
+
* edge retains every direct source kind verbatim — a bespoke kind writes and
|
|
40
|
+
* survives every frozen edge. NO custom session event type, NO `ignorable`
|
|
41
|
+
* marker — a mount-only plugin cannot publish either (`.mstar/projects/
|
|
42
|
+
* _default/references/subagent-role-badge-visible/explore-role-seam-report.md`
|
|
43
|
+
* §Q3). The guard test in `tests/role-notice.spec.ts` pins the exact literal.
|
|
44
|
+
* Pre-upgrade durable rows (`kind: 'plugin', plugin: 'dsh-llm-fallbacks'`)
|
|
45
|
+
* migrate to `kind: 'plugin:dsh-llm-fallbacks'` at the V3→V4 edge; the role
|
|
46
|
+
* projection accepts that legacy form alongside this kind (see
|
|
47
|
+
* {@link ROLE_NOTICE_LEGACY_SOURCE_KINDS}).
|
|
41
48
|
*
|
|
42
49
|
* Once per child SESSION: the emitter consumes the seam's per-child record
|
|
43
50
|
* marker (`firstNoticePending`) AND a per-agent `emitted` set (mirroring the
|
|
@@ -64,21 +71,42 @@
|
|
|
64
71
|
* @module dsh-llm-fallbacks/role-notice
|
|
65
72
|
*/
|
|
66
73
|
import type { Context } from '@deepseek-ai/cordis';
|
|
67
|
-
import { type UserMessage } from '@deepseek-ai/dsh-llm';
|
|
74
|
+
import { type ContextFormed, type UserMessage } from '@deepseek-ai/dsh-llm';
|
|
68
75
|
import type { SubagentSeamRecord } from './subagents-seam.ts';
|
|
76
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
77
|
+
interface MessageSourceMap {
|
|
78
|
+
'llm-fallbacks-role-notice': {
|
|
79
|
+
kind: 'llm-fallbacks-role-notice';
|
|
80
|
+
} & ContextFormed;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
69
83
|
/**
|
|
70
|
-
* The
|
|
71
|
-
*
|
|
72
|
-
*
|
|
84
|
+
* The provenance label this plugin's notice rows carried under the pre-0.1.7
|
|
85
|
+
* `kind: 'plugin'` source API. The durable rows survive as data: the V3→V4
|
|
86
|
+
* session-format edge rewrites every unlisted V3 plugin source to
|
|
87
|
+
* `kind: 'plugin:<plugin>'`, so pre-upgrade child sessions carry
|
|
88
|
+
* `plugin:dsh-llm-fallbacks` rows — {@link ROLE_NOTICE_LEGACY_SOURCE_KINDS}
|
|
89
|
+
* and the role projection gate on this constant to keep badge continuity
|
|
90
|
+
* across the upgrade.
|
|
73
91
|
*/
|
|
74
92
|
export declare const ROLE_NOTICE_PLUGIN = "dsh-llm-fallbacks";
|
|
75
93
|
/**
|
|
76
|
-
* The message source KIND the notice rides.
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
94
|
+
* The message source KIND the notice rides. 0.1.7-rc.1 removed the shared
|
|
95
|
+
* `plugin` kind (`MessageSourceMap` is producer-declared now — each producer
|
|
96
|
+
* declares its own `kind` in its own module, the upstream `model-selection`
|
|
97
|
+
* precedent), so this module declares `llm-fallbacks-role-notice` above and
|
|
98
|
+
* the emitter stamps it with `form: 'notice'` + the bound summary exactly like
|
|
99
|
+
* the host's own model-switch notice. The value is exported so the guard test
|
|
100
|
+
* asserts on the exact literal the emitter uses.
|
|
101
|
+
*/
|
|
102
|
+
export declare const ROLE_NOTICE_SOURCE_KIND = "llm-fallbacks-role-notice";
|
|
103
|
+
/**
|
|
104
|
+
* The source kinds the role projection must ALSO accept besides
|
|
105
|
+
* {@link ROLE_NOTICE_SOURCE_KIND}: the V4-migrated form of the pre-0.1.7
|
|
106
|
+
* `kind: 'plugin', plugin: 'dsh-llm-fallbacks'` rows (see
|
|
107
|
+
* {@link ROLE_NOTICE_PLUGIN}).
|
|
80
108
|
*/
|
|
81
|
-
export declare const
|
|
109
|
+
export declare const ROLE_NOTICE_LEGACY_SOURCE_KINDS: readonly ["plugin:dsh-llm-fallbacks"];
|
|
82
110
|
/** Suffix appended when the role declares a persona that was NOT delivered. */
|
|
83
111
|
export declare const ROLE_NOTICE_PERSONA_SKIPPED_SUFFIX = " (persona not applied)";
|
|
84
112
|
/**
|
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Data source = Task 3's notice row, the plugin's single write primitive: the
|
|
7
7
|
* fold reads the child's committed `user/message` events, keeps the one whose
|
|
8
|
-
* provenance is ours (`source.kind
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
8
|
+
* provenance is ours (`source.kind` is the producer-declared
|
|
9
|
+
* ROLE_NOTICE_SOURCE_KIND, or the V3→V4-migrated form of a pre-0.1.7 row —
|
|
10
|
+
* `ROLE_NOTICE_LEGACY_SOURCE_KINDS`), and parses the role out of its
|
|
11
|
+
* **content text** (`[role: <id>]`). No second write path, no new event type,
|
|
12
|
+
* no plugin-owned store: the projection is a pure READ of the log, and the
|
|
13
|
+
* host's projection cache checkpoints every registered unit at session
|
|
14
|
+
* creation, `turn/end` and disposal — so a settled child (served as an
|
|
15
|
+
* unpublished observation whose `projections` ride the follow opening
|
|
16
|
+
* snapshot) and a post-restart view both work with nothing of ours on disk.
|
|
16
17
|
*
|
|
17
18
|
* NEVER `source.summary`: summaries are truncated at
|
|
18
19
|
* `CONTEXT_SUMMARY_MAX_CHARS = 120` (`@deepseek-ai/dsh-llm` `message.ts`,
|
|
@@ -32,9 +33,9 @@
|
|
|
32
33
|
* The runtime shape relied on — `register({ key, stateSchema, init, apply,
|
|
33
34
|
* wire: { viewSchema, view }, stateVersion })` → unregister disposer — is the
|
|
34
35
|
* documented `ProjectionDefinition` contract of
|
|
35
|
-
* `@deepseek-ai/dsh-session-projection@0.1.
|
|
36
|
+
* `@deepseek-ai/dsh-session-projection@0.1.7-rc.1`
|
|
36
37
|
* (`lib/types/index.d.ts:38-80,150-152`); only `.parse` is ever called on the
|
|
37
|
-
* schemas (`lib/index.js:255,305,
|
|
38
|
+
* schemas (`lib/index.js:255,305,422`).
|
|
38
39
|
*
|
|
39
40
|
* @module dsh-llm-fallbacks/role-projection
|
|
40
41
|
*/
|
package/dist/schema.d.ts
CHANGED
|
@@ -19,8 +19,28 @@
|
|
|
19
19
|
* `roles.default`) on the composed object at startup (warn + gateway
|
|
20
20
|
* `legacyKeys`, see `src/index.ts` apply()).
|
|
21
21
|
*
|
|
22
|
+
* 0.1.7-rc.1 adoption: the whole schema is marked `volatile` (dsh 0.1.6
|
|
23
|
+
* retired `installSection` — the Loader entry Config IS the settings
|
|
24
|
+
* section now, and `SettingsForms.update` only accepts volatile paths, so
|
|
25
|
+
* the marker is what keeps the gateway/card write path legal). One marker
|
|
26
|
+
* on the ROOT object makes every field live-editable and hands `apply` a
|
|
27
|
+
* single identity-stable config reference the Loader commits saves into
|
|
28
|
+
* (no remount per save). The marker rides the generic `.extra()` writer —
|
|
29
|
+
* the exact bytes `.volatile()` (schemastery ≥3.18.4) writes — because the
|
|
30
|
+
* dev-time schemastery resolved from the lockfile can predate that sugar;
|
|
31
|
+
* the `Meta` augmentation below declares the field the ^3.18.2 peer range
|
|
32
|
+
* admits at its upper end.
|
|
33
|
+
*
|
|
22
34
|
* @module dsh-llm-fallbacks/schema
|
|
23
35
|
*/
|
|
24
36
|
import z from '@deepseek-ai/schemastery';
|
|
25
37
|
import type { FallbacksConfig } from './config.ts';
|
|
38
|
+
declare module '@deepseek-ai/schemastery' {
|
|
39
|
+
namespace Schemastery {
|
|
40
|
+
interface Meta {
|
|
41
|
+
/** Live-editable marker read by the Loader / `SettingsForms` (dsh 0.1.6+). */
|
|
42
|
+
volatile?: boolean;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
26
46
|
export declare const Config: z<FallbacksConfig>;
|
package/dist/subagents-seam.d.ts
CHANGED
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
* `chain: []` role gets its persona exactly like a chained one (no routing
|
|
47
47
|
* state is read). Both native start surfaces are covered, with the gate each
|
|
48
48
|
* one's fail-loud contract needs (measured on the installed
|
|
49
|
-
* `@deepseek-ai/dsh-subagent` `0.1.
|
|
49
|
+
* `@deepseek-ai/dsh-subagent` `0.1.7-rc.1`): the one-shot `start` REJECTS a
|
|
50
50
|
* request carrying `persona` for a provider whose
|
|
51
|
-
* `getProvider(name).capabilities.persona` is not `true` (`lib/index.js:
|
|
51
|
+
* `getProvider(name).capabilities.persona` is not `true` (`lib/index.js:3284,3340-3363`,
|
|
52
52
|
* `assertCapabilities`), so the merge pre-checks that flag; the continuable
|
|
53
53
|
* surface is composed by the continuation manager, which applies
|
|
54
|
-
* `request.persona` unconditionally (`lib/index.js:
|
|
55
|
-
* the provider's `prepareContinuable` presence instead (`lib/index.js:
|
|
54
|
+
* `request.persona` unconditionally (`lib/index.js:1691`) and is gated by
|
|
55
|
+
* the provider's `prepareContinuable` presence instead (`lib/index.js:3316-3319`).
|
|
56
56
|
* A gate miss skips the persona with ONE contained debug log — never a failed
|
|
57
57
|
* start. An explicit caller `persona` WINS (the slot is filled only when it is
|
|
58
58
|
* absent), and every skip path returns the caller's OWN request object, so the
|