fix(agents): mark required-param tool errors as non-retryable (#17533)

* Agents: mark missing tool params as non-retryable

* Agents: include all missing required params in tool errors

* Agents: change required-param errors to retry guidance

* Docs: align changelog text for issue #14729 guidance wording
This commit is contained in:
Tak Hoffman
2026-02-15 15:50:44 -06:00
committed by GitHub
parent 50abdaf33b
commit 0c77851516
3 changed files with 32 additions and 3 deletions

View File

@@ -93,6 +93,7 @@ Docs: https://docs.openclaw.ai
- Gateway/Sessions: abort active embedded runs and clear queued session work before `sessions.reset`, returning unavailable if the run does not stop in time. (#16576) Thanks @Grynn.
- Sessions/Agents: harden transcript path resolution for mismatched agent context by preserving explicit store roots and adding safe absolute-path fallback to the correct agent sessions directory. (#16288) Thanks @robbyczgw-cla.
- Agents: add a safety timeout around embedded `session.compact()` to ensure stalled compaction runs settle and release blocked session lanes. (#16331) Thanks @BinHPdev.
- Agents/Tools: make required-parameter validation errors list missing fields and instruct: "Supply correct parameters before retrying," reducing repeated invalid tool-call loops (for example `read({})`). (#14729)
- Agents: keep unresolved mutating tool failures visible until the same action retry succeeds, scope mutation-error surfacing to mutating calls (including `session_status` model changes), and dedupe duplicate failure warnings in outbound replies. (#16131) Thanks @Swader.
- Agents/Process/Bootstrap: preserve unbounded `process log` offset-only pagination (default tail applies only when both `offset` and `limit` are omitted) and enforce strict `bootstrapTotalMaxChars` budgeting across injected bootstrap content (including markers), skipping additional injection when remaining budget is too small. (#16539) Thanks @CharlieGreenman.
- Agents/Workspace: persist bootstrap onboarding state so partially initialized workspaces recover missing `BOOTSTRAP.md` once, while completed onboarding keeps BOOTSTRAP deleted even if runtime files are later recreated. Thanks @gumadeiras.

View File

@@ -102,7 +102,10 @@ describe("createOpenClawCodingTools", () => {
execute,
};
const wrapped = __testing.wrapToolParamNormalization(tool, [{ keys: ["path", "file_path"] }]);
const wrapped = __testing.wrapToolParamNormalization(tool, [
{ keys: ["path", "file_path"], label: "path (path or file_path)" },
{ keys: ["content"], label: "content" },
]);
await wrapped.execute("tool-1", { file_path: "foo.txt", content: "x" });
expect(execute).toHaveBeenCalledWith(
@@ -115,9 +118,21 @@ describe("createOpenClawCodingTools", () => {
await expect(wrapped.execute("tool-2", { content: "x" })).rejects.toThrow(
/Missing required parameter/,
);
await expect(wrapped.execute("tool-2", { content: "x" })).rejects.toThrow(
/Supply correct parameters before retrying\./,
);
await expect(wrapped.execute("tool-3", { file_path: " ", content: "x" })).rejects.toThrow(
/Missing required parameter/,
);
await expect(wrapped.execute("tool-3", { file_path: " ", content: "x" })).rejects.toThrow(
/Supply correct parameters before retrying\./,
);
await expect(wrapped.execute("tool-4", {})).rejects.toThrow(
/Missing required parameters: path \(path or file_path\), content/,
);
await expect(wrapped.execute("tool-4", {})).rejects.toThrow(
/Supply correct parameters before retrying\./,
);
});
});

View File

@@ -87,6 +87,12 @@ type RequiredParamGroup = {
label?: string;
};
const RETRY_GUIDANCE_SUFFIX = " Supply correct parameters before retrying.";
function parameterValidationError(message: string): Error {
return new Error(`${message}.${RETRY_GUIDANCE_SUFFIX}`);
}
export const CLAUDE_PARAM_GROUPS = {
read: [{ keys: ["path", "file_path"], label: "path (path or file_path)" }],
write: [
@@ -245,9 +251,10 @@ export function assertRequiredParams(
toolName: string,
): void {
if (!record || typeof record !== "object") {
throw new Error(`Missing parameters for ${toolName}`);
throw parameterValidationError(`Missing parameters for ${toolName}`);
}
const missingLabels: string[] = [];
for (const group of groups) {
const satisfied = group.keys.some((key) => {
if (!(key in record)) {
@@ -265,9 +272,15 @@ export function assertRequiredParams(
if (!satisfied) {
const label = group.label ?? group.keys.join(" or ");
throw new Error(`Missing required parameter: ${label}`);
missingLabels.push(label);
}
}
if (missingLabels.length > 0) {
const joined = missingLabels.join(", ");
const noun = missingLabels.length === 1 ? "parameter" : "parameters";
throw parameterValidationError(`Missing required ${noun}: ${joined}`);
}
}
// Generic wrapper to normalize parameters for any tool