Skip to content

Commit 35fe5b7

Browse files
authored
fix(opencode): surface subagent tool errors (anomalyco#43821)
1 parent 62cb3f7 commit 35fe5b7

2 files changed

Lines changed: 69 additions & 1 deletion

File tree

packages/opencode/src/tool/task.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ export const TaskTool = Tool.define(
217217
: result.info.error.name
218218
return yield* Effect.fail(new Error(`Subagent failed (task_id: ${nextSession.id}): ${message}`))
219219
}
220+
const failed = result.parts.findLast((item) => item.type === "tool" && item.state.status === "error")
221+
if (failed?.type === "tool" && failed.state.status === "error") {
222+
return yield* Effect.fail(new Error(`Subagent failed (task_id: ${nextSession.id}): ${failed.state.error}`))
223+
}
220224
return result.parts.findLast((item) => item.type === "text")?.text ?? ""
221225
})
222226

packages/opencode/test/tool/task.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,15 @@ function stubOps(opts?: {
100100
onPrompt?: (input: SessionPrompt.PromptInput) => void
101101
text?: string
102102
error?: NonNullable<SessionV1.Assistant["error"]>
103+
toolError?: string
103104
}): TaskPromptOps {
104105
return {
105106
cancel: () => Effect.void,
106107
resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]),
107108
prompt: (input) =>
108109
Effect.sync(() => {
109110
opts?.onPrompt?.(input)
110-
return reply(input, opts?.text ?? "done", opts?.error)
111+
return reply(input, opts?.text ?? "done", opts?.error, opts?.toolError)
111112
}),
112113
}
113114
}
@@ -116,6 +117,7 @@ function reply(
116117
input: SessionPrompt.PromptInput,
117118
text: string,
118119
error?: NonNullable<SessionV1.Assistant["error"]>,
120+
toolError?: string,
119121
): SessionV1.WithParts {
120122
const id = MessageID.ascending()
121123
return {
@@ -143,6 +145,24 @@ function reply(
143145
type: "text",
144146
text,
145147
},
148+
...(toolError
149+
? [
150+
{
151+
id: PartID.ascending(),
152+
messageID: id,
153+
sessionID: input.sessionID,
154+
type: "tool" as const,
155+
tool: "read",
156+
callID: "call-1",
157+
state: {
158+
status: "error" as const,
159+
input: { filePath: "/external" },
160+
error: toolError,
161+
time: { start: Date.now(), end: Date.now() },
162+
},
163+
},
164+
]
165+
: []),
146166
],
147167
}
148168
}
@@ -307,6 +327,50 @@ describe("tool.task", () => {
307327
}),
308328
)
309329

330+
it.instance("execute surfaces terminal child tool errors with a resumable task_id", () =>
331+
Effect.gen(function* () {
332+
const sessions = yield* Session.Service
333+
const { chat, assistant } = yield* seed()
334+
const tool = yield* TaskTool
335+
const def = yield* tool.init()
336+
337+
const exit = yield* def
338+
.execute(
339+
{
340+
description: "inspect external directory",
341+
prompt: "read the external directory",
342+
subagent_type: "general",
343+
},
344+
{
345+
sessionID: chat.id,
346+
messageID: assistant.id,
347+
agent: "build",
348+
abort: new AbortController().signal,
349+
extra: {
350+
promptOps: stubOps({
351+
text: "I will inspect the directory.",
352+
toolError: "The user rejected permission to use this specific tool call.",
353+
}),
354+
},
355+
messages: [],
356+
metadata: () => Effect.void,
357+
ask: () => Effect.void,
358+
},
359+
)
360+
.pipe(Effect.exit)
361+
362+
expect(Exit.isFailure(exit)).toBe(true)
363+
if (Exit.isSuccess(exit)) throw new Error("expected task failure")
364+
const child = (yield* sessions.children(chat.id))[0]
365+
const failure = Cause.squash(exit.cause)
366+
expect(failure).toBeInstanceOf(Error)
367+
if (!(failure instanceof Error)) throw new Error("expected Error defect")
368+
expect(failure.message).toBe(
369+
`Subagent failed (task_id: ${child?.id}): The user rejected permission to use this specific tool call.`,
370+
)
371+
}),
372+
)
373+
310374
it.instance("execute asks by default and skips checks when bypassed", () =>
311375
Effect.gen(function* () {
312376
const { chat, assistant } = yield* seed()

0 commit comments

Comments
 (0)