Skip to content
Open
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
36 changes: 31 additions & 5 deletions packages/opencode/src/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<typeof parameters>): 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"))

Expand All @@ -45,23 +69,25 @@ export const TaskTool = Tool.define("task", async (ctx) => {
description,
parameters,
async execute(params: z.infer<typeof parameters>, 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")
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/test/session/prompt-effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}),
Expand Down
61 changes: 60 additions & 1 deletion packages/opencode/test/tool/task.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -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")
})
})
Loading