mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-12 07:01:09 -07:00
203 lines
5.9 KiB
TypeScript
203 lines
5.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {
|
|
isTool,
|
|
type AnyDeclarativeTool,
|
|
type AnyToolInvocation,
|
|
} from '../index.js';
|
|
import { SHELL_TOOL_NAMES } from './shell-utils.js';
|
|
import levenshtein from 'fast-levenshtein';
|
|
import { ApprovalMode } from '../policy/types.js';
|
|
import {
|
|
CoreToolCallStatus,
|
|
type ToolCallResponseInfo,
|
|
} from '../scheduler/types.js';
|
|
import {
|
|
ASK_USER_DISPLAY_NAME,
|
|
WRITE_FILE_DISPLAY_NAME,
|
|
EDIT_DISPLAY_NAME,
|
|
} from '../tools/tool-names.js';
|
|
|
|
/**
|
|
* Validates if an object is a ToolCallResponseInfo.
|
|
*/
|
|
export function isToolCallResponseInfo(
|
|
data: unknown,
|
|
): data is ToolCallResponseInfo {
|
|
return (
|
|
typeof data === 'object' &&
|
|
data !== null &&
|
|
'callId' in data &&
|
|
'responseParts' in data
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Options for determining if a tool call should be hidden in the CLI history.
|
|
*/
|
|
export interface ShouldHideToolCallParams {
|
|
/** The display name of the tool. */
|
|
displayName: string;
|
|
/** The current status of the tool call. */
|
|
status: CoreToolCallStatus;
|
|
/** The approval mode active when the tool was called. */
|
|
approvalMode?: ApprovalMode;
|
|
/** Whether the tool has produced a result for display. */
|
|
hasResultDisplay: boolean;
|
|
/** The ID of the parent tool call, if any. */
|
|
parentCallId?: string;
|
|
}
|
|
|
|
/**
|
|
* Determines if a tool call should be hidden from the standard tool history UI.
|
|
*
|
|
* We hide tools in several cases:
|
|
* 1. Tool calls that have a parent, as they are "internal" to another tool (e.g. subagent).
|
|
* 2. Ask User tools that are in progress, displayed via specialized UI.
|
|
* 3. Ask User tools that errored without result display, typically param
|
|
* validation errors that the agent automatically recovers from.
|
|
* 4. WriteFile and Edit tools when in Plan Mode, redundant because the
|
|
* resulting plans are displayed separately upon exiting plan mode.
|
|
*/
|
|
export function shouldHideToolCall(params: ShouldHideToolCallParams): boolean {
|
|
const { displayName, status, approvalMode, hasResultDisplay, parentCallId } =
|
|
params;
|
|
|
|
if (parentCallId) {
|
|
return true;
|
|
}
|
|
|
|
switch (displayName) {
|
|
case ASK_USER_DISPLAY_NAME:
|
|
switch (status) {
|
|
case CoreToolCallStatus.Scheduled:
|
|
case CoreToolCallStatus.Validating:
|
|
case CoreToolCallStatus.Executing:
|
|
case CoreToolCallStatus.AwaitingApproval:
|
|
return true;
|
|
case CoreToolCallStatus.Error:
|
|
return !hasResultDisplay;
|
|
default:
|
|
return false;
|
|
}
|
|
case WRITE_FILE_DISPLAY_NAME:
|
|
case EDIT_DISPLAY_NAME:
|
|
return approvalMode === ApprovalMode.PLAN;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a suggestion string for a tool name that was not found in the registry.
|
|
* It finds the closest matches based on Levenshtein distance.
|
|
* @param unknownToolName The tool name that was not found.
|
|
* @param allToolNames The list of all available tool names.
|
|
* @param topN The number of suggestions to return. Defaults to 3.
|
|
* @returns A suggestion string like " Did you mean 'tool'?" or " Did you mean one of: 'tool1', 'tool2'?", or an empty string if no suggestions are found.
|
|
*/
|
|
export function getToolSuggestion(
|
|
unknownToolName: string,
|
|
allToolNames: string[],
|
|
topN = 3,
|
|
): string {
|
|
const matches = allToolNames.map((toolName) => ({
|
|
name: toolName,
|
|
distance: levenshtein.get(unknownToolName, toolName),
|
|
}));
|
|
|
|
matches.sort((a, b) => a.distance - b.distance);
|
|
|
|
const topNResults = matches.slice(0, topN);
|
|
|
|
if (topNResults.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
const suggestedNames = topNResults
|
|
.map((match) => `"${match.name}"`)
|
|
.join(', ');
|
|
|
|
if (topNResults.length > 1) {
|
|
return ` Did you mean one of: ${suggestedNames}?`;
|
|
} else {
|
|
return ` Did you mean ${suggestedNames}?`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a tool invocation matches any of a list of patterns.
|
|
*
|
|
* @param toolOrToolName The tool object or the name of the tool being invoked.
|
|
* @param invocation The invocation object for the tool or the command invoked.
|
|
* @param patterns A list of patterns to match against.
|
|
* Patterns can be:
|
|
* - A tool name (e.g., "ReadFileTool") to match any invocation of that tool.
|
|
* - A tool name with a prefix (e.g., "ShellTool(git status)") to match
|
|
* invocations where the arguments start with that prefix.
|
|
* @returns True if the invocation matches any pattern, false otherwise.
|
|
*/
|
|
export function doesToolInvocationMatch(
|
|
toolOrToolName: AnyDeclarativeTool | string,
|
|
invocation: AnyToolInvocation | string,
|
|
patterns: string[],
|
|
): boolean {
|
|
let toolNames: string[];
|
|
if (isTool(toolOrToolName)) {
|
|
toolNames = [toolOrToolName.name, toolOrToolName.constructor.name];
|
|
} else {
|
|
toolNames = [toolOrToolName];
|
|
}
|
|
|
|
if (toolNames.some((name) => SHELL_TOOL_NAMES.includes(name))) {
|
|
toolNames = [...new Set([...toolNames, ...SHELL_TOOL_NAMES])];
|
|
}
|
|
|
|
for (const pattern of patterns) {
|
|
const openParen = pattern.indexOf('(');
|
|
|
|
if (openParen === -1) {
|
|
// No arguments, just a tool name
|
|
if (toolNames.includes(pattern)) {
|
|
return true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const patternToolName = pattern.substring(0, openParen);
|
|
if (!toolNames.includes(patternToolName)) {
|
|
continue;
|
|
}
|
|
|
|
if (!pattern.endsWith(')')) {
|
|
continue;
|
|
}
|
|
|
|
const argPattern = pattern.substring(openParen + 1, pattern.length - 1);
|
|
|
|
let command: string;
|
|
if (typeof invocation === 'string') {
|
|
command = invocation;
|
|
} else {
|
|
if (!('command' in invocation.params)) {
|
|
// This invocation has no command - nothing to check.
|
|
continue;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
command = String((invocation.params as { command: string }).command);
|
|
}
|
|
|
|
if (toolNames.some((name) => SHELL_TOOL_NAMES.includes(name))) {
|
|
if (command === argPattern || command.startsWith(argPattern + ' ')) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|