refactor: centralize tool-error visibility policy

This commit is contained in:
Peter Steinberger
2026-02-22 15:30:42 +01:00
parent ac3ac6a83a
commit 4c355a28a3
4 changed files with 91 additions and 67 deletions

View File

@@ -28,6 +28,10 @@ type LastToolError = {
mutatingAction?: boolean;
actionFingerprint?: string;
};
type ToolErrorWarningPolicy = {
showWarning: boolean;
includeDetails: boolean;
};
const RECOVERABLE_TOOL_ERROR_KEYWORDS = [
"required",
@@ -44,30 +48,37 @@ function isRecoverableToolError(error: string | undefined): boolean {
return RECOVERABLE_TOOL_ERROR_KEYWORDS.some((keyword) => errorLower.includes(keyword));
}
function shouldShowToolErrorWarning(params: {
function isVerboseToolDetailEnabled(level?: VerboseLevel): boolean {
return level === "on" || level === "full";
}
function resolveToolErrorWarningPolicy(params: {
lastToolError: LastToolError;
hasUserFacingReply: boolean;
suppressToolErrors: boolean;
suppressToolErrorWarnings?: boolean;
verboseLevel?: VerboseLevel;
}): boolean {
}): ToolErrorWarningPolicy {
const includeDetails = isVerboseToolDetailEnabled(params.verboseLevel);
if (params.suppressToolErrorWarnings) {
return false;
return { showWarning: false, includeDetails };
}
const normalizedToolName = params.lastToolError.toolName.trim().toLowerCase();
const verboseEnabled = params.verboseLevel === "on" || params.verboseLevel === "full";
if ((normalizedToolName === "exec" || normalizedToolName === "bash") && !verboseEnabled) {
return false;
if ((normalizedToolName === "exec" || normalizedToolName === "bash") && !includeDetails) {
return { showWarning: false, includeDetails };
}
const isMutatingToolError =
params.lastToolError.mutatingAction ?? isLikelyMutatingToolName(params.lastToolError.toolName);
if (isMutatingToolError) {
return true;
return { showWarning: true, includeDetails };
}
if (params.suppressToolErrors) {
return false;
return { showWarning: false, includeDetails };
}
return !params.hasUserFacingReply && !isRecoverableToolError(params.lastToolError.error);
return {
showWarning: !params.hasUserFacingReply && !isRecoverableToolError(params.lastToolError.error),
includeDetails,
};
}
export function buildEmbeddedRunPayloads(params: {
@@ -256,7 +267,7 @@ export function buildEmbeddedRunPayloads(params: {
}
if (params.lastToolError) {
const shouldShowToolError = shouldShowToolErrorWarning({
const warningPolicy = resolveToolErrorWarningPolicy({
lastToolError: params.lastToolError,
hasUserFacingReply: hasUserFacingAssistantReply,
suppressToolErrors: Boolean(params.config?.messages?.suppressToolErrors),
@@ -266,16 +277,14 @@ export function buildEmbeddedRunPayloads(params: {
// Always surface mutating tool failures so we do not silently confirm actions that did not happen.
// Otherwise, keep the previous behavior and only surface non-recoverable failures when no reply exists.
if (shouldShowToolError) {
if (warningPolicy.showWarning) {
const toolSummary = formatToolAggregate(
params.lastToolError.toolName,
params.lastToolError.meta ? [params.lastToolError.meta] : undefined,
{ markdown: useMarkdown },
);
const verboseErrorDetailsEnabled =
params.verboseLevel === "on" || params.verboseLevel === "full";
const errorSuffix =
verboseErrorDetailsEnabled && params.lastToolError.error
warningPolicy.includeDetails && params.lastToolError.error
? `: ${params.lastToolError.error}`
: "";
const warningText = `⚠️ ${toolSummary} failed${errorSuffix}`;