dsh-llm-fallbacks 0.4.2 → 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 +194 -60
- package/dist/presets.d.ts +7 -4
- package/dist/seeds.d.ts +61 -7
- package/dist/state.d.ts +20 -0
- 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"
|
|
@@ -954,6 +965,7 @@ function overrideConfigWithRouteRule(seed, to, explicitEffort) {
|
|
|
954
965
|
//#endregion
|
|
955
966
|
//#region .build/host/gateway.js
|
|
956
967
|
const FALLBACKS_SETTINGS_NAMESPACE = "fallbacks";
|
|
968
|
+
const SUBAGENT_ROLES_MAX_IDS = 256;
|
|
957
969
|
const CONFIG_KEYS = {
|
|
958
970
|
enabled: true,
|
|
959
971
|
triggerCodes: true,
|
|
@@ -988,11 +1000,13 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
988
1000
|
seeds;
|
|
989
1001
|
settings;
|
|
990
1002
|
subagentPolicy;
|
|
991
|
-
|
|
1003
|
+
subagentRolesSnapshot;
|
|
1004
|
+
constructor(ctx, bridge, seeds, subagentPolicy, subagentRoles) {
|
|
992
1005
|
super(ctx, "fallbacks");
|
|
993
1006
|
this.bridge = bridge;
|
|
994
1007
|
this.seeds = seeds;
|
|
995
1008
|
this.subagentPolicy = subagentPolicy;
|
|
1009
|
+
this.subagentRolesSnapshot = subagentRoles;
|
|
996
1010
|
ctx.inject(["settings"], (sctx) => {
|
|
997
1011
|
this.settings = sctx.settings;
|
|
998
1012
|
return () => {
|
|
@@ -1027,6 +1041,23 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1027
1041
|
outcome
|
|
1028
1042
|
};
|
|
1029
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
|
+
}
|
|
1030
1061
|
readConfig(source = this.bridge.source()) {
|
|
1031
1062
|
const wire = {};
|
|
1032
1063
|
for (const key of Object.keys(CONFIG_KEYS)) {
|
|
@@ -1093,6 +1124,17 @@ function projectSubagentPolicy(snapshot) {
|
|
|
1093
1124
|
};
|
|
1094
1125
|
return wire;
|
|
1095
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
|
+
}
|
|
1096
1138
|
function normalizeRoles(value) {
|
|
1097
1139
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
1098
1140
|
const roles = {};
|
|
@@ -1207,6 +1249,21 @@ function fallbacksTypertContribution() {
|
|
|
1207
1249
|
codec: { mode: "src-json" }
|
|
1208
1250
|
}],
|
|
1209
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" }
|
|
1210
1267
|
}
|
|
1211
1268
|
]
|
|
1212
1269
|
};
|
|
@@ -1544,13 +1601,14 @@ var FallbacksSeedManager = class {
|
|
|
1544
1601
|
constructor(logger) {
|
|
1545
1602
|
this.logger = logger;
|
|
1546
1603
|
}
|
|
1547
|
-
async declare(seeds, io) {
|
|
1604
|
+
async declare(seeds, io, options) {
|
|
1548
1605
|
const outcome = {
|
|
1549
1606
|
applied: [],
|
|
1550
1607
|
skipped: [],
|
|
1551
1608
|
conflicts: []
|
|
1552
1609
|
};
|
|
1553
|
-
const
|
|
1610
|
+
const source = resolveSource(options, this.logger);
|
|
1611
|
+
const entries = new Map();
|
|
1554
1612
|
for (const seed of seeds) {
|
|
1555
1613
|
if (typeof seed.id !== "string" || !ROLE_ID_PATTERN.test(seed.id)) {
|
|
1556
1614
|
outcome.skipped.push({
|
|
@@ -1568,7 +1626,7 @@ var FallbacksSeedManager = class {
|
|
|
1568
1626
|
this.warnSkip(seed.id, "reserved-id");
|
|
1569
1627
|
continue;
|
|
1570
1628
|
}
|
|
1571
|
-
if (
|
|
1629
|
+
if (entries.has(seed.id)) {
|
|
1572
1630
|
outcome.skipped.push({
|
|
1573
1631
|
id: seed.id,
|
|
1574
1632
|
reason: "duplicate-in-batch"
|
|
@@ -1576,33 +1634,37 @@ var FallbacksSeedManager = class {
|
|
|
1576
1634
|
this.warnSkip(seed.id, "duplicate-in-batch");
|
|
1577
1635
|
continue;
|
|
1578
1636
|
}
|
|
1579
|
-
|
|
1637
|
+
entries.set(seed.id, {
|
|
1638
|
+
persona: seed.persona,
|
|
1639
|
+
set: source
|
|
1640
|
+
});
|
|
1580
1641
|
outcome.applied.push(seed.id);
|
|
1581
1642
|
}
|
|
1582
1643
|
const config = io.read();
|
|
1583
1644
|
const currentList = roleRows(config);
|
|
1584
1645
|
const currentRules = roleRules(config);
|
|
1585
|
-
const newList = materialize(currentList,
|
|
1646
|
+
const newList = materialize(currentList, personaView(entries), personaView(this.registry), outcome.conflicts);
|
|
1586
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)`);
|
|
1587
1648
|
const computed = {
|
|
1588
1649
|
list: newList,
|
|
1589
1650
|
rules: currentRules
|
|
1590
1651
|
};
|
|
1591
1652
|
if (!deepEqual(newList, currentList)) await io.writeRoles(computed);
|
|
1592
|
-
this.registry =
|
|
1653
|
+
this.registry = entries;
|
|
1593
1654
|
return outcome;
|
|
1594
1655
|
}
|
|
1595
1656
|
effectiveRoles(io) {
|
|
1596
1657
|
return { roles: roleRows(io.read()).map((row) => {
|
|
1597
|
-
const
|
|
1598
|
-
const seeded =
|
|
1658
|
+
const entry = this.registry.get(row.id.trim());
|
|
1659
|
+
const seeded = entry !== void 0;
|
|
1599
1660
|
const effective = {
|
|
1600
1661
|
id: row.id,
|
|
1601
1662
|
persona: row.persona,
|
|
1602
1663
|
seeded,
|
|
1603
|
-
personaOverridden: seeded && row.persona !==
|
|
1664
|
+
personaOverridden: seeded && row.persona !== entry.persona,
|
|
1665
|
+
source: seeded ? entry.set : "user"
|
|
1604
1666
|
};
|
|
1605
|
-
if (seeded) effective.seedPersona =
|
|
1667
|
+
if (seeded) effective.seedPersona = entry.persona;
|
|
1606
1668
|
if (row.chain !== void 0) effective.chain = row.chain;
|
|
1607
1669
|
if (row.fallback !== void 0) effective.fallback = row.fallback;
|
|
1608
1670
|
return effective;
|
|
@@ -1611,19 +1673,20 @@ var FallbacksSeedManager = class {
|
|
|
1611
1673
|
wireStatus(io) {
|
|
1612
1674
|
const status = [];
|
|
1613
1675
|
for (const row of roleRows(io.read())) {
|
|
1614
|
-
const
|
|
1615
|
-
if (
|
|
1676
|
+
const entry = this.registry.get(row.id.trim());
|
|
1677
|
+
if (entry === void 0) continue;
|
|
1616
1678
|
status.push({
|
|
1617
1679
|
id: row.id,
|
|
1618
|
-
overridden: row.persona !==
|
|
1680
|
+
overridden: row.persona !== entry.persona,
|
|
1681
|
+
source: entry.set
|
|
1619
1682
|
});
|
|
1620
1683
|
}
|
|
1621
1684
|
return status;
|
|
1622
1685
|
}
|
|
1623
1686
|
async revert(id, io) {
|
|
1624
1687
|
const seedId = id.trim();
|
|
1625
|
-
const
|
|
1626
|
-
if (
|
|
1688
|
+
const entry = this.registry.get(seedId);
|
|
1689
|
+
if (entry === void 0) return {
|
|
1627
1690
|
reverted: false,
|
|
1628
1691
|
reason: "not-seeded"
|
|
1629
1692
|
};
|
|
@@ -1635,13 +1698,13 @@ var FallbacksSeedManager = class {
|
|
|
1635
1698
|
reverted: false,
|
|
1636
1699
|
reason: "row-absent"
|
|
1637
1700
|
};
|
|
1638
|
-
if (rows[index].persona ===
|
|
1701
|
+
if (rows[index].persona === entry.persona) return {
|
|
1639
1702
|
reverted: true,
|
|
1640
|
-
persona:
|
|
1703
|
+
persona: entry.persona
|
|
1641
1704
|
};
|
|
1642
1705
|
const nextList = rows.map((row, i) => i === index ? {
|
|
1643
1706
|
...row,
|
|
1644
|
-
persona:
|
|
1707
|
+
persona: entry.persona
|
|
1645
1708
|
} : row);
|
|
1646
1709
|
await io.writeRoles({
|
|
1647
1710
|
list: nextList,
|
|
@@ -1649,7 +1712,7 @@ var FallbacksSeedManager = class {
|
|
|
1649
1712
|
});
|
|
1650
1713
|
return {
|
|
1651
1714
|
reverted: true,
|
|
1652
|
-
persona:
|
|
1715
|
+
persona: entry.persona
|
|
1653
1716
|
};
|
|
1654
1717
|
}
|
|
1655
1718
|
warnSkip(id, reason) {
|
|
@@ -1659,6 +1722,26 @@ var FallbacksSeedManager = class {
|
|
|
1659
1722
|
else this.logger.warn(`llm-fallbacks: seeds: skipping seed id ${shown} — duplicate-in-batch (first wins)`);
|
|
1660
1723
|
}
|
|
1661
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
|
+
}
|
|
1662
1745
|
function materialize(rows, registry, previous, conflicts) {
|
|
1663
1746
|
const next = [];
|
|
1664
1747
|
for (const row of rows) {
|
|
@@ -1733,14 +1816,6 @@ const presetRoles = [
|
|
|
1733
1816
|
id: "scout",
|
|
1734
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."
|
|
1735
1818
|
},
|
|
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
1819
|
{
|
|
1745
1820
|
id: "reviewer",
|
|
1746
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."
|
|
@@ -2097,7 +2172,13 @@ function installTuiSettingsSection(ctx, opts) {
|
|
|
2097
2172
|
ctx.inject(["tuiSettingsSections"], (tctx) => {
|
|
2098
2173
|
const registry = tctx.tuiSettingsSections;
|
|
2099
2174
|
if (registry === void 0) return;
|
|
2100
|
-
|
|
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
|
+
}
|
|
2101
2182
|
});
|
|
2102
2183
|
}
|
|
2103
2184
|
//#endregion
|
|
@@ -2259,6 +2340,8 @@ function installFallbacksAdapter(ctx, readConfig) {
|
|
|
2259
2340
|
const name = "llm-fallbacks";
|
|
2260
2341
|
const provide = ["llm-fallbacks"];
|
|
2261
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]);
|
|
2262
2345
|
const stateStores = new WeakMap();
|
|
2263
2346
|
function stateStore(ctx) {
|
|
2264
2347
|
return stateStores.get(ctx);
|
|
@@ -2271,6 +2354,10 @@ const chainHeadStores = new WeakMap();
|
|
|
2271
2354
|
function chainHeads(ctx) {
|
|
2272
2355
|
return chainHeadStores.get(ctx);
|
|
2273
2356
|
}
|
|
2357
|
+
const subagentRoleRecordStores = new WeakMap();
|
|
2358
|
+
function subagentRoleRecords(ctx) {
|
|
2359
|
+
return subagentRoleRecordStores.get(ctx);
|
|
2360
|
+
}
|
|
2274
2361
|
function latestByAt(map) {
|
|
2275
2362
|
let latest;
|
|
2276
2363
|
for (const value of map.values()) if (latest === void 0 || value.at >= latest.at) latest = value;
|
|
@@ -2290,6 +2377,26 @@ async function makeModelExists(ctx, providers) {
|
|
|
2290
2377
|
}));
|
|
2291
2378
|
return (provider, model) => catalog.get(provider)?.has(model) ?? false;
|
|
2292
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
|
+
}
|
|
2293
2400
|
function countRetryEvents(session, turn, step, provider) {
|
|
2294
2401
|
let count = 0;
|
|
2295
2402
|
const events = session.snapshotEvents();
|
|
@@ -2367,6 +2474,24 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2367
2474
|
};
|
|
2368
2475
|
ctx.inject(["settings"], (sctx) => {
|
|
2369
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
|
+
}
|
|
2370
2495
|
return () => {
|
|
2371
2496
|
writeRoles = () => {
|
|
2372
2497
|
throw new Error(seedsSettingsUnavailable);
|
|
@@ -2377,25 +2502,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2377
2502
|
read: () => source(),
|
|
2378
2503
|
writeRoles: (roles) => writeRoles(roles)
|
|
2379
2504
|
};
|
|
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
|
-
}
|
|
2505
|
+
let serviceOwned = ctx.get("llm-fallbacks") === void 0;
|
|
2399
2506
|
let source = () => entry;
|
|
2400
2507
|
const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source());
|
|
2401
2508
|
validateFallbacksConfig(entry, logger);
|
|
@@ -2422,6 +2529,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2422
2529
|
chainHeadStores.set(ctx, chainHeadMap);
|
|
2423
2530
|
const blockedAttemptMap = new Map();
|
|
2424
2531
|
blockedAttemptStores.set(ctx, blockedAttemptMap);
|
|
2532
|
+
const subagentRoleRecordMap = new Map();
|
|
2533
|
+
subagentRoleRecordStores.set(ctx, subagentRoleRecordMap);
|
|
2425
2534
|
try {
|
|
2426
2535
|
new FallbacksConfigGateway(ctx, bridge, seeds, () => {
|
|
2427
2536
|
try {
|
|
@@ -2436,7 +2545,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2436
2545
|
} catch {
|
|
2437
2546
|
return { policy: { state: "unprovable" } };
|
|
2438
2547
|
}
|
|
2439
|
-
});
|
|
2548
|
+
}, () => subagentRoleRecordMap);
|
|
2440
2549
|
} catch (error) {
|
|
2441
2550
|
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
2442
2551
|
ctx.logger("llm-fallbacks").debug("fallbacks gateway already registered — no gateway on this fiber (multi-fiber dedupe)");
|
|
@@ -2455,7 +2564,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2455
2564
|
const dispatchInjected = new Set();
|
|
2456
2565
|
const slotWinners = new Map();
|
|
2457
2566
|
const lastKnownPolicySettings = new Map();
|
|
2458
|
-
async function decide(agent, turn, step, current, reason, state) {
|
|
2567
|
+
async function decide(agent, turn, step, current, reason, state, failureCode) {
|
|
2459
2568
|
const config = source();
|
|
2460
2569
|
if (state !== void 0) {
|
|
2461
2570
|
states.syncStep(state, turn, step);
|
|
@@ -2468,10 +2577,17 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2468
2577
|
const modelExists = hasWildcardEntry(config.roles.list, rootTail, role) ? await makeModelExists(ctx, [...new Set(all.map((candidate) => candidate.provider))]) : void 0;
|
|
2469
2578
|
const cooldown = { isSuppressed: (key) => state !== void 0 && states.isSuppressed(state, key, Date.now(), source().recovery ?? "timer") };
|
|
2470
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);
|
|
2471
2585
|
const surviving = selectCandidates(all, wildcard, createCandidateFilter({
|
|
2472
2586
|
current,
|
|
2473
2587
|
cooldown,
|
|
2474
|
-
failed
|
|
2588
|
+
failed,
|
|
2589
|
+
minContextWindow,
|
|
2590
|
+
contextWindowOf
|
|
2475
2591
|
}), modelExists);
|
|
2476
2592
|
const first = surviving[0];
|
|
2477
2593
|
let target;
|
|
@@ -2511,7 +2627,9 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2511
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, {
|
|
2512
2628
|
current,
|
|
2513
2629
|
cooldown,
|
|
2514
|
-
failed
|
|
2630
|
+
failed,
|
|
2631
|
+
minContextWindow,
|
|
2632
|
+
contextWindowOf
|
|
2515
2633
|
}).map(({ candidate, skip }) => skip === void 0 ? `${candidate.provider}/${candidate.model}` : `${candidate.provider}/${candidate.model} (skipped: ${skip})`));
|
|
2516
2634
|
return {
|
|
2517
2635
|
from: {
|
|
@@ -2529,10 +2647,12 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2529
2647
|
function commit(state, pending, turn, step) {
|
|
2530
2648
|
const config = source();
|
|
2531
2649
|
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
2650
|
states.syncStep(state, turn, step);
|
|
2534
2651
|
states.writePending(state, pending);
|
|
2535
|
-
|
|
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
|
+
}
|
|
2536
2656
|
states.recordFailure(state, fromKey);
|
|
2537
2657
|
states.recordSwitch(state);
|
|
2538
2658
|
}
|
|
@@ -2551,12 +2671,16 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2551
2671
|
const current = currentModel(agent, provider);
|
|
2552
2672
|
if (!current.model) return next();
|
|
2553
2673
|
try {
|
|
2554
|
-
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);
|
|
2555
2675
|
if (pending === null) {
|
|
2556
2676
|
failHalfOpenProbe(agent.id, current);
|
|
2557
2677
|
return next();
|
|
2558
2678
|
}
|
|
2559
|
-
|
|
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);
|
|
2560
2684
|
return { kind: "retry" };
|
|
2561
2685
|
} catch (error) {
|
|
2562
2686
|
logger.warn("llm-fallbacks: decision path failed, passing the original failure through: %s", error?.message ?? String(error));
|
|
@@ -2628,6 +2752,14 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2628
2752
|
model: head.model
|
|
2629
2753
|
};
|
|
2630
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
|
+
});
|
|
2631
2763
|
if (to !== void 0 && !(to.provider === seed.provider && to.model === seed.model)) {
|
|
2632
2764
|
if (policy.state === "enabled") chainHeadMap.set(agent.id, {
|
|
2633
2765
|
at: Date.now(),
|
|
@@ -2676,6 +2808,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2676
2808
|
slotWinners.delete(agent.id);
|
|
2677
2809
|
blockedAttemptMap.delete(agent.id);
|
|
2678
2810
|
chainHeadMap.delete(agent.id);
|
|
2811
|
+
subagentRoleRecordMap.delete(agent.id);
|
|
2679
2812
|
lastKnownPolicySettings.delete(agent.id);
|
|
2680
2813
|
});
|
|
2681
2814
|
ctx.on("session/event", (session, event) => {
|
|
@@ -2694,6 +2827,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2694
2827
|
slotWinners.clear();
|
|
2695
2828
|
blockedAttemptMap.clear();
|
|
2696
2829
|
chainHeadMap.clear();
|
|
2830
|
+
subagentRoleRecordMap.clear();
|
|
2697
2831
|
lastKnownPolicySettings.clear();
|
|
2698
2832
|
}, "llm-fallbacks: clear per-agent state");
|
|
2699
2833
|
const fallbacksCommandController = {
|
|
@@ -2766,10 +2900,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2766
2900
|
ctx.inject(["settings"], () => {
|
|
2767
2901
|
if (!serviceOwned) return;
|
|
2768
2902
|
if (seedsIo.read().presets === "none") return;
|
|
2769
|
-
seeds.declare(presetRoles, seedsIo).catch((error) => {
|
|
2903
|
+
seeds.declare(presetRoles, seedsIo, { bundled: true }).catch((error) => {
|
|
2770
2904
|
logger.error("llm-fallbacks: seeds: preset role declaration failed — %s", error?.message ?? String(error));
|
|
2771
2905
|
});
|
|
2772
2906
|
});
|
|
2773
2907
|
}
|
|
2774
2908
|
//#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 };
|
|
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[];
|
package/dist/seeds.d.ts
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
* State model (spec §9.2) — two stores, strictly separated:
|
|
12
12
|
* 1. operator config rows (persisted; a seeded role is a plain
|
|
13
13
|
* `roles.list` row `{ id, persona }`), and
|
|
14
|
-
* 2. an in-memory per-apply seed registry (`Map<id,
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* 2. an in-memory per-apply seed registry (`Map<id, { persona, set }>` —
|
|
15
|
+
* the declared default persona plus its provenance label), declare =
|
|
16
|
+
* replacement (the batch is the companion's full current set).
|
|
17
|
+
* `seeded` / `personaOverridden` / `source` are DERIVED at read time, never
|
|
18
|
+
* stored — a config round-trip cannot orphan an override (AC-3), and
|
|
19
|
+
* provenance rides the in-memory registry only, so it can never churn a
|
|
20
|
+
* settings write.
|
|
18
21
|
*
|
|
19
22
|
* Materialization (spec §9.2): append `{ id, persona }` (two keys only,
|
|
20
23
|
* R4) / attach row untouched / at-default tracking / override preserved +
|
|
@@ -31,6 +34,41 @@ export interface SeedDeclaration {
|
|
|
31
34
|
id: string;
|
|
32
35
|
persona: string;
|
|
33
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Per-row provenance label (spec §2). The fixed labels: `bundled` (the
|
|
39
|
+
* plugin's own bundled preset self-declare), `external` (a companion
|
|
40
|
+
* declare without a set name — or with an invalid one, degraded), `user`
|
|
41
|
+
* (no live declaration). Any other value is a declared set name; the
|
|
42
|
+
* reserved names can never be declared as a set, so the fixed labels are
|
|
43
|
+
* unambiguous.
|
|
44
|
+
*/
|
|
45
|
+
export type SeedSource = 'bundled' | 'external' | 'user' | (string & {});
|
|
46
|
+
/**
|
|
47
|
+
* Optional provenance for one `declare()` call (spec §2) — the PUBLIC
|
|
48
|
+
* options face (a companion's registered set name). This is also the type
|
|
49
|
+
* of the `FallbacksService.declareSeeds` second parameter: consumers can
|
|
50
|
+
* never label a declare `bundled` (reserved).
|
|
51
|
+
*/
|
|
52
|
+
export interface SeedsDeclareOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Registered set name — trimmed. Empty after trim, non-string, or a
|
|
55
|
+
* reserved value (`bundled` / `user` / `external`) warns once and
|
|
56
|
+
* degrades the batch to the unnamed `external` source; the seeds still
|
|
57
|
+
* apply (a bad label must never drop roles).
|
|
58
|
+
*/
|
|
59
|
+
set?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The manager-level declare options — the public face plus the INTERNAL
|
|
63
|
+
* bundled marker for the plugin's own preset self-declare. Module-internal
|
|
64
|
+
* (never exported, never re-exported from the package root): only the
|
|
65
|
+
* preset child call site passes `bundled`, and only a structurally-checked
|
|
66
|
+
* object literal at that — the type itself is not nameable by consumers.
|
|
67
|
+
*/
|
|
68
|
+
interface SeedsInternalDeclareOptions extends SeedsDeclareOptions {
|
|
69
|
+
/** INTERNAL — bundled preset self-declare provenance. Not a consumer option. */
|
|
70
|
+
bundled?: true;
|
|
71
|
+
}
|
|
34
72
|
/**
|
|
35
73
|
* Why one declared id was skipped (spec §9.1). Per-id skip + warn, never
|
|
36
74
|
* coercion — valid siblings in the same batch still apply (AC-5).
|
|
@@ -68,6 +106,8 @@ export interface EffectiveRole {
|
|
|
68
106
|
seeded: boolean;
|
|
69
107
|
/** `seeded` && row persona !== current seed default. */
|
|
70
108
|
personaOverridden: boolean;
|
|
109
|
+
/** Where the row's seed state comes from (read-time derived, never persisted) — `user` iff not seeded. */
|
|
110
|
+
source: SeedSource;
|
|
71
111
|
/** Present iff seeded. */
|
|
72
112
|
seedPersona?: string;
|
|
73
113
|
}
|
|
@@ -86,6 +126,12 @@ export interface SeedRevertOutcome {
|
|
|
86
126
|
export interface SeedsWireStatus {
|
|
87
127
|
id: string;
|
|
88
128
|
overridden: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Provenance label (spec §2) — additive and OPTIONAL on the wire: a
|
|
131
|
+
* gateway predating the field (version skew) sends entries without it;
|
|
132
|
+
* consumers treat a missing label as "no badge".
|
|
133
|
+
*/
|
|
134
|
+
source?: SeedSource;
|
|
89
135
|
}
|
|
90
136
|
/**
|
|
91
137
|
* The narrow IO seam this module writes through — keeps `src/seeds.ts`
|
|
@@ -105,6 +151,7 @@ export interface SeedsIo {
|
|
|
105
151
|
*/
|
|
106
152
|
export declare class FallbacksSeedManager {
|
|
107
153
|
private readonly logger;
|
|
154
|
+
/** Per-apply declaration registry: declared default persona + provenance label. */
|
|
108
155
|
private registry;
|
|
109
156
|
constructor(logger: FallbacksConfigLogger);
|
|
110
157
|
/**
|
|
@@ -112,6 +159,10 @@ export declare class FallbacksSeedManager {
|
|
|
112
159
|
* companion's FULL current declaration set; ids omitted from the batch
|
|
113
160
|
* drop out of the registry while their rows remain (R2).
|
|
114
161
|
*
|
|
162
|
+
* `options` labels the whole batch's provenance (spec §2), resolved once
|
|
163
|
+
* per call: an invalid set name warns once and degrades to the unnamed
|
|
164
|
+
* `external` case — the seeds still apply.
|
|
165
|
+
*
|
|
115
166
|
* Per-id validation AS DECLARED (spec §9.3): non-string / pattern miss /
|
|
116
167
|
* reserved `inherit` / duplicate-in-batch → skip + warn; valid siblings
|
|
117
168
|
* still apply (AC-5). Materializes per spec §9.2, writes only when the
|
|
@@ -119,15 +170,17 @@ export declare class FallbacksSeedManager {
|
|
|
119
170
|
* (idempotent, AC-1), and commits the registry only after a successful
|
|
120
171
|
* write (compute → write → commit; retry-safe).
|
|
121
172
|
*/
|
|
122
|
-
declare(seeds: readonly SeedDeclaration[], io: SeedsIo): Promise<SeedDeclareOutcome>;
|
|
173
|
+
declare(seeds: readonly SeedDeclaration[], io: SeedsIo, options?: SeedsInternalDeclareOptions): Promise<SeedDeclareOutcome>;
|
|
123
174
|
/**
|
|
124
175
|
* Readback (b) — sync, derived: every config row annotated with
|
|
125
176
|
* `seeded` / `personaOverridden` / `seedPersona` (trimmed row-id
|
|
126
|
-
* membership in the live declaration set; persona inequality)
|
|
177
|
+
* membership in the live declaration set; persona inequality) plus the
|
|
178
|
+
* row's provenance `source` (spec §2): the declaring set's label, or
|
|
179
|
+
* `user` when no live declaration covers the row. Nothing
|
|
127
180
|
* override-shaped is stored, so a config round-trip cannot orphan state.
|
|
128
181
|
*/
|
|
129
182
|
effectiveRoles(io: SeedsIo): EffectiveRolesReadback;
|
|
130
|
-
/** Card badge state (spec §9.4): seeded rows, with the override flag. */
|
|
183
|
+
/** Card badge state (spec §9.4): seeded rows, with the override flag and the provenance label. */
|
|
131
184
|
wireStatus(io: SeedsIo): SeedsWireStatus[];
|
|
132
185
|
/**
|
|
133
186
|
* Revert one id to the CURRENT declared seed default (AC-3). Writes
|
|
@@ -139,3 +192,4 @@ export declare class FallbacksSeedManager {
|
|
|
139
192
|
revert(id: string, io: SeedsIo): Promise<SeedRevertOutcome>;
|
|
140
193
|
private warnSkip;
|
|
141
194
|
}
|
|
195
|
+
export {};
|