mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge pull request #10485 from Kilo-Org/fix-subagent-error-recovery
fix(cli): surface failed subagent task errors
This commit is contained in:
@@ -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() },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
() =>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user