diff --git a/packages/core/src/scheduler/tool-executor.test.ts b/packages/core/src/scheduler/tool-executor.test.ts index a193c8ae69..bf5b683a4a 100644 --- a/packages/core/src/scheduler/tool-executor.test.ts +++ b/packages/core/src/scheduler/tool-executor.test.ts @@ -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; + 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; + expect(response['error']).toContain('User cancelled tool execution.'); + } + }); + it('should return cancelled result when signal is aborted', async () => { const mockTool = new MockTool({ name: 'slowTool', diff --git a/packages/core/src/scheduler/tool-executor.ts b/packages/core/src/scheduler/tool-executor.ts index e5491630d2..1ec89fe41d 100644 --- a/packages/core/src/scheduler/tool-executor.ts +++ b/packages/core/src/scheduler/tool-executor.ts @@ -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 = diff --git a/packages/core/src/tools/web-search.ts b/packages/core/src/tools/web-search.ts index 2756599b28..8898d8e9d9 100644 --- a/packages/core/src/tools/web-search.ts +++ b/packages/core/src/tools/web-search.ts @@ -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)}`;