dsh-llm-fallbacks 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -24
- package/README.zh-CN.md +81 -24
- package/dist/client/index.js +12 -8
- package/dist/client/locales.d.ts +2 -0
- package/dist/events.d.ts +2 -2
- package/dist/index.d.ts +6 -4
- package/dist/index.js +156 -63
- package/dist/role-resolution.d.ts +11 -0
- package/dist/roles.d.ts +4 -1
- package/dist/seeds.d.ts +63 -21
- package/dist/time-slots.d.ts +3 -2
- package/dist/virtual-adapter.d.ts +42 -6
- package/package.json +4 -3
- package/scripts/repair-session-logs.ts +1858 -0
- package/scripts/session-logs/catalog.ts +547 -0
- package/scripts/session-logs/classify.ts +47 -0
- package/scripts/session-logs/publish.ts +530 -0
- package/scripts/session-logs/rules.ts +1227 -0
- package/scripts/repair-fallbacks-switch-logs.ts +0 -342
package/dist/index.js
CHANGED
|
@@ -668,11 +668,15 @@ function annotateCandidates(candidates, surviving, options) {
|
|
|
668
668
|
}
|
|
669
669
|
//#endregion
|
|
670
670
|
//#region .build/host/roles.js
|
|
671
|
-
function resolveRole(agent, rules, roleIds, warn = console.warn) {
|
|
671
|
+
function resolveRole(agent, rules, roleIds, warn = console.warn, servedRoute) {
|
|
672
672
|
if ((agent.session?.header?.origin ?? "root") !== "subagent") return INHERIT_ROLE_ID;
|
|
673
|
+
const recordedRoute = {
|
|
674
|
+
provider: agent.options?.provider,
|
|
675
|
+
model: agent.options?.model
|
|
676
|
+
};
|
|
673
677
|
for (const rule of rules) {
|
|
674
|
-
|
|
675
|
-
if (
|
|
678
|
+
const matchesPair = (pair) => (!rule.provider || rule.provider === pair.provider) && (!rule.model || rule.model === pair.model);
|
|
679
|
+
if (!matchesPair(recordedRoute) && !(servedRoute !== void 0 && matchesPair(servedRoute))) continue;
|
|
676
680
|
const target = rule.role.trim();
|
|
677
681
|
if (target === "inherit") return INHERIT_ROLE_ID;
|
|
678
682
|
const declared = roleIds.get(target);
|
|
@@ -692,7 +696,7 @@ async function resolveRoleAtDispatch(agent, rules, roleIds, opts) {
|
|
|
692
696
|
const declared = roleIds.get(preset);
|
|
693
697
|
if (declared !== void 0) return declared;
|
|
694
698
|
}
|
|
695
|
-
const role = resolveRole(agent, rules, roleIds, opts.warn);
|
|
699
|
+
const role = resolveRole(agent, rules, roleIds, opts.warn, opts.servedRoute);
|
|
696
700
|
if (role !== "inherit") return role;
|
|
697
701
|
if (opts.automatchEnabled && opts.automatch !== void 0) {
|
|
698
702
|
const picked = await opts.automatch(agent);
|
|
@@ -1601,6 +1605,7 @@ function registerFallbacksCommands(registry, controller, locale = "zh") {
|
|
|
1601
1605
|
var FallbacksSeedManager = class {
|
|
1602
1606
|
logger;
|
|
1603
1607
|
registry = new Map();
|
|
1608
|
+
declareSeq = 0;
|
|
1604
1609
|
constructor(logger) {
|
|
1605
1610
|
this.logger = logger;
|
|
1606
1611
|
}
|
|
@@ -1646,28 +1651,69 @@ var FallbacksSeedManager = class {
|
|
|
1646
1651
|
const config = io.read();
|
|
1647
1652
|
const currentList = roleRows(config);
|
|
1648
1653
|
const currentRules = roleRules(config);
|
|
1649
|
-
const
|
|
1654
|
+
const candidate = new Map(this.registry);
|
|
1655
|
+
if (entries.size === 0) candidate.delete(source);
|
|
1656
|
+
else candidate.set(source, {
|
|
1657
|
+
seq: this.declareSeq + 1,
|
|
1658
|
+
entries
|
|
1659
|
+
});
|
|
1660
|
+
const prior = new Map();
|
|
1661
|
+
const incoming = new Map();
|
|
1662
|
+
for (const id of entries.keys()) {
|
|
1663
|
+
const before = this.resolveDeclared(id)?.persona;
|
|
1664
|
+
const after = this.resolveDeclared(id, candidate)?.persona;
|
|
1665
|
+
if (before !== void 0) prior.set(id, before);
|
|
1666
|
+
if (after !== void 0) incoming.set(id, after);
|
|
1667
|
+
}
|
|
1668
|
+
const newList = materialize(currentList, incoming, prior, outcome.conflicts);
|
|
1650
1669
|
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)`);
|
|
1651
1670
|
const computed = {
|
|
1652
1671
|
list: newList,
|
|
1653
1672
|
rules: currentRules
|
|
1654
1673
|
};
|
|
1655
1674
|
if (!deepEqual(newList, currentList)) await io.writeRoles(computed);
|
|
1656
|
-
this.registry
|
|
1675
|
+
if (entries.size === 0) this.registry.delete(source);
|
|
1676
|
+
else this.registry.set(source, {
|
|
1677
|
+
seq: ++this.declareSeq,
|
|
1678
|
+
entries
|
|
1679
|
+
});
|
|
1657
1680
|
return outcome;
|
|
1658
1681
|
}
|
|
1682
|
+
resolveDeclared(id, registry = this.registry) {
|
|
1683
|
+
const bundled = registry.get("bundled")?.entries.get(id);
|
|
1684
|
+
if (bundled !== void 0) return {
|
|
1685
|
+
persona: bundled.persona,
|
|
1686
|
+
source: "bundled"
|
|
1687
|
+
};
|
|
1688
|
+
let winner;
|
|
1689
|
+
for (const [label, slice] of registry) {
|
|
1690
|
+
if (label === "bundled") continue;
|
|
1691
|
+
const entry = slice.entries.get(id);
|
|
1692
|
+
if (entry === void 0) continue;
|
|
1693
|
+
if (winner === void 0 || slice.seq > winner.seq) winner = {
|
|
1694
|
+
persona: entry.persona,
|
|
1695
|
+
source: label,
|
|
1696
|
+
seq: slice.seq
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
if (winner === void 0) return void 0;
|
|
1700
|
+
return {
|
|
1701
|
+
persona: winner.persona,
|
|
1702
|
+
source: winner.source
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1659
1705
|
effectiveRoles(io) {
|
|
1660
1706
|
return { roles: roleRows(io.read()).map((row) => {
|
|
1661
|
-
const
|
|
1662
|
-
const seeded =
|
|
1707
|
+
const declared = this.resolveDeclared(row.id.trim());
|
|
1708
|
+
const seeded = declared !== void 0;
|
|
1663
1709
|
const effective = {
|
|
1664
1710
|
id: row.id,
|
|
1665
1711
|
persona: row.persona,
|
|
1666
1712
|
seeded,
|
|
1667
|
-
personaOverridden: seeded && row.persona !==
|
|
1668
|
-
source: seeded ?
|
|
1713
|
+
personaOverridden: seeded && row.persona !== declared.persona,
|
|
1714
|
+
source: seeded ? declared.source : "user"
|
|
1669
1715
|
};
|
|
1670
|
-
if (seeded) effective.seedPersona =
|
|
1716
|
+
if (seeded) effective.seedPersona = declared.persona;
|
|
1671
1717
|
if (row.chain !== void 0) effective.chain = row.chain;
|
|
1672
1718
|
if (row.fallback !== void 0) effective.fallback = row.fallback;
|
|
1673
1719
|
return effective;
|
|
@@ -1676,20 +1722,20 @@ var FallbacksSeedManager = class {
|
|
|
1676
1722
|
wireStatus(io) {
|
|
1677
1723
|
const status = [];
|
|
1678
1724
|
for (const row of roleRows(io.read())) {
|
|
1679
|
-
const
|
|
1680
|
-
if (
|
|
1725
|
+
const declared = this.resolveDeclared(row.id.trim());
|
|
1726
|
+
if (declared === void 0) continue;
|
|
1681
1727
|
status.push({
|
|
1682
1728
|
id: row.id,
|
|
1683
|
-
overridden: row.persona !==
|
|
1684
|
-
source:
|
|
1729
|
+
overridden: row.persona !== declared.persona,
|
|
1730
|
+
source: declared.source
|
|
1685
1731
|
});
|
|
1686
1732
|
}
|
|
1687
1733
|
return status;
|
|
1688
1734
|
}
|
|
1689
1735
|
async revert(id, io) {
|
|
1690
1736
|
const seedId = id.trim();
|
|
1691
|
-
const
|
|
1692
|
-
if (
|
|
1737
|
+
const declared = this.resolveDeclared(seedId);
|
|
1738
|
+
if (declared === void 0) return {
|
|
1693
1739
|
reverted: false,
|
|
1694
1740
|
reason: "not-seeded"
|
|
1695
1741
|
};
|
|
@@ -1701,13 +1747,13 @@ var FallbacksSeedManager = class {
|
|
|
1701
1747
|
reverted: false,
|
|
1702
1748
|
reason: "row-absent"
|
|
1703
1749
|
};
|
|
1704
|
-
if (rows[index].persona ===
|
|
1750
|
+
if (rows[index].persona === declared.persona) return {
|
|
1705
1751
|
reverted: true,
|
|
1706
|
-
persona:
|
|
1752
|
+
persona: declared.persona
|
|
1707
1753
|
};
|
|
1708
1754
|
const nextList = rows.map((row, i) => i === index ? {
|
|
1709
1755
|
...row,
|
|
1710
|
-
persona:
|
|
1756
|
+
persona: declared.persona
|
|
1711
1757
|
} : row);
|
|
1712
1758
|
await io.writeRoles({
|
|
1713
1759
|
list: nextList,
|
|
@@ -1715,7 +1761,7 @@ var FallbacksSeedManager = class {
|
|
|
1715
1761
|
});
|
|
1716
1762
|
return {
|
|
1717
1763
|
reverted: true,
|
|
1718
|
-
persona:
|
|
1764
|
+
persona: declared.persona
|
|
1719
1765
|
};
|
|
1720
1766
|
}
|
|
1721
1767
|
warnSkip(id, reason) {
|
|
@@ -1740,43 +1786,38 @@ function resolveSource(options, logger) {
|
|
|
1740
1786
|
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
1787
|
return "external";
|
|
1742
1788
|
}
|
|
1743
|
-
function
|
|
1744
|
-
const personas = new Map();
|
|
1745
|
-
for (const [id, entry] of entries) personas.set(id, entry.persona);
|
|
1746
|
-
return personas;
|
|
1747
|
-
}
|
|
1748
|
-
function materialize(rows, registry, previous, conflicts) {
|
|
1789
|
+
function materialize(rows, incoming, prior, conflicts) {
|
|
1749
1790
|
const next = [];
|
|
1750
1791
|
for (const row of rows) {
|
|
1751
1792
|
const seedId = row.id.trim();
|
|
1752
|
-
const
|
|
1753
|
-
if (
|
|
1793
|
+
const inc = incoming.get(seedId);
|
|
1794
|
+
if (inc === void 0) {
|
|
1754
1795
|
next.push(row);
|
|
1755
1796
|
continue;
|
|
1756
1797
|
}
|
|
1757
|
-
const
|
|
1758
|
-
if (
|
|
1759
|
-
if (row.persona !==
|
|
1798
|
+
const before = prior.get(seedId);
|
|
1799
|
+
if (before === void 0) {
|
|
1800
|
+
if (row.persona !== inc) conflicts.push({
|
|
1760
1801
|
id: seedId,
|
|
1761
1802
|
kind: "persona-source"
|
|
1762
1803
|
});
|
|
1763
1804
|
next.push(row);
|
|
1764
1805
|
continue;
|
|
1765
1806
|
}
|
|
1766
|
-
if (row.persona ===
|
|
1807
|
+
if (row.persona === before && inc !== before) {
|
|
1767
1808
|
next.push({
|
|
1768
1809
|
...row,
|
|
1769
|
-
persona:
|
|
1810
|
+
persona: inc
|
|
1770
1811
|
});
|
|
1771
1812
|
continue;
|
|
1772
1813
|
}
|
|
1773
|
-
if (row.persona !==
|
|
1814
|
+
if (row.persona !== inc) conflicts.push({
|
|
1774
1815
|
id: seedId,
|
|
1775
1816
|
kind: "persona-source"
|
|
1776
1817
|
});
|
|
1777
1818
|
next.push(row);
|
|
1778
1819
|
}
|
|
1779
|
-
for (const [id, persona] of
|
|
1820
|
+
for (const [id, persona] of incoming) if (!rows.some((row) => row.id.trim() === id)) next.push({
|
|
1780
1821
|
id,
|
|
1781
1822
|
persona
|
|
1782
1823
|
});
|
|
@@ -2230,6 +2271,37 @@ async function resolveHeadDisplayName(llm, head) {
|
|
|
2230
2271
|
} catch {}
|
|
2231
2272
|
return head.model;
|
|
2232
2273
|
}
|
|
2274
|
+
function envelopeProvenance(replayState) {
|
|
2275
|
+
if (typeof replayState !== "object" || replayState === null) return void 0;
|
|
2276
|
+
const response = replayState.response;
|
|
2277
|
+
if (typeof response !== "object" || response === null) return void 0;
|
|
2278
|
+
const { provider, model } = response;
|
|
2279
|
+
if (typeof provider !== "string" || provider === "" || provider === "FallbacksChain") return void 0;
|
|
2280
|
+
if (typeof model !== "string" || model === "") return void 0;
|
|
2281
|
+
return {
|
|
2282
|
+
provider,
|
|
2283
|
+
model
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
function restoreEnvelopeProvenance(messages) {
|
|
2287
|
+
let changed = false;
|
|
2288
|
+
const restored = messages.map((message) => {
|
|
2289
|
+
const source = message.source;
|
|
2290
|
+
if (message.role !== "assistant" || source.kind !== "model") return message;
|
|
2291
|
+
if (source.provider !== "FallbacksChain" || source.model !== "Auto") return message;
|
|
2292
|
+
const provenance = envelopeProvenance(source.replayState);
|
|
2293
|
+
if (provenance === void 0) return message;
|
|
2294
|
+
changed = true;
|
|
2295
|
+
return {
|
|
2296
|
+
...message,
|
|
2297
|
+
source: {
|
|
2298
|
+
...source,
|
|
2299
|
+
...provenance
|
|
2300
|
+
}
|
|
2301
|
+
};
|
|
2302
|
+
});
|
|
2303
|
+
return changed ? restored : messages;
|
|
2304
|
+
}
|
|
2233
2305
|
var FallbacksChainAdapter = class extends LlmAdapter {
|
|
2234
2306
|
readConfig;
|
|
2235
2307
|
getLlm;
|
|
@@ -2277,6 +2349,15 @@ var FallbacksChainAdapter = class extends LlmAdapter {
|
|
|
2277
2349
|
name: model
|
|
2278
2350
|
};
|
|
2279
2351
|
}
|
|
2352
|
+
providerRetryPolicy(_provider) {
|
|
2353
|
+
try {
|
|
2354
|
+
const head = effectiveHeadOf(this.readConfig(), new Date());
|
|
2355
|
+
if (head === void 0 || head.provider === "FallbacksChain") return void 0;
|
|
2356
|
+
return this.getLlm()?.providerRetryPolicy(head.provider);
|
|
2357
|
+
} catch {
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2280
2361
|
imageRequestPricing(_provider, _model) {
|
|
2281
2362
|
try {
|
|
2282
2363
|
const head = effectiveHeadOf(this.readConfig(), new Date());
|
|
@@ -2297,7 +2378,8 @@ var FallbacksChainAdapter = class extends LlmAdapter {
|
|
|
2297
2378
|
return llm.stream({
|
|
2298
2379
|
...options,
|
|
2299
2380
|
provider: head.provider,
|
|
2300
|
-
model: head.model
|
|
2381
|
+
model: head.model,
|
|
2382
|
+
messages: restoreEnvelopeProvenance(options.messages)
|
|
2301
2383
|
});
|
|
2302
2384
|
}
|
|
2303
2385
|
};
|
|
@@ -2412,11 +2494,29 @@ function countRetryEvents(session, turn, step, provider) {
|
|
|
2412
2494
|
}
|
|
2413
2495
|
return count;
|
|
2414
2496
|
}
|
|
2415
|
-
function
|
|
2416
|
-
return
|
|
2497
|
+
function anchorServedRoute(config, route, now) {
|
|
2498
|
+
if (route.provider !== "FallbacksChain" || route.model !== "Auto") return route;
|
|
2499
|
+
const head = effectiveHeadOf(config, now);
|
|
2500
|
+
return head === void 0 ? route : {
|
|
2501
|
+
provider: head.provider,
|
|
2502
|
+
model: head.model
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
function servedRouteFor(agent, config) {
|
|
2506
|
+
const provider = agent.options?.provider;
|
|
2507
|
+
const model = agent.options?.model;
|
|
2508
|
+
if (provider === void 0 || model === void 0) return void 0;
|
|
2509
|
+
const anchored = anchorServedRoute(config, {
|
|
2510
|
+
provider,
|
|
2511
|
+
model
|
|
2512
|
+
}, new Date());
|
|
2513
|
+
return anchored.provider === provider && anchored.model === model ? void 0 : anchored;
|
|
2514
|
+
}
|
|
2515
|
+
function currentModel(agent, provider, config, now) {
|
|
2516
|
+
return anchorServedRoute(config, {
|
|
2417
2517
|
provider,
|
|
2418
2518
|
model: agent.session.requestHeader()?.config.model ?? agent.options.model ?? ""
|
|
2419
|
-
};
|
|
2519
|
+
}, now);
|
|
2420
2520
|
}
|
|
2421
2521
|
function slotWinnerKey(winner) {
|
|
2422
2522
|
if (winner === "all-day") return "all-day";
|
|
@@ -2573,7 +2673,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2573
2673
|
states.syncStep(state, turn, step);
|
|
2574
2674
|
if (state.stepFailures.switchCount >= config.maxSwitchesPerStep) return null;
|
|
2575
2675
|
}
|
|
2576
|
-
const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn);
|
|
2676
|
+
const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn, servedRouteFor(agent, config));
|
|
2577
2677
|
const rootTail = agent.session?.header?.origin === "subagent" ? config.rootChain : resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai");
|
|
2578
2678
|
const { all, wildcard } = resolveChainViews(config.roles.list, rootTail, role, current.provider, current.model, logger.warn);
|
|
2579
2679
|
if (all.length === 0) return null;
|
|
@@ -2671,7 +2771,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2671
2771
|
ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure }, next) => {
|
|
2672
2772
|
const config = source();
|
|
2673
2773
|
if (!config.enabled || !config.triggerCodes.includes(failure.code)) return next();
|
|
2674
|
-
const current = currentModel(agent, provider);
|
|
2774
|
+
const current = currentModel(agent, provider, config, new Date());
|
|
2675
2775
|
if (!current.model) return next();
|
|
2676
2776
|
try {
|
|
2677
2777
|
const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id), failure.code);
|
|
@@ -2706,17 +2806,6 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2706
2806
|
label: slot.label
|
|
2707
2807
|
});
|
|
2708
2808
|
}
|
|
2709
|
-
if (config.enabled && seed.provider === "FallbacksChain" && seed.model === "Auto" && agent.session?.header?.origin !== "subagent") {
|
|
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");
|
|
2711
|
-
else {
|
|
2712
|
-
const head = firstDispatchableExactHead(resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai"));
|
|
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");
|
|
2714
|
-
else {
|
|
2715
|
-
logger.info("llm-fallbacks: FallbacksChain/Auto selection overrides to the effective head %s/%s", head.provider, head.model);
|
|
2716
|
-
return overrideConfig(seed, head);
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
}
|
|
2720
2809
|
if (config.enabled && hasChains && agent.session?.header?.origin === "subagent" && !dispatchInjected.has(agent.id)) {
|
|
2721
2810
|
dispatchInjected.add(agent.id);
|
|
2722
2811
|
try {
|
|
@@ -2739,7 +2828,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2739
2828
|
const role = await resolveRoleAtDispatch(agent, config.roles.rules, roleIds, {
|
|
2740
2829
|
automatchEnabled: config.roleAutoMatch ?? true,
|
|
2741
2830
|
automatch: (agent) => pickRoleByLlm(ctx, config.roles, agent, { warn: logger.warn }),
|
|
2742
|
-
warn: logger.warn
|
|
2831
|
+
warn: logger.warn,
|
|
2832
|
+
servedRoute: servedRouteFor(agent, config)
|
|
2743
2833
|
});
|
|
2744
2834
|
if (role !== "inherit") {
|
|
2745
2835
|
const { all, wildcard } = resolveChainViews(config.roles.list, config.rootChain, role, seed.provider, seed.model, logger.warn);
|
|
@@ -2784,19 +2874,17 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2784
2874
|
}
|
|
2785
2875
|
if (hasChains && config.enabled && config.alwaysModeRetryCap > 0 && countRetryEvents(agent.session, turn, step, seed.provider) >= config.alwaysModeRetryCap) {
|
|
2786
2876
|
const decisionState = states.peek(agent.id);
|
|
2787
|
-
const
|
|
2877
|
+
const capRoute = anchorServedRoute(config, {
|
|
2788
2878
|
provider: seed.provider,
|
|
2789
2879
|
model: seed.model
|
|
2790
|
-
},
|
|
2880
|
+
}, new Date());
|
|
2881
|
+
const pending = await decide(agent, turn, step, capRoute, "always-cap", decisionState);
|
|
2791
2882
|
if (pending !== null) {
|
|
2792
2883
|
const commitState = states.get(agent.id);
|
|
2793
2884
|
commit(commitState, pending, turn, step);
|
|
2794
2885
|
const appliedCap = states.applyPending(commitState, turn, step);
|
|
2795
2886
|
if (appliedCap !== void 0) return overrideConfig(seed, appliedCap.to);
|
|
2796
|
-
} else failHalfOpenProbe(agent.id,
|
|
2797
|
-
provider: seed.provider,
|
|
2798
|
-
model: seed.model
|
|
2799
|
-
});
|
|
2887
|
+
} else failHalfOpenProbe(agent.id, capRoute);
|
|
2800
2888
|
}
|
|
2801
2889
|
return seed;
|
|
2802
2890
|
});
|
|
@@ -2816,13 +2904,18 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2816
2904
|
});
|
|
2817
2905
|
ctx.on("session/event", (session, event) => {
|
|
2818
2906
|
if (event.type !== "assistant/message") return;
|
|
2819
|
-
|
|
2907
|
+
const config = source();
|
|
2908
|
+
if ((config.recovery ?? "timer") !== "half-open") return;
|
|
2820
2909
|
if (event.data.interrupted === true) return;
|
|
2821
2910
|
const message = event.data.message;
|
|
2822
2911
|
if (message.source.kind !== "model") return;
|
|
2823
2912
|
const state = states.peek(session.id);
|
|
2824
2913
|
if (state === void 0) return;
|
|
2825
|
-
|
|
2914
|
+
const observed = anchorServedRoute(config, {
|
|
2915
|
+
provider: message.source.provider,
|
|
2916
|
+
model: message.source.model
|
|
2917
|
+
}, new Date());
|
|
2918
|
+
states.observeSuccess(state, selectorKey(observed.provider, observed.model));
|
|
2826
2919
|
});
|
|
2827
2920
|
ctx.effect(() => () => {
|
|
2828
2921
|
states.clear();
|
|
@@ -2836,7 +2929,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
2836
2929
|
const fallbacksCommandController = {
|
|
2837
2930
|
getSnapshot(agent) {
|
|
2838
2931
|
const config = source();
|
|
2839
|
-
const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn);
|
|
2932
|
+
const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn, servedRouteFor(agent, config));
|
|
2840
2933
|
const state = states.peek(agent.id);
|
|
2841
2934
|
const recovery = config.recovery ?? "timer";
|
|
2842
2935
|
if (state !== void 0 && recovery === "half-open") states.syncRecovery(state, Date.now(), recovery);
|
|
@@ -51,6 +51,17 @@ export interface ResolveRoleAtDispatchOptions {
|
|
|
51
51
|
automatch?: (agent: AgentLike) => Promise<string | null>;
|
|
52
52
|
/** Warning sink — the decision path injects the plugin logger. */
|
|
53
53
|
warn: (message: string) => void;
|
|
54
|
+
/**
|
|
55
|
+
* The route the agent's request was actually SERVED by, when that differs
|
|
56
|
+
* from the pair recorded on the agent (the virtual `FallbacksChain/Auto`
|
|
57
|
+
* row). Rule matching accepts either, so a rule keyed on the real chain head
|
|
58
|
+
* keeps matching a child whose recorded route is the virtual pair (QC1 I-1).
|
|
59
|
+
* `undefined` for every real route.
|
|
60
|
+
*/
|
|
61
|
+
servedRoute?: {
|
|
62
|
+
provider: string;
|
|
63
|
+
model: string;
|
|
64
|
+
};
|
|
54
65
|
}
|
|
55
66
|
/**
|
|
56
67
|
* Resolve the dispatch-time role for an agent across three ordered stages
|
package/dist/roles.d.ts
CHANGED
|
@@ -82,4 +82,7 @@ export interface AgentLike {
|
|
|
82
82
|
* decision path injects the cordis `logger.warn` so every plugin warning
|
|
83
83
|
* flows through the plugin log namespace (qc2 F-002 / qc3 S-3).
|
|
84
84
|
*/
|
|
85
|
-
export declare function resolveRole(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, warn?: (message: string) => void
|
|
85
|
+
export declare function resolveRole(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, warn?: (message: string) => void, servedRoute?: {
|
|
86
|
+
provider: string;
|
|
87
|
+
model: string;
|
|
88
|
+
}): string;
|
package/dist/seeds.d.ts
CHANGED
|
@@ -11,20 +11,34 @@
|
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* 2. an in-memory per-apply seed registry keyed by PRODUCER LABEL
|
|
15
|
+
* (`bundled` | set name | `external`), each slice holding that
|
|
16
|
+
* producer's current id → `{ persona, set }` declarations. Declare =
|
|
17
|
+
* replacement PER PRODUCER: a declare replaces only its own slice, so
|
|
18
|
+
* the plugin's bundled presets stay `bundled` even when a companion
|
|
19
|
+
* merge-preserves them into its own batch (plan
|
|
20
|
+
* seeds-source-and-persona-width, Decisions #1).
|
|
17
21
|
* `seeded` / `personaOverridden` / `source` are DERIVED at read time, never
|
|
18
22
|
* stored — a config round-trip cannot orphan an override (AC-3), and
|
|
19
23
|
* provenance rides the in-memory registry only, so it can never churn a
|
|
20
24
|
* settings write.
|
|
21
25
|
*
|
|
26
|
+
* Resolution precedence (Decisions #2): `bundled` wins for any id the
|
|
27
|
+
* plugin's own preset self-declare currently declares; otherwise the most
|
|
28
|
+
* recent non-bundled producer declaring the id wins (its set name, or
|
|
29
|
+
* `external` when unnamed); no live declaration ⇒ `user`. Unnamed producers
|
|
30
|
+
* share the `external` slice (Decisions #3); `{ set }` is the remedy.
|
|
31
|
+
*
|
|
22
32
|
* Materialization (spec §9.2): append `{ id, persona }` (two keys only,
|
|
23
33
|
* R4) / attach row untouched / at-default tracking / override preserved +
|
|
24
|
-
* `'persona-source'` conflict / omitted id drops from the
|
|
25
|
-
* the row stays (R2).
|
|
26
|
-
*
|
|
27
|
-
*
|
|
34
|
+
* `'persona-source'` conflict / omitted id drops from the declaring
|
|
35
|
+
* producer's slice while the row stays (R2). Tracking is driven by the
|
|
36
|
+
* id's RESOLVED effective default — `prior` = resolved before the
|
|
37
|
+
* declare, `incoming` = resolved after the candidate slice commit — so a
|
|
38
|
+
* producer that is not the resolution winner for an id it declares never
|
|
39
|
+
* rewrites the row (F-001). No delta → no settings write (idempotent,
|
|
40
|
+
* AC-1). Compute → write → commit registry: a failed write throws and
|
|
41
|
+
* leaves the registry unchanged (retry-safe).
|
|
28
42
|
*
|
|
29
43
|
* @module dsh-llm-fallbacks/seeds
|
|
30
44
|
*/
|
|
@@ -151,13 +165,22 @@ export interface SeedsIo {
|
|
|
151
165
|
*/
|
|
152
166
|
export declare class FallbacksSeedManager {
|
|
153
167
|
private readonly logger;
|
|
154
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Per-producer declaration registry: producer label (`bundled` | set
|
|
170
|
+
* name | `external`) → that producer's current slice. A declare replaces
|
|
171
|
+
* only its own slice; an empty batch drops the slice.
|
|
172
|
+
*/
|
|
155
173
|
private registry;
|
|
174
|
+
/** Monotonic declare counter — "most recent producer" for resolution precedence. */
|
|
175
|
+
private declareSeq;
|
|
156
176
|
constructor(logger: FallbacksConfigLogger);
|
|
157
177
|
/**
|
|
158
|
-
* Declare seeds with replacement semantics — the batch is
|
|
159
|
-
*
|
|
160
|
-
* drop out of
|
|
178
|
+
* Declare seeds with replacement semantics PER PRODUCER — the batch is
|
|
179
|
+
* the producer's FULL current declaration set; ids omitted from the
|
|
180
|
+
* batch drop out of that producer's slice while their rows remain (R2).
|
|
181
|
+
* Other producers' slices are untouched, so a companion merge-preserving
|
|
182
|
+
* the bundled presets into its own batch never re-labels them (Decisions
|
|
183
|
+
* #1).
|
|
161
184
|
*
|
|
162
185
|
* `options` labels the whole batch's provenance (spec §2), resolved once
|
|
163
186
|
* per call: an invalid set name warns once and degrades to the unnamed
|
|
@@ -165,27 +188,46 @@ export declare class FallbacksSeedManager {
|
|
|
165
188
|
*
|
|
166
189
|
* Per-id validation AS DECLARED (spec §9.3): non-string / pattern miss /
|
|
167
190
|
* reserved `inherit` / duplicate-in-batch → skip + warn; valid siblings
|
|
168
|
-
* still apply (AC-5). Materializes per spec §9.2
|
|
169
|
-
*
|
|
170
|
-
* (
|
|
171
|
-
*
|
|
191
|
+
* still apply (AC-5). Materializes per spec §9.2 against the id's
|
|
192
|
+
* RESOLVED effective default before/after the candidate slice commit
|
|
193
|
+
* (at-default tracking follows the resolution winner — a producer that
|
|
194
|
+
* is not the winner for an id it declares never rewrites the row),
|
|
195
|
+
* writes only when the computed `{ list, rules }` differs from the
|
|
196
|
+
* current composed roles (idempotent, AC-1), and commits the slice only
|
|
197
|
+
* after a successful write (compute → write → commit; retry-safe). An
|
|
198
|
+
* empty batch drops the producer's slice.
|
|
172
199
|
*/
|
|
173
200
|
declare(seeds: readonly SeedDeclaration[], io: SeedsIo, options?: SeedsInternalDeclareOptions): Promise<SeedDeclareOutcome>;
|
|
201
|
+
/**
|
|
202
|
+
* Resolve one id's live declaration across producer slices (spec §2,
|
|
203
|
+
* Decisions #2): the bundled preset self-declare wins for any id it
|
|
204
|
+
* currently declares; otherwise the most recent non-bundled producer
|
|
205
|
+
* declaring the id wins (its set name, or `external` when unnamed); no
|
|
206
|
+
* live declaration ⇒ `undefined` (the row reads `user`). Single place —
|
|
207
|
+
* `effectiveRoles`, `wireStatus`, and `revert` can never disagree. The
|
|
208
|
+
* optional `registry` argument resolves against a CANDIDATE registry
|
|
209
|
+
* (the declaring slice already replaced) — the same precedence, used by
|
|
210
|
+
* `declare` to compute the post-commit effective default for tracking.
|
|
211
|
+
*/
|
|
212
|
+
private resolveDeclared;
|
|
174
213
|
/**
|
|
175
214
|
* Readback (b) — sync, derived: every config row annotated with
|
|
176
215
|
* `seeded` / `personaOverridden` / `seedPersona` (trimmed row-id
|
|
177
216
|
* membership in the live declaration set; persona inequality) plus the
|
|
178
|
-
* row's provenance `source` (spec §2): the
|
|
179
|
-
* `user` when no live declaration covers the row.
|
|
180
|
-
* override-shaped is stored, so a config round-trip cannot
|
|
217
|
+
* row's provenance `source` (spec §2): the winning producer's label per
|
|
218
|
+
* `resolveDeclared`, or `user` when no live declaration covers the row.
|
|
219
|
+
* Nothing override-shaped is stored, so a config round-trip cannot
|
|
220
|
+
* orphan state.
|
|
181
221
|
*/
|
|
182
222
|
effectiveRoles(io: SeedsIo): EffectiveRolesReadback;
|
|
183
223
|
/** Card badge state (spec §9.4): seeded rows, with the override flag and the provenance label. */
|
|
184
224
|
wireStatus(io: SeedsIo): SeedsWireStatus[];
|
|
185
225
|
/**
|
|
186
|
-
* Revert one id to the CURRENT declared seed default (AC-3)
|
|
187
|
-
*
|
|
188
|
-
*
|
|
226
|
+
* Revert one id to the CURRENT declared seed default (AC-3) — resolved
|
|
227
|
+
* through the same precedence as the readbacks, so a preserved bundled id
|
|
228
|
+
* reverts to the bundled persona, never a companion's copy. Writes
|
|
229
|
+
* persona only — the row is otherwise copied verbatim (R4). Ids with no
|
|
230
|
+
* live declaration (`not-seeded`) or with a deleted row (`row-absent`)
|
|
189
231
|
* return a non-reverted outcome without throwing; a failed settings
|
|
190
232
|
* write propagates loudly (spec §9.1).
|
|
191
233
|
*/
|
package/dist/time-slots.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export declare const PRESETS: Record<PresetId, PresetDefinition>;
|
|
|
78
78
|
* are the ordered walk before that last-resort fallback. An empty chain or
|
|
79
79
|
* a chain whose tail is not an official model (including the retired V4
|
|
80
80
|
* ids) keeps slot rows inert and refuses the virtual-row
|
|
81
|
-
*
|
|
81
|
+
* delegate; the v0.2.2 failure walk over the raw chain stays
|
|
82
82
|
* verbatim.
|
|
83
83
|
*/
|
|
84
84
|
export declare function isAllDayConforming(chain: readonly string[]): boolean;
|
|
@@ -92,7 +92,8 @@ export declare function isAllDayConforming(chain: readonly string[]): boolean;
|
|
|
92
92
|
* P6 (qc1 F-001): without a conforming all-day the slot rows are inert, so
|
|
93
93
|
* this IS the raw `rootChain` — the gate lives in {@link resolveSlotState},
|
|
94
94
|
* the single source every slot surface (this, the 分时切换 log, the
|
|
95
|
-
* `/fallbacks` strip,
|
|
95
|
+
* `/fallbacks` strip, the virtual adapter delegate, its served-route anchor)
|
|
96
|
+
* reads.
|
|
96
97
|
*/
|
|
97
98
|
export declare function resolveEffectiveChain(config: FallbacksConfig, now: Date, tz: string): string[];
|
|
98
99
|
/**
|