diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index af130a70d919..0f55068fbb5f 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -15,7 +15,18 @@ import { Permission } from "@/permission" const parameters = z.object({ description: z.string().describe("A short (3-5 words) description of the task"), prompt: z.string().describe("The task for the agent to perform"), - subagent_type: z.string().describe("The type of specialized agent to use for this task"), + subagent_type: z + .string() + .describe("The type of specialized agent to use for this task") + .optional(), + agent: z + .string() + .describe("Alias for subagent_type — the agent to use for this task") + .optional(), + agent_type: z + .string() + .describe("Alias for subagent_type — the agent to use for this task") + .optional(), task_id: z .string() .describe( @@ -25,6 +36,19 @@ const parameters = z.object({ command: z.string().describe("The command that triggered this task").optional(), }) +/** + * Resolve the agent type from params, accepting "agent" and "agent_type" as + * aliases for "subagent_type". Non-Claude models frequently use the shorter + * parameter names when calling the task tool. + */ +export function resolveAgentType(params: z.infer): string { + const resolved = params.subagent_type || params.agent || params.agent_type + if (!resolved) { + throw new Error("One of subagent_type, agent, or agent_type is required") + } + return resolved +} + export const TaskTool = Tool.define("task", async (ctx) => { const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary")) @@ -45,23 +69,25 @@ export const TaskTool = Tool.define("task", async (ctx) => { description, parameters, async execute(params: z.infer, ctx) { + const agentType = resolveAgentType(params) + const config = await Config.get() // Skip permission check when user explicitly invoked via @ or command subtask if (!ctx.extra?.bypassAgentCheck) { await ctx.ask({ permission: "task", - patterns: [params.subagent_type], + patterns: [agentType], always: ["*"], metadata: { description: params.description, - subagent_type: params.subagent_type, + subagent_type: agentType, }, }) } - const agent = await Agent.get(params.subagent_type) - if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`) + const agent = await Agent.get(agentType) + if (!agent) throw new Error(`Unknown agent type: ${agentType} is not a valid agent type`) const hasTaskPermission = agent.permission.some((rule) => rule.permission === "task") const hasTodoWritePermission = agent.permission.some((rule) => rule.permission === "todowrite") diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 6f81ffca39f7..1731febffd1a 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -628,7 +628,9 @@ it.live( parameters: z.object({ description: z.string(), prompt: z.string(), - subagent_type: z.string(), + subagent_type: z.string().optional(), + agent: z.string().optional(), + agent_type: z.string().optional(), task_id: z.string().optional(), command: z.string().optional(), }), diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index aae48a30ab3f..237dbf672112 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test" import { Agent } from "../../src/agent/agent" import { Instance } from "../../src/project/instance" -import { TaskTool } from "../../src/tool/task" +import { TaskTool, resolveAgentType } from "../../src/tool/task" import { tmpdir } from "../fixture/fixture" afterEach(async () => { @@ -47,3 +47,62 @@ describe("tool.task", () => { }) }) }) + +describe("resolveAgentType", () => { + test("returns subagent_type when provided", () => { + const result = resolveAgentType({ + description: "test", + prompt: "test", + subagent_type: "Engineer", + }) + expect(result).toBe("Engineer") + }) + + test("falls back to agent when subagent_type is missing", () => { + const result = resolveAgentType({ + description: "test", + prompt: "test", + agent: "Engineer", + }) + expect(result).toBe("Engineer") + }) + + test("falls back to agent_type when subagent_type and agent are missing", () => { + const result = resolveAgentType({ + description: "test", + prompt: "test", + agent_type: "Engineer", + }) + expect(result).toBe("Engineer") + }) + + test("prefers subagent_type over agent and agent_type", () => { + const result = resolveAgentType({ + description: "test", + prompt: "test", + subagent_type: "Architect", + agent: "Engineer", + agent_type: "Designer", + }) + expect(result).toBe("Architect") + }) + + test("prefers agent over agent_type when subagent_type is missing", () => { + const result = resolveAgentType({ + description: "test", + prompt: "test", + agent: "Engineer", + agent_type: "Designer", + }) + expect(result).toBe("Engineer") + }) + + test("throws when no agent parameter is provided", () => { + expect(() => + resolveAgentType({ + description: "test", + prompt: "test", + }), + ).toThrow("One of subagent_type, agent, or agent_type is required") + }) +})