Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/plugin/tool-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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),
Comment thread
WarGloom marked this conversation as resolved.
pluginConfig.agent_definitions
? loadAgentDefinitions(pluginConfig.agent_definitions, "definition-file")
: {},
]
const discoveredNames = new Set<string>()
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,
Expand Down
66 changes: 66 additions & 0 deletions src/tools/delegate-task/task-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,72 @@ function createDelegateTask(...args: Parameters<typeof import("./tools").createD
expect(description).not.toContain("hephaestus")
expect(description).not.toContain("prometheus")
})

test("#given no availableSubagentNames #when tool is created #then subagent_type is plain string (back-compat)", () => {
//#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<string, string>
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 {}
32 changes: 23 additions & 9 deletions src/tools/delegate-task/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/tools/delegate-task/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface DelegateTaskToolOptions {
modelFallbackControllerAccessor?: ModelFallbackControllerAccessor
onSyncSessionCreated?: (event: SyncSessionCreatedEvent) => Promise<void>
syncPollTimeoutMs?: number
availableSubagentNames?: readonly string[]
}

import type { DelegatedModelConfig } from "../../shared/model-resolution-types"
Expand Down
Loading