From 7025c779f74b2c68afa05bd2f70ce1123ae9cecc Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 21 May 2026 14:15:04 +0200 Subject: [PATCH 1/2] fix(cli): surface failed subagent task errors --- .changeset/clean-agents-fail.md | 5 ++ packages/opencode/src/session/processor.ts | 1 + packages/opencode/src/tool/task.ts | 7 +++ packages/opencode/test/session/prompt.test.ts | 44 +++++++++++++++ packages/opencode/test/tool/task.test.ts | 56 ++++++++++++++++++- 5 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 .changeset/clean-agents-fail.md 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 f29d6b3d16b..f320991d298 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -240,6 +240,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 f39d0bee9ff..e6e0e4c5d35 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, Schema } from "effect" export interface TaskPromptOps { @@ -173,6 +174,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 64161047bcf..005e64d58eb 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -745,6 +745,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 3646c1b1851..90d6572975d 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect } from "bun:test" -import { Effect, Layer } from "effect" +import { Cause, Effect, Layer } from "effect" import { Agent } from "../../src/agent/agent" import { Config } from "@/config/config" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" @@ -324,6 +324,60 @@ describe("tool.task", () => { ), ) + // kilocode_change start - terminal child assistant errors fail the task tool boundary + it.live("execute fails when child prompt returns assistant error", () => + provideTmpdirInstance(() => + Effect.gen(function* () { + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* tool.init() + const promptOps: TaskPromptOps = { + cancel() {}, + 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._tag).toBe("Failure") + if (exit._tag !== "Failure") return + const err = Cause.squash(exit.cause) + expect(err).toBeInstanceOf(Error) + expect(err instanceof Error ? err.message : String(err)).toBe("child prompt failed") + }), + ), + ) + // kilocode_change end + it.live("execute shapes child permissions for task, todowrite, and primary tools", () => provideTmpdirInstance( () => From 2caa5489f2b262422bfb578251424f7a57b9734f Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 21 May 2026 14:15:50 +0200 Subject: [PATCH 2/2] chore(cli): annotate task failure test import --- packages/opencode/test/tool/task.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 90d6572975d..d7d1b0c8193 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect } from "bun:test" -import { Cause, Effect, Layer } from "effect" +import { Cause, Effect, Layer } from "effect" // kilocode_change - inspect task failure exit causes import { Agent } from "../../src/agent/agent" import { Config } from "@/config/config" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"