fix(cli): surface failed subagent task errors

This commit is contained in:
marius-kilocode
2026-05-21 14:15:04 +02:00
parent c03f3a016a
commit 7025c779f7
5 changed files with 112 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Surface failed sub-agent tasks as tool errors so parent sessions can recover.
@@ -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() },
},
})
+7
View File
@@ -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: {
@@ -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",
() =>
+55 -1
View File
@@ -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(
() =>