fix(opencode): surface resumable subagent errors (#43657)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
e49772a8b4
commit
c313504c82
@@ -210,6 +210,15 @@ export const TaskTool = Tool.define(
|
|||||||
agent: next.name,
|
agent: next.name,
|
||||||
parts,
|
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 ?? ""
|
return result.parts.findLast((item) => item.type === "text")?.text ?? ""
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SessionV1 } from "@opencode-ai/core/v1/session"
|
|||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
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 { Agent } from "../../src/agent/agent"
|
||||||
import { BackgroundJob } from "@/background/job"
|
import { BackgroundJob } from "@/background/job"
|
||||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||||
@@ -96,19 +96,27 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
|
|||||||
return { chat, assistant }
|
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<SessionV1.Assistant["error"]>
|
||||||
|
}): TaskPromptOps {
|
||||||
return {
|
return {
|
||||||
cancel: () => Effect.void,
|
cancel: () => Effect.void,
|
||||||
resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]),
|
resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]),
|
||||||
prompt: (input) =>
|
prompt: (input) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
opts?.onPrompt?.(input)
|
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.Assistant["error"]>,
|
||||||
|
): SessionV1.WithParts {
|
||||||
const id = MessageID.ascending()
|
const id = MessageID.ascending()
|
||||||
return {
|
return {
|
||||||
info: {
|
info: {
|
||||||
@@ -125,6 +133,7 @@ function reply(input: SessionPrompt.PromptInput, text: string): SessionV1.WithPa
|
|||||||
providerID: input.model?.providerID ?? ref.providerID,
|
providerID: input.model?.providerID ?? ref.providerID,
|
||||||
time: { created: Date.now() },
|
time: { created: Date.now() },
|
||||||
finish: "stop",
|
finish: "stop",
|
||||||
|
error,
|
||||||
},
|
},
|
||||||
parts: [
|
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", () =>
|
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