refactor: extract model selection logic from delegate-task into focused modules
- Create available-models.ts for model availability checking - Create model-selection.ts for category-to-model resolution logic - Update category-resolver, subagent-resolver, and sync modules to import from new focused modules instead of monolithic sources
This commit is contained in:
parent
caf08af88b
commit
c9be2e1696
64
src/tools/delegate-task/available-models.ts
Normal file
64
src/tools/delegate-task/available-models.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import type { OpencodeClient } from "./types"
|
||||||
|
import { log } from "../../shared/logger"
|
||||||
|
import { readConnectedProvidersCache, readProviderModelsCache } from "../../shared/connected-providers-cache"
|
||||||
|
|
||||||
|
function addFromProviderModels(
|
||||||
|
out: Set<string>,
|
||||||
|
providerID: string,
|
||||||
|
models: Array<string | { id?: string }> | undefined
|
||||||
|
): void {
|
||||||
|
if (!models) return
|
||||||
|
for (const item of models) {
|
||||||
|
const modelID = typeof item === "string" ? item : item?.id
|
||||||
|
if (!modelID) continue
|
||||||
|
out.add(`${providerID}/${modelID}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAvailableModelsForDelegateTask(client: OpencodeClient): Promise<Set<string>> {
|
||||||
|
const providerModelsCache = readProviderModelsCache()
|
||||||
|
|
||||||
|
if (providerModelsCache?.models) {
|
||||||
|
const connected = new Set(providerModelsCache.connected)
|
||||||
|
|
||||||
|
const out = new Set<string>()
|
||||||
|
for (const [providerID, models] of Object.entries(providerModelsCache.models)) {
|
||||||
|
if (!connected.has(providerID)) continue
|
||||||
|
addFromProviderModels(out, providerID, models as Array<string | { id?: string }> | undefined)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectedProviders = readConnectedProvidersCache()
|
||||||
|
|
||||||
|
if (!connectedProviders || connectedProviders.length === 0) {
|
||||||
|
return new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelList = (client as unknown as { model?: { list?: () => Promise<unknown> } })
|
||||||
|
?.model
|
||||||
|
?.list
|
||||||
|
|
||||||
|
if (!modelList) {
|
||||||
|
return new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await modelList()
|
||||||
|
const rows = Array.isArray(result)
|
||||||
|
? result
|
||||||
|
: ((result as { data?: unknown }).data as Array<{ provider?: string; id?: string }> | undefined) ?? []
|
||||||
|
|
||||||
|
const connected = new Set(connectedProviders)
|
||||||
|
const out = new Set<string>()
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!row?.provider || !row?.id) continue
|
||||||
|
if (!connected.has(row.provider)) continue
|
||||||
|
out.add(`${row.provider}/${row.id}`)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
} catch (err) {
|
||||||
|
log("[delegate-task] client.model.list failed", { error: String(err) })
|
||||||
|
return new Set()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import type { CategoryConfig, CategoriesConfig } from "../../config/schema"
|
import type { CategoryConfig, CategoriesConfig } from "../../config/schema"
|
||||||
import { DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS } from "./constants"
|
import { DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS } from "./constants"
|
||||||
import { resolveModel } from "../../shared"
|
import { resolveModel } from "../../shared/model-resolver"
|
||||||
import { isModelAvailable } from "../../shared/model-availability"
|
import { isModelAvailable } from "../../shared/model-availability"
|
||||||
import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared/logger"
|
||||||
|
|
||||||
export interface ResolveCategoryConfigOptions {
|
export interface ResolveCategoryConfigOptions {
|
||||||
userCategories?: CategoriesConfig
|
userCategories?: CategoriesConfig
|
||||||
|
|||||||
@ -5,10 +5,9 @@ import { DEFAULT_CATEGORIES } from "./constants"
|
|||||||
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
||||||
import { resolveCategoryConfig } from "./categories"
|
import { resolveCategoryConfig } from "./categories"
|
||||||
import { parseModelString } from "./model-string-parser"
|
import { parseModelString } from "./model-string-parser"
|
||||||
import { fetchAvailableModels } from "../../shared/model-availability"
|
|
||||||
import { readConnectedProvidersCache } from "../../shared/connected-providers-cache"
|
|
||||||
import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
||||||
import { resolveModelPipeline } from "../../shared"
|
import { getAvailableModelsForDelegateTask } from "./available-models"
|
||||||
|
import { resolveModelForDelegateTask } from "./model-selection"
|
||||||
|
|
||||||
export interface CategoryResolutionResult {
|
export interface CategoryResolutionResult {
|
||||||
agentToUse: string
|
agentToUse: string
|
||||||
@ -28,10 +27,7 @@ export async function resolveCategoryExecution(
|
|||||||
): Promise<CategoryResolutionResult> {
|
): Promise<CategoryResolutionResult> {
|
||||||
const { client, userCategories, sisyphusJuniorModel } = executorCtx
|
const { client, userCategories, sisyphusJuniorModel } = executorCtx
|
||||||
|
|
||||||
const connectedProviders = readConnectedProvidersCache()
|
const availableModels = await getAvailableModelsForDelegateTask(client)
|
||||||
const availableModels = await fetchAvailableModels(client, {
|
|
||||||
connectedProviders: connectedProviders ?? undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const resolved = resolveCategoryConfig(args.category!, {
|
const resolved = resolveCategoryConfig(args.category!, {
|
||||||
userCategories,
|
userCategories,
|
||||||
@ -71,20 +67,16 @@ export async function resolveCategoryExecution(
|
|||||||
: { model: actualModel, type: "system-default", source: "system-default" }
|
: { model: actualModel, type: "system-default", source: "system-default" }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const resolution = resolveModelPipeline({
|
const resolution = resolveModelForDelegateTask({
|
||||||
intent: {
|
|
||||||
userModel: explicitCategoryModel ?? overrideModel,
|
userModel: explicitCategoryModel ?? overrideModel,
|
||||||
categoryDefaultModel: resolved.model,
|
categoryDefaultModel: resolved.model,
|
||||||
},
|
|
||||||
constraints: { availableModels },
|
|
||||||
policy: {
|
|
||||||
fallbackChain: requirement.fallbackChain,
|
fallbackChain: requirement.fallbackChain,
|
||||||
|
availableModels,
|
||||||
systemDefaultModel,
|
systemDefaultModel,
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
const { model: resolvedModel, provenance, variant: resolvedVariant } = resolution
|
const { model: resolvedModel, variant: resolvedVariant } = resolution
|
||||||
actualModel = resolvedModel
|
actualModel = resolvedModel
|
||||||
|
|
||||||
if (!parseModelString(actualModel)) {
|
if (!parseModelString(actualModel)) {
|
||||||
@ -99,20 +91,19 @@ export async function resolveCategoryExecution(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let type: "user-defined" | "inherited" | "category-default" | "system-default"
|
const type: "user-defined" | "inherited" | "category-default" | "system-default" =
|
||||||
const source = provenance
|
(explicitCategoryModel || overrideModel)
|
||||||
switch (provenance) {
|
? "user-defined"
|
||||||
case "override":
|
: (systemDefaultModel && actualModel === systemDefaultModel)
|
||||||
type = "user-defined"
|
? "system-default"
|
||||||
break
|
: "category-default"
|
||||||
case "category-default":
|
|
||||||
case "provider-fallback":
|
const source: "override" | "category-default" | "system-default" =
|
||||||
type = "category-default"
|
type === "user-defined"
|
||||||
break
|
? "override"
|
||||||
case "system-default":
|
: type === "system-default"
|
||||||
type = "system-default"
|
? "system-default"
|
||||||
break
|
: "category-default"
|
||||||
}
|
|
||||||
|
|
||||||
modelInfo = { model: actualModel, type, source }
|
modelInfo = { model: actualModel, type, source }
|
||||||
|
|
||||||
|
|||||||
67
src/tools/delegate-task/model-selection.ts
Normal file
67
src/tools/delegate-task/model-selection.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import type { FallbackEntry } from "../../shared/model-requirements"
|
||||||
|
import { fuzzyMatchModel } from "../../shared/model-availability"
|
||||||
|
|
||||||
|
function normalizeModel(model?: string): string | undefined {
|
||||||
|
const trimmed = model?.trim()
|
||||||
|
return trimmed || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveModelForDelegateTask(input: {
|
||||||
|
userModel?: string
|
||||||
|
categoryDefaultModel?: string
|
||||||
|
fallbackChain?: FallbackEntry[]
|
||||||
|
availableModels: Set<string>
|
||||||
|
systemDefaultModel?: string
|
||||||
|
}): { model: string; variant?: string } | undefined {
|
||||||
|
const userModel = normalizeModel(input.userModel)
|
||||||
|
if (userModel) {
|
||||||
|
return { model: userModel }
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryDefault = normalizeModel(input.categoryDefaultModel)
|
||||||
|
if (categoryDefault) {
|
||||||
|
if (input.availableModels.size === 0) {
|
||||||
|
return { model: categoryDefault }
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = categoryDefault.split("/")
|
||||||
|
const providerHint = parts.length >= 2 ? [parts[0]] : undefined
|
||||||
|
const match = fuzzyMatchModel(categoryDefault, input.availableModels, providerHint)
|
||||||
|
if (match) {
|
||||||
|
return { model: match }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackChain = input.fallbackChain
|
||||||
|
if (fallbackChain && fallbackChain.length > 0) {
|
||||||
|
if (input.availableModels.size === 0) {
|
||||||
|
const first = fallbackChain[0]
|
||||||
|
const provider = first?.providers?.[0]
|
||||||
|
if (provider) {
|
||||||
|
return { model: `${provider}/${first.model}`, variant: first.variant }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const entry of fallbackChain) {
|
||||||
|
for (const provider of entry.providers) {
|
||||||
|
const fullModel = `${provider}/${entry.model}`
|
||||||
|
const match = fuzzyMatchModel(fullModel, input.availableModels, [provider])
|
||||||
|
if (match) {
|
||||||
|
return { model: match, variant: entry.variant }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const crossProviderMatch = fuzzyMatchModel(entry.model, input.availableModels)
|
||||||
|
if (crossProviderMatch) {
|
||||||
|
return { model: crossProviderMatch, variant: entry.variant }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemDefaultModel = normalizeModel(input.systemDefaultModel)
|
||||||
|
if (systemDefaultModel) {
|
||||||
|
return { model: systemDefaultModel }
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
@ -2,7 +2,8 @@ import type { ToolContextWithMetadata } from "./types"
|
|||||||
import type { ParentContext } from "./executor-types"
|
import type { ParentContext } from "./executor-types"
|
||||||
import { findNearestMessageWithFields, findFirstMessageWithAgent } from "../../features/hook-message-injector"
|
import { findNearestMessageWithFields, findFirstMessageWithAgent } from "../../features/hook-message-injector"
|
||||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||||
import { log, getMessageDir } from "../../shared"
|
import { log } from "../../shared/logger"
|
||||||
|
import { getMessageDir } from "../../shared/session-utils"
|
||||||
|
|
||||||
export function resolveParentContext(ctx: ToolContextWithMetadata): ParentContext {
|
export function resolveParentContext(ctx: ToolContextWithMetadata): ParentContext {
|
||||||
const messageDir = getMessageDir(ctx.sessionID)
|
const messageDir = getMessageDir(ctx.sessionID)
|
||||||
|
|||||||
@ -3,10 +3,9 @@ import type { ExecutorContext } from "./executor-types"
|
|||||||
import { isPlanFamily } from "./constants"
|
import { isPlanFamily } from "./constants"
|
||||||
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
||||||
import { parseModelString } from "./model-string-parser"
|
import { parseModelString } from "./model-string-parser"
|
||||||
import { resolveModelPipeline } from "../../shared"
|
|
||||||
import { fetchAvailableModels } from "../../shared/model-availability"
|
|
||||||
import { readConnectedProvidersCache } from "../../shared/connected-providers-cache"
|
|
||||||
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
||||||
|
import { getAvailableModelsForDelegateTask } from "./available-models"
|
||||||
|
import { resolveModelForDelegateTask } from "./model-selection"
|
||||||
|
|
||||||
export async function resolveSubagentExecution(
|
export async function resolveSubagentExecution(
|
||||||
args: DelegateTaskArgs,
|
args: DelegateTaskArgs,
|
||||||
@ -86,26 +85,19 @@ Create the work plan directly - that's your job as the planning agent.`,
|
|||||||
?? (agentOverrides ? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentNameLower)?.[1] : undefined)
|
?? (agentOverrides ? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentNameLower)?.[1] : undefined)
|
||||||
const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentNameLower]
|
const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentNameLower]
|
||||||
|
|
||||||
if (agentOverride?.model || agentRequirement) {
|
if (agentOverride?.model || agentRequirement || matchedAgent.model) {
|
||||||
const connectedProviders = readConnectedProvidersCache()
|
const availableModels = await getAvailableModelsForDelegateTask(client)
|
||||||
const availableModels = await fetchAvailableModels(client, {
|
|
||||||
connectedProviders: connectedProviders ?? undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const matchedAgentModelStr = matchedAgent.model
|
const matchedAgentModelStr = matchedAgent.model
|
||||||
? `${matchedAgent.model.providerID}/${matchedAgent.model.modelID}`
|
? `${matchedAgent.model.providerID}/${matchedAgent.model.modelID}`
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const resolution = resolveModelPipeline({
|
const resolution = resolveModelForDelegateTask({
|
||||||
intent: {
|
|
||||||
userModel: agentOverride?.model,
|
userModel: agentOverride?.model,
|
||||||
categoryDefaultModel: matchedAgentModelStr,
|
categoryDefaultModel: matchedAgentModelStr,
|
||||||
},
|
|
||||||
constraints: { availableModels },
|
|
||||||
policy: {
|
|
||||||
fallbackChain: agentRequirement?.fallbackChain,
|
fallbackChain: agentRequirement?.fallbackChain,
|
||||||
|
availableModels,
|
||||||
systemDefaultModel: undefined,
|
systemDefaultModel: undefined,
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
@ -115,7 +107,9 @@ Create the work plan directly - that's your job as the planning agent.`,
|
|||||||
categoryModel = variantToUse ? { ...parsed, variant: variantToUse } : parsed
|
categoryModel = variantToUse ? { ...parsed, variant: variantToUse } : parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (matchedAgent.model) {
|
}
|
||||||
|
|
||||||
|
if (!categoryModel && matchedAgent.model) {
|
||||||
categoryModel = matchedAgent.model
|
categoryModel = matchedAgent.model
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import type { ExecutorContext, SessionMessage } from "./executor-types"
|
|||||||
import { isPlanFamily } from "./constants"
|
import { isPlanFamily } from "./constants"
|
||||||
import { storeToolMetadata } from "../../features/tool-metadata-store"
|
import { storeToolMetadata } from "../../features/tool-metadata-store"
|
||||||
import { getTaskToastManager } from "../../features/task-toast-manager"
|
import { getTaskToastManager } from "../../features/task-toast-manager"
|
||||||
import { getAgentToolRestrictions, getMessageDir, promptSyncWithModelSuggestionRetry } from "../../shared"
|
import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions"
|
||||||
|
import { getMessageDir } from "../../shared/session-utils"
|
||||||
|
import { promptSyncWithModelSuggestionRetry } from "../../shared/model-suggestion-retry"
|
||||||
import { findNearestMessageWithFields } from "../../features/hook-message-injector"
|
import { findNearestMessageWithFields } from "../../features/hook-message-injector"
|
||||||
import { formatDuration } from "./time-formatter"
|
import { formatDuration } from "./time-formatter"
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { DelegateTaskArgs, OpencodeClient } from "./types"
|
import type { DelegateTaskArgs, OpencodeClient } from "./types"
|
||||||
import { isPlanFamily } from "./constants"
|
import { isPlanFamily } from "./constants"
|
||||||
import { promptSyncWithModelSuggestionRetry } from "../../shared"
|
import { promptSyncWithModelSuggestionRetry } from "../../shared/model-suggestion-retry"
|
||||||
import { formatDetailedError } from "./error-formatting"
|
import { formatDetailedError } from "./error-formatting"
|
||||||
|
|
||||||
export async function sendSyncPrompt(
|
export async function sendSyncPrompt(
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { ToolContextWithMetadata, OpencodeClient } from "./types"
|
import type { ToolContextWithMetadata, OpencodeClient } from "./types"
|
||||||
import { getTimingConfig } from "./timing"
|
import { getTimingConfig } from "./timing"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared/logger"
|
||||||
|
|
||||||
export async function pollSyncSession(
|
export async function pollSyncSession(
|
||||||
ctx: ToolContextWithMetadata,
|
ctx: ToolContextWithMetadata,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { ExecutorContext, ParentContext } from "./executor-types"
|
|||||||
import { getTaskToastManager } from "../../features/task-toast-manager"
|
import { getTaskToastManager } from "../../features/task-toast-manager"
|
||||||
import { storeToolMetadata } from "../../features/tool-metadata-store"
|
import { storeToolMetadata } from "../../features/tool-metadata-store"
|
||||||
import { subagentSessions } from "../../features/claude-code-session-state"
|
import { subagentSessions } from "../../features/claude-code-session-state"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared/logger"
|
||||||
import { formatDuration } from "./time-formatter"
|
import { formatDuration } from "./time-formatter"
|
||||||
import { formatDetailedError } from "./error-formatting"
|
import { formatDetailedError } from "./error-formatting"
|
||||||
import { createSyncSession } from "./sync-session-creator"
|
import { createSyncSession } from "./sync-session-creator"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
|
||||||
import type { DelegateTaskArgs, ToolContextWithMetadata, DelegateTaskToolOptions } from "./types"
|
import type { DelegateTaskArgs, ToolContextWithMetadata, DelegateTaskToolOptions } from "./types"
|
||||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "./constants"
|
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "./constants"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared/logger"
|
||||||
import { buildSystemContent } from "./prompt-builder"
|
import { buildSystemContent } from "./prompt-builder"
|
||||||
import type {
|
import type {
|
||||||
AvailableCategory,
|
AvailableCategory,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user