From e22040616c9c82797dc4187dc8c2611d779bfd76 Mon Sep 17 00:00:00 2001 From: EyJunge1 <149941075+EyJunge1@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:53:23 +0200 Subject: [PATCH] fix(ai): retry tool calls after argument validation failure Validation errors in executeToolCall returned immediately and skipped the built-in retry loop, aborting user-profile-learning for the idle cycle. Break out of the tool-call loop instead so the model can fix invalid JSON within maxIterations. Fixes #388 --- .../ai/providers/openai-chat-completion.ts | 6 +- tests/openai-chat-completion-provider.test.ts | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/services/ai/providers/openai-chat-completion.ts b/src/services/ai/providers/openai-chat-completion.ts index faebf6c7..f590084a 100644 --- a/src/services/ai/providers/openai-chat-completion.ts +++ b/src/services/ai/providers/openai-chat-completion.ts @@ -466,11 +466,7 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider { JSON.stringify({ success: false, error: errorMessage }) ); - return { - success: false, - error: errorMessage, - iterations, - }; + break; } } diff --git a/tests/openai-chat-completion-provider.test.ts b/tests/openai-chat-completion-provider.test.ts index 9fe38e16..5de3ba73 100644 --- a/tests/openai-chat-completion-provider.test.ts +++ b/tests/openai-chat-completion-provider.test.ts @@ -412,6 +412,91 @@ describe("OpenAIChatCompletionProvider", () => { expect(result.iterations).toBe(1); }); + it("retries after tool-argument validation failure and succeeds on next attempt", async () => { + const invalidArguments = JSON.stringify({ + preferences: [], + patterns: [], + workflows: "none", + }); + const validArguments = JSON.stringify({ + preferences: [], + patterns: [], + workflows: [], + codingStyle: {}, + domainKnowledge: [], + }); + + const responses = [ + { + choices: [ + { + message: { + content: null, + tool_calls: [ + { + id: "call-1", + type: "function", + function: { name: "save_memories", arguments: invalidArguments }, + }, + ], + }, + }, + ], + }, + { + choices: [ + { + message: { + content: null, + tool_calls: [ + { + id: "call-2", + type: "function", + function: { name: "save_memories", arguments: validArguments }, + }, + ], + }, + }, + ], + }, + ]; + + let callCount = 0; + const requestBodies: Array<{ messages?: Array<{ role?: string; content?: string }> }> = []; + + globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + const body = JSON.parse(String(init?.body ?? "{}")); + requestBodies.push(body); + const responseBody = responses[callCount] ?? responses[responses.length - 1]; + callCount++; + return { + ok: true, + status: 200, + statusText: "OK", + text: async () => JSON.stringify(responseBody), + json: async () => responseBody, + } as Response; + }) as typeof fetch; + + const result = await makeProvider({ + maxIterations: 3, + apiUrl: "https://api.openai.com/v1", + }).executeToolCall("system", "user", toolSchema, "session-id"); + + expect(result.success).toBe(true); + expect(result.iterations).toBe(2); + expect(callCount).toBe(2); + + const secondMessages = requestBodies[1]?.messages ?? []; + const retryFeedback = secondMessages.some( + (msg) => + typeof msg.content === "string" && + (msg.content.includes("Validation failed") || + msg.content.includes("workflows must be an array")) + ); + expect(retryFeedback).toBe(true); + }); + it("returns success: false after max iterations with no tool call", async () => { globalThis.fetch = makeFetch({ ok: true,