fix(core): handle AbortError when ESC cancels tool execution (#20863)

This commit is contained in:
Prasanna Pal
2026-03-10 22:41:08 +05:30
committed by GitHub
parent a220874281
commit 0b78de9601
3 changed files with 96 additions and 6 deletions

View File

@@ -211,6 +211,87 @@ describe('ToolExecutor', () => {
});
});
it('should return cancelled result when executeToolWithHooks rejects with AbortError', async () => {
const mockTool = new MockTool({
name: 'webSearchTool',
description: 'Mock web search',
});
const invocation = mockTool.build({});
const abortErr = new Error('The user aborted a request.');
abortErr.name = 'AbortError';
vi.mocked(coreToolHookTriggers.executeToolWithHooks).mockRejectedValue(
abortErr,
);
const scheduledCall: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
request: {
callId: 'call-abort',
name: 'webSearchTool',
args: {},
isClientInitiated: false,
prompt_id: 'prompt-abort',
},
tool: mockTool,
invocation: invocation as unknown as AnyToolInvocation,
startTime: Date.now(),
};
const result = await executor.execute({
call: scheduledCall,
signal: new AbortController().signal,
onUpdateToolCall: vi.fn(),
});
expect(result.status).toBe(CoreToolCallStatus.Cancelled);
if (result.status === CoreToolCallStatus.Cancelled) {
const response = result.response.responseParts[0]?.functionResponse
?.response as Record<string, unknown>;
expect(response['error']).toContain('Operation cancelled.');
}
});
it('should return cancelled result when executeToolWithHooks rejects with "Operation cancelled by user" message', async () => {
const mockTool = new MockTool({
name: 'someTool',
description: 'Mock',
});
const invocation = mockTool.build({});
const cancelErr = new Error('Operation cancelled by user');
vi.mocked(coreToolHookTriggers.executeToolWithHooks).mockRejectedValue(
cancelErr,
);
const scheduledCall: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
request: {
callId: 'call-cancel-msg',
name: 'someTool',
args: {},
isClientInitiated: false,
prompt_id: 'prompt-cancel-msg',
},
tool: mockTool,
invocation: invocation as unknown as AnyToolInvocation,
startTime: Date.now(),
};
const result = await executor.execute({
call: scheduledCall,
signal: new AbortController().signal,
onUpdateToolCall: vi.fn(),
});
expect(result.status).toBe(CoreToolCallStatus.Cancelled);
if (result.status === CoreToolCallStatus.Cancelled) {
const response = result.response.responseParts[0]?.functionResponse
?.response as Record<string, unknown>;
expect(response['error']).toContain('User cancelled tool execution.');
}
});
it('should return cancelled result when signal is aborted', async () => {
const mockTool = new MockTool({
name: 'slowTool',

View File

@@ -16,6 +16,7 @@ import {
type AgentLoopContext,
type ToolLiveOutput,
} from '../index.js';
import { isAbortError } from '../utils/errors.js';
import { SHELL_TOOL_NAME } from '../tools/tool-names.js';
import { ShellToolInvocation } from '../tools/shell.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
@@ -159,15 +160,17 @@ export class ToolExecutor {
}
} catch (executionError: unknown) {
spanMetadata.error = executionError;
const isAbortError =
executionError instanceof Error &&
(executionError.name === 'AbortError' ||
const abortedByError =
isAbortError(executionError) ||
(executionError instanceof Error &&
executionError.message.includes('Operation cancelled by user'));
if (signal.aborted || isAbortError) {
if (signal.aborted || abortedByError) {
completedToolCall = await this.createCancelledResult(
call,
'User cancelled tool execution.',
isAbortError(executionError)
? 'Operation cancelled.'
: 'User cancelled tool execution.',
);
} else {
const error =

View File

@@ -16,7 +16,7 @@ import {
} from './tools.js';
import { ToolErrorType } from './tool-error.js';
import { getErrorMessage } from '../utils/errors.js';
import { getErrorMessage, isAbortError } from '../utils/errors.js';
import { type Config } from '../config/config.js';
import { getResponseText } from '../utils/partUtils.js';
import { debugLogger } from '../utils/debugLogger.js';
@@ -175,6 +175,12 @@ class WebSearchToolInvocation extends BaseToolInvocation<
sources,
};
} catch (error: unknown) {
if (isAbortError(error)) {
return {
llmContent: 'Web search was cancelled.',
returnDisplay: 'Search cancelled.',
};
}
const errorMessage = `Error during web search for query "${
this.params.query
}": ${getErrorMessage(error)}`;