fix(opencode): remove task status tool
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}),
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user