Agents: clarify invalid required-param hints

This commit is contained in:
Josh Lehman 2026-03-30 07:14:16 -07:00
parent ed5ff7f84f
commit c1cf0691c9
No known key found for this signature in database
GPG Key ID: D141B425AC7F876B
3 changed files with 72 additions and 4 deletions

View File

@ -4,6 +4,10 @@ Docs: https://docs.openclaw.ai
## Unreleased
### Fixes
- Agents/tools: include value-shape hints in missing-parameter tool errors so dropped, empty-string, and wrong-type write payloads are easier to diagnose from logs. (#55317) Thanks @priyansh19.
## 2026.4.2-beta.1
### Breaking

View File

@ -47,6 +47,40 @@ describe("assertRequiredParams", () => {
).toThrow(/\(received: file_path\)[^,]/);
});
it("shows empty-string values for present params that still fail validation", () => {
expect(() =>
assertRequiredParams(
{ path: "/tmp/a.txt", content: " " },
[
{ keys: ["path", "file_path"], label: "path alias" },
{ keys: ["content"], label: "content" },
],
"write",
),
).toThrow(/\(received: path, content=<empty-string>\)/);
});
it("shows wrong-type values for present params that still fail validation", async () => {
const tool = wrapToolParamNormalization(
{
name: "write",
label: "write",
description: "write a file",
parameters: {},
execute: vi.fn(),
},
CLAUDE_PARAM_GROUPS.write,
);
await expect(
tool.execute(
"id",
{ file_path: "test.txt", content: { unexpected: true } },
new AbortController().signal,
vi.fn(),
),
).rejects.toThrow(/\(received: (?:path, content=<object>|content=<object>, path)\)/);
});
it("includes multiple received keys when several params are present", () => {
expect(() =>
assertRequiredParams(

View File

@ -13,6 +13,39 @@ function parameterValidationError(message: string): Error {
return new Error(`${message}.${RETRY_GUIDANCE_SUFFIX}`);
}
function describeReceivedParamValue(value: unknown, allowEmpty = false): string | undefined {
if (value === undefined || value === null) {
return undefined;
}
if (typeof value === "string") {
if (allowEmpty || value.trim().length > 0) {
return undefined;
}
return "<empty-string>";
}
if (Array.isArray(value)) {
return "<array>";
}
return `<${typeof value}>`;
}
function formatReceivedParamHint(
record: Record<string, unknown>,
groups: readonly RequiredParamGroup[],
): string {
const allowEmptyKeys = new Set(
groups.filter((group) => group.allowEmpty).flatMap((group) => group.keys),
);
const received = Object.keys(record).flatMap((key) => {
const detail = describeReceivedParamValue(record[key], allowEmptyKeys.has(key));
if (record[key] === undefined || record[key] === null) {
return [];
}
return [detail ? `${key}=${detail}` : key];
});
return received.length > 0 ? ` (received: ${received.join(", ")})` : "";
}
export const CLAUDE_PARAM_GROUPS = {
read: [{ keys: ["path", "file_path", "filePath", "file"], label: "path alias" }],
write: [
@ -275,10 +308,7 @@ export function assertRequiredParams(
if (missingLabels.length > 0) {
const joined = missingLabels.join(", ");
const noun = missingLabels.length === 1 ? "parameter" : "parameters";
const receivedKeys = Object.keys(record).filter(
(k) => record[k] !== undefined && record[k] !== null,
);
const receivedHint = receivedKeys.length > 0 ? ` (received: ${receivedKeys.join(", ")})` : "";
const receivedHint = formatReceivedParamHint(record, groups);
throw parameterValidationError(`Missing required ${noun}: ${joined}${receivedHint}`);
}
}