From afda4d6202cc179f3d7029d50c9e5752c3b3097d Mon Sep 17 00:00:00 2001 From: yaogangqiang Date: Tue, 1 Sep 2026 17:00:51 +0800 Subject: [PATCH] fix(agent): stop prepared tools after preflight abort (#8936) fixes #8935 Co-authored-by: yaogangqiang --- packages/agent/src/agent-loop.ts | 9 +++ .../8935-parallel-preflight-abort.test.ts | 72 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 packages/coding-agent/test/suite/regressions/8935-parallel-preflight-abort.test.ts diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 7a9d3516b..193e3faf9 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -518,6 +518,15 @@ async function executeToolCallsParallel( } finalizedCalls.push(async () => { + if (signal?.aborted) { + const finalized = { + toolCall, + result: createErrorToolResult("Operation aborted"), + isError: true, + } satisfies FinalizedToolCallOutcome; + await emitToolExecutionEnd(finalized, emit); + return finalized; + } const executed = await executePreparedToolCall(preparation, signal, emit); const finalized = await finalizeExecutedToolCall( currentContext, diff --git a/packages/coding-agent/test/suite/regressions/8935-parallel-preflight-abort.test.ts b/packages/coding-agent/test/suite/regressions/8935-parallel-preflight-abort.test.ts new file mode 100644 index 000000000..da10cf815 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/8935-parallel-preflight-abort.test.ts @@ -0,0 +1,72 @@ +import type { AgentTool } from "@earendil-works/pi-agent-core"; +import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai"; +import { Type } from "typebox"; +import { afterEach, describe, expect, it } from "vitest"; +import { createHarness, getMessageText, type Harness } from "../harness.ts"; + +describe("#8935 parallel preflight abort", () => { + const harnesses: Harness[] = []; + + afterEach(() => { + while (harnesses.length > 0) { + harnesses.pop()?.cleanup(); + } + }); + + it("does not start prepared tools after a later preflight aborts", async () => { + const executions: string[] = []; + const preflights: string[] = []; + const resultHooks: string[] = []; + const externalWrite: AgentTool = { + name: "external_write", + label: "External write", + description: "Perform an external write", + parameters: Type.Object({ value: Type.String() }), + execute: async (_toolCallId, params) => { + const value = + typeof params === "object" && params !== null && "value" in params ? String(params.value) : ""; + executions.push(value); + return { content: [{ type: "text", text: value }], details: { value } }; + }, + }; + const harness = await createHarness({ + tools: [externalWrite], + extensionFactories: [ + (pi) => { + pi.on("tool_call", async (event, ctx) => { + const value = "value" in event.input ? String(event.input.value) : ""; + preflights.push(value); + if (value === "second") ctx.abort(); + }); + pi.on("tool_result", async (event) => { + resultHooks.push(event.toolCallId); + }); + }, + ], + }); + harnesses.push(harness); + harness.setResponses([ + fauxAssistantMessage( + [fauxToolCall("external_write", { value: "first" }), fauxToolCall("external_write", { value: "second" })], + { stopReason: "toolUse" }, + ), + ]); + + await harness.session.prompt("run both writes"); + + expect(preflights).toEqual(["first", "second"]); + expect(executions).toEqual([]); + expect(resultHooks).toEqual([]); + + const starts = harness.eventsOfType("tool_execution_start"); + const ends = harness.eventsOfType("tool_execution_end"); + expect(starts).toHaveLength(2); + expect(ends).toHaveLength(2); + expect(new Set(ends.map((event) => event.toolCallId))).toEqual(new Set(starts.map((event) => event.toolCallId))); + expect(ends.every((event) => event.isError)).toBe(true); + + const toolResults = harness.session.messages.filter((message) => message.role === "toolResult"); + expect(toolResults.map((message) => message.toolCallId)).toEqual(starts.map((event) => event.toolCallId)); + expect(toolResults.map(getMessageText)).toEqual(["Operation aborted", "Operation aborted"]); + }); +});