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
20 changes: 16 additions & 4 deletions packages/opencode/src/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export const Parameters = Schema.Struct({
"This should only be set if you mean to resume a previous task (you can pass a prior task_id and the task will continue the same subagent session as before instead of creating a fresh one)",
}),
command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }),
model: Schema.optional(Schema.String).annotate({
description:
"Optional model to use for this subagent (e.g., 'gpt-4-vision-preview', 'claude-3-opus', 'llama-3.1-8b'). Overrides the agent's default model. Format: 'modelID' or 'providerID/modelID'.",
}),
})

export const TaskTool = Tool.define(
Expand Down Expand Up @@ -104,10 +108,18 @@ export const TaskTool = Tool.define(
const msg = yield* Effect.sync(() => MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }))
if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message"))

const model = next.model ?? {
modelID: msg.info.modelID,
providerID: msg.info.providerID,
}
// Determine model: params.model > agent default > parent message model
const model = params.model
? (params.model.includes("/")
? (() => {
const [providerID, modelID] = params.model.split("/")
return { modelID, providerID }
})()
: { modelID: params.model, providerID: msg.info.providerID })
: next.model ?? {
modelID: msg.info.modelID,
providerID: msg.info.providerID,
}

yield* ctx.metadata({
title: params.description,
Expand Down
8 changes: 8 additions & 0 deletions packages/opencode/src/tool/task.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ Launch a new agent to handle complex, multistep tasks autonomously.

When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.

Available parameters:
- description: A short (3-5 words) description of the task
- prompt: The task for the agent to perform
- subagent_type: The type of specialized agent to use (general, explore)
- task_id: (Optional) Resume a previous task session
- command: (Optional) The command that triggered this task
- model: (Optional) Model to use for this subagent. Format: 'modelID' (e.g., 'gpt-4-vision-preview') or 'providerID/modelID' (e.g., 'openai/gpt-4-vision-preview'). Overrides the agent's default model.

When to use the Task tool:
- When you are instructed to execute custom slash commands. Use the Task tool with the slash command invocation as the entire prompt. The slash command can take arguments. For example: Task(description="Check the file", prompt="/check-file path/to/file.py")

Expand Down
111 changes: 109 additions & 2 deletions packages/opencode/test/tool/task.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,115 @@ describe("tool.task", () => {
}),
)

it.instance(
"execute shapes child permissions for task, todowrite, and primary tools",
it.instance("execute uses model parameter when provided", () =>
Effect.gen(function* () {
const { chat, assistant } = yield* seed()
const tool = yield* TaskTool
const def = yield* tool.init()
let seen: SessionPrompt.PromptInput | undefined
const promptOps = stubOps({ onPrompt: (input) => (seen = input) })

const result = yield* def.execute(
{
description: "analyze image",
prompt: "Analyze this screenshot",
subagent_type: "general",
model: "gpt-4-vision-preview",
},
{
sessionID: chat.id,
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
extra: { promptOps },
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
},
)

expect(seen?.model).toEqual({
modelID: "gpt-4-vision-preview",
providerID: ref.providerID,
})
expect(result.metadata.model).toEqual({
modelID: "gpt-4-vision-preview",
providerID: ref.providerID,
})
}),
)

it.instance("execute uses providerID/modelID format when model contains slash", () =>
Effect.gen(function* () {
const { chat, assistant } = yield* seed()
const tool = yield* TaskTool
const def = yield* tool.init()
let seen: SessionPrompt.PromptInput | undefined
const promptOps = stubOps({ onPrompt: (input) => (seen = input) })

const result = yield* def.execute(
{
description: "research task",
prompt: "Research this topic",
subagent_type: "general",
model: "openai/gpt-4-turbo",
},
{
sessionID: chat.id,
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
extra: { promptOps },
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
},
)

expect(seen?.model).toEqual({
modelID: "gpt-4-turbo",
providerID: "openai",
})
expect(result.metadata.model).toEqual({
modelID: "gpt-4-turbo",
providerID: "openai",
})
}),
)

it.instance("execute falls back to agent model when model param not provided", () =>
Effect.gen(function* () {
const { chat, assistant } = yield* seed()
const tool = yield* TaskTool
const def = yield* tool.init()
let seen: SessionPrompt.PromptInput | undefined
const promptOps = stubOps({ onPrompt: (input) => (seen = input) })

const result = yield* def.execute(
{
description: "regular task",
prompt: "Do the thing",
subagent_type: "general",
},
{
sessionID: chat.id,
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
extra: { promptOps },
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
},
)

// Should use parent message model when no agent default and no param
expect(seen?.model).toEqual({
modelID: ref.modelID,
providerID: ref.providerID,
})
}),
)
() =>
Effect.gen(function* () {
const sessions = yield* Session.Service
Expand Down
Loading