diff --git a/.changeset/clean-agents-fail.md b/.changeset/clean-agents-fail.md new file mode 100644 index 00000000000..d363e5442c1 --- /dev/null +++ b/.changeset/clean-agents-fail.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Surface failed sub-agent tasks as tool errors so parent sessions can recover. diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 1db62f2fd79..9723f489339 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -246,6 +246,7 @@ export const layer: Layer.Layer< status: "error", input: match.part.state.input, error: errorMessage(error), + metadata: match.part.state.metadata, // kilocode_change - preserve running tool metadata on failure time: { start: match.part.state.time.start, end: Date.now() }, }, }) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 24075d02847..ad709f1d614 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -9,6 +9,7 @@ import { Config } from "@/config/config" import { KiloTask } from "../kilocode/tool/task" // kilocode_change import { KiloCostPropagation } from "../kilocode/session/cost-propagation" // kilocode_change import { KiloSessionProcessor } from "../kilocode/session/processor" // kilocode_change +import { errorMessage } from "@/util/error" // kilocode_change import { Effect, Exit, Schema } from "effect" import { EffectBridge } from "@/effect/bridge" @@ -180,6 +181,12 @@ export const TaskTool = Tool.define( parts, }) + // kilocode_change start - expose terminal child assistant errors through the task tool boundary + if (result.info.role === "assistant" && result.info.error) { + return yield* Effect.fail(new Error(errorMessage(result.info.error))) + } + // kilocode_change end + return { title: params.description, metadata: { diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index fd055871774..78085bccec2 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -746,6 +746,50 @@ it.live( 10_000, // kilocode_change ) +// kilocode_change start - child task failures stay tool errors so the parent can recover +it.live("failed task tool preserves metadata and lets the parent follow up", () => + provideTmpdirServer( + Effect.fnUntraced(function* ({ llm }) { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ + title: "Pinned", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) + yield* llm.tool("task", { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + }) + yield* llm.error(400, { error: { message: "child prompt failed" } }) + yield* llm.text("parent recovered") + yield* user(chat.id, "hello") + + const result = yield* prompt.loop({ sessionID: chat.id }) + expect(yield* llm.calls).toBe(3) + expect(result.parts.some((part) => part.type === "text" && part.text === "parent recovered")).toBe(true) + + const msgs = yield* MessageV2.filterCompactedEffect(chat.id) + const part = msgs + .flatMap((msg) => msg.parts) + .find( + (part): part is ErrorToolPart => + part.type === "tool" && part.tool === "task" && part.state.status === "error", + ) + expect(part).toBeDefined() + if (!part) return + expect(part.state.error).toContain("child prompt failed") + expect(part.state.metadata?.sessionId).toBeDefined() + + const hits = yield* llm.hits + expect(hits).toHaveLength(3) + expect(JSON.stringify(hits.at(-1)?.body)).toContain("child prompt failed") + }), + { git: true, config: providerCfg }, + ), +) +// kilocode_change end + it.live( "loop sets status to busy then idle", () => diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 8bd5ac390b9..758e27c2e32 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -454,6 +454,54 @@ describe("tool.task", () => { }, }, ) + + // kilocode_change start - terminal child assistant errors fail the task tool boundary + it.instance("execute fails when child prompt returns assistant error", () => + Effect.gen(function* () { + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* tool.init() + const promptOps: TaskPromptOps = { + cancel: () => Effect.void, + resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]), + prompt: (input) => + Effect.sync(() => { + const result = reply(input, "partial") + if (result.info.role !== "assistant") return result + return { + ...result, + info: { + ...result.info, + error: MessageV2.fromError(new Error("child prompt failed"), { providerID: ref.providerID }), + }, + } + }), + } + + 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 }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + }, + ) + .pipe(Effect.exit) + + expect(Exit.isFailure(exit)).toBe(true) + }), + ) + // kilocode_change end }) // kilocode_change start - subagent cost propagation coverage (#6321)