670d8ab175
- fix(switcher): use lastIndexOf for multi-slash model IDs (e.g. aws/anthropic/claude-sonnet-4) - fix(model-resolution): same lastIndexOf fix in doctor parseProviderModel - fix(call-omo-agent): resolve model from agent config and forward to both background and sync executors via DelegatedModelConfig - fix(subagent-resolver): inherit category model/variant when agent uses category reference without explicit model override - test: add model override forwarding tests for call-omo-agent - test: add multi-slash model ID test for switcher
215 lines
9.0 KiB
TypeScript
215 lines
9.0 KiB
TypeScript
import type { DelegateTaskArgs } from "./types"
|
|
import type { ExecutorContext } from "./executor-types"
|
|
import type { DelegatedModelConfig } from "./types"
|
|
import { isPlanFamily } from "./constants"
|
|
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
|
import { normalizeModelFormat } from "../../shared/model-format-normalizer"
|
|
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
|
import { normalizeFallbackModels, flattenToFallbackModelStrings } from "../../shared/model-resolver"
|
|
import { buildFallbackChainFromModels, findMostSpecificFallbackEntry } from "../../shared/fallback-chain-from-models"
|
|
import { getAgentDisplayName, getAgentConfigKey } from "../../shared/agent-display-names"
|
|
import { normalizeSDKResponse } from "../../shared"
|
|
import { log } from "../../shared/logger"
|
|
import { getAvailableModelsForDelegateTask } from "./available-models"
|
|
import type { FallbackEntry } from "../../shared/model-requirements"
|
|
import { resolveModelForDelegateTask } from "./model-selection"
|
|
|
|
export async function resolveSubagentExecution(
|
|
args: DelegateTaskArgs,
|
|
executorCtx: ExecutorContext,
|
|
parentAgent: string | undefined,
|
|
categoryExamples: string
|
|
): Promise<{ agentToUse: string; categoryModel: DelegatedModelConfig | undefined; fallbackChain?: FallbackEntry[]; error?: string }> {
|
|
const { client, agentOverrides, userCategories } = executorCtx
|
|
|
|
if (!args.subagent_type?.trim()) {
|
|
return { agentToUse: "", categoryModel: undefined, error: `Agent name cannot be empty.` }
|
|
}
|
|
|
|
const agentName = args.subagent_type.trim()
|
|
|
|
if (agentName.toLowerCase() === SISYPHUS_JUNIOR_AGENT.toLowerCase()) {
|
|
return {
|
|
agentToUse: "",
|
|
categoryModel: undefined,
|
|
error: `Cannot use subagent_type="${SISYPHUS_JUNIOR_AGENT}" directly. Use category parameter instead (e.g., ${categoryExamples}).
|
|
|
|
Sisyphus-Junior is spawned automatically when you specify a category. Pick the appropriate category for your task domain.`,
|
|
}
|
|
}
|
|
|
|
if (isPlanFamily(agentName) && isPlanFamily(parentAgent)) {
|
|
return {
|
|
agentToUse: "",
|
|
categoryModel: undefined,
|
|
error: `You are a plan-family agent (plan/prometheus). You cannot delegate to other plan-family agents via task.
|
|
|
|
Create the work plan directly - that's your job as the planning agent.`,
|
|
}
|
|
}
|
|
|
|
let agentToUse = agentName
|
|
let categoryModel: DelegatedModelConfig | undefined
|
|
let fallbackChain: FallbackEntry[] | undefined = undefined
|
|
|
|
try {
|
|
const agentsResult = await client.app.agents()
|
|
type AgentInfo = {
|
|
name: string
|
|
mode?: "subagent" | "primary" | "all"
|
|
model?: string | { providerID: string; modelID: string }
|
|
}
|
|
const agents = normalizeSDKResponse(agentsResult, [] as AgentInfo[], {
|
|
preferResponseOnMissingData: true,
|
|
})
|
|
|
|
const callableAgents = agents.filter((a) => a.mode !== "primary")
|
|
|
|
const resolvedDisplayName = getAgentDisplayName(agentToUse)
|
|
const matchedAgent = callableAgents.find(
|
|
(agent) => agent.name.toLowerCase() === agentToUse.toLowerCase()
|
|
|| agent.name.toLowerCase() === resolvedDisplayName.toLowerCase()
|
|
)
|
|
if (!matchedAgent) {
|
|
const isPrimaryAgent = agents
|
|
.filter((a) => a.mode === "primary")
|
|
.find((agent) => agent.name.toLowerCase() === agentToUse.toLowerCase()
|
|
|| agent.name.toLowerCase() === resolvedDisplayName.toLowerCase())
|
|
|
|
if (isPrimaryAgent) {
|
|
return {
|
|
agentToUse: "",
|
|
categoryModel: undefined,
|
|
error: `Cannot call primary agent "${isPrimaryAgent.name}" via task. Primary agents are top-level orchestrators.`,
|
|
}
|
|
}
|
|
|
|
const availableAgents = callableAgents
|
|
.map((a) => a.name)
|
|
.sort()
|
|
.join(", ")
|
|
return {
|
|
agentToUse: "",
|
|
categoryModel: undefined,
|
|
error: `Unknown agent: "${agentToUse}". Available agents: ${availableAgents}`,
|
|
}
|
|
}
|
|
|
|
agentToUse = matchedAgent.name
|
|
|
|
const agentConfigKey = getAgentConfigKey(agentToUse)
|
|
const agentOverride = agentOverrides?.[agentConfigKey as keyof typeof agentOverrides]
|
|
?? (agentOverrides ? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentConfigKey)?.[1] : undefined)
|
|
const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentConfigKey]
|
|
const agentCategoryModel = agentOverride?.category
|
|
? userCategories?.[agentOverride.category]?.model
|
|
: undefined
|
|
const normalizedAgentFallbackModels = normalizeFallbackModels(
|
|
agentOverride?.fallback_models
|
|
?? (agentOverride?.category ? userCategories?.[agentOverride.category]?.fallback_models : undefined)
|
|
)
|
|
|
|
if (agentOverride?.model || agentCategoryModel || agentRequirement || matchedAgent.model) {
|
|
const availableModels = await getAvailableModelsForDelegateTask(client)
|
|
|
|
const normalizedMatchedModel = matchedAgent.model
|
|
? normalizeModelFormat(matchedAgent.model)
|
|
: undefined
|
|
const matchedAgentModelStr = normalizedMatchedModel
|
|
? `${normalizedMatchedModel.providerID}/${normalizedMatchedModel.modelID}`
|
|
: undefined
|
|
|
|
const resolution = resolveModelForDelegateTask({
|
|
userModel: agentOverride?.model ?? agentCategoryModel,
|
|
userFallbackModels: flattenToFallbackModelStrings(normalizedAgentFallbackModels),
|
|
categoryDefaultModel: matchedAgentModelStr,
|
|
fallbackChain: agentRequirement?.fallbackChain,
|
|
availableModels,
|
|
systemDefaultModel: undefined,
|
|
})
|
|
|
|
const resolutionSkipped = resolution && 'skipped' in resolution
|
|
|
|
if (resolution && !resolutionSkipped) {
|
|
const normalized = normalizeModelFormat(resolution.model)
|
|
if (normalized) {
|
|
const variantToUse = agentOverride?.variant ?? resolution.variant
|
|
categoryModel = variantToUse ? { ...normalized, variant: variantToUse } : normalized
|
|
}
|
|
} else if (resolutionSkipped && (agentOverride?.model ?? agentCategoryModel)) {
|
|
// Cold cache: resolution was skipped but user explicitly configured a model.
|
|
// Honor the user override directly — don't fall through to hardcoded fallback chain.
|
|
const normalized = normalizeModelFormat((agentOverride?.model ?? agentCategoryModel)!)
|
|
if (normalized) {
|
|
const agentCategoryVariant = agentOverride?.category
|
|
? userCategories?.[agentOverride.category]?.variant
|
|
: undefined
|
|
const variantToUse = agentOverride?.variant ?? agentCategoryVariant
|
|
categoryModel = variantToUse ? { ...normalized, variant: variantToUse } : normalized
|
|
log("[delegate-task] Cold cache: using explicit user override for subagent", {
|
|
agent: agentToUse,
|
|
model: agentOverride?.model ?? agentCategoryModel,
|
|
})
|
|
}
|
|
}
|
|
|
|
const defaultProviderID = categoryModel?.providerID
|
|
?? normalizedMatchedModel?.providerID
|
|
?? "opencode"
|
|
const configuredFallbackChain = buildFallbackChainFromModels(
|
|
normalizedAgentFallbackModels,
|
|
defaultProviderID,
|
|
)
|
|
// Don't assign hardcoded fallback chain when resolution was skipped (cold cache)
|
|
// — the chain may contain model IDs that don't exist in the provider yet.
|
|
fallbackChain = configuredFallbackChain ?? (resolutionSkipped ? undefined : agentRequirement?.fallbackChain)
|
|
|
|
// Only promote fallback-only settings when resolution actually selected a fallback model.
|
|
const resolvedFallbackEntry = (resolution && !('skipped' in resolution)) ? resolution.fallbackEntry : undefined
|
|
const matchedFallback = (resolution && !('skipped' in resolution)) ? resolution.matchedFallback === true : false
|
|
const effectiveEntry = matchedFallback && categoryModel
|
|
? (
|
|
resolvedFallbackEntry
|
|
?? (configuredFallbackChain
|
|
? findMostSpecificFallbackEntry(categoryModel.providerID, categoryModel.modelID, configuredFallbackChain)
|
|
: undefined)
|
|
)
|
|
: undefined
|
|
|
|
if (categoryModel && effectiveEntry) {
|
|
categoryModel = {
|
|
...categoryModel,
|
|
variant: agentOverride?.variant ?? effectiveEntry.variant ?? categoryModel.variant,
|
|
reasoningEffort: effectiveEntry.reasoningEffort,
|
|
temperature: effectiveEntry.temperature,
|
|
top_p: effectiveEntry.top_p,
|
|
maxTokens: effectiveEntry.maxTokens,
|
|
thinking: effectiveEntry.thinking,
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!categoryModel && matchedAgent.model) {
|
|
const normalizedMatchedModel = normalizeModelFormat(matchedAgent.model)
|
|
if (normalizedMatchedModel) {
|
|
categoryModel = normalizedMatchedModel
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
log("[delegate-task] Failed to resolve subagent execution", {
|
|
requestedAgent: agentToUse,
|
|
parentAgent,
|
|
error: errorMessage,
|
|
})
|
|
|
|
return {
|
|
agentToUse: "",
|
|
categoryModel: undefined,
|
|
error: `Failed to delegate to agent "${agentToUse}": ${errorMessage}`,
|
|
}
|
|
}
|
|
|
|
return { agentToUse, categoryModel, fallbackChain }
|
|
}
|