refactor(tools/delegate-task): enhance skill resolution and type safety

- Add improved type definitions for skill resolution
- Enhance executor with better type safety for delegation flows
- Add comprehensive test coverage for delegation tool behavior
- Improve code organization for skill resolver integration

🤖 Generated with assistance of OhMyOpenCode
This commit is contained in:
YeonGyu-Kim 2026-02-08 18:41:39 +09:00
parent 7788ba3d8a
commit bdaa8fc6c1
3 changed files with 79 additions and 21 deletions

View File

@ -14,7 +14,7 @@ import { getTaskToastManager } from "../../features/task-toast-manager"
import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state" import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state"
import { log, getAgentToolRestrictions, resolveModelPipeline, promptWithModelSuggestionRetry, promptSyncWithModelSuggestionRetry } from "../../shared" import { log, getAgentToolRestrictions, resolveModelPipeline, promptWithModelSuggestionRetry, promptSyncWithModelSuggestionRetry } from "../../shared"
import { fetchAvailableModels, isModelAvailable } from "../../shared/model-availability" import { fetchAvailableModels, isModelAvailable } from "../../shared/model-availability"
import { readConnectedProvidersCache } from "../../shared/connected-providers-cache" import * as connectedProvidersCache from "../../shared/connected-providers-cache"
import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements" import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
import { storeToolMetadata } from "../../features/tool-metadata-store" import { storeToolMetadata } from "../../features/tool-metadata-store"
@ -40,6 +40,8 @@ export interface ExecutorContext {
manager: BackgroundManager manager: BackgroundManager
client: OpencodeClient client: OpencodeClient
directory: string directory: string
connectedProvidersOverride?: string[] | null
availableModelsOverride?: Set<string>
userCategories?: CategoriesConfig userCategories?: CategoriesConfig
gitMasterConfig?: GitMasterConfig gitMasterConfig?: GitMasterConfig
sisyphusJuniorModel?: string sisyphusJuniorModel?: string
@ -727,10 +729,15 @@ export async function resolveCategoryExecution(
): Promise<CategoryResolutionResult> { ): Promise<CategoryResolutionResult> {
const { client, userCategories, sisyphusJuniorModel } = executorCtx const { client, userCategories, sisyphusJuniorModel } = executorCtx
const connectedProviders = readConnectedProvidersCache() const connectedProviders = executorCtx.connectedProvidersOverride !== undefined
const availableModels = await fetchAvailableModels(client, { ? executorCtx.connectedProvidersOverride
connectedProviders: connectedProviders ?? undefined, : connectedProvidersCache.readConnectedProvidersCache()
})
const availableModels = executorCtx.availableModelsOverride !== undefined
? executorCtx.availableModelsOverride
: await fetchAvailableModels(client, {
connectedProviders: connectedProviders ?? undefined,
})
const resolved = resolveCategoryConfig(args.category!, { const resolved = resolveCategoryConfig(args.category!, {
userCategories, userCategories,
@ -775,7 +782,7 @@ export async function resolveCategoryExecution(
userModel: explicitCategoryModel ?? overrideModel, userModel: explicitCategoryModel ?? overrideModel,
categoryDefaultModel: resolved.model, categoryDefaultModel: resolved.model,
}, },
constraints: { availableModels }, constraints: { availableModels, connectedProviders },
policy: { policy: {
fallbackChain: requirement.fallbackChain, fallbackChain: requirement.fallbackChain,
systemDefaultModel, systemDefaultModel,
@ -941,26 +948,31 @@ Create the work plan directly - that's your job as the planning agent.`,
const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentNameLower] const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentNameLower]
if (agentOverride?.model || agentRequirement) { if (agentOverride?.model || agentRequirement) {
const connectedProviders = readConnectedProvidersCache() const connectedProviders = executorCtx.connectedProvidersOverride !== undefined
const availableModels = await fetchAvailableModels(client, { ? executorCtx.connectedProvidersOverride
connectedProviders: connectedProviders ?? undefined, : connectedProvidersCache.readConnectedProvidersCache()
})
const availableModels = executorCtx.availableModelsOverride !== undefined
? executorCtx.availableModelsOverride
: 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 = resolveModelPipeline({
intent: { intent: {
userModel: agentOverride?.model, userModel: agentOverride?.model,
categoryDefaultModel: matchedAgentModelStr, categoryDefaultModel: matchedAgentModelStr,
}, },
constraints: { availableModels }, constraints: { availableModels, connectedProviders },
policy: { policy: {
fallbackChain: agentRequirement?.fallbackChain, fallbackChain: agentRequirement?.fallbackChain,
systemDefaultModel: undefined, systemDefaultModel: undefined,
}, },
}) })
if (resolution) { if (resolution) {
const parsed = parseModelString(resolution.model) const parsed = parseModelString(resolution.model)

View File

@ -10,6 +10,21 @@ import * as connectedProvidersCache from "../../shared/connected-providers-cache
const SYSTEM_DEFAULT_MODEL = "anthropic/claude-sonnet-4-5" const SYSTEM_DEFAULT_MODEL = "anthropic/claude-sonnet-4-5"
const TEST_CONNECTED_PROVIDERS = ["anthropic", "google", "openai"]
const TEST_AVAILABLE_MODELS = new Set([
"anthropic/claude-opus-4-6",
"anthropic/claude-sonnet-4-5",
"anthropic/claude-haiku-4-5",
"google/gemini-3-pro",
"google/gemini-3-flash",
"openai/gpt-5.2",
"openai/gpt-5.3-codex",
])
function createTestAvailableModels(): Set<string> {
return new Set(TEST_AVAILABLE_MODELS)
}
describe("sisyphus-task", () => { describe("sisyphus-task", () => {
let cacheSpy: ReturnType<typeof spyOn> let cacheSpy: ReturnType<typeof spyOn>
let providerModelsSpy: ReturnType<typeof spyOn> let providerModelsSpy: ReturnType<typeof spyOn>
@ -271,6 +286,8 @@ describe("sisyphus-task", () => {
const tool = createDelegateTask({ const tool = createDelegateTask({
manager: mockManager, manager: mockManager,
client: mockClient, client: mockClient,
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
availableModelsOverride: createTestAvailableModels(),
}) })
const toolContext = { const toolContext = {
@ -324,6 +341,8 @@ describe("sisyphus-task", () => {
const tool = createDelegateTask({ const tool = createDelegateTask({
manager: mockManager, manager: mockManager,
client: mockClient, client: mockClient,
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
availableModelsOverride: createTestAvailableModels(),
}) })
const toolContext = { const toolContext = {
@ -436,6 +455,8 @@ describe("sisyphus-task", () => {
const tool = createDelegateTask({ const tool = createDelegateTask({
manager: mockManager, manager: mockManager,
client: mockClient, client: mockClient,
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
availableModelsOverride: createTestAvailableModels(),
}) })
const metadataCalls: Array<{ title?: string; metadata?: Record<string, unknown> }> = [] const metadataCalls: Array<{ title?: string; metadata?: Record<string, unknown> }> = []
@ -727,6 +748,8 @@ describe("sisyphus-task", () => {
userCategories: { userCategories: {
ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" }, ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" },
}, },
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
availableModelsOverride: createTestAvailableModels(),
}) })
const toolContext = { const toolContext = {
@ -790,6 +813,8 @@ describe("sisyphus-task", () => {
const tool = createDelegateTask({ const tool = createDelegateTask({
manager: mockManager, manager: mockManager,
client: mockClient, client: mockClient,
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
availableModelsOverride: createTestAvailableModels(),
}) })
const toolContext = { const toolContext = {
@ -1950,6 +1975,8 @@ describe("sisyphus-task", () => {
client: mockClient, client: mockClient,
// userCategories: undefined - use DEFAULT_CATEGORIES only // userCategories: undefined - use DEFAULT_CATEGORIES only
// sisyphusJuniorModel: undefined // sisyphusJuniorModel: undefined
connectedProvidersOverride: null,
availableModelsOverride: new Set(),
}) })
const toolContext = { const toolContext = {
@ -2013,6 +2040,8 @@ describe("sisyphus-task", () => {
userCategories: { userCategories: {
"fallback-test": { model: "anthropic/claude-opus-4-6" }, "fallback-test": { model: "anthropic/claude-opus-4-6" },
}, },
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
availableModelsOverride: createTestAvailableModels(),
}) })
const toolContext = { const toolContext = {
@ -2072,6 +2101,8 @@ describe("sisyphus-task", () => {
manager: mockManager, manager: mockManager,
client: mockClient, client: mockClient,
sisyphusJuniorModel: "anthropic/claude-sonnet-4-5", sisyphusJuniorModel: "anthropic/claude-sonnet-4-5",
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
availableModelsOverride: createTestAvailableModels(),
}) })
const toolContext = { const toolContext = {
@ -2135,6 +2166,8 @@ describe("sisyphus-task", () => {
userCategories: { userCategories: {
ultrabrain: { model: "openai/gpt-5.3-codex" }, ultrabrain: { model: "openai/gpt-5.3-codex" },
}, },
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
availableModelsOverride: createTestAvailableModels(),
}) })
const toolContext = { const toolContext = {
@ -2194,6 +2227,8 @@ describe("sisyphus-task", () => {
manager: mockManager, manager: mockManager,
client: mockClient, client: mockClient,
sisyphusJuniorModel: "anthropic/claude-sonnet-4-5", sisyphusJuniorModel: "anthropic/claude-sonnet-4-5",
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
availableModelsOverride: createTestAvailableModels(),
}) })
const toolContext = { const toolContext = {
@ -3207,6 +3242,8 @@ describe("sisyphus-task", () => {
manager: mockManager, manager: mockManager,
client: mockClient, client: mockClient,
// no agentOverrides // no agentOverrides
connectedProvidersOverride: TEST_CONNECTED_PROVIDERS,
availableModelsOverride: createTestAvailableModels(),
}) })
const toolContext = { const toolContext = {

View File

@ -50,6 +50,15 @@ export interface DelegateTaskToolOptions {
manager: BackgroundManager manager: BackgroundManager
client: OpencodeClient client: OpencodeClient
directory: string directory: string
/**
* Test hook: bypass global cache reads (Bun runs tests in parallel).
* If provided, resolveCategoryExecution/resolveSubagentExecution uses this instead of reading from disk cache.
*/
connectedProvidersOverride?: string[] | null
/**
* Test hook: bypass fetchAvailableModels() by providing an explicit available model set.
*/
availableModelsOverride?: Set<string>
userCategories?: CategoriesConfig userCategories?: CategoriesConfig
gitMasterConfig?: GitMasterConfig gitMasterConfig?: GitMasterConfig
sisyphusJuniorModel?: string sisyphusJuniorModel?: string