fix(opencode): surface subagent tool errors (#43821)
This commit is contained in:
@@ -217,6 +217,10 @@ export const TaskTool = Tool.define(
|
|||||||
: result.info.error.name
|
: result.info.error.name
|
||||||
return yield* Effect.fail(new Error(`Subagent failed (task_id: ${nextSession.id}): ${message}`))
|
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 ?? ""
|
return result.parts.findLast((item) => item.type === "text")?.text ?? ""
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ function stubOps(opts?: {
|
|||||||
onPrompt?: (input: SessionPrompt.PromptInput) => void
|
onPrompt?: (input: SessionPrompt.PromptInput) => void
|
||||||
text?: string
|
text?: string
|
||||||
error?: NonNullable<SessionV1.Assistant["error"]>
|
error?: NonNullable<SessionV1.Assistant["error"]>
|
||||||
|
toolError?: string
|
||||||
}): TaskPromptOps {
|
}): TaskPromptOps {
|
||||||
return {
|
return {
|
||||||
cancel: () => Effect.void,
|
cancel: () => Effect.void,
|
||||||
@@ -107,7 +108,7 @@ function stubOps(opts?: {
|
|||||||
prompt: (input) =>
|
prompt: (input) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
opts?.onPrompt?.(input)
|
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,
|
input: SessionPrompt.PromptInput,
|
||||||
text: string,
|
text: string,
|
||||||
error?: NonNullable<SessionV1.Assistant["error"]>,
|
error?: NonNullable<SessionV1.Assistant["error"]>,
|
||||||
|
toolError?: string,
|
||||||
): SessionV1.WithParts {
|
): SessionV1.WithParts {
|
||||||
const id = MessageID.ascending()
|
const id = MessageID.ascending()
|
||||||
return {
|
return {
|
||||||
@@ -143,6 +145,24 @@ function reply(
|
|||||||
type: "text",
|
type: "text",
|
||||||
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", () =>
|
it.instance("execute asks by default and skips checks when bypassed", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
|
|||||||
Reference in New Issue
Block a user