fix(agent): retried subagent model fallbacks
Installed subagent-scoped retry fallback chains from ordered task model candidates so provider failures can advance to the next configured worker model. Updated task result tracking to surface the fallback-applied final model and added focused regression coverage for the executor wiring. Fixes #2750
This commit is contained in:
@@ -7,11 +7,15 @@
|
||||
import path from "node:path";
|
||||
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Usage } from "@oh-my-pi/pi-ai";
|
||||
import type { Api, Model, Usage } from "@oh-my-pi/pi-ai";
|
||||
import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import type { Rule } from "../capability/rule";
|
||||
import { ModelRegistry } from "../config/model-registry";
|
||||
import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
||||
import {
|
||||
formatModelString,
|
||||
resolveModelOverride,
|
||||
resolveModelOverrideWithAuthFallback,
|
||||
} from "../config/model-resolver";
|
||||
import type { PromptTemplate } from "../config/prompt-templates";
|
||||
import { Settings } from "../config/settings";
|
||||
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
||||
@@ -120,6 +124,74 @@ function normalizeModelPatterns(value: string | string[] | undefined): string[]
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX = "subagent:";
|
||||
|
||||
interface SubagentRetryFallbackCandidate {
|
||||
model: Model<Api>;
|
||||
selector: string;
|
||||
}
|
||||
|
||||
function formatSubagentRetryFallbackSelector(model: Model<Api>, thinkingLevel: ThinkingLevel | undefined): string {
|
||||
const selector = formatModelString(model);
|
||||
return thinkingLevel ? `${selector}:${thinkingLevel}` : selector;
|
||||
}
|
||||
|
||||
function resolveSubagentRetryFallbackCandidates(
|
||||
modelPatterns: string[],
|
||||
modelRegistry: ModelRegistry,
|
||||
settings: Settings,
|
||||
): SubagentRetryFallbackCandidate[] {
|
||||
const candidates: SubagentRetryFallbackCandidate[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const pattern of modelPatterns) {
|
||||
const resolved = resolveModelOverride([pattern], modelRegistry, settings);
|
||||
if (!resolved.model) continue;
|
||||
const selector = formatSubagentRetryFallbackSelector(
|
||||
resolved.model,
|
||||
resolved.explicitThinkingLevel ? resolved.thinkingLevel : undefined,
|
||||
);
|
||||
if (seen.has(selector)) continue;
|
||||
seen.add(selector);
|
||||
candidates.push({ model: resolved.model, selector });
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function installSubagentRetryFallbackChain(args: {
|
||||
settings: Settings;
|
||||
id: string;
|
||||
candidates: SubagentRetryFallbackCandidate[];
|
||||
model: Model<Api> | undefined;
|
||||
authFallbackUsed: boolean;
|
||||
}): string | undefined {
|
||||
const { settings, id, candidates, model, authFallbackUsed } = args;
|
||||
if (!model || authFallbackUsed || candidates.length <= 1) return undefined;
|
||||
|
||||
const selectedIndex = candidates.findIndex(
|
||||
candidate => candidate.model.provider === model.provider && candidate.model.id === model.id,
|
||||
);
|
||||
if (selectedIndex < 0) return undefined;
|
||||
const fallbackSelectors = candidates.slice(selectedIndex + 1).map(candidate => candidate.selector);
|
||||
if (fallbackSelectors.length === 0) return undefined;
|
||||
|
||||
const role = `${SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX}${id}`;
|
||||
const modelRoles: Record<string, string> = {};
|
||||
const existingRoles = settings.getModelRoles();
|
||||
for (const existingRole in existingRoles) {
|
||||
const selector = existingRoles[existingRole];
|
||||
if (selector) {
|
||||
modelRoles[existingRole] = selector;
|
||||
}
|
||||
}
|
||||
modelRoles[role] = candidates[selectedIndex].selector;
|
||||
settings.override("modelRoles", modelRoles);
|
||||
settings.override("retry.fallbackChains", {
|
||||
...settings.get("retry.fallbackChains"),
|
||||
[role]: fallbackSelectors,
|
||||
});
|
||||
return role;
|
||||
}
|
||||
|
||||
function renderIrcPeerRoster(selfId: string): string {
|
||||
const peers = AgentRegistry.global()
|
||||
.list()
|
||||
@@ -1222,6 +1294,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
||||
popLoopPhase();
|
||||
}
|
||||
}
|
||||
if (event.type === "retry_fallback_applied") {
|
||||
progress.resolvedModel = event.to;
|
||||
scheduleProgress(true);
|
||||
return;
|
||||
}
|
||||
if (event.type === "retry_fallback_succeeded") {
|
||||
progress.resolvedModel = event.model;
|
||||
scheduleProgress(true);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const captureSalvage = (session: AgentSession): void => {
|
||||
@@ -1817,6 +1899,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
resolvedModel: model.id,
|
||||
});
|
||||
}
|
||||
const retryFallbackRole = installSubagentRetryFallbackChain({
|
||||
settings: subagentSettings,
|
||||
id,
|
||||
candidates: resolveSubagentRetryFallbackCandidates(modelPatterns, modelRegistry, settings),
|
||||
model,
|
||||
authFallbackUsed,
|
||||
});
|
||||
if (retryFallbackRole) {
|
||||
logger.debug("Configured subagent runtime model fallback chain", {
|
||||
role: retryFallbackRole,
|
||||
requested: modelPatterns,
|
||||
});
|
||||
}
|
||||
if (model?.contextWindow && model.contextWindow > 0) {
|
||||
progress.contextWindow = model.contextWindow;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user