mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
fix(core): handle AbortError when ESC cancels tool execution (#20863)
This commit is contained in:
@@ -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 () => {
|
it('should return cancelled result when signal is aborted', async () => {
|
||||||
const mockTool = new MockTool({
|
const mockTool = new MockTool({
|
||||||
name: 'slowTool',
|
name: 'slowTool',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type AgentLoopContext,
|
type AgentLoopContext,
|
||||||
type ToolLiveOutput,
|
type ToolLiveOutput,
|
||||||
} from '../index.js';
|
} from '../index.js';
|
||||||
|
import { isAbortError } from '../utils/errors.js';
|
||||||
import { SHELL_TOOL_NAME } from '../tools/tool-names.js';
|
import { SHELL_TOOL_NAME } from '../tools/tool-names.js';
|
||||||
import { ShellToolInvocation } from '../tools/shell.js';
|
import { ShellToolInvocation } from '../tools/shell.js';
|
||||||
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
|
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
|
||||||
@@ -159,15 +160,17 @@ export class ToolExecutor {
|
|||||||
}
|
}
|
||||||
} catch (executionError: unknown) {
|
} catch (executionError: unknown) {
|
||||||
spanMetadata.error = executionError;
|
spanMetadata.error = executionError;
|
||||||
const isAbortError =
|
const abortedByError =
|
||||||
executionError instanceof Error &&
|
isAbortError(executionError) ||
|
||||||
(executionError.name === 'AbortError' ||
|
(executionError instanceof Error &&
|
||||||
executionError.message.includes('Operation cancelled by user'));
|
executionError.message.includes('Operation cancelled by user'));
|
||||||
|
|
||||||
if (signal.aborted || isAbortError) {
|
if (signal.aborted || abortedByError) {
|
||||||
completedToolCall = await this.createCancelledResult(
|
completedToolCall = await this.createCancelledResult(
|
||||||
call,
|
call,
|
||||||
'User cancelled tool execution.',
|
isAbortError(executionError)
|
||||||
|
? 'Operation cancelled.'
|
||||||
|
: 'User cancelled tool execution.',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const error =
|
const error =
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { ToolErrorType } from './tool-error.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 { type Config } from '../config/config.js';
|
||||||
import { getResponseText } from '../utils/partUtils.js';
|
import { getResponseText } from '../utils/partUtils.js';
|
||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
@@ -175,6 +175,12 @@ class WebSearchToolInvocation extends BaseToolInvocation<
|
|||||||
sources,
|
sources,
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
if (isAbortError(error)) {
|
||||||
|
return {
|
||||||
|
llmContent: 'Web search was cancelled.',
|
||||||
|
returnDisplay: 'Search cancelled.',
|
||||||
|
};
|
||||||
|
}
|
||||||
const errorMessage = `Error during web search for query "${
|
const errorMessage = `Error during web search for query "${
|
||||||
this.params.query
|
this.params.query
|
||||||
}": ${getErrorMessage(error)}`;
|
}": ${getErrorMessage(error)}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user