open-multi-agent-kit 0.90.9 → 0.91.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/CHANGELOG.md +22 -3
- package/README.md +14 -1
- package/dist/core/agent-session.d.ts +27 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +188 -25
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +0 -8
- package/dist/core/command-safety.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +7 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +12 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/metrics-sampler.d.ts +20 -0
- package/dist/core/metrics-sampler.d.ts.map +1 -1
- package/dist/core/metrics-sampler.js +52 -0
- package/dist/core/metrics-sampler.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +4 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +15 -6
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/provider-resilience.d.ts +51 -0
- package/dist/core/provider-resilience.d.ts.map +1 -0
- package/dist/core/provider-resilience.js +86 -0
- package/dist/core/provider-resilience.js.map +1 -0
- package/dist/core/run-journal.d.ts.map +1 -1
- package/dist/core/run-journal.js +14 -22
- package/dist/core/run-journal.js.map +1 -1
- package/dist/core/session-termination.d.ts +8 -2
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +45 -3
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/settings-manager.d.ts +30 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +17 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +47 -0
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/omp-pure-seams.d.ts +41 -0
- package/dist/core/tools/omp-pure-seams.d.ts.map +1 -0
- package/dist/core/tools/omp-pure-seams.js +142 -0
- package/dist/core/tools/omp-pure-seams.js.map +1 -0
- package/dist/core/tools/omp-seam-runtime.d.ts +100 -0
- package/dist/core/tools/omp-seam-runtime.d.ts.map +1 -0
- package/dist/core/tools/omp-seam-runtime.js +40 -0
- package/dist/core/tools/omp-seam-runtime.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +34 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +11 -8
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/theme/omk-aurora-dark.json +80 -0
- package/dist/modes/interactive/theme/omk-aurora-light.json +80 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +5 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/docs/adaptorch-preview.md +1 -1
- package/docs/provider-resilience.md +87 -0
- package/docs/settings.md +30 -0
- package/docs/themes.md +2 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +22 -13
- package/package.json +6 -5
|
@@ -23,6 +23,7 @@ import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
|
23
23
|
import { resolvePath } from "../utils/paths.js";
|
|
24
24
|
import { sanitizeBinaryOutput } from "../utils/shell.js";
|
|
25
25
|
import { sleep } from "../utils/sleep.js";
|
|
26
|
+
import { createAdaptorchBridge } from "./adaptorch-bridge.js";
|
|
26
27
|
import { applyCategoryTimeoutDefaults, resolveAgentToolSettings, resolveToolTimeoutCategory, } from "./agent-tool-settings.js";
|
|
27
28
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
28
29
|
import { parseBangInvocation } from "./bang-skill-invocation.js";
|
|
@@ -47,6 +48,7 @@ import { grokPlaybookAppendForProvider } from "./grok-playbook.js";
|
|
|
47
48
|
import { assertLoadoutAccess, decideLoadoutAccess } from "./loadout-access-policy.js";
|
|
48
49
|
import { findExactModelReferenceMatch } from "./model-resolver.js";
|
|
49
50
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
51
|
+
import { isContentSafetyStopMessage, isStickySafetyModel, isTransientProviderErrorMessage, pickFailoverCandidate, resolveProviderResilience, stickySafetyBlockMessage, } from "./provider-resilience.js";
|
|
50
52
|
import { getBiasStepsForCell, getDefaultRouterBiasSnapshotPath, parseRouterBiasSnapshot, } from "./reasoning-router-bias.js";
|
|
51
53
|
import { classifyTaskV4, resolveThinkingLevelV4WithUncertainty } from "./reasoning-router-v4.js";
|
|
52
54
|
import { redactSensitiveText } from "./redaction.js";
|
|
@@ -254,6 +256,15 @@ export class AgentSession {
|
|
|
254
256
|
*/
|
|
255
257
|
_reasoningRouterBiasSnapshot = null;
|
|
256
258
|
_reasoningRouterBiasSnapshotLoaded = false;
|
|
259
|
+
/**
|
|
260
|
+
* AdaptOrch advisory bridge (default-off, global-only opt-in via
|
|
261
|
+
* `adaptorchBridge.enabled` in ~/.omk/agent/settings.json). Lazily
|
|
262
|
+
* constructed on the first v4 auto-turn when enabled; stays `null`
|
|
263
|
+
* otherwise. The bridge is advisory-only: its hint is fused into the
|
|
264
|
+
* resolver as a bounded ±2 step nudge, never as an override.
|
|
265
|
+
*/
|
|
266
|
+
_adaptorchBridge = null;
|
|
267
|
+
_adaptorchBridgeInitAttempted = false;
|
|
257
268
|
// Event subscription state
|
|
258
269
|
_unsubscribeAgent;
|
|
259
270
|
_eventListeners = [];
|
|
@@ -472,17 +483,27 @@ export class AgentSession {
|
|
|
472
483
|
if (isContextOverflow(message, this.model?.contextWindow ?? 0)) {
|
|
473
484
|
return { area: "provider", code: "context_overflow" };
|
|
474
485
|
}
|
|
486
|
+
// Fable/Claude often emit stop_reason=refusal on benign turns (false positive).
|
|
487
|
+
// Match broadly so we never mislabel these as provider.protocol.
|
|
488
|
+
if (/stop_reason\s*=\s*(refusal|sensitive)|content\/safety stop|safety stop|provider\.refusal|kind=provider_refusal/i.test(text)) {
|
|
489
|
+
return { area: "provider", code: "refusal" };
|
|
490
|
+
}
|
|
475
491
|
if (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text)) {
|
|
476
492
|
return { area: "provider", code: "auth" };
|
|
477
493
|
}
|
|
478
494
|
if (/rate.?limit|too many requests|429|quota|available balance|billing/i.test(text)) {
|
|
479
495
|
return { area: "provider", code: "rate_limit" };
|
|
480
496
|
}
|
|
497
|
+
// Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.
|
|
498
|
+
// Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.
|
|
499
|
+
if (/tool_call_id\s+is\s+not\s+found|tool_call_id\s+not\s+found|unknown\s+tool_call_id/i.test(text)) {
|
|
500
|
+
return { area: "provider", code: "protocol" };
|
|
501
|
+
}
|
|
481
502
|
if (/tool.+timed? out|tool.+timeout/i.test(text))
|
|
482
503
|
return { area: "tool", code: "timeout" };
|
|
483
|
-
if (/
|
|
504
|
+
if (/\btool\b/i.test(text) && !/tool_call_id/i.test(text))
|
|
484
505
|
return { area: "tool", code: "fatal" };
|
|
485
|
-
if (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn
|
|
506
|
+
if (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn|^terminated$/i.test(text)) {
|
|
486
507
|
return { area: "provider", code: "network" };
|
|
487
508
|
}
|
|
488
509
|
return { area: "provider", code: "protocol" };
|
|
@@ -1531,6 +1552,8 @@ export class AgentSession {
|
|
|
1531
1552
|
if (!this.model) {
|
|
1532
1553
|
throw new Error(formatNoModelSelectedMessage());
|
|
1533
1554
|
}
|
|
1555
|
+
// Root-level: eject sticky safety models (Fable) before the turn leaves the machine.
|
|
1556
|
+
await this._ejectStickySafetyModelIfNeeded();
|
|
1534
1557
|
// Grok OAuth: refuse Imagine ids on the chat/completions path (tool-only).
|
|
1535
1558
|
assertTextChatModelForCompletion(this.model.id, this.model.provider);
|
|
1536
1559
|
if (!this._modelRegistry.hasConfiguredAuth(this.model)) {
|
|
@@ -1935,6 +1958,11 @@ export class AgentSession {
|
|
|
1935
1958
|
if (!this._modelRegistry.hasConfiguredAuth(model)) {
|
|
1936
1959
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
1937
1960
|
}
|
|
1961
|
+
// Root-level: block sticky safety models (Fable) unless explicitly allowed in settings.
|
|
1962
|
+
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
1963
|
+
if (resilience.blockStickySafetyModels && isStickySafetyModel(model.id, model.provider)) {
|
|
1964
|
+
throw new Error(stickySafetyBlockMessage(model.id, model.provider));
|
|
1965
|
+
}
|
|
1938
1966
|
// Grok OAuth: block selecting Imagine models as the session chat model.
|
|
1939
1967
|
assertTextChatModelForCompletion(model.id, model.provider);
|
|
1940
1968
|
const previousModel = this.model;
|
|
@@ -1959,7 +1987,15 @@ export class AgentSession {
|
|
|
1959
1987
|
return this._cycleAvailableModel(direction);
|
|
1960
1988
|
}
|
|
1961
1989
|
async _cycleScopedModel(direction) {
|
|
1962
|
-
const
|
|
1990
|
+
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
1991
|
+
const scopedModels = this._scopedModels.filter((scoped) => {
|
|
1992
|
+
if (!this._modelRegistry.hasConfiguredAuth(scoped.model))
|
|
1993
|
+
return false;
|
|
1994
|
+
if (resilience.blockStickySafetyModels && isStickySafetyModel(scoped.model.id, scoped.model.provider)) {
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1997
|
+
return true;
|
|
1998
|
+
});
|
|
1963
1999
|
if (scopedModels.length <= 1)
|
|
1964
2000
|
return undefined;
|
|
1965
2001
|
const currentModel = this.model;
|
|
@@ -1970,20 +2006,19 @@ export class AgentSession {
|
|
|
1970
2006
|
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
1971
2007
|
const next = scopedModels[nextIndex];
|
|
1972
2008
|
const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
|
|
1973
|
-
//
|
|
1974
|
-
this.
|
|
1975
|
-
|
|
1976
|
-
this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
|
|
1977
|
-
// Apply thinking level.
|
|
1978
|
-
// - Explicit scoped model thinking level overrides current session level
|
|
1979
|
-
// - Undefined scoped model thinking level inherits the current session preference
|
|
1980
|
-
// setThinkingLevel clamps to model capabilities.
|
|
2009
|
+
// Single path through setModel (sticky block + auth + Imagine guards).
|
|
2010
|
+
await this.setModel(next.model);
|
|
2011
|
+
// Explicit scoped thinking level still applies after switch.
|
|
1981
2012
|
this.setThinkingLevel(thinkingLevel);
|
|
1982
|
-
await this._emitModelSelect(next.model, currentModel, "cycle");
|
|
1983
2013
|
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
|
1984
2014
|
}
|
|
1985
2015
|
async _cycleAvailableModel(direction) {
|
|
1986
|
-
const
|
|
2016
|
+
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
2017
|
+
const raw = await this._modelRegistry.getAvailable();
|
|
2018
|
+
// Hard-kill sticky safety models from the cycle list (Fable must not re-enter via hotkey).
|
|
2019
|
+
const availableModels = resilience.blockStickySafetyModels
|
|
2020
|
+
? raw.filter((m) => !isStickySafetyModel(m.id, m.provider))
|
|
2021
|
+
: raw;
|
|
1987
2022
|
if (availableModels.length <= 1)
|
|
1988
2023
|
return undefined;
|
|
1989
2024
|
const currentModel = this.model;
|
|
@@ -1993,13 +2028,8 @@ export class AgentSession {
|
|
|
1993
2028
|
const len = availableModels.length;
|
|
1994
2029
|
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
1995
2030
|
const nextModel = availableModels[nextIndex];
|
|
1996
|
-
|
|
1997
|
-
this.
|
|
1998
|
-
this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
|
|
1999
|
-
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
|
|
2000
|
-
// Re-clamp thinking level for new model's capabilities
|
|
2001
|
-
this.setThinkingLevel(thinkingLevel);
|
|
2002
|
-
await this._emitModelSelect(nextModel, currentModel, "cycle");
|
|
2031
|
+
// Always go through setModel so sticky blocks / auth / Imagine guards stay single-path.
|
|
2032
|
+
await this.setModel(nextModel);
|
|
2003
2033
|
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
|
|
2004
2034
|
}
|
|
2005
2035
|
// =========================================================================
|
|
@@ -2099,7 +2129,7 @@ export class AgentSession {
|
|
|
2099
2129
|
hadFence: features.hadFence,
|
|
2100
2130
|
hadDiff: features.hadDiff,
|
|
2101
2131
|
});
|
|
2102
|
-
const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias,
|
|
2132
|
+
const resolved = resolveThinkingLevelV4WithUncertainty(verdict, availableLevels, undefined, bias, this._getAdaptorchHint(verdict, features));
|
|
2103
2133
|
if (learningEnabled && resolved !== "off") {
|
|
2104
2134
|
const record = {
|
|
2105
2135
|
routerVersion: "v4",
|
|
@@ -2193,6 +2223,76 @@ export class AgentSession {
|
|
|
2193
2223
|
return 1;
|
|
2194
2224
|
return 0;
|
|
2195
2225
|
}
|
|
2226
|
+
/**
|
|
2227
|
+
* AdaptOrch advisory bridge hint accessor (default-off, global-only).
|
|
2228
|
+
* Lazily constructs the bridge on first call when enabled; returns `null`
|
|
2229
|
+
* (no hint) otherwise. The bridge's `getFreshHint` is synchronous and
|
|
2230
|
+
* cache-read-only, so this never blocks the turn-start path. A fire-and-
|
|
2231
|
+
* forget `requestRefresh` is issued for the NEXT turn's benefit.
|
|
2232
|
+
*
|
|
2233
|
+
* The returned hint is shaped as `{ level, confidence }` for the resolver's
|
|
2234
|
+
* hint-fusion slot: confidence is mapped from the bridge's closed
|
|
2235
|
+
* `confidenceBand` enum (low=0.5, medium=0.75, high=0.95) and the level is
|
|
2236
|
+
* derived from the bridge's `taskClass` via the same static rule table the
|
|
2237
|
+
* resolver uses. The resolver's own HINT_CONFIDENCE_THRESHOLD (0.7) gates
|
|
2238
|
+
* whether the hint actually fuses, so a "low" band hint is structurally
|
|
2239
|
+
* inert.
|
|
2240
|
+
*/
|
|
2241
|
+
_getAdaptorchHint(verdict, features) {
|
|
2242
|
+
if (!this.settingsManager.getAdaptorchBridgeEnabled())
|
|
2243
|
+
return null;
|
|
2244
|
+
// Lazy init (at most once per session)
|
|
2245
|
+
if (!this._adaptorchBridgeInitAttempted) {
|
|
2246
|
+
this._adaptorchBridgeInitAttempted = true;
|
|
2247
|
+
const bridgeSettings = this.settingsManager.getAdaptorchBridgeSettings();
|
|
2248
|
+
this._adaptorchBridge = createAdaptorchBridge({
|
|
2249
|
+
// The advisory function is a no-op stub until a real MCP transport
|
|
2250
|
+
// is wired by a future lane. The bridge's circuit breaker and budget
|
|
2251
|
+
// counter ensure this stub can never cause harm.
|
|
2252
|
+
advisoryFn: async () => null,
|
|
2253
|
+
ttlMs: bridgeSettings?.ttlMs,
|
|
2254
|
+
timeoutMs: bridgeSettings?.timeoutMs,
|
|
2255
|
+
maxConsultsPerSession: bridgeSettings?.maxConsultsPerSession,
|
|
2256
|
+
failureThreshold: bridgeSettings?.failureThreshold,
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
const bridge = this._adaptorchBridge;
|
|
2260
|
+
if (bridge === null)
|
|
2261
|
+
return null;
|
|
2262
|
+
const payload = {
|
|
2263
|
+
schemaVersion: 1,
|
|
2264
|
+
taskClass: verdict.taskClass,
|
|
2265
|
+
runnerUp: verdict.taskClass,
|
|
2266
|
+
marginBucket: "high",
|
|
2267
|
+
lenBucket: features.lenBucket,
|
|
2268
|
+
hadFence: features.hadFence,
|
|
2269
|
+
hadDiff: features.hadDiff,
|
|
2270
|
+
pressureBucket: this._computePressureBucket(),
|
|
2271
|
+
};
|
|
2272
|
+
// Fire-and-forget refresh for next turn
|
|
2273
|
+
bridge.requestRefresh(payload);
|
|
2274
|
+
// Synchronous cache read for this turn
|
|
2275
|
+
const hint = bridge.getFreshHint(payload);
|
|
2276
|
+
if (hint === null)
|
|
2277
|
+
return null;
|
|
2278
|
+
// Map confidenceBand -> numeric confidence for the resolver's threshold gate
|
|
2279
|
+
const confidenceMap = { low: 0.5, medium: 0.75, high: 0.95 };
|
|
2280
|
+
const confidence = confidenceMap[hint.confidenceBand] ?? 0.5;
|
|
2281
|
+
// Map taskClass -> ThinkingLevel via the same rule table the resolver uses
|
|
2282
|
+
const levelMap = {
|
|
2283
|
+
trivial: "minimal",
|
|
2284
|
+
"simple-edit": "low",
|
|
2285
|
+
"code-gen": "medium",
|
|
2286
|
+
debug: "high",
|
|
2287
|
+
refactor: "high",
|
|
2288
|
+
review: "high",
|
|
2289
|
+
plan: "xhigh",
|
|
2290
|
+
};
|
|
2291
|
+
const level = levelMap[hint.taskClass];
|
|
2292
|
+
if (level === undefined)
|
|
2293
|
+
return null;
|
|
2294
|
+
return { level, confidence };
|
|
2295
|
+
}
|
|
2196
2296
|
/**
|
|
2197
2297
|
* Cycle to next thinking level.
|
|
2198
2298
|
* @returns New level, or undefined if model doesn't support thinking
|
|
@@ -3371,13 +3471,68 @@ export class AgentSession {
|
|
|
3371
3471
|
const err = message.errorMessage;
|
|
3372
3472
|
if (this._isNonRetryableProviderLimitError(err))
|
|
3373
3473
|
return false;
|
|
3374
|
-
//
|
|
3375
|
-
return
|
|
3474
|
+
// Shared contract with provider-resilience.ts (Fable safety stop, K3 orphan tool_call_id, terminated).
|
|
3475
|
+
return isTransientProviderErrorMessage(err);
|
|
3376
3476
|
}
|
|
3377
3477
|
/**
|
|
3378
3478
|
* Prepare a retryable error for continuation with exponential backoff.
|
|
3379
3479
|
* @returns true if the caller should continue the agent, false otherwise
|
|
3380
3480
|
*/
|
|
3481
|
+
/** Eject sticky safety model at prompt boundary (session resume / leftover default). */
|
|
3482
|
+
async _ejectStickySafetyModelIfNeeded() {
|
|
3483
|
+
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
3484
|
+
if (!resilience.blockStickySafetyModels)
|
|
3485
|
+
return;
|
|
3486
|
+
const current = this.model;
|
|
3487
|
+
if (!current || !isStickySafetyModel(current.id, current.provider))
|
|
3488
|
+
return;
|
|
3489
|
+
const pick = pickFailoverCandidate(resilience.failoverCandidates, { provider: current.provider, id: current.id }, (c) => {
|
|
3490
|
+
const next = this._modelRegistry.find(c.provider, c.id);
|
|
3491
|
+
return !!next && this._modelRegistry.hasConfiguredAuth(next);
|
|
3492
|
+
});
|
|
3493
|
+
if (!pick) {
|
|
3494
|
+
throw new Error(stickySafetyBlockMessage(current.id, current.provider));
|
|
3495
|
+
}
|
|
3496
|
+
const next = this._modelRegistry.find(pick.provider, pick.id);
|
|
3497
|
+
if (!next) {
|
|
3498
|
+
throw new Error(stickySafetyBlockMessage(current.id, current.provider));
|
|
3499
|
+
}
|
|
3500
|
+
// Bypass setModel sticky check by temporarily allowing via direct state switch path:
|
|
3501
|
+
// setModel itself blocks sticky targets, not sources — safe.
|
|
3502
|
+
await this.setModel(next);
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Sticky safety models (e.g. claude-fable-5) often false-positive on coding turns.
|
|
3506
|
+
* Retrying the same model repeats the refusal — switch via provider-resilience chain first.
|
|
3507
|
+
*/
|
|
3508
|
+
async _maybeFailoverFromSafetyStop(message) {
|
|
3509
|
+
const resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());
|
|
3510
|
+
if (!resilience.autoFailoverOnSafetyStop)
|
|
3511
|
+
return undefined;
|
|
3512
|
+
if (!isContentSafetyStopMessage(message.errorMessage))
|
|
3513
|
+
return undefined;
|
|
3514
|
+
const current = this.model;
|
|
3515
|
+
const currentId = message.model || current?.id || "";
|
|
3516
|
+
const currentProvider = message.provider || current?.provider || "";
|
|
3517
|
+
if (!isStickySafetyModel(currentId, currentProvider))
|
|
3518
|
+
return undefined;
|
|
3519
|
+
const pick = pickFailoverCandidate(resilience.failoverCandidates, current ? { provider: current.provider, id: current.id } : undefined, (c) => {
|
|
3520
|
+
const next = this._modelRegistry.find(c.provider, c.id);
|
|
3521
|
+
return !!next && this._modelRegistry.hasConfiguredAuth(next);
|
|
3522
|
+
});
|
|
3523
|
+
if (!pick)
|
|
3524
|
+
return undefined;
|
|
3525
|
+
const next = this._modelRegistry.find(pick.provider, pick.id);
|
|
3526
|
+
if (!next)
|
|
3527
|
+
return undefined;
|
|
3528
|
+
try {
|
|
3529
|
+
await this.setModel(next);
|
|
3530
|
+
return `${next.provider}/${next.id}`;
|
|
3531
|
+
}
|
|
3532
|
+
catch {
|
|
3533
|
+
return undefined;
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3381
3536
|
async _prepareRetry(message) {
|
|
3382
3537
|
const settings = this.settingsManager.getRetrySettings();
|
|
3383
3538
|
if (!settings.enabled) {
|
|
@@ -3389,13 +3544,21 @@ export class AgentSession {
|
|
|
3389
3544
|
this._retryAttempt--;
|
|
3390
3545
|
return false;
|
|
3391
3546
|
}
|
|
3392
|
-
|
|
3547
|
+
// Content/safety stop on Fable: switch model BEFORE delay so the retry is not another Fable refusal.
|
|
3548
|
+
const failoverTo = await this._maybeFailoverFromSafetyStop(message);
|
|
3549
|
+
// Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.
|
|
3550
|
+
const delayMs = failoverTo
|
|
3551
|
+
? Math.min(400, settings.baseDelayMs)
|
|
3552
|
+
: settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
|
|
3553
|
+
const errorMessage = failoverTo
|
|
3554
|
+
? `${message.errorMessage || "content/safety stop"} → failover ${failoverTo}`
|
|
3555
|
+
: message.errorMessage || "Unknown error";
|
|
3393
3556
|
this._emit({
|
|
3394
3557
|
type: "auto_retry_start",
|
|
3395
3558
|
attempt: this._retryAttempt,
|
|
3396
3559
|
maxAttempts: settings.maxRetries,
|
|
3397
3560
|
delayMs,
|
|
3398
|
-
errorMessage
|
|
3561
|
+
errorMessage,
|
|
3399
3562
|
});
|
|
3400
3563
|
// Remove error message from agent state (keep in session for history)
|
|
3401
3564
|
const messages = this.agent.state.messages;
|