dsh-llm-fallbacks 0.6.13 → 0.8.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 +34 -30
- package/README.zh-CN.md +32 -28
- package/dist/chain-walk.d.ts +197 -0
- package/dist/chains.d.ts +35 -82
- package/dist/client/FallbacksCard.d.ts +27 -80
- package/dist/client/fallbacks-store.d.ts +71 -225
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.js +1332 -2110
- package/dist/client/locales.d.ts +61 -117
- package/dist/commands.d.ts +70 -112
- package/dist/config.d.ts +152 -117
- package/dist/cooldown.d.ts +1 -15
- package/dist/events.d.ts +8 -8
- package/dist/gateway.d.ts +66 -110
- package/dist/index.d.ts +21 -64
- package/dist/index.js +900 -1313
- package/dist/role-resolution.d.ts +15 -83
- package/dist/schema.d.ts +6 -5
- package/dist/seeds.d.ts +5 -7
- package/dist/state.d.ts +15 -141
- package/dist/subagents-seam.d.ts +51 -14
- package/dist/time-slots.d.ts +21 -24
- package/dist/tui-settings.d.ts +5 -6
- package/dist/virtual-adapter.d.ts +70 -119
- package/docs/DETAIL.md +1 -0
- package/docs/DETAIL.zh.md +1 -0
- package/docs/preview.svg +47 -0
- package/docs/preview.zh.svg +47 -0
- package/locale/en.json +2 -1
- package/locale/zh.json +2 -1
- package/package.json +29 -25
- package/dist/authorized-route.d.ts +0 -68
- package/dist/automatch.d.ts +0 -98
- package/dist/client/GeneralFallbacksRow.d.ts +0 -19
- package/dist/override.d.ts +0 -39
- package/dist/roles.d.ts +0 -90
- package/dist/route-allowlist.d.ts +0 -43
- package/dist/subagent-policy.d.ts +0 -123
package/dist/index.js
CHANGED
|
@@ -227,18 +227,18 @@ function isAllDayConforming(chain) {
|
|
|
227
227
|
const tail = chain[chain.length - 1];
|
|
228
228
|
return isOfficialAllDayId(tail);
|
|
229
229
|
}
|
|
230
|
-
function resolveEffectiveChain(
|
|
231
|
-
const state = resolveSlotState(
|
|
232
|
-
return state.winner === "all-day" ?
|
|
230
|
+
function resolveEffectiveChain(chain, now, tz) {
|
|
231
|
+
const state = resolveSlotState(chain, now, tz);
|
|
232
|
+
return state.winner === "all-day" ? chain.entries : state.winner.chain;
|
|
233
233
|
}
|
|
234
|
-
function resolveSlotState(
|
|
235
|
-
if (!isAllDayConforming(
|
|
234
|
+
function resolveSlotState(chain, now, tz) {
|
|
235
|
+
if (!isAllDayConforming(chain.entries)) return {
|
|
236
236
|
winner: "all-day",
|
|
237
237
|
label: "all-day"
|
|
238
238
|
};
|
|
239
239
|
const clock = wallClock(now, tz);
|
|
240
240
|
const seenPresets = new Set();
|
|
241
|
-
const rows = Array.isArray(
|
|
241
|
+
const rows = Array.isArray(chain.timeSlots) ? chain.timeSlots : [];
|
|
242
242
|
for (const row of rows) {
|
|
243
243
|
if (typeof row !== "object" || row === null) {
|
|
244
244
|
warnMalformed(row, "row is not an object");
|
|
@@ -324,33 +324,57 @@ var RecoveryStore = class {
|
|
|
324
324
|
};
|
|
325
325
|
//#endregion
|
|
326
326
|
//#region .build/host/config.js
|
|
327
|
+
const CHAIN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,31}$/;
|
|
327
328
|
const defaultFallbacksConfig = {
|
|
328
329
|
triggerCodes: [
|
|
329
330
|
"AUTH",
|
|
330
331
|
"QUOTA",
|
|
331
332
|
"RATE_LIMIT"
|
|
332
333
|
],
|
|
333
|
-
|
|
334
|
-
roles: {
|
|
335
|
-
list: [],
|
|
336
|
-
rules: []
|
|
337
|
-
},
|
|
334
|
+
chains: [],
|
|
335
|
+
roles: { list: [] },
|
|
338
336
|
cooldownMs: 3e5,
|
|
339
337
|
revertPolicy: "cooldown-expiry",
|
|
340
338
|
maxSwitchesPerStep: 8,
|
|
341
|
-
alwaysModeRetryCap: 5,
|
|
342
339
|
presets: "bundled",
|
|
343
|
-
roleAutoMatch: true,
|
|
344
|
-
timeSlots: [],
|
|
345
340
|
tz: "Asia/Shanghai",
|
|
346
341
|
recovery: "timer"
|
|
347
342
|
};
|
|
343
|
+
const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
348
344
|
const INHERIT_ROLE_ID = "inherit";
|
|
349
345
|
function isFallbackActive(config) {
|
|
350
|
-
return config.
|
|
346
|
+
return config.chains.length > 0;
|
|
351
347
|
}
|
|
352
|
-
const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
353
348
|
function validateFallbacksConfig(config, logger) {
|
|
349
|
+
const declaredChainIds = new Set();
|
|
350
|
+
for (const chain of config.chains) {
|
|
351
|
+
if (!CHAIN_ID_PATTERN.test(chain.id)) logger.warn(`llm-fallbacks: invalid chain id "${chain.id}" — must match /^[A-Za-z0-9][A-Za-z0-9-]{0,31}$/`);
|
|
352
|
+
if (declaredChainIds.has(chain.id)) logger.warn(`llm-fallbacks: duplicate chain id "${chain.id}" — chain ids must be unique (case-sensitive)`);
|
|
353
|
+
declaredChainIds.add(chain.id);
|
|
354
|
+
if (chain.entries.length > 0 && !isAllDayConforming(chain.entries)) logger.warn(`llm-fallbacks: chain "${chain.id}" must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro) — its time-slot rows and the virtual picker delegate stay inert until the tail conforms`);
|
|
355
|
+
for (const entry of chain.entries) try {
|
|
356
|
+
parseSelector(entry);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
logger.warn(`llm-fallbacks: ignoring invalid chain "${chain.id}" entry "${entry}": ${error.message}`);
|
|
359
|
+
}
|
|
360
|
+
if (Array.isArray(chain.timeSlots)) {
|
|
361
|
+
const seenSlotPresets = new Set();
|
|
362
|
+
for (const row of chain.timeSlots) {
|
|
363
|
+
if (row.kind !== "preset") continue;
|
|
364
|
+
const preset = row.preset;
|
|
365
|
+
if (typeof preset !== "string" || !Object.hasOwn(PRESETS, preset)) {
|
|
366
|
+
logger.warn(`llm-fallbacks: chain "${chain.id}" has an unknown time-slot preset ${JSON.stringify(preset)} — row ignored`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (seenSlotPresets.has(preset)) {
|
|
370
|
+
logger.warn(`llm-fallbacks: chain "${chain.id}" has a duplicate time-slot preset "${preset}" — only the first row takes effect`);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
seenSlotPresets.add(preset);
|
|
374
|
+
if (row.start !== void 0 || row.end !== void 0 || row.days !== void 0 && row.days.length > 0) logger.warn(`llm-fallbacks: chain "${chain.id}" preset "${preset}" carries its own window/day fields — preset windows are fixed code constants; the row is ignored`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
354
378
|
const declaredIds = new Set();
|
|
355
379
|
for (const role of config.roles.list) {
|
|
356
380
|
const id = role.id.trim();
|
|
@@ -358,201 +382,153 @@ function validateFallbacksConfig(config, logger) {
|
|
|
358
382
|
if (id === "inherit") logger.warn(`llm-fallbacks: role id "${role.id}" is reserved — "inherit" cannot be declared in roles.list`);
|
|
359
383
|
if (declaredIds.has(id)) logger.warn(`llm-fallbacks: duplicate role id "${role.id}" — role ids must be unique`);
|
|
360
384
|
declaredIds.add(id);
|
|
361
|
-
|
|
362
|
-
parseSelector(entry);
|
|
363
|
-
} catch (error) {
|
|
364
|
-
logger.warn(`llm-fallbacks: ignoring invalid chain entry "${entry}" in role "${role.id}": ${error.message}`);
|
|
365
|
-
}
|
|
366
|
-
if ((role.chain ?? []).length === 0) logger.warn(`llm-fallbacks: role "${role.id}" has no model config — declare at least one chain entry, or use the built-in "inherit" rule target instead`);
|
|
367
|
-
if (role.fallback !== void 0 && role.fallback !== "inherit-root" && role.fallback !== "none") logger.warn(`llm-fallbacks: role "${role.id}" has invalid fallback "${String(role.fallback)}" — expected "inherit-root" or "none"`);
|
|
368
|
-
}
|
|
369
|
-
if (config.rootChain.length > 0 && !isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: rootChain must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro) — time-slot rows and the virtual picker row stay inert until the all-day chain tail conforms");
|
|
370
|
-
for (const entry of config.rootChain) try {
|
|
371
|
-
parseSelector(entry);
|
|
372
|
-
} catch (error) {
|
|
373
|
-
logger.warn(`llm-fallbacks: ignoring invalid rootChain entry "${entry}": ${error.message}`);
|
|
374
|
-
}
|
|
375
|
-
const validTargets = new Set([...declaredIds, INHERIT_ROLE_ID]);
|
|
376
|
-
for (const rule of config.roles.rules) if (!validTargets.has(rule.role.trim())) logger.warn(`llm-fallbacks: rule references undeclared role "${rule.role}" — expected one of roles.list ids or "inherit"`);
|
|
377
|
-
if (Array.isArray(config.timeSlots)) {
|
|
378
|
-
const seenSlotPresets = new Set();
|
|
379
|
-
for (const row of config.timeSlots) {
|
|
380
|
-
if (row.kind !== "preset") continue;
|
|
381
|
-
const preset = row.preset;
|
|
382
|
-
if (typeof preset !== "string" || !Object.hasOwn(PRESETS, preset)) {
|
|
383
|
-
logger.warn(`llm-fallbacks: unknown time-slot preset ${JSON.stringify(preset)} — row ignored`);
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (seenSlotPresets.has(preset)) {
|
|
387
|
-
logger.warn(`llm-fallbacks: duplicate time-slot preset "${preset}" — only the first row takes effect`);
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
seenSlotPresets.add(preset);
|
|
391
|
-
if (row.start !== void 0 || row.end !== void 0 || row.days !== void 0 && row.days.length > 0) logger.warn(`llm-fallbacks: preset "${preset}" carries its own window/day fields — preset windows are fixed code constants; the row is ignored`);
|
|
392
|
-
}
|
|
385
|
+
if (role.chain !== void 0 && role.chain !== "" && !declaredChainIds.has(role.chain)) logger.warn(`llm-fallbacks: role "${role.id}" specifies unknown chain "${role.chain}" — the child inherits the delegating agent's route`);
|
|
393
386
|
}
|
|
394
387
|
if ((config.recovery ?? "timer") === "half-open" && config.cooldownMs >= 36e5) logger.warn(`llm-fallbacks: recovery "half-open" escalation is inert — cooldownMs (${config.cooldownMs} ms) is at or above the 1-hour escalation cap (${ESCALATION_CAP_MS} ms), so every suppression stays flat at cooldownMs`);
|
|
395
388
|
}
|
|
396
389
|
function detectLegacyKeys(source) {
|
|
397
|
-
const keys =
|
|
398
|
-
if (Object.hasOwn(source, "
|
|
390
|
+
const keys = new Set();
|
|
391
|
+
if (Object.hasOwn(source, "alwaysModeRetryCap")) keys.add("alwaysModeRetryCap");
|
|
392
|
+
if (Object.hasOwn(source, "roleAutoMatch")) keys.add("roleAutoMatch");
|
|
393
|
+
if (Object.hasOwn(source, "rootChain")) keys.add("rootChain");
|
|
394
|
+
if (Object.hasOwn(source, "timeSlots")) keys.add("timeSlots");
|
|
399
395
|
const roles = source.roles;
|
|
400
396
|
if (isRecordLike(roles)) {
|
|
401
|
-
if (Object.hasOwn(roles, "default")) keys.
|
|
402
|
-
|
|
397
|
+
if (Object.hasOwn(roles, "default")) keys.add("roles.default");
|
|
398
|
+
if (Object.hasOwn(roles, "rules")) keys.add("roles.rules");
|
|
403
399
|
if (Array.isArray(roles.list)) for (const item of roles.list) {
|
|
404
400
|
if (!isRecordLike(item)) continue;
|
|
405
|
-
if (
|
|
406
|
-
if (Object.hasOwn(item, "
|
|
407
|
-
if (Object.hasOwn(item, "
|
|
408
|
-
|
|
409
|
-
if (Array.isArray(roles.rules)) {
|
|
410
|
-
for (const rule of roles.rules) if (isRecordLike(rule) && typeof rule.role === "string" && rule.role !== "inherit" && !declared.has(rule.role)) keys.push(`roles.rules[].role: ${rule.role}`);
|
|
401
|
+
if (Object.hasOwn(item, "label")) keys.add("roles.list[].label");
|
|
402
|
+
if (Object.hasOwn(item, "description")) keys.add("roles.list[].description");
|
|
403
|
+
if (Object.hasOwn(item, "fallback")) keys.add("roles.list[].fallback");
|
|
404
|
+
if (Array.isArray(item.chain)) keys.add("roles.list[].chain");
|
|
411
405
|
}
|
|
412
406
|
}
|
|
413
|
-
return keys;
|
|
407
|
+
return [...keys];
|
|
414
408
|
}
|
|
415
409
|
function isRecordLike(value) {
|
|
416
410
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
417
411
|
}
|
|
412
|
+
const MIGRATED_CHAIN_ID = "Auto";
|
|
413
|
+
const MIGRATED_CHAIN_LABEL = "默认链";
|
|
414
|
+
function migrateLegacyConfig(source) {
|
|
415
|
+
const raw = isRecordLike(source) ? source : {};
|
|
416
|
+
const defaults = defaultFallbacksConfig;
|
|
417
|
+
return {
|
|
418
|
+
triggerCodes: stringArrayOf(raw["triggerCodes"]) ?? [...defaults.triggerCodes],
|
|
419
|
+
chains: migrateChains(raw),
|
|
420
|
+
roles: { list: migrateRoles(raw["roles"]) },
|
|
421
|
+
cooldownMs: finiteNumberOr(raw["cooldownMs"], defaults.cooldownMs),
|
|
422
|
+
revertPolicy: raw["revertPolicy"] === "never" ? "never" : defaults.revertPolicy,
|
|
423
|
+
maxSwitchesPerStep: finiteNumberOr(raw["maxSwitchesPerStep"], defaults.maxSwitchesPerStep),
|
|
424
|
+
presets: raw["presets"] === "none" ? "none" : defaults.presets,
|
|
425
|
+
tz: typeof raw["tz"] === "string" && raw["tz"] !== "" ? raw["tz"] : defaults.tz,
|
|
426
|
+
recovery: raw["recovery"] === "half-open" ? "half-open" : defaults.recovery
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function migrateChains(raw) {
|
|
430
|
+
const declared = Array.isArray(raw["chains"]) ? raw["chains"] : void 0;
|
|
431
|
+
if (declared !== void 0 && declared.length > 0) {
|
|
432
|
+
const chains = [];
|
|
433
|
+
for (const entry of declared) {
|
|
434
|
+
const chain = normalizeChain(entry);
|
|
435
|
+
if (chain !== void 0) chains.push(chain);
|
|
436
|
+
}
|
|
437
|
+
return chains;
|
|
438
|
+
}
|
|
439
|
+
const rootChain = stringArrayOf(raw["rootChain"]);
|
|
440
|
+
const legacyRows = Array.isArray(raw["timeSlots"]) ? raw["timeSlots"].filter(isRecordLike).map((row) => ({ ...row })) : [];
|
|
441
|
+
if (rootChain === void 0 || rootChain.length === 0) return [];
|
|
442
|
+
return [{
|
|
443
|
+
id: MIGRATED_CHAIN_ID,
|
|
444
|
+
label: MIGRATED_CHAIN_LABEL,
|
|
445
|
+
entries: [...rootChain],
|
|
446
|
+
timeSlots: legacyRows
|
|
447
|
+
}];
|
|
448
|
+
}
|
|
449
|
+
function normalizeChain(value) {
|
|
450
|
+
if (!isRecordLike(value)) return void 0;
|
|
451
|
+
const id = typeof value["id"] === "string" ? value["id"] : void 0;
|
|
452
|
+
if (id === void 0) return void 0;
|
|
453
|
+
const timeSlots = Array.isArray(value["timeSlots"]) ? value["timeSlots"].filter(isRecordLike).map((row) => ({ ...row })) : [];
|
|
454
|
+
return {
|
|
455
|
+
id,
|
|
456
|
+
label: typeof value["label"] === "string" ? value["label"] : id,
|
|
457
|
+
entries: stringArrayOf(value["entries"]) ?? [],
|
|
458
|
+
timeSlots
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function migrateRoles(value) {
|
|
462
|
+
if (!isRecordLike(value)) return [];
|
|
463
|
+
const list = Array.isArray(value["list"]) ? value["list"] : [];
|
|
464
|
+
const roles = [];
|
|
465
|
+
for (const row of list) {
|
|
466
|
+
if (!isRecordLike(row)) continue;
|
|
467
|
+
const id = typeof row["id"] === "string" ? row["id"] : void 0;
|
|
468
|
+
if (id === void 0) continue;
|
|
469
|
+
const role = {
|
|
470
|
+
...row,
|
|
471
|
+
id,
|
|
472
|
+
persona: typeof row["persona"] === "string" ? row["persona"] : ""
|
|
473
|
+
};
|
|
474
|
+
for (const stale of [
|
|
475
|
+
"fallback",
|
|
476
|
+
"label",
|
|
477
|
+
"description"
|
|
478
|
+
]) delete role[stale];
|
|
479
|
+
if (typeof row["chain"] !== "string" || row["chain"] === "") delete role["chain"];
|
|
480
|
+
roles.push(role);
|
|
481
|
+
}
|
|
482
|
+
return roles;
|
|
483
|
+
}
|
|
484
|
+
function stringArrayOf(value) {
|
|
485
|
+
if (!Array.isArray(value)) return void 0;
|
|
486
|
+
const out = [];
|
|
487
|
+
for (const item of value) {
|
|
488
|
+
if (typeof item !== "string") return void 0;
|
|
489
|
+
out.push(item);
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
function finiteNumberOr(value, fallback) {
|
|
494
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
495
|
+
}
|
|
418
496
|
const Config = z.object({
|
|
419
497
|
triggerCodes: z.array(z.string()).default([
|
|
420
498
|
"AUTH",
|
|
421
499
|
"QUOTA",
|
|
422
500
|
"RATE_LIMIT"
|
|
423
501
|
]),
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
chain: z.array(z.string())
|
|
435
|
-
fallback: z.union([z.const("inherit-root"), z.const("none")]).default("inherit-root")
|
|
436
|
-
})).default([]),
|
|
437
|
-
rules: z.array(z.object({
|
|
438
|
-
origin: z.union([z.const("root"), z.const("subagent")]),
|
|
439
|
-
provider: z.string(),
|
|
440
|
-
model: z.string(),
|
|
441
|
-
role: z.string().required()
|
|
502
|
+
chains: z.array(z.object({
|
|
503
|
+
id: z.string().required(),
|
|
504
|
+
label: z.string().required(),
|
|
505
|
+
entries: z.array(z.string()).default([]),
|
|
506
|
+
timeSlots: z.array(z.object({
|
|
507
|
+
kind: z.string(),
|
|
508
|
+
preset: z.string(),
|
|
509
|
+
start: z.string(),
|
|
510
|
+
end: z.string(),
|
|
511
|
+
days: z.array(z.number()),
|
|
512
|
+
chain: z.array(z.string())
|
|
442
513
|
})).default([])
|
|
443
|
-
}).default(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
514
|
+
})).default([]),
|
|
515
|
+
roles: z.object({ list: z.array(z.object({
|
|
516
|
+
id: z.string().required(),
|
|
517
|
+
persona: z.string().default(""),
|
|
518
|
+
prompt: z.string(),
|
|
519
|
+
permissions: z.object({
|
|
520
|
+
allow: z.array(z.string()),
|
|
521
|
+
deny: z.array(z.string())
|
|
522
|
+
}),
|
|
523
|
+
chain: z.union([z.string(), z.array(z.string())])
|
|
524
|
+
})).default([]) }).default({ list: [] }),
|
|
447
525
|
cooldownMs: z.number().default(3e5),
|
|
448
526
|
revertPolicy: z.union([z.const("cooldown-expiry"), z.const("never")]).default("cooldown-expiry"),
|
|
449
527
|
maxSwitchesPerStep: z.number().default(8),
|
|
450
|
-
alwaysModeRetryCap: z.number().default(5),
|
|
451
528
|
presets: z.union([z.const("bundled"), z.const("none")]).default("bundled"),
|
|
452
|
-
roleAutoMatch: z.boolean().default(true),
|
|
453
|
-
timeSlots: z.array(z.object({
|
|
454
|
-
kind: z.string(),
|
|
455
|
-
preset: z.string(),
|
|
456
|
-
start: z.string(),
|
|
457
|
-
end: z.string(),
|
|
458
|
-
days: z.array(z.number()),
|
|
459
|
-
chain: z.array(z.string())
|
|
460
|
-
})).default([]),
|
|
461
529
|
tz: z.string().default("Asia/Shanghai"),
|
|
462
530
|
recovery: z.union([z.const("timer"), z.const("half-open")]).default("timer")
|
|
463
531
|
}).extra("volatile", true);
|
|
464
|
-
function buildAutomatchPrompt(roles, agent) {
|
|
465
|
-
const lines = [
|
|
466
|
-
"You are assigning a subagent to its best-fit role from a declared taxonomy.",
|
|
467
|
-
"",
|
|
468
|
-
"Declared roles:",
|
|
469
|
-
...roles.map((role) => `- ${role.id.trim()}: ${role.persona}`),
|
|
470
|
-
"",
|
|
471
|
-
`Agent context — origin: ${agent.session?.header?.origin ?? "root"}`
|
|
472
|
-
];
|
|
473
|
-
const preset = agent.session?.header?.agentPreset?.trim();
|
|
474
|
-
if (preset !== void 0 && preset !== "") lines.push(`agentPreset: ${preset}`);
|
|
475
|
-
lines.push("", "Reply with EXACTLY ONE of the declared role ids above, or the literal \"none\" when no role fits.", "Do not add any explanation, punctuation, or other text.");
|
|
476
|
-
return lines.join("\n");
|
|
477
|
-
}
|
|
478
|
-
function parseAutomatchAnswer(answer, roleIds) {
|
|
479
|
-
const cleaned = answer.replace(/^[^a-zA-Z0-9-]+/, "").replace(/[^a-zA-Z0-9-]+$/, "");
|
|
480
|
-
if (cleaned === "") return null;
|
|
481
|
-
const key = cleaned.toLowerCase();
|
|
482
|
-
if (key === "none") return null;
|
|
483
|
-
return roleIds.get(key) ?? null;
|
|
484
|
-
}
|
|
485
|
-
async function pickRoleByLlm(ctx, roles, agent, opts) {
|
|
486
|
-
try {
|
|
487
|
-
const list = Array.isArray(roles.list) ? roles.list : [];
|
|
488
|
-
if (list.length === 0) return null;
|
|
489
|
-
const llm = ctx.get("llm");
|
|
490
|
-
if (llm === void 0) return null;
|
|
491
|
-
const route = pickJudgmentRoute(list, agent);
|
|
492
|
-
if (route === null) return null;
|
|
493
|
-
const roleIds = new Map(list.map((role) => [role.id.trim(), role.id]));
|
|
494
|
-
const answer = await streamRoleAnswer(llm, route, buildAutomatchPrompt(list, agent), opts);
|
|
495
|
-
if (answer === null) return null;
|
|
496
|
-
return parseAutomatchAnswer(answer, roleIds);
|
|
497
|
-
} catch (error) {
|
|
498
|
-
opts.warn(`llm-fallbacks: auto-match failed — falling back to "inherit": ${describeError(error)}`);
|
|
499
|
-
return null;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
function pickJudgmentRoute(roles, agent) {
|
|
503
|
-
for (const role of roles) for (const entry of role.chain ?? []) try {
|
|
504
|
-
const selector = parseSelector(entry);
|
|
505
|
-
if (selector.model !== void 0) return {
|
|
506
|
-
provider: selector.provider,
|
|
507
|
-
model: selector.model
|
|
508
|
-
};
|
|
509
|
-
} catch {}
|
|
510
|
-
const provider = agent.options?.provider;
|
|
511
|
-
const model = agent.options?.model;
|
|
512
|
-
if (provider !== void 0 && model !== void 0) return {
|
|
513
|
-
provider,
|
|
514
|
-
model
|
|
515
|
-
};
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
async function streamRoleAnswer(llm, route, prompt, opts) {
|
|
519
|
-
const controller = new AbortController();
|
|
520
|
-
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5e3);
|
|
521
|
-
let text = "";
|
|
522
|
-
let finishKind;
|
|
523
|
-
try {
|
|
524
|
-
for await (const chunk of llm.stream({
|
|
525
|
-
provider: route.provider,
|
|
526
|
-
model: route.model,
|
|
527
|
-
system: prompt,
|
|
528
|
-
messages: [createUserMessage({
|
|
529
|
-
content: [{
|
|
530
|
-
type: "text",
|
|
531
|
-
text: "Pick the best-fit role for this agent."
|
|
532
|
-
}],
|
|
533
|
-
source: { kind: "user" }
|
|
534
|
-
})],
|
|
535
|
-
maxTokens: 32,
|
|
536
|
-
signal: controller.signal
|
|
537
|
-
})) if (chunk.type === "text-delta") text += chunk.text;
|
|
538
|
-
else if (chunk.type === "finish") {
|
|
539
|
-
finishKind = chunk.reason.kind;
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
if (controller.signal.aborted) return null;
|
|
543
|
-
if (finishKind === "error" || finishKind === "aborted") return null;
|
|
544
|
-
return text;
|
|
545
|
-
} catch (error) {
|
|
546
|
-
if (controller.signal.aborted) opts.warn("llm-fallbacks: auto-match timed out — falling back to \"inherit\"");
|
|
547
|
-
else opts.warn(`llm-fallbacks: auto-match call failed — falling back to "inherit": ${describeError(error)}`);
|
|
548
|
-
return null;
|
|
549
|
-
} finally {
|
|
550
|
-
clearTimeout(timer);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
function describeError(error) {
|
|
554
|
-
return error instanceof Error ? error.message : String(error);
|
|
555
|
-
}
|
|
556
532
|
//#endregion
|
|
557
533
|
//#region .build/host/chains.js
|
|
558
534
|
function resolveCandidate(entry, failing, modelExists) {
|
|
@@ -569,19 +545,11 @@ function resolveCandidate(entry, failing, modelExists) {
|
|
|
569
545
|
}
|
|
570
546
|
return selector;
|
|
571
547
|
}
|
|
572
|
-
function
|
|
573
|
-
if (role.trim() === "inherit") return rootChain;
|
|
574
|
-
const roleDef = roles.find((declared) => declared.id.trim() === role.trim());
|
|
575
|
-
if (roleDef === void 0) return rootChain;
|
|
576
|
-
return [...roleDef.chain ?? [], ...roleDef.fallback === "none" ? [] : rootChain];
|
|
577
|
-
}
|
|
578
|
-
function resolveChainViews(roles, rootChain, role, provider, model, warn = console.warn) {
|
|
548
|
+
function resolveChainViews(entries, provider, model, warn = console.warn) {
|
|
579
549
|
const failing = {
|
|
580
550
|
provider,
|
|
581
551
|
model
|
|
582
552
|
};
|
|
583
|
-
const entries = buildRoleEntries(roles, rootChain, role);
|
|
584
|
-
if (role.trim() !== "inherit" && !roles.some((declared) => declared.id.trim() === role.trim())) warn(`llm-fallbacks: unknown role "${role}" — falling back to rootChain`);
|
|
585
553
|
const all = [];
|
|
586
554
|
const wildcard = [];
|
|
587
555
|
for (const entry of entries) {
|
|
@@ -611,12 +579,11 @@ function selectCandidates(all, wildcard, filter, modelExists) {
|
|
|
611
579
|
}
|
|
612
580
|
return surviving;
|
|
613
581
|
}
|
|
614
|
-
function resolveChain(
|
|
615
|
-
const { all, wildcard } = resolveChainViews(
|
|
582
|
+
function resolveChain(entries, provider, model, filter, modelExists, warn = console.warn) {
|
|
583
|
+
const { all, wildcard } = resolveChainViews(entries, provider, model, warn);
|
|
616
584
|
return selectCandidates(all, wildcard, filter, modelExists);
|
|
617
585
|
}
|
|
618
|
-
function hasWildcardEntry(
|
|
619
|
-
const entries = buildRoleEntries(roles, rootChain, role);
|
|
586
|
+
function hasWildcardEntry(entries) {
|
|
620
587
|
for (const entry of entries) try {
|
|
621
588
|
if (parseSelector(entry).model === void 0) return true;
|
|
622
589
|
} catch {}
|
|
@@ -668,148 +635,463 @@ function annotateCandidates(candidates, surviving, options) {
|
|
|
668
635
|
});
|
|
669
636
|
}
|
|
670
637
|
//#endregion
|
|
671
|
-
//#region .build/host/
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (!matchesPair(recordedRoute) && !(servedRoute !== void 0 && matchesPair(servedRoute))) continue;
|
|
681
|
-
const target = rule.role.trim();
|
|
682
|
-
if (target === "inherit") return INHERIT_ROLE_ID;
|
|
683
|
-
const declared = roleIds.get(target);
|
|
684
|
-
if (declared === void 0) {
|
|
685
|
-
warn(`llm-fallbacks: rule references undeclared role "${rule.role}" — falling back to "inherit"`);
|
|
686
|
-
return INHERIT_ROLE_ID;
|
|
638
|
+
//#region .build/host/virtual-adapter.js
|
|
639
|
+
const FALLBACKS_PROVIDER$1 = "FallbacksChain";
|
|
640
|
+
function firstDispatchableExactHead(chain) {
|
|
641
|
+
for (const entry of chain) {
|
|
642
|
+
let selector;
|
|
643
|
+
try {
|
|
644
|
+
selector = parseSelector(entry);
|
|
645
|
+
} catch {
|
|
646
|
+
continue;
|
|
687
647
|
}
|
|
688
|
-
|
|
648
|
+
if (selector.model === void 0) continue;
|
|
649
|
+
if (selector.provider === "FallbacksChain") continue;
|
|
650
|
+
return {
|
|
651
|
+
provider: selector.provider,
|
|
652
|
+
model: selector.model
|
|
653
|
+
};
|
|
689
654
|
}
|
|
690
|
-
return INHERIT_ROLE_ID;
|
|
691
655
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
656
|
+
function effectiveHeadOf(chain, now, tz = "Asia/Shanghai") {
|
|
657
|
+
if (!isAllDayConforming(chain.entries)) return void 0;
|
|
658
|
+
return firstDispatchableExactHead(resolveEffectiveChain(chain, now, tz));
|
|
659
|
+
}
|
|
660
|
+
function pickerDisplayName(chain, now = new Date(), modelDisplayName, tz = "Asia/Shanghai") {
|
|
661
|
+
const head = effectiveHeadOf(chain, now, tz);
|
|
662
|
+
if (head === void 0) return chain.label;
|
|
663
|
+
const slot = resolveSlotState(chain, now, tz);
|
|
664
|
+
const model = modelDisplayName !== void 0 && modelDisplayName !== "" ? modelDisplayName : head.model;
|
|
665
|
+
return `${chain.label}: ${model}[${slot.label}]`;
|
|
666
|
+
}
|
|
667
|
+
async function resolveHeadDisplayName(llm, head) {
|
|
668
|
+
if (llm === void 0) return head.model;
|
|
669
|
+
try {
|
|
670
|
+
const row = (await llm.listModels(head.provider)).find((model) => model.id === head.model);
|
|
671
|
+
if (row !== void 0 && row.name !== "") return row.name;
|
|
672
|
+
} catch {}
|
|
673
|
+
try {
|
|
674
|
+
const info = await llm.resolveModelInfo(head.provider, head.model);
|
|
675
|
+
if (info.name !== "") return info.name;
|
|
676
|
+
} catch {}
|
|
677
|
+
return head.model;
|
|
678
|
+
}
|
|
679
|
+
var FallbacksChainAdapter = class extends LlmAdapter {
|
|
680
|
+
readConfig;
|
|
681
|
+
getLlm;
|
|
682
|
+
walk;
|
|
683
|
+
constructor(readConfig, getLlm, walk) {
|
|
684
|
+
super();
|
|
685
|
+
this.readConfig = readConfig;
|
|
686
|
+
this.getLlm = getLlm;
|
|
687
|
+
this.walk = walk;
|
|
688
|
+
}
|
|
689
|
+
providerInfo(provider) {
|
|
690
|
+
return {
|
|
691
|
+
id: provider,
|
|
692
|
+
name: FALLBACKS_PROVIDER$1
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
async listModels(provider) {
|
|
696
|
+
const config = this.readConfig();
|
|
697
|
+
const now = new Date();
|
|
698
|
+
const tz = config.tz ?? "Asia/Shanghai";
|
|
699
|
+
return Promise.all(config.chains.map(async (chain) => {
|
|
700
|
+
const head = effectiveHeadOf(chain, now, tz);
|
|
701
|
+
const display = head === void 0 ? void 0 : await resolveHeadDisplayName(this.getLlm(), head);
|
|
702
|
+
return {
|
|
703
|
+
provider,
|
|
704
|
+
id: chain.id,
|
|
705
|
+
name: pickerDisplayName(chain, now, display, tz)
|
|
706
|
+
};
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
async resolveModel(provider, model, signal) {
|
|
710
|
+
const chain = this.readConfig().chains.find((entry) => entry.id === model);
|
|
711
|
+
const head = chain === void 0 ? void 0 : effectiveHeadOf(chain, new Date(), this.readConfig().tz ?? "Asia/Shanghai");
|
|
712
|
+
const llm = this.getLlm();
|
|
713
|
+
if (head !== void 0 && llm !== void 0) try {
|
|
714
|
+
const info = await llm.resolveModelInfo(head.provider, head.model, signal);
|
|
715
|
+
return {
|
|
716
|
+
provider,
|
|
717
|
+
id: model,
|
|
718
|
+
name: info.name,
|
|
719
|
+
...info.description === void 0 ? {} : { description: info.description },
|
|
720
|
+
...info.inputModalities === void 0 ? {} : { inputModalities: info.inputModalities },
|
|
721
|
+
...info.context === void 0 ? {} : { context: info.context },
|
|
722
|
+
...info.defaultMaxTokens === void 0 ? {} : { defaultMaxTokens: info.defaultMaxTokens },
|
|
723
|
+
...info.reasoning === void 0 ? {} : { reasoning: info.reasoning }
|
|
724
|
+
};
|
|
725
|
+
} catch {}
|
|
726
|
+
return {
|
|
727
|
+
provider,
|
|
728
|
+
id: model,
|
|
729
|
+
name: model
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
providerRetryPolicy(_provider) {}
|
|
733
|
+
imageRequestPricing(_provider, model) {
|
|
734
|
+
try {
|
|
735
|
+
const chain = this.readConfig().chains.find((entry) => entry.id === model);
|
|
736
|
+
const head = chain === void 0 ? void 0 : effectiveHeadOf(chain, new Date(), this.readConfig().tz ?? "Asia/Shanghai");
|
|
737
|
+
if (head === void 0 || head.provider === "FallbacksChain") return void 0;
|
|
738
|
+
return this.getLlm()?.imageRequestPricing(head.provider, head.model);
|
|
739
|
+
} catch {
|
|
740
|
+
return;
|
|
713
741
|
}
|
|
714
742
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
743
|
+
stream(options) {
|
|
744
|
+
return this.walk(options);
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
function installFallbacksAdapter(ctx, readConfig, walk) {
|
|
748
|
+
let llm;
|
|
749
|
+
let disposeAdapter;
|
|
750
|
+
let registered = false;
|
|
751
|
+
const logger = ctx.logger("llm-fallbacks");
|
|
752
|
+
const adapter = new FallbacksChainAdapter(readConfig, () => llm, walk);
|
|
753
|
+
const reconcile = () => {
|
|
754
|
+
const shouldRegister = isFallbackActive(readConfig());
|
|
755
|
+
if (shouldRegister && !registered) {
|
|
756
|
+
if (llm === void 0) return;
|
|
757
|
+
try {
|
|
758
|
+
disposeAdapter = llm.registerAdapter([FALLBACKS_PROVIDER$1], adapter);
|
|
759
|
+
registered = true;
|
|
760
|
+
logger.info("llm-fallbacks: virtual adapter registered — FallbacksChain/<chain id> rows are selectable in the model picker");
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "DUPLICATE_ADAPTER") throw error;
|
|
763
|
+
logger.debug("llm-fallbacks: virtual adapter already registered — no route on this fiber (multi-fiber dedupe)");
|
|
764
|
+
}
|
|
765
|
+
} else if (!shouldRegister && registered) {
|
|
766
|
+
disposeAdapter?.();
|
|
767
|
+
disposeAdapter = void 0;
|
|
768
|
+
registered = false;
|
|
769
|
+
logger.info("llm-fallbacks: virtual adapter unregistered — FallbacksChain rows hidden from the model picker");
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
ctx.inject(["llm"], (llmCtx) => {
|
|
773
|
+
llm = llmCtx.llm;
|
|
774
|
+
reconcile();
|
|
775
|
+
return () => {
|
|
776
|
+
disposeAdapter?.();
|
|
777
|
+
disposeAdapter = void 0;
|
|
778
|
+
registered = false;
|
|
779
|
+
llm = void 0;
|
|
780
|
+
};
|
|
781
|
+
});
|
|
782
|
+
return reconcile;
|
|
719
783
|
}
|
|
720
784
|
//#endregion
|
|
721
|
-
//#region .build/host/
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
785
|
+
//#region .build/host/chain-walk.js
|
|
786
|
+
const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
|
|
787
|
+
const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
|
|
788
|
+
const UNKNOWN_CHAIN_CODE = "UNKNOWN_CHAIN";
|
|
789
|
+
const LLM_UNAVAILABLE_CODE = "LLM_UNAVAILABLE";
|
|
790
|
+
const REQUEST_SCOPED_CODES = new Set(["CONTEXT_WINDOW_EXCEEDED"]);
|
|
791
|
+
async function makeModelExists(catalog, providers) {
|
|
792
|
+
if (catalog === void 0 || typeof catalog.listModels !== "function") return () => true;
|
|
793
|
+
const models = new Map();
|
|
794
|
+
await Promise.all(providers.map(async (provider) => {
|
|
795
|
+
try {
|
|
796
|
+
const rows = await catalog.listModels(provider);
|
|
797
|
+
models.set(provider, new Set(rows.map((row) => row.id)));
|
|
798
|
+
} catch {
|
|
799
|
+
models.set(provider, new Set());
|
|
800
|
+
}
|
|
801
|
+
}));
|
|
802
|
+
return (provider, model) => models.get(provider)?.has(model) ?? false;
|
|
803
|
+
}
|
|
804
|
+
async function makeContextWindowOf(catalog, routes) {
|
|
805
|
+
if (catalog === void 0) return () => void 0;
|
|
806
|
+
const windows = new Map();
|
|
807
|
+
if (typeof catalog.listModels === "function") await Promise.all([...new Set(routes.map((route) => route.provider))].map(async (provider) => {
|
|
808
|
+
try {
|
|
809
|
+
for (const row of await catalog.listModels(provider)) if (typeof row.contextWindow === "number") windows.set(selectorKey(provider, row.id), row.contextWindow);
|
|
810
|
+
} catch {}
|
|
811
|
+
}));
|
|
812
|
+
const resolveModelInfo = catalog.resolveModelInfo?.bind(catalog);
|
|
813
|
+
if (resolveModelInfo !== void 0) await Promise.all(routes.map(async (route) => {
|
|
814
|
+
const key = selectorKey(route.provider, route.model);
|
|
815
|
+
if (windows.has(key)) return;
|
|
816
|
+
try {
|
|
817
|
+
const window = (await resolveModelInfo(route.provider, route.model))?.context?.contextWindow;
|
|
818
|
+
if (typeof window === "number") windows.set(key, window);
|
|
819
|
+
} catch {}
|
|
820
|
+
}));
|
|
821
|
+
return (provider, model) => windows.get(selectorKey(provider, model));
|
|
728
822
|
}
|
|
729
|
-
function
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
823
|
+
function makeModelInfoOf(catalog) {
|
|
824
|
+
const resolveModelInfo = catalog?.resolveModelInfo?.bind(catalog);
|
|
825
|
+
if (resolveModelInfo === void 0) return () => Promise.resolve(void 0);
|
|
826
|
+
const cache = new Map();
|
|
827
|
+
return (provider, model) => {
|
|
828
|
+
const key = selectorKey(provider, model);
|
|
829
|
+
let info = cache.get(key);
|
|
830
|
+
if (info === void 0) {
|
|
831
|
+
info = (async () => {
|
|
832
|
+
try {
|
|
833
|
+
return await resolveModelInfo(provider, model);
|
|
834
|
+
} catch {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
})();
|
|
838
|
+
cache.set(key, info);
|
|
839
|
+
}
|
|
840
|
+
return info;
|
|
841
|
+
};
|
|
747
842
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
843
|
+
async function adaptForTarget(options, target, modelInfoOf, debug) {
|
|
844
|
+
if (options.reasoningEffort === void 0 && options.maxTokens === void 0) return options;
|
|
845
|
+
const info = await modelInfoOf(target.provider, target.model);
|
|
846
|
+
if (info === void 0 || info === null) return options;
|
|
847
|
+
let adapted;
|
|
848
|
+
const dropped = [];
|
|
849
|
+
try {
|
|
850
|
+
adapted = { ...options };
|
|
851
|
+
const effort = options.reasoningEffort;
|
|
852
|
+
if (effort !== void 0 && info.reasoning !== void 0 && !(info.reasoning.efforts ?? []).some((supported) => supported.id === effort)) {
|
|
853
|
+
delete adapted.reasoningEffort;
|
|
854
|
+
dropped.push("reasoningEffort");
|
|
855
|
+
}
|
|
856
|
+
const maxTokens = options.maxTokens;
|
|
857
|
+
if (maxTokens !== void 0 && info.defaultMaxTokens !== void 0 && maxTokens > info.defaultMaxTokens) {
|
|
858
|
+
delete adapted.maxTokens;
|
|
859
|
+
dropped.push("maxTokens");
|
|
860
|
+
}
|
|
861
|
+
} catch {
|
|
862
|
+
return options;
|
|
758
863
|
}
|
|
759
|
-
|
|
864
|
+
if (dropped.length > 0) try {
|
|
865
|
+
debug?.("llm-fallbacks: parameter adaptation candidate=%s/%s dropped=%s", target.provider, target.model, dropped.join(","));
|
|
866
|
+
} catch {}
|
|
867
|
+
return adapted;
|
|
760
868
|
}
|
|
761
|
-
|
|
762
|
-
|
|
869
|
+
const MAX_TOKENS_REJECTION = /max_tokens[\s\S]{0,40}?(非法|invalid|range|范围|exceed|too\s+large)/i;
|
|
870
|
+
function repairableControl(failure, options) {
|
|
871
|
+
if (failure.code === "UNSUPPORTED_REASONING_EFFORT") return options.reasoningEffort === void 0 ? void 0 : "reasoningEffort";
|
|
872
|
+
if (failure.code === "INVALID_REQUEST" && options.maxTokens !== void 0 && MAX_TOKENS_REJECTION.test(failure.message)) return "maxTokens";
|
|
763
873
|
}
|
|
764
|
-
|
|
765
|
-
|
|
874
|
+
const SLOT_MARKER_SEPARATOR = "\0";
|
|
875
|
+
function slotMarkerKey(agentId, chainId) {
|
|
876
|
+
return agentId + SLOT_MARKER_SEPARATOR + chainId;
|
|
766
877
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
function parseRoutes(value) {
|
|
770
|
-
if (!Array.isArray(value)) return void 0;
|
|
771
|
-
const routes = [];
|
|
772
|
-
for (const candidate of value) {
|
|
773
|
-
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate) || !("provider" in candidate) || typeof candidate.provider !== "string" || candidate.provider.length === 0 || !("model" in candidate) || typeof candidate.model !== "string" || candidate.model.length === 0) return;
|
|
774
|
-
routes.push({
|
|
775
|
-
provider: candidate.provider,
|
|
776
|
-
model: candidate.model
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
return routes;
|
|
878
|
+
function slotMarkerPrefix(agentId) {
|
|
879
|
+
return agentId + SLOT_MARKER_SEPARATOR;
|
|
780
880
|
}
|
|
781
|
-
function
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const routes = parseRoutes(typeof data === "object" && data !== null && "allowedModels" in data ? data.allowedModels : void 0);
|
|
789
|
-
if (routes === void 0 || routes.length === 0) return {
|
|
790
|
-
ok: false,
|
|
791
|
-
present: true
|
|
792
|
-
};
|
|
881
|
+
function envelopeProvenance(replayState) {
|
|
882
|
+
if (typeof replayState !== "object" || replayState === null) return void 0;
|
|
883
|
+
const response = replayState.response;
|
|
884
|
+
if (typeof response !== "object" || response === null) return void 0;
|
|
885
|
+
const { provider, model } = response;
|
|
886
|
+
if (typeof provider !== "string" || provider === "" || provider === "FallbacksChain") return void 0;
|
|
887
|
+
if (typeof model !== "string" || model === "") return void 0;
|
|
793
888
|
return {
|
|
794
|
-
|
|
795
|
-
|
|
889
|
+
provider,
|
|
890
|
+
model
|
|
796
891
|
};
|
|
797
892
|
}
|
|
798
|
-
function
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
893
|
+
function restoreEnvelopeProvenance(messages) {
|
|
894
|
+
let changed = false;
|
|
895
|
+
const restored = messages.map((message) => {
|
|
896
|
+
const source = message.source;
|
|
897
|
+
if (message.role !== "assistant" || source?.kind !== "model") return message;
|
|
898
|
+
if (source.provider !== "FallbacksChain") return message;
|
|
899
|
+
const provenance = envelopeProvenance(source.replayState);
|
|
900
|
+
if (provenance === void 0) return message;
|
|
901
|
+
changed = true;
|
|
902
|
+
return {
|
|
903
|
+
...message,
|
|
904
|
+
source: {
|
|
905
|
+
...source,
|
|
906
|
+
...provenance
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
});
|
|
910
|
+
return changed ? restored : messages;
|
|
811
911
|
}
|
|
812
|
-
|
|
912
|
+
function slotWinnerKey(winner) {
|
|
913
|
+
if (winner === "all-day") return "all-day";
|
|
914
|
+
return winner.kind === "preset" ? `preset:${winner.preset}` : `custom:${winner.start}-${winner.end}`;
|
|
915
|
+
}
|
|
916
|
+
function logSlotRotation(deps, start) {
|
|
917
|
+
const { config, now, chain, owner } = start;
|
|
918
|
+
const markers = deps.slotWinners;
|
|
919
|
+
if (markers === void 0 || owner === void 0) return;
|
|
920
|
+
if (!isAllDayConforming(chain.entries) || (chain.timeSlots?.length ?? 0) === 0) return;
|
|
921
|
+
const slot = resolveSlotState(chain, now, config.tz ?? "Asia/Shanghai");
|
|
922
|
+
const markerKey = slotMarkerKey(owner, chain.id);
|
|
923
|
+
const key = slotWinnerKey(slot.winner);
|
|
924
|
+
const previous = markers.get(markerKey);
|
|
925
|
+
if (previous !== void 0 && previous.key !== key) deps.log.info("llm-fallbacks: agent \"%s\" time-slot switch (分时切换) on chain \"%s\": %s -> %s", owner, chain.id, previous.label, slot.label);
|
|
926
|
+
markers.set(markerKey, {
|
|
927
|
+
key,
|
|
928
|
+
label: slot.label
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
function resolveWalk(deps, options) {
|
|
932
|
+
const config = deps.readConfig();
|
|
933
|
+
const now = new Date();
|
|
934
|
+
const owner = options.sessionId;
|
|
935
|
+
const chain = config.chains.find((entry) => entry.id === options.model);
|
|
936
|
+
if (chain === void 0) throw new LlmError(`llm-fallbacks: unknown virtual chain "${String(options.model)}" — re-select a chain in the model picker`, UNKNOWN_CHAIN_CODE);
|
|
937
|
+
const entries = resolveEffectiveChain(chain, now, config.tz ?? "Asia/Shanghai");
|
|
938
|
+
if (entries.length === 0) throw new LlmError("llm-fallbacks: the effective chain is empty — the virtual FallbacksChain route has no head to delegate to", EMPTY_EFFECTIVE_CHAIN_CODE);
|
|
939
|
+
if (!isAllDayConforming(chain.entries)) throw new LlmError("llm-fallbacks: the chain is not conforming (exactly one official model required) — refusing to delegate", UNDISPATCHABLE_HEAD_CODE);
|
|
940
|
+
const head = firstDispatchableExactHead(entries);
|
|
941
|
+
if (head === void 0) throw new LlmError("llm-fallbacks: the effective chain head cannot be dispatched (wildcard, malformed, or self-route) — refusing to delegate", UNDISPATCHABLE_HEAD_CODE);
|
|
942
|
+
const llm = deps.getLlm();
|
|
943
|
+
if (llm === void 0) throw new LlmError("llm-fallbacks: the llm runtime is unavailable — cannot delegate the FallbacksChain call", LLM_UNAVAILABLE_CODE);
|
|
944
|
+
return {
|
|
945
|
+
config,
|
|
946
|
+
now,
|
|
947
|
+
chain,
|
|
948
|
+
entries,
|
|
949
|
+
head,
|
|
950
|
+
llm,
|
|
951
|
+
owner
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
function walkChain(deps, options) {
|
|
955
|
+
return walk(deps, resolveWalk(deps, options), options);
|
|
956
|
+
}
|
|
957
|
+
async function* walk(deps, start, options) {
|
|
958
|
+
const { config, chain, head, llm, owner } = start;
|
|
959
|
+
logSlotRotation(deps, start);
|
|
960
|
+
const modelInfoOf = makeModelInfoOf(llm);
|
|
961
|
+
const messages = restoreEnvelopeProvenance(options.messages);
|
|
962
|
+
const stateOf = () => owner === void 0 ? void 0 : deps.states.peek(owner);
|
|
963
|
+
const failedKeys = new Set();
|
|
964
|
+
const isCooled = (key) => {
|
|
965
|
+
const state = stateOf();
|
|
966
|
+
return state !== void 0 && deps.states.isSuppressed(state, key, Date.now(), config.recovery ?? "timer");
|
|
967
|
+
};
|
|
968
|
+
const coolRoute = (failed) => {
|
|
969
|
+
if (owner === void 0) return;
|
|
970
|
+
const state = deps.states.get(owner);
|
|
971
|
+
const key = selectorKey(failed.provider, failed.model);
|
|
972
|
+
const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : (config.recovery ?? "timer") === "half-open" ? Date.now() + escalatedCooldownMs(config.cooldownMs, state.recovery.recordFailure(key)) : Date.now() + config.cooldownMs;
|
|
973
|
+
deps.states.suppress(state, key, until);
|
|
974
|
+
};
|
|
975
|
+
const bookFailure = (failed, code) => {
|
|
976
|
+
failedKeys.add(selectorKey(failed.provider, failed.model));
|
|
977
|
+
if (!REQUEST_SCOPED_CODES.has(code)) coolRoute(failed);
|
|
978
|
+
};
|
|
979
|
+
const advanceFrom = async (from, failureCode) => {
|
|
980
|
+
const { all, wildcard } = resolveChainViews(start.entries, from.provider, from.model, deps.log.warn);
|
|
981
|
+
const own = all.map((candidate) => candidate.provider !== FALLBACKS_PROVIDER$1);
|
|
982
|
+
const candidates = all.filter((_, index) => own[index]);
|
|
983
|
+
const wildcards = wildcard.filter((_, index) => own[index]);
|
|
984
|
+
const exists = hasWildcardEntry(start.entries) ? await makeModelExists(llm, [...new Set(candidates.map((candidate) => candidate.provider))]) : void 0;
|
|
985
|
+
const contextWindowOf = failureCode === "CONTEXT_WINDOW_EXCEEDED" ? await makeContextWindowOf(llm, [from, ...candidates.flatMap((candidate) => candidate.model === void 0 ? [] : [{
|
|
986
|
+
provider: candidate.provider,
|
|
987
|
+
model: candidate.model
|
|
988
|
+
}])]) : void 0;
|
|
989
|
+
const minContextWindow = contextWindowOf?.(from.provider, from.model);
|
|
990
|
+
const cooldown = { isSuppressed: (key) => isCooled(key) };
|
|
991
|
+
const failed = { has: (key) => failedKeys.has(key) };
|
|
992
|
+
const surviving = selectCandidates(candidates, wildcards, createCandidateFilter({
|
|
993
|
+
current: from,
|
|
994
|
+
cooldown,
|
|
995
|
+
failed,
|
|
996
|
+
minContextWindow,
|
|
997
|
+
contextWindowOf
|
|
998
|
+
}), exists);
|
|
999
|
+
const annotated = annotateCandidates(candidates, surviving, {
|
|
1000
|
+
current: from,
|
|
1001
|
+
cooldown,
|
|
1002
|
+
failed,
|
|
1003
|
+
minContextWindow,
|
|
1004
|
+
contextWindowOf
|
|
1005
|
+
});
|
|
1006
|
+
const describeCandidates = (list) => list.map(({ candidate, skip }) => skip === void 0 ? `${candidate.provider}/${candidate.model}` : `${candidate.provider}/${candidate.model} (skipped: ${skip})`);
|
|
1007
|
+
const first = surviving[0];
|
|
1008
|
+
if (first?.model === void 0) return { candidates: describeCandidates(annotated) };
|
|
1009
|
+
return {
|
|
1010
|
+
target: {
|
|
1011
|
+
provider: first.provider,
|
|
1012
|
+
model: first.model
|
|
1013
|
+
},
|
|
1014
|
+
candidates: describeCandidates(annotated)
|
|
1015
|
+
};
|
|
1016
|
+
};
|
|
1017
|
+
let target;
|
|
1018
|
+
if (isCooled(selectorKey(head.provider, head.model))) target = (await advanceFrom(head))?.target ?? head;
|
|
1019
|
+
else target = head;
|
|
1020
|
+
let switches = 0;
|
|
1021
|
+
for (;;) {
|
|
1022
|
+
const key = selectorKey(target.provider, target.model);
|
|
1023
|
+
const state = stateOf();
|
|
1024
|
+
if ((config.recovery ?? "timer") === "half-open" && state !== void 0 && state.recovery.tryMarkProbeLogged(key)) deps.log.info("llm-fallbacks: agent \"%s\" half-open probe %s/%s (chain=%s, reason=%s)", owner ?? "", target.provider, target.model, chain.id, "trigger-code");
|
|
1025
|
+
let committed = false;
|
|
1026
|
+
let swallowed;
|
|
1027
|
+
let attemptOptions = await adaptForTarget(options, target, modelInfoOf, deps.log.debug);
|
|
1028
|
+
const repaired = new Set();
|
|
1029
|
+
for (;;) {
|
|
1030
|
+
committed = false;
|
|
1031
|
+
swallowed = void 0;
|
|
1032
|
+
let repair;
|
|
1033
|
+
const pendingUsage = [];
|
|
1034
|
+
for await (const chunk of llm.stream({
|
|
1035
|
+
...attemptOptions,
|
|
1036
|
+
provider: target.provider,
|
|
1037
|
+
model: target.model,
|
|
1038
|
+
messages
|
|
1039
|
+
})) {
|
|
1040
|
+
const failure = chunk.type === "finish" && chunk.reason.kind === "error" ? chunk.reason.failure : void 0;
|
|
1041
|
+
if (failure !== void 0 && !committed) {
|
|
1042
|
+
const repairable = repairableControl(failure, attemptOptions);
|
|
1043
|
+
if (repairable !== void 0 && !repaired.has(repairable)) {
|
|
1044
|
+
repair = repairable;
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
if (config.triggerCodes.includes(failure.code)) {
|
|
1048
|
+
swallowed = {
|
|
1049
|
+
chunk,
|
|
1050
|
+
code: failure.code
|
|
1051
|
+
};
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (chunk.type === "finish") {
|
|
1056
|
+
if (failure === void 0) {
|
|
1057
|
+
if (chunk.reason.kind !== "aborted" && state !== void 0) deps.states.observeSuccess(state, key);
|
|
1058
|
+
} else if (config.triggerCodes.includes(failure.code)) bookFailure(target, failure.code);
|
|
1059
|
+
for (const pending of pendingUsage) yield pending;
|
|
1060
|
+
pendingUsage.length = 0;
|
|
1061
|
+
yield chunk;
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (chunk.type === "usage" && !committed) {
|
|
1065
|
+
pendingUsage.push(chunk);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
for (const pending of pendingUsage) yield pending;
|
|
1069
|
+
pendingUsage.length = 0;
|
|
1070
|
+
yield chunk;
|
|
1071
|
+
if (chunk.type !== "usage") committed = true;
|
|
1072
|
+
}
|
|
1073
|
+
if (repair === void 0) {
|
|
1074
|
+
for (const pending of pendingUsage) yield pending;
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
repaired.add(repair);
|
|
1078
|
+
attemptOptions = { ...attemptOptions };
|
|
1079
|
+
delete attemptOptions[repair];
|
|
1080
|
+
deps.log.info("llm-fallbacks: agent \"%s\" parameter repair (参数修复) candidate=%s/%s dropped=%s", owner ?? "", target.provider, target.model, repair);
|
|
1081
|
+
}
|
|
1082
|
+
if (swallowed === void 0) return;
|
|
1083
|
+
bookFailure(target, swallowed.code);
|
|
1084
|
+
const next = switches < config.maxSwitchesPerStep ? await advanceFrom(target, swallowed.code) : void 0;
|
|
1085
|
+
if (next === void 0 || next.target === void 0) {
|
|
1086
|
+
yield swallowed.chunk;
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
switches += 1;
|
|
1090
|
+
deps.log.info("llm-fallbacks: agent \"%s\" fallback switch (降级切换) %s/%s -> %s/%s (chain=%s, reason=%s, candidates=%o)", owner ?? "", target.provider, target.model, next.target.provider, next.target.model, chain.id, "trigger-code", next.candidates);
|
|
1091
|
+
target = next.target;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
//#endregion
|
|
813
1095
|
//#region .build/host/role-notice.js
|
|
814
1096
|
const ROLE_NOTICE_PLUGIN = "dsh-llm-fallbacks";
|
|
815
1097
|
const ROLE_NOTICE_SOURCE_KIND = "llm-fallbacks-role-notice";
|
|
@@ -1025,7 +1307,7 @@ function declaredExecuteAsValues(prompt) {
|
|
|
1025
1307
|
function canonicalizeRoleReference(raw) {
|
|
1026
1308
|
const trimmed = raw.trim();
|
|
1027
1309
|
const id = (trimmed.startsWith("@") ? trimmed.slice(1) : trimmed).trim();
|
|
1028
|
-
if (id === ""
|
|
1310
|
+
if (id === "") return void 0;
|
|
1029
1311
|
return id;
|
|
1030
1312
|
}
|
|
1031
1313
|
function lookupDeclaredRole(id, roleIds) {
|
|
@@ -1086,7 +1368,8 @@ function seamStateFor(root) {
|
|
|
1086
1368
|
if (existing !== void 0) return existing;
|
|
1087
1369
|
const created = {
|
|
1088
1370
|
records: new Map(),
|
|
1089
|
-
noticeEmitted: new Set()
|
|
1371
|
+
noticeEmitted: new Set(),
|
|
1372
|
+
fibers: 0
|
|
1090
1373
|
};
|
|
1091
1374
|
subagentSeamStates.set(root, created);
|
|
1092
1375
|
return created;
|
|
@@ -1105,6 +1388,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1105
1388
|
const pending = new Map();
|
|
1106
1389
|
const wrappers = new WeakMap();
|
|
1107
1390
|
const disposers = [];
|
|
1391
|
+
let disposed = false;
|
|
1108
1392
|
let unreadableServiceLogged = false;
|
|
1109
1393
|
let noRoleLogged = false;
|
|
1110
1394
|
const debug = (message) => {
|
|
@@ -1115,15 +1399,44 @@ function installSubagentSeam(ctx, options) {
|
|
|
1115
1399
|
const reportDegrade = (error) => {
|
|
1116
1400
|
debug(`llm-fallbacks: subagent role seam degraded to the native path (start unaffected): ${errorMessage$1(error)}`);
|
|
1117
1401
|
};
|
|
1118
|
-
const recordChild = (childId, role, at,
|
|
1402
|
+
const recordChild = (childId, role, at, verdict = {}) => {
|
|
1119
1403
|
const existing = records.get(childId);
|
|
1120
1404
|
records.set(childId, {
|
|
1121
1405
|
role,
|
|
1122
1406
|
at,
|
|
1123
1407
|
firstNoticePending: existing?.firstNoticePending ?? true,
|
|
1124
|
-
...existing?.personaNotApplied === true || personaMissed === true ? { personaNotApplied: true } : {}
|
|
1408
|
+
...existing?.personaNotApplied === true || verdict.personaMissed === true ? { personaNotApplied: true } : {}
|
|
1125
1409
|
});
|
|
1126
1410
|
};
|
|
1411
|
+
const hasExplicitAgentOptions = (request) => {
|
|
1412
|
+
const agentOptions = request?.agentOptions;
|
|
1413
|
+
return typeof agentOptions?.provider === "string" || typeof agentOptions?.model === "string";
|
|
1414
|
+
};
|
|
1415
|
+
const injectRoleChain = (spec, role) => {
|
|
1416
|
+
try {
|
|
1417
|
+
if (options.source === void 0) return void 0;
|
|
1418
|
+
if (hasExplicitAgentOptions(spec.request)) return void 0;
|
|
1419
|
+
const config = options.source();
|
|
1420
|
+
if (!isFallbackActive(config)) return void 0;
|
|
1421
|
+
const chainId = config.roles.list.find((entry) => entry.id === role)?.chain;
|
|
1422
|
+
if (chainId === void 0 || chainId === "") return void 0;
|
|
1423
|
+
if (!config.chains.some((chain) => chain.id === chainId)) {
|
|
1424
|
+
debug(`llm-fallbacks: subagent seam — role "${role}" names undeclared chain "${chainId}"; the child inherits the delegating agent's route`);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
...spec.request,
|
|
1429
|
+
agentOptions: {
|
|
1430
|
+
...spec.request?.agentOptions,
|
|
1431
|
+
provider: FALLBACKS_PROVIDER$1,
|
|
1432
|
+
model: chainId
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
reportDegrade(error);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1127
1440
|
const beginCorrelation = (spec, role, at) => {
|
|
1128
1441
|
const parentSessionId = parentSessionIdOf(spec?.request);
|
|
1129
1442
|
const label = typeof spec?.label === "string" ? spec.label : void 0;
|
|
@@ -1143,7 +1456,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1143
1456
|
const endCorrelation = (key) => {
|
|
1144
1457
|
if (key !== void 0) pending.delete(key);
|
|
1145
1458
|
};
|
|
1146
|
-
const
|
|
1459
|
+
const resolveSeamRole = (request) => {
|
|
1147
1460
|
try {
|
|
1148
1461
|
const prompt = promptTextOf(request?.prompt);
|
|
1149
1462
|
if (prompt.trim() === "") return void 0;
|
|
@@ -1209,7 +1522,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1209
1522
|
if (cached !== void 0) return cached;
|
|
1210
1523
|
const wrapper = Object.create(value);
|
|
1211
1524
|
wrapper.startActivation = (spec) => {
|
|
1212
|
-
const role =
|
|
1525
|
+
const role = resolveSeamRole(spec?.request) ?? (typeof spec?.childId === "string" ? records.get(spec.childId)?.role : void 0);
|
|
1213
1526
|
if (role === void 0) return service.startActivation(spec);
|
|
1214
1527
|
const at = Date.now();
|
|
1215
1528
|
let effectiveSpec = spec;
|
|
@@ -1218,9 +1531,14 @@ function installSubagentSeam(ctx, options) {
|
|
|
1218
1531
|
try {
|
|
1219
1532
|
if (isLocalBackend(service, spec.provider)) {
|
|
1220
1533
|
reservedId = spec.childId ?? randomUUID();
|
|
1221
|
-
|
|
1534
|
+
const stamped = spec.childId !== void 0 && records.has(spec.childId) ? void 0 : injectRoleChain(spec, role);
|
|
1535
|
+
effectiveSpec = stamped === void 0 ? reservedId === spec.childId ? spec : {
|
|
1222
1536
|
...spec,
|
|
1223
1537
|
childId: reservedId
|
|
1538
|
+
} : {
|
|
1539
|
+
...spec,
|
|
1540
|
+
childId: reservedId,
|
|
1541
|
+
request: stamped
|
|
1224
1542
|
};
|
|
1225
1543
|
recordChild(reservedId, role, at);
|
|
1226
1544
|
}
|
|
@@ -1252,7 +1570,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1252
1570
|
try {
|
|
1253
1571
|
observeStartResult(result, (childId) => {
|
|
1254
1572
|
endCorrelation(pendingKey);
|
|
1255
|
-
recordChild(childId, role, at, reservationPersonaMissed);
|
|
1573
|
+
recordChild(childId, role, at, { personaMissed: reservationPersonaMissed });
|
|
1256
1574
|
}, () => {}, () => {
|
|
1257
1575
|
endCorrelation(pendingKey);
|
|
1258
1576
|
if (reservedId !== void 0) records.delete(reservedId);
|
|
@@ -1266,8 +1584,19 @@ function installSubagentSeam(ctx, options) {
|
|
|
1266
1584
|
wrappers.set(value, wrapper);
|
|
1267
1585
|
return wrapper;
|
|
1268
1586
|
};
|
|
1587
|
+
const installSurface = (register) => {
|
|
1588
|
+
try {
|
|
1589
|
+
disposers.push(register());
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
for (const withdraw of disposers.splice(0)) try {
|
|
1592
|
+
withdraw();
|
|
1593
|
+
} catch {}
|
|
1594
|
+
if (ownsWrapper) subagentSeamRoots.delete(root);
|
|
1595
|
+
throw error;
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1269
1598
|
if (ownsWrapper) {
|
|
1270
|
-
|
|
1599
|
+
installSurface(() => ctx.on(SUBAGENT_SEAM_EVENT, (_readCtx, name, _error, next) => {
|
|
1271
1600
|
const value = next();
|
|
1272
1601
|
if (name !== "subagents") return value;
|
|
1273
1602
|
try {
|
|
@@ -1277,7 +1606,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1277
1606
|
return value;
|
|
1278
1607
|
}
|
|
1279
1608
|
}));
|
|
1280
|
-
|
|
1609
|
+
installSurface(() => ctx.on("session/event", (session, event) => {
|
|
1281
1610
|
try {
|
|
1282
1611
|
if (event.type !== SUBAGENT_CATALOG_EVENT) return;
|
|
1283
1612
|
const data = event.data;
|
|
@@ -1295,21 +1624,39 @@ function installSubagentSeam(ctx, options) {
|
|
|
1295
1624
|
}
|
|
1296
1625
|
}));
|
|
1297
1626
|
}
|
|
1298
|
-
|
|
1627
|
+
installSurface(() => installRoleNotice(ctx, {
|
|
1299
1628
|
records,
|
|
1300
1629
|
emitted: noticeEmitted,
|
|
1301
1630
|
debug
|
|
1302
1631
|
}));
|
|
1303
|
-
|
|
1632
|
+
installSurface(() => installRoleProjection(ctx, { debug }));
|
|
1633
|
+
state.fibers += 1;
|
|
1634
|
+
const dispose = () => {
|
|
1635
|
+
if (disposed) return;
|
|
1636
|
+
disposed = true;
|
|
1637
|
+
if (ownsWrapper) subagentSeamRoots.delete(root);
|
|
1638
|
+
try {
|
|
1639
|
+
for (const withdraw of disposers.splice(0)) withdraw();
|
|
1640
|
+
} finally {
|
|
1641
|
+
state.fibers -= 1;
|
|
1642
|
+
if (state.fibers === 0) {
|
|
1643
|
+
state.records.clear();
|
|
1644
|
+
state.noticeEmitted.clear();
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1304
1648
|
const seam = {
|
|
1305
1649
|
records,
|
|
1306
1650
|
noticeEmitted,
|
|
1307
1651
|
ownsWrapper,
|
|
1308
|
-
dispose
|
|
1309
|
-
if (ownsWrapper) subagentSeamRoots.delete(root);
|
|
1310
|
-
for (const dispose of disposers.splice(0)) dispose();
|
|
1311
|
-
}
|
|
1652
|
+
dispose
|
|
1312
1653
|
};
|
|
1654
|
+
try {
|
|
1655
|
+
ctx.effect(() => () => dispose(), "llm-fallbacks: subagent seam teardown");
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
dispose();
|
|
1658
|
+
throw error;
|
|
1659
|
+
}
|
|
1313
1660
|
subagentSeamStores.set(ctx, seam);
|
|
1314
1661
|
return seam;
|
|
1315
1662
|
}
|
|
@@ -1440,21 +1787,6 @@ var CooldownStore = class {
|
|
|
1440
1787
|
return active;
|
|
1441
1788
|
}
|
|
1442
1789
|
};
|
|
1443
|
-
var StepFailureSet = class {
|
|
1444
|
-
keys = new Set();
|
|
1445
|
-
get size() {
|
|
1446
|
-
return this.keys.size;
|
|
1447
|
-
}
|
|
1448
|
-
add(key) {
|
|
1449
|
-
this.keys.add(key);
|
|
1450
|
-
}
|
|
1451
|
-
has(key) {
|
|
1452
|
-
return this.keys.has(key);
|
|
1453
|
-
}
|
|
1454
|
-
reset() {
|
|
1455
|
-
this.keys.clear();
|
|
1456
|
-
}
|
|
1457
|
-
};
|
|
1458
1790
|
//#endregion
|
|
1459
1791
|
//#region .build/host/state.js
|
|
1460
1792
|
var FallbackStateStore = class {
|
|
@@ -1472,12 +1804,6 @@ var FallbackStateStore = class {
|
|
|
1472
1804
|
let state = this.states.get(agentId);
|
|
1473
1805
|
if (state === void 0) {
|
|
1474
1806
|
state = {
|
|
1475
|
-
stepFailures: {
|
|
1476
|
-
turn: 0,
|
|
1477
|
-
step: 0,
|
|
1478
|
-
failed: new StepFailureSet(),
|
|
1479
|
-
switchCount: 0
|
|
1480
|
-
},
|
|
1481
1807
|
cooldown: new CooldownStore(),
|
|
1482
1808
|
recovery: new RecoveryStore()
|
|
1483
1809
|
};
|
|
@@ -1491,42 +1817,6 @@ var FallbackStateStore = class {
|
|
|
1491
1817
|
clear() {
|
|
1492
1818
|
this.states.clear();
|
|
1493
1819
|
}
|
|
1494
|
-
syncStep(state, turn, step) {
|
|
1495
|
-
const { stepFailures } = state;
|
|
1496
|
-
if (stepFailures.turn === turn && stepFailures.step === step) return;
|
|
1497
|
-
stepFailures.turn = turn;
|
|
1498
|
-
stepFailures.step = step;
|
|
1499
|
-
stepFailures.failed.reset();
|
|
1500
|
-
stepFailures.switchCount = 0;
|
|
1501
|
-
}
|
|
1502
|
-
recordFailure(state, key) {
|
|
1503
|
-
state.stepFailures.failed.add(key);
|
|
1504
|
-
}
|
|
1505
|
-
recordSwitch(state) {
|
|
1506
|
-
state.stepFailures.switchCount += 1;
|
|
1507
|
-
}
|
|
1508
|
-
writePending(state, pending) {
|
|
1509
|
-
state.pendingSwitch = pending;
|
|
1510
|
-
state.appliedTurnStep = void 0;
|
|
1511
|
-
}
|
|
1512
|
-
applyPending(state, turn, step) {
|
|
1513
|
-
const pending = state.pendingSwitch;
|
|
1514
|
-
if (pending === void 0) return void 0;
|
|
1515
|
-
const applied = state.appliedTurnStep;
|
|
1516
|
-
if (applied !== void 0 && applied.turn === turn && applied.step === step) return void 0;
|
|
1517
|
-
state.appliedTurnStep = {
|
|
1518
|
-
turn,
|
|
1519
|
-
step
|
|
1520
|
-
};
|
|
1521
|
-
state.pendingSwitch = void 0;
|
|
1522
|
-
return pending;
|
|
1523
|
-
}
|
|
1524
|
-
clearStepState(state) {
|
|
1525
|
-
state.pendingSwitch = void 0;
|
|
1526
|
-
state.appliedTurnStep = void 0;
|
|
1527
|
-
state.stepFailures.failed.reset();
|
|
1528
|
-
state.stepFailures.switchCount = 0;
|
|
1529
|
-
}
|
|
1530
1820
|
suppress(state, key, untilEpochMs) {
|
|
1531
1821
|
state.cooldown.suppress(key, untilEpochMs);
|
|
1532
1822
|
}
|
|
@@ -1549,53 +1839,31 @@ var FallbackStateStore = class {
|
|
|
1549
1839
|
}
|
|
1550
1840
|
};
|
|
1551
1841
|
//#endregion
|
|
1552
|
-
//#region .build/host/override.js
|
|
1553
|
-
function overrideConfigWithRouteRule(seed, to, explicitEffort) {
|
|
1554
|
-
if (explicitEffort !== void 0) return {
|
|
1555
|
-
...seed,
|
|
1556
|
-
reasoningEffort: explicitEffort,
|
|
1557
|
-
provider: to.provider,
|
|
1558
|
-
model: to.model
|
|
1559
|
-
};
|
|
1560
|
-
if (!(to.provider !== seed.provider || to.model !== seed.model)) return {
|
|
1561
|
-
...seed,
|
|
1562
|
-
provider: to.provider,
|
|
1563
|
-
model: to.model
|
|
1564
|
-
};
|
|
1565
|
-
const { reasoningEffort: _routeOwned, ...withoutRouteOwnedEffort } = seed;
|
|
1566
|
-
return {
|
|
1567
|
-
...withoutRouteOwnedEffort,
|
|
1568
|
-
provider: to.provider,
|
|
1569
|
-
model: to.model
|
|
1570
|
-
};
|
|
1571
|
-
}
|
|
1572
|
-
//#endregion
|
|
1573
1842
|
//#region .build/host/gateway.js
|
|
1574
1843
|
const FALLBACKS_PROFILE_ENTRY = "llm-fallbacks";
|
|
1575
|
-
const SUBAGENT_POLICY_SETTINGS_NS = "subagent-model-selection-settings";
|
|
1576
|
-
const VIRTUAL_ALL_ROUTE = {
|
|
1577
|
-
provider: "FallbacksChain",
|
|
1578
|
-
model: "Auto"
|
|
1579
|
-
};
|
|
1580
1844
|
const CONFIG_KEYS = {
|
|
1581
1845
|
triggerCodes: true,
|
|
1582
|
-
|
|
1846
|
+
chains: true,
|
|
1583
1847
|
roles: true,
|
|
1584
1848
|
cooldownMs: true,
|
|
1585
1849
|
revertPolicy: true,
|
|
1586
1850
|
maxSwitchesPerStep: true,
|
|
1587
|
-
alwaysModeRetryCap: true,
|
|
1588
1851
|
presets: true,
|
|
1589
|
-
roleAutoMatch: true,
|
|
1590
|
-
timeSlots: true,
|
|
1591
1852
|
tz: true,
|
|
1592
1853
|
recovery: true
|
|
1593
1854
|
};
|
|
1594
|
-
const ROLES_KEYS = {
|
|
1595
|
-
|
|
1596
|
-
|
|
1855
|
+
const ROLES_KEYS = { list: true };
|
|
1856
|
+
const LEGACY_KEYS = new Set([
|
|
1857
|
+
"enabled",
|
|
1858
|
+
"rootChain",
|
|
1859
|
+
"timeSlots"
|
|
1860
|
+
]);
|
|
1861
|
+
const CHAIN_KEYS = {
|
|
1862
|
+
id: true,
|
|
1863
|
+
label: true,
|
|
1864
|
+
entries: true,
|
|
1865
|
+
timeSlots: true
|
|
1597
1866
|
};
|
|
1598
|
-
const LEGACY_KEYS = new Set(["enabled"]);
|
|
1599
1867
|
const SLOT_ROW_KEYS = {
|
|
1600
1868
|
kind: true,
|
|
1601
1869
|
preset: true,
|
|
@@ -1610,12 +1878,10 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1610
1878
|
bridge;
|
|
1611
1879
|
seeds;
|
|
1612
1880
|
settings;
|
|
1613
|
-
|
|
1614
|
-
constructor(ctx, bridge, seeds, subagentPolicy) {
|
|
1881
|
+
constructor(ctx, bridge, seeds) {
|
|
1615
1882
|
super(ctx, "fallbacks");
|
|
1616
1883
|
this.bridge = bridge;
|
|
1617
1884
|
this.seeds = seeds;
|
|
1618
|
-
this.subagentPolicy = subagentPolicy;
|
|
1619
1885
|
ctx.inject(["settings"], (sctx) => {
|
|
1620
1886
|
this.settings = sctx.settings;
|
|
1621
1887
|
return () => {
|
|
@@ -1629,11 +1895,10 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1629
1895
|
async set(patch) {
|
|
1630
1896
|
validateConfigPatch(patch);
|
|
1631
1897
|
if (Object.keys(patch).length === 0) return this.readResult();
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
await settings.update(FALLBACKS_PROFILE_ENTRY, normalized);
|
|
1898
|
+
if (this.settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
1899
|
+
const normalized = stripLegacyForWrite(Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== null)));
|
|
1900
|
+
if (Object.keys(normalized).length === 0 && Object.keys(patch).length === 0) return this.readResult();
|
|
1901
|
+
await this.writeSection(normalized);
|
|
1637
1902
|
return this.readResult();
|
|
1638
1903
|
}
|
|
1639
1904
|
async reset() {
|
|
@@ -1650,40 +1915,23 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1650
1915
|
outcome
|
|
1651
1916
|
};
|
|
1652
1917
|
}
|
|
1653
|
-
async syncAllowlist() {
|
|
1654
|
-
const snapshot = this.subagentPolicy;
|
|
1655
|
-
if (snapshot === void 0) throw new Error("fallbacks: subagent policy is unavailable — allowlist cannot be synced");
|
|
1656
|
-
const { policy } = snapshot();
|
|
1657
|
-
if (policy.state !== "enabled") throw new Error(`fallbacks: subagent policy is ${policy.state} — allowlist cannot be synced`);
|
|
1658
|
-
const settings = this.settings;
|
|
1659
|
-
if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
1660
|
-
const merged = mergedAllowlist(policy.allowedModels, this.bridge.source());
|
|
1661
|
-
await settings.mutate(SUBAGENT_POLICY_SETTINGS_NS, [{
|
|
1662
|
-
op: "set",
|
|
1663
|
-
path: ["allowedModels"],
|
|
1664
|
-
value: merged
|
|
1665
|
-
}]);
|
|
1666
|
-
return this.readResult();
|
|
1667
|
-
}
|
|
1668
1918
|
readConfig(source = this.bridge.source()) {
|
|
1919
|
+
const migrated = migrateLegacyConfig(source);
|
|
1669
1920
|
const wire = {};
|
|
1670
1921
|
for (const key of Object.keys(CONFIG_KEYS)) {
|
|
1671
|
-
const value =
|
|
1922
|
+
const value = migrated[key];
|
|
1672
1923
|
if (value === void 0) continue;
|
|
1673
1924
|
wire[key] = key === "roles" ? normalizeRoles(value) : value;
|
|
1674
1925
|
}
|
|
1675
1926
|
return wire;
|
|
1676
1927
|
}
|
|
1677
1928
|
readResult() {
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
config: this.readConfig(source),
|
|
1681
|
-
legacyKeys: detectLegacyKeys(
|
|
1929
|
+
const raw = this.bridge.rawSource?.() ?? this.bridge.source();
|
|
1930
|
+
return {
|
|
1931
|
+
config: this.readConfig(this.bridge.source()),
|
|
1932
|
+
legacyKeys: detectLegacyKeys(raw),
|
|
1682
1933
|
seeds: this.seeds.wireStatus(this.seedsIo())
|
|
1683
1934
|
};
|
|
1684
|
-
const projected = projectSubagentPolicy(this.subagentPolicy);
|
|
1685
|
-
if (projected !== void 0) result.subagentPolicy = projected;
|
|
1686
|
-
return result;
|
|
1687
1935
|
}
|
|
1688
1936
|
seedsIo() {
|
|
1689
1937
|
const settings = this.settings;
|
|
@@ -1691,83 +1939,100 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
|
|
|
1691
1939
|
read: () => this.bridge.source(),
|
|
1692
1940
|
writeRoles: (roles) => {
|
|
1693
1941
|
if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
1694
|
-
return
|
|
1942
|
+
return this.writeSection(stripLegacyForWrite({ roles }));
|
|
1695
1943
|
}
|
|
1696
1944
|
};
|
|
1697
1945
|
}
|
|
1946
|
+
async writeSection(patch) {
|
|
1947
|
+
const settings = this.settings;
|
|
1948
|
+
if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
|
|
1949
|
+
await writeFallbacksSection(settings, patch);
|
|
1950
|
+
}
|
|
1698
1951
|
};
|
|
1699
|
-
function
|
|
1700
|
-
const
|
|
1701
|
-
const
|
|
1702
|
-
const
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1952
|
+
async function writeFallbacksSection(settings, patch, ns = FALLBACKS_PROFILE_ENTRY) {
|
|
1953
|
+
const stored = storedSection(settings, ns);
|
|
1954
|
+
const cleaned = stripLegacyForWrite(patch);
|
|
1955
|
+
const patchHasRoles = cleaned["roles"] !== void 0;
|
|
1956
|
+
const removals = legacySectionRemovals(stored).filter((op) => !(patchHasRoles && op.path[0] === "roles"));
|
|
1957
|
+
const ops = [];
|
|
1958
|
+
const storedChains = stored?.["chains"];
|
|
1959
|
+
const storedHasUsableChains = Array.isArray(storedChains) && storedChains.length > 0;
|
|
1960
|
+
if (cleaned["chains"] === void 0 && !storedHasUsableChains && stored !== void 0 && (Object.hasOwn(stored, "rootChain") || Object.hasOwn(stored, "timeSlots"))) {
|
|
1961
|
+
const migrated = migrateLegacyConfig({
|
|
1962
|
+
...stored,
|
|
1963
|
+
...cleaned
|
|
1709
1964
|
});
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
let parsed;
|
|
1715
|
-
try {
|
|
1716
|
-
parsed = parseSelector(entry);
|
|
1717
|
-
} catch {
|
|
1718
|
-
continue;
|
|
1719
|
-
}
|
|
1720
|
-
if (parsed.model === void 0) continue;
|
|
1721
|
-
push({
|
|
1722
|
-
provider: parsed.provider,
|
|
1723
|
-
model: parsed.model
|
|
1965
|
+
if (migrated.chains.length > 0) ops.push({
|
|
1966
|
+
op: "set",
|
|
1967
|
+
path: ["chains"],
|
|
1968
|
+
value: migrated.chains
|
|
1724
1969
|
});
|
|
1725
1970
|
}
|
|
1726
|
-
push(
|
|
1727
|
-
|
|
1971
|
+
for (const [key, value] of Object.entries(cleaned)) ops.push({
|
|
1972
|
+
op: "set",
|
|
1973
|
+
path: [key],
|
|
1974
|
+
value
|
|
1975
|
+
});
|
|
1976
|
+
ops.push(...removals);
|
|
1977
|
+
if (ops.length > 0) await settings.mutate(ns, ops);
|
|
1978
|
+
}
|
|
1979
|
+
function storedSection(settings, ns) {
|
|
1980
|
+
const user = settings.describe().find((candidate) => candidate.ns === ns)?.user;
|
|
1981
|
+
if (typeof user !== "object" || user === null || Array.isArray(user)) return void 0;
|
|
1982
|
+
return user;
|
|
1983
|
+
}
|
|
1984
|
+
function legacySectionRemovals(section) {
|
|
1985
|
+
if (section === void 0) return [];
|
|
1986
|
+
const removals = [];
|
|
1987
|
+
for (const key of LEGACY_KEYS) if (Object.hasOwn(section, key)) removals.push({
|
|
1988
|
+
op: "unset",
|
|
1989
|
+
path: [key]
|
|
1990
|
+
});
|
|
1991
|
+
const roles = section["roles"];
|
|
1992
|
+
if (typeof roles !== "object" || roles === null || Array.isArray(roles)) return removals;
|
|
1993
|
+
const declared = roles;
|
|
1994
|
+
for (const key of ["rules", "default"]) if (Object.hasOwn(declared, key)) removals.push({
|
|
1995
|
+
op: "unset",
|
|
1996
|
+
path: ["roles", key]
|
|
1997
|
+
});
|
|
1998
|
+
if (!Array.isArray(declared["list"])) return removals;
|
|
1999
|
+
if (declared["list"].some((row) => typeof row === "object" && row !== null && !Array.isArray(row) && (Object.hasOwn(row, "fallback") || Array.isArray(row["chain"])))) {
|
|
2000
|
+
const cleanedRoles = normalizeRoles({ list: declared["list"] });
|
|
2001
|
+
removals.push({
|
|
2002
|
+
op: "set",
|
|
2003
|
+
path: ["roles", "list"],
|
|
2004
|
+
value: cleanedRoles["list"] ?? []
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
return removals;
|
|
1728
2008
|
}
|
|
1729
|
-
function
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
state: "enabled",
|
|
1742
|
-
allowedModels: policy.allowedModels.map((route) => ({
|
|
1743
|
-
provider: route.provider,
|
|
1744
|
-
model: route.model
|
|
1745
|
-
}))
|
|
1746
|
-
};
|
|
1747
|
-
if (head !== void 0) wire.head = {
|
|
1748
|
-
route: {
|
|
1749
|
-
provider: head.route.provider,
|
|
1750
|
-
model: head.route.model
|
|
1751
|
-
},
|
|
1752
|
-
source: head.source
|
|
1753
|
-
};
|
|
1754
|
-
if (blockedAttempt !== void 0) wire.blockedAttempt = {
|
|
1755
|
-
at: blockedAttempt.at,
|
|
1756
|
-
route: {
|
|
1757
|
-
provider: blockedAttempt.route.provider,
|
|
1758
|
-
model: blockedAttempt.route.model
|
|
1759
|
-
},
|
|
1760
|
-
reason: blockedAttempt.reason
|
|
1761
|
-
};
|
|
1762
|
-
return wire;
|
|
2009
|
+
function stripLegacyForWrite(value) {
|
|
2010
|
+
const cleaned = {};
|
|
2011
|
+
for (const [key, member] of Object.entries(value)) {
|
|
2012
|
+
if (LEGACY_KEYS.has(key)) continue;
|
|
2013
|
+
if (key === "roles") {
|
|
2014
|
+
const roles = normalizeRoles(member);
|
|
2015
|
+
if (roles !== void 0) cleaned[key] = roles;
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
cleaned[key] = member;
|
|
2019
|
+
}
|
|
2020
|
+
return cleaned;
|
|
1763
2021
|
}
|
|
1764
2022
|
function normalizeRoles(value) {
|
|
1765
2023
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
1766
2024
|
const roles = {};
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
if (
|
|
1770
|
-
|
|
2025
|
+
const list = value["list"];
|
|
2026
|
+
if (Array.isArray(list)) roles["list"] = list.map((item) => {
|
|
2027
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) return item;
|
|
2028
|
+
const row = {};
|
|
2029
|
+
for (const [key, member] of Object.entries(item)) {
|
|
2030
|
+
if (key === "fallback") continue;
|
|
2031
|
+
if (key === "chain" && Array.isArray(member)) continue;
|
|
2032
|
+
row[key] = member;
|
|
2033
|
+
}
|
|
2034
|
+
return row;
|
|
2035
|
+
});
|
|
1771
2036
|
return roles;
|
|
1772
2037
|
}
|
|
1773
2038
|
function validateConfigPatch(patch) {
|
|
@@ -1780,17 +2045,28 @@ function validateConfigPatch(patch) {
|
|
|
1780
2045
|
for (const nestedKey of Object.keys(roles)) if (!Object.hasOwn(ROLES_KEYS, nestedKey)) throw new Error(`dsh-llm-fallbacks: unknown config key "roles.${nestedKey}"`);
|
|
1781
2046
|
}
|
|
1782
2047
|
}
|
|
1783
|
-
if (key === "
|
|
1784
|
-
const rows = patch[key];
|
|
1785
|
-
if (rows !== null && rows !== void 0) validateTimeSlotsPatch(rows);
|
|
1786
|
-
}
|
|
1787
|
-
if (key === "rootChain") {
|
|
1788
|
-
const chain = patch[key];
|
|
1789
|
-
if (chain !== null && chain !== void 0 && (!Array.isArray(chain) || !isAllDayConforming(chain))) throw new Error("dsh-llm-fallbacks: rootChain must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro)");
|
|
1790
|
-
}
|
|
2048
|
+
if (key === "chains") validateChainsPatch(patch[key]);
|
|
1791
2049
|
}
|
|
1792
2050
|
Config(patch);
|
|
1793
2051
|
}
|
|
2052
|
+
function validateChainsPatch(chains) {
|
|
2053
|
+
if (!Array.isArray(chains)) throw new Error("dsh-llm-fallbacks: chains must be an array of named chains");
|
|
2054
|
+
const seenIds = new Set();
|
|
2055
|
+
chains.forEach((chain, index) => {
|
|
2056
|
+
const at = `chains[${index}]`;
|
|
2057
|
+
if (chain === null || typeof chain !== "object" || Array.isArray(chain)) throw new Error(`dsh-llm-fallbacks: ${at} must be a plain object`);
|
|
2058
|
+
const record = chain;
|
|
2059
|
+
for (const nestedKey of Object.keys(record)) if (!Object.hasOwn(CHAIN_KEYS, nestedKey)) throw new Error(`dsh-llm-fallbacks: unknown config key "${at}.${nestedKey}"`);
|
|
2060
|
+
if (typeof record.id !== "string" || !CHAIN_ID_PATTERN.test(record.id)) throw new Error(`dsh-llm-fallbacks: ${at}.id must match /^[A-Za-z0-9][A-Za-z0-9-]{0,31}$/ (got ${JSON.stringify(record.id)})`);
|
|
2061
|
+
if (seenIds.has(record.id)) throw new Error(`dsh-llm-fallbacks: ${at} duplicates chain id "${record.id}" — chain ids must be unique (case-sensitive)`);
|
|
2062
|
+
seenIds.add(record.id);
|
|
2063
|
+
if (typeof record.label !== "string") throw new Error(`dsh-llm-fallbacks: ${at}.label must be a string`);
|
|
2064
|
+
const entries = record.entries;
|
|
2065
|
+
if (entries !== void 0 && entries !== null && (!Array.isArray(entries) || entries.some((entry) => typeof entry !== "string"))) throw new Error(`dsh-llm-fallbacks: ${at}.entries must be a string array`);
|
|
2066
|
+
if (Array.isArray(entries) && entries.length > 0 && !isAllDayConforming(entries)) throw new Error(`dsh-llm-fallbacks: ${at}.entries must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro)`);
|
|
2067
|
+
if (record.timeSlots !== void 0 && record.timeSlots !== null) validateTimeSlotsPatch(record.timeSlots);
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
1794
2070
|
function validateTimeSlotsPatch(rows) {
|
|
1795
2071
|
if (!Array.isArray(rows)) throw new Error("dsh-llm-fallbacks: timeSlots must be an array of slot rows");
|
|
1796
2072
|
const seenPresets = new Set();
|
|
@@ -1875,16 +2151,6 @@ function fallbacksTypertContribution() {
|
|
|
1875
2151
|
codec: { mode: "src-json" }
|
|
1876
2152
|
}],
|
|
1877
2153
|
result: { mode: "src-json" }
|
|
1878
|
-
},
|
|
1879
|
-
{
|
|
1880
|
-
id: "dsh-llm-fallbacks#fallbacks/sync-allowlist",
|
|
1881
|
-
service: "fallbacks",
|
|
1882
|
-
namespace: "fallbacks",
|
|
1883
|
-
method: "sync-allowlist",
|
|
1884
|
-
implementation: "syncAllowlist",
|
|
1885
|
-
invocation: { kind: "direct" },
|
|
1886
|
-
parameters: [],
|
|
1887
|
-
result: { mode: "src-json" }
|
|
1888
2154
|
}
|
|
1889
2155
|
]
|
|
1890
2156
|
};
|
|
@@ -1909,7 +2175,6 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
1909
2175
|
origin: "会话来源",
|
|
1910
2176
|
role: "角色",
|
|
1911
2177
|
chain: "链",
|
|
1912
|
-
inheritRoot: "(inherit-root)",
|
|
1913
2178
|
chainNone: "未配置",
|
|
1914
2179
|
slot: "分时",
|
|
1915
2180
|
switches: "最近降级切换",
|
|
@@ -1928,22 +2193,20 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
1928
2193
|
configEnabled: "已启用",
|
|
1929
2194
|
configDisabled: "未启用",
|
|
1930
2195
|
configTriggerCodes: "触发码",
|
|
1931
|
-
|
|
2196
|
+
configChains: "虚拟链",
|
|
1932
2197
|
configEmpty: "(空)",
|
|
1933
|
-
|
|
2198
|
+
configChainItem: "{id}({label}): {entries}|分时槽: {slots}",
|
|
1934
2199
|
configSlotPresetItem: "{preset}(chain: {n}, window {window})",
|
|
1935
2200
|
configSlotPresetBare: "{preset}(chain: {n})",
|
|
1936
2201
|
configSlotCustomItem: "custom {start}-{end}(chain: {n})",
|
|
1937
2202
|
configTz: "时区",
|
|
1938
2203
|
configRoles: "角色",
|
|
1939
|
-
configRoleItem: "{id}
|
|
1940
|
-
|
|
2204
|
+
configRoleItem: "{id}(指定降级链: {chain})",
|
|
2205
|
+
configRoleUnspecified: "未指定(沿用 dsh 默认)",
|
|
1941
2206
|
configCooldown: "冷却",
|
|
1942
2207
|
configRevert: "回主策略",
|
|
1943
|
-
configMaxSwitches: "
|
|
1944
|
-
configAlwaysCap: "always 上限",
|
|
2208
|
+
configMaxSwitches: "单次请求行走最大切换",
|
|
1945
2209
|
configPresets: "预置",
|
|
1946
|
-
configRoleAutoMatch: "角色自动匹配",
|
|
1947
2210
|
configEdit: "编辑:/settings(TUI 设置界面)或 ~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)/ $DSH_HOME/settings.yaml(fallbacks: 分节)",
|
|
1948
2211
|
configEditHint: "TUI 通过 /settings 修改配置;文件编辑仍然可用",
|
|
1949
2212
|
revertSeedOk: "角色 {id} 已还原为 Seed 默认",
|
|
@@ -1964,7 +2227,6 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
1964
2227
|
origin: "Session origin",
|
|
1965
2228
|
role: "Role",
|
|
1966
2229
|
chain: "Chain",
|
|
1967
|
-
inheritRoot: " (inherit-root)",
|
|
1968
2230
|
chainNone: "not configured",
|
|
1969
2231
|
slot: "Time slot",
|
|
1970
2232
|
switches: "Recent fallback switches",
|
|
@@ -1983,22 +2245,20 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
1983
2245
|
configEnabled: "enabled",
|
|
1984
2246
|
configDisabled: "disabled",
|
|
1985
2247
|
configTriggerCodes: "Trigger codes",
|
|
1986
|
-
|
|
2248
|
+
configChains: "Chains",
|
|
1987
2249
|
configEmpty: "(empty)",
|
|
1988
|
-
|
|
2250
|
+
configChainItem: "{id} ({label}): {entries} | slots: {slots}",
|
|
1989
2251
|
configSlotPresetItem: "{preset} (chain: {n}, window {window})",
|
|
1990
2252
|
configSlotPresetBare: "{preset} (chain: {n})",
|
|
1991
2253
|
configSlotCustomItem: "custom {start}-{end} (chain: {n})",
|
|
1992
2254
|
configTz: "TZ",
|
|
1993
2255
|
configRoles: "Roles",
|
|
1994
|
-
configRoleItem: "{id} (chain: {
|
|
1995
|
-
|
|
2256
|
+
configRoleItem: "{id} (specified chain: {chain})",
|
|
2257
|
+
configRoleUnspecified: "not specified (dsh default)",
|
|
1996
2258
|
configCooldown: "Cooldown",
|
|
1997
2259
|
configRevert: "Revert",
|
|
1998
|
-
configMaxSwitches: "Max switches
|
|
1999
|
-
configAlwaysCap: "Always-mode cap",
|
|
2260
|
+
configMaxSwitches: "Max switches per request walk",
|
|
2000
2261
|
configPresets: "Presets",
|
|
2001
|
-
configRoleAutoMatch: "Auto-match",
|
|
2002
2262
|
configEdit: "Edit: /settings (TUI settings screen) or ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) / $DSH_HOME/settings.yaml (fallbacks: section)",
|
|
2003
2263
|
configEditHint: "TUI edits config via /settings; file editing still works",
|
|
2004
2264
|
revertSeedOk: "role {id} reverted to its seed default",
|
|
@@ -2044,23 +2304,6 @@ function recentFallbacksSwitches(events, limit) {
|
|
|
2044
2304
|
}
|
|
2045
2305
|
return found;
|
|
2046
2306
|
}
|
|
2047
|
-
function resolveChainForDiagnostic(roles, rootChain, role, warn = console.warn) {
|
|
2048
|
-
if (role.trim() === "inherit") return {
|
|
2049
|
-
chainRole: false,
|
|
2050
|
-
chain: rootChain,
|
|
2051
|
-
inherit: rootChain.length > 0
|
|
2052
|
-
};
|
|
2053
|
-
const roleDef = roles.find((declared) => declared.id.trim() === role.trim());
|
|
2054
|
-
if (roleDef === void 0) warn(`llm-fallbacks: unknown role "${role}" — falling back to rootChain`);
|
|
2055
|
-
const roleChain = roleDef?.chain ?? [];
|
|
2056
|
-
const chain = roleChain.length > 0 ? roleChain : roleDef?.fallback === "none" ? [] : rootChain;
|
|
2057
|
-
const inherit = rootChain.length > 0 && (roleDef === void 0 || roleDef.fallback !== "none");
|
|
2058
|
-
return {
|
|
2059
|
-
chainRole: roleChain.length > 0,
|
|
2060
|
-
chain,
|
|
2061
|
-
inherit
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
2307
|
function formatSwitch(entry, t) {
|
|
2065
2308
|
const from = `${entry.from.provider}/${entry.from.model}`;
|
|
2066
2309
|
const to = `${entry.to.provider}/${entry.to.model}`;
|
|
@@ -2076,7 +2319,7 @@ function formatConfigList(items) {
|
|
|
2076
2319
|
return [...items.slice(0, 5), "…"].join(", ");
|
|
2077
2320
|
}
|
|
2078
2321
|
function formatConfigRoles(roles, t) {
|
|
2079
|
-
const items = roles.slice(0, 5).map((role) => t.configRoleItem.replace("{id}", role.id).replace("{
|
|
2322
|
+
const items = roles.slice(0, 5).map((role) => t.configRoleItem.replace("{id}", role.id).replace("{chain}", role.chain === void 0 || role.chain === "" ? t.configRoleUnspecified : role.chain));
|
|
2080
2323
|
const list = items.length === 0 ? "" : `${items.join(", ")}${roles.length > 5 ? ", …" : ""}`;
|
|
2081
2324
|
return `${roles.length}${list.length === 0 ? "" : ` — ${list}`}`;
|
|
2082
2325
|
}
|
|
@@ -2131,27 +2374,18 @@ function formatConfigTimeSlots(slots, t) {
|
|
|
2131
2374
|
const list = items.length === 0 ? "" : `${items.join(", ")}${slots.length > 5 ? ", …" : ""}`;
|
|
2132
2375
|
return `${slots.length}${list.length === 0 ? "" : ` — ${list}`}`;
|
|
2133
2376
|
}
|
|
2134
|
-
function formatConfigRules(rules, t) {
|
|
2135
|
-
const items = rules.slice(0, 5).map((rule) => `${rule.provider === "" ? "*" : rule.provider}/${rule.model === "" ? "*" : rule.model} → ${rule.role}`);
|
|
2136
|
-
const list = items.length === 0 ? "" : `${items.join(", ")}${rules.length > 5 ? ", …" : ""}`;
|
|
2137
|
-
return `${rules.length}${list.length === 0 ? "" : ` — ${list}`}`;
|
|
2138
|
-
}
|
|
2139
2377
|
function fallbacksConfigText(summary, locale = "zh") {
|
|
2140
2378
|
const t = FALLBACKS_COMMAND_LOCALES[locale];
|
|
2141
2379
|
return [
|
|
2142
2380
|
`${t.configTitle}: ${summary.active ? t.configEnabled : t.configDisabled}`,
|
|
2143
2381
|
`${t.configTriggerCodes}: ${summary.triggerCodes.length === 0 ? t.configEmpty : formatConfigList(summary.triggerCodes)}`,
|
|
2144
|
-
|
|
2145
|
-
`${t.configTimeSlots}: ${summary.timeSlots.length === 0 ? t.configEmpty : formatConfigTimeSlots(summary.timeSlots, t)}`,
|
|
2382
|
+
...summary.chains.map((chain) => t.configChainItem.replace("{id}", chain.id).replace("{label}", chain.label).replace("{entries}", chain.entries.length === 0 ? t.configEmpty : chain.entries.join(" → ")).replace("{slots}", chain.timeSlots.length === 0 ? t.configEmpty : formatConfigTimeSlots(chain.timeSlots, t))),
|
|
2146
2383
|
`${t.configTz}: ${summary.tz}`,
|
|
2147
2384
|
`${t.configRoles}: ${formatConfigRoles(summary.roles, t)}`,
|
|
2148
|
-
`${t.configRules}: ${summary.rules.length === 0 ? t.configEmpty : formatConfigRules(summary.rules, t)}`,
|
|
2149
2385
|
`${t.configCooldown}: ${summary.cooldownMs} ms`,
|
|
2150
2386
|
`${t.configRevert}: ${summary.revertPolicy}`,
|
|
2151
2387
|
`${t.configMaxSwitches}: ${summary.maxSwitchesPerStep}`,
|
|
2152
|
-
`${t.configAlwaysCap}: ${summary.alwaysModeRetryCap}`,
|
|
2153
2388
|
`${t.configPresets}: ${summary.presets}`,
|
|
2154
|
-
`${t.configRoleAutoMatch}: ${summary.roleAutoMatch ? t.configEnabled : t.configDisabled}`,
|
|
2155
2389
|
"",
|
|
2156
2390
|
t.configEdit,
|
|
2157
2391
|
t.configEditHint
|
|
@@ -2161,13 +2395,13 @@ function fallbacksCommandText(snapshot, locale = "zh") {
|
|
|
2161
2395
|
const t = FALLBACKS_COMMAND_LOCALES[locale];
|
|
2162
2396
|
const lines = [t.title];
|
|
2163
2397
|
lines.push(`${t.origin}: ${snapshot.origin}`);
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2398
|
+
const roleChain = snapshot.roleChain === void 0 || snapshot.roleChain === "" ? t.configRoleUnspecified : snapshot.roleChain;
|
|
2399
|
+
lines.push(`${t.role}: ${snapshot.role === "" ? t.configRoleUnspecified : snapshot.role}(${roleChain})`);
|
|
2400
|
+
if (snapshot.chains.length === 0) lines.push(`${t.chain}: ${t.chainNone}`);
|
|
2401
|
+
else for (const chain of snapshot.chains) {
|
|
2402
|
+
const marker = chain.selected ? " ●" : "";
|
|
2403
|
+
lines.push(`${t.chain}${marker}: ${chain.id}(${chain.label}): ${chain.entries.length === 0 ? t.chainNone : chain.entries.join(" → ")}|${t.slot}: ${chain.slotLabel}`);
|
|
2169
2404
|
}
|
|
2170
|
-
lines.push(`${t.slot}: ${snapshot.slot.label}`);
|
|
2171
2405
|
if (snapshot.switches.length === 0) lines.push(`${t.switches}: ${t.switchesNone}`);
|
|
2172
2406
|
else {
|
|
2173
2407
|
lines.push(`${t.switches} (${snapshot.switches.length}):`);
|
|
@@ -2262,9 +2496,7 @@ var FallbacksSeedManager = class {
|
|
|
2262
2496
|
});
|
|
2263
2497
|
outcome.applied.push(seed.id);
|
|
2264
2498
|
}
|
|
2265
|
-
const
|
|
2266
|
-
const currentList = roleRows(config);
|
|
2267
|
-
const currentRules = roleRules(config);
|
|
2499
|
+
const currentList = roleRows(io.read());
|
|
2268
2500
|
const candidate = new Map(this.registry);
|
|
2269
2501
|
if (entries.size === 0) candidate.delete(source);
|
|
2270
2502
|
else candidate.set(source, {
|
|
@@ -2281,10 +2513,7 @@ var FallbacksSeedManager = class {
|
|
|
2281
2513
|
}
|
|
2282
2514
|
const newList = materialize(currentList, incoming, prior, outcome.conflicts);
|
|
2283
2515
|
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)`);
|
|
2284
|
-
const computed = {
|
|
2285
|
-
list: newList,
|
|
2286
|
-
rules: currentRules
|
|
2287
|
-
};
|
|
2516
|
+
const computed = { list: newList };
|
|
2288
2517
|
if (!deepEqual(newList, currentList)) await io.writeRoles(computed);
|
|
2289
2518
|
if (entries.size === 0) this.registry.delete(source);
|
|
2290
2519
|
else this.registry.set(source, {
|
|
@@ -2328,8 +2557,7 @@ var FallbacksSeedManager = class {
|
|
|
2328
2557
|
source: seeded ? declared.source : "user"
|
|
2329
2558
|
};
|
|
2330
2559
|
if (seeded) effective.seedPersona = declared.persona;
|
|
2331
|
-
if (row.chain !== void 0) effective.chain = row.chain;
|
|
2332
|
-
if (row.fallback !== void 0) effective.fallback = row.fallback;
|
|
2560
|
+
if (row.chain !== void 0 && row.chain !== "") effective.chain = row.chain;
|
|
2333
2561
|
return effective;
|
|
2334
2562
|
}) };
|
|
2335
2563
|
}
|
|
@@ -2353,9 +2581,7 @@ var FallbacksSeedManager = class {
|
|
|
2353
2581
|
reverted: false,
|
|
2354
2582
|
reason: "not-seeded"
|
|
2355
2583
|
};
|
|
2356
|
-
const
|
|
2357
|
-
const rows = roleRows(config);
|
|
2358
|
-
const rules = roleRules(config);
|
|
2584
|
+
const rows = roleRows(io.read());
|
|
2359
2585
|
const index = rows.findIndex((row) => row.id.trim() === seedId);
|
|
2360
2586
|
if (index === -1) return {
|
|
2361
2587
|
reverted: false,
|
|
@@ -2369,10 +2595,7 @@ var FallbacksSeedManager = class {
|
|
|
2369
2595
|
...row,
|
|
2370
2596
|
persona: declared.persona
|
|
2371
2597
|
} : row);
|
|
2372
|
-
await io.writeRoles({
|
|
2373
|
-
list: nextList,
|
|
2374
|
-
rules
|
|
2375
|
-
});
|
|
2598
|
+
await io.writeRoles({ list: nextList });
|
|
2376
2599
|
return {
|
|
2377
2600
|
reverted: true,
|
|
2378
2601
|
persona: declared.persona
|
|
@@ -2441,10 +2664,6 @@ function roleRows(config) {
|
|
|
2441
2664
|
const list = config.roles?.list;
|
|
2442
2665
|
return Array.isArray(list) ? list : [];
|
|
2443
2666
|
}
|
|
2444
|
-
function roleRules(config) {
|
|
2445
|
-
const rules = config.roles?.rules;
|
|
2446
|
-
return Array.isArray(rules) ? rules : [];
|
|
2447
|
-
}
|
|
2448
2667
|
function deepEqual(a, b) {
|
|
2449
2668
|
if (a === b) return true;
|
|
2450
2669
|
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
@@ -2508,7 +2727,7 @@ function fallbacksChildren(canonicalPath) {
|
|
|
2508
2727
|
if (canonicalPath.length === 2 && canonicalPath[1] === "config") return [REVERT_SEED_NODE];
|
|
2509
2728
|
return [];
|
|
2510
2729
|
}
|
|
2511
|
-
const FALLBACKS_PROVIDER
|
|
2730
|
+
const FALLBACKS_PROVIDER = {
|
|
2512
2731
|
root: FALLBACKS_TUI_ROOT,
|
|
2513
2732
|
descriptions: {
|
|
2514
2733
|
zh: FALLBACKS_COMMAND_LOCALES.zh.description,
|
|
@@ -2522,7 +2741,7 @@ function installTuiClient(ctx, opts) {
|
|
|
2522
2741
|
const trees = tctx.tuiCommandTrees;
|
|
2523
2742
|
if (trees === void 0) return;
|
|
2524
2743
|
try {
|
|
2525
|
-
return trees.register(FALLBACKS_PROVIDER
|
|
2744
|
+
return trees.register(FALLBACKS_PROVIDER);
|
|
2526
2745
|
} catch (error) {
|
|
2527
2746
|
if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
|
|
2528
2747
|
tctx.logger("llm-fallbacks").debug("llm-fallbacks: tui command tree already registered — no provider on this fiber");
|
|
@@ -2593,8 +2812,8 @@ function buildFallbacksTuiSection() {
|
|
|
2593
2812
|
ns: FALLBACKS_TUI_SECTION_NS,
|
|
2594
2813
|
title: "fallbacks",
|
|
2595
2814
|
descriptions: {
|
|
2596
|
-
zh: "
|
|
2597
|
-
en: "Fallback settings:
|
|
2815
|
+
zh: "回退设置:虚拟链(默认降级链 + 链尾 + 分时槽)、角色与高级选项(与 Web 设置卡片完全一致)。主代理需选中 FallbacksChain/<链 id> 才会执行降级链;子代理沿用派发代理的链。",
|
|
2816
|
+
en: "Fallback settings: named chains (default degradation chain + tail + time slots), roles, and advanced options (full parity with the web settings card). The main agent must select FallbacksChain/<chain id> to walk a chain; subagents inherit the delegating agent's chain."
|
|
2598
2817
|
},
|
|
2599
2818
|
groups: [
|
|
2600
2819
|
{
|
|
@@ -2631,16 +2850,6 @@ function buildFallbacksTuiSection() {
|
|
|
2631
2850
|
}
|
|
2632
2851
|
],
|
|
2633
2852
|
fields: [
|
|
2634
|
-
{
|
|
2635
|
-
path: ["roleAutoMatch"],
|
|
2636
|
-
label: "LLM role auto-match",
|
|
2637
|
-
descriptions: {
|
|
2638
|
-
zh: "LLM 角色自动匹配",
|
|
2639
|
-
en: "LLM role auto-match"
|
|
2640
|
-
},
|
|
2641
|
-
group: "general",
|
|
2642
|
-
kind: "boolean"
|
|
2643
|
-
},
|
|
2644
2853
|
{
|
|
2645
2854
|
path: ["presets"],
|
|
2646
2855
|
label: "Preset roles",
|
|
@@ -2700,63 +2909,34 @@ function buildFallbacksTuiSection() {
|
|
|
2700
2909
|
parse: tzParse
|
|
2701
2910
|
},
|
|
2702
2911
|
{
|
|
2703
|
-
path: ["
|
|
2704
|
-
label: "
|
|
2912
|
+
path: ["chains"],
|
|
2913
|
+
label: "Chains (JSON)",
|
|
2705
2914
|
descriptions: {
|
|
2706
|
-
zh: "
|
|
2707
|
-
en: "
|
|
2915
|
+
zh: "虚拟链(JSON)",
|
|
2916
|
+
en: "Chains (JSON)"
|
|
2708
2917
|
},
|
|
2709
|
-
hint: "JSON array of
|
|
2918
|
+
hint: "JSON array of named chains ({ id, label, entries, timeSlots }); each entries array must end with an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro).",
|
|
2710
2919
|
hintDescriptions: {
|
|
2711
|
-
zh: "
|
|
2712
|
-
en: "JSON array of
|
|
2920
|
+
zh: "命名虚拟链 JSON 数组({ id, label, entries, timeSlots });每条 entries 末项必须是官方模型(deepseek-official/deepseek-flash 或 deepseek-official/deepseek-v4-pro)。",
|
|
2921
|
+
en: "JSON array of named chains ({ id, label, entries, timeSlots }); each entries array must end with an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-v4-pro)."
|
|
2713
2922
|
},
|
|
2714
2923
|
group: "chain",
|
|
2715
2924
|
kind: "text",
|
|
2716
2925
|
format: jsonFieldFormat,
|
|
2717
|
-
parse: jsonFieldParse((parsed) => ({
|
|
2718
|
-
},
|
|
2719
|
-
{
|
|
2720
|
-
path: ["timeSlots"],
|
|
2721
|
-
label: "Time slots (JSON)",
|
|
2722
|
-
descriptions: {
|
|
2723
|
-
zh: "分时切换(JSON)",
|
|
2724
|
-
en: "Time slots (JSON)"
|
|
2725
|
-
},
|
|
2726
|
-
hint: "JSON array of slot rows (kind: preset|custom).",
|
|
2727
|
-
hintDescriptions: {
|
|
2728
|
-
zh: "分时行 JSON 数组(kind: preset|custom)。",
|
|
2729
|
-
en: "JSON array of slot rows (kind: preset|custom)."
|
|
2730
|
-
},
|
|
2731
|
-
group: "chain",
|
|
2732
|
-
kind: "text",
|
|
2733
|
-
format: jsonFieldFormat,
|
|
2734
|
-
parse: jsonFieldParse((parsed) => ({ timeSlots: parsed }))
|
|
2926
|
+
parse: jsonFieldParse((parsed) => ({ chains: parsed }))
|
|
2735
2927
|
},
|
|
2736
2928
|
{
|
|
2737
2929
|
path: ["roles", "list"],
|
|
2738
2930
|
label: "Roles (JSON)",
|
|
2739
2931
|
descriptions: {
|
|
2740
|
-
zh: "角色列表(JSON
|
|
2741
|
-
en: "Roles (JSON)"
|
|
2932
|
+
zh: "角色列表(JSON,chain 为指定降级链指针)",
|
|
2933
|
+
en: "Roles (JSON; chain is the specified-chain pointer)"
|
|
2742
2934
|
},
|
|
2743
2935
|
group: "roles",
|
|
2744
2936
|
kind: "text",
|
|
2745
2937
|
format: jsonFieldFormat,
|
|
2746
2938
|
parse: jsonFieldParse((parsed) => ({ roles: { list: parsed } }))
|
|
2747
2939
|
},
|
|
2748
|
-
{
|
|
2749
|
-
path: ["roles", "rules"],
|
|
2750
|
-
label: "Role rules (JSON)",
|
|
2751
|
-
descriptions: {
|
|
2752
|
-
zh: "角色规则(JSON)",
|
|
2753
|
-
en: "Role rules (JSON)"
|
|
2754
|
-
},
|
|
2755
|
-
group: "roles",
|
|
2756
|
-
kind: "text",
|
|
2757
|
-
format: jsonFieldFormat,
|
|
2758
|
-
parse: jsonFieldParse((parsed) => ({ roles: { rules: parsed } }))
|
|
2759
|
-
},
|
|
2760
2940
|
{
|
|
2761
2941
|
path: ["cooldownMs"],
|
|
2762
2942
|
label: "Cooldown (ms)",
|
|
@@ -2769,20 +2949,10 @@ function buildFallbacksTuiSection() {
|
|
|
2769
2949
|
},
|
|
2770
2950
|
{
|
|
2771
2951
|
path: ["maxSwitchesPerStep"],
|
|
2772
|
-
label: "Max switches per
|
|
2773
|
-
descriptions: {
|
|
2774
|
-
zh: "单步最大切换次数",
|
|
2775
|
-
en: "Max switches per step"
|
|
2776
|
-
},
|
|
2777
|
-
group: "advanced",
|
|
2778
|
-
kind: "number"
|
|
2779
|
-
},
|
|
2780
|
-
{
|
|
2781
|
-
path: ["alwaysModeRetryCap"],
|
|
2782
|
-
label: "Always-mode retry cap",
|
|
2952
|
+
label: "Max switches per request walk",
|
|
2783
2953
|
descriptions: {
|
|
2784
|
-
zh: "
|
|
2785
|
-
en: "
|
|
2954
|
+
zh: "单次请求行走最大切换",
|
|
2955
|
+
en: "Max switches per request walk"
|
|
2786
2956
|
},
|
|
2787
2957
|
group: "advanced",
|
|
2788
2958
|
kind: "number"
|
|
@@ -2830,343 +3000,14 @@ function installTuiSettingsSection(ctx, opts) {
|
|
|
2830
3000
|
});
|
|
2831
3001
|
}
|
|
2832
3002
|
//#endregion
|
|
2833
|
-
//#region .build/host/virtual-adapter.js
|
|
2834
|
-
const FALLBACKS_PROVIDER = "FallbacksChain";
|
|
2835
|
-
const FALLBACKS_CHAIN_MODEL = "Auto";
|
|
2836
|
-
const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
|
|
2837
|
-
const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
|
|
2838
|
-
const LLM_UNAVAILABLE_CODE = "LLM_UNAVAILABLE";
|
|
2839
|
-
function firstDispatchableExactHead(chain) {
|
|
2840
|
-
for (const entry of chain) {
|
|
2841
|
-
let selector;
|
|
2842
|
-
try {
|
|
2843
|
-
selector = parseSelector(entry);
|
|
2844
|
-
} catch {
|
|
2845
|
-
continue;
|
|
2846
|
-
}
|
|
2847
|
-
if (selector.model === void 0) continue;
|
|
2848
|
-
if (selector.provider === "FallbacksChain") continue;
|
|
2849
|
-
return {
|
|
2850
|
-
provider: selector.provider,
|
|
2851
|
-
model: selector.model
|
|
2852
|
-
};
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
function effectiveHeadOf(config, now) {
|
|
2856
|
-
if (!isAllDayConforming(config.rootChain)) return void 0;
|
|
2857
|
-
return firstDispatchableExactHead(resolveEffectiveChain(config, now, config.tz ?? "Asia/Shanghai"));
|
|
2858
|
-
}
|
|
2859
|
-
function pickerDisplayName(config, now = new Date(), modelDisplayName) {
|
|
2860
|
-
const head = effectiveHeadOf(config, now);
|
|
2861
|
-
if (head === void 0) return FALLBACKS_CHAIN_MODEL;
|
|
2862
|
-
const slot = resolveSlotState(config, now, config.tz ?? "Asia/Shanghai");
|
|
2863
|
-
const model = modelDisplayName !== void 0 && modelDisplayName !== "" ? modelDisplayName : head.model;
|
|
2864
|
-
return `${FALLBACKS_CHAIN_MODEL}: ${model}[${slot.label}]`;
|
|
2865
|
-
}
|
|
2866
|
-
async function resolveHeadDisplayName(llm, head) {
|
|
2867
|
-
if (llm === void 0) return head.model;
|
|
2868
|
-
try {
|
|
2869
|
-
const row = (await llm.listModels(head.provider)).find((model) => model.id === head.model);
|
|
2870
|
-
if (row !== void 0 && row.name !== "") return row.name;
|
|
2871
|
-
} catch {}
|
|
2872
|
-
try {
|
|
2873
|
-
const info = await llm.resolveModelInfo(head.provider, head.model);
|
|
2874
|
-
if (info.name !== "") return info.name;
|
|
2875
|
-
} catch {}
|
|
2876
|
-
return head.model;
|
|
2877
|
-
}
|
|
2878
|
-
function envelopeProvenance(replayState) {
|
|
2879
|
-
if (typeof replayState !== "object" || replayState === null) return void 0;
|
|
2880
|
-
const response = replayState.response;
|
|
2881
|
-
if (typeof response !== "object" || response === null) return void 0;
|
|
2882
|
-
const { provider, model } = response;
|
|
2883
|
-
if (typeof provider !== "string" || provider === "" || provider === "FallbacksChain") return void 0;
|
|
2884
|
-
if (typeof model !== "string" || model === "") return void 0;
|
|
2885
|
-
return {
|
|
2886
|
-
provider,
|
|
2887
|
-
model
|
|
2888
|
-
};
|
|
2889
|
-
}
|
|
2890
|
-
function restoreEnvelopeProvenance(messages) {
|
|
2891
|
-
let changed = false;
|
|
2892
|
-
const restored = messages.map((message) => {
|
|
2893
|
-
const source = message.source;
|
|
2894
|
-
if (message.role !== "assistant" || source?.kind !== "model") return message;
|
|
2895
|
-
if (source.provider !== "FallbacksChain" || source.model !== "Auto") return message;
|
|
2896
|
-
const provenance = envelopeProvenance(source.replayState);
|
|
2897
|
-
if (provenance === void 0) return message;
|
|
2898
|
-
changed = true;
|
|
2899
|
-
return {
|
|
2900
|
-
...message,
|
|
2901
|
-
source: {
|
|
2902
|
-
...source,
|
|
2903
|
-
...provenance
|
|
2904
|
-
}
|
|
2905
|
-
};
|
|
2906
|
-
});
|
|
2907
|
-
return changed ? restored : messages;
|
|
2908
|
-
}
|
|
2909
|
-
var FallbacksChainAdapter = class extends LlmAdapter {
|
|
2910
|
-
readConfig;
|
|
2911
|
-
getLlm;
|
|
2912
|
-
constructor(readConfig, getLlm) {
|
|
2913
|
-
super();
|
|
2914
|
-
this.readConfig = readConfig;
|
|
2915
|
-
this.getLlm = getLlm;
|
|
2916
|
-
}
|
|
2917
|
-
providerInfo(provider) {
|
|
2918
|
-
return {
|
|
2919
|
-
id: provider,
|
|
2920
|
-
name: FALLBACKS_PROVIDER
|
|
2921
|
-
};
|
|
2922
|
-
}
|
|
2923
|
-
async listModels(provider) {
|
|
2924
|
-
const config = this.readConfig();
|
|
2925
|
-
const now = new Date();
|
|
2926
|
-
const head = effectiveHeadOf(config, now);
|
|
2927
|
-
const display = head === void 0 ? void 0 : await resolveHeadDisplayName(this.getLlm(), head);
|
|
2928
|
-
return [{
|
|
2929
|
-
provider,
|
|
2930
|
-
id: FALLBACKS_CHAIN_MODEL,
|
|
2931
|
-
name: pickerDisplayName(config, now, display)
|
|
2932
|
-
}];
|
|
2933
|
-
}
|
|
2934
|
-
async resolveModel(provider, model, signal) {
|
|
2935
|
-
const head = effectiveHeadOf(this.readConfig(), new Date());
|
|
2936
|
-
const llm = this.getLlm();
|
|
2937
|
-
if (head !== void 0 && llm !== void 0) try {
|
|
2938
|
-
const info = await llm.resolveModelInfo(head.provider, head.model, signal);
|
|
2939
|
-
return {
|
|
2940
|
-
provider,
|
|
2941
|
-
id: model,
|
|
2942
|
-
name: info.name,
|
|
2943
|
-
...info.description === void 0 ? {} : { description: info.description },
|
|
2944
|
-
...info.inputModalities === void 0 ? {} : { inputModalities: info.inputModalities },
|
|
2945
|
-
...info.context === void 0 ? {} : { context: info.context },
|
|
2946
|
-
...info.defaultMaxTokens === void 0 ? {} : { defaultMaxTokens: info.defaultMaxTokens },
|
|
2947
|
-
...info.reasoning === void 0 ? {} : { reasoning: info.reasoning }
|
|
2948
|
-
};
|
|
2949
|
-
} catch {}
|
|
2950
|
-
return {
|
|
2951
|
-
provider,
|
|
2952
|
-
id: model,
|
|
2953
|
-
name: model
|
|
2954
|
-
};
|
|
2955
|
-
}
|
|
2956
|
-
providerRetryPolicy(_provider) {
|
|
2957
|
-
try {
|
|
2958
|
-
const head = effectiveHeadOf(this.readConfig(), new Date());
|
|
2959
|
-
if (head === void 0 || head.provider === "FallbacksChain") return void 0;
|
|
2960
|
-
return this.getLlm()?.providerRetryPolicy(head.provider);
|
|
2961
|
-
} catch {
|
|
2962
|
-
return;
|
|
2963
|
-
}
|
|
2964
|
-
}
|
|
2965
|
-
imageRequestPricing(_provider, _model) {
|
|
2966
|
-
try {
|
|
2967
|
-
const head = effectiveHeadOf(this.readConfig(), new Date());
|
|
2968
|
-
if (head === void 0 || head.provider === "FallbacksChain") return void 0;
|
|
2969
|
-
return this.getLlm()?.imageRequestPricing(head.provider, head.model);
|
|
2970
|
-
} catch {
|
|
2971
|
-
return;
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
stream(options) {
|
|
2975
|
-
const config = this.readConfig();
|
|
2976
|
-
const now = new Date();
|
|
2977
|
-
if (resolveEffectiveChain(config, now, config.tz ?? "Asia/Shanghai").length === 0) throw new LlmError("llm-fallbacks: the effective chain is empty — the virtual FallbacksChain route has no head to delegate to", EMPTY_EFFECTIVE_CHAIN_CODE);
|
|
2978
|
-
const head = effectiveHeadOf(config, now);
|
|
2979
|
-
if (head === void 0) throw new LlmError("llm-fallbacks: the effective chain head cannot be dispatched (non-conforming all-day, wildcard, malformed, or self-route) — refusing to delegate", UNDISPATCHABLE_HEAD_CODE);
|
|
2980
|
-
const llm = this.getLlm();
|
|
2981
|
-
if (llm === void 0) throw new LlmError("llm-fallbacks: the llm runtime is unavailable — cannot delegate the FallbacksChain call", LLM_UNAVAILABLE_CODE);
|
|
2982
|
-
return llm.stream({
|
|
2983
|
-
...options,
|
|
2984
|
-
provider: head.provider,
|
|
2985
|
-
model: head.model,
|
|
2986
|
-
messages: restoreEnvelopeProvenance(options.messages)
|
|
2987
|
-
});
|
|
2988
|
-
}
|
|
2989
|
-
};
|
|
2990
|
-
function installFallbacksAdapter(ctx, readConfig) {
|
|
2991
|
-
let llm;
|
|
2992
|
-
let disposeAdapter;
|
|
2993
|
-
let registered = false;
|
|
2994
|
-
const logger = ctx.logger("llm-fallbacks");
|
|
2995
|
-
const adapter = new FallbacksChainAdapter(readConfig, () => llm);
|
|
2996
|
-
const reconcile = () => {
|
|
2997
|
-
const shouldRegister = isFallbackActive(readConfig());
|
|
2998
|
-
if (shouldRegister && !registered) {
|
|
2999
|
-
if (llm === void 0) return;
|
|
3000
|
-
try {
|
|
3001
|
-
disposeAdapter = llm.registerAdapter([FALLBACKS_PROVIDER], adapter);
|
|
3002
|
-
registered = true;
|
|
3003
|
-
logger.info("llm-fallbacks: virtual adapter registered — FallbacksChain/Auto is selectable in the model picker");
|
|
3004
|
-
} catch (error) {
|
|
3005
|
-
if (!(error instanceof Error) || !("code" in error) || error.code !== "DUPLICATE_ADAPTER") throw error;
|
|
3006
|
-
logger.debug("llm-fallbacks: virtual adapter already registered — no route on this fiber (multi-fiber dedupe)");
|
|
3007
|
-
}
|
|
3008
|
-
} else if (!shouldRegister && registered) {
|
|
3009
|
-
disposeAdapter?.();
|
|
3010
|
-
disposeAdapter = void 0;
|
|
3011
|
-
registered = false;
|
|
3012
|
-
logger.info("llm-fallbacks: virtual adapter unregistered — FallbacksChain/Auto hidden from the model picker");
|
|
3013
|
-
}
|
|
3014
|
-
};
|
|
3015
|
-
ctx.inject(["llm"], (llmCtx) => {
|
|
3016
|
-
llm = llmCtx.llm;
|
|
3017
|
-
reconcile();
|
|
3018
|
-
return () => {
|
|
3019
|
-
disposeAdapter?.();
|
|
3020
|
-
disposeAdapter = void 0;
|
|
3021
|
-
registered = false;
|
|
3022
|
-
llm = void 0;
|
|
3023
|
-
};
|
|
3024
|
-
});
|
|
3025
|
-
return reconcile;
|
|
3026
|
-
}
|
|
3027
|
-
//#endregion
|
|
3028
3003
|
//#region .build/host/index.js
|
|
3029
3004
|
const name = "llm-fallbacks";
|
|
3030
3005
|
const provide = ["llm-fallbacks"];
|
|
3031
3006
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
3032
|
-
const CONTEXT_WINDOW_EXCEEDED_CODE = "CONTEXT_WINDOW_EXCEEDED";
|
|
3033
|
-
const REQUEST_SCOPED_CODES = new Set([CONTEXT_WINDOW_EXCEEDED_CODE]);
|
|
3034
3007
|
const stateStores = new WeakMap();
|
|
3035
3008
|
function stateStore(ctx) {
|
|
3036
3009
|
return stateStores.get(ctx);
|
|
3037
3010
|
}
|
|
3038
|
-
const blockedAttemptStores = new WeakMap();
|
|
3039
|
-
function blockedAttempts(ctx) {
|
|
3040
|
-
return blockedAttemptStores.get(ctx);
|
|
3041
|
-
}
|
|
3042
|
-
const chainHeadStores = new WeakMap();
|
|
3043
|
-
function chainHeads(ctx) {
|
|
3044
|
-
return chainHeadStores.get(ctx);
|
|
3045
|
-
}
|
|
3046
|
-
function latestByAt(map) {
|
|
3047
|
-
let latest;
|
|
3048
|
-
for (const value of map.values()) if (latest === void 0 || value.at >= latest.at) latest = value;
|
|
3049
|
-
return latest;
|
|
3050
|
-
}
|
|
3051
|
-
async function makeModelExists(ctx, providers) {
|
|
3052
|
-
const llm = ctx.get("llm");
|
|
3053
|
-
if (llm === void 0 || typeof llm.listModels !== "function") return () => true;
|
|
3054
|
-
const catalog = new Map();
|
|
3055
|
-
await Promise.all(providers.map(async (provider) => {
|
|
3056
|
-
try {
|
|
3057
|
-
const models = await llm.listModels(provider);
|
|
3058
|
-
catalog.set(provider, new Set(models.map((model) => model.id)));
|
|
3059
|
-
} catch {
|
|
3060
|
-
catalog.set(provider, new Set());
|
|
3061
|
-
}
|
|
3062
|
-
}));
|
|
3063
|
-
return (provider, model) => catalog.get(provider)?.has(model) ?? false;
|
|
3064
|
-
}
|
|
3065
|
-
async function makeContextWindowOf(ctx, routes) {
|
|
3066
|
-
const llm = ctx.get("llm");
|
|
3067
|
-
if (llm === void 0) return () => void 0;
|
|
3068
|
-
const windows = new Map();
|
|
3069
|
-
if (typeof llm.listModels === "function") await Promise.all([...new Set(routes.map((route) => route.provider))].map(async (provider) => {
|
|
3070
|
-
try {
|
|
3071
|
-
for (const model of await llm.listModels(provider)) if (typeof model.contextWindow === "number") windows.set(selectorKey(provider, model.id), model.contextWindow);
|
|
3072
|
-
} catch {}
|
|
3073
|
-
}));
|
|
3074
|
-
const resolveModelInfo = llm.resolveModelInfo?.bind(llm);
|
|
3075
|
-
if (resolveModelInfo !== void 0) await Promise.all(routes.map(async (route) => {
|
|
3076
|
-
const key = selectorKey(route.provider, route.model);
|
|
3077
|
-
if (windows.has(key)) return;
|
|
3078
|
-
try {
|
|
3079
|
-
const window = (await resolveModelInfo(route.provider, route.model))?.context?.contextWindow;
|
|
3080
|
-
if (typeof window === "number") windows.set(key, window);
|
|
3081
|
-
} catch {}
|
|
3082
|
-
}));
|
|
3083
|
-
return (provider, model) => windows.get(selectorKey(provider, model));
|
|
3084
|
-
}
|
|
3085
|
-
function countRetryEvents(session, turn, step, provider) {
|
|
3086
|
-
let count = 0;
|
|
3087
|
-
const events = session.snapshotEvents();
|
|
3088
|
-
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
3089
|
-
const event = events[index];
|
|
3090
|
-
const data = event.data;
|
|
3091
|
-
if (typeof data.turn === "number" && typeof data.step === "number" && (data.turn < turn || data.turn === turn && data.step < step)) break;
|
|
3092
|
-
if (event.type !== "llm/retry") continue;
|
|
3093
|
-
if (data.turn === turn && data.step === step && event.data.provider === provider && event.data.mode === "always") count += 1;
|
|
3094
|
-
}
|
|
3095
|
-
return count;
|
|
3096
|
-
}
|
|
3097
|
-
function anchorServedRoute(config, route, now) {
|
|
3098
|
-
if (route.provider !== "FallbacksChain" || route.model !== "Auto") return route;
|
|
3099
|
-
const head = effectiveHeadOf(config, now);
|
|
3100
|
-
return head === void 0 ? route : {
|
|
3101
|
-
provider: head.provider,
|
|
3102
|
-
model: head.model
|
|
3103
|
-
};
|
|
3104
|
-
}
|
|
3105
|
-
function servedRouteFor(agent, config) {
|
|
3106
|
-
const provider = agent.options?.provider;
|
|
3107
|
-
const model = agent.options?.model;
|
|
3108
|
-
if (provider === void 0 || model === void 0) return void 0;
|
|
3109
|
-
const anchored = anchorServedRoute(config, {
|
|
3110
|
-
provider,
|
|
3111
|
-
model
|
|
3112
|
-
}, new Date());
|
|
3113
|
-
return anchored.provider === provider && anchored.model === model ? void 0 : anchored;
|
|
3114
|
-
}
|
|
3115
|
-
function currentModel(agent, provider, config, now) {
|
|
3116
|
-
return anchorServedRoute(config, {
|
|
3117
|
-
provider,
|
|
3118
|
-
model: agent.session.requestHeader()?.config.model ?? agent.options.model ?? ""
|
|
3119
|
-
}, now);
|
|
3120
|
-
}
|
|
3121
|
-
function slotWinnerKey(winner) {
|
|
3122
|
-
if (winner === "all-day") return "all-day";
|
|
3123
|
-
return winner.kind === "preset" ? `preset:${winner.preset}` : `custom:${winner.start}-${winner.end}`;
|
|
3124
|
-
}
|
|
3125
|
-
function overrideConfig(seed, to, explicitEffort) {
|
|
3126
|
-
return overrideConfigWithRouteRule(seed, to, explicitEffort);
|
|
3127
|
-
}
|
|
3128
|
-
function readSubagentSettings(ctx) {
|
|
3129
|
-
const service = ctx.get("subagentModelSelection");
|
|
3130
|
-
return service === void 0 ? void 0 : service.current();
|
|
3131
|
-
}
|
|
3132
|
-
function readGuardedPolicySettings(ctx, agentId, lastKnown, warn) {
|
|
3133
|
-
try {
|
|
3134
|
-
const service = ctx.get("subagentModelSelection");
|
|
3135
|
-
if (service === void 0) return {
|
|
3136
|
-
ok: true,
|
|
3137
|
-
settings: lastKnown.get(agentId)
|
|
3138
|
-
};
|
|
3139
|
-
const settings = service.current();
|
|
3140
|
-
lastKnown.set(agentId, settings);
|
|
3141
|
-
return {
|
|
3142
|
-
ok: true,
|
|
3143
|
-
settings
|
|
3144
|
-
};
|
|
3145
|
-
} catch (error) {
|
|
3146
|
-
warn("llm-fallbacks: agent \"%s\" subagent model-selection settings read failed — %s", agentId, error?.message ?? String(error));
|
|
3147
|
-
const known = lastKnown.get(agentId);
|
|
3148
|
-
return known === void 0 ? { ok: false } : {
|
|
3149
|
-
ok: true,
|
|
3150
|
-
settings: known
|
|
3151
|
-
};
|
|
3152
|
-
}
|
|
3153
|
-
}
|
|
3154
|
-
function authorizedRouteView(ctx, agent) {
|
|
3155
|
-
const header = agent.session?.header;
|
|
3156
|
-
const parentId = header?.origin === "subagent" ? header.parentSession : void 0;
|
|
3157
|
-
const parent = parentId === void 0 ? void 0 : ctx.get("agents")?.get(parentId);
|
|
3158
|
-
const parentHeader = parent?.session.requestHeader()?.config;
|
|
3159
|
-
const parentOptions = parent?.options;
|
|
3160
|
-
return {
|
|
3161
|
-
requestHeader: agent.session?.requestHeader()?.config,
|
|
3162
|
-
events: agent.session?.snapshotEvents(),
|
|
3163
|
-
options: agent.options,
|
|
3164
|
-
inherited: parentHeader ?? (typeof parentOptions?.provider === "string" && typeof parentOptions?.model === "string" ? {
|
|
3165
|
-
provider: parentOptions.provider,
|
|
3166
|
-
model: parentOptions.model
|
|
3167
|
-
} : void 0)
|
|
3168
|
-
};
|
|
3169
|
-
}
|
|
3170
3011
|
function isLiveConfigRef(value) {
|
|
3171
3012
|
return typeof value === "object" && value !== null && typeof value.get === "function";
|
|
3172
3013
|
}
|
|
@@ -3194,12 +3035,11 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3194
3035
|
throw new Error(seedsSettingsUnavailable);
|
|
3195
3036
|
};
|
|
3196
3037
|
ctx.inject(["settings"], (sctx) => {
|
|
3197
|
-
writeRoles = (roles) => sctx.settings
|
|
3038
|
+
writeRoles = (roles) => writeFallbacksSection(sctx.settings, { roles });
|
|
3198
3039
|
try {
|
|
3199
3040
|
sctx.provide("llm-fallbacks", {
|
|
3200
3041
|
name: "llm-fallbacks",
|
|
3201
3042
|
version,
|
|
3202
|
-
resolveRole,
|
|
3203
3043
|
resolveChain,
|
|
3204
3044
|
validateFallbacksConfig,
|
|
3205
3045
|
detectLegacyKeys,
|
|
@@ -3226,17 +3066,16 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3226
3066
|
};
|
|
3227
3067
|
let serviceOwned = ctx.get("llm-fallbacks") === void 0;
|
|
3228
3068
|
let settingsComposed;
|
|
3229
|
-
|
|
3069
|
+
const rawSource = () => {
|
|
3230
3070
|
if (isLiveConfigRef(config)) return config.get();
|
|
3231
3071
|
if (settingsComposed !== void 0) return settingsComposed;
|
|
3232
3072
|
return entry;
|
|
3233
3073
|
};
|
|
3234
|
-
const
|
|
3235
|
-
validateFallbacksConfig(entry, logger);
|
|
3236
|
-
const legacyKeys = detectLegacyKeys(
|
|
3237
|
-
if (legacyKeys.length > 0) logger.warn("llm-fallbacks: legacy config keys detected (
|
|
3074
|
+
const source = () => migrateLegacyConfig(rawSource());
|
|
3075
|
+
validateFallbacksConfig(migrateLegacyConfig(entry), logger);
|
|
3076
|
+
const legacyKeys = detectLegacyKeys(rawSource());
|
|
3077
|
+
if (legacyKeys.length > 0) logger.warn("llm-fallbacks: legacy config keys detected (v1 two-block keys); see docs/configuration.md migration table — %o", legacyKeys);
|
|
3238
3078
|
let roleIds = new Map(entry.roles.list.map((role) => [role.id.trim(), role.id]));
|
|
3239
|
-
let hasChains = entry.rootChain.length > 0 || entry.roles.list.some((role) => (role.chain?.length ?? 0) > 0);
|
|
3240
3079
|
ctx.inject(["settings"], (sctx) => {
|
|
3241
3080
|
const settings = sctx.settings;
|
|
3242
3081
|
const refreshComposed = () => {
|
|
@@ -3249,7 +3088,6 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3249
3088
|
if (!isLiveConfigRef(config)) refreshComposed();
|
|
3250
3089
|
const current = source();
|
|
3251
3090
|
roleIds = new Map(current.roles.list.map((role) => [role.id.trim(), role.id]));
|
|
3252
|
-
hasChains = current.rootChain.length > 0 || current.roles.list.some((role) => (role.chain?.length ?? 0) > 0);
|
|
3253
3091
|
reconcileFallbacksAdapter();
|
|
3254
3092
|
slotWinners.clear();
|
|
3255
3093
|
});
|
|
@@ -3258,14 +3096,14 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3258
3096
|
settingsComposed = void 0;
|
|
3259
3097
|
};
|
|
3260
3098
|
});
|
|
3261
|
-
const bridge = {
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
blockedAttemptStores.set(ctx, blockedAttemptMap);
|
|
3099
|
+
const bridge = {
|
|
3100
|
+
source: () => source(),
|
|
3101
|
+
rawSource: () => rawSource()
|
|
3102
|
+
};
|
|
3266
3103
|
const subagentSeam = installSubagentSeam(ctx, {
|
|
3267
3104
|
roleIds: () => roleIds,
|
|
3268
3105
|
roles: () => source().roles.list,
|
|
3106
|
+
source: () => source(),
|
|
3269
3107
|
debug: (message) => logger.debug(message)
|
|
3270
3108
|
});
|
|
3271
3109
|
if (!subagentSeam.ownsWrapper) ctx.logger("llm-fallbacks").debug("subagent role seam wrapper already installed on this context root — this fiber registers the notice emitter and the projection unit only (multi-fiber dedupe)");
|
|
@@ -3275,20 +3113,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3275
3113
|
debug: (message) => logger.debug(message)
|
|
3276
3114
|
});
|
|
3277
3115
|
try {
|
|
3278
|
-
new FallbacksConfigGateway(ctx, bridge, seeds
|
|
3279
|
-
try {
|
|
3280
|
-
return {
|
|
3281
|
-
policy: effectivePolicy({
|
|
3282
|
-
ok: false,
|
|
3283
|
-
present: false
|
|
3284
|
-
}, readSubagentSettings(ctx)),
|
|
3285
|
-
head: latestByAt(chainHeadMap),
|
|
3286
|
-
blockedAttempt: latestByAt(blockedAttemptMap)
|
|
3287
|
-
};
|
|
3288
|
-
} catch {
|
|
3289
|
-
return { policy: { state: "unprovable" } };
|
|
3290
|
-
}
|
|
3291
|
-
});
|
|
3116
|
+
new FallbacksConfigGateway(ctx, bridge, seeds);
|
|
3292
3117
|
} catch (error) {
|
|
3293
3118
|
if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
|
|
3294
3119
|
ctx.logger("llm-fallbacks").debug("fallbacks gateway already registered — no gateway on this fiber (multi-fiber dedupe)");
|
|
@@ -3304,283 +3129,48 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3304
3129
|
});
|
|
3305
3130
|
const states = new FallbackStateStore();
|
|
3306
3131
|
stateStores.set(ctx, states);
|
|
3307
|
-
const dispatchInjected = new Set();
|
|
3308
|
-
const dispatchRoles = new Map();
|
|
3309
|
-
const rememberedRole = (agentId) => {
|
|
3310
|
-
const role = dispatchRoles.get(agentId);
|
|
3311
|
-
return role !== void 0 && roleIds.has(role.trim()) ? role : void 0;
|
|
3312
|
-
};
|
|
3313
3132
|
const slotWinners = new Map();
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
const rootTail = agent.session?.header?.origin === "subagent" ? config.rootChain : resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai");
|
|
3323
|
-
const { all, wildcard } = resolveChainViews(config.roles.list, rootTail, role, current.provider, current.model, logger.warn);
|
|
3324
|
-
if (all.length === 0) return null;
|
|
3325
|
-
const modelExists = hasWildcardEntry(config.roles.list, rootTail, role) ? await makeModelExists(ctx, [...new Set(all.map((candidate) => candidate.provider))]) : void 0;
|
|
3326
|
-
const cooldown = { isSuppressed: (key) => state !== void 0 && states.isSuppressed(state, key, Date.now(), source().recovery ?? "timer") };
|
|
3327
|
-
const failed = { has: (key) => state !== void 0 && state.stepFailures.failed.has(key) };
|
|
3328
|
-
const contextWindowOf = failureCode === CONTEXT_WINDOW_EXCEEDED_CODE ? await makeContextWindowOf(ctx, [current, ...all.flatMap((candidate) => candidate.model === void 0 ? [] : [{
|
|
3329
|
-
provider: candidate.provider,
|
|
3330
|
-
model: candidate.model
|
|
3331
|
-
}])]) : void 0;
|
|
3332
|
-
const minContextWindow = contextWindowOf?.(current.provider, current.model);
|
|
3333
|
-
const surviving = selectCandidates(all, wildcard, createCandidateFilter({
|
|
3334
|
-
current,
|
|
3335
|
-
cooldown,
|
|
3336
|
-
failed,
|
|
3337
|
-
minContextWindow,
|
|
3338
|
-
contextWindowOf
|
|
3339
|
-
}), modelExists);
|
|
3340
|
-
const first = surviving[0];
|
|
3341
|
-
let target;
|
|
3342
|
-
if (first !== void 0 && first.model !== void 0) target = {
|
|
3343
|
-
provider: first.provider,
|
|
3344
|
-
model: first.model
|
|
3345
|
-
};
|
|
3346
|
-
if (agent.session?.header?.origin === "subagent" && target !== void 0) {
|
|
3347
|
-
const settingsRead = readGuardedPolicySettings(ctx, agent.id, lastKnownPolicySettings, logger.warn);
|
|
3348
|
-
const policy = settingsRead.ok ? effectivePolicy(readSessionPolicyEvent(agent.session.snapshotEvents()), settingsRead.settings) : { state: "unprovable" };
|
|
3349
|
-
if (policy.state === "unprovable") {
|
|
3350
|
-
logger.warn("llm-fallbacks: agent \"%s\" subagent model-selection policy is on but unreadable — skipping fallback switch", agent.id);
|
|
3351
|
-
return null;
|
|
3352
|
-
}
|
|
3353
|
-
if (policy.state === "enabled") {
|
|
3354
|
-
const allowed = firstAllowedCandidate(resolvedRoutes(surviving), policy.allowedModels);
|
|
3355
|
-
if (allowed === void 0) {
|
|
3356
|
-
logger.warn("llm-fallbacks: agent \"%s\" fallback switch %s/%s blocked: resolved candidates are all outside the subagent allowlist — no switch", agent.id, target.provider, target.model);
|
|
3357
|
-
blockedAttemptMap.set(agent.id, {
|
|
3358
|
-
at: Date.now(),
|
|
3359
|
-
route: {
|
|
3360
|
-
provider: target.provider,
|
|
3361
|
-
model: target.model
|
|
3362
|
-
},
|
|
3363
|
-
reason
|
|
3364
|
-
});
|
|
3365
|
-
return null;
|
|
3366
|
-
}
|
|
3367
|
-
target = {
|
|
3368
|
-
provider: allowed.provider,
|
|
3369
|
-
model: allowed.model
|
|
3370
|
-
};
|
|
3371
|
-
}
|
|
3372
|
-
}
|
|
3373
|
-
if (target === void 0) return null;
|
|
3374
|
-
if ((source().recovery ?? "timer") === "half-open" && state !== void 0 && state.recovery.tryMarkProbeLogged(selectorKey(target.provider, target.model))) logger.info("llm-fallbacks: agent \"%s\" half-open probe %s/%s (role=%s, reason=%s)", agent.id, target.provider, target.model, role, reason);
|
|
3375
|
-
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, {
|
|
3376
|
-
current,
|
|
3377
|
-
cooldown,
|
|
3378
|
-
failed,
|
|
3379
|
-
minContextWindow,
|
|
3380
|
-
contextWindowOf
|
|
3381
|
-
}).map(({ candidate, skip }) => skip === void 0 ? `${candidate.provider}/${candidate.model}` : `${candidate.provider}/${candidate.model} (skipped: ${skip})`));
|
|
3382
|
-
return {
|
|
3383
|
-
from: {
|
|
3384
|
-
provider: current.provider,
|
|
3385
|
-
model: current.model
|
|
3386
|
-
},
|
|
3387
|
-
to: {
|
|
3388
|
-
provider: target.provider,
|
|
3389
|
-
model: target.model
|
|
3390
|
-
},
|
|
3391
|
-
role,
|
|
3392
|
-
reason
|
|
3393
|
-
};
|
|
3394
|
-
}
|
|
3395
|
-
function commit(state, pending, turn, step) {
|
|
3396
|
-
const config = source();
|
|
3397
|
-
const fromKey = selectorKey(pending.from.provider, pending.from.model);
|
|
3398
|
-
states.syncStep(state, turn, step);
|
|
3399
|
-
states.writePending(state, pending);
|
|
3400
|
-
if ((pending.scope ?? "route") === "route") {
|
|
3401
|
-
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;
|
|
3402
|
-
states.suppress(state, fromKey, until);
|
|
3403
|
-
}
|
|
3404
|
-
states.recordFailure(state, fromKey);
|
|
3405
|
-
states.recordSwitch(state);
|
|
3406
|
-
}
|
|
3407
|
-
function failHalfOpenProbe(agentId, current) {
|
|
3408
|
-
const key = selectorKey(current.provider, current.model);
|
|
3409
|
-
if ((source().recovery ?? "timer") !== "half-open") return;
|
|
3410
|
-
if (source().revertPolicy === "never") return;
|
|
3411
|
-
if (states.peek(agentId)?.recovery.isHalfOpen(key) !== true) return;
|
|
3412
|
-
const state = states.get(agentId);
|
|
3413
|
-
const n = state.recovery.recordFailure(key);
|
|
3414
|
-
states.suppress(state, key, Date.now() + escalatedCooldownMs(source().cooldownMs, n));
|
|
3415
|
-
}
|
|
3416
|
-
ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure }, next) => {
|
|
3417
|
-
const config = source();
|
|
3418
|
-
if (!isFallbackActive(config) || !config.triggerCodes.includes(failure.code)) return next();
|
|
3419
|
-
const current = currentModel(agent, provider, config, new Date());
|
|
3420
|
-
if (!current.model) return next();
|
|
3421
|
-
try {
|
|
3422
|
-
const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id), failure.code);
|
|
3423
|
-
if (pending === null) {
|
|
3424
|
-
failHalfOpenProbe(agent.id, current);
|
|
3425
|
-
return next();
|
|
3426
|
-
}
|
|
3427
|
-
const scope = REQUEST_SCOPED_CODES.has(failure.code) ? "request" : void 0;
|
|
3428
|
-
commit(states.get(agent.id), scope === void 0 ? pending : {
|
|
3429
|
-
...pending,
|
|
3430
|
-
scope
|
|
3431
|
-
}, turn, step);
|
|
3432
|
-
return { kind: "retry" };
|
|
3433
|
-
} catch (error) {
|
|
3434
|
-
logger.warn("llm-fallbacks: decision path failed, passing the original failure through: %s", error?.message ?? String(error));
|
|
3435
|
-
return next();
|
|
3436
|
-
}
|
|
3437
|
-
});
|
|
3438
|
-
ctx.on("agent/request", async ({ agent, turn, step }, next) => {
|
|
3439
|
-
const seed = await next();
|
|
3440
|
-
const state = states.peek(agent.id);
|
|
3441
|
-
const applied = state === void 0 ? void 0 : states.applyPending(state, turn, step);
|
|
3442
|
-
if (applied !== void 0) return overrideConfig(seed, applied.to);
|
|
3443
|
-
const config = source();
|
|
3444
|
-
if (isFallbackActive(config) && isAllDayConforming(config.rootChain) && (config.timeSlots?.length ?? 0) > 0 && agent.session?.header?.origin !== "subagent") {
|
|
3445
|
-
const slot = resolveSlotState(config, new Date(), config.tz ?? "Asia/Shanghai");
|
|
3446
|
-
const key = slotWinnerKey(slot.winner);
|
|
3447
|
-
const previous = slotWinners.get(agent.id);
|
|
3448
|
-
if (previous !== void 0 && previous.key !== key) logger.info("llm-fallbacks: agent \"%s\" time-slot switch (分时切换): %s -> %s", agent.id, previous.label, slot.label);
|
|
3449
|
-
slotWinners.set(agent.id, {
|
|
3450
|
-
key,
|
|
3451
|
-
label: slot.label
|
|
3452
|
-
});
|
|
3453
|
-
}
|
|
3454
|
-
if (isFallbackActive(config) && hasChains && agent.session?.header?.origin === "subagent" && !dispatchInjected.has(agent.id)) {
|
|
3455
|
-
dispatchInjected.add(agent.id);
|
|
3456
|
-
try {
|
|
3457
|
-
const settingsRead = readGuardedPolicySettings(ctx, agent.id, lastKnownPolicySettings, logger.warn);
|
|
3458
|
-
const policy = settingsRead.ok ? effectivePolicy(readSessionPolicyEvent(agent.session.snapshotEvents()), settingsRead.settings) : { state: "unprovable" };
|
|
3459
|
-
if (policy.state === "unprovable") logger.warn("llm-fallbacks: agent \"%s\" subagent model-selection policy is on but unreadable — skipping role-inject", agent.id);
|
|
3460
|
-
else {
|
|
3461
|
-
const authorized = policy.state === "enabled" ? detectAuthorizedRoute(authorizedRouteView(ctx, agent)) : void 0;
|
|
3462
|
-
if (authorized !== void 0) {
|
|
3463
|
-
chainHeadMap.set(agent.id, {
|
|
3464
|
-
at: Date.now(),
|
|
3465
|
-
route: {
|
|
3466
|
-
provider: authorized.provider,
|
|
3467
|
-
model: authorized.model
|
|
3468
|
-
},
|
|
3469
|
-
source: "authorized"
|
|
3470
|
-
});
|
|
3471
|
-
logger.info("llm-fallbacks: agent \"%s\" authorized child route %s/%s is the chain head — skipping role-inject", agent.id, authorized.provider, authorized.model);
|
|
3472
|
-
} else {
|
|
3473
|
-
const role = await resolveRoleAtDispatch(agent, config.roles.rules, roleIds, {
|
|
3474
|
-
automatchEnabled: config.roleAutoMatch ?? true,
|
|
3475
|
-
automatch: (agent) => pickRoleByLlm(ctx, config.roles, agent, { warn: logger.warn }),
|
|
3476
|
-
dispatchRole: subagentSeam.records.get(agent.id)?.role,
|
|
3477
|
-
warn: logger.warn,
|
|
3478
|
-
servedRoute: servedRouteFor(agent, config)
|
|
3479
|
-
});
|
|
3480
|
-
if (role !== "inherit") {
|
|
3481
|
-
dispatchRoles.set(agent.id, role);
|
|
3482
|
-
const { all, wildcard } = resolveChainViews(config.roles.list, config.rootChain, role, seed.provider, seed.model, logger.warn);
|
|
3483
|
-
let to;
|
|
3484
|
-
if (policy.state === "enabled") {
|
|
3485
|
-
const routes = resolvedRoutes(all);
|
|
3486
|
-
to = firstAllowedCandidate(routes, policy.allowedModels);
|
|
3487
|
-
if (to === void 0 && routes.length > 0) logger.warn("llm-fallbacks: agent \"%s\" role-inject candidates are all outside the subagent allowlist — skipping inject (host seed stands)", agent.id);
|
|
3488
|
-
} else {
|
|
3489
|
-
const head = firstExactCandidate(all, wildcard);
|
|
3490
|
-
if (head !== void 0 && head.model !== void 0) to = {
|
|
3491
|
-
provider: head.provider,
|
|
3492
|
-
model: head.model
|
|
3493
|
-
};
|
|
3494
|
-
}
|
|
3495
|
-
if (to !== void 0 && !(to.provider === seed.provider && to.model === seed.model)) {
|
|
3496
|
-
if (policy.state === "enabled") chainHeadMap.set(agent.id, {
|
|
3497
|
-
at: Date.now(),
|
|
3498
|
-
route: {
|
|
3499
|
-
provider: to.provider,
|
|
3500
|
-
model: to.model
|
|
3501
|
-
},
|
|
3502
|
-
source: "injected"
|
|
3503
|
-
});
|
|
3504
|
-
logger.info("llm-fallbacks: agent \"%s\" role-inject role=%s model=%s/%s", agent.id, role, to.provider, to.model);
|
|
3505
|
-
return overrideConfig(seed, to);
|
|
3506
|
-
}
|
|
3507
|
-
}
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
} catch (error) {
|
|
3511
|
-
logger.warn("llm-fallbacks: dispatch role-injection failed, proceeding with the request unchanged: %s", error?.message ?? String(error));
|
|
3512
|
-
}
|
|
3513
|
-
}
|
|
3514
|
-
if (hasChains && isFallbackActive(config) && config.alwaysModeRetryCap > 0 && countRetryEvents(agent.session, turn, step, seed.provider) >= config.alwaysModeRetryCap) {
|
|
3515
|
-
const decisionState = states.peek(agent.id);
|
|
3516
|
-
const capRoute = anchorServedRoute(config, {
|
|
3517
|
-
provider: seed.provider,
|
|
3518
|
-
model: seed.model
|
|
3519
|
-
}, new Date());
|
|
3520
|
-
const pending = await decide(agent, turn, step, capRoute, "always-cap", decisionState);
|
|
3521
|
-
if (pending !== null) {
|
|
3522
|
-
const commitState = states.get(agent.id);
|
|
3523
|
-
commit(commitState, pending, turn, step);
|
|
3524
|
-
const appliedCap = states.applyPending(commitState, turn, step);
|
|
3525
|
-
if (appliedCap !== void 0) return overrideConfig(seed, appliedCap.to);
|
|
3526
|
-
} else failHalfOpenProbe(agent.id, capRoute);
|
|
3527
|
-
}
|
|
3528
|
-
return seed;
|
|
3529
|
-
});
|
|
3530
|
-
ctx.on("agent/status", ({ agent, status }) => {
|
|
3531
|
-
if (status !== "idle") return;
|
|
3532
|
-
const state = states.peek(agent.id);
|
|
3533
|
-
if (state !== void 0) states.clearStepState(state);
|
|
3534
|
-
});
|
|
3133
|
+
const walkDeps = {
|
|
3134
|
+
readConfig: () => source(),
|
|
3135
|
+
getLlm: () => ctx.get("llm"),
|
|
3136
|
+
states,
|
|
3137
|
+
slotWinners,
|
|
3138
|
+
log: logger
|
|
3139
|
+
};
|
|
3140
|
+
const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source(), (options) => walkChain(walkDeps, options));
|
|
3535
3141
|
ctx.on("agent/disposed", ({ agent }) => {
|
|
3536
3142
|
states.delete(agent.id);
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
slotWinners.delete(agent.id);
|
|
3540
|
-
blockedAttemptMap.delete(agent.id);
|
|
3541
|
-
chainHeadMap.delete(agent.id);
|
|
3143
|
+
const markerPrefix = slotMarkerPrefix(agent.id);
|
|
3144
|
+
for (const key of slotWinners.keys()) if (key.startsWith(markerPrefix)) slotWinners.delete(key);
|
|
3542
3145
|
subagentSeam.records.delete(agent.id);
|
|
3543
|
-
lastKnownPolicySettings.delete(agent.id);
|
|
3544
|
-
});
|
|
3545
|
-
ctx.on("session/event", (session, event) => {
|
|
3546
|
-
if (event.type !== "assistant/message") return;
|
|
3547
|
-
const config = source();
|
|
3548
|
-
if ((config.recovery ?? "timer") !== "half-open") return;
|
|
3549
|
-
if (event.data.interrupted === true) return;
|
|
3550
|
-
const message = event.data.message;
|
|
3551
|
-
if (message.source.kind !== "model") return;
|
|
3552
|
-
const state = states.peek(session.id);
|
|
3553
|
-
if (state === void 0) return;
|
|
3554
|
-
const observed = anchorServedRoute(config, {
|
|
3555
|
-
provider: message.source.provider,
|
|
3556
|
-
model: message.source.model
|
|
3557
|
-
}, new Date());
|
|
3558
|
-
states.observeSuccess(state, selectorKey(observed.provider, observed.model));
|
|
3559
3146
|
});
|
|
3560
3147
|
ctx.effect(() => () => {
|
|
3561
3148
|
states.clear();
|
|
3562
|
-
dispatchInjected.clear();
|
|
3563
|
-
dispatchRoles.clear();
|
|
3564
3149
|
slotWinners.clear();
|
|
3565
|
-
blockedAttemptMap.clear();
|
|
3566
|
-
chainHeadMap.clear();
|
|
3567
|
-
subagentSeam.records.clear();
|
|
3568
|
-
subagentSeam.noticeEmitted.clear();
|
|
3569
|
-
subagentSeam.dispose();
|
|
3570
|
-
lastKnownPolicySettings.clear();
|
|
3571
3150
|
}, "llm-fallbacks: clear per-agent state");
|
|
3572
3151
|
const fallbacksCommandController = {
|
|
3573
3152
|
getSnapshot(agent) {
|
|
3574
3153
|
const config = source();
|
|
3575
|
-
const role = rememberedRole(agent.id) ?? resolveRole(agent, config.roles.rules, roleIds, logger.warn, servedRouteFor(agent, config));
|
|
3576
3154
|
const state = states.peek(agent.id);
|
|
3577
3155
|
const recovery = config.recovery ?? "timer";
|
|
3578
3156
|
if (state !== void 0 && recovery === "half-open") states.syncRecovery(state, Date.now(), recovery);
|
|
3157
|
+
const roleRecord = subagentSeam.records.get(agent.id);
|
|
3158
|
+
const role = roleRecord?.role ?? "";
|
|
3159
|
+
const roleChain = roleRecord === void 0 ? void 0 : source().roles.list.find((entry) => entry.id === role)?.chain;
|
|
3160
|
+
const selectedChainId = agent.options?.provider === "FallbacksChain" ? agent.options?.model : void 0;
|
|
3161
|
+
const now = new Date();
|
|
3162
|
+
const chains = source().chains.map((chain) => ({
|
|
3163
|
+
id: chain.id,
|
|
3164
|
+
label: chain.label,
|
|
3165
|
+
entries: chain.entries,
|
|
3166
|
+
slotLabel: resolveSlotState(chain, now, source().tz ?? "Asia/Shanghai").label,
|
|
3167
|
+
selected: chain.id === selectedChainId
|
|
3168
|
+
}));
|
|
3579
3169
|
return {
|
|
3580
3170
|
origin: agent.session.header?.origin ?? "root",
|
|
3581
3171
|
role,
|
|
3582
|
-
...
|
|
3583
|
-
|
|
3172
|
+
...roleChain === void 0 ? {} : { roleChain },
|
|
3173
|
+
chains,
|
|
3584
3174
|
switches: recentFallbacksSwitches(agent.session.snapshotEvents(), 5),
|
|
3585
3175
|
cooldown: state === void 0 ? [] : recovery === "half-open" ? [...state.cooldown.snapshot(), ...state.recovery.halfOpenEntries().map((entry) => ({
|
|
3586
3176
|
key: entry.key,
|
|
@@ -3594,32 +3184,29 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3594
3184
|
return {
|
|
3595
3185
|
active: isFallbackActive(config),
|
|
3596
3186
|
triggerCodes: config.triggerCodes,
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3187
|
+
chains: config.chains.map((chain) => ({
|
|
3188
|
+
id: chain.id,
|
|
3189
|
+
label: chain.label,
|
|
3190
|
+
entries: Array.isArray(chain.entries) ? chain.entries : [],
|
|
3191
|
+
timeSlots: (chain.timeSlots ?? []).map((row) => row.kind === "preset" ? {
|
|
3192
|
+
preset: row.preset,
|
|
3193
|
+
chainCount: (row.chain ?? []).length
|
|
3194
|
+
} : {
|
|
3195
|
+
start: row.start,
|
|
3196
|
+
end: row.end,
|
|
3197
|
+
days: row.days,
|
|
3198
|
+
chainCount: (row.chain ?? []).length
|
|
3199
|
+
})
|
|
3200
|
+
})),
|
|
3607
3201
|
tz: config.tz ?? "Asia/Shanghai",
|
|
3608
3202
|
roles: config.roles.list.map((role) => ({
|
|
3609
3203
|
id: role.id,
|
|
3610
|
-
|
|
3611
|
-
})),
|
|
3612
|
-
rules: config.roles.rules.map((rule) => ({
|
|
3613
|
-
provider: rule.provider ?? "",
|
|
3614
|
-
model: rule.model ?? "",
|
|
3615
|
-
role: rule.role
|
|
3204
|
+
chain: role.chain
|
|
3616
3205
|
})),
|
|
3617
3206
|
cooldownMs: config.cooldownMs,
|
|
3618
3207
|
revertPolicy: config.revertPolicy,
|
|
3619
3208
|
maxSwitchesPerStep: config.maxSwitchesPerStep,
|
|
3620
|
-
|
|
3621
|
-
presets: config.presets ?? "bundled",
|
|
3622
|
-
roleAutoMatch: config.roleAutoMatch ?? true
|
|
3209
|
+
presets: config.presets ?? "bundled"
|
|
3623
3210
|
};
|
|
3624
3211
|
},
|
|
3625
3212
|
revertSeed: async (roleId) => {
|
|
@@ -3645,4 +3232,4 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3645
3232
|
});
|
|
3646
3233
|
}
|
|
3647
3234
|
//#endregion
|
|
3648
|
-
export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, ROLE_PROJECTION_KEY, SelectorError, annotateCandidates, apply,
|
|
3235
|
+
export { CHAIN_ID_PATTERN, Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, ROLE_PROJECTION_KEY, SelectorError, annotateCandidates, apply, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, readRoleProjectionValue, resolveCandidate, resolveChain, resolveChainViews, selectCandidates, stateStore, validateFallbacksConfig };
|