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:
roboomp
2026-06-16 06:48:55 +00:00
parent e24f789de4
commit 8c9910318d
3 changed files with 204 additions and 2 deletions
+97 -2
View File
@@ -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;
}