Unify dynamic fallback chains for background subagents

This commit is contained in:
Ravi Tharuma
2026-03-04 20:43:55 +01:00
committed by YeonGyu-Kim
parent d0cc76c7e0
commit f4a810288f
13 changed files with 388 additions and 37 deletions
@@ -64,4 +64,26 @@ describe("executeBackground", () => {
expect(result).toContain("interrupt")
expect(result).toContain("test-task-id")
})
test("passes fallbackChain to background manager launch", async () => {
//#given
const fallbackChain = [
{ providers: ["quotio"], model: "kimi-k2.5", variant: undefined },
{ providers: ["openai"], model: "gpt-5.2", variant: "high" },
]
launchMock.mockResolvedValueOnce({
id: "test-task-id",
sessionID: "sub-session",
description: "Test task",
agent: "test-agent",
status: "pending",
})
//#when
await executeBackground(testArgs, testContext, mockManager, mockClient, fallbackChain)
//#then
const launchArgs = launchMock.mock.calls.at(-1)?.[0]
expect(launchArgs.fallbackChain).toEqual(fallbackChain)
})
})
@@ -2,6 +2,7 @@ import type { CallOmoAgentArgs } from "./types"
import type { BackgroundManager } from "../../features/background-agent"
import type { PluginInput } from "@opencode-ai/plugin"
import { log } from "../../shared"
import type { FallbackEntry } from "../../shared/model-requirements"
import { resolveMessageContext } from "../../features/hook-message-injector"
import { getSessionAgent } from "../../features/claude-code-session-state"
import { getMessageDir } from "./message-dir"
@@ -17,7 +18,8 @@ export async function executeBackground(
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void
},
manager: BackgroundManager,
client: PluginInput["client"]
client: PluginInput["client"],
fallbackChain?: FallbackEntry[],
): Promise<string> {
try {
const messageDir = getMessageDir(toolContext.sessionID)
@@ -48,6 +50,7 @@ export async function executeBackground(
parentMessageID: toolContext.messageID,
parentAgent,
parentTools: getSessionTools(toolContext.sessionID),
fallbackChain,
})
const WAIT_FOR_SESSION_INTERVAL_MS = 50
+44
View File
@@ -99,4 +99,48 @@ describe("createCallOmoAgent", () => {
//#then
expect(result).not.toContain("disabled via disabled_agents")
})
test("uses agent override fallback_models when launching background subagent", async () => {
//#given
const launch = mock(() => Promise.resolve({
id: "task-fallback",
sessionID: "sub-session",
description: "Test task",
agent: "explore",
status: "pending",
}))
const managerWithLaunch = {
launch,
getTask: mock(() => undefined),
} as unknown as BackgroundManager
const toolDef = createCallOmoAgent(
mockCtx,
managerWithLaunch,
[],
{
explore: {
fallback_models: ["quotio/kimi-k2.5", "openai/gpt-5.2(high)"],
},
},
)
const executeFunc = toolDef.execute as Function
//#when
await executeFunc(
{
description: "Test fallback",
prompt: "Test prompt",
subagent_type: "explore",
run_in_background: true,
},
{ sessionID: "test", messageID: "msg", agent: "test", abort: new AbortController().signal }
)
//#then
const launchArgs = launch.mock.calls[0]?.[0]
expect(launchArgs.fallbackChain).toEqual([
{ providers: ["quotio"], model: "kimi-k2.5", variant: undefined },
{ providers: ["openai"], model: "gpt-5.2", variant: "high" },
])
})
})
+39 -2
View File
@@ -2,14 +2,46 @@ import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin
import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants"
import type { AllowedAgentType, CallOmoAgentArgs, ToolContextWithMetadata } from "./types"
import type { BackgroundManager } from "../../features/background-agent"
import type { CategoriesConfig, AgentOverrides } from "../../config/schema"
import type { FallbackEntry } from "../../shared/model-requirements"
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
import { getAgentConfigKey } from "../../shared/agent-display-names"
import { normalizeFallbackModels } from "../../shared/model-resolver"
import { buildFallbackChainFromModels } from "../../shared/fallback-chain-from-models"
import { log } from "../../shared"
import { executeBackground } from "./background-executor"
import { executeSync } from "./sync-executor"
function resolveFallbackChainForCallOmoAgent(args: {
subagentType: string
agentOverrides?: AgentOverrides
userCategories?: CategoriesConfig
}): FallbackEntry[] | undefined {
const { subagentType, agentOverrides, userCategories } = args
const agentConfigKey = getAgentConfigKey(subagentType)
const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentConfigKey]
const agentOverride = agentOverrides?.[agentConfigKey as keyof AgentOverrides]
?? (agentOverrides
? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentConfigKey)?.[1]
: undefined)
const normalizedFallbackModels = normalizeFallbackModels(
agentOverride?.fallback_models
?? (agentOverride?.category ? userCategories?.[agentOverride.category]?.fallback_models : undefined)
)
const defaultProviderID = agentRequirement?.fallbackChain?.[0]?.providers?.[0] ?? "opencode"
const configuredFallbackChain = buildFallbackChainFromModels(normalizedFallbackModels, defaultProviderID)
return configuredFallbackChain ?? agentRequirement?.fallbackChain
}
export function createCallOmoAgent(
ctx: PluginInput,
backgroundManager: BackgroundManager,
disabledAgents: string[] = []
disabledAgents: string[] = [],
agentOverrides?: AgentOverrides,
userCategories?: CategoriesConfig,
): ToolDefinition {
const agentDescriptions = ALLOWED_AGENTS.map(
(name) => `- ${name}: Specialized agent for ${name} tasks`
@@ -54,7 +86,12 @@ export function createCallOmoAgent(
if (args.session_id) {
return `Error: session_id is not supported in background mode. Use run_in_background=false to continue an existing session.`
}
return await executeBackground(args, toolCtx, backgroundManager, ctx.client)
const fallbackChain = resolveFallbackChainForCallOmoAgent({
subagentType: args.subagent_type,
agentOverrides,
userCategories,
})
return await executeBackground(args, toolCtx, backgroundManager, ctx.client, fallbackChain)
}
return await executeSync(args, toolCtx, ctx)