mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
Hide AskUser tool validation errors from UI (agent self-corrects) (#18954)
This commit is contained in:
@@ -157,6 +157,7 @@ export * from './tools/read-many-files.js';
|
||||
export * from './tools/mcp-client.js';
|
||||
export * from './tools/mcp-tool.js';
|
||||
export * from './tools/write-todos.js';
|
||||
export * from './tools/ask-user.js';
|
||||
|
||||
// MCP OAuth
|
||||
export { MCPOAuthProvider } from './mcp/oauth-provider.js';
|
||||
|
||||
@@ -5,10 +5,97 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AskUserTool } from './ask-user.js';
|
||||
import {
|
||||
AskUserTool,
|
||||
shouldHideAskUserTool,
|
||||
isCompletedAskUserTool,
|
||||
} from './ask-user.js';
|
||||
import { QuestionType, type Question } from '../confirmation-bus/types.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import { ToolConfirmationOutcome } from './tools.js';
|
||||
import { ToolErrorType } from './tool-error.js';
|
||||
import { ASK_USER_DISPLAY_NAME } from './tool-names.js';
|
||||
|
||||
describe('AskUserTool Helpers', () => {
|
||||
describe('shouldHideAskUserTool', () => {
|
||||
it('returns false for non-AskUser tools', () => {
|
||||
expect(shouldHideAskUserTool('other-tool', 'Success', true)).toBe(false);
|
||||
});
|
||||
|
||||
it('hides Pending AskUser tool', () => {
|
||||
expect(
|
||||
shouldHideAskUserTool(ASK_USER_DISPLAY_NAME, 'Pending', false),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('hides Executing AskUser tool', () => {
|
||||
expect(
|
||||
shouldHideAskUserTool(ASK_USER_DISPLAY_NAME, 'Executing', false),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('hides Confirming AskUser tool', () => {
|
||||
expect(
|
||||
shouldHideAskUserTool(ASK_USER_DISPLAY_NAME, 'Confirming', false),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('shows Success AskUser tool', () => {
|
||||
expect(
|
||||
shouldHideAskUserTool(ASK_USER_DISPLAY_NAME, 'Success', true),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('shows Canceled AskUser tool', () => {
|
||||
expect(
|
||||
shouldHideAskUserTool(ASK_USER_DISPLAY_NAME, 'Canceled', true),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('hides Error AskUser tool without result display', () => {
|
||||
expect(shouldHideAskUserTool(ASK_USER_DISPLAY_NAME, 'Error', false)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows Error AskUser tool with result display', () => {
|
||||
expect(shouldHideAskUserTool(ASK_USER_DISPLAY_NAME, 'Error', true)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCompletedAskUserTool', () => {
|
||||
it('returns false for non-AskUser tools', () => {
|
||||
expect(isCompletedAskUserTool('other-tool', 'Success')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for Success status', () => {
|
||||
expect(isCompletedAskUserTool(ASK_USER_DISPLAY_NAME, 'Success')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true for Error status', () => {
|
||||
expect(isCompletedAskUserTool(ASK_USER_DISPLAY_NAME, 'Error')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for Canceled status', () => {
|
||||
expect(isCompletedAskUserTool(ASK_USER_DISPLAY_NAME, 'Canceled')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false for in-progress statuses', () => {
|
||||
expect(isCompletedAskUserTool(ASK_USER_DISPLAY_NAME, 'Executing')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isCompletedAskUserTool(ASK_USER_DISPLAY_NAME, 'Pending')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AskUserTool', () => {
|
||||
let mockMessageBus: MessageBus;
|
||||
@@ -228,6 +315,55 @@ describe('AskUserTool', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateBuildAndExecute', () => {
|
||||
it('should hide validation errors from returnDisplay', async () => {
|
||||
const params = {
|
||||
questions: [{ question: 'Test?', header: 'This is way too long' }],
|
||||
};
|
||||
|
||||
const result = await tool.validateBuildAndExecute(
|
||||
params,
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error?.type).toBe(ToolErrorType.INVALID_TOOL_PARAMS);
|
||||
expect(result.returnDisplay).toBe('');
|
||||
});
|
||||
|
||||
it('should NOT hide non-validation errors (if any were to occur)', async () => {
|
||||
const validateParamsSpy = vi
|
||||
.spyOn(tool, 'validateToolParams')
|
||||
.mockReturnValue(null);
|
||||
|
||||
const params = {
|
||||
questions: [{ question: 'Valid?', header: 'Valid' }],
|
||||
};
|
||||
|
||||
const mockInvocation = {
|
||||
execute: vi.fn().mockRejectedValue(new Error('Some execution error')),
|
||||
params,
|
||||
getDescription: vi.fn().mockReturnValue(''),
|
||||
toolLocations: vi.fn().mockReturnValue([]),
|
||||
shouldConfirmExecute: vi.fn().mockResolvedValue(false),
|
||||
};
|
||||
|
||||
const buildSpy = vi.spyOn(tool, 'build').mockReturnValue(mockInvocation);
|
||||
|
||||
const result = await tool.validateBuildAndExecute(
|
||||
params,
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error?.type).toBe(ToolErrorType.EXECUTION_FAILED);
|
||||
expect(result.returnDisplay).toBe('Some execution error');
|
||||
|
||||
buildSpy.mockRestore();
|
||||
validateParamsSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldConfirmExecute', () => {
|
||||
it('should return confirmation details with normalized questions', async () => {
|
||||
const questions = [
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type ToolConfirmationPayload,
|
||||
ToolConfirmationOutcome,
|
||||
} from './tools.js';
|
||||
import { ToolErrorType } from './tool-error.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import { QuestionType, type Question } from '../confirmation-bus/types.js';
|
||||
import { ASK_USER_TOOL_NAME, ASK_USER_DISPLAY_NAME } from './tool-names.js';
|
||||
@@ -154,6 +155,23 @@ export class AskUserTool extends BaseDeclarativeTool<
|
||||
): AskUserInvocation {
|
||||
return new AskUserInvocation(params, messageBus, toolName, toolDisplayName);
|
||||
}
|
||||
|
||||
override async validateBuildAndExecute(
|
||||
params: AskUserParams,
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<ToolResult> {
|
||||
const result = await super.validateBuildAndExecute(params, abortSignal);
|
||||
if (
|
||||
result.error &&
|
||||
result.error.type === ToolErrorType.INVALID_TOOL_PARAMS
|
||||
) {
|
||||
return {
|
||||
...result,
|
||||
returnDisplay: '',
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class AskUserInvocation extends BaseToolInvocation<
|
||||
@@ -242,3 +260,45 @@ export class AskUserInvocation extends BaseToolInvocation<
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an 'Ask User' tool call should be hidden from the standard tool history UI.
|
||||
*
|
||||
* We hide Ask User tools in two cases:
|
||||
* 1. They are in progress because they are displayed using a specialized UI (AskUserDialog).
|
||||
* 2. They have errored without a result display (e.g. validation errors), in which case
|
||||
* the agent self-corrects and we don't want to clutter the UI.
|
||||
*
|
||||
* NOTE: The 'status' parameter values are intended to match the CLI's ToolCallStatus enum.
|
||||
*/
|
||||
export function shouldHideAskUserTool(
|
||||
name: string,
|
||||
status: string,
|
||||
hasResultDisplay: boolean,
|
||||
): boolean {
|
||||
if (name !== ASK_USER_DISPLAY_NAME) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 1: In-progress tools (Pending, Executing, Confirming)
|
||||
if (['Pending', 'Executing', 'Confirming'].includes(status)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Case 2: Error without result display
|
||||
if (status === 'Error' && !hasResultDisplay) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the tool name and status correspond to a completed 'Ask User' tool call.
|
||||
*/
|
||||
export function isCompletedAskUserTool(name: string, status: string): boolean {
|
||||
return (
|
||||
name === ASK_USER_DISPLAY_NAME &&
|
||||
['Success', 'Error', 'Canceled'].includes(status)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user