@blockrun/franklin 3.15.6 → 3.15.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/loop.js +41 -2
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -22,7 +22,7 @@ import { appendAudit, extractLastUserPrompt } from '../stats/audit.js';
|
|
|
22
22
|
import { estimateCost, OPUS_PRICING } from '../pricing.js';
|
|
23
23
|
import { maybeMidSessionExtract } from '../learnings/extractor.js';
|
|
24
24
|
import { extractMentions, buildEntityContext, loadEntities } from '../brain/store.js';
|
|
25
|
-
import { routeRequestAsync, resolveTierToModel, parseRoutingProfile } from '../router/index.js';
|
|
25
|
+
import { routeRequestAsync, resolveTierToModel, parseRoutingProfile, getFallbackChain } from '../router/index.js';
|
|
26
26
|
import { recordOutcome } from '../router/local-elo.js';
|
|
27
27
|
import { shouldPlan, getPlanningPrompt, getExecutorModel, isExecutorStuck, toolCallSignature } from './planner.js';
|
|
28
28
|
import { shouldVerify, runVerification } from './verification.js';
|
|
@@ -505,6 +505,11 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
505
505
|
let recoveryAttempts = 0;
|
|
506
506
|
let autoContinuationCount = 0;
|
|
507
507
|
const MAX_RECOVERY_ATTEMPTS = 5;
|
|
508
|
+
// Track per-model server-error streak so we can break out of a stuck
|
|
509
|
+
// upstream and try the next model in the routing fallback chain instead
|
|
510
|
+
// of burning all MAX_RECOVERY_ATTEMPTS retries on the same failure.
|
|
511
|
+
const serverErrorsByModel = new Map();
|
|
512
|
+
const SERVER_ERROR_STREAK_BEFORE_SWITCH = 2;
|
|
508
513
|
let compactFailures = 0;
|
|
509
514
|
let maxTokensOverride;
|
|
510
515
|
const turnIdleReference = lastSessionActivity;
|
|
@@ -993,14 +998,48 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
993
998
|
}
|
|
994
999
|
}
|
|
995
1000
|
if (classified.isTransient && recoveryAttempts < effectiveMaxRetries) {
|
|
1001
|
+
// Server-error streak guard: if the same model 5xx's twice in a row
|
|
1002
|
+
// it's almost always an upstream incident, not a blip. Switch to
|
|
1003
|
+
// the next routing fallback instead of waiting out 5 backoffs on a
|
|
1004
|
+
// dead provider — same idea as the payment-failure auto-fallback
|
|
1005
|
+
// below, but for transient server errors. Skipped for non-server
|
|
1006
|
+
// transients (rate limits, network blips) where retry is the right
|
|
1007
|
+
// call. Also skipped when the user picked a concrete model — they
|
|
1008
|
+
// explicitly chose this one, so we shouldn't silently swap.
|
|
1009
|
+
if (classified.category === 'server' && parseRoutingProfile(config.model)) {
|
|
1010
|
+
const streak = (serverErrorsByModel.get(resolvedModel) ?? 0) + 1;
|
|
1011
|
+
serverErrorsByModel.set(resolvedModel, streak);
|
|
1012
|
+
if (streak >= SERVER_ERROR_STREAK_BEFORE_SWITCH) {
|
|
1013
|
+
const fallbackChain = getFallbackChain(routingTier ?? 'MEDIUM', parseRoutingProfile(config.model) ?? 'auto');
|
|
1014
|
+
const nextModel = fallbackChain.find(m => m !== resolvedModel && (serverErrorsByModel.get(m) ?? 0) < SERVER_ERROR_STREAK_BEFORE_SWITCH);
|
|
1015
|
+
if (nextModel) {
|
|
1016
|
+
config.model = nextModel;
|
|
1017
|
+
config.onModelChange?.(nextModel, 'system');
|
|
1018
|
+
recoveryAttempts = 0;
|
|
1019
|
+
onEvent({
|
|
1020
|
+
kind: 'text_delta',
|
|
1021
|
+
text: `\n*${resolvedModel} keeps 5xx'ing (${streak} in a row) — switching to ${nextModel}*\n`,
|
|
1022
|
+
});
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
// No alternative left in the fallback chain — fall through to
|
|
1026
|
+
// the normal retry path so we at least exhaust attempts before
|
|
1027
|
+
// surrender.
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
996
1030
|
recoveryAttempts++;
|
|
997
1031
|
const backoffMs = getBackoffDelay(recoveryAttempts);
|
|
998
1032
|
if (config.debug) {
|
|
999
1033
|
console.error(`[franklin] ${classified.label} error — retrying in ${(backoffMs / 1000).toFixed(1)}s (attempt ${recoveryAttempts}/${effectiveMaxRetries}): ${errMsg.slice(0, 100)}`);
|
|
1000
1034
|
}
|
|
1035
|
+
// Surface the actual error + model so the user can see which model
|
|
1036
|
+
// is failing and what the upstream said. Old "Retrying after Server
|
|
1037
|
+
// error" was uninformative — users couldn't tell whether to wait,
|
|
1038
|
+
// /retry, or /model-switch.
|
|
1039
|
+
const errSnippet = errMsg.replace(/\s+/g, ' ').slice(0, 100);
|
|
1001
1040
|
onEvent({
|
|
1002
1041
|
kind: 'text_delta',
|
|
1003
|
-
text: `\n*Retrying
|
|
1042
|
+
text: `\n*Retrying ${recoveryAttempts}/${effectiveMaxRetries} on ${resolvedModel} — ${classified.label}: ${errSnippet}*\n`,
|
|
1004
1043
|
});
|
|
1005
1044
|
await new Promise(r => setTimeout(r, backoffMs));
|
|
1006
1045
|
continue;
|
package/package.json
CHANGED