Skip to content

Commit cff65f8

Browse files
fix: strip hallucinated args from tool calls to prevent retry loops
Claude models sometimes hallucinate extra arguments (e.g. limit, content_search_mode) that fail schema validation and cause infinite retry storms. Strip unknown top-level properties when schema has additionalProperties: false, log stripped args, and append a feedback note to the tool result so the model learns not to retry with them. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
1 parent 77cac19 commit cff65f8

File tree

2 files changed

+44
-3
lines changed

2 files changed

+44
-3
lines changed

src/handlers/useTool.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ export async function handleUseTool(
120120
}
121121

122122
// Validate arguments unconditionally (before checking dry_run)
123+
let strippedArgs: string[] = [];
123124
try {
124-
validator.validate(schema, args, { package_id, tool_id });
125+
strippedArgs = validator.validate(schema, args, { package_id, tool_id });
125126
} catch (error) {
126127
if (error instanceof ValidationError) {
127128
let helpMessage = `Argument validation failed for tool '${tool_id}' in package '${package_id}'.\n`;
@@ -171,11 +172,16 @@ export async function handleUseTool(
171172
telemetry: { duration_ms: 0, status: "ok" },
172173
};
173174

175+
let dryRunJson = JSON.stringify(result, null, 2);
176+
if (strippedArgs.length > 0) {
177+
dryRunJson += `\n\nNote: Removed unknown arguments before validation: ${strippedArgs.join(', ')}. These are not valid for this tool.`;
178+
}
179+
174180
return {
175181
content: [
176182
{
177183
type: "text",
178-
text: JSON.stringify(result, null, 2),
184+
text: dryRunJson,
179185
},
180186
],
181187
isError: false,
@@ -235,6 +241,10 @@ export async function handleUseTool(
235241
outputJson = JSON.stringify(result, null, 2);
236242
}
237243

244+
if (strippedArgs.length > 0) {
245+
outputJson += `\n\nNote: Removed unknown arguments before execution: ${strippedArgs.join(', ')}. These are not valid for this tool.`;
246+
}
247+
238248
return {
239249
content: [
240250
{

src/validator.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ export class Validator {
3131
addFormats(this.ajv);
3232
}
3333

34-
validate(schema: any, data: any, context?: { package_id?: string; tool_id?: string }): void {
34+
/** Validates data against schema. Mutates `data` in place to strip unknown
35+
* top-level properties when `additionalProperties: false`. Returns names of
36+
* any stripped properties (empty array if none). */
37+
validate(schema: any, data: any, context?: { package_id?: string; tool_id?: string }): string[] {
3538
logger.debug("Validating arguments", {
3639
package_id: context?.package_id,
3740
tool_id: context?.tool_id,
@@ -43,6 +46,32 @@ export class Validator {
4346
throw new ValidationError("Schema is required", []);
4447
}
4548

49+
// Strip unknown top-level properties when schema forbids them.
50+
// Claude models sometimes hallucinate extra args (e.g. "limit") that cause
51+
// validation failures and retry loops. We strip and log rather than reject.
52+
const strippedArgs: string[] = [];
53+
if (
54+
schema.additionalProperties === false &&
55+
schema.properties &&
56+
typeof data === 'object' &&
57+
data !== null
58+
) {
59+
const allowed = new Set(Object.keys(schema.properties));
60+
for (const key of Object.keys(data)) {
61+
if (!allowed.has(key)) {
62+
strippedArgs.push(key);
63+
delete data[key];
64+
}
65+
}
66+
if (strippedArgs.length > 0) {
67+
logger.warn("Stripped unknown properties from tool args", {
68+
package_id: context?.package_id,
69+
tool_id: context?.tool_id,
70+
stripped: strippedArgs,
71+
});
72+
}
73+
}
74+
4675
// Compile schema with better error handling for format issues
4776
let validate;
4877
try {
@@ -83,6 +112,8 @@ export class Validator {
83112
package_id: context?.package_id,
84113
tool_id: context?.tool_id,
85114
});
115+
116+
return strippedArgs;
86117
}
87118
}
88119

0 commit comments

Comments
 (0)