dsh-llm-fallbacks 0.4.2 → 0.4.4
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 +44 -16
- package/README.zh-CN.md +44 -16
- package/dist/chains.d.ts +24 -5
- package/dist/client/SubagentRoleBadge.d.ts +23 -0
- package/dist/client/fallbacks-store.d.ts +58 -17
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +368 -195
- package/dist/client/locales.d.ts +14 -6
- package/dist/config.d.ts +1 -1
- package/dist/events.d.ts +5 -3
- package/dist/gateway.d.ts +55 -2
- package/dist/index.d.ts +34 -5
- package/dist/index.js +204 -67
- package/dist/presets.d.ts +7 -4
- package/dist/seeds.d.ts +61 -7
- package/dist/state.d.ts +20 -0
- package/dist/time-slots.d.ts +19 -10
- package/dist/tui-settings.d.ts +8 -6
- package/dist/tui.d.ts +12 -5
- package/package.json +44 -24
- package/scripts/build-client.ts +15 -6
- package/scripts/prepare-release.ts +1 -1
- package/scripts/repair-fallbacks-switch-logs.ts +97 -129
package/dist/index.js
CHANGED
|
@@ -34,6 +34,9 @@ function resolveWildcardEntry(failingModel, provider) {
|
|
|
34
34
|
raw: `${provider}/${failingModel}`
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
function isOfficialAllDayId(id) {
|
|
38
|
+
return id === "deepseek-official/deepseek-flash" || id === "deepseek-official/deepseek-pro";
|
|
39
|
+
}
|
|
37
40
|
const PRESETS = {
|
|
38
41
|
"liang-peak": {
|
|
39
42
|
windows: [{
|
|
@@ -220,7 +223,7 @@ function labelOf(row) {
|
|
|
220
223
|
function isAllDayConforming(chain) {
|
|
221
224
|
if (chain.length < 1) return false;
|
|
222
225
|
const tail = chain[chain.length - 1];
|
|
223
|
-
return tail
|
|
226
|
+
return isOfficialAllDayId(tail);
|
|
224
227
|
}
|
|
225
228
|
function resolveEffectiveChain(config, now, tz) {
|
|
226
229
|
const state = resolveSlotState(config, now, tz);
|
|
@@ -359,7 +362,7 @@ function validateFallbacksConfig(config, logger) {
|
|
|
359
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`);
|
|
360
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"`);
|
|
361
364
|
}
|
|
362
|
-
if (config.rootChain.length > 0 && !isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: rootChain must end with exactly one official
|
|
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");
|
|
363
366
|
for (const entry of config.rootChain) try {
|
|
364
367
|
parseSelector(entry);
|
|
365
368
|
} catch (error) {
|
|
@@ -618,18 +621,25 @@ function hasWildcardEntry(roles, rootChain, role) {
|
|
|
618
621
|
} catch {}
|
|
619
622
|
return false;
|
|
620
623
|
}
|
|
624
|
+
function fitsContextWindow(candidate, minContextWindow, contextWindowOf) {
|
|
625
|
+
if (minContextWindow === void 0 || contextWindowOf === void 0) return true;
|
|
626
|
+
if (candidate.model === void 0) return true;
|
|
627
|
+
const candidateWindow = contextWindowOf(candidate.provider, candidate.model);
|
|
628
|
+
return candidateWindow === void 0 || candidateWindow > minContextWindow;
|
|
629
|
+
}
|
|
621
630
|
function createCandidateFilter(options) {
|
|
622
|
-
const { current, cooldown, failed, modelExists } = options;
|
|
631
|
+
const { current, cooldown, failed, modelExists, minContextWindow, contextWindowOf } = options;
|
|
623
632
|
return (candidate) => {
|
|
624
633
|
if (candidate.provider === current.provider && candidate.model === current.model) return false;
|
|
625
634
|
if (cooldown.isSuppressed(selectorKey(candidate.provider, candidate.model))) return false;
|
|
626
635
|
if (failed.has(selectorKey(candidate.provider, candidate.model))) return false;
|
|
636
|
+
if (!fitsContextWindow(candidate, minContextWindow, contextWindowOf)) return false;
|
|
627
637
|
if (modelExists && candidate.model !== void 0 && !modelExists(candidate.provider, candidate.model)) return false;
|
|
628
638
|
return true;
|
|
629
639
|
};
|
|
630
640
|
}
|
|
631
641
|
function annotateCandidates(candidates, surviving, options) {
|
|
632
|
-
const { current, cooldown, failed } = options;
|
|
642
|
+
const { current, cooldown, failed, minContextWindow, contextWindowOf } = options;
|
|
633
643
|
const usable = new Set(surviving.map((candidate) => selectorKey(candidate.provider, candidate.model)));
|
|
634
644
|
return candidates.map((candidate) => {
|
|
635
645
|
if (candidate.provider === current.provider && candidate.model === current.model) return {
|
|
@@ -646,6 +656,10 @@ function annotateCandidates(candidates, surviving, options) {
|
|
|
646
656
|
candidate,
|
|
647
657
|
skip: "step-failed"
|
|
648
658
|
};
|
|
659
|
+
if (!fitsContextWindow(candidate, minContextWindow, contextWindowOf)) return {
|
|
660
|
+
candidate,
|
|
661
|
+
skip: "context-window"
|
|
662
|
+
};
|
|
649
663
|
return {
|
|
650
664
|
candidate,
|
|
651
665
|
skip: "missing-id"
|
|
@@ -954,6 +968,7 @@ function overrideConfigWithRouteRule(seed, to, explicitEffort) {
|
|
|
954
968
|
//#endregion
|
|
955
969
|
//#region .build/host/gateway.js
|
|
956
970
|
const FALLBACKS_SETTINGS_NAMESPACE = "fallbacks";
|
|
971
|
+
const SUBAGENT_ROLES_MAX_IDS = 256;
|
|
957
972
|
const CONFIG_KEYS = {
|
|
958
973
|
enabled: true,
|
|
959
974
|
triggerCodes: true,
|
|
@@ -988,11 +1003,13 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
988
1003
|
seeds;
|
|
989
1004
|
settings;
|
|
990
1005
|
subagentPolicy;
|
|
991
|
-
|
|
1006
|
+
subagentRolesSnapshot;
|
|
1007
|
+
constructor(ctx, bridge, seeds, subagentPolicy, subagentRoles) {
|
|
992
1008
|
super(ctx, "fallbacks");
|
|
993
1009
|
this.bridge = bridge;
|
|
994
1010
|
this.seeds = seeds;
|
|
995
1011
|
this.subagentPolicy = subagentPolicy;
|
|
1012
|
+
this.subagentRolesSnapshot = subagentRoles;
|
|
996
1013
|
ctx.inject(["settings"], (sctx) => {
|
|
997
1014
|
this.settings = sctx.settings;
|
|
998
1015
|
return () => {
|
|
@@ -1027,6 +1044,23 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1027
1044
|
outcome
|
|
1028
1045
|
};
|
|
1029
1046
|
}
|
|
1047
|
+
subagentRoles(ids) {
|
|
1048
|
+
if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string")) throw new TypeError("dsh-llm-fallbacks: subagent-roles ids must be an array of strings");
|
|
1049
|
+
if (ids.length > SUBAGENT_ROLES_MAX_IDS) throw new TypeError(`dsh-llm-fallbacks: subagent-roles ids batch exceeds ${SUBAGENT_ROLES_MAX_IDS} entries`);
|
|
1050
|
+
if (this.subagentRolesSnapshot === void 0) return {};
|
|
1051
|
+
let records;
|
|
1052
|
+
try {
|
|
1053
|
+
records = this.subagentRolesSnapshot();
|
|
1054
|
+
} catch {
|
|
1055
|
+
return {};
|
|
1056
|
+
}
|
|
1057
|
+
const projected = new Map();
|
|
1058
|
+
for (const id of ids) {
|
|
1059
|
+
const record = records.get(id);
|
|
1060
|
+
if (record !== void 0) projected.set(id, projectSubagentRoleRecord(record));
|
|
1061
|
+
}
|
|
1062
|
+
return Object.fromEntries(projected);
|
|
1063
|
+
}
|
|
1030
1064
|
readConfig(source = this.bridge.source()) {
|
|
1031
1065
|
const wire = {};
|
|
1032
1066
|
for (const key of Object.keys(CONFIG_KEYS)) {
|
|
@@ -1093,6 +1127,17 @@ function projectSubagentPolicy(snapshot) {
|
|
|
1093
1127
|
};
|
|
1094
1128
|
return wire;
|
|
1095
1129
|
}
|
|
1130
|
+
function projectSubagentRoleRecord(record) {
|
|
1131
|
+
const wire = {
|
|
1132
|
+
role: record.role,
|
|
1133
|
+
at: record.at
|
|
1134
|
+
};
|
|
1135
|
+
if (record.model !== void 0) wire.model = {
|
|
1136
|
+
provider: record.model.provider,
|
|
1137
|
+
model: record.model.model
|
|
1138
|
+
};
|
|
1139
|
+
return wire;
|
|
1140
|
+
}
|
|
1096
1141
|
function normalizeRoles(value) {
|
|
1097
1142
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
1098
1143
|
const roles = {};
|
|
@@ -1118,7 +1163,7 @@ function validateConfigPatch(patch) {
|
|
|
1118
1163
|
}
|
|
1119
1164
|
if (key === "rootChain") {
|
|
1120
1165
|
const chain = patch[key];
|
|
1121
|
-
if (chain !== null && chain !== void 0 && (!Array.isArray(chain) || !isAllDayConforming(chain))) throw new Error("dsh-llm-fallbacks: rootChain must end with exactly one official
|
|
1166
|
+
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)");
|
|
1122
1167
|
}
|
|
1123
1168
|
}
|
|
1124
1169
|
Config(patch);
|
|
@@ -1207,6 +1252,21 @@ function fallbacksTypertContribution() {
|
|
|
1207
1252
|
codec: { mode: "src-json" }
|
|
1208
1253
|
}],
|
|
1209
1254
|
result: { mode: "src-json" }
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
id: "dsh-llm-fallbacks#fallbacks/subagent-roles",
|
|
1258
|
+
service: "fallbacks",
|
|
1259
|
+
namespace: "fallbacks",
|
|
1260
|
+
method: "subagent-roles",
|
|
1261
|
+
implementation: "subagentRoles",
|
|
1262
|
+
invocation: { kind: "direct" },
|
|
1263
|
+
parameters: [{
|
|
1264
|
+
name: "ids",
|
|
1265
|
+
wire: "ids",
|
|
1266
|
+
source: "json",
|
|
1267
|
+
codec: { mode: "src-json" }
|
|
1268
|
+
}],
|
|
1269
|
+
result: { mode: "src-json" }
|
|
1210
1270
|
}
|
|
1211
1271
|
]
|
|
1212
1272
|
};
|
|
@@ -1544,13 +1604,14 @@ var FallbacksSeedManager = class {
|
|
|
1544
1604
|
constructor(logger) {
|
|
1545
1605
|
this.logger = logger;
|
|
1546
1606
|
}
|
|
1547
|
-
async declare(seeds, io) {
|
|
1607
|
+
async declare(seeds, io, options) {
|
|
1548
1608
|
const outcome = {
|
|
1549
1609
|
applied: [],
|
|
1550
1610
|
skipped: [],
|
|
1551
1611
|
conflicts: []
|
|
1552
1612
|
};
|
|
1553
|
-
const
|
|
1613
|
+
const source = resolveSource(options, this.logger);
|
|
1614
|
+
const entries = new Map();
|
|
1554
1615
|
for (const seed of seeds) {
|
|
1555
1616
|
if (typeof seed.id !== "string" || !ROLE_ID_PATTERN.test(seed.id)) {
|
|
1556
1617
|
outcome.skipped.push({
|
|
@@ -1568,7 +1629,7 @@ var FallbacksSeedManager = class {
|
|
|
1568
1629
|
this.warnSkip(seed.id, "reserved-id");
|
|
1569
1630
|
continue;
|
|
1570
1631
|
}
|
|
1571
|
-
if (
|
|
1632
|
+
if (entries.has(seed.id)) {
|
|
1572
1633
|
outcome.skipped.push({
|
|
1573
1634
|
id: seed.id,
|
|
1574
1635
|
reason: "duplicate-in-batch"
|
|
@@ -1576,33 +1637,37 @@ var FallbacksSeedManager = class {
|
|
|
1576
1637
|
this.warnSkip(seed.id, "duplicate-in-batch");
|
|
1577
1638
|
continue;
|
|
1578
1639
|
}
|
|
1579
|
-
|
|
1640
|
+
entries.set(seed.id, {
|
|
1641
|
+
persona: seed.persona,
|
|
1642
|
+
set: source
|
|
1643
|
+
});
|
|
1580
1644
|
outcome.applied.push(seed.id);
|
|
1581
1645
|
}
|
|
1582
1646
|
const config = io.read();
|
|
1583
1647
|
const currentList = roleRows(config);
|
|
1584
1648
|
const currentRules = roleRules(config);
|
|
1585
|
-
const newList = materialize(currentList,
|
|
1649
|
+
const newList = materialize(currentList, personaView(entries), personaView(this.registry), outcome.conflicts);
|
|
1586
1650
|
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)`);
|
|
1587
1651
|
const computed = {
|
|
1588
1652
|
list: newList,
|
|
1589
1653
|
rules: currentRules
|
|
1590
1654
|
};
|
|
1591
1655
|
if (!deepEqual(newList, currentList)) await io.writeRoles(computed);
|
|
1592
|
-
this.registry =
|
|
1656
|
+
this.registry = entries;
|
|
1593
1657
|
return outcome;
|
|
1594
1658
|
}
|
|
1595
1659
|
effectiveRoles(io) {
|
|
1596
1660
|
return { roles: roleRows(io.read()).map((row) => {
|
|
1597
|
-
const
|
|
1598
|
-
const seeded =
|
|
1661
|
+
const entry = this.registry.get(row.id.trim());
|
|
1662
|
+
const seeded = entry !== void 0;
|
|
1599
1663
|
const effective = {
|
|
1600
1664
|
id: row.id,
|
|
1601
1665
|
persona: row.persona,
|
|
1602
1666
|
seeded,
|
|
1603
|
-
personaOverridden: seeded && row.persona !==
|
|
1667
|
+
personaOverridden: seeded && row.persona !== entry.persona,
|
|
1668
|
+
source: seeded ? entry.set : "user"
|
|
1604
1669
|
};
|
|
1605
|
-
if (seeded) effective.seedPersona =
|
|
1670
|
+
if (seeded) effective.seedPersona = entry.persona;
|
|
1606
1671
|
if (row.chain !== void 0) effective.chain = row.chain;
|
|
1607
1672
|
if (row.fallback !== void 0) effective.fallback = row.fallback;
|
|
1608
1673
|
return effective;
|
|
@@ -1611,19 +1676,20 @@ var FallbacksSeedManager = class {
|
|
|
1611
1676
|
wireStatus(io) {
|
|
1612
1677
|
const status = [];
|
|
1613
1678
|
for (const row of roleRows(io.read())) {
|
|
1614
|
-
const
|
|
1615
|
-
if (
|
|
1679
|
+
const entry = this.registry.get(row.id.trim());
|
|
1680
|
+
if (entry === void 0) continue;
|
|
1616
1681
|
status.push({
|
|
1617
1682
|
id: row.id,
|
|
1618
|
-
overridden: row.persona !==
|
|
1683
|
+
overridden: row.persona !== entry.persona,
|
|
1684
|
+
source: entry.set
|
|
1619
1685
|
});
|
|
1620
1686
|
}
|
|
1621
1687
|
return status;
|
|
1622
1688
|
}
|
|
1623
1689
|
async revert(id, io) {
|
|
1624
1690
|
const seedId = id.trim();
|
|
1625
|
-
const
|
|
1626
|
-
if (
|
|
1691
|
+
const entry = this.registry.get(seedId);
|
|
1692
|
+
if (entry === void 0) return {
|
|
1627
1693
|
reverted: false,
|
|
1628
1694
|
reason: "not-seeded"
|
|
1629
1695
|
};
|
|
@@ -1635,13 +1701,13 @@ var FallbacksSeedManager = class {
|
|
|
1635
1701
|
reverted: false,
|
|
1636
1702
|
reason: "row-absent"
|
|
1637
1703
|
};
|
|
1638
|
-
if (rows[index].persona ===
|
|
1704
|
+
if (rows[index].persona === entry.persona) return {
|
|
1639
1705
|
reverted: true,
|
|
1640
|
-
persona:
|
|
1706
|
+
persona: entry.persona
|
|
1641
1707
|
};
|
|
1642
1708
|
const nextList = rows.map((row, i) => i === index ? {
|
|
1643
1709
|
...row,
|
|
1644
|
-
persona:
|
|
1710
|
+
persona: entry.persona
|
|
1645
1711
|
} : row);
|
|
1646
1712
|
await io.writeRoles({
|
|
1647
1713
|
list: nextList,
|
|
@@ -1649,7 +1715,7 @@ var FallbacksSeedManager = class {
|
|
|
1649
1715
|
});
|
|
1650
1716
|
return {
|
|
1651
1717
|
reverted: true,
|
|
1652
|
-
persona:
|
|
1718
|
+
persona: entry.persona
|
|
1653
1719
|
};
|
|
1654
1720
|
}
|
|
1655
1721
|
warnSkip(id, reason) {
|
|
@@ -1659,6 +1725,26 @@ var FallbacksSeedManager = class {
|
|
|
1659
1725
|
else this.logger.warn(`llm-fallbacks: seeds: skipping seed id ${shown} — duplicate-in-batch (first wins)`);
|
|
1660
1726
|
}
|
|
1661
1727
|
};
|
|
1728
|
+
const RESERVED_SET_NAMES = [
|
|
1729
|
+
"bundled",
|
|
1730
|
+
"user",
|
|
1731
|
+
"external"
|
|
1732
|
+
];
|
|
1733
|
+
function resolveSource(options, logger) {
|
|
1734
|
+
if (options?.bundled === true) return "bundled";
|
|
1735
|
+
const raw = options?.set;
|
|
1736
|
+
if (raw === void 0) return "external";
|
|
1737
|
+
if (typeof raw === "string" && raw.trim() !== "" && !RESERVED_SET_NAMES.includes(raw.trim())) return raw.trim();
|
|
1738
|
+
const shown = typeof raw === "string" ? JSON.stringify(raw) : String(raw);
|
|
1739
|
+
const reserved = RESERVED_SET_NAMES.map((name) => JSON.stringify(name)).join(" / ");
|
|
1740
|
+
logger.warn(`llm-fallbacks: seeds: ignoring declare set name ${shown} — invalid (empty after trim, non-string, or a reserved label ${reserved}); provenance degrades to "external", the seeds still apply`);
|
|
1741
|
+
return "external";
|
|
1742
|
+
}
|
|
1743
|
+
function personaView(entries) {
|
|
1744
|
+
const personas = new Map();
|
|
1745
|
+
for (const [id, entry] of entries) personas.set(id, entry.persona);
|
|
1746
|
+
return personas;
|
|
1747
|
+
}
|
|
1662
1748
|
function materialize(rows, registry, previous, conflicts) {
|
|
1663
1749
|
const next = [];
|
|
1664
1750
|
for (const row of rows) {
|
|
@@ -1733,14 +1819,6 @@ const presetRoles = [
|
|
|
1733
1819
|
id: "scout",
|
|
1734
1820
|
persona: "Read-only scout for exploratory codebase research, rapid analysis, and broad pattern search. Return compressed, structured findings another agent can reuse without re-reading the tree. Run searches in parallel; if a search is empty, try at least one alternate strategy before concluding the target is absent. Infer thoroughness from the task (quick, medium, or thorough; default medium); never write, edit, or run state-changing commands."
|
|
1735
1821
|
},
|
|
1736
|
-
{
|
|
1737
|
-
id: "designer",
|
|
1738
|
-
persona: "UI/UX specialist for design implementation, review, and visual refinement. Analyze the existing design system first (tokens, theme, and primitives) and compose with it; if none exists, define a minimal system before implementing. Cover loading, empty, error, disabled, hover, and focus states; verify accessibility (contrast, focus rings, semantic HTML) and responsive layout. Avoid generic AI-slop patterns; in review, cite file and line with a concrete issue and a specific fix."
|
|
1739
|
-
},
|
|
1740
|
-
{
|
|
1741
|
-
id: "librarian",
|
|
1742
|
-
persona: "Research specialist for external libraries and APIs who returns definitive, source-verified answers. Treat source as truth, documentation as aspiration, and training data as history; prefer locally installed packages, then official docs. Cross-check at least two locations; copy API signatures verbatim and report the investigated version. Stay read-only on the user's project; if a lookup is empty, try at least two fallback strategies before concluding nothing exists."
|
|
1743
|
-
},
|
|
1744
1822
|
{
|
|
1745
1823
|
id: "reviewer",
|
|
1746
1824
|
persona: "Code-review specialist for quality and security analysis of a patch before merge. Anchor every finding to the assigned diff; report only issues that are provable, actionable, unintentional, and introduced by the patch. For any new type, variant, or value that crosses a module boundary, inspect the consuming-side dispatch point. Rank findings P0 (blocks release) through P3 (nice to have); do not edit files or trigger builds."
|
|
@@ -1983,10 +2061,10 @@ function buildFallbacksTuiSection() {
|
|
|
1983
2061
|
zh: "根降级链(JSON)",
|
|
1984
2062
|
en: "Root chain (JSON)"
|
|
1985
2063
|
},
|
|
1986
|
-
hint: "JSON array of model selectors; the last entry must be an official
|
|
2064
|
+
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 a legal tail but not yet available in the catalog — the settings card shows it disabled.",
|
|
1987
2065
|
hintDescriptions: {
|
|
1988
|
-
zh: "模型选择器 JSON
|
|
1989
|
-
en: "JSON array of model selectors; the last entry must be an official
|
|
2066
|
+
zh: "模型选择器 JSON 数组;末项必须是官方模型(deepseek-official/deepseek-flash 或 deepseek-official/deepseek-pro)。Pro 是合法链尾但目录暂未提供——设置卡中显示为禁用。",
|
|
2067
|
+
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 a legal tail but not yet available in the catalog — the settings card shows it disabled."
|
|
1990
2068
|
},
|
|
1991
2069
|
group: "chain",
|
|
1992
2070
|
kind: "text",
|
|
@@ -2097,7 +2175,13 @@ function installTuiSettingsSection(ctx, opts) {
|
|
|
2097
2175
|
ctx.inject(["tuiSettingsSections"], (tctx) => {
|
|
2098
2176
|
const registry = tctx.tuiSettingsSections;
|
|
2099
2177
|
if (registry === void 0) return;
|
|
2100
|
-
|
|
2178
|
+
try {
|
|
2179
|
+
return registry.register(buildFallbacksTuiSection());
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
|
|
2182
|
+
tctx.logger("llm-fallbacks").debug("llm-fallbacks: tui settings section already registered — no section on this fiber");
|
|
2183
|
+
return () => {};
|
|
2184
|
+
}
|
|
2101
2185
|
});
|
|
2102
2186
|
}
|
|
2103
2187
|
//#endregion
|
|
@@ -2259,6 +2343,8 @@ function installFallbacksAdapter(ctx, readConfig) {
|
|
|
2259
2343
|
const name = "llm-fallbacks";
|
|
2260
2344
|
const provide = ["llm-fallbacks"];
|
|
2261
2345
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
2346
|
+
const CONTEXT_WINDOW_EXCEEDED_CODE = "CONTEXT_WINDOW_EXCEEDED";
|
|
2347
|
+
const REQUEST_SCOPED_CODES = new Set([CONTEXT_WINDOW_EXCEEDED_CODE]);
|
|
2262
2348
|
const stateStores = new WeakMap();
|
|
2263
2349
|
function stateStore(ctx) {
|
|
2264
2350
|
return stateStores.get(ctx);
|
|
@@ -2271,6 +2357,10 @@ const chainHeadStores = new WeakMap();
|
|
|
2271
2357
|
function chainHeads(ctx) {
|
|
2272
2358
|
return chainHeadStores.get(ctx);
|
|
2273
2359
|
}
|
|
2360
|
+
const subagentRoleRecordStores = new WeakMap();
|
|
2361
|
+
function subagentRoleRecords(ctx) {
|
|
2362
|
+
return subagentRoleRecordStores.get(ctx);
|
|
2363
|
+
}
|
|
2274
2364
|
function latestByAt(map) {
|
|
2275
2365
|
let latest;
|
|
2276
2366
|
for (const value of map.values()) if (latest === void 0 || value.at >= latest.at) latest = value;
|
|
@@ -2290,6 +2380,26 @@ async function makeModelExists(ctx, providers) {
|
|
|
2290
2380
|
}));
|
|
2291
2381
|
return (provider, model) => catalog.get(provider)?.has(model) ?? false;
|
|
2292
2382
|
}
|
|
2383
|
+
async function makeContextWindowOf(ctx, routes) {
|
|
2384
|
+
const llm = ctx.get("llm");
|
|
2385
|
+
if (llm === void 0) return () => void 0;
|
|
2386
|
+
const windows = new Map();
|
|
2387
|
+
if (typeof llm.listModels === "function") await Promise.all([...new Set(routes.map((route) => route.provider))].map(async (provider) => {
|
|
2388
|
+
try {
|
|
2389
|
+
for (const model of await llm.listModels(provider)) if (typeof model.contextWindow === "number") windows.set(selectorKey(provider, model.id), model.contextWindow);
|
|
2390
|
+
} catch {}
|
|
2391
|
+
}));
|
|
2392
|
+
const resolveModelInfo = llm.resolveModelInfo?.bind(llm);
|
|
2393
|
+
if (resolveModelInfo !== void 0) await Promise.all(routes.map(async (route) => {
|
|
2394
|
+
const key = selectorKey(route.provider, route.model);
|
|
2395
|
+
if (windows.has(key)) return;
|
|
2396
|
+
try {
|
|
2397
|
+
const window = (await resolveModelInfo(route.provider, route.model))?.context?.contextWindow;
|
|
2398
|
+
if (typeof window === "number") windows.set(key, window);
|
|
2399
|
+
} catch {}
|
|
2400
|
+
}));
|
|
2401
|
+
return (provider, model) => windows.get(selectorKey(provider, model));
|
|
2402
|
+
}
|
|
2293
2403
|
function countRetryEvents(session, turn, step, provider) {
|
|
2294
2404
|
let count = 0;
|
|
2295
2405
|
const events = session.snapshotEvents();
|
|
@@ -2367,6 +2477,24 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2367
2477
|
};
|
|
2368
2478
|
ctx.inject(["settings"], (sctx) => {
|
|
2369
2479
|
writeRoles = (roles) => sctx.settings.update(FALLBACKS_SETTINGS_NAMESPACE, { roles });
|
|
2480
|
+
try {
|
|
2481
|
+
sctx.provide("llm-fallbacks", {
|
|
2482
|
+
name: "llm-fallbacks",
|
|
2483
|
+
version,
|
|
2484
|
+
resolveRole,
|
|
2485
|
+
resolveChain,
|
|
2486
|
+
validateFallbacksConfig,
|
|
2487
|
+
detectLegacyKeys,
|
|
2488
|
+
declareSeeds: (declarations, options) => seeds.declare(declarations, seedsIo, options?.set === void 0 ? void 0 : { set: options.set }),
|
|
2489
|
+
getEffectiveRoles: () => seeds.effectiveRoles(seedsIo),
|
|
2490
|
+
revertSeededPersona: (id) => seeds.revert(id, seedsIo)
|
|
2491
|
+
});
|
|
2492
|
+
serviceOwned = true;
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
serviceOwned = false;
|
|
2495
|
+
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
2496
|
+
ctx.logger("llm-fallbacks").debug("fallbacks service already registered — no service on this fiber (multi-fiber dedupe)");
|
|
2497
|
+
}
|
|
2370
2498
|
return () => {
|
|
2371
2499
|
writeRoles = () => {
|
|
2372
2500
|
throw new Error(seedsSettingsUnavailable);
|
|
@@ -2377,25 +2505,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2377
2505
|
read: () => source(),
|
|
2378
2506
|
writeRoles: (roles) => writeRoles(roles)
|
|
2379
2507
|
};
|
|
2380
|
-
let serviceOwned =
|
|
2381
|
-
try {
|
|
2382
|
-
ctx.provide("llm-fallbacks", {
|
|
2383
|
-
name: "llm-fallbacks",
|
|
2384
|
-
version,
|
|
2385
|
-
resolveRole,
|
|
2386
|
-
resolveChain,
|
|
2387
|
-
validateFallbacksConfig,
|
|
2388
|
-
detectLegacyKeys,
|
|
2389
|
-
declareSeeds: (declarations) => seeds.declare(declarations, seedsIo),
|
|
2390
|
-
getEffectiveRoles: () => seeds.effectiveRoles(seedsIo),
|
|
2391
|
-
revertSeededPersona: (id) => seeds.revert(id, seedsIo)
|
|
2392
|
-
});
|
|
2393
|
-
serviceOwned = true;
|
|
2394
|
-
} catch (error) {
|
|
2395
|
-
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
2396
|
-
serviceOwned = false;
|
|
2397
|
-
ctx.logger("llm-fallbacks").debug("fallbacks service already registered — no service on this fiber (multi-fiber dedupe)");
|
|
2398
|
-
}
|
|
2508
|
+
let serviceOwned = ctx.get("llm-fallbacks") === void 0;
|
|
2399
2509
|
let source = () => entry;
|
|
2400
2510
|
const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source());
|
|
2401
2511
|
validateFallbacksConfig(entry, logger);
|
|
@@ -2422,6 +2532,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2422
2532
|
chainHeadStores.set(ctx, chainHeadMap);
|
|
2423
2533
|
const blockedAttemptMap = new Map();
|
|
2424
2534
|
blockedAttemptStores.set(ctx, blockedAttemptMap);
|
|
2535
|
+
const subagentRoleRecordMap = new Map();
|
|
2536
|
+
subagentRoleRecordStores.set(ctx, subagentRoleRecordMap);
|
|
2425
2537
|
try {
|
|
2426
2538
|
new FallbacksConfigGateway(ctx, bridge, seeds, () => {
|
|
2427
2539
|
try {
|
|
@@ -2436,7 +2548,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2436
2548
|
} catch {
|
|
2437
2549
|
return { policy: { state: "unprovable" } };
|
|
2438
2550
|
}
|
|
2439
|
-
});
|
|
2551
|
+
}, () => subagentRoleRecordMap);
|
|
2440
2552
|
} catch (error) {
|
|
2441
2553
|
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
2442
2554
|
ctx.logger("llm-fallbacks").debug("fallbacks gateway already registered — no gateway on this fiber (multi-fiber dedupe)");
|
|
@@ -2455,7 +2567,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2455
2567
|
const dispatchInjected = new Set();
|
|
2456
2568
|
const slotWinners = new Map();
|
|
2457
2569
|
const lastKnownPolicySettings = new Map();
|
|
2458
|
-
async function decide(agent, turn, step, current, reason, state) {
|
|
2570
|
+
async function decide(agent, turn, step, current, reason, state, failureCode) {
|
|
2459
2571
|
const config = source();
|
|
2460
2572
|
if (state !== void 0) {
|
|
2461
2573
|
states.syncStep(state, turn, step);
|
|
@@ -2468,10 +2580,17 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2468
2580
|
const modelExists = hasWildcardEntry(config.roles.list, rootTail, role) ? await makeModelExists(ctx, [...new Set(all.map((candidate) => candidate.provider))]) : void 0;
|
|
2469
2581
|
const cooldown = { isSuppressed: (key) => state !== void 0 && states.isSuppressed(state, key, Date.now(), source().recovery ?? "timer") };
|
|
2470
2582
|
const failed = { has: (key) => state !== void 0 && state.stepFailures.failed.has(key) };
|
|
2583
|
+
const contextWindowOf = failureCode === CONTEXT_WINDOW_EXCEEDED_CODE ? await makeContextWindowOf(ctx, [current, ...all.flatMap((candidate) => candidate.model === void 0 ? [] : [{
|
|
2584
|
+
provider: candidate.provider,
|
|
2585
|
+
model: candidate.model
|
|
2586
|
+
}])]) : void 0;
|
|
2587
|
+
const minContextWindow = contextWindowOf?.(current.provider, current.model);
|
|
2471
2588
|
const surviving = selectCandidates(all, wildcard, createCandidateFilter({
|
|
2472
2589
|
current,
|
|
2473
2590
|
cooldown,
|
|
2474
|
-
failed
|
|
2591
|
+
failed,
|
|
2592
|
+
minContextWindow,
|
|
2593
|
+
contextWindowOf
|
|
2475
2594
|
}), modelExists);
|
|
2476
2595
|
const first = surviving[0];
|
|
2477
2596
|
let target;
|
|
@@ -2511,7 +2630,9 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2511
2630
|
logger.info("llm-fallbacks: agent \"%s\" fallback switch (降级切换) %s/%s -> %s/%s (role=%s, reason=%s, candidates=%o)", agent.id, current.provider, current.model, target.provider, target.model, role, reason, annotateCandidates(all, surviving, {
|
|
2512
2631
|
current,
|
|
2513
2632
|
cooldown,
|
|
2514
|
-
failed
|
|
2633
|
+
failed,
|
|
2634
|
+
minContextWindow,
|
|
2635
|
+
contextWindowOf
|
|
2515
2636
|
}).map(({ candidate, skip }) => skip === void 0 ? `${candidate.provider}/${candidate.model}` : `${candidate.provider}/${candidate.model} (skipped: ${skip})`));
|
|
2516
2637
|
return {
|
|
2517
2638
|
from: {
|
|
@@ -2529,10 +2650,12 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2529
2650
|
function commit(state, pending, turn, step) {
|
|
2530
2651
|
const config = source();
|
|
2531
2652
|
const fromKey = selectorKey(pending.from.provider, pending.from.model);
|
|
2532
|
-
const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : config.recovery === "half-open" ? Date.now() + escalatedCooldownMs(config.cooldownMs, state.recovery.recordFailure(fromKey)) : Date.now() + config.cooldownMs;
|
|
2533
2653
|
states.syncStep(state, turn, step);
|
|
2534
2654
|
states.writePending(state, pending);
|
|
2535
|
-
|
|
2655
|
+
if ((pending.scope ?? "route") === "route") {
|
|
2656
|
+
const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : config.recovery === "half-open" ? Date.now() + escalatedCooldownMs(config.cooldownMs, state.recovery.recordFailure(fromKey)) : Date.now() + config.cooldownMs;
|
|
2657
|
+
states.suppress(state, fromKey, until);
|
|
2658
|
+
}
|
|
2536
2659
|
states.recordFailure(state, fromKey);
|
|
2537
2660
|
states.recordSwitch(state);
|
|
2538
2661
|
}
|
|
@@ -2551,12 +2674,16 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2551
2674
|
const current = currentModel(agent, provider);
|
|
2552
2675
|
if (!current.model) return next();
|
|
2553
2676
|
try {
|
|
2554
|
-
const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id));
|
|
2677
|
+
const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id), failure.code);
|
|
2555
2678
|
if (pending === null) {
|
|
2556
2679
|
failHalfOpenProbe(agent.id, current);
|
|
2557
2680
|
return next();
|
|
2558
2681
|
}
|
|
2559
|
-
|
|
2682
|
+
const scope = REQUEST_SCOPED_CODES.has(failure.code) ? "request" : void 0;
|
|
2683
|
+
commit(states.get(agent.id), scope === void 0 ? pending : {
|
|
2684
|
+
...pending,
|
|
2685
|
+
scope
|
|
2686
|
+
}, turn, step);
|
|
2560
2687
|
return { kind: "retry" };
|
|
2561
2688
|
} catch (error) {
|
|
2562
2689
|
logger.warn("llm-fallbacks: decision path failed, passing the original failure through: %s", error?.message ?? String(error));
|
|
@@ -2580,7 +2707,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2580
2707
|
});
|
|
2581
2708
|
}
|
|
2582
2709
|
if (config.enabled && seed.provider === "FallbacksChain" && seed.model === "Auto" && agent.session?.header?.origin !== "subagent") {
|
|
2583
|
-
if (!isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: FallbacksChain/Auto selected but the all-day rootChain is not conforming (exactly one official
|
|
2710
|
+
if (!isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: FallbacksChain/Auto selected but the all-day rootChain is not conforming (exactly one official model: deepseek-official/deepseek-flash or deepseek-official/deepseek-pro) — no primary override");
|
|
2584
2711
|
else {
|
|
2585
2712
|
const head = firstDispatchableExactHead(resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai"));
|
|
2586
2713
|
if (head === void 0) logger.warn("llm-fallbacks: FallbacksChain/Auto selected but the effective chain has no exact head (empty, wildcard-only, or self-route) — no primary override");
|
|
@@ -2628,6 +2755,14 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2628
2755
|
model: head.model
|
|
2629
2756
|
};
|
|
2630
2757
|
}
|
|
2758
|
+
subagentRoleRecordMap.set(agent.id, {
|
|
2759
|
+
role,
|
|
2760
|
+
model: to ?? {
|
|
2761
|
+
provider: seed.provider,
|
|
2762
|
+
model: seed.model
|
|
2763
|
+
},
|
|
2764
|
+
at: Date.now()
|
|
2765
|
+
});
|
|
2631
2766
|
if (to !== void 0 && !(to.provider === seed.provider && to.model === seed.model)) {
|
|
2632
2767
|
if (policy.state === "enabled") chainHeadMap.set(agent.id, {
|
|
2633
2768
|
at: Date.now(),
|
|
@@ -2676,6 +2811,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2676
2811
|
slotWinners.delete(agent.id);
|
|
2677
2812
|
blockedAttemptMap.delete(agent.id);
|
|
2678
2813
|
chainHeadMap.delete(agent.id);
|
|
2814
|
+
subagentRoleRecordMap.delete(agent.id);
|
|
2679
2815
|
lastKnownPolicySettings.delete(agent.id);
|
|
2680
2816
|
});
|
|
2681
2817
|
ctx.on("session/event", (session, event) => {
|
|
@@ -2694,6 +2830,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2694
2830
|
slotWinners.clear();
|
|
2695
2831
|
blockedAttemptMap.clear();
|
|
2696
2832
|
chainHeadMap.clear();
|
|
2833
|
+
subagentRoleRecordMap.clear();
|
|
2697
2834
|
lastKnownPolicySettings.clear();
|
|
2698
2835
|
}, "llm-fallbacks: clear per-agent state");
|
|
2699
2836
|
const fallbacksCommandController = {
|
|
@@ -2766,10 +2903,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2766
2903
|
ctx.inject(["settings"], () => {
|
|
2767
2904
|
if (!serviceOwned) return;
|
|
2768
2905
|
if (seedsIo.read().presets === "none") return;
|
|
2769
|
-
seeds.declare(presetRoles, seedsIo).catch((error) => {
|
|
2906
|
+
seeds.declare(presetRoles, seedsIo, { bundled: true }).catch((error) => {
|
|
2770
2907
|
logger.error("llm-fallbacks: seeds: preset role declaration failed — %s", error?.message ?? String(error));
|
|
2771
2908
|
});
|
|
2772
2909
|
});
|
|
2773
2910
|
}
|
|
2774
2911
|
//#endregion
|
|
2775
|
-
export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, blockedAttempts, chainHeads, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };
|
|
2912
|
+
export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, blockedAttempts, chainHeads, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, subagentRoleRecords, validateFallbacksConfig };
|
package/dist/presets.d.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bundled preset role declarations (plan fallbacks-preset-roles Task 1
|
|
2
|
+
* Bundled preset role declarations (plan fallbacks-preset-roles Task 1;
|
|
3
|
+
* trimmed 7 → 5 by the seed-source-provenance plan, D4).
|
|
3
4
|
*
|
|
4
5
|
* Derivation: distilled from the omp bundled agent prompts —
|
|
5
|
-
* `packages/coding-agent/src/prompts/agents/{scout,
|
|
6
|
+
* `packages/coding-agent/src/prompts/agents/{scout,reviewer,security-reviewer}.md`
|
|
6
7
|
* and `task.md` (task/sonic share the body; frontmatter injected in
|
|
7
8
|
* `src/task/agents.ts`) — snapshot date 2026-08-16. Each persona is a
|
|
8
9
|
* concise distillation (frontmatter description + core directives), NOT a
|
|
9
10
|
* verbatim copy of the full prompt; the frozen text lives in spec
|
|
10
|
-
* `fallbacks-preset-roles-spec.md` §9.2 (implementer SSOT).
|
|
11
|
+
* `fallbacks-preset-roles-spec.md` §9.2 (implementer SSOT). `designer` and
|
|
12
|
+
* `librarian` were removed from the bundled set; rows persisted by earlier
|
|
13
|
+
* versions survive untouched (they read back with source `user`).
|
|
11
14
|
*
|
|
12
15
|
* Pure data module: no io, no side effects, no classes. Types import only
|
|
13
16
|
* `./seeds.ts` — no `@deepseek-ai/*` imports (bundle purity gate).
|
|
14
17
|
*/
|
|
15
18
|
import type { SeedDeclaration } from './seeds.ts';
|
|
16
|
-
/** The
|
|
19
|
+
/** The 5 bundled omp-style preset roles (spec §9.1 shape, §9.2 personas). */
|
|
17
20
|
export declare const presetRoles: readonly SeedDeclaration[];
|