fix(opencode): remove task status tool

This commit is contained in:
Aiden Cline
2026-05-25 00:29:20 -05:00
parent d5f397a2da
commit a4df9877d3
6 changed files with 5 additions and 307 deletions
-4
View File
@@ -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,
+4 -6
View File
@@ -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",
"",
"<task_result>",
"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.",
"</task_result>",
].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) {
-179
View File
@@ -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<MessageV2.Assistant["error"]>) {
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<InspectResult> = 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<typeof Parameters>,
_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<typeof Parameters>, ctx: Tool.Context) =>
run(params, ctx).pipe(Effect.orDie),
}
}),
)
@@ -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`)
- `<task_result>...</task_result>` or `<task_error>...</task_error>` containing final output, error summary, or current progress text
+1 -13
View File
@@ -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
@@ -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<RuntimeFlags.Info> = {}) =>
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)
}),
)
})