From a4df9877d36ba1106f7a4c71f1b5699cb11259ef Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 25 May 2026 00:29:20 -0500 Subject: [PATCH 1/8] fix(opencode): remove task status tool --- packages/opencode/src/tool/registry.ts | 4 - packages/opencode/src/tool/task.ts | 10 +- packages/opencode/src/tool/task_status.ts | 179 ------------------ packages/opencode/src/tool/task_status.txt | 13 -- packages/opencode/test/tool/registry.test.ts | 14 +- .../opencode/test/tool/task_status.test.ts | 92 --------- 6 files changed, 5 insertions(+), 307 deletions(-) delete mode 100644 packages/opencode/src/tool/task_status.ts delete mode 100644 packages/opencode/src/tool/task_status.txt delete mode 100644 packages/opencode/test/tool/task_status.test.ts diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 6ef6d39a65a5..cfb4bf00d911 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -7,7 +7,6 @@ import { GlobTool } from "./glob" import { GrepTool } from "./grep" import { ReadTool } from "./read" import { TaskTool } from "./task" -import { TaskStatusTool } from "./task_status" import { TodoWriteTool } from "./todo" import { WebFetchTool } from "./webfetch" import { WriteTool } from "./write" @@ -119,7 +118,6 @@ export const layer: Layer.Layer< const invalid = yield* InvalidTool const task = yield* TaskTool - const taskStatus = yield* TaskStatusTool const read = yield* ReadTool const question = yield* QuestionTool const todo = yield* TodoWriteTool @@ -235,7 +233,6 @@ export const layer: Layer.Layer< edit: Tool.init(edit), write: Tool.init(writetool), task: Tool.init(task), - task_status: Tool.init(taskStatus), fetch: Tool.init(webfetch), todo: Tool.init(todo), search: Tool.init(websearch), @@ -260,7 +257,6 @@ export const layer: Layer.Layer< tool.edit, tool.write, tool.task, - ...(flags.experimentalBackgroundSubagents ? [tool.task_status] : []), tool.fetch, tool.todo, tool.search, diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index fece68800b06..d3d97333d871 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -29,7 +29,7 @@ const BACKGROUND_DESCRIPTION = [ "", [ "Background mode: background=true launches the subagent asynchronously.", - "Use task_status(task_id=..., wait=false) to poll, or wait=true to block until done.", + "The parent agent is notified automatically when the background task finishes.", ].join(" "), ].join("\n") @@ -70,11 +70,11 @@ function output(sessionID: SessionID, text: string) { function backgroundOutput(sessionID: SessionID) { return [ - `task_id: ${sessionID} (for polling this task with task_status)`, + `task_id: ${sessionID}`, "state: running", "", "", - "Background task started. Continue your current work and call task_status when you need the result.", + "Background task started. The parent agent will be notified automatically when it finishes.", "", ].join("\n") } @@ -270,9 +270,7 @@ 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. Use task_status to check progress.`), - ) + return yield* Effect.fail(new Error(`Task ${nextSession.id} is already running.`)) } if (runInBackground) { diff --git a/packages/opencode/src/tool/task_status.ts b/packages/opencode/src/tool/task_status.ts deleted file mode 100644 index b458b4fc45fa..000000000000 --- a/packages/opencode/src/tool/task_status.ts +++ /dev/null @@ -1,179 +0,0 @@ -import * as Tool from "./tool" -import DESCRIPTION from "./task_status.txt" -import { BackgroundJob } from "@/background/job" -import { Session } from "@/session/session" -import { MessageV2 } from "@/session/message-v2" -import { SessionID } from "@/session/schema" -import { SessionStatus } from "@/session/status" -import { PositiveInt } from "@opencode-ai/core/schema" -import { RuntimeFlags } from "@/effect/runtime-flags" -import { Effect, Option, Schema } from "effect" - -const DEFAULT_TIMEOUT = 60_000 -const POLL_MS = 300 - -const Parameters = Schema.Struct({ - task_id: SessionID.annotate({ description: "The task_id returned by the task tool" }), - wait: Schema.optional(Schema.Boolean).annotate({ - description: "When true, wait until the task reaches a terminal state or timeout", - }), - timeout_ms: Schema.optional(PositiveInt).annotate({ - description: "Maximum milliseconds to wait when wait=true (default: 60000)", - }), -}) - -type State = BackgroundJob.Status -type InspectResult = { state: State; text: string } - -function format(input: { taskID: SessionID; state: State; text: string }) { - const tag = input.state === "completed" || input.state === "running" ? "task_result" : "task_error" - return [`task_id: ${input.taskID}`, `state: ${input.state}`, "", `<${tag}>`, input.text, ``].join("\n") -} - -function errorText(error: NonNullable) { - const data = Reflect.get(error, "data") - const message = data && typeof data === "object" ? Reflect.get(data, "message") : undefined - if (typeof message === "string" && message) return message - return error.name -} - -function inspectMessage(message: MessageV2.WithParts): InspectResult | undefined { - if (message.info.role !== "assistant") return - const text = message.parts.findLast((part) => part.type === "text")?.text ?? "" - if (message.info.error) return { state: "error", text: text || errorText(message.info.error) } - if (message.info.finish && !["tool-calls", "unknown"].includes(message.info.finish)) - return { state: "completed", text } - return { state: "running", text: text || "Task is still running." } -} - -export const TaskStatusTool = Tool.define( - "task_status", - Effect.gen(function* () { - const jobs = yield* BackgroundJob.Service - const sessions = yield* Session.Service - const status = yield* SessionStatus.Service - const flags = yield* RuntimeFlags.Service - - const inspect: (taskID: SessionID) => Effect.Effect = Effect.fn("TaskStatusTool.inspect")(function* ( - taskID: SessionID, - ) { - const job = yield* jobs.get(taskID) - if (job) { - return { - state: job.status, - text: - job.output ?? - job.error ?? - (job.status === "running" - ? "Task is still running." - : job.status === "cancelled" - ? "Task was cancelled." - : ""), - } - } - - const current = yield* status.get(taskID) - if (current.type === "busy" || current.type === "retry") { - return { - state: "running", - text: current.type === "retry" ? `Task is retrying: ${current.message}` : "Task is still running.", - } - } - - const latestAssistant = yield* sessions - .findMessage(taskID, (item) => item.info.role === "assistant") - .pipe(Effect.orDie) - if (Option.isSome(latestAssistant)) { - const latest = inspectMessage(latestAssistant.value) - if (!latest) return { state: "error", text: "Task is not running in this process." } - if (latest.state === "running") - return { state: "error", text: "Task is not running in this process and has no final output." } - return latest - } - return { state: "error", text: "Task is not running in this process and has not produced output." } - }) - - const waitForTerminal: ( - taskID: SessionID, - timeout: number, - ) => Effect.Effect<{ result: InspectResult; timedOut: boolean }> = Effect.fn("TaskStatusTool.waitForTerminal")( - function* (taskID: SessionID, timeout: number) { - const result = yield* inspect(taskID) - if (result.state !== "running") return { result, timedOut: false } - if (timeout <= 0) return { result, timedOut: true } - const sleep = Math.min(POLL_MS, timeout) - yield* Effect.sleep(`${sleep} millis`) - return yield* waitForTerminal(taskID, timeout - sleep) - }, - ) - - const run = Effect.fn("TaskStatusTool.execute")(function* ( - params: Schema.Schema.Type, - _ctx: Tool.Context, - ) { - if (!flags.experimentalBackgroundSubagents) { - return yield* Effect.fail(new Error("task_status requires OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true")) - } - - const session = yield* sessions.get(params.task_id).pipe(Effect.catchCause(() => Effect.succeed(undefined))) - if (!session) { - return { - title: "Task status", - metadata: { - task_id: params.task_id, - state: "error" as const, - timed_out: false, - }, - output: format({ - taskID: params.task_id, - state: "error", - text: `Task not found: ${params.task_id}`, - }), - } - } - - const waited = - params.wait === true - ? yield* jobs.wait({ id: params.task_id, timeout: params.timeout_ms ?? DEFAULT_TIMEOUT }) - : { info: yield* jobs.get(params.task_id), timedOut: false } - const inspected = waited.info - ? { - result: { - state: waited.info.status, - text: - waited.info.output ?? - waited.info.error ?? - (waited.info.status === "running" ? "Task is still running." : ""), - }, - timedOut: waited.timedOut, - } - : params.wait === true - ? yield* waitForTerminal(params.task_id, params.timeout_ms ?? DEFAULT_TIMEOUT) - : { result: yield* inspect(params.task_id), timedOut: false } - const text = inspected.timedOut - ? `Timed out after ${params.timeout_ms ?? DEFAULT_TIMEOUT}ms while waiting for task completion.` - : inspected.result.text - - return { - title: "Task status", - metadata: { - task_id: params.task_id, - state: inspected.result.state, - timed_out: inspected.timedOut, - }, - output: format({ - taskID: params.task_id, - state: inspected.result.state, - text, - }), - } - }) - - return { - description: DESCRIPTION, - parameters: Parameters, - execute: (params: Schema.Schema.Type, ctx: Tool.Context) => - run(params, ctx).pipe(Effect.orDie), - } - }), -) diff --git a/packages/opencode/src/tool/task_status.txt b/packages/opencode/src/tool/task_status.txt deleted file mode 100644 index ed6fa727b2a9..000000000000 --- a/packages/opencode/src/tool/task_status.txt +++ /dev/null @@ -1,13 +0,0 @@ -Poll the status of a background subagent task launched with the task tool. - -Use this for tasks started with `task(background=true)`. - -Parameters: -- `task_id` (required): the task session id returned by the task tool -- `wait` (optional): when true, wait for completion -- `timeout_ms` (optional): max wait duration in milliseconds when `wait=true` - -Returns compact, parseable output: -- `task_id` -- `state` (`running`, `completed`, `error`, or `cancelled`) -- `...` or `...` containing final output, error summary, or current progress text diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index d3549e66f340..25c50678adc3 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -99,9 +99,6 @@ const it = testEffect(Layer.mergeAll(registryLayer(), node, Agent.defaultLayer)) const scout = testEffect( Layer.mergeAll(registryLayer({ flags: { experimentalScout: true } }), node, Agent.defaultLayer), ) -const background = testEffect( - Layer.mergeAll(registryLayer({ flags: { experimentalBackgroundSubagents: true } }), node, Agent.defaultLayer), -) const withBrokenPlugin = testEffect( Layer.mergeAll(registryLayer({ plugin: brokenPluginLayer }), node, Agent.defaultLayer), ) @@ -131,7 +128,7 @@ describe("tool.registry", () => { }), ) - it.instance("hides task_status unless experimental background subagents are enabled", () => + it.instance("does not expose task_status", () => Effect.gen(function* () { const registry = yield* ToolRegistry.Service const ids = yield* registry.ids() @@ -157,15 +154,6 @@ describe("tool.registry", () => { }), ) - background.instance("shows task_status when experimental background subagents are enabled", () => - Effect.gen(function* () { - const registry = yield* ToolRegistry.Service - const ids = yield* registry.ids() - - expect(ids).toContain("task_status") - }), - ) - it.instance("loads tools from .opencode/tool (singular)", () => Effect.gen(function* () { const test = yield* TestInstance diff --git a/packages/opencode/test/tool/task_status.test.ts b/packages/opencode/test/tool/task_status.test.ts deleted file mode 100644 index 23bd49c616c7..000000000000 --- a/packages/opencode/test/tool/task_status.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { afterEach, describe, expect } from "bun:test" -import { Effect, Layer } from "effect" -import { Agent } from "@/agent/agent" -import { BackgroundJob } from "@/background/job" -import { Bus } from "@/bus" -import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" -import { Session } from "@/session/session" -import { MessageID } from "@/session/schema" -import { SessionStatus } from "@/session/status" -import { TaskStatusTool } from "@/tool/task_status" -import { Truncate } from "@/tool/truncate" -import { RuntimeFlags } from "@/effect/runtime-flags" -import { disposeAllInstances } from "../fixture/fixture" -import { testEffect } from "../lib/effect" - -afterEach(async () => { - await disposeAllInstances() -}) - -const layer = (flags: Partial = {}) => - Layer.mergeAll( - Agent.defaultLayer, - BackgroundJob.defaultLayer, - Bus.defaultLayer, - CrossSpawnSpawner.defaultLayer, - Session.defaultLayer, - SessionStatus.defaultLayer, - Truncate.defaultLayer, - RuntimeFlags.layer(flags), - ) - -const it = testEffect(layer({ experimentalBackgroundSubagents: true })) - -describe("tool.task_status", () => { - it.instance("returns completed background job output", () => - Effect.gen(function* () { - const jobs = yield* BackgroundJob.Service - const sessions = yield* Session.Service - const tool = yield* TaskStatusTool - const def = yield* tool.init() - const chat = yield* sessions.create({}) - - yield* jobs.start({ id: chat.id, type: "task", run: Effect.succeed("all done") }) - - const result = yield* def.execute( - { task_id: chat.id, wait: true, timeout_ms: 1_000 }, - { - sessionID: chat.id, - messageID: MessageID.ascending(), - agent: "build", - abort: new AbortController().signal, - messages: [], - metadata: () => Effect.void, - ask: () => Effect.void, - }, - ) - - expect(result.output).toContain("state: completed") - expect(result.output).toContain("all done") - expect(result.metadata.timed_out).toBe(false) - }), - ) - - it.instance("wait=true times out while the background job is running", () => - Effect.gen(function* () { - const jobs = yield* BackgroundJob.Service - const sessions = yield* Session.Service - const tool = yield* TaskStatusTool - const def = yield* tool.init() - const chat = yield* sessions.create({}) - - yield* jobs.start({ id: chat.id, type: "task", run: Effect.never }) - - const result = yield* def.execute( - { task_id: chat.id, wait: true, timeout_ms: 50 }, - { - sessionID: chat.id, - messageID: MessageID.ascending(), - agent: "build", - abort: new AbortController().signal, - messages: [], - metadata: () => Effect.void, - ask: () => Effect.void, - }, - ) - - expect(result.output).toContain("state: running") - expect(result.output).toContain("Timed out after 50ms") - expect(result.metadata.timed_out).toBe(true) - }), - ) -}) From 7f20e5fa97d10d933ad84a602d2378af3ec2753a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 25 May 2026 00:47:25 -0500 Subject: [PATCH 2/8] fix(opencode): clarify background task guidance --- packages/opencode/src/tool/task.ts | 11 +++++++---- packages/opencode/src/tool/task.txt | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index d3d97333d871..a09cbcbeb249 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -28,8 +28,10 @@ const BACKGROUND_DESCRIPTION = [ "", "", [ - "Background mode: background=true launches the subagent asynchronously.", - "The parent agent is notified automatically when the background task finishes.", + "Background mode: background=true launches the subagent asynchronously and returns immediately.", + "Foreground is the default; use it when you need the result before continuing.", + "Use background only for independent work that can run while you continue elsewhere.", + "You will be notified automatically when it finishes.", ].join(" "), ].join("\n") @@ -54,7 +56,7 @@ export const Parameters = Schema.Struct({ }), command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }), background: Schema.optional(Schema.Boolean).annotate({ - description: "When true, launch the subagent in the background and return immediately", + description: "Run the agent in the background and deliver a notification when it completes", }), }) @@ -74,7 +76,8 @@ function backgroundOutput(sessionID: SessionID) { "state: running", "", "", - "Background task started. The parent agent will be notified automatically when it finishes.", + "Background task started. You will be notified automatically when it finishes; do not poll for progress.", + "Do not duplicate its work. Continue only with non-overlapping work, or stop if there is nothing else useful to do.", "", ].join("\n") } diff --git a/packages/opencode/src/tool/task.txt b/packages/opencode/src/tool/task.txt index e2ac605005b4..3b8bf78191cb 100644 --- a/packages/opencode/src/tool/task.txt +++ b/packages/opencode/src/tool/task.txt @@ -13,6 +13,6 @@ 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. -4. The agent's outputs should generally be trusted +4. Treat the agent's response as a report. If it wrote or edited code, inspect the actual changes before reporting that work as done. 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. From b9d3eba202a92ff9c768fc7743af6eb0be7d1dad Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 25 May 2026 01:27:46 -0500 Subject: [PATCH 3/8] fix(opencode): use async prompt for background completions --- packages/opencode/src/session/prompt.ts | 1 - packages/opencode/src/tool/task.ts | 76 ++++++------------------ packages/opencode/test/tool/task.test.ts | 27 +-------- 3 files changed, 21 insertions(+), 83 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 2fc93c482521..d860c5048433 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -128,7 +128,6 @@ export const layer = Layer.effect( cancel: (sessionID: SessionID) => cancel(sessionID), resolvePromptParts: (template: string) => resolvePromptParts(template), prompt: (input: PromptInput) => prompt(input).pipe(Effect.catch(Effect.die)), - loop: (input: LoopInput) => loop(input), } satisfies TaskPromptOps }) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index a09cbcbeb249..4f5d156d5c9d 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -2,17 +2,14 @@ import * as Tool from "./tool" import DESCRIPTION from "./task.txt" import { ToolJsonSchema } from "./json-schema" import { BackgroundJob } from "@/background/job" -import { Bus } from "@/bus" import { Session } from "@/session/session" import { SessionID, MessageID } from "../session/schema" import { MessageV2 } from "../session/message-v2" import { Agent } from "../agent/agent" import { deriveSubagentSessionPermission } from "../agent/subagent-permissions" import type { SessionPrompt } from "../session/prompt" -import { SessionStatus } from "@/session/status" import { Config } from "@/config/config" -import { TuiEvent } from "@/cli/cmd/tui/event" -import { Cause, Effect, Exit, Option, Schema, Scope } from "effect" +import { Cause, Effect, Exit, Schema, Scope } from "effect" import { EffectBridge } from "@/effect/bridge" import { RuntimeFlags } from "@/effect/runtime-flags" @@ -20,7 +17,6 @@ export interface TaskPromptOps { cancel(sessionID: SessionID): Effect.Effect resolvePromptParts(template: string): Effect.Effect prompt(input: SessionPrompt.PromptInput): Effect.Effect - loop(input: SessionPrompt.LoopInput): Effect.Effect } const id = "task" @@ -108,11 +104,9 @@ export const TaskTool = Tool.define( Effect.gen(function* () { const agent = yield* Agent.Service const background = yield* BackgroundJob.Service - const bus = yield* Bus.Service const config = yield* Config.Service const sessions = yield* Session.Service const scope = yield* Scope.Scope - const status = yield* SessionStatus.Service const flags = yield* RuntimeFlags.Service const run = Effect.fn("TaskTool.execute")(function* ( @@ -214,61 +208,29 @@ export const TaskTool = Tool.define( return result.parts.findLast((item) => item.type === "text")?.text ?? "" }) - const resumeWhenIdle: (input: { userID: MessageID; state: "completed" | "error" }) => Effect.Effect = - Effect.fn("TaskTool.resumeWhenIdle")(function* (input: { userID: MessageID; state: "completed" | "error" }) { - const latest = yield* sessions - .findMessage(ctx.sessionID, (item) => item.info.role === "user") - .pipe(Effect.orDie) - if (Option.isNone(latest)) return - if (latest.value.info.id !== input.userID) return - if ((yield* status.get(ctx.sessionID)).type !== "idle") { - yield* Effect.sleep("300 millis") - return yield* resumeWhenIdle(input) - } - yield* bus.publish(TuiEvent.ToastShow, { - title: input.state === "completed" ? "Background task complete" : "Background task failed", - message: - input.state === "completed" - ? `Background task "${params.description}" finished. Resuming the main thread.` - : `Background task "${params.description}" failed. Resuming the main thread.`, - variant: input.state === "completed" ? "success" : "error", - duration: 5000, - }) - yield* ops - .loop({ sessionID: ctx.sessionID }) - .pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true })) - }) - - const continueIfIdle = Effect.fn("TaskTool.continueIfIdle")(function* (input: { - userID: MessageID - state: "completed" | "error" - }) { - yield* resumeWhenIdle(input).pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true })) - }) - const inject = Effect.fn("TaskTool.injectBackgroundResult")(function* ( state: "completed" | "error", text: string, ) { const currentParent = yield* sessions.get(ctx.sessionID) - const message = yield* ops.prompt({ - sessionID: ctx.sessionID, - noReply: true, - agent: currentParent.agent ?? ctx.agent, - parts: [ - { - type: "text", - synthetic: true, - text: backgroundMessage({ - sessionID: nextSession.id, - description: params.description, - state, - text, - }), - }, - ], - }) - yield* continueIfIdle({ userID: message.info.id, state }) + yield* ops + .prompt({ + sessionID: ctx.sessionID, + agent: currentParent.agent ?? ctx.agent, + parts: [ + { + type: "text", + synthetic: true, + text: backgroundMessage({ + sessionID: nextSession.id, + description: params.description, + state, + text, + }), + }, + ], + }) + .pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true })) }) const existing = yield* background.get(nextSession.id) diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 2b7d001572a0..08787e08aa1c 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -92,7 +92,6 @@ function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void; opts?.onPrompt?.(input) return reply(input, opts?.text ?? "done") }), - loop: (input) => Effect.succeed(reply({ sessionID: input.sessionID, parts: [] }, opts?.text ?? "done")), } } @@ -307,7 +306,6 @@ describe("tool.task", () => { ready.resolve(input) return cancelled.promise }).pipe(Effect.as(reply(input, "cancelled"))), - loop: (input) => Effect.succeed(reply({ sessionID: input.sessionID, parts: [] }, "done")), } const fiber = yield* def @@ -549,10 +547,9 @@ describe("tool.task", () => { }), ) - background.instance("background task completion does not wait for the parent resume loop", () => + background.instance("background task completion does not wait for the parent async prompt", () => Effect.gen(function* () { const jobs = yield* BackgroundJob.Service - const sessions = yield* Session.Service const { chat, assistant } = yield* seed() const tool = yield* TaskTool const def = yield* tool.init() @@ -573,27 +570,7 @@ describe("tool.task", () => { promptOps: { ...stubOps({ text: "background done" }), prompt: (input) => - input.noReply - ? Effect.gen(function* () { - const user = yield* sessions.updateMessage({ - id: input.messageID ?? MessageID.ascending(), - role: "user", - sessionID: input.sessionID, - agent: input.agent ?? "build", - model: input.model ?? ref, - time: { created: Date.now() }, - }) - const parts = input.parts.map((part) => ({ - ...part, - id: part.id ?? PartID.ascending(), - messageID: user.id, - sessionID: input.sessionID, - })) - yield* Effect.forEach(parts, (part) => sessions.updatePart(part), { discard: true }) - return { info: user, parts } - }) - : Effect.succeed(reply(input, "background done")), - loop: () => Effect.never, + input.sessionID === chat.id ? Effect.never : Effect.succeed(reply(input, "background done")), } satisfies TaskPromptOps, }, messages: [], From ba05513db87d847112c8daf5bbde01b594fc0987 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 25 May 2026 08:54:44 -0500 Subject: [PATCH 4/8] test(opencode): update task parameter snapshot --- .../opencode/test/tool/__snapshots__/parameters.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap index 9fede8175929..d33a2aa15426 100644 --- a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap +++ b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap @@ -320,7 +320,7 @@ exports[`tool parameters JSON Schema (wire shape) task 1`] = ` "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "background": { - "description": "When true, launch the subagent in the background and return immediately", + "description": "Run the agent in the background and deliver a notification when it completes", "type": "boolean", }, "command": { From 94c1414aa43a44c5257bf69398c79060d0480b94 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 25 May 2026 11:27:09 -0500 Subject: [PATCH 5/8] fix(opencode): structure task output messages --- packages/opencode/src/tool/task.ts | 27 ++++++++++--------- .../opencode/test/cli/run/entry.body.test.ts | 8 +++--- .../test/cli/run/scrollback.surface.test.ts | 4 +-- packages/opencode/test/tool/task.test.ts | 6 ++--- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 4f5d156d5c9d..a645edc51c4b 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -57,24 +57,20 @@ export const Parameters = Schema.Struct({ }) function output(sessionID: SessionID, text: string) { - return [ - `task_id: ${sessionID} (for resuming to continue this task if needed)`, - "", - "", - text, - "", - ].join("\n") + return [``, "", text, "", ""].join( + "\n", + ) } function backgroundOutput(sessionID: SessionID) { return [ - `task_id: ${sessionID}`, - "state: running", - "", + ``, + "Background task started", "", "Background task started. You will be notified automatically when it finishes; do not poll for progress.", "Do not duplicate its work. Continue only with non-overlapping work, or stop if there is nothing else useful to do.", "", + "", ].join("\n") } @@ -89,9 +85,14 @@ function backgroundMessage(input: { input.state === "completed" ? `Background task completed: ${input.description}` : `Background task failed: ${input.description}` - return [title, `task_id: ${input.sessionID}`, `state: ${input.state}`, "", `<${tag}>`, input.text, ``].join( - "\n", - ) + return [ + ``, + `${title}`, + `<${tag}>`, + input.text, + ``, + "", + ].join("\n") } function errorText(error: unknown) { diff --git a/packages/opencode/test/cli/run/entry.body.test.ts b/packages/opencode/test/cli/run/entry.body.test.ts index e65fd016590c..a33bb0e0dbd2 100644 --- a/packages/opencode/test/cli/run/entry.body.test.ts +++ b/packages/opencode/test/cli/run/entry.body.test.ts @@ -235,11 +235,11 @@ describe("run entry body", () => { }, title: "", output: [ - "task_id: child-1 (for resuming to continue this task if needed)", - "", + '', "", "# Findings\n\n- Footer stays live", "", + "", ].join("\n"), metadata: { sessionId: "child-1", @@ -265,11 +265,11 @@ describe("run entry body", () => { }, title: "", output: [ - "task_id: child-1 (for resuming to continue this task if needed)", - "", + '', "", "", "", + "", ].join("\n"), metadata: { sessionId: "child-1", diff --git a/packages/opencode/test/cli/run/scrollback.surface.test.ts b/packages/opencode/test/cli/run/scrollback.surface.test.ts index da196b7e1020..0d8b297a3524 100644 --- a/packages/opencode/test/cli/run/scrollback.surface.test.ts +++ b/packages/opencode/test/cli/run/scrollback.surface.test.ts @@ -938,8 +938,7 @@ test("renders promoted task markdown without a leading blank row", async () => { subagent_type: "explore", }, output: [ - "task_id: child-1 (for resuming to continue this task if needed)", - "", + '', "", "Location: `/tmp/run.ts`", "", @@ -947,6 +946,7 @@ test("renders promoted task markdown without a leading blank row", async () => { "- Local interactive mode", "- Attach mode", "", + "", ].join("\n"), metadata: { sessionId: "child-1", diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 08787e08aa1c..17e7fbea614f 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -236,7 +236,7 @@ describe("tool.task", () => { expect(kids).toHaveLength(1) expect(kids[0]?.id).toBe(child.id) expect(result.metadata.sessionId).toBe(child.id) - expect(result.output).toContain(`task_id: ${child.id}`) + expect(result.output).toContain(``) expect(seen?.sessionID).toBe(child.id) }), ) @@ -369,7 +369,7 @@ describe("tool.task", () => { expect(kids).toHaveLength(1) expect(kids[0]?.id).toBe(result.metadata.sessionId) expect(result.metadata.sessionId).not.toBe("ses_missing") - expect(result.output).toContain(`task_id: ${result.metadata.sessionId}`) + expect(result.output).toContain(``) expect(seen?.sessionID).toBe(result.metadata.sessionId) }), ) @@ -509,7 +509,7 @@ describe("tool.task", () => { const job = yield* jobs.get(result.metadata.sessionId) expect(result.metadata.background).toBe(true) - expect(result.output).toContain("state: running") + expect(result.output).toContain(`state="running"`) expect(job?.status).toBe("running") }), ) From 2249124af612bad095f56781be6a10636f285711 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 25 May 2026 12:17:30 -0500 Subject: [PATCH 6/8] refactor(opencode): simplify background task wiring --- packages/opencode/src/tool/registry.ts | 4 +--- packages/opencode/src/tool/task.ts | 22 ++++++++-------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index cfb4bf00d911..d7f7de778e00 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -52,7 +52,6 @@ import { Skill } from "../skill" import { Permission } from "@/permission" import { Reference } from "@/reference/reference" import { BackgroundJob } from "@/background/job" -import { SessionStatus } from "@/session/status" import { RuntimeFlags } from "@/effect/runtime-flags" const log = Log.create({ service: "tool.registry" }) @@ -90,7 +89,6 @@ export const layer: Layer.Layer< | Agent.Service | Skill.Service | Session.Service - | SessionStatus.Service | BackgroundJob.Service | Provider.Service | Git.Service @@ -381,7 +379,7 @@ export const defaultLayer = Layer.suspend(() => Layer.provide(Skill.defaultLayer), Layer.provide(Agent.defaultLayer), Layer.provide(Session.defaultLayer), - Layer.provide(Layer.mergeAll(SessionStatus.defaultLayer, BackgroundJob.defaultLayer)), + Layer.provide(BackgroundJob.defaultLayer), Layer.provide(Provider.defaultLayer), Layer.provide(Layer.mergeAll(Git.defaultLayer, RepositoryCache.defaultLayer)), Layer.provide(Reference.defaultLayer), diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index a645edc51c4b..08cbca17b4b6 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -31,7 +31,7 @@ const BACKGROUND_DESCRIPTION = [ ].join(" "), ].join("\n") -const BaseParameters = Schema.Struct({ +const BaseParameterFields = { description: Schema.String.annotate({ description: "A short (3-5 words) description of the task" }), prompt: Schema.String.annotate({ description: "The task for the agent to perform" }), subagent_type: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }), @@ -40,17 +40,12 @@ const BaseParameters = 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" }), -}) +} + +const BaseParameters = Schema.Struct(BaseParameterFields) export const Parameters = Schema.Struct({ - description: Schema.String.annotate({ description: "A short (3-5 words) description of the task" }), - prompt: Schema.String.annotate({ description: "The task for the agent to perform" }), - 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)", - }), - command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }), + ...BaseParameterFields, background: Schema.optional(Schema.Boolean).annotate({ description: "Run the agent in the background and deliver a notification when it completes", }), @@ -139,9 +134,8 @@ export const TaskTool = Tool.define( return yield* Effect.fail(new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)) } - const taskID = params.task_id - const session = taskID - ? yield* sessions.get(SessionID.make(taskID)).pipe(Effect.catchCause(() => Effect.succeed(undefined))) + const session = params.task_id + ? yield* sessions.get(SessionID.make(params.task_id)).pipe(Effect.catchCause(() => Effect.succeed(undefined))) : undefined const parent = yield* sessions.get(ctx.sessionID) const parentAgent = parent.agent @@ -187,7 +181,6 @@ export const TaskTool = Tool.define( const ops = ctx.extra?.promptOps as TaskPromptOps if (!ops) return yield* Effect.fail(new Error("TaskTool requires promptOps in ctx.extra")) - const runCancel = yield* EffectBridge.make() const runTask = Effect.fn("TaskTool.runTask")(function* () { const parts = yield* ops.resolvePromptParts(params.prompt) @@ -266,6 +259,7 @@ export const TaskTool = Tool.define( } } + const runCancel = yield* EffectBridge.make() const cancel = ops.cancel(nextSession.id) function onAbort() { From ec95fca2037d24dfff9bc5a58dd6711ebf063139 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 25 May 2026 13:08:20 -0500 Subject: [PATCH 7/8] fix(opencode): restore task output trust guidance --- packages/opencode/src/tool/task.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/task.txt b/packages/opencode/src/tool/task.txt index 3b8bf78191cb..e2ac605005b4 100644 --- a/packages/opencode/src/tool/task.txt +++ b/packages/opencode/src/tool/task.txt @@ -13,6 +13,6 @@ 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. -4. Treat the agent's response as a report. If it wrote or edited code, inspect the actual changes before reporting that work as done. +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. From eef36abbb33c6d210d5b29d345a67355312c3eb1 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 25 May 2026 13:10:10 -0500 Subject: [PATCH 8/8] fix(opencode): clarify background parameter description --- packages/opencode/src/tool/task.ts | 2 +- .../opencode/test/tool/__snapshots__/parameters.test.ts.snap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 08cbca17b4b6..864400c1c162 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -47,7 +47,7 @@ const BaseParameters = Schema.Struct(BaseParameterFields) export const Parameters = Schema.Struct({ ...BaseParameterFields, background: Schema.optional(Schema.Boolean).annotate({ - description: "Run the agent in the background and deliver a notification when it completes", + description: "Run the agent in the background. You will be notified when it completes.", }), }) diff --git a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap index d33a2aa15426..1be32979ddfe 100644 --- a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap +++ b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap @@ -320,7 +320,7 @@ exports[`tool parameters JSON Schema (wire shape) task 1`] = ` "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "background": { - "description": "Run the agent in the background and deliver a notification when it completes", + "description": "Run the agent in the background. You will be notified when it completes.", "type": "boolean", }, "command": {