dsh-llm-fallbacks 0.1.4 → 0.1.5
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 +2 -1
- package/README.zh-CN.md +2 -1
- package/dist/client/fallbacks-store.d.ts +26 -1
- package/dist/client/index.js +133 -24
- package/dist/client/locales.d.ts +8 -0
- package/dist/gateway.d.ts +75 -23
- package/dist/index.d.ts +13 -3
- package/dist/index.js +243 -5
- package/dist/seeds.d.ts +141 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -134,7 +134,8 @@ The plugin installs as a **pure mount** — it never modifies the dsh source tre
|
|
|
134
134
|
Beyond the dsh plugin mount, `dsh-llm-fallbacks` exposes a programmable consumer surface — both faces share the same function implementations (single point of truth, no copied logic):
|
|
135
135
|
|
|
136
136
|
- **Library API**: import the runtime functions and types from the package root — `import { resolveRole, resolveChain, validateFallbacksConfig } from 'dsh-llm-fallbacks'`.
|
|
137
|
-
- **Named cordis service**: while the plugin is applied, `ctx.get('llm-fallbacks')` returns a pure-function service (`{ name, version, resolveRole, resolveChain, validateFallbacksConfig, detectLegacyKeys }`); after the plugin is disposed it is `undefined`. Runtime state stays observable via `fallbacks/switch` events, not through the service.
|
|
137
|
+
- **Named cordis service**: while the plugin is applied, `ctx.get('llm-fallbacks')` returns a pure-function service (`{ name, version, resolveRole, resolveChain, validateFallbacksConfig, detectLegacyKeys, declareSeeds, getEffectiveRoles, revertSeededPersona }`); after the plugin is disposed it is `undefined`. Runtime state stays observable via `fallbacks/switch` events, not through the service.
|
|
138
|
+
- **Role seeds**: companion plugins auto-provision role rows with zero operator hand-edit — `declareSeeds([{ id, persona }])` declares the full current seed set (re-declaring is a no-op; ids failing `/^[a-z0-9-]{1,32}$/` or equal to `inherit` are skipped per-id with a warn, never coerced), `getEffectiveRoles()` reads back which roles are seeded and whether the persona is overridden, and `revertSeededPersona(id)` restores the **currently declared** seed default. The settings card shows a seed-default / override badge with a revert button; seeds never write `chain` / `fallback`.
|
|
138
139
|
|
|
139
140
|
Full contract (export inventory, minimal examples, lifecycle, typing) → [docs/consumer-api.md](docs/consumer-api.md).
|
|
140
141
|
|
package/README.zh-CN.md
CHANGED
|
@@ -129,7 +129,8 @@ fallbacks:
|
|
|
129
129
|
除插件挂载外,`dsh-llm-fallbacks` 还暴露可编程消费面——两个入口共享同一函数实现(单点真相,无复制逻辑):
|
|
130
130
|
|
|
131
131
|
- **库 API**:从包根直接 import 运行时函数与类型——`import { resolveRole, resolveChain, validateFallbacksConfig } from 'dsh-llm-fallbacks'`。
|
|
132
|
-
- **具名 cordis service**:插件 apply 期间 `ctx.get('llm-fallbacks')` 返回纯函数服务(`{ name, version, resolveRole, resolveChain, validateFallbacksConfig, detectLegacyKeys }`),插件 dispose 后为 `undefined`。运行态(冷却、最近切换)请读 `fallbacks/switch` 事件,不走服务对象。
|
|
132
|
+
- **具名 cordis service**:插件 apply 期间 `ctx.get('llm-fallbacks')` 返回纯函数服务(`{ name, version, resolveRole, resolveChain, validateFallbacksConfig, detectLegacyKeys, declareSeeds, getEffectiveRoles, revertSeededPersona }`),插件 dispose 后为 `undefined`。运行态(冷却、最近切换)请读 `fallbacks/switch` 事件,不走服务对象。
|
|
133
|
+
- **角色 seeds(种子角色)**:伴随插件可**零手工配置**自动补全角色行——`declareSeeds([{ id, persona }])` 声明完整当前种子集(重复声明为 no-op;不匹配 `/^[a-z0-9-]{1,32}$/` 或等于保留字 `inherit` 的 id 按条跳过并告警,绝不改写),`getEffectiveRoles()` 回读哪些角色为 seeded、persona 是否被覆盖,`revertSeededPersona(id)` 恢复**当前声明**的 seed 默认 persona。设置卡显示 seed 默认 / override 徽标并带 revert 按钮;seeds 永不写 `chain` / `fallback`。
|
|
133
134
|
|
|
134
135
|
完整契约(导出清单、最小示例、生命周期、类型说明)见 [docs/consumer-api.md](docs/consumer-api.md)。
|
|
135
136
|
|
|
@@ -28,6 +28,7 @@ import type { ClientConnectionRpc, ConfigurableProviderView, HistoryEntry, IApiC
|
|
|
28
28
|
import { type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
29
29
|
import { type FallbackStrategy, type FallbacksConfig, type FallbacksRole, type FallbacksRoleRule, type FallbacksRoles } from '../config.ts';
|
|
30
30
|
import type { FallbacksSwitchEventData } from '../events.ts';
|
|
31
|
+
import type { SeedsWireStatus } from '../seeds.ts';
|
|
31
32
|
/** The plugin's settings namespace on the host wire (settings/document-updated ns filter). */
|
|
32
33
|
export declare const FALLBACKS_SETTINGS_NS = "fallbacks";
|
|
33
34
|
/** Single-page history read for the status block (spec §2.5 D-5: `HISTORY_PAGE_MESSAGES`-sized). */
|
|
@@ -92,6 +93,13 @@ export interface FallbacksSettingsState {
|
|
|
92
93
|
* over the user layer, so it cannot delete legacy keys).
|
|
93
94
|
*/
|
|
94
95
|
legacyKeys: string[];
|
|
96
|
+
/**
|
|
97
|
+
* Seeded-role badge state (spec §9.4): one entry per live seed, with the
|
|
98
|
+
* gateway's override verdict. The wire field is authoritative — absent on
|
|
99
|
+
* an old response it keeps the last accepted value (the `legacyKeys`
|
|
100
|
+
* honest rule: only a `get` may settle seed truth).
|
|
101
|
+
*/
|
|
102
|
+
seeds: SeedsWireStatus[];
|
|
95
103
|
/** Provider/model directory snapshot (spec §2.5 D-4). */
|
|
96
104
|
catalogStatus: 'idle' | 'loading' | 'ready' | 'error';
|
|
97
105
|
/** Catalog read diagnostic: whole-load failure or per-provider lookups. */
|
|
@@ -345,6 +353,20 @@ export declare class FallbacksSettingsController {
|
|
|
345
353
|
* merge cannot express). Same error handling as {@link save} (KD-G3).
|
|
346
354
|
*/
|
|
347
355
|
resetToDefaults(): Promise<void>;
|
|
356
|
+
/**
|
|
357
|
+
* Revert one seeded role to its CURRENT declared seed default (spec §9.4,
|
|
358
|
+
* AC-3) through the gateway channel (`/api/fallbacks/revert-seed`). Same
|
|
359
|
+
* write guards as {@link save} — writable / saving / write-generation —
|
|
360
|
+
* and the same KD-G3 error handling: any business rejection or transport
|
|
361
|
+
* failure surfaces its message in `state.error` for the error banner and
|
|
362
|
+
* the form stays editable for retry. A business `{ reverted: false,
|
|
363
|
+
* reason }` outcome is still a successful RPC — the post-write read
|
|
364
|
+
* result (config / legacyKeys / seeds) lands either way, and the revert
|
|
365
|
+
* button stays disabled while the write is in flight.
|
|
366
|
+
* @param id - the seeded role id; the host matches it by trimmed id
|
|
367
|
+
* against the seed registry (spec §9.3).
|
|
368
|
+
*/
|
|
369
|
+
revertSeed(id: string): Promise<void>;
|
|
348
370
|
/** Stop in-flight responses from publishing after plugin disposal. */
|
|
349
371
|
dispose(): void;
|
|
350
372
|
/**
|
|
@@ -358,7 +380,10 @@ export declare class FallbacksSettingsController {
|
|
|
358
380
|
* same publish: the wire field drives the migration banner. save/reset
|
|
359
381
|
* pass the POST-WRITE value (W-1/F-1) — or the previous value when the
|
|
360
382
|
* response omits the field, so a write can never clear the banner
|
|
361
|
-
* against server truth; only a real `get` may.
|
|
383
|
+
* against server truth; only a real `get` may. `seeds` (spec §9.4)
|
|
384
|
+
* follows the same honest rule: the wire badge field is authoritative
|
|
385
|
+
* only when a real config resolved — a transient channel-down keeps the
|
|
386
|
+
* last accepted badge state.
|
|
362
387
|
*/
|
|
363
388
|
private accept;
|
|
364
389
|
private fail;
|
package/dist/client/index.js
CHANGED
|
@@ -100,6 +100,20 @@ window.__ModuleLoader__.load({
|
|
|
100
100
|
return current;
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
103
|
+
* Shape-guard the wire `seeds` badge field (spec §9.4): only `{ id,
|
|
104
|
+
* overridden }` entries survive — the `legacyKeys` element-filter
|
|
105
|
+
* precedent. A non-array value resolves to `[]`; malformed entries are
|
|
106
|
+
* dropped, so an all-bad array also lands `[]`. The store never trusts a
|
|
107
|
+
* misshapen badge field.
|
|
108
|
+
*/
|
|
109
|
+
function parseSeedsWire(value) {
|
|
110
|
+
if (!Array.isArray(value)) return [];
|
|
111
|
+
return value.filter((entry) => {
|
|
112
|
+
if (!isRecord(entry)) return false;
|
|
113
|
+
return typeof entry.id === "string" && typeof entry.overridden === "boolean";
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
103
117
|
* The provider dropdown's offer set (spec §2.5 D-4): catalog providers whose
|
|
104
118
|
* settings profile resolves in the describe namespaces — the Models page's
|
|
105
119
|
* `configured` predicate (`ui-models` store.ts): a provider is configured
|
|
@@ -410,6 +424,7 @@ window.__ModuleLoader__.load({
|
|
|
410
424
|
config: defaultFallbacksConfig,
|
|
411
425
|
present: false,
|
|
412
426
|
legacyKeys: [],
|
|
427
|
+
seeds: [],
|
|
413
428
|
catalogStatus: "idle",
|
|
414
429
|
catalogError: null,
|
|
415
430
|
providers: [],
|
|
@@ -473,14 +488,16 @@ window.__ModuleLoader__.load({
|
|
|
473
488
|
const writable = describeResult.result.value.writable;
|
|
474
489
|
let config;
|
|
475
490
|
let legacyKeys = [];
|
|
491
|
+
let seeds = [];
|
|
476
492
|
if (getResult !== void 0 && getResult.ok && getResult.value !== null && typeof getResult.value === "object") {
|
|
477
493
|
if ("config" in getResult.value) config = getResult.value.config;
|
|
478
494
|
if ("legacyKeys" in getResult.value) {
|
|
479
495
|
const wireLegacyKeys = getResult.value.legacyKeys;
|
|
480
496
|
if (Array.isArray(wireLegacyKeys)) legacyKeys = wireLegacyKeys.filter((key) => typeof key === "string");
|
|
481
497
|
}
|
|
498
|
+
if ("seeds" in getResult.value) seeds = parseSeedsWire(getResult.value.seeds);
|
|
482
499
|
}
|
|
483
|
-
this.accept(config, writable, legacyKeys);
|
|
500
|
+
this.accept(config, writable, legacyKeys, seeds);
|
|
484
501
|
} catch (error) {
|
|
485
502
|
if (generation !== this.readGeneration) return;
|
|
486
503
|
if (writeGenerationAtStart !== this.writeGeneration) return;
|
|
@@ -616,7 +633,12 @@ window.__ModuleLoader__.load({
|
|
|
616
633
|
const wireLegacyKeys = value.legacyKeys;
|
|
617
634
|
if (Array.isArray(wireLegacyKeys)) legacyKeys = wireLegacyKeys.filter((key) => typeof key === "string");
|
|
618
635
|
}
|
|
619
|
-
this.
|
|
636
|
+
let seeds = this.store.getSnapshot().seeds;
|
|
637
|
+
if (value !== null && typeof value === "object" && "seeds" in value) {
|
|
638
|
+
const wireSeeds = value.seeds;
|
|
639
|
+
if (Array.isArray(wireSeeds)) seeds = parseSeedsWire(wireSeeds);
|
|
640
|
+
}
|
|
641
|
+
this.accept(config, true, legacyKeys, seeds);
|
|
620
642
|
} catch (error) {
|
|
621
643
|
if (generation !== this.writeGeneration) return;
|
|
622
644
|
this.fail(error);
|
|
@@ -647,7 +669,55 @@ window.__ModuleLoader__.load({
|
|
|
647
669
|
const wireLegacyKeys = value.legacyKeys;
|
|
648
670
|
if (Array.isArray(wireLegacyKeys)) legacyKeys = wireLegacyKeys.filter((key) => typeof key === "string");
|
|
649
671
|
}
|
|
650
|
-
this.
|
|
672
|
+
let seeds = this.store.getSnapshot().seeds;
|
|
673
|
+
if (value !== null && typeof value === "object" && "seeds" in value) {
|
|
674
|
+
const wireSeeds = value.seeds;
|
|
675
|
+
if (Array.isArray(wireSeeds)) seeds = parseSeedsWire(wireSeeds);
|
|
676
|
+
}
|
|
677
|
+
this.accept(config, true, legacyKeys, seeds);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (generation !== this.writeGeneration) return;
|
|
680
|
+
this.fail(error);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Revert one seeded role to its CURRENT declared seed default (spec §9.4,
|
|
685
|
+
* AC-3) through the gateway channel (`/api/fallbacks/revert-seed`). Same
|
|
686
|
+
* write guards as {@link save} — writable / saving / write-generation —
|
|
687
|
+
* and the same KD-G3 error handling: any business rejection or transport
|
|
688
|
+
* failure surfaces its message in `state.error` for the error banner and
|
|
689
|
+
* the form stays editable for retry. A business `{ reverted: false,
|
|
690
|
+
* reason }` outcome is still a successful RPC — the post-write read
|
|
691
|
+
* result (config / legacyKeys / seeds) lands either way, and the revert
|
|
692
|
+
* button stays disabled while the write is in flight.
|
|
693
|
+
* @param id - the seeded role id; the host matches it by trimmed id
|
|
694
|
+
* against the seed registry (spec §9.3).
|
|
695
|
+
*/
|
|
696
|
+
async revertSeed(id) {
|
|
697
|
+
const state = this.store.getSnapshot();
|
|
698
|
+
if (!state.writable || state.status === "saving") return;
|
|
699
|
+
const generation = ++this.writeGeneration;
|
|
700
|
+
this.store.update((draft) => {
|
|
701
|
+
draft.status = "saving";
|
|
702
|
+
draft.error = null;
|
|
703
|
+
});
|
|
704
|
+
try {
|
|
705
|
+
const result = await this.rpc.call("/api", "fallbacks/revert-seed", { args: { id } });
|
|
706
|
+
if (generation !== this.writeGeneration) return;
|
|
707
|
+
if (!result.ok) throw result.error;
|
|
708
|
+
const value = result.value;
|
|
709
|
+
const config = value !== null && typeof value === "object" && "config" in value ? value.config : void 0;
|
|
710
|
+
let legacyKeys = this.store.getSnapshot().legacyKeys;
|
|
711
|
+
if (value !== null && typeof value === "object" && "legacyKeys" in value) {
|
|
712
|
+
const wireLegacyKeys = value.legacyKeys;
|
|
713
|
+
if (Array.isArray(wireLegacyKeys)) legacyKeys = wireLegacyKeys.filter((key) => typeof key === "string");
|
|
714
|
+
}
|
|
715
|
+
let seeds = this.store.getSnapshot().seeds;
|
|
716
|
+
if (value !== null && typeof value === "object" && "seeds" in value) {
|
|
717
|
+
const wireSeeds = value.seeds;
|
|
718
|
+
if (Array.isArray(wireSeeds)) seeds = parseSeedsWire(wireSeeds);
|
|
719
|
+
}
|
|
720
|
+
this.accept(config, true, legacyKeys, seeds);
|
|
651
721
|
} catch (error) {
|
|
652
722
|
if (generation !== this.writeGeneration) return;
|
|
653
723
|
this.fail(error);
|
|
@@ -672,9 +742,12 @@ window.__ModuleLoader__.load({
|
|
|
672
742
|
* same publish: the wire field drives the migration banner. save/reset
|
|
673
743
|
* pass the POST-WRITE value (W-1/F-1) — or the previous value when the
|
|
674
744
|
* response omits the field, so a write can never clear the banner
|
|
675
|
-
* against server truth; only a real `get` may.
|
|
745
|
+
* against server truth; only a real `get` may. `seeds` (spec §9.4)
|
|
746
|
+
* follows the same honest rule: the wire badge field is authoritative
|
|
747
|
+
* only when a real config resolved — a transient channel-down keeps the
|
|
748
|
+
* last accepted badge state.
|
|
676
749
|
*/
|
|
677
|
-
accept(config, writable, legacyKeys) {
|
|
750
|
+
accept(config, writable, legacyKeys, seeds) {
|
|
678
751
|
const parsed = config === void 0 ? void 0 : parseFallbacksConfig(config);
|
|
679
752
|
this.store.update((state) => {
|
|
680
753
|
state.status = "ready";
|
|
@@ -682,6 +755,7 @@ window.__ModuleLoader__.load({
|
|
|
682
755
|
state.writable = writable;
|
|
683
756
|
state.present = parsed !== void 0;
|
|
684
757
|
state.legacyKeys = parsed === void 0 ? state.legacyKeys : legacyKeys;
|
|
758
|
+
state.seeds = parsed === void 0 ? state.seeds : seeds;
|
|
685
759
|
if (parsed !== void 0) state.config = parsed;
|
|
686
760
|
state.configuredProviders = configuredProvidersOf(state.providers, this.namespaces);
|
|
687
761
|
});
|
|
@@ -789,6 +863,10 @@ window.__ModuleLoader__.load({
|
|
|
789
863
|
"roles.idPlaceholder": "例如 reviewer",
|
|
790
864
|
"roles.persona": "人格提示",
|
|
791
865
|
"roles.personaPlaceholder": "例如:你是资深代码审查员",
|
|
866
|
+
"roles.seedDefault": "seed 默认",
|
|
867
|
+
"roles.seedOverride": "seed 覆盖",
|
|
868
|
+
"roles.revertPersona": "还原 Seed 默认",
|
|
869
|
+
"roles.seedChainOptional": "角色 \"{id}\" 为 seed 角色:链可留空,保存不会被拦截",
|
|
792
870
|
"roles.fallback": "链拼接策略",
|
|
793
871
|
"roles.fallback.inherit-root": "继承 root(角色链后追加 rootChain)",
|
|
794
872
|
"roles.fallback.none": "仅角色链(不追加 rootChain)",
|
|
@@ -919,6 +997,10 @@ window.__ModuleLoader__.load({
|
|
|
919
997
|
"roles.idPlaceholder": "e.g. reviewer",
|
|
920
998
|
"roles.persona": "Persona",
|
|
921
999
|
"roles.personaPlaceholder": "e.g. you are a senior code reviewer",
|
|
1000
|
+
"roles.seedDefault": "Seed default",
|
|
1001
|
+
"roles.seedOverride": "Seed override",
|
|
1002
|
+
"roles.revertPersona": "Revert to seed default",
|
|
1003
|
+
"roles.seedChainOptional": "Role \"{id}\" is a seed role: the chain may stay empty",
|
|
922
1004
|
"roles.fallback": "Chain append",
|
|
923
1005
|
"roles.fallback.inherit-root": "Inherit root (append rootChain after the role chain)",
|
|
924
1006
|
"roles.fallback.none": "Role chain only (no rootChain)",
|
|
@@ -1205,8 +1287,14 @@ window.__ModuleLoader__.load({
|
|
|
1205
1287
|
* rootChain and role chains. Returns one localized message per violation;
|
|
1206
1288
|
* a non-empty result blocks {@link save} — the draft is never written.
|
|
1207
1289
|
* `persona` is free text and never validated.
|
|
1290
|
+
*
|
|
1291
|
+
* `seededIds` is the live trimmed-id → overridden map derived from
|
|
1292
|
+
* `state.seeds` (spec §9.4): the empty-chain block relaxes for seeded ids
|
|
1293
|
+
* only (spec §9.6 / AC-3 — a seeded role's chain is legitimately empty by
|
|
1294
|
+
* design, R4, and its persona edits must stay persistable); non-seeded
|
|
1295
|
+
* behavior is byte-identical.
|
|
1208
1296
|
*/
|
|
1209
|
-
function validateDraft(draft, t) {
|
|
1297
|
+
function validateDraft(draft, t, seededIds) {
|
|
1210
1298
|
const errors = [];
|
|
1211
1299
|
const declaredIds = /* @__PURE__ */ new Set();
|
|
1212
1300
|
for (const role of draft.roles.list) {
|
|
@@ -1222,7 +1310,7 @@ window.__ModuleLoader__.load({
|
|
|
1222
1310
|
message: error.message
|
|
1223
1311
|
}));
|
|
1224
1312
|
}
|
|
1225
|
-
if ((role.chain ?? []).length === 0) errors.push(t("validation.roleChainRequired", { id: role.id }));
|
|
1313
|
+
if ((role.chain ?? []).length === 0 && !seededIds.has(role.id.trim())) errors.push(t("validation.roleChainRequired", { id: role.id }));
|
|
1226
1314
|
}
|
|
1227
1315
|
for (const entry of draft.rootChain) try {
|
|
1228
1316
|
parseSelector(entry);
|
|
@@ -1572,6 +1660,8 @@ window.__ModuleLoader__.load({
|
|
|
1572
1660
|
const unknownCodes = scalars.triggerCodes.filter((code) => !KNOWN_TRIGGER_CODES.includes(code));
|
|
1573
1661
|
const roleOptions = ruleRoleOptions({ list: roleRows });
|
|
1574
1662
|
const invalidRoleIds = validationAttempted ? collectInvalidRoleIds(roleRows) : null;
|
|
1663
|
+
const seededIds = /* @__PURE__ */ new Map();
|
|
1664
|
+
for (const seed of state.seeds) seededIds.set(seed.id.trim(), seed.overridden);
|
|
1575
1665
|
const effectiveModel = deriveEffectiveModel(state.config, state.switches);
|
|
1576
1666
|
const effectiveModelLine = effectiveModel.kind === "unavailable" ? t("status.effectiveModel.unavailable") : `${effectiveModel.provider}/${effectiveModel.model} · ${t("status.effectiveModel.note")}`;
|
|
1577
1667
|
const latestSwitch = state.switches[0];
|
|
@@ -1605,7 +1695,7 @@ window.__ModuleLoader__.load({
|
|
|
1605
1695
|
dirty
|
|
1606
1696
|
]);
|
|
1607
1697
|
const save = () => {
|
|
1608
|
-
const errors = validateDraft(draft, t);
|
|
1698
|
+
const errors = validateDraft(draft, t, seededIds);
|
|
1609
1699
|
if (hasEmptyRuleRows) errors.push(t("validation.ruleRoleRequired"));
|
|
1610
1700
|
if (errors.length > 0) {
|
|
1611
1701
|
setValidationErrors(errors);
|
|
@@ -1626,7 +1716,7 @@ window.__ModuleLoader__.load({
|
|
|
1626
1716
|
};
|
|
1627
1717
|
(0, react.useEffect)(() => {
|
|
1628
1718
|
if (!validationAttempted) return;
|
|
1629
|
-
if (validateDraft(draft, t).length === 0 && !ruleRows.some((row) => row.role === "")) {
|
|
1719
|
+
if (validateDraft(draft, t, seededIds).length === 0 && !ruleRows.some((row) => row.role === "")) {
|
|
1630
1720
|
setValidationErrors([]);
|
|
1631
1721
|
setValidationAttempted(false);
|
|
1632
1722
|
}
|
|
@@ -1849,6 +1939,7 @@ window.__ModuleLoader__.load({
|
|
|
1849
1939
|
className: FallbacksCard_module_css_default.list,
|
|
1850
1940
|
children: roleRows.map((row, index) => {
|
|
1851
1941
|
const invalid = invalidRoleIds?.has(row.id.trim()) ?? false;
|
|
1942
|
+
const seed = seededIds.get(row.id.trim());
|
|
1852
1943
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1853
1944
|
className: FallbacksCard_module_css_default.editorCard,
|
|
1854
1945
|
children: [
|
|
@@ -1883,20 +1974,38 @@ window.__ModuleLoader__.load({
|
|
|
1883
1974
|
className: FallbacksCard_module_css_default.ruleGrid,
|
|
1884
1975
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1885
1976
|
className: FallbacksCard_module_css_default.ruleCell,
|
|
1886
|
-
children: [
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1977
|
+
children: [
|
|
1978
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1979
|
+
className: FallbacksCard_module_css_default.ruleCellLabel,
|
|
1980
|
+
children: t("roles.persona")
|
|
1981
|
+
}),
|
|
1982
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1983
|
+
rows: 3,
|
|
1984
|
+
className: `${FallbacksCard_module_css_default.input} ${FallbacksCard_module_css_default.inputTextarea}`,
|
|
1985
|
+
value: row.persona,
|
|
1986
|
+
placeholder: t("roles.personaPlaceholder"),
|
|
1987
|
+
"aria-label": t("roles.persona"),
|
|
1988
|
+
disabled: !writable,
|
|
1989
|
+
onChange: (event) => {
|
|
1990
|
+
updateRoleRow(index, { persona: event.target.value });
|
|
1991
|
+
}
|
|
1992
|
+
}),
|
|
1993
|
+
seed !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1994
|
+
className: FallbacksCard_module_css_default.hint,
|
|
1995
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1996
|
+
className: FallbacksCard_module_css_default.pending,
|
|
1997
|
+
children: t(seed ? "roles.seedOverride" : "roles.seedDefault")
|
|
1998
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1999
|
+
variant: "outline",
|
|
2000
|
+
size: "sm",
|
|
2001
|
+
disabled: !writable || saving,
|
|
2002
|
+
onClick: () => {
|
|
2003
|
+
controller.revertSeed(row.id.trim());
|
|
2004
|
+
},
|
|
2005
|
+
children: t("roles.revertPersona")
|
|
2006
|
+
})]
|
|
2007
|
+
})
|
|
2008
|
+
]
|
|
1900
2009
|
})
|
|
1901
2010
|
}),
|
|
1902
2011
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1915,7 +2024,7 @@ window.__ModuleLoader__.load({
|
|
|
1915
2024
|
}
|
|
1916
2025
|
}, selectorIndex)), row.selectors.every((selector) => selectorRowToRaw(selector) === "") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1917
2026
|
className: FallbacksCard_module_css_default.hint,
|
|
1918
|
-
children: t("validation.roleChainRequired", { id: row.id })
|
|
2027
|
+
children: seed !== void 0 ? t("roles.seedChainOptional", { id: row.id }) : t("validation.roleChainRequired", { id: row.id })
|
|
1919
2028
|
})]
|
|
1920
2029
|
}),
|
|
1921
2030
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
package/dist/client/locales.d.ts
CHANGED
|
@@ -54,6 +54,10 @@ export declare const zh: {
|
|
|
54
54
|
'roles.idPlaceholder': string;
|
|
55
55
|
'roles.persona': string;
|
|
56
56
|
'roles.personaPlaceholder': string;
|
|
57
|
+
'roles.seedDefault': string;
|
|
58
|
+
'roles.seedOverride': string;
|
|
59
|
+
'roles.revertPersona': string;
|
|
60
|
+
'roles.seedChainOptional': string;
|
|
57
61
|
'roles.fallback': string;
|
|
58
62
|
'roles.fallback.inherit-root': string;
|
|
59
63
|
'roles.fallback.none': string;
|
|
@@ -186,6 +190,10 @@ export declare const en: {
|
|
|
186
190
|
'roles.idPlaceholder': string;
|
|
187
191
|
'roles.persona': string;
|
|
188
192
|
'roles.personaPlaceholder': string;
|
|
193
|
+
'roles.seedDefault': string;
|
|
194
|
+
'roles.seedOverride': string;
|
|
195
|
+
'roles.revertPersona': string;
|
|
196
|
+
'roles.seedChainOptional': string;
|
|
189
197
|
'roles.fallback': string;
|
|
190
198
|
'roles.fallback.inherit-root': string;
|
|
191
199
|
'roles.fallback.none': string;
|
package/dist/gateway.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* T1 (plan llm-fallbacks-settings-gateway)
|
|
3
|
-
* gateway: the `/api/fallbacks/get` +
|
|
4
|
-
* `/api/fallbacks/reset`
|
|
2
|
+
* T1 (plan llm-fallbacks-settings-gateway) + T3 (plan fallbacks-role-seeds) —
|
|
3
|
+
* host-side `fallbacks` config gateway: the `/api/fallbacks/get` +
|
|
4
|
+
* `/api/fallbacks/set` + `/api/fallbacks/reset` + `/api/fallbacks/revert-seed`
|
|
5
|
+
* endpoints.
|
|
5
6
|
*
|
|
6
7
|
* Transport: the typertGateway `/api` interceptor is the single host-wide RPC
|
|
7
8
|
* slot (a plugin must NOT `connection.rpc.intercept('/api')` again — it would
|
|
@@ -37,7 +38,11 @@
|
|
|
37
38
|
* method — advisor has only get/set) clears the user layer via
|
|
38
39
|
* `ctx.settings.replace(ns, {})`: `set` is merge-only and cannot express
|
|
39
40
|
* "reset to composition defaults" (sending default VALUES as a patch would
|
|
40
|
-
* pin stale defaults into the user layer).
|
|
41
|
+
* pin stale defaults into the user layer). Every read response (get/set/reset)
|
|
42
|
+
* carries the additive `seeds: SeedsWireStatus[]` badge state (spec §9.4) and
|
|
43
|
+
* `revert-seed` exposes revert-to-current-seed-default for one id — both
|
|
44
|
+
* delegate to the per-apply `FallbacksSeedManager` passed into the
|
|
45
|
+
* constructor (single point of truth, no copied manager logic).
|
|
41
46
|
*
|
|
42
47
|
* The settings service is OPTIONAL (no settings service → the bridge source
|
|
43
48
|
* stays the entry, `get` still works; `set`/`reset` fail with a clear
|
|
@@ -57,6 +62,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
57
62
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
58
63
|
import type { TypertContribution } from '@deepseek-ai/dsh-typert-registry';
|
|
59
64
|
import type { FallbacksConfig } from './config';
|
|
65
|
+
import type { FallbacksSeedManager, SeedRevertOutcome, SeedsWireStatus } from './seeds';
|
|
60
66
|
/** The `fallbacks` settings namespace (registered when a settings service exists). */
|
|
61
67
|
export declare const FALLBACKS_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
|
|
62
68
|
/**
|
|
@@ -72,6 +78,20 @@ export interface FallbacksSettingsBridge {
|
|
|
72
78
|
}
|
|
73
79
|
/** Patch shape accepted by `fallbacks.set` — any subset of the config keys. */
|
|
74
80
|
export type FallbacksConfigPatch = Partial<FallbacksConfig>;
|
|
81
|
+
/**
|
|
82
|
+
* The wire response of every read (get/set/reset — W-1/F-1): the normalized
|
|
83
|
+
* config plus `legacyKeys` plus the additive `seeds` badge state (spec §9.4,
|
|
84
|
+
* legacyKeys precedent — old clients ignore it).
|
|
85
|
+
*/
|
|
86
|
+
export interface FallbacksReadResult {
|
|
87
|
+
config: FallbacksConfig;
|
|
88
|
+
legacyKeys: string[];
|
|
89
|
+
seeds: SeedsWireStatus[];
|
|
90
|
+
}
|
|
91
|
+
/** The `fallbacks/revert-seed` response — a read result plus the revert outcome. */
|
|
92
|
+
export interface FallbacksRevertResult extends FallbacksReadResult {
|
|
93
|
+
outcome: SeedRevertOutcome;
|
|
94
|
+
}
|
|
75
95
|
/**
|
|
76
96
|
* The host-side `fallbacks` config gateway (`/api/fallbacks/get` +
|
|
77
97
|
* `/api/fallbacks/set` + `/api/fallbacks/reset`). Registered as the cordis
|
|
@@ -86,14 +106,19 @@ export type FallbacksConfigPatch = Partial<FallbacksConfig>;
|
|
|
86
106
|
*/
|
|
87
107
|
export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
88
108
|
private readonly bridge;
|
|
109
|
+
/** The per-apply seed manager — single point of truth for seed state (spec §9.4). */
|
|
110
|
+
private readonly seeds;
|
|
89
111
|
/** The live settings service once the optional inject child activates. */
|
|
90
112
|
private settings;
|
|
91
113
|
/**
|
|
92
114
|
* @param ctx - owning context (the plugin fiber's ctx inside `apply`).
|
|
93
115
|
* @param bridge - the same `FallbacksSettingsBridge` the runtime reads, so
|
|
94
116
|
* get/set/reset always operate on the live composed config.
|
|
117
|
+
* @param seeds - the per-apply `FallbacksSeedManager` constructed in
|
|
118
|
+
* `apply()` — the gateway delegates badge state and revert to it (no
|
|
119
|
+
* copied manager logic), through the io seam built over this bridge.
|
|
95
120
|
*/
|
|
96
|
-
constructor(ctx: Context, bridge: FallbacksSettingsBridge);
|
|
121
|
+
constructor(ctx: Context, bridge: FallbacksSettingsBridge, seeds: FallbacksSeedManager);
|
|
97
122
|
/**
|
|
98
123
|
* Read the current composed config (schema defaults → entry base → settings
|
|
99
124
|
* user layer). No hard-gate resolver (ADR-2): the raw composed config is
|
|
@@ -102,12 +127,11 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
|
102
127
|
* two-block-era fields (`chains` / `roles.default` / undeclared rule
|
|
103
128
|
* role refs) detected on the composed source (schemastery retains them,
|
|
104
129
|
* plan Task 1 Step 1), so the client can show a migration banner (spec
|
|
105
|
-
* §9, incremental field — old clients ignore it)
|
|
130
|
+
* §9, incremental field — old clients ignore it) — plus the additive
|
|
131
|
+
* `seeds` badge state (spec §9.4, legacyKeys precedent — old clients
|
|
132
|
+
* ignore it).
|
|
106
133
|
*/
|
|
107
|
-
get():
|
|
108
|
-
config: FallbacksConfig;
|
|
109
|
-
legacyKeys: string[];
|
|
110
|
-
};
|
|
134
|
+
get(): FallbacksReadResult;
|
|
111
135
|
/**
|
|
112
136
|
* Validate a config patch and write it to the settings USER layer (live —
|
|
113
137
|
* the runtime re-reads the same bridge source; no restart needed).
|
|
@@ -117,28 +141,42 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
|
117
141
|
* POST-WRITE composed source (W-1/F-1): `set` is a settings MERGE, so a
|
|
118
142
|
* legacy user layer (`chains` / `roles.default`) survives a new-shape
|
|
119
143
|
* save — the response must keep reporting it, or the client banner
|
|
120
|
-
* would clear against server truth
|
|
144
|
+
* would clear against server truth — plus the post-write `seeds` badge
|
|
145
|
+
* state (same W-1/F-1 rule as `legacyKeys`). Same shape as `get`.
|
|
121
146
|
* @throws when the patch fails `Config` validation, or when no settings
|
|
122
147
|
* service is composed (KD-G5: the write channel is unavailable).
|
|
123
148
|
*/
|
|
124
|
-
set(patch: FallbacksConfigPatch): Promise<
|
|
125
|
-
config: FallbacksConfig;
|
|
126
|
-
legacyKeys: string[];
|
|
127
|
-
}>;
|
|
149
|
+
set(patch: FallbacksConfigPatch): Promise<FallbacksReadResult>;
|
|
128
150
|
/**
|
|
129
151
|
* Clear the fallbacks settings USER layer so the composition defaults
|
|
130
152
|
* reapply (`settings.replace(ns, {})` — the in-process removal path a
|
|
131
153
|
* merge-only `set` cannot express).
|
|
132
154
|
* @returns the new composed config plus `legacyKeys` on the post-write
|
|
133
155
|
* source — `replace` drops the user layer, but legacy keys carried by
|
|
134
|
-
* the entry base survive and are correctly re-reported (W-1/F-1)
|
|
156
|
+
* the entry base survive and are correctly re-reported (W-1/F-1) — plus
|
|
157
|
+
* the post-write `seeds` badge state (clearing the user layer also
|
|
158
|
+
* clears the materialized seed rows, so the honest response reports the
|
|
159
|
+
* emptied state; the registry survives and the next declare re-materializes).
|
|
135
160
|
* @throws when no settings service is composed (KD-G5: the write channel
|
|
136
161
|
* is unavailable).
|
|
137
162
|
*/
|
|
138
|
-
reset(): Promise<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
reset(): Promise<FallbacksReadResult>;
|
|
164
|
+
/**
|
|
165
|
+
* Revert one seeded role to its CURRENT declared seed default (AC-3, spec
|
|
166
|
+
* §9.4) — the gateway half of surface (c), delegating to the same manager
|
|
167
|
+
* the service method uses (single point of truth). Business failures are
|
|
168
|
+
* values, never throws: a non-seeded id or a deleted row returns
|
|
169
|
+
* `{ reverted: false, reason }` without writing. The only throw is the
|
|
170
|
+
* KD-G5 settings-unavailable path, and only when a write is actually
|
|
171
|
+
* needed (an idempotent revert at the default needs no channel).
|
|
172
|
+
* @param id - the seeded role id; matched by trimmed id against the
|
|
173
|
+
* registry (row matching, spec §9.3).
|
|
174
|
+
* @returns the post-write read result (config / legacyKeys / seeds — the
|
|
175
|
+
* write happened before this read, W-1/F-1) plus the revert `outcome`.
|
|
176
|
+
* @throws TypeError when `id` is not a string; Error when the settings
|
|
177
|
+
* write channel is unavailable (KD-G5).
|
|
178
|
+
*/
|
|
179
|
+
revertSeed(id: string): Promise<FallbacksRevertResult>;
|
|
142
180
|
/**
|
|
143
181
|
* Read the live composed config and normalize it to the typertGateway JSON
|
|
144
182
|
* wire boundary. Containment (guide §10): a malformed stored user layer
|
|
@@ -156,11 +194,25 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
|
|
|
156
194
|
/**
|
|
157
195
|
* The wire response of every read (get/set/reset — W-1/F-1): the
|
|
158
196
|
* normalized config plus `legacyKeys` detected on the live composed
|
|
159
|
-
* source. set/reset must report the
|
|
160
|
-
* merge retains legacy user-layer keys,
|
|
161
|
-
* the honest response keeps the migration
|
|
197
|
+
* source plus the additive `seeds` badge state. set/reset must report the
|
|
198
|
+
* POST-WRITE source: the settings merge retains legacy user-layer keys,
|
|
199
|
+
* so a save cannot clear them — the honest response keeps the migration
|
|
200
|
+
* banner until a get agrees; `seeds` follows the same rule (post-write
|
|
201
|
+
* badge state, W-1/F-1).
|
|
162
202
|
*/
|
|
163
203
|
private readResult;
|
|
204
|
+
/**
|
|
205
|
+
* The io seam the seed manager writes through (spec §9.1): `read` walks
|
|
206
|
+
* the same live bridge source the gateway reads; `writeRoles` persists a
|
|
207
|
+
* full `{ list, rules }` to the settings user layer — both arrays always
|
|
208
|
+
* computed from a fresh composed read, so the write stays correct under
|
|
209
|
+
* dsh-settings `mergeLayers` array-replace semantics and never touches
|
|
210
|
+
* operator rules. The write channel fails with the same KD-G5 message as
|
|
211
|
+
* set/reset when no settings service is composed. Built fresh per call so
|
|
212
|
+
* the mutable settings capture is read at call time (the inject child
|
|
213
|
+
* swaps it when the settings service appears/disappears).
|
|
214
|
+
*/
|
|
215
|
+
private seedsIo;
|
|
164
216
|
}
|
|
165
217
|
/**
|
|
166
218
|
* The explicit typert contribution for the `fallbacks` gateway endpoints —
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { Config } from './schema.ts';
|
|
|
34
34
|
import { resolveChain } from './chains.ts';
|
|
35
35
|
import { resolveRole } from './roles.ts';
|
|
36
36
|
import { FallbackStateStore } from './state.ts';
|
|
37
|
+
import { type EffectiveRolesReadback, type SeedDeclaration, type SeedDeclareOutcome, type SeedRevertOutcome } from './seeds.ts';
|
|
37
38
|
/** The plugin row id mounted by the profile bundle patch. */
|
|
38
39
|
export declare const name = "llm-fallbacks";
|
|
39
40
|
/**
|
|
@@ -46,9 +47,11 @@ export declare const provide: readonly ["llm-fallbacks"];
|
|
|
46
47
|
/**
|
|
47
48
|
* The named cordis service `ctx.get('llm-fallbacks')` exposes while the
|
|
48
49
|
* plugin is applied: the pure-function library surface + `name`/`version`
|
|
49
|
-
* metadata
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* metadata, plus the three ADDITIVE role-seed methods (spec §9.1, plan
|
|
51
|
+
* fallbacks-role-seeds T2). Deliberately no state BEARING FIELDS (no
|
|
52
|
+
* stateStore, no event emitters, no filter helpers) — the seed methods are
|
|
53
|
+
* closures over the per-apply `FallbacksSeedManager`, so state stays behind
|
|
54
|
+
* the closure and dies with the fiber (spec §9.5).
|
|
52
55
|
*/
|
|
53
56
|
export interface FallbacksService {
|
|
54
57
|
/** Matches the plugin `name`. */
|
|
@@ -59,6 +62,12 @@ export interface FallbacksService {
|
|
|
59
62
|
resolveChain: typeof resolveChain;
|
|
60
63
|
validateFallbacksConfig: typeof validateFallbacksConfig;
|
|
61
64
|
detectLegacyKeys: typeof detectLegacyKeys;
|
|
65
|
+
/** (a) Declare the companion's FULL current seed set (replacement semantics, spec §9.1). */
|
|
66
|
+
declareSeeds(seeds: readonly SeedDeclaration[]): Promise<SeedDeclareOutcome>;
|
|
67
|
+
/** (b) Sync readback — effective taxonomy with seed annotations. */
|
|
68
|
+
getEffectiveRoles(): EffectiveRolesReadback;
|
|
69
|
+
/** (c) Revert one id to the CURRENT declared seed default. */
|
|
70
|
+
revertSeededPersona(id: string): Promise<SeedRevertOutcome>;
|
|
62
71
|
}
|
|
63
72
|
declare module '@deepseek-ai/cordis' {
|
|
64
73
|
interface Context {
|
|
@@ -75,6 +84,7 @@ export { resolveRole, type AgentLike, type Origin, } from './roles.ts';
|
|
|
75
84
|
export { annotateCandidates, createCandidateFilter, hasWildcardEntry, resolveCandidate, resolveChain, resolveChainViews, selectCandidates, type AnnotatedCandidate, type CandidateFilterOptions, type CandidateSkipReason, type FailingModel, } from './chains.ts';
|
|
76
85
|
export { defaultFallbacksConfig, detectLegacyKeys, INHERIT_ROLE_ID, ROLE_ID_PATTERN, validateFallbacksConfig, type FallbacksConfig, type FallbacksConfigLogger, type FallbacksRole, type FallbacksRoles, type FallbacksRoleRule, type FallbackStrategy, type RevertPolicy, } from './config.ts';
|
|
77
86
|
export { parseSelector, SelectorError, type Selector, } from './selectors.ts';
|
|
87
|
+
export { FallbacksSeedManager, type EffectiveRole, type EffectiveRolesReadback, type SeedConflict, type SeedDeclaration, type SeedDeclareOutcome, type SeedRevertFailReason, type SeedRevertOutcome, type SeedSkipReason, type SeedsIo, type SeedsWireStatus, } from './seeds.ts';
|
|
78
88
|
/**
|
|
79
89
|
* @internal Test seam (T3 review Minor 3): the per-agent fallback state store
|
|
80
90
|
* of the plugin applied to `ctx` — last apply wins. Not part of the plugin's
|
package/dist/index.js
CHANGED
|
@@ -403,10 +403,12 @@ const ROLES_KEYS = {
|
|
|
403
403
|
};
|
|
404
404
|
var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
405
405
|
bridge;
|
|
406
|
+
seeds;
|
|
406
407
|
settings;
|
|
407
|
-
constructor(ctx, bridge) {
|
|
408
|
+
constructor(ctx, bridge, seeds) {
|
|
408
409
|
super(ctx, "fallbacks");
|
|
409
410
|
this.bridge = bridge;
|
|
411
|
+
this.seeds = seeds;
|
|
410
412
|
ctx.inject(["settings"], (sctx) => {
|
|
411
413
|
this.settings = sctx.settings;
|
|
412
414
|
return () => {
|
|
@@ -433,6 +435,14 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
433
435
|
await settings.replace(FALLBACKS_SETTINGS_NAMESPACE, {});
|
|
434
436
|
return this.readResult();
|
|
435
437
|
}
|
|
438
|
+
async revertSeed(id) {
|
|
439
|
+
if (typeof id !== "string") throw new TypeError("dsh-llm-fallbacks: seed revert id must be a string");
|
|
440
|
+
const outcome = await this.seeds.revert(id, this.seedsIo());
|
|
441
|
+
return {
|
|
442
|
+
...this.readResult(),
|
|
443
|
+
outcome
|
|
444
|
+
};
|
|
445
|
+
}
|
|
436
446
|
readConfig(source = this.bridge.source()) {
|
|
437
447
|
const wire = {};
|
|
438
448
|
for (const key of Object.keys(CONFIG_KEYS)) {
|
|
@@ -446,7 +456,18 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
446
456
|
const source = this.bridge.source();
|
|
447
457
|
return {
|
|
448
458
|
config: this.readConfig(source),
|
|
449
|
-
legacyKeys: detectLegacyKeys(source)
|
|
459
|
+
legacyKeys: detectLegacyKeys(source),
|
|
460
|
+
seeds: this.seeds.wireStatus(this.seedsIo())
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
seedsIo() {
|
|
464
|
+
const settings = this.settings;
|
|
465
|
+
return {
|
|
466
|
+
read: () => this.bridge.source(),
|
|
467
|
+
writeRoles: (roles) => {
|
|
468
|
+
if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
469
|
+
return settings.update(FALLBACKS_SETTINGS_NAMESPACE, { roles });
|
|
470
|
+
}
|
|
450
471
|
};
|
|
451
472
|
}
|
|
452
473
|
};
|
|
@@ -514,6 +535,21 @@ function fallbacksTypertContribution() {
|
|
|
514
535
|
invocation: { kind: "direct" },
|
|
515
536
|
parameters: [],
|
|
516
537
|
result: { mode: "src-json" }
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: "dsh-llm-fallbacks#fallbacks/revert-seed",
|
|
541
|
+
service: "fallbacks",
|
|
542
|
+
namespace: "fallbacks",
|
|
543
|
+
method: "revert-seed",
|
|
544
|
+
implementation: "revertSeed",
|
|
545
|
+
invocation: { kind: "direct" },
|
|
546
|
+
parameters: [{
|
|
547
|
+
name: "id",
|
|
548
|
+
wire: "id",
|
|
549
|
+
source: "json",
|
|
550
|
+
codec: { mode: "src-json" }
|
|
551
|
+
}],
|
|
552
|
+
result: { mode: "src-json" }
|
|
517
553
|
}
|
|
518
554
|
]
|
|
519
555
|
};
|
|
@@ -644,6 +680,188 @@ function registerFallbacksCommands(registry, controller, locale = "zh") {
|
|
|
644
680
|
});
|
|
645
681
|
}
|
|
646
682
|
//#endregion
|
|
683
|
+
//#region .build/host/seeds.js
|
|
684
|
+
var FallbacksSeedManager = class {
|
|
685
|
+
logger;
|
|
686
|
+
registry = new Map();
|
|
687
|
+
constructor(logger) {
|
|
688
|
+
this.logger = logger;
|
|
689
|
+
}
|
|
690
|
+
async declare(seeds, io) {
|
|
691
|
+
const outcome = {
|
|
692
|
+
applied: [],
|
|
693
|
+
skipped: [],
|
|
694
|
+
conflicts: []
|
|
695
|
+
};
|
|
696
|
+
const registry = new Map();
|
|
697
|
+
for (const seed of seeds) {
|
|
698
|
+
if (typeof seed.id !== "string" || !ROLE_ID_PATTERN.test(seed.id)) {
|
|
699
|
+
outcome.skipped.push({
|
|
700
|
+
id: String(seed.id),
|
|
701
|
+
reason: "invalid-id"
|
|
702
|
+
});
|
|
703
|
+
this.warnSkip(seed.id, "invalid-id");
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
if (seed.id === "inherit") {
|
|
707
|
+
outcome.skipped.push({
|
|
708
|
+
id: seed.id,
|
|
709
|
+
reason: "reserved-id"
|
|
710
|
+
});
|
|
711
|
+
this.warnSkip(seed.id, "reserved-id");
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (registry.has(seed.id)) {
|
|
715
|
+
outcome.skipped.push({
|
|
716
|
+
id: seed.id,
|
|
717
|
+
reason: "duplicate-in-batch"
|
|
718
|
+
});
|
|
719
|
+
this.warnSkip(seed.id, "duplicate-in-batch");
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
registry.set(seed.id, seed.persona);
|
|
723
|
+
outcome.applied.push(seed.id);
|
|
724
|
+
}
|
|
725
|
+
const config = io.read();
|
|
726
|
+
const currentList = roleRows(config);
|
|
727
|
+
const currentRules = roleRules(config);
|
|
728
|
+
const newList = materialize(currentList, registry, this.registry, outcome.conflicts);
|
|
729
|
+
for (const conflict of outcome.conflicts) this.logger.warn(`llm-fallbacks: seeds: persona-source conflict for seed id ${JSON.stringify(conflict.id)} — operator row persona kept (never overwritten)`);
|
|
730
|
+
const computed = {
|
|
731
|
+
list: newList,
|
|
732
|
+
rules: currentRules
|
|
733
|
+
};
|
|
734
|
+
if (!deepEqual(newList, currentList)) await io.writeRoles(computed);
|
|
735
|
+
this.registry = registry;
|
|
736
|
+
return outcome;
|
|
737
|
+
}
|
|
738
|
+
effectiveRoles(io) {
|
|
739
|
+
return { roles: roleRows(io.read()).map((row) => {
|
|
740
|
+
const seedPersona = this.registry.get(row.id.trim());
|
|
741
|
+
const seeded = seedPersona !== void 0;
|
|
742
|
+
const effective = {
|
|
743
|
+
id: row.id,
|
|
744
|
+
persona: row.persona,
|
|
745
|
+
seeded,
|
|
746
|
+
personaOverridden: seeded && row.persona !== seedPersona
|
|
747
|
+
};
|
|
748
|
+
if (seeded) effective.seedPersona = seedPersona;
|
|
749
|
+
if (row.chain !== void 0) effective.chain = row.chain;
|
|
750
|
+
if (row.fallback !== void 0) effective.fallback = row.fallback;
|
|
751
|
+
return effective;
|
|
752
|
+
}) };
|
|
753
|
+
}
|
|
754
|
+
wireStatus(io) {
|
|
755
|
+
const status = [];
|
|
756
|
+
for (const row of roleRows(io.read())) {
|
|
757
|
+
const seedPersona = this.registry.get(row.id.trim());
|
|
758
|
+
if (seedPersona === void 0) continue;
|
|
759
|
+
status.push({
|
|
760
|
+
id: row.id,
|
|
761
|
+
overridden: row.persona !== seedPersona
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
return status;
|
|
765
|
+
}
|
|
766
|
+
async revert(id, io) {
|
|
767
|
+
const seedId = id.trim();
|
|
768
|
+
const seedPersona = this.registry.get(seedId);
|
|
769
|
+
if (seedPersona === void 0) return {
|
|
770
|
+
reverted: false,
|
|
771
|
+
reason: "not-seeded"
|
|
772
|
+
};
|
|
773
|
+
const config = io.read();
|
|
774
|
+
const rows = roleRows(config);
|
|
775
|
+
const rules = roleRules(config);
|
|
776
|
+
const index = rows.findIndex((row) => row.id.trim() === seedId);
|
|
777
|
+
if (index === -1) return {
|
|
778
|
+
reverted: false,
|
|
779
|
+
reason: "row-absent"
|
|
780
|
+
};
|
|
781
|
+
if (rows[index].persona === seedPersona) return {
|
|
782
|
+
reverted: true,
|
|
783
|
+
persona: seedPersona
|
|
784
|
+
};
|
|
785
|
+
const nextList = rows.map((row, i) => i === index ? {
|
|
786
|
+
...row,
|
|
787
|
+
persona: seedPersona
|
|
788
|
+
} : row);
|
|
789
|
+
await io.writeRoles({
|
|
790
|
+
list: nextList,
|
|
791
|
+
rules
|
|
792
|
+
});
|
|
793
|
+
return {
|
|
794
|
+
reverted: true,
|
|
795
|
+
persona: seedPersona
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
warnSkip(id, reason) {
|
|
799
|
+
const shown = typeof id === "string" ? JSON.stringify(id) : String(id);
|
|
800
|
+
if (reason === "invalid-id") this.logger.warn(`llm-fallbacks: seeds: skipping seed id ${shown} — invalid-id (must match ${String(ROLE_ID_PATTERN)} as declared)`);
|
|
801
|
+
else if (reason === "reserved-id") this.logger.warn(`llm-fallbacks: seeds: skipping seed id ${shown} — reserved-id ("${INHERIT_ROLE_ID}" is not a legal seed target)`);
|
|
802
|
+
else this.logger.warn(`llm-fallbacks: seeds: skipping seed id ${shown} — duplicate-in-batch (first wins)`);
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
function materialize(rows, registry, previous, conflicts) {
|
|
806
|
+
const next = [];
|
|
807
|
+
for (const row of rows) {
|
|
808
|
+
const seedId = row.id.trim();
|
|
809
|
+
const incoming = registry.get(seedId);
|
|
810
|
+
if (incoming === void 0) {
|
|
811
|
+
next.push(row);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const prior = previous.get(seedId);
|
|
815
|
+
if (prior === void 0) {
|
|
816
|
+
if (row.persona !== incoming) conflicts.push({
|
|
817
|
+
id: seedId,
|
|
818
|
+
kind: "persona-source"
|
|
819
|
+
});
|
|
820
|
+
next.push(row);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (row.persona === prior) {
|
|
824
|
+
next.push({
|
|
825
|
+
...row,
|
|
826
|
+
persona: incoming
|
|
827
|
+
});
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if (row.persona !== incoming) conflicts.push({
|
|
831
|
+
id: seedId,
|
|
832
|
+
kind: "persona-source"
|
|
833
|
+
});
|
|
834
|
+
next.push(row);
|
|
835
|
+
}
|
|
836
|
+
for (const [id, persona] of registry) if (!rows.some((row) => row.id.trim() === id)) next.push({
|
|
837
|
+
id,
|
|
838
|
+
persona
|
|
839
|
+
});
|
|
840
|
+
return next;
|
|
841
|
+
}
|
|
842
|
+
function roleRows(config) {
|
|
843
|
+
const list = config.roles?.list;
|
|
844
|
+
return Array.isArray(list) ? list : [];
|
|
845
|
+
}
|
|
846
|
+
function roleRules(config) {
|
|
847
|
+
const rules = config.roles?.rules;
|
|
848
|
+
return Array.isArray(rules) ? rules : [];
|
|
849
|
+
}
|
|
850
|
+
function deepEqual(a, b) {
|
|
851
|
+
if (a === b) return true;
|
|
852
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
853
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
854
|
+
if (Array.isArray(a)) {
|
|
855
|
+
const aa = a;
|
|
856
|
+
const bb = b;
|
|
857
|
+
return aa.length === bb.length && aa.every((item, index) => deepEqual(item, bb[index]));
|
|
858
|
+
}
|
|
859
|
+
const aKeys = Object.keys(a);
|
|
860
|
+
const bKeys = Object.keys(b);
|
|
861
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
862
|
+
return aKeys.every((key) => deepEqual(a[key], b[key]));
|
|
863
|
+
}
|
|
864
|
+
//#endregion
|
|
647
865
|
//#region .build/host/index.js
|
|
648
866
|
const name = "llm-fallbacks";
|
|
649
867
|
const provide = ["llm-fallbacks"];
|
|
@@ -695,6 +913,23 @@ function overrideConfig(seed, to) {
|
|
|
695
913
|
function apply(ctx, config = defaultFallbacksConfig) {
|
|
696
914
|
const logger = ctx.logger("llm-fallbacks");
|
|
697
915
|
const entry = Config(config);
|
|
916
|
+
const seeds = new FallbacksSeedManager(logger);
|
|
917
|
+
const seedsSettingsUnavailable = "llm-fallbacks: seeds: settings service is unavailable — seed roles cannot be written";
|
|
918
|
+
let writeRoles = () => {
|
|
919
|
+
throw new Error(seedsSettingsUnavailable);
|
|
920
|
+
};
|
|
921
|
+
ctx.inject(["settings"], (sctx) => {
|
|
922
|
+
writeRoles = (roles) => sctx.settings.update(FALLBACKS_SETTINGS_NAMESPACE, { roles });
|
|
923
|
+
return () => {
|
|
924
|
+
writeRoles = () => {
|
|
925
|
+
throw new Error(seedsSettingsUnavailable);
|
|
926
|
+
};
|
|
927
|
+
};
|
|
928
|
+
});
|
|
929
|
+
const seedsIo = {
|
|
930
|
+
read: () => source(),
|
|
931
|
+
writeRoles: (roles) => writeRoles(roles)
|
|
932
|
+
};
|
|
698
933
|
try {
|
|
699
934
|
ctx.provide("llm-fallbacks", {
|
|
700
935
|
name: "llm-fallbacks",
|
|
@@ -702,7 +937,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
702
937
|
resolveRole,
|
|
703
938
|
resolveChain,
|
|
704
939
|
validateFallbacksConfig,
|
|
705
|
-
detectLegacyKeys
|
|
940
|
+
detectLegacyKeys,
|
|
941
|
+
declareSeeds: (declarations) => seeds.declare(declarations, seedsIo),
|
|
942
|
+
getEffectiveRoles: () => seeds.effectiveRoles(seedsIo),
|
|
943
|
+
revertSeededPersona: (id) => seeds.revert(id, seedsIo)
|
|
706
944
|
});
|
|
707
945
|
} catch (error) {
|
|
708
946
|
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
@@ -726,7 +964,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
726
964
|
});
|
|
727
965
|
const bridge = { source: () => source() };
|
|
728
966
|
try {
|
|
729
|
-
new FallbacksConfigGateway(ctx, bridge);
|
|
967
|
+
new FallbacksConfigGateway(ctx, bridge, seeds);
|
|
730
968
|
} catch (error) {
|
|
731
969
|
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
732
970
|
ctx.logger("llm-fallbacks").debug("fallbacks gateway already registered — no gateway on this fiber (multi-fiber dedupe)");
|
|
@@ -861,4 +1099,4 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
861
1099
|
});
|
|
862
1100
|
}
|
|
863
1101
|
//#endregion
|
|
864
|
-
export { Config, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };
|
|
1102
|
+
export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };
|
package/dist/seeds.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-seeds domain module (plan fallbacks-role-seeds Task 1).
|
|
3
|
+
*
|
|
4
|
+
* A companion plugin declares `[{ id, persona }]` seeds; this module
|
|
5
|
+
* validates each id AS DECLARED (`ROLE_ID_PATTERN`, reserved `inherit`),
|
|
6
|
+
* merges the declarations into the role taxonomy with immutable-id /
|
|
7
|
+
* override / revert semantics, and writes through a narrow IO seam
|
|
8
|
+
* (`SeedsIo`). It is free of `@deepseek-ai/*` value imports (bundle
|
|
9
|
+
* purity gate) — the client may type-only import from here.
|
|
10
|
+
*
|
|
11
|
+
* State model (spec §9.2) — two stores, strictly separated:
|
|
12
|
+
* 1. operator config rows (persisted; a seeded role is a plain
|
|
13
|
+
* `roles.list` row `{ id, persona }`), and
|
|
14
|
+
* 2. an in-memory per-apply seed registry (`Map<id, seedPersona>`),
|
|
15
|
+
* declare = replacement (the batch is the companion's full current set).
|
|
16
|
+
* `seeded` / `personaOverridden` are DERIVED at read time, never stored —
|
|
17
|
+
* a config round-trip cannot orphan an override (AC-3).
|
|
18
|
+
*
|
|
19
|
+
* Materialization (spec §9.2): append `{ id, persona }` (two keys only,
|
|
20
|
+
* R4) / attach row untouched / at-default tracking / override preserved +
|
|
21
|
+
* `'persona-source'` conflict / omitted id drops from the registry while
|
|
22
|
+
* the row stays (R2). No delta → no settings write (idempotent, AC-1).
|
|
23
|
+
* Compute → write → commit registry: a failed write throws and leaves the
|
|
24
|
+
* registry unchanged (retry-safe).
|
|
25
|
+
*
|
|
26
|
+
* @module dsh-llm-fallbacks/seeds
|
|
27
|
+
*/
|
|
28
|
+
import { type FallbackStrategy, type FallbacksConfig, type FallbacksConfigLogger, type FallbacksRoles } from './config.ts';
|
|
29
|
+
/** A seed declaration from a companion plugin (spec §9.1). */
|
|
30
|
+
export interface SeedDeclaration {
|
|
31
|
+
id: string;
|
|
32
|
+
persona: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Why one declared id was skipped (spec §9.1). Per-id skip + warn, never
|
|
36
|
+
* coercion — valid siblings in the same batch still apply (AC-5).
|
|
37
|
+
*/
|
|
38
|
+
export type SeedSkipReason = 'invalid-id' | 'reserved-id' | 'duplicate-in-batch';
|
|
39
|
+
/**
|
|
40
|
+
* A loud, non-destructive conflict (AC-2): never silently duplicated or
|
|
41
|
+
* merged.
|
|
42
|
+
*/
|
|
43
|
+
export interface SeedConflict {
|
|
44
|
+
id: string;
|
|
45
|
+
/** Existing row persona differs from the seed default — operator override retained, never overwritten. */
|
|
46
|
+
kind: 'persona-source';
|
|
47
|
+
}
|
|
48
|
+
/** Structured result of one `declare()` (spec §9.1) — the readable status channel. */
|
|
49
|
+
export interface SeedDeclareOutcome {
|
|
50
|
+
applied: string[];
|
|
51
|
+
skipped: Array<{
|
|
52
|
+
id: string;
|
|
53
|
+
reason: SeedSkipReason;
|
|
54
|
+
}>;
|
|
55
|
+
conflicts: SeedConflict[];
|
|
56
|
+
}
|
|
57
|
+
/** Effective role readback entry (spec §9.1) — `chain`/`fallback` passthrough (R4). */
|
|
58
|
+
export interface EffectiveRole {
|
|
59
|
+
/** The config row id (raw declared form). */
|
|
60
|
+
id: string;
|
|
61
|
+
/** Effective row persona. */
|
|
62
|
+
persona: string;
|
|
63
|
+
/** Passthrough — never touched by seeds (R4). */
|
|
64
|
+
chain?: string[];
|
|
65
|
+
/** Passthrough — never touched by seeds (R4). */
|
|
66
|
+
fallback?: FallbackStrategy;
|
|
67
|
+
/** Id is in the live declaration set (trimmed row-id match). */
|
|
68
|
+
seeded: boolean;
|
|
69
|
+
/** `seeded` && row persona !== current seed default. */
|
|
70
|
+
personaOverridden: boolean;
|
|
71
|
+
/** Present iff seeded. */
|
|
72
|
+
seedPersona?: string;
|
|
73
|
+
}
|
|
74
|
+
/** Service readback (b): effective taxonomy with seed annotations. */
|
|
75
|
+
export interface EffectiveRolesReadback {
|
|
76
|
+
roles: EffectiveRole[];
|
|
77
|
+
}
|
|
78
|
+
export type SeedRevertFailReason = 'not-seeded' | 'row-absent' | 'settings-unavailable';
|
|
79
|
+
export interface SeedRevertOutcome {
|
|
80
|
+
reverted: boolean;
|
|
81
|
+
/** Restored current seed default — present iff reverted. */
|
|
82
|
+
persona?: string;
|
|
83
|
+
reason?: SeedRevertFailReason;
|
|
84
|
+
}
|
|
85
|
+
/** Gateway wire entry (card badge state, spec §9.4). */
|
|
86
|
+
export interface SeedsWireStatus {
|
|
87
|
+
id: string;
|
|
88
|
+
overridden: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The narrow IO seam this module writes through — keeps `src/seeds.ts`
|
|
92
|
+
* free of `@deepseek-ai/*` value imports (bundle purity gate).
|
|
93
|
+
*/
|
|
94
|
+
export interface SeedsIo {
|
|
95
|
+
/** Fresh composed config read (the same source the gateway reads). */
|
|
96
|
+
read(): FallbacksConfig;
|
|
97
|
+
/** Persist a full `{ list, rules }` to the settings user layer. */
|
|
98
|
+
writeRoles(roles: FallbacksRoles): Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* In-memory per-apply seed manager (spec §9.2): declare / readback /
|
|
102
|
+
* revert over the operator config through a `SeedsIo` seam. Created in
|
|
103
|
+
* `apply()` (per-apply, no module-level global) with a structured logger;
|
|
104
|
+
* warn messages carry the `llm-fallbacks: seeds:` prefix (spec §9.7).
|
|
105
|
+
*/
|
|
106
|
+
export declare class FallbacksSeedManager {
|
|
107
|
+
private readonly logger;
|
|
108
|
+
private registry;
|
|
109
|
+
constructor(logger: FallbacksConfigLogger);
|
|
110
|
+
/**
|
|
111
|
+
* Declare seeds with replacement semantics — the batch is the
|
|
112
|
+
* companion's FULL current declaration set; ids omitted from the batch
|
|
113
|
+
* drop out of the registry while their rows remain (R2).
|
|
114
|
+
*
|
|
115
|
+
* Per-id validation AS DECLARED (spec §9.3): non-string / pattern miss /
|
|
116
|
+
* reserved `inherit` / duplicate-in-batch → skip + warn; valid siblings
|
|
117
|
+
* still apply (AC-5). Materializes per spec §9.2, writes only when the
|
|
118
|
+
* computed `{ list, rules }` differs from the current composed roles
|
|
119
|
+
* (idempotent, AC-1), and commits the registry only after a successful
|
|
120
|
+
* write (compute → write → commit; retry-safe).
|
|
121
|
+
*/
|
|
122
|
+
declare(seeds: readonly SeedDeclaration[], io: SeedsIo): Promise<SeedDeclareOutcome>;
|
|
123
|
+
/**
|
|
124
|
+
* Readback (b) — sync, derived: every config row annotated with
|
|
125
|
+
* `seeded` / `personaOverridden` / `seedPersona` (trimmed row-id
|
|
126
|
+
* membership in the live declaration set; persona inequality). Nothing
|
|
127
|
+
* override-shaped is stored, so a config round-trip cannot orphan state.
|
|
128
|
+
*/
|
|
129
|
+
effectiveRoles(io: SeedsIo): EffectiveRolesReadback;
|
|
130
|
+
/** Card badge state (spec §9.4): seeded rows, with the override flag. */
|
|
131
|
+
wireStatus(io: SeedsIo): SeedsWireStatus[];
|
|
132
|
+
/**
|
|
133
|
+
* Revert one id to the CURRENT declared seed default (AC-3). Writes
|
|
134
|
+
* persona only — the row is otherwise copied verbatim (R4). Ids absent
|
|
135
|
+
* from the registry (`not-seeded`) or with a deleted row (`row-absent`)
|
|
136
|
+
* return a non-reverted outcome without throwing; a failed settings
|
|
137
|
+
* write propagates loudly (spec §9.1).
|
|
138
|
+
*/
|
|
139
|
+
revert(id: string, io: SeedsIo): Promise<SeedRevertOutcome>;
|
|
140
|
+
private warnSkip;
|
|
141
|
+
}
|