From 35fe5b7212d6cd69308d4006048740a351d27a52 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:44:44 -0500 Subject: [PATCH] fix(opencode): surface subagent tool errors (#43821) --- packages/opencode/src/tool/task.ts | 4 ++ packages/opencode/test/tool/task.test.ts | 66 +++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index de5d396992d..d8ca640cfba 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -217,6 +217,10 @@ export const TaskTool = Tool.define( : result.info.error.name return yield* Effect.fail(new Error(`Subagent failed (task_id: ${nextSession.id}): ${message}`)) } + const failed = result.parts.findLast((item) => item.type === "tool" && item.state.status === "error") + if (failed?.type === "tool" && failed.state.status === "error") { + return yield* Effect.fail(new Error(`Subagent failed (task_id: ${nextSession.id}): ${failed.state.error}`)) + } 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 967dd046a42..42f46fd35d7 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -100,6 +100,7 @@ function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void text?: string error?: NonNullable + toolError?: string }): TaskPromptOps { return { cancel: () => Effect.void, @@ -107,7 +108,7 @@ function stubOps(opts?: { prompt: (input) => Effect.sync(() => { opts?.onPrompt?.(input) - return reply(input, opts?.text ?? "done", opts?.error) + return reply(input, opts?.text ?? "done", opts?.error, opts?.toolError) }), } } @@ -116,6 +117,7 @@ function reply( input: SessionPrompt.PromptInput, text: string, error?: NonNullable, + toolError?: string, ): SessionV1.WithParts { const id = MessageID.ascending() return { @@ -143,6 +145,24 @@ function reply( type: "text", text, }, + ...(toolError + ? [ + { + id: PartID.ascending(), + messageID: id, + sessionID: input.sessionID, + type: "tool" as const, + tool: "read", + callID: "call-1", + state: { + status: "error" as const, + input: { filePath: "/external" }, + error: toolError, + time: { start: Date.now(), end: Date.now() }, + }, + }, + ] + : []), ], } } @@ -307,6 +327,50 @@ describe("tool.task", () => { }), ) + it.instance("execute surfaces terminal child tool 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 external directory", + prompt: "read the external directory", + subagent_type: "general", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { + promptOps: stubOps({ + text: "I will inspect the directory.", + toolError: "The user rejected permission to use this specific tool call.", + }), + }, + 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] + 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}): The user rejected permission to use this specific tool call.`, + ) + }), + ) + it.instance("execute asks by default and skips checks when bypassed", () => Effect.gen(function* () { const { chat, assistant } = yield* seed()