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' ;
/**
* 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 ;
}