2025-08-27 02:17:43 +10:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { AnyDeclarativeTool , AnyToolInvocation } from '../index.js' ;
import { isTool } from '../index.js' ;
2025-10-06 12:15:21 -07:00
import { SHELL_TOOL_NAMES } from './shell-utils.js' ;
2025-12-26 15:51:39 -05:00
import levenshtein from 'fast-levenshtein' ;
2026-02-13 18:15:21 -05:00
import { ApprovalMode } from '../policy/types.js' ;
import { CoreToolCallStatus } from '../scheduler/types.js' ;
import {
ASK_USER_DISPLAY_NAME ,
WRITE_FILE_DISPLAY_NAME ,
EDIT_DISPLAY_NAME ,
} from '../tools/tool-names.js' ;
/**
* 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 ;
}
/**
* Determines if a tool call should be hidden from the standard tool history UI.
*
* We hide tools in several cases:
* 1. Ask User tools that are in progress, displayed via specialized UI.
* 2. Ask User tools that errored without result display, typically param
* validation errors that the agent automatically recovers from.
* 3. 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 } = params ;
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 ;
}
}
2025-12-26 15:51:39 -05:00
/**
* 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 } ? ` ;
}
}
2025-08-27 02:17:43 +10:00
/**
* 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.
2025-10-15 12:44:07 -07:00
* @param invocation The invocation object for the tool or the command invoked.
2025-08-27 02:17:43 +10:00
* @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 ,
2025-10-15 12:44:07 -07:00
invocation : AnyToolInvocation | string ,
2025-08-27 02:17:43 +10:00
patterns : string [ ] ,
) : boolean {
let toolNames : string [ ] ;
if ( isTool ( toolOrToolName ) ) {
toolNames = [ toolOrToolName . name , toolOrToolName . constructor . name ] ;
} else {
2025-12-12 17:43:43 -08:00
toolNames = [ toolOrToolName ] ;
2025-08-27 02:17:43 +10:00
}
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 ) ;
2025-10-15 12:44:07 -07:00
let command : string ;
if ( typeof invocation === 'string' ) {
command = invocation ;
} else {
if ( ! ( 'command' in invocation . params ) ) {
// This invocation has no command - nothing to check.
continue ;
}
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-10-15 12:44:07 -07:00
command = String ( ( invocation . params as { command : string } ) . command ) ;
}
if ( toolNames . some ( ( name ) = > SHELL_TOOL_NAMES . includes ( name ) ) ) {
if ( command === argPattern || command . startsWith ( argPattern + ' ' ) ) {
2025-08-27 02:17:43 +10:00
return true ;
}
}
}
return false ;
}