diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 5e1ad9dc405..49e429245cd 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -30,6 +30,7 @@ export namespace Command { model: z.string().optional(), template: z.string(), subtask: z.boolean().optional(), + returnPrompt: z.string().optional(), }) .meta({ ref: "Command", @@ -49,6 +50,7 @@ export namespace Command { description: command.description, template: command.template, subtask: command.subtask, + returnPrompt: command.returnPrompt, } } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 779a4e8e2a3..7441f6656a9 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -346,6 +346,7 @@ export namespace Config { agent: z.string().optional(), model: z.string().optional(), subtask: z.boolean().optional(), + returnPrompt: z.string().optional(), }) export type Command = z.infer diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 20b612f5480..6dcf37e16fd 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -156,6 +156,8 @@ export namespace MessageV2 { prompt: z.string(), description: z.string(), agent: z.string(), + returnPrompt: z.string().optional(), + model: z.string().optional(), }) export type SubtaskPart = z.infer diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 741e3cc7e05..d1ac9f77cc0 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -291,6 +291,7 @@ export namespace SessionPrompt { // TODO: centralize "invoke tool" logic if (task?.type === "subtask") { const taskTool = await TaskTool.init() + const taskModel = task.model ? Provider.parseModel(task.model) : model const assistantMessage = (await Session.updateMessage({ id: Identifier.ascending("message"), role: "assistant", @@ -308,8 +309,8 @@ export namespace SessionPrompt { reasoning: 0, cache: { read: 0, write: 0 }, }, - modelID: model.modelID, - providerID: model.providerID, + modelID: taskModel.modelID, + providerID: taskModel.providerID, time: { created: Date.now(), }, @@ -327,6 +328,8 @@ export namespace SessionPrompt { prompt: task.prompt, description: task.description, subagent_type: task.agent, + returnPrompt: task.returnPrompt, + model: task.model, }, time: { start: Date.now(), @@ -339,6 +342,8 @@ export namespace SessionPrompt { prompt: task.prompt, description: task.description, subagent_type: task.agent, + returnPrompt: task.returnPrompt, + model: task.model, }, { agent: task.agent, @@ -1311,7 +1316,7 @@ export namespace SessionPrompt { export async function command(input: CommandInput) { log.info("command", input) const command = await Command.get(input.command) - const agentName = command.agent ?? input.agent ?? "build" + const agentName = input.agent ?? "build" const raw = input.arguments.match(argsRegex) ?? [] const args = raw.map((arg) => arg.replace(quoteTrimRegex, "")) @@ -1349,28 +1354,20 @@ export namespace SessionPrompt { } template = template.trim() - const model = await (async () => { - if (command.model) { - return Provider.parseModel(command.model) - } - if (command.agent) { - const cmdAgent = await Agent.get(command.agent) - if (cmdAgent.model) { - return cmdAgent.model - } - } - if (input.model) return Provider.parseModel(input.model) - return await lastModel(input.sessionID) - })() const agent = await Agent.get(agentName) + const model = input.model ? Provider.parseModel(input.model) : (agent.model ?? (await lastModel(input.sessionID))) + + const cmdAgent = command.agent ? await Agent.get(command.agent) : agent const parts = - (agent.mode === "subagent" && command.subtask !== false) || command.subtask === true + (cmdAgent.mode === "subagent" && command.subtask !== false) || command.subtask === true ? [ { type: "subtask" as const, - agent: agent.name, + agent: cmdAgent.name, description: command.description ?? "", + returnPrompt: command.returnPrompt, + model: command.model, // TODO: how can we make task tool accept a more complex input? prompt: await resolvePromptParts(template).then((x) => x.find((y) => y.type === "text")?.text ?? ""), }, diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 3bb7fb2bf39..485eff127d8 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -7,6 +7,7 @@ import { MessageV2 } from "../session/message-v2" import { Identifier } from "../id/id" import { Agent } from "../agent/agent" import { SessionPrompt } from "../session/prompt" +import { Provider } from "../provider/provider" import { iife } from "@/util/iife" import { defer } from "@/util/defer" @@ -25,6 +26,8 @@ export const TaskTool = Tool.define("task", async () => { 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"), session_id: z.string().describe("Existing Task session to continue").optional(), + returnPrompt: z.string().describe("Optional instructions for resuming in the parent session").optional(), + model: z.string().describe("Optional model override in provider/model form").optional(), }), async execute(params, ctx) { const agent = await Agent.get(params.subagent_type) @@ -66,10 +69,12 @@ export const TaskTool = Tool.define("task", async () => { }) }) - const model = agent.model ?? { - modelID: msg.info.modelID, - providerID: msg.info.providerID, - } + const model = params.model + ? Provider.parseModel(params.model) + : (agent.model ?? { + modelID: msg.info.modelID, + providerID: msg.info.providerID, + }) function cancel() { SessionPrompt.cancel(session.id) @@ -100,7 +105,9 @@ export const TaskTool = Tool.define("task", async () => { all = all.flatMap((msg) => msg.parts.filter((x: any) => x.type === "tool") as MessageV2.ToolPart[]) const text = result.parts.findLast((x) => x.type === "text")?.text ?? "" - const output = text + "\n\n" + ["", `session_id: ${session.id}`, ""].join("\n") + const exit = params.returnPrompt ? `\n\n${params.returnPrompt}` : "" + const output = + text + exit + "\n\n" + ["", `session_id: ${session.id}`, ""].join("\n") return { title: params.description, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index f026a7538c1..8b55b06899f 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index cf2861858e4..d65d93a4ee4 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -26,4 +26,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 3ba41cc2390..043f7645acc 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -383,6 +383,8 @@ export type Part = prompt: string description: string agent: string + returnPrompt?: string + model?: string } | ReasoningPart | FilePart @@ -1005,6 +1007,7 @@ export type Config = { agent?: string model?: string subtask?: boolean + returnPrompt?: string } } watcher?: { @@ -1294,6 +1297,8 @@ export type SubtaskPartInput = { prompt: string description: string agent: string + returnPrompt?: string + model?: string } export type Command = { @@ -1303,6 +1308,7 @@ export type Command = { model?: string template: string subtask?: boolean + returnPrompt?: string } export type Model = { diff --git a/packages/web/src/content/docs/commands.mdx b/packages/web/src/content/docs/commands.mdx index 463ad9e498f..bc581f3b29d 100644 --- a/packages/web/src/content/docs/commands.mdx +++ b/packages/web/src/content/docs/commands.mdx @@ -312,6 +312,39 @@ This is an **optional** config option. --- +### Return Prompt + +Use the `returnPrompt` option to provide instructions that are appended to the subagent's response when it returns to the parent session. This is useful for guiding what the parent agent should do with the subtask results and keeps the agent loop going. + +```json title="opencode.json" +{ + "command": { + "research": { + "agent": "general", + "subtask": true, + "returnPrompt": "Challenge, verify and validate the plan before implementing it." + } + } +} +``` + +Or in markdown format: + +```md title=".opencode/command/research.md" +--- +description: Research and implement +agent: general +subtask: true +returnPrompt: Challenge, verify and validate the plan before implementing it. +--- + +Research how to implement $ARGUMENTS and provide a detailed plan. +``` + +This is an **optional** config option. Only applies when the command runs as a subtask. + +--- + ## Built-in opencode includes several built-in commands like `/init`, `/undo`, `/redo`, `/share`, `/help`; [learn more](/docs/tui#commands).