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, `${tag}>`].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)
- }),
- )
-})