diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 1384e5d19725..b39ad735fa9f 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -49,6 +49,15 @@ const BaseParameterFields = { "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" }), + images: Schema.optional( + Schema.Array( + Schema.Struct({ + mime: Schema.String.annotate({ description: "The MIME type of the image (e.g., image/png, image/jpeg)" }), + url: Schema.String.annotate({ description: "The URL of the image file" }), + filename: Schema.optional(Schema.String).annotate({ description: "Optional filename for the image" }), + }), + ), + ).annotate({ description: "Optional array of images to pass to the subagent for analysis" }), } const BaseParameters = Schema.Struct(BaseParameterFields) @@ -199,6 +208,18 @@ export const TaskTool = Tool.define( const runTask = Effect.fn("TaskTool.runTask")(function* () { const parts = yield* ops.resolvePromptParts(params.prompt) + + // Add images to parts if provided + if (params.images && params.images.length > 0) { + const imageParts: SessionV1.FilePartInput[] = params.images.map((image) => ({ + type: "file", + mime: image.mime, + url: image.url, + filename: image.filename, + })) + parts.push(...imageParts) + } + const result = yield* ops.prompt({ messageID: MessageID.ascending(), sessionID: nextSession.id, diff --git a/packages/opencode/src/tool/task.txt b/packages/opencode/src/tool/task.txt index c5e412f409d9..6cfb249fe537 100644 --- a/packages/opencode/src/tool/task.txt +++ b/packages/opencode/src/tool/task.txt @@ -17,3 +17,4 @@ Usage notes: 5. The agent's outputs should generally be trusted 6. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent. Tell it how to verify its work if possible (e.g., relevant test commands). 7. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement. +8. You can pass images to the subagent using the images parameter for visual analysis tasks. The images parameter accepts an array of image objects with mime type, url, and optional filename. diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 2bcf05a2a49b..14a1afd95687 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -982,4 +982,58 @@ describe("tool.task", () => { expect((yield* jobs.get(grandchild.id))?.status).toBe("cancelled") }), ) + + it.instance("execute passes images to subagent 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({ text: "analyzed", onPrompt: (input) => (seen = input) }) + + const images = [ + { mime: "image/png", url: "https://example.com/image1.png", filename: "test1.png" }, + { mime: "image/jpeg", url: "https://example.com/image2.jpg" }, + ] + + const result = yield* def.execute( + { + description: "analyze images", + prompt: "analyze the provided images", + subagent_type: "general", + images, + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + expect(result.output).toContain(``) + expect(seen?.parts).toBeDefined() + expect(seen?.parts).toHaveLength(3) // 1 text part + 2 image parts + + // Check image parts + const imageParts = seen?.parts.filter((p) => p.type === "file") + expect(imageParts).toHaveLength(2) + expect(imageParts?.[0]).toEqual({ + type: "file", + mime: "image/png", + url: "https://example.com/image1.png", + filename: "test1.png", + }) + expect(imageParts?.[1]).toEqual({ + type: "file", + mime: "image/jpeg", + url: "https://example.com/image2.jpg", + filename: undefined, + }) + }), + ) })