Skip to content

Commit 1832bff

Browse files
authored
fix(ai): retry tool calls after argument validation failure (#389)
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
1 parent 5b943cd commit 1832bff

2 files changed

Lines changed: 86 additions & 5 deletions

File tree

‎src/services/ai/providers/openai-chat-completion.ts‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -466,11 +466,7 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
466466
JSON.stringify({ success: false, error: errorMessage })
467467
);
468468

469-
return {
470-
success: false,
471-
error: errorMessage,
472-
iterations,
473-
};
469+
break;
474470
}
475471
}
476472

‎tests/openai-chat-completion-provider.test.ts‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,91 @@ describe("OpenAIChatCompletionProvider", () => {
412412
expect(result.iterations).toBe(1);
413413
});
414414

415+
it("retries after tool-argument validation failure and succeeds on next attempt", async () => {
416+
const invalidArguments = JSON.stringify({
417+
preferences: [],
418+
patterns: [],
419+
workflows: "none",
420+
});
421+
const validArguments = JSON.stringify({
422+
preferences: [],
423+
patterns: [],
424+
workflows: [],
425+
codingStyle: {},
426+
domainKnowledge: [],
427+
});
428+
429+
const responses = [
430+
{
431+
choices: [
432+
{
433+
message: {
434+
content: null,
435+
tool_calls: [
436+
{
437+
id: "call-1",
438+
type: "function",
439+
function: { name: "save_memories", arguments: invalidArguments },
440+
},
441+
],
442+
},
443+
},
444+
],
445+
},
446+
{
447+
choices: [
448+
{
449+
message: {
450+
content: null,
451+
tool_calls: [
452+
{
453+
id: "call-2",
454+
type: "function",
455+
function: { name: "save_memories", arguments: validArguments },
456+
},
457+
],
458+
},
459+
},
460+
],
461+
},
462+
];
463+
464+
let callCount = 0;
465+
const requestBodies: Array<{ messages?: Array<{ role?: string; content?: string }> }> = [];
466+
467+
globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
468+
const body = JSON.parse(String(init?.body ?? "{}"));
469+
requestBodies.push(body);
470+
const responseBody = responses[callCount] ?? responses[responses.length - 1];
471+
callCount++;
472+
return {
473+
ok: true,
474+
status: 200,
475+
statusText: "OK",
476+
text: async () => JSON.stringify(responseBody),
477+
json: async () => responseBody,
478+
} as Response;
479+
}) as typeof fetch;
480+
481+
const result = await makeProvider({
482+
maxIterations: 3,
483+
apiUrl: "https://api.openai.com/v1",
484+
}).executeToolCall("system", "user", toolSchema, "session-id");
485+
486+
expect(result.success).toBe(true);
487+
expect(result.iterations).toBe(2);
488+
expect(callCount).toBe(2);
489+
490+
const secondMessages = requestBodies[1]?.messages ?? [];
491+
const retryFeedback = secondMessages.some(
492+
(msg) =>
493+
typeof msg.content === "string" &&
494+
(msg.content.includes("Validation failed") ||
495+
msg.content.includes("workflows must be an array"))
496+
);
497+
expect(retryFeedback).toBe(true);
498+
});
499+
415500
it("returns success: false after max iterations with no tool call", async () => {
416501
globalThis.fetch = makeFetch({
417502
ok: true,

0 commit comments

Comments
 (0)