diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index a3e46185a0..1ff25c5896 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -29,6 +29,25 @@ import { createTaskUpdateTool, createHashlineEditTool, } from "../tools" +import { + loadUserAgents, + loadProjectAgents, + loadOpencodeGlobalAgents, + loadOpencodeProjectAgents, + readOpencodeConfigAgents, + loadAgentDefinitions, +} from "../features/claude-code-agent-loader" +// Mirror of runtime-registered built-in subagents. Sync with the agent registry. +const BUILTIN_SUBAGENT_NAMES = [ + "explore", + "librarian", + "oracle", + "hephaestus", + "multimodal-looker", + "metis", + "momus", + "sisyphus-junior", +] as const import { getMainSessionID } from "../features/claude-code-session-state" import { filterDisabledTools } from "../shared/disabled-tools" import { isTaskSystemEnabled, log } from "../shared" @@ -178,10 +197,39 @@ export function createToolRegistry(args: { ) const lookAt = isMultimodalLookerEnabled ? factories.createLookAt(ctx) : null + const disabledAgents = new Set((pluginConfig.disabled_agents ?? []).map((a) => a.toLowerCase())) + const includeClaudeAgents = pluginConfig.claude_code?.agents ?? true + const agentDicts = [ + includeClaudeAgents ? loadUserAgents() : {}, + includeClaudeAgents ? loadProjectAgents(ctx.directory) : {}, + loadOpencodeGlobalAgents(), + loadOpencodeProjectAgents(ctx.directory), + readOpencodeConfigAgents(ctx.directory), + pluginConfig.agent_definitions + ? loadAgentDefinitions(pluginConfig.agent_definitions, "definition-file") + : {}, + ] + const discoveredNames = new Set() + for (const dict of agentDicts) { + for (const [name, cfg] of Object.entries(dict)) { + const mode = (cfg as { mode?: string } | null | undefined)?.mode + if (mode === "subagent" || mode === "all" || mode === undefined) { + discoveredNames.add(name) + } + } + } + const builtinNames = [...BUILTIN_SUBAGENT_NAMES] + const availableSubagentNames = Array.from( + new Set([...builtinNames, ...discoveredNames]), + ) + .filter((name) => !disabledAgents.has(name.toLowerCase())) + .sort((a, b) => a.localeCompare(b)) + const delegateTask = factories.createDelegateTask({ manager: managers.backgroundManager, client: ctx.client, directory: ctx.directory, + availableSubagentNames, userCategories: pluginConfig.categories, agentOverrides: pluginConfig.agents, gitMasterConfig: pluginConfig.git_master, diff --git a/src/tools/delegate-task/task-schema.test.ts b/src/tools/delegate-task/task-schema.test.ts index c50d175bcb..dbfb42b5f8 100644 --- a/src/tools/delegate-task/task-schema.test.ts +++ b/src/tools/delegate-task/task-schema.test.ts @@ -46,6 +46,72 @@ function createDelegateTask(...args: Parameters { + //#given + const toolDefinition = createDelegateTask({ manager: {} as never, client: {} as never, directory: "/tmp/test" }) + + //#when + const subagentSchema = toolDefinition.args.subagent_type as unknown as { + def: { type: string; innerType: { def: { type: string } } } + } + + //#then + expect(subagentSchema.def.type).toBe("optional") + expect(subagentSchema.def.innerType.def.type).toBe("string") + }) + + test("#given availableSubagentNames option #when tool is created #then subagent_type emits enum with those names", () => { + //#given + const toolDefinition = createDelegateTask({ + manager: {} as never, + client: {} as never, + directory: "/tmp/test", + availableSubagentNames: ["oracle", "librarian", "dev"], + }) + + //#when + const subagentSchema = toolDefinition.args.subagent_type as unknown as { + def: { + type: string + innerType: { + def: { + type: string + entries?: Record + values?: string[] + } + } + } + } + + //#then + expect(subagentSchema.def.type).toBe("optional") + expect(subagentSchema.def.innerType.def.type).toBe("enum") + const inner = subagentSchema.def.innerType.def + const values = inner.entries ? Object.values(inner.entries) : (inner.values ?? []) + expect(values).toContain("oracle") + expect(values).toContain("librarian") + expect(values).toContain("dev") + }) + + test("#given empty availableSubagentNames #when tool is created #then subagent_type stays plain string", () => { + //#given + const toolDefinition = createDelegateTask({ + manager: {} as never, + client: {} as never, + directory: "/tmp/test", + availableSubagentNames: [], + }) + + //#when + const subagentSchema = toolDefinition.args.subagent_type as unknown as { + def: { type: string; innerType: { def: { type: string } } } + } + + //#then + expect(subagentSchema.def.type).toBe("optional") + expect(subagentSchema.def.innerType.def.type).toBe("string") + }) }) export {} diff --git a/src/tools/delegate-task/tools.ts b/src/tools/delegate-task/tools.ts index 268c455ca9..d1b31b6292 100644 --- a/src/tools/delegate-task/tools.ts +++ b/src/tools/delegate-task/tools.ts @@ -20,19 +20,33 @@ export { resolveCategoryConfig } from "./categories" export type { SyncSessionCreatedEvent, DelegateTaskToolOptions, BuildSystemContentInput } from "./types" export { buildSystemContent, buildTaskPrompt } from "./prompt-builder" -const delegateTaskArgsSchema = { - load_skills: tool.schema.array(tool.schema.string()).describe("Skill names to inject. REQUIRED - pass [] if no skills needed."), - description: tool.schema.string().optional().describe("Short task description (3-5 words). Auto-generated from prompt if omitted."), - prompt: tool.schema.string().describe("Full detailed prompt for the agent"), - run_in_background: tool.schema.boolean().describe("REQUIRED. true=async (returns task_id), false=sync (waits). Use false for task delegation, true ONLY for parallel exploration."), - category: tool.schema.string().optional().describe("REQUIRED if subagent_type not provided. Do NOT provide both category and subagent_type."), - subagent_type: tool.schema.string().optional().describe("REQUIRED if category not provided. Do NOT provide both category and subagent_type."), - task_id: tool.schema.string().optional().describe("Existing task to continue. Canonical resume identifier."), - command: tool.schema.string().optional().describe("The command that triggered this task"), +function buildSubagentTypeSchema(availableSubagentNames?: readonly string[]) { + const describe = "REQUIRED if category not provided. Do NOT provide both category and subagent_type." + if (availableSubagentNames && availableSubagentNames.length > 0) { + return tool.schema + .enum(availableSubagentNames as unknown as [string, ...string[]]) + .optional() + .describe(describe) + } + return tool.schema.string().optional().describe(describe) +} + +function buildDelegateTaskArgsSchema(availableSubagentNames?: readonly string[]) { + return { + load_skills: tool.schema.array(tool.schema.string()).describe("Skill names to inject. REQUIRED - pass [] if no skills needed."), + description: tool.schema.string().optional().describe("Short task description (3-5 words). Auto-generated from prompt if omitted."), + prompt: tool.schema.string().describe("Full detailed prompt for the agent"), + run_in_background: tool.schema.boolean().describe("REQUIRED. true=async (returns task_id), false=sync (waits). Use false for task delegation, true ONLY for parallel exploration."), + category: tool.schema.string().optional().describe("REQUIRED if subagent_type not provided. Do NOT provide both category and subagent_type."), + subagent_type: buildSubagentTypeSchema(availableSubagentNames), + task_id: tool.schema.string().optional().describe("Existing task to continue. Canonical resume identifier."), + command: tool.schema.string().optional().describe("The command that triggered this task"), + } } export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefinition { const { availableCategories, availableSkills, categoryExamples, description } = createDelegateTaskPresentation(options) + const delegateTaskArgsSchema = buildDelegateTaskArgsSchema(options.availableSubagentNames) return tool({ description, diff --git a/src/tools/delegate-task/types.ts b/src/tools/delegate-task/types.ts index 1eb767960a..98f2c7bc4d 100644 --- a/src/tools/delegate-task/types.ts +++ b/src/tools/delegate-task/types.ts @@ -69,6 +69,7 @@ export interface DelegateTaskToolOptions { modelFallbackControllerAccessor?: ModelFallbackControllerAccessor onSyncSessionCreated?: (event: SyncSessionCreatedEvent) => Promise syncPollTimeoutMs?: number + availableSubagentNames?: readonly string[] } import type { DelegatedModelConfig } from "../../shared/model-resolution-types"