refactor(core): adopt CoreToolCallStatus enum for type safety (#18998)

This commit is contained in:
Jerop Kipruto
2026-02-13 11:27:20 -05:00
committed by GitHub
parent d0c6a56c65
commit 60be42f095
22 changed files with 631 additions and 431 deletions

View File

@@ -50,6 +50,7 @@ import {
coreEvents,
applyAdminAllowlist,
getAdminBlockedMcpServersMessage,
CoreToolCallStatus,
} from '@google/gemini-cli-core';
import { maybeRequestConsentOrFail } from './extensions/consent.js';
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
@@ -383,7 +384,7 @@ Would you like to attempt to install via "git clone" instead?`,
newExtensionConfig.version,
previousExtensionConfig.version,
installMetadata.type,
'success',
CoreToolCallStatus.Success,
),
);
} else {
@@ -395,7 +396,7 @@ Would you like to attempt to install via "git clone" instead?`,
getExtensionId(newExtensionConfig, installMetadata),
newExtensionConfig.version,
installMetadata.type,
'success',
CoreToolCallStatus.Success,
),
);
await this.enableExtension(
@@ -433,7 +434,7 @@ Would you like to attempt to install via "git clone" instead?`,
newExtensionConfig?.version ?? '',
previousExtensionConfig.version,
installMetadata.type,
'error',
CoreToolCallStatus.Error,
),
);
} else {
@@ -445,7 +446,7 @@ Would you like to attempt to install via "git clone" instead?`,
extensionId ?? '',
newExtensionConfig?.version ?? '',
installMetadata.type,
'error',
CoreToolCallStatus.Error,
),
);
}
@@ -491,7 +492,7 @@ Would you like to attempt to install via "git clone" instead?`,
extension.name,
hashValue(extension.name),
extension.id,
'success',
CoreToolCallStatus.Success,
),
);
}

View File

@@ -18,6 +18,7 @@ import {
type ExecutingToolCall,
type WaitingToolCall,
type CancelledToolCall,
CoreToolCallStatus,
} from '@google/gemini-cli-core';
import { ToolCallStatus } from '../types.js';
@@ -28,13 +29,13 @@ describe('toolMapping', () => {
describe('mapCoreStatusToDisplayStatus', () => {
it.each([
['validating', ToolCallStatus.Pending],
['awaiting_approval', ToolCallStatus.Confirming],
['executing', ToolCallStatus.Executing],
['success', ToolCallStatus.Success],
['cancelled', ToolCallStatus.Canceled],
['error', ToolCallStatus.Error],
['scheduled', ToolCallStatus.Pending],
[CoreToolCallStatus.Validating, ToolCallStatus.Pending],
[CoreToolCallStatus.AwaitingApproval, ToolCallStatus.Confirming],
[CoreToolCallStatus.Executing, ToolCallStatus.Executing],
[CoreToolCallStatus.Success, ToolCallStatus.Success],
[CoreToolCallStatus.Cancelled, ToolCallStatus.Canceled],
[CoreToolCallStatus.Error, ToolCallStatus.Error],
[CoreToolCallStatus.Scheduled, ToolCallStatus.Pending],
] as const)('maps %s to %s', (coreStatus, expectedDisplayStatus) => {
expect(mapCoreStatusToDisplayStatus(coreStatus)).toBe(
expectedDisplayStatus,
@@ -77,7 +78,7 @@ describe('toolMapping', () => {
it('handles a single tool call input', () => {
const toolCall: ScheduledToolCall = {
status: 'scheduled',
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -91,13 +92,13 @@ describe('toolMapping', () => {
it('handles an array of tool calls', () => {
const toolCall1: ScheduledToolCall = {
status: 'scheduled',
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
};
const toolCall2: ScheduledToolCall = {
status: 'scheduled',
status: CoreToolCallStatus.Scheduled,
request: { ...mockRequest, callId: 'call-2' },
tool: mockTool,
invocation: mockInvocation,
@@ -111,7 +112,7 @@ describe('toolMapping', () => {
it('maps successful tool call properties correctly', () => {
const toolCall: SuccessfulToolCall = {
status: 'success',
status: CoreToolCallStatus.Success,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -139,7 +140,7 @@ describe('toolMapping', () => {
it('maps executing tool call properties correctly with live output and ptyId', () => {
const toolCall: ExecutingToolCall = {
status: 'executing',
status: CoreToolCallStatus.Executing,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -166,7 +167,7 @@ describe('toolMapping', () => {
};
const toolCall: WaitingToolCall = {
status: 'awaiting_approval',
status: CoreToolCallStatus.AwaitingApproval,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -193,7 +194,7 @@ describe('toolMapping', () => {
};
const toolCall: WaitingToolCall = {
status: 'awaiting_approval',
status: CoreToolCallStatus.AwaitingApproval,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -211,7 +212,7 @@ describe('toolMapping', () => {
it('maps error tool call missing tool definition', () => {
// e.g. "TOOL_NOT_REGISTERED" errors
const toolCall: ToolCall = {
status: 'error',
status: CoreToolCallStatus.Error,
request: mockRequest, // name: 'test_tool'
response: { ...mockResponse, resultDisplay: 'Tool not found' },
// notice: no `tool` or `invocation` defined here
@@ -229,7 +230,7 @@ describe('toolMapping', () => {
it('maps cancelled tool call properties correctly', () => {
const toolCall: CancelledToolCall = {
status: 'cancelled',
status: CoreToolCallStatus.Cancelled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -248,7 +249,7 @@ describe('toolMapping', () => {
it('propagates borderTop and borderBottom options correctly', () => {
const toolCall: ScheduledToolCall = {
status: 'scheduled',
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,
@@ -264,7 +265,7 @@ describe('toolMapping', () => {
it('sets resultDisplay to undefined for pre-execution statuses', () => {
const toolCall: ScheduledToolCall = {
status: 'scheduled',
status: CoreToolCallStatus.Scheduled,
request: mockRequest,
tool: mockTool,
invocation: mockInvocation,

View File

@@ -10,6 +10,8 @@ import {
type SerializableConfirmationDetails,
type ToolResultDisplay,
debugLogger,
CoreToolCallStatus,
checkExhaustive,
} from '@google/gemini-cli-core';
import {
ToolCallStatus,
@@ -17,25 +19,23 @@ import {
type IndividualToolCallDisplay,
} from '../types.js';
import { checkExhaustive } from '@google/gemini-cli-core';
export function mapCoreStatusToDisplayStatus(
coreStatus: CoreStatus,
): ToolCallStatus {
switch (coreStatus) {
case 'validating':
case CoreToolCallStatus.Validating:
return ToolCallStatus.Pending;
case 'awaiting_approval':
case CoreToolCallStatus.AwaitingApproval:
return ToolCallStatus.Confirming;
case 'executing':
case CoreToolCallStatus.Executing:
return ToolCallStatus.Executing;
case 'success':
case CoreToolCallStatus.Success:
return ToolCallStatus.Success;
case 'cancelled':
case CoreToolCallStatus.Cancelled:
return ToolCallStatus.Canceled;
case 'error':
case CoreToolCallStatus.Error:
return ToolCallStatus.Error;
case 'scheduled':
case CoreToolCallStatus.Scheduled:
return ToolCallStatus.Pending;
default:
return checkExhaustive(coreStatus);
@@ -60,7 +60,7 @@ export function mapToDisplay(
const displayName = call.tool?.displayName ?? call.request.name;
if (call.status === 'error') {
if (call.status === CoreToolCallStatus.Error) {
description = JSON.stringify(call.request.args);
} else {
description = call.invocation.getDescription();
@@ -82,25 +82,25 @@ export function mapToDisplay(
let correlationId: string | undefined = undefined;
switch (call.status) {
case 'success':
case CoreToolCallStatus.Success:
resultDisplay = call.response.resultDisplay;
outputFile = call.response.outputFile;
break;
case 'error':
case 'cancelled':
case CoreToolCallStatus.Error:
case CoreToolCallStatus.Cancelled:
resultDisplay = call.response.resultDisplay;
break;
case 'awaiting_approval':
case CoreToolCallStatus.AwaitingApproval:
correlationId = call.correlationId;
// Pass through details. Context handles dispatch (callback vs bus).
confirmationDetails = call.confirmationDetails;
break;
case 'executing':
case CoreToolCallStatus.Executing:
resultDisplay = call.liveOutput;
ptyId = call.pid;
break;
case 'scheduled':
case 'validating':
case CoreToolCallStatus.Scheduled:
case CoreToolCallStatus.Validating:
break;
default: {
const exhaustiveCheck: never = call;

View File

@@ -27,6 +27,7 @@ import type {
AnyToolInvocation,
} from '@google/gemini-cli-core';
import {
CoreToolCallStatus,
ApprovalMode,
AuthType,
GeminiEventType as ServerGeminiEventType,
@@ -343,14 +344,14 @@ describe('useGeminiStream', () => {
mockCancelAllToolCalls(...args);
lastToolCalls = lastToolCalls.map((tc) => {
if (
tc.status === 'awaiting_approval' ||
tc.status === 'executing' ||
tc.status === 'scheduled' ||
tc.status === 'validating'
tc.status === CoreToolCallStatus.AwaitingApproval ||
tc.status === CoreToolCallStatus.Executing ||
tc.status === CoreToolCallStatus.Scheduled ||
tc.status === CoreToolCallStatus.Validating
) {
return {
...tc,
status: 'cancelled',
status: CoreToolCallStatus.Cancelled,
response: {
callId: tc.request.callId,
responseParts: [],
@@ -406,7 +407,8 @@ describe('useGeminiStream', () => {
toolName: string,
callId: string,
confirmationType: 'edit' | 'info',
status: TrackedToolCall['status'] = 'awaiting_approval',
status: TrackedToolCall['status'] = CoreToolCallStatus.AwaitingApproval,
mockOnConfirm: Mock = vi.fn(),
): TrackedWaitingToolCall => ({
request: {
callId,
@@ -415,7 +417,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
status: status as 'awaiting_approval',
status: status as CoreToolCallStatus.AwaitingApproval,
responseSubmittedToGemini: false,
confirmationDetails:
confirmationType === 'edit'
@@ -427,11 +429,13 @@ describe('useGeminiStream', () => {
fileDiff: 'fake diff',
originalContent: 'old',
newContent: 'new',
onConfirm: mockOnConfirm,
}
: {
type: 'info',
title: `${toolName} confirmation`,
prompt: `Execute ${toolName}?`,
onConfirm: mockOnConfirm,
},
tool: {
name: toolName,
@@ -500,7 +504,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
status: 'success',
status: CoreToolCallStatus.Success,
responseSubmittedToGemini: false,
response: {
callId: 'call1',
@@ -528,7 +532,7 @@ describe('useGeminiStream', () => {
args: {},
prompt_id: 'prompt-id-1',
},
status: 'executing',
status: CoreToolCallStatus.Executing,
responseSubmittedToGemini: false,
tool: {
name: 'tool2',
@@ -566,7 +570,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-2',
},
status: 'success',
status: CoreToolCallStatus.Success,
responseSubmittedToGemini: false,
response: {
callId: 'call1',
@@ -588,7 +592,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-2',
},
status: 'error',
status: CoreToolCallStatus.Error,
responseSubmittedToGemini: false,
response: {
callId: 'call2',
@@ -675,10 +679,10 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-3',
},
status: 'cancelled',
status: CoreToolCallStatus.Cancelled,
response: {
callId: '1',
responseParts: [{ text: 'cancelled' }],
responseParts: [{ text: CoreToolCallStatus.Cancelled }],
errorType: undefined, // FIX: Added missing property
},
responseSubmittedToGemini: false,
@@ -744,7 +748,7 @@ describe('useGeminiStream', () => {
expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith(['1']);
expect(client.addHistory).toHaveBeenCalledWith({
role: 'user',
parts: [{ text: 'cancelled' }],
parts: [{ text: CoreToolCallStatus.Cancelled }],
});
// Ensure we do NOT call back to the API
expect(mockSendMessageStream).not.toHaveBeenCalled();
@@ -761,7 +765,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-stop',
},
status: 'error',
status: CoreToolCallStatus.Error,
response: {
callId: 'stop-call',
responseParts: [{ text: 'error occurred' }],
@@ -825,7 +829,7 @@ describe('useGeminiStream', () => {
invocation: {
getDescription: () => `Mock description`,
} as unknown as AnyToolInvocation,
status: 'cancelled',
status: CoreToolCallStatus.Cancelled,
response: {
callId: 'cancel-1',
responseParts: [
@@ -854,7 +858,7 @@ describe('useGeminiStream', () => {
invocation: {
getDescription: () => `Mock description`,
} as unknown as AnyToolInvocation,
status: 'cancelled',
status: CoreToolCallStatus.Cancelled,
response: {
callId: 'cancel-2',
responseParts: [
@@ -954,7 +958,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-4',
},
status: 'executing',
status: CoreToolCallStatus.Executing,
responseSubmittedToGemini: false,
tool: {
name: 'tool1',
@@ -972,7 +976,7 @@ describe('useGeminiStream', () => {
const completedToolCalls: TrackedToolCall[] = [
{
...(initialToolCalls[0] as TrackedExecutingToolCall),
status: 'success',
status: CoreToolCallStatus.Success,
response: {
callId: 'call1',
responseParts: toolCallResponseParts,
@@ -1278,7 +1282,7 @@ describe('useGeminiStream', () => {
const toolCalls: TrackedToolCall[] = [
{
request: { callId: 'call1', name: 'tool1', args: {} },
status: 'executing',
status: CoreToolCallStatus.Executing,
responseSubmittedToGemini: false,
tool: {
name: 'tool1',
@@ -1318,7 +1322,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
status: 'awaiting_approval',
status: CoreToolCallStatus.AwaitingApproval,
responseSubmittedToGemini: false,
tool: {
name: 'some_tool',
@@ -1630,7 +1634,7 @@ describe('useGeminiStream', () => {
isClientInitiated: true,
prompt_id: 'prompt-id-6',
},
status: 'success',
status: CoreToolCallStatus.Success,
responseSubmittedToGemini: false,
response: {
callId: 'save-mem-call-1',
@@ -1875,7 +1879,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
status: 'awaiting_approval',
status: CoreToolCallStatus.AwaitingApproval,
responseSubmittedToGemini: false,
// No confirmationDetails
tool: {
@@ -1900,8 +1904,15 @@ describe('useGeminiStream', () => {
});
it('should only process tool calls with awaiting_approval status', async () => {
const mockOnConfirmAwaiting = vi.fn().mockResolvedValue(undefined);
const mixedStatusToolCalls: TrackedToolCall[] = [
createMockToolCall('replace', 'call1', 'edit'),
createMockToolCall(
'replace',
'call1',
'edit',
CoreToolCallStatus.AwaitingApproval,
mockOnConfirmAwaiting,
),
{
request: {
callId: 'call2',
@@ -1910,7 +1921,7 @@ describe('useGeminiStream', () => {
isClientInitiated: false,
prompt_id: 'prompt-id-1',
},
status: 'executing',
status: CoreToolCallStatus.Executing,
responseSubmittedToGemini: false,
tool: {
name: 'write_file',
@@ -2206,7 +2217,7 @@ describe('useGeminiStream', () => {
// were added to history during the await scheduleToolCalls(...) block.
const tools = requests.map((r: any) => ({
request: r,
status: 'success',
status: CoreToolCallStatus.Success,
tool: { displayName: r.name, name: r.name },
invocation: { getDescription: () => 'desc' },
response: { responseParts: [], resultDisplay: 'done' },
@@ -2681,7 +2692,7 @@ describe('useGeminiStream', () => {
const newToolCalls: TrackedToolCall[] = [
{
request: { callId: 'call1', name: 'tool1', args: {} },
status: 'executing',
status: CoreToolCallStatus.Executing,
tool: {
name: 'tool1',
displayName: 'tool1',
@@ -2809,7 +2820,7 @@ describe('useGeminiStream', () => {
await waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
type: CoreToolCallStatus.Error,
}),
expect.any(Number),
);

View File

@@ -13,6 +13,7 @@ import type {
ConversationRecord,
} from '@google/gemini-cli-core';
import {
CoreToolCallStatus,
AuthType,
logToolCall,
convertToFunctionResponse,
@@ -451,7 +452,10 @@ export class Session {
await this.sendUpdate({
sessionUpdate: 'tool_call',
toolCallId: toolCall.id,
status: toolCall.status === 'success' ? 'completed' : 'failed',
status:
toolCall.status === CoreToolCallStatus.Success
? 'completed'
: 'failed',
title: toolCall.displayName || toolCall.name,
content: toolCallContent,
kind: tool ? toAcpToolKind(tool.kind) : 'other',
@@ -477,7 +481,7 @@ export class Session {
while (nextMessage !== null) {
if (pendingSend.signal.aborted) {
chat.addHistory(nextMessage);
return { stopReason: 'cancelled' };
return { stopReason: CoreToolCallStatus.Cancelled };
}
const functionCalls: FunctionCall[] = [];
@@ -494,7 +498,7 @@ export class Session {
for await (const resp of responseStream) {
if (pendingSend.signal.aborted) {
return { stopReason: 'cancelled' };
return { stopReason: CoreToolCallStatus.Cancelled };
}
if (
@@ -529,7 +533,7 @@ export class Session {
}
if (pendingSend.signal.aborted) {
return { stopReason: 'cancelled' };
return { stopReason: CoreToolCallStatus.Cancelled };
}
} catch (error) {
if (getErrorStatus(error) === 429) {
@@ -543,7 +547,7 @@ export class Session {
pendingSend.signal.aborted ||
(error instanceof Error && error.name === 'AbortError')
) {
return { stopReason: 'cancelled' };
return { stopReason: CoreToolCallStatus.Cancelled };
}
throw new acp.RequestError(
@@ -663,7 +667,7 @@ export class Session {
const output = await this.connection.requestPermission(params);
const outcome =
output.outcome.outcome === 'cancelled'
output.outcome.outcome === CoreToolCallStatus.Cancelled
? ToolConfirmationOutcome.Cancel
: z
.nativeEnum(ToolConfirmationOutcome)
@@ -728,7 +732,7 @@ export class Session {
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
{
status: 'success',
status: CoreToolCallStatus.Success,
request: {
callId,
name: fc.name,
@@ -773,7 +777,7 @@ export class Session {
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
{
status: 'error',
status: CoreToolCallStatus.Error,
request: {
callId,
name: fc.name,