fix(subagents): add model fallback support to sessions_spawn tool (#17197)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 5d20c2cd37
Co-authored-by: misterdas <170702047+misterdas@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
This commit is contained in:
misterdas
2026-02-15 20:55:47 +05:30
committed by GitHub
parent 75f3b5069b
commit c211fd112c
3 changed files with 75 additions and 5 deletions

View File

@@ -196,6 +196,71 @@ describe("agentCommand", () => {
});
});
it("uses default fallback list for session model overrides", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
fs.mkdirSync(path.dirname(store), { recursive: true });
fs.writeFileSync(
store,
JSON.stringify(
{
"agent:main:subagent:test": {
sessionId: "session-subagent",
updatedAt: Date.now(),
providerOverride: "anthropic",
modelOverride: "claude-opus-4-5",
},
},
null,
2,
),
);
mockConfig(home, store, {
model: {
primary: "openai/gpt-4.1-mini",
fallbacks: ["openai/gpt-5.2"],
},
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
"openai/gpt-5.2": {},
},
});
vi.mocked(loadModelCatalog).mockResolvedValueOnce([
{ id: "claude-opus-4-5", name: "Opus", provider: "anthropic" },
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" },
{ id: "gpt-5.2", name: "GPT-5.2", provider: "openai" },
]);
vi.mocked(runEmbeddedPiAgent)
.mockRejectedValueOnce(Object.assign(new Error("rate limited"), { status: 429 }))
.mockResolvedValueOnce({
payloads: [{ text: "ok" }],
meta: {
durationMs: 5,
agentMeta: { sessionId: "session-subagent", provider: "openai", model: "gpt-5.2" },
},
});
await agentCommand(
{
message: "hi",
sessionKey: "agent:main:subagent:test",
},
runtime,
);
const attempts = vi
.mocked(runEmbeddedPiAgent)
.mock.calls.map((call) => ({ provider: call[0]?.provider, model: call[0]?.model }));
expect(attempts).toEqual([
{ provider: "anthropic", model: "claude-opus-4-5" },
{ provider: "openai", model: "gpt-5.2" },
]);
});
});
it("keeps explicit sessionKey even when sessionId exists elsewhere", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");

View File

@@ -396,13 +396,17 @@ export async function agentCommand(
opts.replyChannel ?? opts.channel,
);
const spawnedBy = opts.spawnedBy ?? sessionEntry?.spawnedBy;
// When a session has an explicit model override, prevent the fallback logic
// from silently appending the global primary model as a backstop. Passing an
// empty array (instead of undefined) tells resolveFallbackCandidates to skip
// the implicit primary append, so the session stays on its overridden model.
// When a session has an explicit model override, keep the candidate chain
// anchored to that override (no implicit configured-primary append), while
// still preserving configured fallback lists unless the agent explicitly
// overrides fallbacks with its own list (including an empty list to disable).
const agentFallbacksOverride = resolveAgentModelFallbacksOverride(cfg, sessionAgentId);
const defaultFallbacks =
typeof cfg.agents?.defaults?.model === "object"
? (cfg.agents.defaults.model.fallbacks ?? [])
: [];
const effectiveFallbacksOverride = storedModelOverride
? (agentFallbacksOverride ?? [])
? (agentFallbacksOverride ?? defaultFallbacks)
: agentFallbacksOverride;
// Track model fallback attempts so retries on an existing session don't