Files
Moltbot/src/auto-reply/fallback-state.ts

181 lines
6.0 KiB
TypeScript

import type { SessionEntry } from "../config/sessions.js";
import { formatProviderModelRef } from "./model-runtime.js";
import type { RuntimeFallbackAttempt } from "./reply/agent-runner-execution.js";
const FALLBACK_REASON_PART_MAX = 80;
export type FallbackNoticeState = Pick<
SessionEntry,
"fallbackNoticeSelectedModel" | "fallbackNoticeActiveModel" | "fallbackNoticeReason"
>;
export function normalizeFallbackModelRef(value?: string): string | undefined {
const trimmed = String(value ?? "").trim();
return trimmed || undefined;
}
function truncateFallbackReasonPart(value: string, max = FALLBACK_REASON_PART_MAX): string {
const text = String(value ?? "")
.replace(/\s+/g, " ")
.trim();
if (text.length <= max) {
return text;
}
return `${text.slice(0, Math.max(0, max - 1)).trimEnd()}`;
}
export function formatFallbackAttemptReason(attempt: RuntimeFallbackAttempt): string {
const reason = attempt.reason?.trim();
if (reason) {
return reason.replace(/_/g, " ");
}
const code = attempt.code?.trim();
if (code) {
return code;
}
if (typeof attempt.status === "number") {
return `HTTP ${attempt.status}`;
}
return truncateFallbackReasonPart(attempt.error || "error");
}
function formatFallbackAttemptSummary(attempt: RuntimeFallbackAttempt): string {
return `${formatProviderModelRef(attempt.provider, attempt.model)} ${formatFallbackAttemptReason(attempt)}`;
}
export function buildFallbackReasonSummary(attempts: RuntimeFallbackAttempt[]): string {
const firstAttempt = attempts[0];
const firstReason = firstAttempt
? formatFallbackAttemptReason(firstAttempt)
: "selected model unavailable";
const moreAttempts = attempts.length > 1 ? ` (+${attempts.length - 1} more attempts)` : "";
return `${truncateFallbackReasonPart(firstReason)}${moreAttempts}`;
}
export function buildFallbackAttemptSummaries(attempts: RuntimeFallbackAttempt[]): string[] {
return attempts.map((attempt) =>
truncateFallbackReasonPart(formatFallbackAttemptSummary(attempt)),
);
}
export function buildFallbackNotice(params: {
selectedProvider: string;
selectedModel: string;
activeProvider: string;
activeModel: string;
attempts: RuntimeFallbackAttempt[];
}): string | null {
const selected = formatProviderModelRef(params.selectedProvider, params.selectedModel);
const active = formatProviderModelRef(params.activeProvider, params.activeModel);
if (selected === active) {
return null;
}
const reasonSummary = buildFallbackReasonSummary(params.attempts);
return `↪️ Model Fallback: ${active} (selected ${selected}; ${reasonSummary})`;
}
export function buildFallbackClearedNotice(params: {
selectedProvider: string;
selectedModel: string;
previousActiveModel?: string;
}): string {
const selected = formatProviderModelRef(params.selectedProvider, params.selectedModel);
const previous = normalizeFallbackModelRef(params.previousActiveModel);
if (previous && previous !== selected) {
return `↪️ Model Fallback cleared: ${selected} (was ${previous})`;
}
return `↪️ Model Fallback cleared: ${selected}`;
}
export function resolveActiveFallbackState(params: {
selectedModelRef: string;
activeModelRef: string;
state?: FallbackNoticeState;
}): { active: boolean; reason?: string } {
const selected = normalizeFallbackModelRef(params.state?.fallbackNoticeSelectedModel);
const active = normalizeFallbackModelRef(params.state?.fallbackNoticeActiveModel);
const reason = normalizeFallbackModelRef(params.state?.fallbackNoticeReason);
const fallbackActive =
params.selectedModelRef !== params.activeModelRef &&
selected === params.selectedModelRef &&
active === params.activeModelRef;
return {
active: fallbackActive,
reason: fallbackActive ? reason : undefined,
};
}
export type ResolvedFallbackTransition = {
selectedModelRef: string;
activeModelRef: string;
fallbackActive: boolean;
fallbackTransitioned: boolean;
fallbackCleared: boolean;
reasonSummary: string;
attemptSummaries: string[];
previousState: {
selectedModel?: string;
activeModel?: string;
reason?: string;
};
nextState: {
selectedModel?: string;
activeModel?: string;
reason?: string;
};
stateChanged: boolean;
};
export function resolveFallbackTransition(params: {
selectedProvider: string;
selectedModel: string;
activeProvider: string;
activeModel: string;
attempts: RuntimeFallbackAttempt[];
state?: FallbackNoticeState;
}): ResolvedFallbackTransition {
const selectedModelRef = formatProviderModelRef(params.selectedProvider, params.selectedModel);
const activeModelRef = formatProviderModelRef(params.activeProvider, params.activeModel);
const previousState = {
selectedModel: normalizeFallbackModelRef(params.state?.fallbackNoticeSelectedModel),
activeModel: normalizeFallbackModelRef(params.state?.fallbackNoticeActiveModel),
reason: normalizeFallbackModelRef(params.state?.fallbackNoticeReason),
};
const fallbackActive = selectedModelRef !== activeModelRef;
const fallbackTransitioned =
fallbackActive &&
(previousState.selectedModel !== selectedModelRef ||
previousState.activeModel !== activeModelRef);
const fallbackCleared =
!fallbackActive && Boolean(previousState.selectedModel || previousState.activeModel);
const reasonSummary = buildFallbackReasonSummary(params.attempts);
const attemptSummaries = buildFallbackAttemptSummaries(params.attempts);
const nextState = fallbackActive
? {
selectedModel: selectedModelRef,
activeModel: activeModelRef,
reason: reasonSummary,
}
: {
selectedModel: undefined,
activeModel: undefined,
reason: undefined,
};
const stateChanged =
previousState.selectedModel !== nextState.selectedModel ||
previousState.activeModel !== nextState.activeModel ||
previousState.reason !== nextState.reason;
return {
selectedModelRef,
activeModelRef,
fallbackActive,
fallbackTransitioned,
fallbackCleared,
reasonSummary,
attemptSummaries,
previousState,
nextState,
stateChanged,
};
}