From c313504c824cf2103a4e703d994d1914d84fc8b7 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:57:33 -0500 Subject: [PATCH] fix(opencode): surface resumable subagent errors (#43657) Co-authored-by: rekram1-node --- packages/opencode/src/tool/task.ts | 9 ++++ packages/opencode/test/tool/task.test.ts | 60 ++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 1384e5d1972..74aabd7598a 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -210,6 +210,15 @@ export const TaskTool = Tool.define( agent: next.name, parts, }) + if (result.info.role === "assistant" && result.info.error) { + const message = + "message" in result.info.error.data && typeof result.info.error.data.message === "string" + ? result.info.error.data.message + : result.info.error.name + return yield* Effect.fail( + new Error(`Subagent failed (task_id: ${nextSession.id}): ${message}`), + ) + } return result.parts.findLast((item) => item.type === "text")?.text ?? "" }) diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 2bcf05a2a49..967dd046a42 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -3,7 +3,7 @@ import { SessionV1 } from "@opencode-ai/core/v1/session" import { Database } from "@opencode-ai/core/database/database" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { SessionProjector } from "@opencode-ai/core/session/projector" -import { Deferred, Effect, Exit, Fiber, Layer } from "effect" +import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect" import { Agent } from "../../src/agent/agent" import { BackgroundJob } from "@/background/job" import { EventV2Bridge } from "@/event-v2-bridge" @@ -96,19 +96,27 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") { return { chat, assistant } }) -function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void; text?: string }): TaskPromptOps { +function stubOps(opts?: { + onPrompt?: (input: SessionPrompt.PromptInput) => void + text?: string + error?: NonNullable +}): TaskPromptOps { return { cancel: () => Effect.void, resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]), prompt: (input) => Effect.sync(() => { opts?.onPrompt?.(input) - return reply(input, opts?.text ?? "done") + return reply(input, opts?.text ?? "done", opts?.error) }), } } -function reply(input: SessionPrompt.PromptInput, text: string): SessionV1.WithParts { +function reply( + input: SessionPrompt.PromptInput, + text: string, + error?: NonNullable, +): SessionV1.WithParts { const id = MessageID.ascending() return { info: { @@ -125,6 +133,7 @@ function reply(input: SessionPrompt.PromptInput, text: string): SessionV1.WithPa providerID: input.model?.providerID ?? ref.providerID, time: { created: Date.now() }, finish: "stop", + error, }, parts: [ { @@ -255,6 +264,49 @@ describe("tool.task", () => { }), ) + it.instance("execute surfaces child errors with a resumable task_id", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* tool.init() + + const exit = yield* def + .execute( + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { + promptOps: stubOps({ + text: "", + error: new SessionV1.APIError({ message: "Network connection lost", isRetryable: false }).toObject(), + }), + }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + .pipe(Effect.exit) + + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isSuccess(exit)) throw new Error("expected task failure") + const child = (yield* sessions.children(chat.id))[0] + expect(child).toBeDefined() + const failure = Cause.squash(exit.cause) + expect(failure).toBeInstanceOf(Error) + if (!(failure instanceof Error)) throw new Error("expected Error defect") + expect(failure.message).toBe(`Subagent failed (task_id: ${child?.id}): Network connection lost`) + }), + ) + it.instance("execute asks by default and skips checks when bypassed", () => Effect.gen(function* () { const { chat, assistant } = yield* seed()