From a4df9877d36ba1106f7a4c71f1b5699cb11259ef Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 25 May 2026 00:29:20 -0500 Subject: [PATCH] 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 6ef6d39a65..cfb4bf00d9 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 fece68800b..d3d97333d8 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 b458b4fc45..0000000000 --- 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 ed6fa727b2..0000000000 --- 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 d3549e66f3..25c50678ad 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 23bd49c616..0000000000 --- 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) - }), - ) -})