fix(exec): return plain-text tool result on failure instead of raw JSON

When an exec command fails (e.g. timeout), the tool previously rejected
with an Error, which the tool adapter caught and wrapped in a JSON object
({ status, tool, error }). The model then received this raw JSON as the
tool result and could parrot it verbatim to the user.

Now exec failures resolve with a proper tool result containing the error
as human-readable text in content[], matching the success path structure.
The model sees plain text it can naturally incorporate into its reply.

Also fixes a pre-existing format issue in update-cli.test.ts.

Fixes #52484

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Martin Garramon
2026-03-22 20:05:11 -03:00
committed by Peter Steinberger
parent 339a67262d
commit 22c75a55b0
2 changed files with 22 additions and 5 deletions

View File

@@ -598,7 +598,22 @@ export function createExecTool(
return;
}
if (outcome.status === "failed") {
reject(new Error(outcome.reason ?? "Command failed."));
const failText = outcome.reason ?? "Command failed.";
resolve({
content: [
{
type: "text",
text: `${getWarningText()}${failText}`,
},
],
details: {
status: "failed",
exitCode: outcome.exitCode ?? null,
durationMs: outcome.durationMs,
aggregated: outcome.aggregated,
cwd: run.session.cwd,
},
});
return;
}
resolve({

View File

@@ -461,10 +461,12 @@ describe("exec tool backgrounding", () => {
backgroundMs: 10,
allowBackground: false,
});
await expect(executeExecCommand(customBash, longDelayCmd)).rejects.toThrow(/timed out/i);
await expect(executeExecCommand(customBash, longDelayCmd)).rejects.toThrow(
/re-run with a higher timeout/i,
);
const result = await executeExecCommand(customBash, longDelayCmd);
const text = (result as { content: { text: string }[] }).content[0].text;
expect(text).toMatch(/timed out/i);
expect(text).toMatch(/re-run with a higher timeout/i);
const details = (result as { details: { status: string } }).details;
expect(details.status).toBe("failed");
});
it.each<DisallowedElevationCase>(DISALLOWED_ELEVATION_CASES)(