fix: tool use permission hint

This commit is contained in:
mingholy.lmh
2025-11-04 14:01:33 +08:00
parent 22a7e26e1f
commit 14ad26f27e
6 changed files with 366 additions and 101 deletions

View File

@@ -291,90 +291,74 @@ describe('errors', () => {
).mockReturnValue(OutputFormat.JSON);
});
it('should format error as JSON and exit with default code', () => {
expect(() => {
handleToolError(toolName, toolError, mockConfig);
}).toThrow('process.exit called with code: 54');
it('should log error message to stderr and not exit', () => {
handleToolError(toolName, toolError, mockConfig);
// In JSON mode, should not exit (just log to stderr)
expect(consoleErrorSpy).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
type: 'FatalToolExecutionError',
message: 'Error executing tool test-tool: Tool failed',
code: 54,
},
},
null,
2,
),
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should use custom error code', () => {
expect(() => {
handleToolError(toolName, toolError, mockConfig, 'CUSTOM_TOOL_ERROR');
}).toThrow('process.exit called with code: 54');
it('should log error with custom error code and not exit', () => {
handleToolError(toolName, toolError, mockConfig, 'CUSTOM_TOOL_ERROR');
// In JSON mode, should not exit (just log to stderr)
expect(consoleErrorSpy).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
type: 'FatalToolExecutionError',
message: 'Error executing tool test-tool: Tool failed',
code: 'CUSTOM_TOOL_ERROR',
},
},
null,
2,
),
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should use numeric error code and exit with that code', () => {
expect(() => {
handleToolError(toolName, toolError, mockConfig, 500);
}).toThrow('process.exit called with code: 500');
it('should log error with numeric error code and not exit', () => {
handleToolError(toolName, toolError, mockConfig, 500);
// In JSON mode, should not exit (just log to stderr)
expect(consoleErrorSpy).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
type: 'FatalToolExecutionError',
message: 'Error executing tool test-tool: Tool failed',
code: 500,
},
},
null,
2,
),
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should prefer resultDisplay over error message', () => {
expect(() => {
handleToolError(
toolName,
toolError,
mockConfig,
'DISPLAY_ERROR',
'Display message',
);
}).toThrow('process.exit called with code: 54');
expect(consoleErrorSpy).toHaveBeenCalledWith(
JSON.stringify(
{
error: {
type: 'FatalToolExecutionError',
message: 'Error executing tool test-tool: Display message',
code: 'DISPLAY_ERROR',
},
},
null,
2,
),
handleToolError(
toolName,
toolError,
mockConfig,
'DISPLAY_ERROR',
'Display message',
);
// In JSON mode, should not exit (just log to stderr)
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Display message',
);
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should not exit in JSON mode', () => {
handleToolError(toolName, toolError, mockConfig);
// Should not throw (no exit)
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should not exit in STREAM_JSON mode', () => {
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.STREAM_JSON);
handleToolError(toolName, toolError, mockConfig);
// Should not exit in STREAM_JSON mode
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error executing tool test-tool: Tool failed',
);
expect(processExitSpy).not.toHaveBeenCalled();
});
});
});

View File

@@ -10,7 +10,6 @@ import {
JsonFormatter,
parseAndFormatApiError,
FatalTurnLimitedError,
FatalToolExecutionError,
FatalCancellationError,
} from '@qwen-code/qwen-code-core';
@@ -88,32 +87,29 @@ export function handleError(
/**
* Handles tool execution errors specifically.
* In JSON mode, outputs formatted JSON error and exits.
* In JSON/STREAM_JSON mode, outputs error message to stderr only and does not exit.
* The error will be properly formatted in the tool_result block by the adapter,
* allowing the session to continue so the LLM can decide what to do next.
* In text mode, outputs error message to stderr only.
*
* @param toolName - Name of the tool that failed
* @param toolError - The error that occurred during tool execution
* @param config - Configuration object
* @param errorCode - Optional error code
* @param resultDisplay - Optional display message for the error
*/
export function handleToolError(
toolName: string,
toolError: Error,
config: Config,
errorCode?: string | number,
_errorCode?: string | number,
resultDisplay?: string,
): void {
const errorMessage = `Error executing tool ${toolName}: ${resultDisplay || toolError.message}`;
const toolExecutionError = new FatalToolExecutionError(errorMessage);
if (config.getOutputFormat() === OutputFormat.JSON) {
const formatter = new JsonFormatter();
const formattedError = formatter.formatError(
toolExecutionError,
errorCode ?? toolExecutionError.exitCode,
// Always just log to stderr; JSON/streaming formatting happens in the tool_result block elsewhere
if (config.getDebugMode()) {
console.error(
`Error executing tool ${toolName}: ${resultDisplay || toolError.message}`,
);
console.error(formattedError);
process.exit(
typeof errorCode === 'number' ? errorCode : toolExecutionError.exitCode,
);
} else {
console.error(errorMessage);
}
}