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/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
- constructor(ctx, bridge, seeds, subagentPolicy) {
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 registry = new Map();
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 (registry.has(seed.id)) {
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
- registry.set(seed.id, seed.persona);
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, registry, this.registry, outcome.conflicts);
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 = 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 seedPersona = this.registry.get(row.id.trim());
1597
- const seeded = seedPersona !== void 0;
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 !== seedPersona
1664
+ personaOverridden: seeded && row.persona !== entry.persona,
1665
+ source: seeded ? entry.set : "user"
1603
1666
  };
1604
- if (seeded) effective.seedPersona = 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 seedPersona = this.registry.get(row.id.trim());
1614
- if (seedPersona === void 0) continue;
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 !== seedPersona
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 seedPersona = this.registry.get(seedId);
1625
- if (seedPersona === void 0) return {
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 === seedPersona) return {
1701
+ if (rows[index].persona === entry.persona) return {
1638
1702
  reverted: true,
1639
- persona: seedPersona
1703
+ persona: entry.persona
1640
1704
  };
1641
1705
  const nextList = rows.map((row, i) => i === index ? {
1642
1706
  ...row,
1643
- persona: seedPersona
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: seedPersona
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
- return registry.register(buildFallbacksTuiSection());
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 = false;
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
- states.suppress(state, fromKey, until);
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
- commit(states.get(agent.id), pending, turn, step);
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,designer,librarian,reviewer,security-reviewer}.md`
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 7 bundled omp-style preset roles (spec §9.1 shape, §9.2 personas). */
19
+ /** The 5 bundled omp-style preset roles (spec §9.1 shape, §9.2 personas). */
17
20
  export declare const presetRoles: readonly SeedDeclaration[];