dsh-llm-fallbacks 0.6.13 → 0.7.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 +14 -10
- package/README.zh-CN.md +14 -10
- package/dist/chain-walk.d.ts +211 -0
- package/dist/chains.d.ts +28 -6
- package/dist/client/index.js +86 -194
- package/dist/client/locales.d.ts +2 -12
- package/dist/commands.d.ts +3 -10
- package/dist/config.d.ts +4 -16
- package/dist/cooldown.d.ts +1 -15
- package/dist/events.d.ts +8 -8
- package/dist/gateway.d.ts +5 -3
- package/dist/index.d.ts +19 -40
- package/dist/index.js +685 -875
- package/dist/role-resolution.d.ts +12 -51
- package/dist/roles.d.ts +3 -4
- package/dist/state.d.ts +18 -102
- package/dist/subagent-policy.d.ts +22 -8
- package/dist/subagents-seam.d.ts +70 -11
- package/dist/tui-settings.d.ts +4 -2
- package/dist/virtual-adapter.d.ts +42 -46
- 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/override.d.ts +0 -39
package/dist/index.js
CHANGED
|
@@ -338,9 +338,7 @@ const defaultFallbacksConfig = {
|
|
|
338
338
|
cooldownMs: 3e5,
|
|
339
339
|
revertPolicy: "cooldown-expiry",
|
|
340
340
|
maxSwitchesPerStep: 8,
|
|
341
|
-
alwaysModeRetryCap: 5,
|
|
342
341
|
presets: "bundled",
|
|
343
|
-
roleAutoMatch: true,
|
|
344
342
|
timeSlots: [],
|
|
345
343
|
tz: "Asia/Shanghai",
|
|
346
344
|
recovery: "timer"
|
|
@@ -396,6 +394,8 @@ function validateFallbacksConfig(config, logger) {
|
|
|
396
394
|
function detectLegacyKeys(source) {
|
|
397
395
|
const keys = [];
|
|
398
396
|
if (Object.hasOwn(source, "chains")) keys.push("chains");
|
|
397
|
+
if (Object.hasOwn(source, "alwaysModeRetryCap")) keys.push("alwaysModeRetryCap");
|
|
398
|
+
if (Object.hasOwn(source, "roleAutoMatch")) keys.push("roleAutoMatch");
|
|
399
399
|
const roles = source.roles;
|
|
400
400
|
if (isRecordLike(roles)) {
|
|
401
401
|
if (Object.hasOwn(roles, "default")) keys.push("roles.default");
|
|
@@ -447,9 +447,7 @@ const Config = z.object({
|
|
|
447
447
|
cooldownMs: z.number().default(3e5),
|
|
448
448
|
revertPolicy: z.union([z.const("cooldown-expiry"), z.const("never")]).default("cooldown-expiry"),
|
|
449
449
|
maxSwitchesPerStep: z.number().default(8),
|
|
450
|
-
alwaysModeRetryCap: z.number().default(5),
|
|
451
450
|
presets: z.union([z.const("bundled"), z.const("none")]).default("bundled"),
|
|
452
|
-
roleAutoMatch: z.boolean().default(true),
|
|
453
451
|
timeSlots: z.array(z.object({
|
|
454
452
|
kind: z.string(),
|
|
455
453
|
preset: z.string(),
|
|
@@ -461,98 +459,6 @@ const Config = z.object({
|
|
|
461
459
|
tz: z.string().default("Asia/Shanghai"),
|
|
462
460
|
recovery: z.union([z.const("timer"), z.const("half-open")]).default("timer")
|
|
463
461
|
}).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
462
|
//#endregion
|
|
557
463
|
//#region .build/host/chains.js
|
|
558
464
|
function resolveCandidate(entry, failing, modelExists) {
|
|
@@ -569,7 +475,7 @@ function resolveCandidate(entry, failing, modelExists) {
|
|
|
569
475
|
}
|
|
570
476
|
return selector;
|
|
571
477
|
}
|
|
572
|
-
function
|
|
478
|
+
function roleChainEntries(roles, rootChain, role) {
|
|
573
479
|
if (role.trim() === "inherit") return rootChain;
|
|
574
480
|
const roleDef = roles.find((declared) => declared.id.trim() === role.trim());
|
|
575
481
|
if (roleDef === void 0) return rootChain;
|
|
@@ -580,7 +486,7 @@ function resolveChainViews(roles, rootChain, role, provider, model, warn = conso
|
|
|
580
486
|
provider,
|
|
581
487
|
model
|
|
582
488
|
};
|
|
583
|
-
const entries =
|
|
489
|
+
const entries = roleChainEntries(roles, rootChain, role);
|
|
584
490
|
if (role.trim() !== "inherit" && !roles.some((declared) => declared.id.trim() === role.trim())) warn(`llm-fallbacks: unknown role "${role}" — falling back to rootChain`);
|
|
585
491
|
const all = [];
|
|
586
492
|
const wildcard = [];
|
|
@@ -616,7 +522,7 @@ function resolveChain(roles, rootChain, role, provider, model, filter, modelExis
|
|
|
616
522
|
return selectCandidates(all, wildcard, filter, modelExists);
|
|
617
523
|
}
|
|
618
524
|
function hasWildcardEntry(roles, rootChain, role) {
|
|
619
|
-
const entries =
|
|
525
|
+
const entries = roleChainEntries(roles, rootChain, role);
|
|
620
526
|
for (const entry of entries) try {
|
|
621
527
|
if (parseSelector(entry).model === void 0) return true;
|
|
622
528
|
} catch {}
|
|
@@ -668,6 +574,25 @@ function annotateCandidates(candidates, surviving, options) {
|
|
|
668
574
|
});
|
|
669
575
|
}
|
|
670
576
|
//#endregion
|
|
577
|
+
//#region .build/host/route-allowlist.js
|
|
578
|
+
function resolvedRoutes(candidates) {
|
|
579
|
+
const routes = [];
|
|
580
|
+
for (const candidate of candidates) {
|
|
581
|
+
if (candidate.model === void 0) continue;
|
|
582
|
+
routes.push({
|
|
583
|
+
provider: candidate.provider,
|
|
584
|
+
model: candidate.model
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return routes;
|
|
588
|
+
}
|
|
589
|
+
function routeOnAllowlist(route, allowedModels) {
|
|
590
|
+
return allowedModels.some((allowed) => allowed.provider === route.provider && allowed.model === route.model);
|
|
591
|
+
}
|
|
592
|
+
function firstAllowedCandidate(resolved, allowedModels) {
|
|
593
|
+
return resolved.find((route) => routeOnAllowlist(route, allowedModels));
|
|
594
|
+
}
|
|
595
|
+
//#endregion
|
|
671
596
|
//#region .build/host/roles.js
|
|
672
597
|
function resolveRole(agent, rules, roleIds, warn = console.warn, servedRoute) {
|
|
673
598
|
if ((agent.session?.header?.origin ?? "root") !== "subagent") return INHERIT_ROLE_ID;
|
|
@@ -691,7 +616,7 @@ function resolveRole(agent, rules, roleIds, warn = console.warn, servedRoute) {
|
|
|
691
616
|
}
|
|
692
617
|
//#endregion
|
|
693
618
|
//#region .build/host/role-resolution.js
|
|
694
|
-
|
|
619
|
+
function resolveDeclaredRole(agent, rules, roleIds, opts) {
|
|
695
620
|
const preset = agent.session?.header?.agentPreset?.trim();
|
|
696
621
|
if (preset !== void 0 && preset !== "" && preset !== "inherit") {
|
|
697
622
|
const declared = roleIds.get(preset);
|
|
@@ -702,67 +627,453 @@ async function resolveRoleAtDispatch(agent, rules, roleIds, opts) {
|
|
|
702
627
|
const declared = roleIds.get(dispatch);
|
|
703
628
|
if (declared !== void 0) return declared;
|
|
704
629
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
630
|
+
return resolveRole(agent, rules, roleIds, opts.warn, opts.servedRoute);
|
|
631
|
+
}
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region .build/host/virtual-adapter.js
|
|
634
|
+
const FALLBACKS_PROVIDER$1 = "FallbacksChain";
|
|
635
|
+
const FALLBACKS_CHAIN_MODEL = "Auto";
|
|
636
|
+
function firstDispatchableExactHead(chain, allowedModels) {
|
|
637
|
+
for (const entry of chain) {
|
|
638
|
+
let selector;
|
|
639
|
+
try {
|
|
640
|
+
selector = parseSelector(entry);
|
|
641
|
+
} catch {
|
|
642
|
+
continue;
|
|
713
643
|
}
|
|
644
|
+
if (selector.model === void 0) continue;
|
|
645
|
+
if (selector.provider === "FallbacksChain") continue;
|
|
646
|
+
const head = {
|
|
647
|
+
provider: selector.provider,
|
|
648
|
+
model: selector.model
|
|
649
|
+
};
|
|
650
|
+
if (allowedModels !== void 0 && !routeOnAllowlist(head, allowedModels)) continue;
|
|
651
|
+
return head;
|
|
714
652
|
}
|
|
715
|
-
return INHERIT_ROLE_ID;
|
|
716
653
|
}
|
|
717
|
-
function
|
|
718
|
-
|
|
654
|
+
function effectiveHeadOf(config, now) {
|
|
655
|
+
if (!isAllDayConforming(config.rootChain)) return void 0;
|
|
656
|
+
return firstDispatchableExactHead(resolveEffectiveChain(config, now, config.tz ?? "Asia/Shanghai"));
|
|
657
|
+
}
|
|
658
|
+
function pickerDisplayName(config, now = new Date(), modelDisplayName) {
|
|
659
|
+
const head = effectiveHeadOf(config, now);
|
|
660
|
+
if (head === void 0) return FALLBACKS_CHAIN_MODEL;
|
|
661
|
+
const slot = resolveSlotState(config, now, config.tz ?? "Asia/Shanghai");
|
|
662
|
+
const model = modelDisplayName !== void 0 && modelDisplayName !== "" ? modelDisplayName : head.model;
|
|
663
|
+
return `${FALLBACKS_CHAIN_MODEL}: ${model}[${slot.label}]`;
|
|
664
|
+
}
|
|
665
|
+
async function resolveHeadDisplayName(llm, head) {
|
|
666
|
+
if (llm === void 0) return head.model;
|
|
667
|
+
try {
|
|
668
|
+
const row = (await llm.listModels(head.provider)).find((model) => model.id === head.model);
|
|
669
|
+
if (row !== void 0 && row.name !== "") return row.name;
|
|
670
|
+
} catch {}
|
|
671
|
+
try {
|
|
672
|
+
const info = await llm.resolveModelInfo(head.provider, head.model);
|
|
673
|
+
if (info.name !== "") return info.name;
|
|
674
|
+
} catch {}
|
|
675
|
+
return head.model;
|
|
676
|
+
}
|
|
677
|
+
var FallbacksChainAdapter = class extends LlmAdapter {
|
|
678
|
+
readConfig;
|
|
679
|
+
getLlm;
|
|
680
|
+
walk;
|
|
681
|
+
constructor(readConfig, getLlm, walk) {
|
|
682
|
+
super();
|
|
683
|
+
this.readConfig = readConfig;
|
|
684
|
+
this.getLlm = getLlm;
|
|
685
|
+
this.walk = walk;
|
|
686
|
+
}
|
|
687
|
+
providerInfo(provider) {
|
|
688
|
+
return {
|
|
689
|
+
id: provider,
|
|
690
|
+
name: FALLBACKS_PROVIDER$1
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
async listModels(provider) {
|
|
694
|
+
const config = this.readConfig();
|
|
695
|
+
const now = new Date();
|
|
696
|
+
const head = effectiveHeadOf(config, now);
|
|
697
|
+
const display = head === void 0 ? void 0 : await resolveHeadDisplayName(this.getLlm(), head);
|
|
698
|
+
return [{
|
|
699
|
+
provider,
|
|
700
|
+
id: FALLBACKS_CHAIN_MODEL,
|
|
701
|
+
name: pickerDisplayName(config, now, display)
|
|
702
|
+
}];
|
|
703
|
+
}
|
|
704
|
+
async resolveModel(provider, model, signal) {
|
|
705
|
+
const head = effectiveHeadOf(this.readConfig(), new Date());
|
|
706
|
+
const llm = this.getLlm();
|
|
707
|
+
if (head !== void 0 && llm !== void 0) try {
|
|
708
|
+
const info = await llm.resolveModelInfo(head.provider, head.model, signal);
|
|
709
|
+
return {
|
|
710
|
+
provider,
|
|
711
|
+
id: model,
|
|
712
|
+
name: info.name,
|
|
713
|
+
...info.description === void 0 ? {} : { description: info.description },
|
|
714
|
+
...info.inputModalities === void 0 ? {} : { inputModalities: info.inputModalities },
|
|
715
|
+
...info.context === void 0 ? {} : { context: info.context },
|
|
716
|
+
...info.defaultMaxTokens === void 0 ? {} : { defaultMaxTokens: info.defaultMaxTokens },
|
|
717
|
+
...info.reasoning === void 0 ? {} : { reasoning: info.reasoning }
|
|
718
|
+
};
|
|
719
|
+
} catch {}
|
|
720
|
+
return {
|
|
721
|
+
provider,
|
|
722
|
+
id: model,
|
|
723
|
+
name: model
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
providerRetryPolicy(_provider) {
|
|
727
|
+
try {
|
|
728
|
+
const head = effectiveHeadOf(this.readConfig(), new Date());
|
|
729
|
+
if (head === void 0 || head.provider === "FallbacksChain") return void 0;
|
|
730
|
+
return this.getLlm()?.providerRetryPolicy(head.provider);
|
|
731
|
+
} catch {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
imageRequestPricing(_provider, _model) {
|
|
736
|
+
try {
|
|
737
|
+
const head = effectiveHeadOf(this.readConfig(), new Date());
|
|
738
|
+
if (head === void 0 || head.provider === "FallbacksChain") return void 0;
|
|
739
|
+
return this.getLlm()?.imageRequestPricing(head.provider, head.model);
|
|
740
|
+
} catch {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
stream(options) {
|
|
745
|
+
return this.walk(options);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
function installFallbacksAdapter(ctx, readConfig, walk) {
|
|
749
|
+
let llm;
|
|
750
|
+
let disposeAdapter;
|
|
751
|
+
let registered = false;
|
|
752
|
+
const logger = ctx.logger("llm-fallbacks");
|
|
753
|
+
const adapter = new FallbacksChainAdapter(readConfig, () => llm, walk);
|
|
754
|
+
const reconcile = () => {
|
|
755
|
+
const shouldRegister = isFallbackActive(readConfig());
|
|
756
|
+
if (shouldRegister && !registered) {
|
|
757
|
+
if (llm === void 0) return;
|
|
758
|
+
try {
|
|
759
|
+
disposeAdapter = llm.registerAdapter([FALLBACKS_PROVIDER$1], adapter);
|
|
760
|
+
registered = true;
|
|
761
|
+
logger.info("llm-fallbacks: virtual adapter registered — FallbacksChain/Auto is selectable in the model picker");
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "DUPLICATE_ADAPTER") throw error;
|
|
764
|
+
logger.debug("llm-fallbacks: virtual adapter already registered — no route on this fiber (multi-fiber dedupe)");
|
|
765
|
+
}
|
|
766
|
+
} else if (!shouldRegister && registered) {
|
|
767
|
+
disposeAdapter?.();
|
|
768
|
+
disposeAdapter = void 0;
|
|
769
|
+
registered = false;
|
|
770
|
+
logger.info("llm-fallbacks: virtual adapter unregistered — FallbacksChain/Auto hidden from the model picker");
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
ctx.inject(["llm"], (llmCtx) => {
|
|
774
|
+
llm = llmCtx.llm;
|
|
775
|
+
reconcile();
|
|
776
|
+
return () => {
|
|
777
|
+
disposeAdapter?.();
|
|
778
|
+
disposeAdapter = void 0;
|
|
779
|
+
registered = false;
|
|
780
|
+
llm = void 0;
|
|
781
|
+
};
|
|
782
|
+
});
|
|
783
|
+
return reconcile;
|
|
719
784
|
}
|
|
720
785
|
//#endregion
|
|
721
|
-
//#region .build/host/
|
|
722
|
-
|
|
723
|
-
|
|
786
|
+
//#region .build/host/chain-walk.js
|
|
787
|
+
const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
|
|
788
|
+
const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
|
|
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));
|
|
822
|
+
}
|
|
823
|
+
function envelopeProvenance(replayState) {
|
|
824
|
+
if (typeof replayState !== "object" || replayState === null) return void 0;
|
|
825
|
+
const response = replayState.response;
|
|
826
|
+
if (typeof response !== "object" || response === null) return void 0;
|
|
827
|
+
const { provider, model } = response;
|
|
828
|
+
if (typeof provider !== "string" || provider === "" || provider === "FallbacksChain") return void 0;
|
|
829
|
+
if (typeof model !== "string" || model === "") return void 0;
|
|
724
830
|
return {
|
|
725
|
-
provider
|
|
726
|
-
model
|
|
831
|
+
provider,
|
|
832
|
+
model
|
|
727
833
|
};
|
|
728
834
|
}
|
|
729
|
-
function
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
if (
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
835
|
+
function restoreEnvelopeProvenance(messages) {
|
|
836
|
+
let changed = false;
|
|
837
|
+
const restored = messages.map((message) => {
|
|
838
|
+
const source = message.source;
|
|
839
|
+
if (message.role !== "assistant" || source?.kind !== "model") return message;
|
|
840
|
+
if (source.provider !== "FallbacksChain" || source.model !== "Auto") return message;
|
|
841
|
+
const provenance = envelopeProvenance(source.replayState);
|
|
842
|
+
if (provenance === void 0) return message;
|
|
843
|
+
changed = true;
|
|
844
|
+
return {
|
|
845
|
+
...message,
|
|
846
|
+
source: {
|
|
847
|
+
...source,
|
|
848
|
+
...provenance
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
});
|
|
852
|
+
return changed ? restored : messages;
|
|
737
853
|
}
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
return route;
|
|
854
|
+
function agentLikeOf(view) {
|
|
855
|
+
return {
|
|
856
|
+
options: view?.optionsRoute,
|
|
857
|
+
session: { header: {
|
|
858
|
+
origin: view?.origin ?? "root",
|
|
859
|
+
agentPreset: view?.agentPreset
|
|
860
|
+
} }
|
|
861
|
+
};
|
|
747
862
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
863
|
+
function servedRouteOf(config, route, now) {
|
|
864
|
+
if (route?.provider !== "FallbacksChain" || route.model !== "Auto") return void 0;
|
|
865
|
+
const head = effectiveHeadOf(config, now);
|
|
866
|
+
return head === void 0 ? void 0 : {
|
|
867
|
+
provider: head.provider,
|
|
868
|
+
model: head.model
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function slotWinnerKey(winner) {
|
|
872
|
+
if (winner === "all-day") return "all-day";
|
|
873
|
+
return winner.kind === "preset" ? `preset:${winner.preset}` : `custom:${winner.start}-${winner.end}`;
|
|
874
|
+
}
|
|
875
|
+
function logSlotRotation(deps, start) {
|
|
876
|
+
const { config, now, origin, owner } = start;
|
|
877
|
+
const markers = deps.slotWinners;
|
|
878
|
+
if (markers === void 0 || owner === void 0 || origin !== "root") return;
|
|
879
|
+
if (!isAllDayConforming(config.rootChain) || (config.timeSlots?.length ?? 0) === 0) return;
|
|
880
|
+
const slot = resolveSlotState(config, now, config.tz ?? "Asia/Shanghai");
|
|
881
|
+
const key = slotWinnerKey(slot.winner);
|
|
882
|
+
const previous = markers.get(owner);
|
|
883
|
+
if (previous !== void 0 && previous.key !== key) deps.log.info("llm-fallbacks: agent \"%s\" time-slot switch (分时切换): %s -> %s", owner, previous.label, slot.label);
|
|
884
|
+
markers.set(owner, {
|
|
885
|
+
key,
|
|
886
|
+
label: slot.label
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
function resolveWalk(deps, options) {
|
|
890
|
+
const config = deps.readConfig();
|
|
891
|
+
const now = new Date();
|
|
892
|
+
const view = deps.resolveAgentView(options.sessionId);
|
|
893
|
+
const owner = options.sessionId;
|
|
894
|
+
const seam = owner === void 0 ? void 0 : deps.seamRecord(owner);
|
|
895
|
+
const role = resolveDeclaredRole(agentLikeOf(view), config.roles.rules, deps.roleIds(), {
|
|
896
|
+
dispatchRole: seam?.role,
|
|
897
|
+
servedRoute: servedRouteOf(config, view?.optionsRoute, now),
|
|
898
|
+
warn: deps.log.warn
|
|
899
|
+
});
|
|
900
|
+
const origin = view?.origin ?? "root";
|
|
901
|
+
const rootTail = origin === "subagent" ? config.rootChain : resolveEffectiveChain(config, now, config.tz ?? "Asia/Shanghai");
|
|
902
|
+
const entries = roleChainEntries(config.roles.list, rootTail, role);
|
|
903
|
+
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);
|
|
904
|
+
if (origin === "root" && !isAllDayConforming(config.rootChain)) throw new LlmError("llm-fallbacks: the all-day chain is not conforming (exactly one official model required) — refusing to delegate", UNDISPATCHABLE_HEAD_CODE);
|
|
905
|
+
const policy = owner !== void 0 && seam?.virtualRouteInjected === true ? deps.policyFor(owner) : void 0;
|
|
906
|
+
if (policy?.state === "unprovable") throw new LlmError("llm-fallbacks: the subagent model-selection policy is unprovable — refusing to delegate the FallbacksChain call", UNDISPATCHABLE_HEAD_CODE);
|
|
907
|
+
const dispatchable = firstDispatchableExactHead(entries);
|
|
908
|
+
const allowedModels = policy?.state === "enabled" ? policy.allowedModels : void 0;
|
|
909
|
+
const head = allowedModels === void 0 ? dispatchable : firstDispatchableExactHead(entries, allowedModels);
|
|
910
|
+
if (head === void 0) throw new LlmError(dispatchable === void 0 ? "llm-fallbacks: the effective chain head cannot be dispatched (wildcard, malformed, or self-route) — refusing to delegate" : "llm-fallbacks: every dispatchable route of the effective chain is outside the subagent allowlist — refusing to delegate", UNDISPATCHABLE_HEAD_CODE);
|
|
911
|
+
const llm = deps.getLlm();
|
|
912
|
+
if (llm === void 0) throw new LlmError("llm-fallbacks: the llm runtime is unavailable — cannot delegate the FallbacksChain call", LLM_UNAVAILABLE_CODE);
|
|
913
|
+
return {
|
|
914
|
+
config,
|
|
915
|
+
now,
|
|
916
|
+
origin,
|
|
917
|
+
role,
|
|
918
|
+
rootTail,
|
|
919
|
+
head,
|
|
920
|
+
llm,
|
|
921
|
+
owner,
|
|
922
|
+
seam
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function walkChain(deps, options) {
|
|
926
|
+
return walk(deps, resolveWalk(deps, options), options);
|
|
927
|
+
}
|
|
928
|
+
async function* walk(deps, start, options) {
|
|
929
|
+
const { config, role, rootTail, head, llm, owner } = start;
|
|
930
|
+
logSlotRotation(deps, start);
|
|
931
|
+
const messages = restoreEnvelopeProvenance(options.messages);
|
|
932
|
+
const stateOf = () => owner === void 0 ? void 0 : deps.states.peek(owner);
|
|
933
|
+
const failedKeys = new Set();
|
|
934
|
+
const isCooled = (key) => {
|
|
935
|
+
const state = stateOf();
|
|
936
|
+
return state !== void 0 && deps.states.isSuppressed(state, key, Date.now(), config.recovery ?? "timer");
|
|
937
|
+
};
|
|
938
|
+
const coolRoute = (failed) => {
|
|
939
|
+
if (owner === void 0) return;
|
|
940
|
+
const state = deps.states.get(owner);
|
|
941
|
+
const key = selectorKey(failed.provider, failed.model);
|
|
942
|
+
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;
|
|
943
|
+
deps.states.suppress(state, key, until);
|
|
944
|
+
};
|
|
945
|
+
const bookFailure = (failed, code) => {
|
|
946
|
+
failedKeys.add(selectorKey(failed.provider, failed.model));
|
|
947
|
+
if (!REQUEST_SCOPED_CODES.has(code)) coolRoute(failed);
|
|
948
|
+
};
|
|
949
|
+
const advanceFrom = async (from, failureCode, blockedReason) => {
|
|
950
|
+
const { all, wildcard } = resolveChainViews(config.roles.list, rootTail, role, from.provider, from.model, deps.log.warn);
|
|
951
|
+
const own = all.map((candidate) => candidate.provider !== FALLBACKS_PROVIDER$1);
|
|
952
|
+
const candidates = all.filter((_, index) => own[index]);
|
|
953
|
+
const wildcards = wildcard.filter((_, index) => own[index]);
|
|
954
|
+
const exists = hasWildcardEntry(config.roles.list, rootTail, role) ? await makeModelExists(llm, [...new Set(candidates.map((candidate) => candidate.provider))]) : void 0;
|
|
955
|
+
const contextWindowOf = failureCode === "CONTEXT_WINDOW_EXCEEDED" ? await makeContextWindowOf(llm, [from, ...candidates.flatMap((candidate) => candidate.model === void 0 ? [] : [{
|
|
755
956
|
provider: candidate.provider,
|
|
756
957
|
model: candidate.model
|
|
958
|
+
}])]) : void 0;
|
|
959
|
+
const minContextWindow = contextWindowOf?.(from.provider, from.model);
|
|
960
|
+
const cooldown = { isSuppressed: (key) => isCooled(key) };
|
|
961
|
+
const failed = { has: (key) => failedKeys.has(key) };
|
|
962
|
+
const surviving = selectCandidates(candidates, wildcards, createCandidateFilter({
|
|
963
|
+
current: from,
|
|
964
|
+
cooldown,
|
|
965
|
+
failed,
|
|
966
|
+
minContextWindow,
|
|
967
|
+
contextWindowOf
|
|
968
|
+
}), exists);
|
|
969
|
+
const annotated = annotateCandidates(candidates, surviving, {
|
|
970
|
+
current: from,
|
|
971
|
+
cooldown,
|
|
972
|
+
failed,
|
|
973
|
+
minContextWindow,
|
|
974
|
+
contextWindowOf
|
|
757
975
|
});
|
|
976
|
+
const describeCandidates = (list) => list.map(({ candidate, skip }) => skip === void 0 ? `${candidate.provider}/${candidate.model}` : `${candidate.provider}/${candidate.model} (skipped: ${skip})`);
|
|
977
|
+
if (start.seam?.virtualRouteInjected === true && owner !== void 0) {
|
|
978
|
+
const policy = deps.policyFor(owner);
|
|
979
|
+
if (policy.state === "unprovable") {
|
|
980
|
+
deps.log.warn("llm-fallbacks: agent \"%s\" subagent model-selection policy is on but unreadable — no fallback switch (a cooled-head re-entry is refused)", owner);
|
|
981
|
+
return {
|
|
982
|
+
candidates: describeCandidates(annotated),
|
|
983
|
+
currentAdmitted: false
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
if (policy.state === "enabled") {
|
|
987
|
+
const currentAdmitted = routeOnAllowlist(from, policy.allowedModels);
|
|
988
|
+
const permitted = new Set(surviving.filter((candidate) => candidate.model !== void 0 && routeOnAllowlist({
|
|
989
|
+
provider: candidate.provider,
|
|
990
|
+
model: candidate.model
|
|
991
|
+
}, policy.allowedModels)).map((candidate) => selectorKey(candidate.provider, candidate.model)));
|
|
992
|
+
const labelled = annotated.map((entry) => entry.skip === void 0 && !permitted.has(selectorKey(entry.candidate.provider, entry.candidate.model)) ? {
|
|
993
|
+
candidate: entry.candidate,
|
|
994
|
+
skip: "allowlist"
|
|
995
|
+
} : entry);
|
|
996
|
+
const allowed = firstAllowedCandidate(resolvedRoutes(surviving), policy.allowedModels);
|
|
997
|
+
if (allowed === void 0) {
|
|
998
|
+
const wouldBe = surviving[0];
|
|
999
|
+
if (wouldBe?.model !== void 0) {
|
|
1000
|
+
deps.log.warn("llm-fallbacks: agent \"%s\" fallback switch %s/%s blocked: resolved candidates are all outside the subagent allowlist — no switch", owner, wouldBe.provider, wouldBe.model);
|
|
1001
|
+
if (blockedReason !== void 0) deps.recordBlocked?.(owner, {
|
|
1002
|
+
provider: wouldBe.provider,
|
|
1003
|
+
model: wouldBe.model
|
|
1004
|
+
}, blockedReason);
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
candidates: describeCandidates(labelled),
|
|
1008
|
+
currentAdmitted
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
return {
|
|
1012
|
+
target: allowed,
|
|
1013
|
+
candidates: describeCandidates(labelled),
|
|
1014
|
+
currentAdmitted
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const first = surviving[0];
|
|
1019
|
+
if (first?.model === void 0) return { candidates: describeCandidates(annotated) };
|
|
1020
|
+
return {
|
|
1021
|
+
target: {
|
|
1022
|
+
provider: first.provider,
|
|
1023
|
+
model: first.model
|
|
1024
|
+
},
|
|
1025
|
+
candidates: describeCandidates(annotated)
|
|
1026
|
+
};
|
|
1027
|
+
};
|
|
1028
|
+
let target;
|
|
1029
|
+
if (isCooled(selectorKey(head.provider, head.model))) {
|
|
1030
|
+
const advance = await advanceFrom(head);
|
|
1031
|
+
if (advance?.target !== void 0) target = advance.target;
|
|
1032
|
+
else if (advance?.currentAdmitted === false) throw new LlmError("llm-fallbacks: the subagent model-selection policy no longer admits the chain head — refusing to delegate the FallbacksChain call", UNDISPATCHABLE_HEAD_CODE);
|
|
1033
|
+
else target = head;
|
|
1034
|
+
} else target = head;
|
|
1035
|
+
let switches = 0;
|
|
1036
|
+
for (;;) {
|
|
1037
|
+
const key = selectorKey(target.provider, target.model);
|
|
1038
|
+
const state = stateOf();
|
|
1039
|
+
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 (role=%s, reason=%s)", owner ?? "", target.provider, target.model, role, "trigger-code");
|
|
1040
|
+
let committed = false;
|
|
1041
|
+
let swallowed;
|
|
1042
|
+
for await (const chunk of llm.stream({
|
|
1043
|
+
...options,
|
|
1044
|
+
provider: target.provider,
|
|
1045
|
+
model: target.model,
|
|
1046
|
+
messages
|
|
1047
|
+
})) {
|
|
1048
|
+
const failure = chunk.type === "finish" && chunk.reason.kind === "error" ? chunk.reason.failure : void 0;
|
|
1049
|
+
if (failure !== void 0 && !committed && config.triggerCodes.includes(failure.code)) {
|
|
1050
|
+
swallowed = {
|
|
1051
|
+
chunk,
|
|
1052
|
+
code: failure.code
|
|
1053
|
+
};
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
if (chunk.type === "finish") {
|
|
1057
|
+
if (failure === void 0) {
|
|
1058
|
+
if (chunk.reason.kind !== "aborted" && state !== void 0) deps.states.observeSuccess(state, key);
|
|
1059
|
+
} else if (config.triggerCodes.includes(failure.code)) bookFailure(target, failure.code);
|
|
1060
|
+
yield chunk;
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
yield chunk;
|
|
1064
|
+
committed = true;
|
|
1065
|
+
}
|
|
1066
|
+
if (swallowed === void 0) return;
|
|
1067
|
+
bookFailure(target, swallowed.code);
|
|
1068
|
+
const next = switches < config.maxSwitchesPerStep ? await advanceFrom(target, swallowed.code, "trigger-code") : void 0;
|
|
1069
|
+
if (next === void 0 || next.target === void 0) {
|
|
1070
|
+
yield swallowed.chunk;
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
switches += 1;
|
|
1074
|
+
deps.log.info("llm-fallbacks: agent \"%s\" fallback switch (降级切换) %s/%s -> %s/%s (role=%s, reason=%s, candidates=%o)", owner ?? "", target.provider, target.model, next.target.provider, next.target.model, role, "trigger-code", next.candidates);
|
|
1075
|
+
target = next.target;
|
|
758
1076
|
}
|
|
759
|
-
return routes;
|
|
760
|
-
}
|
|
761
|
-
function routeOnAllowlist(route, allowedModels) {
|
|
762
|
-
return allowedModels.some((allowed) => allowed.provider === route.provider && allowed.model === route.model);
|
|
763
|
-
}
|
|
764
|
-
function firstAllowedCandidate(resolved, allowedModels) {
|
|
765
|
-
return resolved.find((route) => routeOnAllowlist(route, allowedModels));
|
|
766
1077
|
}
|
|
767
1078
|
//#endregion
|
|
768
1079
|
//#region .build/host/subagent-policy.js
|
|
@@ -778,6 +1089,16 @@ function parseRoutes(value) {
|
|
|
778
1089
|
}
|
|
779
1090
|
return routes;
|
|
780
1091
|
}
|
|
1092
|
+
function policySettingsFrom(value) {
|
|
1093
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
1094
|
+
if (!("enabled" in value) || typeof value.enabled !== "boolean") return void 0;
|
|
1095
|
+
const routes = "allowedModels" in value ? parseRoutes(value.allowedModels) : void 0;
|
|
1096
|
+
if (routes === void 0) return void 0;
|
|
1097
|
+
return {
|
|
1098
|
+
enabled: value.enabled,
|
|
1099
|
+
allowedModels: routes
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
781
1102
|
function readSessionPolicyEvent(events) {
|
|
782
1103
|
const event = events.find((candidate) => candidate.type === "subagent/model-selection-policy");
|
|
783
1104
|
if (event === void 0) return {
|
|
@@ -1086,7 +1407,8 @@ function seamStateFor(root) {
|
|
|
1086
1407
|
if (existing !== void 0) return existing;
|
|
1087
1408
|
const created = {
|
|
1088
1409
|
records: new Map(),
|
|
1089
|
-
noticeEmitted: new Set()
|
|
1410
|
+
noticeEmitted: new Set(),
|
|
1411
|
+
fibers: 0
|
|
1090
1412
|
};
|
|
1091
1413
|
subagentSeamStates.set(root, created);
|
|
1092
1414
|
return created;
|
|
@@ -1105,6 +1427,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1105
1427
|
const pending = new Map();
|
|
1106
1428
|
const wrappers = new WeakMap();
|
|
1107
1429
|
const disposers = [];
|
|
1430
|
+
let disposed = false;
|
|
1108
1431
|
let unreadableServiceLogged = false;
|
|
1109
1432
|
let noRoleLogged = false;
|
|
1110
1433
|
const debug = (message) => {
|
|
@@ -1115,15 +1438,60 @@ function installSubagentSeam(ctx, options) {
|
|
|
1115
1438
|
const reportDegrade = (error) => {
|
|
1116
1439
|
debug(`llm-fallbacks: subagent role seam degraded to the native path (start unaffected): ${errorMessage$1(error)}`);
|
|
1117
1440
|
};
|
|
1118
|
-
const recordChild = (childId, role, at,
|
|
1441
|
+
const recordChild = (childId, role, at, verdict = {}) => {
|
|
1119
1442
|
const existing = records.get(childId);
|
|
1120
1443
|
records.set(childId, {
|
|
1121
1444
|
role,
|
|
1122
1445
|
at,
|
|
1123
1446
|
firstNoticePending: existing?.firstNoticePending ?? true,
|
|
1124
|
-
...existing?.personaNotApplied === true || personaMissed === true ? { personaNotApplied: true } : {}
|
|
1447
|
+
...existing?.personaNotApplied === true || verdict.personaMissed === true ? { personaNotApplied: true } : {},
|
|
1448
|
+
...existing?.virtualRouteInjected === true || verdict.virtualRouteInjected === true ? { virtualRouteInjected: true } : {}
|
|
1125
1449
|
});
|
|
1126
1450
|
};
|
|
1451
|
+
const explicitRoute = (request) => {
|
|
1452
|
+
const provider = request?.agentOptions?.provider;
|
|
1453
|
+
const model = request?.agentOptions?.model;
|
|
1454
|
+
return typeof provider === "string" && provider !== "" && typeof model === "string" && model !== "" ? {
|
|
1455
|
+
provider,
|
|
1456
|
+
model
|
|
1457
|
+
} : void 0;
|
|
1458
|
+
};
|
|
1459
|
+
const injectRoute = (spec, role, agentId) => {
|
|
1460
|
+
try {
|
|
1461
|
+
if (options.source === void 0) return void 0;
|
|
1462
|
+
const agentOptions = spec.request?.agentOptions;
|
|
1463
|
+
if (typeof agentOptions?.provider === "string" || typeof agentOptions?.model === "string") return void 0;
|
|
1464
|
+
const config = options.source();
|
|
1465
|
+
if (!isFallbackActive(config)) return void 0;
|
|
1466
|
+
const policy = options.policyFor?.(agentId);
|
|
1467
|
+
if (policy?.state === "unprovable") {
|
|
1468
|
+
debug(`llm-fallbacks: subagent seam — model-selection policy is unprovable for "${agentId}"; not mounting the virtual chain route`);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
const chain = roleChainEntries(config.roles.list, config.rootChain, role);
|
|
1472
|
+
const allowedModels = policy?.state === "enabled" ? policy.allowedModels : void 0;
|
|
1473
|
+
const dispatchable = firstDispatchableExactHead(chain);
|
|
1474
|
+
const head = allowedModels === void 0 ? dispatchable : firstDispatchableExactHead(chain, allowedModels);
|
|
1475
|
+
if (head === void 0) {
|
|
1476
|
+
if (allowedModels !== void 0 && dispatchable !== void 0) debug(`llm-fallbacks: subagent seam — no route of role "${role}" is on the subagent allowlist; not mounting the virtual chain route`);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
return {
|
|
1480
|
+
request: {
|
|
1481
|
+
...spec.request,
|
|
1482
|
+
agentOptions: {
|
|
1483
|
+
...agentOptions,
|
|
1484
|
+
provider: FALLBACKS_PROVIDER$1,
|
|
1485
|
+
model: FALLBACKS_CHAIN_MODEL
|
|
1486
|
+
}
|
|
1487
|
+
},
|
|
1488
|
+
head
|
|
1489
|
+
};
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
reportDegrade(error);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1127
1495
|
const beginCorrelation = (spec, role, at) => {
|
|
1128
1496
|
const parentSessionId = parentSessionIdOf(spec?.request);
|
|
1129
1497
|
const label = typeof spec?.label === "string" ? spec.label : void 0;
|
|
@@ -1218,11 +1586,26 @@ function installSubagentSeam(ctx, options) {
|
|
|
1218
1586
|
try {
|
|
1219
1587
|
if (isLocalBackend(service, spec.provider)) {
|
|
1220
1588
|
reservedId = spec.childId ?? randomUUID();
|
|
1221
|
-
|
|
1589
|
+
const authorized = explicitRoute(spec.request);
|
|
1590
|
+
const injected = injectRoute(spec, role, reservedId);
|
|
1591
|
+
effectiveSpec = injected === void 0 ? reservedId === spec.childId ? spec : {
|
|
1222
1592
|
...spec,
|
|
1223
1593
|
childId: reservedId
|
|
1594
|
+
} : {
|
|
1595
|
+
...spec,
|
|
1596
|
+
childId: reservedId,
|
|
1597
|
+
request: injected.request
|
|
1224
1598
|
};
|
|
1225
|
-
recordChild(reservedId, role, at);
|
|
1599
|
+
recordChild(reservedId, role, at, { virtualRouteInjected: injected !== void 0 });
|
|
1600
|
+
const head = injected?.head ?? authorized;
|
|
1601
|
+
if (head !== void 0) options.chainHeads?.set(reservedId, {
|
|
1602
|
+
at,
|
|
1603
|
+
route: {
|
|
1604
|
+
provider: head.provider,
|
|
1605
|
+
model: head.model
|
|
1606
|
+
},
|
|
1607
|
+
source: injected === void 0 ? "authorized" : "injected"
|
|
1608
|
+
});
|
|
1226
1609
|
}
|
|
1227
1610
|
} catch (error) {
|
|
1228
1611
|
if (reservedId !== void 0) records.delete(reservedId);
|
|
@@ -1252,7 +1635,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1252
1635
|
try {
|
|
1253
1636
|
observeStartResult(result, (childId) => {
|
|
1254
1637
|
endCorrelation(pendingKey);
|
|
1255
|
-
recordChild(childId, role, at, reservationPersonaMissed);
|
|
1638
|
+
recordChild(childId, role, at, { personaMissed: reservationPersonaMissed });
|
|
1256
1639
|
}, () => {}, () => {
|
|
1257
1640
|
endCorrelation(pendingKey);
|
|
1258
1641
|
if (reservedId !== void 0) records.delete(reservedId);
|
|
@@ -1266,8 +1649,19 @@ function installSubagentSeam(ctx, options) {
|
|
|
1266
1649
|
wrappers.set(value, wrapper);
|
|
1267
1650
|
return wrapper;
|
|
1268
1651
|
};
|
|
1652
|
+
const installSurface = (register) => {
|
|
1653
|
+
try {
|
|
1654
|
+
disposers.push(register());
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
for (const withdraw of disposers.splice(0)) try {
|
|
1657
|
+
withdraw();
|
|
1658
|
+
} catch {}
|
|
1659
|
+
if (ownsWrapper) subagentSeamRoots.delete(root);
|
|
1660
|
+
throw error;
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1269
1663
|
if (ownsWrapper) {
|
|
1270
|
-
|
|
1664
|
+
installSurface(() => ctx.on(SUBAGENT_SEAM_EVENT, (_readCtx, name, _error, next) => {
|
|
1271
1665
|
const value = next();
|
|
1272
1666
|
if (name !== "subagents") return value;
|
|
1273
1667
|
try {
|
|
@@ -1277,7 +1671,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1277
1671
|
return value;
|
|
1278
1672
|
}
|
|
1279
1673
|
}));
|
|
1280
|
-
|
|
1674
|
+
installSurface(() => ctx.on("session/event", (session, event) => {
|
|
1281
1675
|
try {
|
|
1282
1676
|
if (event.type !== SUBAGENT_CATALOG_EVENT) return;
|
|
1283
1677
|
const data = event.data;
|
|
@@ -1295,21 +1689,39 @@ function installSubagentSeam(ctx, options) {
|
|
|
1295
1689
|
}
|
|
1296
1690
|
}));
|
|
1297
1691
|
}
|
|
1298
|
-
|
|
1692
|
+
installSurface(() => installRoleNotice(ctx, {
|
|
1299
1693
|
records,
|
|
1300
1694
|
emitted: noticeEmitted,
|
|
1301
1695
|
debug
|
|
1302
1696
|
}));
|
|
1303
|
-
|
|
1697
|
+
installSurface(() => installRoleProjection(ctx, { debug }));
|
|
1698
|
+
state.fibers += 1;
|
|
1699
|
+
const dispose = () => {
|
|
1700
|
+
if (disposed) return;
|
|
1701
|
+
disposed = true;
|
|
1702
|
+
if (ownsWrapper) subagentSeamRoots.delete(root);
|
|
1703
|
+
try {
|
|
1704
|
+
for (const withdraw of disposers.splice(0)) withdraw();
|
|
1705
|
+
} finally {
|
|
1706
|
+
state.fibers -= 1;
|
|
1707
|
+
if (state.fibers === 0) {
|
|
1708
|
+
state.records.clear();
|
|
1709
|
+
state.noticeEmitted.clear();
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1304
1713
|
const seam = {
|
|
1305
1714
|
records,
|
|
1306
1715
|
noticeEmitted,
|
|
1307
1716
|
ownsWrapper,
|
|
1308
|
-
dispose
|
|
1309
|
-
if (ownsWrapper) subagentSeamRoots.delete(root);
|
|
1310
|
-
for (const dispose of disposers.splice(0)) dispose();
|
|
1311
|
-
}
|
|
1717
|
+
dispose
|
|
1312
1718
|
};
|
|
1719
|
+
try {
|
|
1720
|
+
ctx.effect(() => () => dispose(), "llm-fallbacks: subagent seam teardown");
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
dispose();
|
|
1723
|
+
throw error;
|
|
1724
|
+
}
|
|
1313
1725
|
subagentSeamStores.set(ctx, seam);
|
|
1314
1726
|
return seam;
|
|
1315
1727
|
}
|
|
@@ -1440,21 +1852,6 @@ var CooldownStore = class {
|
|
|
1440
1852
|
return active;
|
|
1441
1853
|
}
|
|
1442
1854
|
};
|
|
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
1855
|
//#endregion
|
|
1459
1856
|
//#region .build/host/state.js
|
|
1460
1857
|
var FallbackStateStore = class {
|
|
@@ -1472,12 +1869,6 @@ var FallbackStateStore = class {
|
|
|
1472
1869
|
let state = this.states.get(agentId);
|
|
1473
1870
|
if (state === void 0) {
|
|
1474
1871
|
state = {
|
|
1475
|
-
stepFailures: {
|
|
1476
|
-
turn: 0,
|
|
1477
|
-
step: 0,
|
|
1478
|
-
failed: new StepFailureSet(),
|
|
1479
|
-
switchCount: 0
|
|
1480
|
-
},
|
|
1481
1872
|
cooldown: new CooldownStore(),
|
|
1482
1873
|
recovery: new RecoveryStore()
|
|
1483
1874
|
};
|
|
@@ -1491,42 +1882,6 @@ var FallbackStateStore = class {
|
|
|
1491
1882
|
clear() {
|
|
1492
1883
|
this.states.clear();
|
|
1493
1884
|
}
|
|
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
1885
|
suppress(state, key, untilEpochMs) {
|
|
1531
1886
|
state.cooldown.suppress(key, untilEpochMs);
|
|
1532
1887
|
}
|
|
@@ -1549,27 +1904,6 @@ var FallbackStateStore = class {
|
|
|
1549
1904
|
}
|
|
1550
1905
|
};
|
|
1551
1906
|
//#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
1907
|
//#region .build/host/gateway.js
|
|
1574
1908
|
const FALLBACKS_PROFILE_ENTRY = "llm-fallbacks";
|
|
1575
1909
|
const SUBAGENT_POLICY_SETTINGS_NS = "subagent-model-selection-settings";
|
|
@@ -1584,9 +1918,7 @@ const CONFIG_KEYS = {
|
|
|
1584
1918
|
cooldownMs: true,
|
|
1585
1919
|
revertPolicy: true,
|
|
1586
1920
|
maxSwitchesPerStep: true,
|
|
1587
|
-
alwaysModeRetryCap: true,
|
|
1588
1921
|
presets: true,
|
|
1589
|
-
roleAutoMatch: true,
|
|
1590
1922
|
timeSlots: true,
|
|
1591
1923
|
tz: true,
|
|
1592
1924
|
recovery: true
|
|
@@ -1940,10 +2272,8 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
1940
2272
|
configRules: "角色规则",
|
|
1941
2273
|
configCooldown: "冷却",
|
|
1942
2274
|
configRevert: "回主策略",
|
|
1943
|
-
configMaxSwitches: "
|
|
1944
|
-
configAlwaysCap: "always 上限",
|
|
2275
|
+
configMaxSwitches: "单次请求行走最大切换",
|
|
1945
2276
|
configPresets: "预置",
|
|
1946
|
-
configRoleAutoMatch: "角色自动匹配",
|
|
1947
2277
|
configEdit: "编辑:/settings(TUI 设置界面)或 ~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)/ $DSH_HOME/settings.yaml(fallbacks: 分节)",
|
|
1948
2278
|
configEditHint: "TUI 通过 /settings 修改配置;文件编辑仍然可用",
|
|
1949
2279
|
revertSeedOk: "角色 {id} 已还原为 Seed 默认",
|
|
@@ -1995,10 +2325,8 @@ const FALLBACKS_COMMAND_LOCALES = {
|
|
|
1995
2325
|
configRules: "Rules",
|
|
1996
2326
|
configCooldown: "Cooldown",
|
|
1997
2327
|
configRevert: "Revert",
|
|
1998
|
-
configMaxSwitches: "Max switches
|
|
1999
|
-
configAlwaysCap: "Always-mode cap",
|
|
2328
|
+
configMaxSwitches: "Max switches per request walk",
|
|
2000
2329
|
configPresets: "Presets",
|
|
2001
|
-
configRoleAutoMatch: "Auto-match",
|
|
2002
2330
|
configEdit: "Edit: /settings (TUI settings screen) or ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) / $DSH_HOME/settings.yaml (fallbacks: section)",
|
|
2003
2331
|
configEditHint: "TUI edits config via /settings; file editing still works",
|
|
2004
2332
|
revertSeedOk: "role {id} reverted to its seed default",
|
|
@@ -2149,9 +2477,7 @@ function fallbacksConfigText(summary, locale = "zh") {
|
|
|
2149
2477
|
`${t.configCooldown}: ${summary.cooldownMs} ms`,
|
|
2150
2478
|
`${t.configRevert}: ${summary.revertPolicy}`,
|
|
2151
2479
|
`${t.configMaxSwitches}: ${summary.maxSwitchesPerStep}`,
|
|
2152
|
-
`${t.configAlwaysCap}: ${summary.alwaysModeRetryCap}`,
|
|
2153
2480
|
`${t.configPresets}: ${summary.presets}`,
|
|
2154
|
-
`${t.configRoleAutoMatch}: ${summary.roleAutoMatch ? t.configEnabled : t.configDisabled}`,
|
|
2155
2481
|
"",
|
|
2156
2482
|
t.configEdit,
|
|
2157
2483
|
t.configEditHint
|
|
@@ -2508,7 +2834,7 @@ function fallbacksChildren(canonicalPath) {
|
|
|
2508
2834
|
if (canonicalPath.length === 2 && canonicalPath[1] === "config") return [REVERT_SEED_NODE];
|
|
2509
2835
|
return [];
|
|
2510
2836
|
}
|
|
2511
|
-
const FALLBACKS_PROVIDER
|
|
2837
|
+
const FALLBACKS_PROVIDER = {
|
|
2512
2838
|
root: FALLBACKS_TUI_ROOT,
|
|
2513
2839
|
descriptions: {
|
|
2514
2840
|
zh: FALLBACKS_COMMAND_LOCALES.zh.description,
|
|
@@ -2522,7 +2848,7 @@ function installTuiClient(ctx, opts) {
|
|
|
2522
2848
|
const trees = tctx.tuiCommandTrees;
|
|
2523
2849
|
if (trees === void 0) return;
|
|
2524
2850
|
try {
|
|
2525
|
-
return trees.register(FALLBACKS_PROVIDER
|
|
2851
|
+
return trees.register(FALLBACKS_PROVIDER);
|
|
2526
2852
|
} catch (error) {
|
|
2527
2853
|
if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
|
|
2528
2854
|
tctx.logger("llm-fallbacks").debug("llm-fallbacks: tui command tree already registered — no provider on this fiber");
|
|
@@ -2593,8 +2919,8 @@ function buildFallbacksTuiSection() {
|
|
|
2593
2919
|
ns: FALLBACKS_TUI_SECTION_NS,
|
|
2594
2920
|
title: "fallbacks",
|
|
2595
2921
|
descriptions: {
|
|
2596
|
-
zh: "回退设置:降级链、分时切换、角色与高级选项(与 Web
|
|
2597
|
-
en: "Fallback settings: degradation chains, time slots, roles, and advanced options (full parity with the web settings card)."
|
|
2922
|
+
zh: "回退设置:降级链、分时切换、角色与高级选项(与 Web 设置卡片完全一致)。仅选中 FallbacksChain/Auto 的会话会执行降级链;选择真实模型的会话没有插件降级。",
|
|
2923
|
+
en: "Fallback settings: degradation chains, time slots, roles, and advanced options (full parity with the web settings card). Only sessions on FallbacksChain/Auto walk a chain — a real-model selection gets no plugin fallback."
|
|
2598
2924
|
},
|
|
2599
2925
|
groups: [
|
|
2600
2926
|
{
|
|
@@ -2631,16 +2957,6 @@ function buildFallbacksTuiSection() {
|
|
|
2631
2957
|
}
|
|
2632
2958
|
],
|
|
2633
2959
|
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
2960
|
{
|
|
2645
2961
|
path: ["presets"],
|
|
2646
2962
|
label: "Preset roles",
|
|
@@ -2760,277 +3076,70 @@ function buildFallbacksTuiSection() {
|
|
|
2760
3076
|
{
|
|
2761
3077
|
path: ["cooldownMs"],
|
|
2762
3078
|
label: "Cooldown (ms)",
|
|
2763
|
-
descriptions: {
|
|
2764
|
-
zh: "冷却时间(毫秒)",
|
|
2765
|
-
en: "Cooldown (ms)"
|
|
2766
|
-
},
|
|
2767
|
-
group: "advanced",
|
|
2768
|
-
kind: "number"
|
|
2769
|
-
},
|
|
2770
|
-
{
|
|
2771
|
-
path: ["maxSwitchesPerStep"],
|
|
2772
|
-
label: "Max switches per
|
|
2773
|
-
descriptions: {
|
|
2774
|
-
zh: "
|
|
2775
|
-
en: "Max switches per
|
|
2776
|
-
},
|
|
2777
|
-
group: "advanced",
|
|
2778
|
-
kind: "number"
|
|
2779
|
-
},
|
|
2780
|
-
{
|
|
2781
|
-
path: ["
|
|
2782
|
-
label: "
|
|
2783
|
-
descriptions: {
|
|
2784
|
-
zh: "
|
|
2785
|
-
en: "
|
|
2786
|
-
},
|
|
2787
|
-
group: "advanced",
|
|
2788
|
-
kind: "
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
},
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
en: "Cooldown expiry"
|
|
2805
|
-
}
|
|
2806
|
-
}, {
|
|
2807
|
-
value: "never",
|
|
2808
|
-
label: "Never",
|
|
2809
|
-
descriptions: {
|
|
2810
|
-
zh: "从不",
|
|
2811
|
-
en: "Never"
|
|
2812
|
-
}
|
|
2813
|
-
}]
|
|
2814
|
-
}
|
|
2815
|
-
]
|
|
2816
|
-
};
|
|
2817
|
-
}
|
|
2818
|
-
function installTuiSettingsSection(ctx, opts) {
|
|
2819
|
-
if (!opts.serviceOwned) return;
|
|
2820
|
-
ctx.inject(["tuiSettingsSections"], (tctx) => {
|
|
2821
|
-
const registry = tctx.tuiSettingsSections;
|
|
2822
|
-
if (registry === void 0) return;
|
|
2823
|
-
try {
|
|
2824
|
-
return registry.register(buildFallbacksTuiSection());
|
|
2825
|
-
} catch (error) {
|
|
2826
|
-
if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
|
|
2827
|
-
tctx.logger("llm-fallbacks").debug("llm-fallbacks: tui settings section already registered — no section on this fiber");
|
|
2828
|
-
return () => {};
|
|
2829
|
-
}
|
|
2830
|
-
});
|
|
2831
|
-
}
|
|
2832
|
-
//#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
|
|
3079
|
+
descriptions: {
|
|
3080
|
+
zh: "冷却时间(毫秒)",
|
|
3081
|
+
en: "Cooldown (ms)"
|
|
3082
|
+
},
|
|
3083
|
+
group: "advanced",
|
|
3084
|
+
kind: "number"
|
|
3085
|
+
},
|
|
3086
|
+
{
|
|
3087
|
+
path: ["maxSwitchesPerStep"],
|
|
3088
|
+
label: "Max switches per request walk",
|
|
3089
|
+
descriptions: {
|
|
3090
|
+
zh: "单次请求行走最大切换",
|
|
3091
|
+
en: "Max switches per request walk"
|
|
3092
|
+
},
|
|
3093
|
+
group: "advanced",
|
|
3094
|
+
kind: "number"
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
path: ["revertPolicy"],
|
|
3098
|
+
label: "Revert policy",
|
|
3099
|
+
descriptions: {
|
|
3100
|
+
zh: "恢复策略",
|
|
3101
|
+
en: "Revert policy"
|
|
3102
|
+
},
|
|
3103
|
+
group: "advanced",
|
|
3104
|
+
kind: "select",
|
|
3105
|
+
options: [{
|
|
3106
|
+
value: "cooldown-expiry",
|
|
3107
|
+
label: "Cooldown expiry",
|
|
3108
|
+
descriptions: {
|
|
3109
|
+
zh: "冷却到期",
|
|
3110
|
+
en: "Cooldown expiry"
|
|
3111
|
+
}
|
|
3112
|
+
}, {
|
|
3113
|
+
value: "never",
|
|
3114
|
+
label: "Never",
|
|
3115
|
+
descriptions: {
|
|
3116
|
+
zh: "从不",
|
|
3117
|
+
en: "Never"
|
|
3118
|
+
}
|
|
3119
|
+
}]
|
|
2904
3120
|
}
|
|
2905
|
-
|
|
2906
|
-
}
|
|
2907
|
-
return changed ? restored : messages;
|
|
3121
|
+
]
|
|
3122
|
+
};
|
|
2908
3123
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
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) {
|
|
3124
|
+
function installTuiSettingsSection(ctx, opts) {
|
|
3125
|
+
if (!opts.serviceOwned) return;
|
|
3126
|
+
ctx.inject(["tuiSettingsSections"], (tctx) => {
|
|
3127
|
+
const registry = tctx.tuiSettingsSections;
|
|
3128
|
+
if (registry === void 0) return;
|
|
2966
3129
|
try {
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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");
|
|
3130
|
+
return registry.register(buildFallbacksTuiSection());
|
|
3131
|
+
} catch (error) {
|
|
3132
|
+
if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
|
|
3133
|
+
tctx.logger("llm-fallbacks").debug("llm-fallbacks: tui settings section already registered — no section on this fiber");
|
|
3134
|
+
return () => {};
|
|
3013
3135
|
}
|
|
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
3136
|
});
|
|
3025
|
-
return reconcile;
|
|
3026
3137
|
}
|
|
3027
3138
|
//#endregion
|
|
3028
3139
|
//#region .build/host/index.js
|
|
3029
3140
|
const name = "llm-fallbacks";
|
|
3030
3141
|
const provide = ["llm-fallbacks"];
|
|
3031
3142
|
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
3143
|
const stateStores = new WeakMap();
|
|
3035
3144
|
function stateStore(ctx) {
|
|
3036
3145
|
return stateStores.get(ctx);
|
|
@@ -3048,95 +3157,38 @@ function latestByAt(map) {
|
|
|
3048
3157
|
for (const value of map.values()) if (latest === void 0 || value.at >= latest.at) latest = value;
|
|
3049
3158
|
return latest;
|
|
3050
3159
|
}
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
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
|
|
3160
|
+
function agentChainView(ctx, sessionId) {
|
|
3161
|
+
if (sessionId === void 0) return void 0;
|
|
3162
|
+
const agent = ctx.get("agents")?.get(sessionId);
|
|
3163
|
+
if (agent === void 0) return void 0;
|
|
3164
|
+
const header = agent.session?.header;
|
|
3165
|
+
const route = agent.options;
|
|
3166
|
+
return {
|
|
3167
|
+
origin: header?.origin ?? "root",
|
|
3168
|
+
...header?.agentPreset === void 0 ? {} : { agentPreset: header.agentPreset },
|
|
3169
|
+
...typeof route?.provider === "string" && typeof route?.model === "string" ? { optionsRoute: {
|
|
3170
|
+
provider: route.provider,
|
|
3171
|
+
model: route.model
|
|
3172
|
+
} } : {},
|
|
3173
|
+
sessionId
|
|
3103
3174
|
};
|
|
3104
3175
|
}
|
|
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
3176
|
function readSubagentSettings(ctx) {
|
|
3129
|
-
const
|
|
3130
|
-
|
|
3177
|
+
const settings = ctx.get("settings");
|
|
3178
|
+
if (settings === void 0) return void 0;
|
|
3179
|
+
const descriptor = settings.describe().find((d) => d.ns === SUBAGENT_POLICY_SETTINGS_NS);
|
|
3180
|
+
if (descriptor === void 0) return void 0;
|
|
3181
|
+
const policy = policySettingsFrom(descriptor.value);
|
|
3182
|
+
if (policy === void 0) throw new Error("subagent model-selection settings are malformed");
|
|
3183
|
+
return policy;
|
|
3131
3184
|
}
|
|
3132
3185
|
function readGuardedPolicySettings(ctx, agentId, lastKnown, warn) {
|
|
3133
3186
|
try {
|
|
3134
|
-
const
|
|
3135
|
-
if (
|
|
3187
|
+
const settings = readSubagentSettings(ctx);
|
|
3188
|
+
if (settings === void 0) return {
|
|
3136
3189
|
ok: true,
|
|
3137
3190
|
settings: lastKnown.get(agentId)
|
|
3138
3191
|
};
|
|
3139
|
-
const settings = service.current();
|
|
3140
3192
|
lastKnown.set(agentId, settings);
|
|
3141
3193
|
return {
|
|
3142
3194
|
ok: true,
|
|
@@ -3151,22 +3203,6 @@ function readGuardedPolicySettings(ctx, agentId, lastKnown, warn) {
|
|
|
3151
3203
|
};
|
|
3152
3204
|
}
|
|
3153
3205
|
}
|
|
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
3206
|
function isLiveConfigRef(value) {
|
|
3171
3207
|
return typeof value === "object" && value !== null && typeof value.get === "function";
|
|
3172
3208
|
}
|
|
@@ -3231,12 +3267,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3231
3267
|
if (settingsComposed !== void 0) return settingsComposed;
|
|
3232
3268
|
return entry;
|
|
3233
3269
|
};
|
|
3234
|
-
const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source());
|
|
3235
3270
|
validateFallbacksConfig(entry, logger);
|
|
3236
3271
|
const legacyKeys = detectLegacyKeys(source());
|
|
3237
3272
|
if (legacyKeys.length > 0) logger.warn("llm-fallbacks: legacy config keys detected (chains/roles.default/undeclared role refs); see docs/configuration.md migration table — %o", legacyKeys);
|
|
3238
3273
|
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
3274
|
ctx.inject(["settings"], (sctx) => {
|
|
3241
3275
|
const settings = sctx.settings;
|
|
3242
3276
|
const refreshComposed = () => {
|
|
@@ -3249,7 +3283,6 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3249
3283
|
if (!isLiveConfigRef(config)) refreshComposed();
|
|
3250
3284
|
const current = source();
|
|
3251
3285
|
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
3286
|
reconcileFallbacksAdapter();
|
|
3254
3287
|
slotWinners.clear();
|
|
3255
3288
|
});
|
|
@@ -3263,9 +3296,18 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3263
3296
|
chainHeadStores.set(ctx, chainHeadMap);
|
|
3264
3297
|
const blockedAttemptMap = new Map();
|
|
3265
3298
|
blockedAttemptStores.set(ctx, blockedAttemptMap);
|
|
3299
|
+
const lastKnownPolicySettings = new Map();
|
|
3300
|
+
const policyFor = (sessionId) => {
|
|
3301
|
+
const settingsRead = readGuardedPolicySettings(ctx, sessionId, lastKnownPolicySettings, logger.warn);
|
|
3302
|
+
if (!settingsRead.ok) return { state: "unprovable" };
|
|
3303
|
+
return effectivePolicy(readSessionPolicyEvent(ctx.get("agents")?.get(sessionId)?.session.snapshotEvents() ?? []), settingsRead.settings);
|
|
3304
|
+
};
|
|
3266
3305
|
const subagentSeam = installSubagentSeam(ctx, {
|
|
3267
3306
|
roleIds: () => roleIds,
|
|
3268
3307
|
roles: () => source().roles.list,
|
|
3308
|
+
source: () => source(),
|
|
3309
|
+
policyFor,
|
|
3310
|
+
chainHeads: chainHeadMap,
|
|
3269
3311
|
debug: (message) => logger.debug(message)
|
|
3270
3312
|
});
|
|
3271
3313
|
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)");
|
|
@@ -3304,275 +3346,45 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3304
3346
|
});
|
|
3305
3347
|
const states = new FallbackStateStore();
|
|
3306
3348
|
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
3349
|
const slotWinners = new Map();
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
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
|
|
3350
|
+
const walkDeps = {
|
|
3351
|
+
readConfig: () => source(),
|
|
3352
|
+
getLlm: () => ctx.get("llm"),
|
|
3353
|
+
resolveAgentView: (sessionId) => agentChainView(ctx, sessionId),
|
|
3354
|
+
states,
|
|
3355
|
+
roleIds: () => roleIds,
|
|
3356
|
+
seamRecord: (sessionId) => subagentSeam.records.get(sessionId),
|
|
3357
|
+
policyFor,
|
|
3358
|
+
recordBlocked: (sessionId, route, reason) => {
|
|
3359
|
+
blockedAttemptMap.set(sessionId, {
|
|
3360
|
+
at: Date.now(),
|
|
3361
|
+
route,
|
|
3362
|
+
reason
|
|
3452
3363
|
});
|
|
3453
|
-
}
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
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
|
-
});
|
|
3364
|
+
},
|
|
3365
|
+
slotWinners,
|
|
3366
|
+
log: logger
|
|
3367
|
+
};
|
|
3368
|
+
const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source(), (options) => walkChain(walkDeps, options));
|
|
3535
3369
|
ctx.on("agent/disposed", ({ agent }) => {
|
|
3536
3370
|
states.delete(agent.id);
|
|
3537
|
-
dispatchInjected.delete(agent.id);
|
|
3538
|
-
dispatchRoles.delete(agent.id);
|
|
3539
3371
|
slotWinners.delete(agent.id);
|
|
3540
3372
|
blockedAttemptMap.delete(agent.id);
|
|
3541
3373
|
chainHeadMap.delete(agent.id);
|
|
3542
3374
|
subagentSeam.records.delete(agent.id);
|
|
3543
3375
|
lastKnownPolicySettings.delete(agent.id);
|
|
3544
3376
|
});
|
|
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
|
-
});
|
|
3560
3377
|
ctx.effect(() => () => {
|
|
3561
3378
|
states.clear();
|
|
3562
|
-
dispatchInjected.clear();
|
|
3563
|
-
dispatchRoles.clear();
|
|
3564
3379
|
slotWinners.clear();
|
|
3565
3380
|
blockedAttemptMap.clear();
|
|
3566
3381
|
chainHeadMap.clear();
|
|
3567
|
-
subagentSeam.records.clear();
|
|
3568
|
-
subagentSeam.noticeEmitted.clear();
|
|
3569
|
-
subagentSeam.dispose();
|
|
3570
3382
|
lastKnownPolicySettings.clear();
|
|
3571
3383
|
}, "llm-fallbacks: clear per-agent state");
|
|
3572
3384
|
const fallbacksCommandController = {
|
|
3573
3385
|
getSnapshot(agent) {
|
|
3574
3386
|
const config = source();
|
|
3575
|
-
const role =
|
|
3387
|
+
const role = subagentSeam.records.get(agent.id)?.role ?? resolveRole(agent, config.roles.rules, roleIds, logger.warn, servedRouteOf(config, agent.options, new Date()));
|
|
3576
3388
|
const state = states.peek(agent.id);
|
|
3577
3389
|
const recovery = config.recovery ?? "timer";
|
|
3578
3390
|
if (state !== void 0 && recovery === "half-open") states.syncRecovery(state, Date.now(), recovery);
|
|
@@ -3617,9 +3429,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3617
3429
|
cooldownMs: config.cooldownMs,
|
|
3618
3430
|
revertPolicy: config.revertPolicy,
|
|
3619
3431
|
maxSwitchesPerStep: config.maxSwitchesPerStep,
|
|
3620
|
-
|
|
3621
|
-
presets: config.presets ?? "bundled",
|
|
3622
|
-
roleAutoMatch: config.roleAutoMatch ?? true
|
|
3432
|
+
presets: config.presets ?? "bundled"
|
|
3623
3433
|
};
|
|
3624
3434
|
},
|
|
3625
3435
|
revertSeed: async (roleId) => {
|
|
@@ -3645,4 +3455,4 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3645
3455
|
});
|
|
3646
3456
|
}
|
|
3647
3457
|
//#endregion
|
|
3648
|
-
export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, ROLE_PROJECTION_KEY, SelectorError, annotateCandidates, apply, blockedAttempts, chainHeads,
|
|
3458
|
+
export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, ROLE_PROJECTION_KEY, SelectorError, annotateCandidates, apply, blockedAttempts, chainHeads, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, readRoleProjectionValue, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };
|