diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index a9a29debb..565018f55 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -37,7 +37,7 @@ const BaseParameterFields = { subagent_type: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }), task_id: Schema.optional(Schema.String).annotate({ description: - "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)", + "Pass a prior task_id only to continue an idle subagent session with additional work. Do not use it to check progress on a running background task; while a task is running, the follow-up will not be sent and its result will be delivered automatically.", }), command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }), } @@ -67,6 +67,18 @@ function backgroundOutput(sessionID: SessionID) { ].join("\n") } +function backgroundStillRunningOutput(sessionID: SessionID) { + return [ + ``, + "Background task is still running", + "", + "This background task is still running. Your follow-up prompt was not sent.", + "Its result will be delivered automatically when it completes. Wait for that result before continuing this task.", + "", + "", + ].join("\n") +} + function backgroundMessage(input: { sessionID: SessionID description: string @@ -227,7 +239,11 @@ export const TaskTool = Tool.define( const existing = yield* background.get(nextSession.id) if (existing?.status === "running") { - return yield* Effect.fail(new Error(`Task ${nextSession.id} is already running.`)) + return { + title: params.description, + metadata, + output: backgroundStillRunningOutput(nextSession.id), + } } if (runInBackground) { diff --git a/packages/opencode/src/tool/task.txt b/packages/opencode/src/tool/task.txt index e2ac60500..badcfdf91 100644 --- a/packages/opencode/src/tool/task.txt +++ b/packages/opencode/src/tool/task.txt @@ -11,8 +11,8 @@ When NOT to use the Task tool: Usage notes: 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses -2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. The output includes a task_id you can reuse later to continue the same subagent session. -3. Each agent invocation starts with a fresh context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. +2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. The output includes a task_id you can reuse later to continue the same subagent session after it completes. +3. Each agent invocation starts with a fresh context unless you provide task_id to resume an idle subagent session (which continues with its previous messages and tool outputs). Do not use task_id to check progress on a running background task; results are delivered automatically, and a follow-up is not sent while the task is running. When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. 4. The agent's outputs should generally be trusted 5. 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). 6. 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. diff --git a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap index 1be32979d..ad27b42db 100644 --- a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap +++ b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap @@ -340,7 +340,7 @@ exports[`tool parameters JSON Schema (wire shape) task 1`] = ` "type": "string", }, "task_id": { - "description": "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)", + "description": "Pass a prior task_id only to continue an idle subagent session with additional work. Do not use it to check progress on a running background task; while a task is running, the follow-up will not be sent and its result will be delivered automatically.", "type": "string", }, }, diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 17e7fbea6..0a435e1e2 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -514,6 +514,71 @@ describe("tool.task", () => { }), ) + background.instance("resuming a running background task reports that the follow-up was not sent", () => + Effect.gen(function* () { + const jobs = yield* BackgroundJob.Service + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* tool.init() + const startedPrompt = defer() + let promptCount = 0 + const promptOps: TaskPromptOps = { + ...stubOps(), + prompt: (input) => + Effect.sync(() => { + promptCount++ + startedPrompt.resolve(input.sessionID) + }).pipe(Effect.andThen(Effect.never)), + } + + const started = yield* def.execute( + { + description: "review code quality", + prompt: "review the current diff", + subagent_type: "general", + background: true, + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + expect(yield* Effect.promise(() => startedPrompt.promise)).toBe(started.metadata.sessionId) + expect((yield* jobs.get(started.metadata.sessionId))?.status).toBe("running") + + const continued = yield* def.execute( + { + description: "review quality findings", + prompt: "return your findings now if available", + subagent_type: "general", + task_id: started.metadata.sessionId, + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + + expect(continued.output).toContain(``) + expect(continued.output).toContain("Your follow-up prompt was not sent.") + expect(continued.output).toContain("result will be delivered automatically") + expect(promptCount).toBe(1) + }), + ) + background.instance("background tasks complete through the background job service", () => Effect.gen(function* () { const jobs = yield* BackgroundJob.Service