소스 검색

remove the need for polling from experimental background agents (#29179)

Aiden Cline 2 달 전
부모
커밋
dabf2dc013

+ 0 - 1
packages/opencode/src/session/prompt.ts

@@ -134,7 +134,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
     })
 

+ 1 - 7
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"
@@ -53,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" })
@@ -91,7 +89,6 @@ export const layer: Layer.Layer<
   | Agent.Service
   | Skill.Service
   | Session.Service
-  | SessionStatus.Service
   | BackgroundJob.Service
   | Provider.Service
   | Git.Service
@@ -119,7 +116,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 +231,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 +255,6 @@ export const layer: Layer.Layer<
             tool.edit,
             tool.write,
             tool.task,
-            ...(flags.experimentalBackgroundSubagents ? [tool.task_status] : []),
             tool.fetch,
             tool.todo,
             tool.search,
@@ -385,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),

+ 49 - 91
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<void>
   resolvePromptParts(template: string): Effect.Effect<SessionPrompt.PromptInput["parts"]>
   prompt(input: SessionPrompt.PromptInput): Effect.Effect<MessageV2.WithParts>
-  loop(input: SessionPrompt.LoopInput): Effect.Effect<MessageV2.WithParts>
 }
 
 const id = "task"
@@ -28,12 +24,14 @@ 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.",
+    "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")
 
-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" }),
@@ -42,40 +40,32 @@ 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: "When true, launch the subagent in the background and return immediately",
+    description: "Run the agent in the background. You will be notified when it completes.",
   }),
 })
 
 function output(sessionID: SessionID, text: string) {
-  return [
-    `task_id: ${sessionID} (for resuming to continue this task if needed)`,
-    "",
-    "<task_result>",
-    text,
-    "</task_result>",
-  ].join("\n")
+  return [`<task id="${sessionID}" state="completed">`, "<task_result>", text, "</task_result>", "</task>"].join(
+    "\n",
+  )
 }
 
 function backgroundOutput(sessionID: SessionID) {
   return [
-    `task_id: ${sessionID} (for polling this task with task_status)`,
-    "state: running",
-    "",
+    `<task id="${sessionID}" state="running">`,
+    "<summary>Background task started</summary>",
     "<task_result>",
-    "Background task started. Continue your current work and call task_status when you need the result.",
+    "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.",
     "</task_result>",
+    "</task>",
   ].join("\n")
 }
 
@@ -90,9 +80,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, `</${tag}>`].join(
-    "\n",
-  )
+  return [
+    `<task id="${input.sessionID}" state="${input.state}">`,
+    `<summary>${title}</summary>`,
+    `<${tag}>`,
+    input.text,
+    `</${tag}>`,
+    "</task>",
+  ].join("\n")
 }
 
 function errorText(error: unknown) {
@@ -105,11 +100,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* (
@@ -141,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
@@ -189,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)
@@ -211,68 +202,34 @@ export const TaskTool = Tool.define(
         return result.parts.findLast((item) => item.type === "text")?.text ?? ""
       })
 
-      const resumeWhenIdle: (input: { userID: MessageID; state: "completed" | "error" }) => Effect.Effect<void> =
-        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)
       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) {
@@ -302,6 +259,7 @@ export const TaskTool = Tool.define(
         }
       }
 
+      const runCancel = yield* EffectBridge.make()
       const cancel = ops.cancel(nextSession.id)
 
       function onAbort() {

+ 0 - 179
packages/opencode/src/tool/task_status.ts

@@ -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),
-    }
-  }),
-)

+ 0 - 13
packages/opencode/src/tool/task_status.txt

@@ -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

+ 4 - 4
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)",
-              "",
+              '<task id="child-1" state="completed">',
               "<task_result>",
               "# Findings\n\n- Footer stays live",
               "</task_result>",
+              "</task>",
             ].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)",
-              "",
+              '<task id="child-1" state="completed">',
               "<task_result>",
               "",
               "</task_result>",
+              "</task>",
             ].join("\n"),
             metadata: {
               sessionId: "child-1",

+ 2 - 2
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)",
-            "",
+            '<task id="child-1" state="completed">',
             "<task_result>",
             "Location: `/tmp/run.ts`",
             "",
@@ -947,6 +946,7 @@ test("renders promoted task markdown without a leading blank row", async () => {
             "- Local interactive mode",
             "- Attach mode",
             "</task_result>",
+            "</task>",
           ].join("\n"),
           metadata: {
             sessionId: "child-1",

+ 1 - 1
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. You will be notified when it completes.",
       "type": "boolean",
     },
     "command": {

+ 1 - 13
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

+ 5 - 28
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")),
   }
 }
 
@@ -237,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(`<task id="${child.id}" state="completed">`)
       expect(seen?.sessionID).toBe(child.id)
     }),
   )
@@ -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
@@ -371,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(`<task id="${result.metadata.sessionId}" state="completed">`)
       expect(seen?.sessionID).toBe(result.metadata.sessionId)
     }),
   )
@@ -511,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")
     }),
   )
@@ -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: [],

+ 0 - 92
packages/opencode/test/tool/task_status.test.ts

@@ -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)
-    }),
-  )
-})