dsh-llm-fallbacks 0.4.1 → 0.4.3
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 +31 -2
- package/README.zh-CN.md +31 -2
- 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 +311 -141
- package/dist/client/locales.d.ts +12 -6
- package/dist/config.d.ts +1 -1
- package/dist/gateway.d.ts +55 -2
- package/dist/index.d.ts +34 -5
- package/dist/index.js +196 -61
- package/dist/presets.d.ts +7 -4
- package/dist/seeds.d.ts +61 -7
- package/dist/state.d.ts +32 -4
- package/dist/tui-settings.d.ts +8 -6
- package/dist/tui.d.ts +12 -5
- package/package.json +1 -1
- package/scripts/build-client.ts +6 -1
- package/scripts/prepare-release.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -618,18 +618,25 @@ function hasWildcardEntry(roles, rootChain, role) {
|
|
|
618
618
|
} catch {}
|
|
619
619
|
return false;
|
|
620
620
|
}
|
|
621
|
+
function fitsContextWindow(candidate, minContextWindow, contextWindowOf) {
|
|
622
|
+
if (minContextWindow === void 0 || contextWindowOf === void 0) return true;
|
|
623
|
+
if (candidate.model === void 0) return true;
|
|
624
|
+
const candidateWindow = contextWindowOf(candidate.provider, candidate.model);
|
|
625
|
+
return candidateWindow === void 0 || candidateWindow > minContextWindow;
|
|
626
|
+
}
|
|
621
627
|
function createCandidateFilter(options) {
|
|
622
|
-
const { current, cooldown, failed, modelExists } = options;
|
|
628
|
+
const { current, cooldown, failed, modelExists, minContextWindow, contextWindowOf } = options;
|
|
623
629
|
return (candidate) => {
|
|
624
630
|
if (candidate.provider === current.provider && candidate.model === current.model) return false;
|
|
625
631
|
if (cooldown.isSuppressed(selectorKey(candidate.provider, candidate.model))) return false;
|
|
626
632
|
if (failed.has(selectorKey(candidate.provider, candidate.model))) return false;
|
|
633
|
+
if (!fitsContextWindow(candidate, minContextWindow, contextWindowOf)) return false;
|
|
627
634
|
if (modelExists && candidate.model !== void 0 && !modelExists(candidate.provider, candidate.model)) return false;
|
|
628
635
|
return true;
|
|
629
636
|
};
|
|
630
637
|
}
|
|
631
638
|
function annotateCandidates(candidates, surviving, options) {
|
|
632
|
-
const { current, cooldown, failed } = options;
|
|
639
|
+
const { current, cooldown, failed, minContextWindow, contextWindowOf } = options;
|
|
633
640
|
const usable = new Set(surviving.map((candidate) => selectorKey(candidate.provider, candidate.model)));
|
|
634
641
|
return candidates.map((candidate) => {
|
|
635
642
|
if (candidate.provider === current.provider && candidate.model === current.model) return {
|
|
@@ -646,6 +653,10 @@ function annotateCandidates(candidates, surviving, options) {
|
|
|
646
653
|
candidate,
|
|
647
654
|
skip: "step-failed"
|
|
648
655
|
};
|
|
656
|
+
if (!fitsContextWindow(candidate, minContextWindow, contextWindowOf)) return {
|
|
657
|
+
candidate,
|
|
658
|
+
skip: "context-window"
|
|
659
|
+
};
|
|
649
660
|
return {
|
|
650
661
|
candidate,
|
|
651
662
|
skip: "missing-id"
|
|
@@ -925,7 +936,8 @@ var FallbackStateStore = class {
|
|
|
925
936
|
if (mode !== "half-open") return;
|
|
926
937
|
for (const key of state.cooldown.keys()) this.isSuppressed(state, key, now, mode);
|
|
927
938
|
}
|
|
928
|
-
observeSuccess(state, key) {
|
|
939
|
+
observeSuccess(state, key, now = Date.now()) {
|
|
940
|
+
if (state.cooldown.peek(key) !== void 0) this.isSuppressed(state, key, now, "half-open");
|
|
929
941
|
state.recovery.close(key);
|
|
930
942
|
}
|
|
931
943
|
};
|
|
@@ -953,6 +965,7 @@ function overrideConfigWithRouteRule(seed, to, explicitEffort) {
|
|
|
953
965
|
//#endregion
|
|
954
966
|
//#region .build/host/gateway.js
|
|
955
967
|
const FALLBACKS_SETTINGS_NAMESPACE = "fallbacks";
|
|
968
|
+
const SUBAGENT_ROLES_MAX_IDS = 256;
|
|
956
969
|
const CONFIG_KEYS = {
|
|
957
970
|
enabled: true,
|
|
958
971
|
triggerCodes: true,
|
|
@@ -987,11 +1000,13 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
987
1000
|
seeds;
|
|
988
1001
|
settings;
|
|
989
1002
|
subagentPolicy;
|
|
990
|
-
|
|
1003
|
+
subagentRolesSnapshot;
|
|
1004
|
+
constructor(ctx, bridge, seeds, subagentPolicy, subagentRoles) {
|
|
991
1005
|
super(ctx, "fallbacks");
|
|
992
1006
|
this.bridge = bridge;
|
|
993
1007
|
this.seeds = seeds;
|
|
994
1008
|
this.subagentPolicy = subagentPolicy;
|
|
1009
|
+
this.subagentRolesSnapshot = subagentRoles;
|
|
995
1010
|
ctx.inject(["settings"], (sctx) => {
|
|
996
1011
|
this.settings = sctx.settings;
|
|
997
1012
|
return () => {
|
|
@@ -1026,6 +1041,23 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1026
1041
|
outcome
|
|
1027
1042
|
};
|
|
1028
1043
|
}
|
|
1044
|
+
subagentRoles(ids) {
|
|
1045
|
+
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");
|
|
1046
|
+
if (ids.length > SUBAGENT_ROLES_MAX_IDS) throw new TypeError(`dsh-llm-fallbacks: subagent-roles ids batch exceeds ${SUBAGENT_ROLES_MAX_IDS} entries`);
|
|
1047
|
+
if (this.subagentRolesSnapshot === void 0) return {};
|
|
1048
|
+
let records;
|
|
1049
|
+
try {
|
|
1050
|
+
records = this.subagentRolesSnapshot();
|
|
1051
|
+
} catch {
|
|
1052
|
+
return {};
|
|
1053
|
+
}
|
|
1054
|
+
const projected = new Map();
|
|
1055
|
+
for (const id of ids) {
|
|
1056
|
+
const record = records.get(id);
|
|
1057
|
+
if (record !== void 0) projected.set(id, projectSubagentRoleRecord(record));
|
|
1058
|
+
}
|
|
1059
|
+
return Object.fromEntries(projected);
|
|
1060
|
+
}
|
|
1029
1061
|
readConfig(source = this.bridge.source()) {
|
|
1030
1062
|
const wire = {};
|
|
1031
1063
|
for (const key of Object.keys(CONFIG_KEYS)) {
|
|
@@ -1092,6 +1124,17 @@ function projectSubagentPolicy(snapshot) {
|
|
|
1092
1124
|
};
|
|
1093
1125
|
return wire;
|
|
1094
1126
|
}
|
|
1127
|
+
function projectSubagentRoleRecord(record) {
|
|
1128
|
+
const wire = {
|
|
1129
|
+
role: record.role,
|
|
1130
|
+
at: record.at
|
|
1131
|
+
};
|
|
1132
|
+
if (record.model !== void 0) wire.model = {
|
|
1133
|
+
provider: record.model.provider,
|
|
1134
|
+
model: record.model.model
|
|
1135
|
+
};
|
|
1136
|
+
return wire;
|
|
1137
|
+
}
|
|
1095
1138
|
function normalizeRoles(value) {
|
|
1096
1139
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
1097
1140
|
const roles = {};
|
|
@@ -1206,6 +1249,21 @@ function fallbacksTypertContribution() {
|
|
|
1206
1249
|
codec: { mode: "src-json" }
|
|
1207
1250
|
}],
|
|
1208
1251
|
result: { mode: "src-json" }
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
id: "dsh-llm-fallbacks#fallbacks/subagent-roles",
|
|
1255
|
+
service: "fallbacks",
|
|
1256
|
+
namespace: "fallbacks",
|
|
1257
|
+
method: "subagent-roles",
|
|
1258
|
+
implementation: "subagentRoles",
|
|
1259
|
+
invocation: { kind: "direct" },
|
|
1260
|
+
parameters: [{
|
|
1261
|
+
name: "ids",
|
|
1262
|
+
wire: "ids",
|
|
1263
|
+
source: "json",
|
|
1264
|
+
codec: { mode: "src-json" }
|
|
1265
|
+
}],
|
|
1266
|
+
result: { mode: "src-json" }
|
|
1209
1267
|
}
|
|
1210
1268
|
]
|
|
1211
1269
|
};
|
|
@@ -1543,13 +1601,14 @@ var FallbacksSeedManager = class {
|
|
|
1543
1601
|
constructor(logger) {
|
|
1544
1602
|
this.logger = logger;
|
|
1545
1603
|
}
|
|
1546
|
-
async declare(seeds, io) {
|
|
1604
|
+
async declare(seeds, io, options) {
|
|
1547
1605
|
const outcome = {
|
|
1548
1606
|
applied: [],
|
|
1549
1607
|
skipped: [],
|
|
1550
1608
|
conflicts: []
|
|
1551
1609
|
};
|
|
1552
|
-
const
|
|
1610
|
+
const source = resolveSource(options, this.logger);
|
|
1611
|
+
const entries = new Map();
|
|
1553
1612
|
for (const seed of seeds) {
|
|
1554
1613
|
if (typeof seed.id !== "string" || !ROLE_ID_PATTERN.test(seed.id)) {
|
|
1555
1614
|
outcome.skipped.push({
|
|
@@ -1567,7 +1626,7 @@ var FallbacksSeedManager = class {
|
|
|
1567
1626
|
this.warnSkip(seed.id, "reserved-id");
|
|
1568
1627
|
continue;
|
|
1569
1628
|
}
|
|
1570
|
-
if (
|
|
1629
|
+
if (entries.has(seed.id)) {
|
|
1571
1630
|
outcome.skipped.push({
|
|
1572
1631
|
id: seed.id,
|
|
1573
1632
|
reason: "duplicate-in-batch"
|
|
@@ -1575,33 +1634,37 @@ var FallbacksSeedManager = class {
|
|
|
1575
1634
|
this.warnSkip(seed.id, "duplicate-in-batch");
|
|
1576
1635
|
continue;
|
|
1577
1636
|
}
|
|
1578
|
-
|
|
1637
|
+
entries.set(seed.id, {
|
|
1638
|
+
persona: seed.persona,
|
|
1639
|
+
set: source
|
|
1640
|
+
});
|
|
1579
1641
|
outcome.applied.push(seed.id);
|
|
1580
1642
|
}
|
|
1581
1643
|
const config = io.read();
|
|
1582
1644
|
const currentList = roleRows(config);
|
|
1583
1645
|
const currentRules = roleRules(config);
|
|
1584
|
-
const newList = materialize(currentList,
|
|
1646
|
+
const newList = materialize(currentList, personaView(entries), personaView(this.registry), outcome.conflicts);
|
|
1585
1647
|
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)`);
|
|
1586
1648
|
const computed = {
|
|
1587
1649
|
list: newList,
|
|
1588
1650
|
rules: currentRules
|
|
1589
1651
|
};
|
|
1590
1652
|
if (!deepEqual(newList, currentList)) await io.writeRoles(computed);
|
|
1591
|
-
this.registry =
|
|
1653
|
+
this.registry = entries;
|
|
1592
1654
|
return outcome;
|
|
1593
1655
|
}
|
|
1594
1656
|
effectiveRoles(io) {
|
|
1595
1657
|
return { roles: roleRows(io.read()).map((row) => {
|
|
1596
|
-
const
|
|
1597
|
-
const seeded =
|
|
1658
|
+
const entry = this.registry.get(row.id.trim());
|
|
1659
|
+
const seeded = entry !== void 0;
|
|
1598
1660
|
const effective = {
|
|
1599
1661
|
id: row.id,
|
|
1600
1662
|
persona: row.persona,
|
|
1601
1663
|
seeded,
|
|
1602
|
-
personaOverridden: seeded && row.persona !==
|
|
1664
|
+
personaOverridden: seeded && row.persona !== entry.persona,
|
|
1665
|
+
source: seeded ? entry.set : "user"
|
|
1603
1666
|
};
|
|
1604
|
-
if (seeded) effective.seedPersona =
|
|
1667
|
+
if (seeded) effective.seedPersona = entry.persona;
|
|
1605
1668
|
if (row.chain !== void 0) effective.chain = row.chain;
|
|
1606
1669
|
if (row.fallback !== void 0) effective.fallback = row.fallback;
|
|
1607
1670
|
return effective;
|
|
@@ -1610,19 +1673,20 @@ var FallbacksSeedManager = class {
|
|
|
1610
1673
|
wireStatus(io) {
|
|
1611
1674
|
const status = [];
|
|
1612
1675
|
for (const row of roleRows(io.read())) {
|
|
1613
|
-
const
|
|
1614
|
-
if (
|
|
1676
|
+
const entry = this.registry.get(row.id.trim());
|
|
1677
|
+
if (entry === void 0) continue;
|
|
1615
1678
|
status.push({
|
|
1616
1679
|
id: row.id,
|
|
1617
|
-
overridden: row.persona !==
|
|
1680
|
+
overridden: row.persona !== entry.persona,
|
|
1681
|
+
source: entry.set
|
|
1618
1682
|
});
|
|
1619
1683
|
}
|
|
1620
1684
|
return status;
|
|
1621
1685
|
}
|
|
1622
1686
|
async revert(id, io) {
|
|
1623
1687
|
const seedId = id.trim();
|
|
1624
|
-
const
|
|
1625
|
-
if (
|
|
1688
|
+
const entry = this.registry.get(seedId);
|
|
1689
|
+
if (entry === void 0) return {
|
|
1626
1690
|
reverted: false,
|
|
1627
1691
|
reason: "not-seeded"
|
|
1628
1692
|
};
|
|
@@ -1634,13 +1698,13 @@ var FallbacksSeedManager = class {
|
|
|
1634
1698
|
reverted: false,
|
|
1635
1699
|
reason: "row-absent"
|
|
1636
1700
|
};
|
|
1637
|
-
if (rows[index].persona ===
|
|
1701
|
+
if (rows[index].persona === entry.persona) return {
|
|
1638
1702
|
reverted: true,
|
|
1639
|
-
persona:
|
|
1703
|
+
persona: entry.persona
|
|
1640
1704
|
};
|
|
1641
1705
|
const nextList = rows.map((row, i) => i === index ? {
|
|
1642
1706
|
...row,
|
|
1643
|
-
persona:
|
|
1707
|
+
persona: entry.persona
|
|
1644
1708
|
} : row);
|
|
1645
1709
|
await io.writeRoles({
|
|
1646
1710
|
list: nextList,
|
|
@@ -1648,7 +1712,7 @@ var FallbacksSeedManager = class {
|
|
|
1648
1712
|
});
|
|
1649
1713
|
return {
|
|
1650
1714
|
reverted: true,
|
|
1651
|
-
persona:
|
|
1715
|
+
persona: entry.persona
|
|
1652
1716
|
};
|
|
1653
1717
|
}
|
|
1654
1718
|
warnSkip(id, reason) {
|
|
@@ -1658,6 +1722,26 @@ var FallbacksSeedManager = class {
|
|
|
1658
1722
|
else this.logger.warn(`llm-fallbacks: seeds: skipping seed id ${shown} — duplicate-in-batch (first wins)`);
|
|
1659
1723
|
}
|
|
1660
1724
|
};
|
|
1725
|
+
const RESERVED_SET_NAMES = [
|
|
1726
|
+
"bundled",
|
|
1727
|
+
"user",
|
|
1728
|
+
"external"
|
|
1729
|
+
];
|
|
1730
|
+
function resolveSource(options, logger) {
|
|
1731
|
+
if (options?.bundled === true) return "bundled";
|
|
1732
|
+
const raw = options?.set;
|
|
1733
|
+
if (raw === void 0) return "external";
|
|
1734
|
+
if (typeof raw === "string" && raw.trim() !== "" && !RESERVED_SET_NAMES.includes(raw.trim())) return raw.trim();
|
|
1735
|
+
const shown = typeof raw === "string" ? JSON.stringify(raw) : String(raw);
|
|
1736
|
+
const reserved = RESERVED_SET_NAMES.map((name) => JSON.stringify(name)).join(" / ");
|
|
1737
|
+
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`);
|
|
1738
|
+
return "external";
|
|
1739
|
+
}
|
|
1740
|
+
function personaView(entries) {
|
|
1741
|
+
const personas = new Map();
|
|
1742
|
+
for (const [id, entry] of entries) personas.set(id, entry.persona);
|
|
1743
|
+
return personas;
|
|
1744
|
+
}
|
|
1661
1745
|
function materialize(rows, registry, previous, conflicts) {
|
|
1662
1746
|
const next = [];
|
|
1663
1747
|
for (const row of rows) {
|
|
@@ -1732,14 +1816,6 @@ const presetRoles = [
|
|
|
1732
1816
|
id: "scout",
|
|
1733
1817
|
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."
|
|
1734
1818
|
},
|
|
1735
|
-
{
|
|
1736
|
-
id: "designer",
|
|
1737
|
-
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."
|
|
1738
|
-
},
|
|
1739
|
-
{
|
|
1740
|
-
id: "librarian",
|
|
1741
|
-
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."
|
|
1742
|
-
},
|
|
1743
1819
|
{
|
|
1744
1820
|
id: "reviewer",
|
|
1745
1821
|
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."
|
|
@@ -2096,7 +2172,13 @@ function installTuiSettingsSection(ctx, opts) {
|
|
|
2096
2172
|
ctx.inject(["tuiSettingsSections"], (tctx) => {
|
|
2097
2173
|
const registry = tctx.tuiSettingsSections;
|
|
2098
2174
|
if (registry === void 0) return;
|
|
2099
|
-
|
|
2175
|
+
try {
|
|
2176
|
+
return registry.register(buildFallbacksTuiSection());
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
|
|
2179
|
+
tctx.logger("llm-fallbacks").debug("llm-fallbacks: tui settings section already registered — no section on this fiber");
|
|
2180
|
+
return () => {};
|
|
2181
|
+
}
|
|
2100
2182
|
});
|
|
2101
2183
|
}
|
|
2102
2184
|
//#endregion
|
|
@@ -2258,6 +2340,8 @@ function installFallbacksAdapter(ctx, readConfig) {
|
|
|
2258
2340
|
const name = "llm-fallbacks";
|
|
2259
2341
|
const provide = ["llm-fallbacks"];
|
|
2260
2342
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
2343
|
+
const CONTEXT_WINDOW_EXCEEDED_CODE = "CONTEXT_WINDOW_EXCEEDED";
|
|
2344
|
+
const REQUEST_SCOPED_CODES = new Set([CONTEXT_WINDOW_EXCEEDED_CODE]);
|
|
2261
2345
|
const stateStores = new WeakMap();
|
|
2262
2346
|
function stateStore(ctx) {
|
|
2263
2347
|
return stateStores.get(ctx);
|
|
@@ -2270,6 +2354,10 @@ const chainHeadStores = new WeakMap();
|
|
|
2270
2354
|
function chainHeads(ctx) {
|
|
2271
2355
|
return chainHeadStores.get(ctx);
|
|
2272
2356
|
}
|
|
2357
|
+
const subagentRoleRecordStores = new WeakMap();
|
|
2358
|
+
function subagentRoleRecords(ctx) {
|
|
2359
|
+
return subagentRoleRecordStores.get(ctx);
|
|
2360
|
+
}
|
|
2273
2361
|
function latestByAt(map) {
|
|
2274
2362
|
let latest;
|
|
2275
2363
|
for (const value of map.values()) if (latest === void 0 || value.at >= latest.at) latest = value;
|
|
@@ -2289,6 +2377,26 @@ async function makeModelExists(ctx, providers) {
|
|
|
2289
2377
|
}));
|
|
2290
2378
|
return (provider, model) => catalog.get(provider)?.has(model) ?? false;
|
|
2291
2379
|
}
|
|
2380
|
+
async function makeContextWindowOf(ctx, routes) {
|
|
2381
|
+
const llm = ctx.get("llm");
|
|
2382
|
+
if (llm === void 0) return () => void 0;
|
|
2383
|
+
const windows = new Map();
|
|
2384
|
+
if (typeof llm.listModels === "function") await Promise.all([...new Set(routes.map((route) => route.provider))].map(async (provider) => {
|
|
2385
|
+
try {
|
|
2386
|
+
for (const model of await llm.listModels(provider)) if (typeof model.contextWindow === "number") windows.set(selectorKey(provider, model.id), model.contextWindow);
|
|
2387
|
+
} catch {}
|
|
2388
|
+
}));
|
|
2389
|
+
const resolveModelInfo = llm.resolveModelInfo?.bind(llm);
|
|
2390
|
+
if (resolveModelInfo !== void 0) await Promise.all(routes.map(async (route) => {
|
|
2391
|
+
const key = selectorKey(route.provider, route.model);
|
|
2392
|
+
if (windows.has(key)) return;
|
|
2393
|
+
try {
|
|
2394
|
+
const window = (await resolveModelInfo(route.provider, route.model))?.context?.contextWindow;
|
|
2395
|
+
if (typeof window === "number") windows.set(key, window);
|
|
2396
|
+
} catch {}
|
|
2397
|
+
}));
|
|
2398
|
+
return (provider, model) => windows.get(selectorKey(provider, model));
|
|
2399
|
+
}
|
|
2292
2400
|
function countRetryEvents(session, turn, step, provider) {
|
|
2293
2401
|
let count = 0;
|
|
2294
2402
|
const events = session.snapshotEvents();
|
|
@@ -2366,6 +2474,24 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2366
2474
|
};
|
|
2367
2475
|
ctx.inject(["settings"], (sctx) => {
|
|
2368
2476
|
writeRoles = (roles) => sctx.settings.update(FALLBACKS_SETTINGS_NAMESPACE, { roles });
|
|
2477
|
+
try {
|
|
2478
|
+
sctx.provide("llm-fallbacks", {
|
|
2479
|
+
name: "llm-fallbacks",
|
|
2480
|
+
version,
|
|
2481
|
+
resolveRole,
|
|
2482
|
+
resolveChain,
|
|
2483
|
+
validateFallbacksConfig,
|
|
2484
|
+
detectLegacyKeys,
|
|
2485
|
+
declareSeeds: (declarations, options) => seeds.declare(declarations, seedsIo, options?.set === void 0 ? void 0 : { set: options.set }),
|
|
2486
|
+
getEffectiveRoles: () => seeds.effectiveRoles(seedsIo),
|
|
2487
|
+
revertSeededPersona: (id) => seeds.revert(id, seedsIo)
|
|
2488
|
+
});
|
|
2489
|
+
serviceOwned = true;
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
serviceOwned = false;
|
|
2492
|
+
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
2493
|
+
ctx.logger("llm-fallbacks").debug("fallbacks service already registered — no service on this fiber (multi-fiber dedupe)");
|
|
2494
|
+
}
|
|
2369
2495
|
return () => {
|
|
2370
2496
|
writeRoles = () => {
|
|
2371
2497
|
throw new Error(seedsSettingsUnavailable);
|
|
@@ -2376,25 +2502,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2376
2502
|
read: () => source(),
|
|
2377
2503
|
writeRoles: (roles) => writeRoles(roles)
|
|
2378
2504
|
};
|
|
2379
|
-
let serviceOwned =
|
|
2380
|
-
try {
|
|
2381
|
-
ctx.provide("llm-fallbacks", {
|
|
2382
|
-
name: "llm-fallbacks",
|
|
2383
|
-
version,
|
|
2384
|
-
resolveRole,
|
|
2385
|
-
resolveChain,
|
|
2386
|
-
validateFallbacksConfig,
|
|
2387
|
-
detectLegacyKeys,
|
|
2388
|
-
declareSeeds: (declarations) => seeds.declare(declarations, seedsIo),
|
|
2389
|
-
getEffectiveRoles: () => seeds.effectiveRoles(seedsIo),
|
|
2390
|
-
revertSeededPersona: (id) => seeds.revert(id, seedsIo)
|
|
2391
|
-
});
|
|
2392
|
-
serviceOwned = true;
|
|
2393
|
-
} catch (error) {
|
|
2394
|
-
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
2395
|
-
serviceOwned = false;
|
|
2396
|
-
ctx.logger("llm-fallbacks").debug("fallbacks service already registered — no service on this fiber (multi-fiber dedupe)");
|
|
2397
|
-
}
|
|
2505
|
+
let serviceOwned = ctx.get("llm-fallbacks") === void 0;
|
|
2398
2506
|
let source = () => entry;
|
|
2399
2507
|
const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source());
|
|
2400
2508
|
validateFallbacksConfig(entry, logger);
|
|
@@ -2421,6 +2529,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2421
2529
|
chainHeadStores.set(ctx, chainHeadMap);
|
|
2422
2530
|
const blockedAttemptMap = new Map();
|
|
2423
2531
|
blockedAttemptStores.set(ctx, blockedAttemptMap);
|
|
2532
|
+
const subagentRoleRecordMap = new Map();
|
|
2533
|
+
subagentRoleRecordStores.set(ctx, subagentRoleRecordMap);
|
|
2424
2534
|
try {
|
|
2425
2535
|
new FallbacksConfigGateway(ctx, bridge, seeds, () => {
|
|
2426
2536
|
try {
|
|
@@ -2435,7 +2545,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2435
2545
|
} catch {
|
|
2436
2546
|
return { policy: { state: "unprovable" } };
|
|
2437
2547
|
}
|
|
2438
|
-
});
|
|
2548
|
+
}, () => subagentRoleRecordMap);
|
|
2439
2549
|
} catch (error) {
|
|
2440
2550
|
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
2441
2551
|
ctx.logger("llm-fallbacks").debug("fallbacks gateway already registered — no gateway on this fiber (multi-fiber dedupe)");
|
|
@@ -2454,7 +2564,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2454
2564
|
const dispatchInjected = new Set();
|
|
2455
2565
|
const slotWinners = new Map();
|
|
2456
2566
|
const lastKnownPolicySettings = new Map();
|
|
2457
|
-
async function decide(agent, turn, step, current, reason, state) {
|
|
2567
|
+
async function decide(agent, turn, step, current, reason, state, failureCode) {
|
|
2458
2568
|
const config = source();
|
|
2459
2569
|
if (state !== void 0) {
|
|
2460
2570
|
states.syncStep(state, turn, step);
|
|
@@ -2467,10 +2577,17 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2467
2577
|
const modelExists = hasWildcardEntry(config.roles.list, rootTail, role) ? await makeModelExists(ctx, [...new Set(all.map((candidate) => candidate.provider))]) : void 0;
|
|
2468
2578
|
const cooldown = { isSuppressed: (key) => state !== void 0 && states.isSuppressed(state, key, Date.now(), source().recovery ?? "timer") };
|
|
2469
2579
|
const failed = { has: (key) => state !== void 0 && state.stepFailures.failed.has(key) };
|
|
2580
|
+
const contextWindowOf = failureCode === CONTEXT_WINDOW_EXCEEDED_CODE ? await makeContextWindowOf(ctx, [current, ...all.flatMap((candidate) => candidate.model === void 0 ? [] : [{
|
|
2581
|
+
provider: candidate.provider,
|
|
2582
|
+
model: candidate.model
|
|
2583
|
+
}])]) : void 0;
|
|
2584
|
+
const minContextWindow = contextWindowOf?.(current.provider, current.model);
|
|
2470
2585
|
const surviving = selectCandidates(all, wildcard, createCandidateFilter({
|
|
2471
2586
|
current,
|
|
2472
2587
|
cooldown,
|
|
2473
|
-
failed
|
|
2588
|
+
failed,
|
|
2589
|
+
minContextWindow,
|
|
2590
|
+
contextWindowOf
|
|
2474
2591
|
}), modelExists);
|
|
2475
2592
|
const first = surviving[0];
|
|
2476
2593
|
let target;
|
|
@@ -2510,7 +2627,9 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2510
2627
|
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, {
|
|
2511
2628
|
current,
|
|
2512
2629
|
cooldown,
|
|
2513
|
-
failed
|
|
2630
|
+
failed,
|
|
2631
|
+
minContextWindow,
|
|
2632
|
+
contextWindowOf
|
|
2514
2633
|
}).map(({ candidate, skip }) => skip === void 0 ? `${candidate.provider}/${candidate.model}` : `${candidate.provider}/${candidate.model} (skipped: ${skip})`));
|
|
2515
2634
|
return {
|
|
2516
2635
|
from: {
|
|
@@ -2528,10 +2647,12 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2528
2647
|
function commit(state, pending, turn, step) {
|
|
2529
2648
|
const config = source();
|
|
2530
2649
|
const fromKey = selectorKey(pending.from.provider, pending.from.model);
|
|
2531
|
-
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;
|
|
2532
2650
|
states.syncStep(state, turn, step);
|
|
2533
2651
|
states.writePending(state, pending);
|
|
2534
|
-
|
|
2652
|
+
if ((pending.scope ?? "route") === "route") {
|
|
2653
|
+
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;
|
|
2654
|
+
states.suppress(state, fromKey, until);
|
|
2655
|
+
}
|
|
2535
2656
|
states.recordFailure(state, fromKey);
|
|
2536
2657
|
states.recordSwitch(state);
|
|
2537
2658
|
}
|
|
@@ -2550,12 +2671,16 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2550
2671
|
const current = currentModel(agent, provider);
|
|
2551
2672
|
if (!current.model) return next();
|
|
2552
2673
|
try {
|
|
2553
|
-
const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id));
|
|
2674
|
+
const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id), failure.code);
|
|
2554
2675
|
if (pending === null) {
|
|
2555
2676
|
failHalfOpenProbe(agent.id, current);
|
|
2556
2677
|
return next();
|
|
2557
2678
|
}
|
|
2558
|
-
|
|
2679
|
+
const scope = REQUEST_SCOPED_CODES.has(failure.code) ? "request" : void 0;
|
|
2680
|
+
commit(states.get(agent.id), scope === void 0 ? pending : {
|
|
2681
|
+
...pending,
|
|
2682
|
+
scope
|
|
2683
|
+
}, turn, step);
|
|
2559
2684
|
return { kind: "retry" };
|
|
2560
2685
|
} catch (error) {
|
|
2561
2686
|
logger.warn("llm-fallbacks: decision path failed, passing the original failure through: %s", error?.message ?? String(error));
|
|
@@ -2627,6 +2752,14 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2627
2752
|
model: head.model
|
|
2628
2753
|
};
|
|
2629
2754
|
}
|
|
2755
|
+
subagentRoleRecordMap.set(agent.id, {
|
|
2756
|
+
role,
|
|
2757
|
+
model: to ?? {
|
|
2758
|
+
provider: seed.provider,
|
|
2759
|
+
model: seed.model
|
|
2760
|
+
},
|
|
2761
|
+
at: Date.now()
|
|
2762
|
+
});
|
|
2630
2763
|
if (to !== void 0 && !(to.provider === seed.provider && to.model === seed.model)) {
|
|
2631
2764
|
if (policy.state === "enabled") chainHeadMap.set(agent.id, {
|
|
2632
2765
|
at: Date.now(),
|
|
@@ -2675,6 +2808,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2675
2808
|
slotWinners.delete(agent.id);
|
|
2676
2809
|
blockedAttemptMap.delete(agent.id);
|
|
2677
2810
|
chainHeadMap.delete(agent.id);
|
|
2811
|
+
subagentRoleRecordMap.delete(agent.id);
|
|
2678
2812
|
lastKnownPolicySettings.delete(agent.id);
|
|
2679
2813
|
});
|
|
2680
2814
|
ctx.on("session/event", (session, event) => {
|
|
@@ -2693,6 +2827,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2693
2827
|
slotWinners.clear();
|
|
2694
2828
|
blockedAttemptMap.clear();
|
|
2695
2829
|
chainHeadMap.clear();
|
|
2830
|
+
subagentRoleRecordMap.clear();
|
|
2696
2831
|
lastKnownPolicySettings.clear();
|
|
2697
2832
|
}, "llm-fallbacks: clear per-agent state");
|
|
2698
2833
|
const fallbacksCommandController = {
|
|
@@ -2765,10 +2900,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2765
2900
|
ctx.inject(["settings"], () => {
|
|
2766
2901
|
if (!serviceOwned) return;
|
|
2767
2902
|
if (seedsIo.read().presets === "none") return;
|
|
2768
|
-
seeds.declare(presetRoles, seedsIo).catch((error) => {
|
|
2903
|
+
seeds.declare(presetRoles, seedsIo, { bundled: true }).catch((error) => {
|
|
2769
2904
|
logger.error("llm-fallbacks: seeds: preset role declaration failed — %s", error?.message ?? String(error));
|
|
2770
2905
|
});
|
|
2771
2906
|
});
|
|
2772
2907
|
}
|
|
2773
2908
|
//#endregion
|
|
2774
|
-
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 };
|
|
2909
|
+
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[];
|