From afea7fdf2ad2207940b14b4528669eb0bb8640e2 Mon Sep 17 00:00:00 2001 From: ELon mac <2597235979@qq.com> Date: Thu, 23 Jul 2026 18:16:16 +0800 Subject: [PATCH] fix: treat unsuccessful tool results as errors --- packages/core/src/agent/aipex.test.ts | 119 ++++++++++++++++++++++++++ packages/core/src/agent/aipex.ts | 26 ++++-- 2 files changed, 140 insertions(+), 5 deletions(-) diff --git a/packages/core/src/agent/aipex.test.ts b/packages/core/src/agent/aipex.test.ts index e5086e90..c9b852cd 100644 --- a/packages/core/src/agent/aipex.test.ts +++ b/packages/core/src/agent/aipex.test.ts @@ -1115,6 +1115,125 @@ describe("AIPex", () => { } }); + it("should treat a completed tool result with success=false as an error", async () => { + vi.mocked(run).mockResolvedValue( + createMockRunResult({ + finalOutput: "", + streamEvents: [ + { + type: "run_item_stream_event", + name: "tool_called", + item: { rawItem: { name: "click", arguments: "{}" } }, + }, + { + type: "run_item_stream_event", + name: "tool_output", + item: { + rawItem: { name: "click", status: "completed" }, + output: JSON.stringify({ + success: false, + error: "Element is stale", + }), + }, + }, + ], + }), + ); + + const agent = AIPex.create({ + instructions: "Tools", + model: mockModel, + }); + + const events: AgentEvent[] = []; + for await (const event of agent.chat("click")) events.push(event); + + const errorEvent = events.find( + (event) => event.type === "tool_call_error", + ); + expect(errorEvent).toBeDefined(); + if (errorEvent?.type === "tool_call_error") { + expect(errorEvent.toolName).toBe("click"); + expect(errorEvent.error.message).toBe("Element is stale"); + } + expect(events.some((event) => event.type === "tool_call_complete")).toBe( + false, + ); + }); + + it("should provide a fallback message for an unsuccessful tool result", async () => { + vi.mocked(run).mockResolvedValue( + createMockRunResult({ + finalOutput: "", + streamEvents: [ + { + type: "run_item_stream_event", + name: "tool_output", + item: { + rawItem: { name: "submit", status: "completed" }, + output: '{"success":false}', + }, + }, + ], + }), + ); + + const agent = AIPex.create({ + instructions: "Tools", + model: mockModel, + }); + + const events: AgentEvent[] = []; + for await (const event of agent.chat("submit")) events.push(event); + + expect(events).toContainEqual( + expect.objectContaining({ + type: "tool_call_error", + toolName: "submit", + error: expect.objectContaining({ + message: "Tool 'submit' reported success=false", + }), + }), + ); + }); + + it.each([ + ["a regular object", '{"result":2}'], + ["a string result", "plain text"], + ["a non-boolean false value", '{"success":"false"}'], + ])("should keep %s as a completed tool result", async (_label, output) => { + vi.mocked(run).mockResolvedValue( + createMockRunResult({ + finalOutput: "", + streamEvents: [ + { + type: "run_item_stream_event", + name: "tool_output", + item: { + rawItem: { name: "lookup", status: "completed" }, + output, + }, + }, + ], + }), + ); + + const agent = AIPex.create({ + instructions: "Tools", + model: mockModel, + }); + + const events: AgentEvent[] = []; + for await (const event of agent.chat("lookup")) events.push(event); + + expect(events.some((event) => event.type === "tool_call_complete")).toBe( + true, + ); + expect(events.some((event) => event.type === "tool_call_error")).toBe( + false, + ); + }); + it("should emit error event when run fails", async () => { vi.mocked(run).mockRejectedValue(new Error("LLM failed")); diff --git a/packages/core/src/agent/aipex.ts b/packages/core/src/agent/aipex.ts index 83fa5e26..6fe4a083 100644 --- a/packages/core/src/agent/aipex.ts +++ b/packages/core/src/agent/aipex.ts @@ -517,9 +517,25 @@ export class AIPex { } const status = this.getToolStatus(event.item); - if (status !== "completed") { - const toolName = this.extractToolName(event.item); - const failureMessage = this.extractToolFailureMessage(event.item, status); + const toolName = this.extractToolName(event.item); + const result = this.extractToolOutput(event.item); + const reportedFailure = + typeof result === "object" && + result !== null && + "success" in result && + (result as { success?: unknown }).success === false; + + if (status !== "completed" || reportedFailure) { + const failureResult = result as Record; + const failureMessage = + status !== "completed" + ? this.extractToolFailureMessage(event.item, status) + : this.sanitizeErrorMessage( + this.extractErrorFromValue( + failureResult.error ?? failureResult.message, + ) ?? `Tool '${toolName}' reported success=false`, + 500, + ); return { type: "tool_call_error", toolName, @@ -529,8 +545,8 @@ export class AIPex { return { type: "tool_call_complete", - toolName: this.extractToolName(event.item), - result: this.extractToolOutput(event.item), + toolName, + result, }; }