From dfe41ec1bf3fc6909819cdeb65a29cd19fc8b224 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Sat, 27 Sep 2025 00:59:41 +0200 Subject: [PATCH] qualify tool names in prompt files (#268574) * qualified tool names * qualify tool names in prompt files --- .../chat/browser/languageModelToolsService.ts | 170 ++++++++++++++---- .../promptSyntax/promptFileRewriter.ts | 24 +-- .../chat/common/languageModelToolsService.ts | 14 +- .../promptBodyAutocompletion.ts | 10 +- .../languageProviders/promptCodeActions.ts | 85 +++++++++ .../promptHeaderAutocompletion.ts | 10 +- .../languageProviders/promptHovers.ts | 14 +- .../promptSyntax/promptFileContributions.ts | 2 + .../promptSyntax/service/promptValidator.ts | 36 ++-- .../browser/languageModelToolsService.test.ts | 151 +++++++++++----- .../promptSytntax}/promptValidator.test.ts | 98 ++++++---- .../common/mockLanguageModelToolsService.ts | 24 ++- 12 files changed, 457 insertions(+), 181 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts rename src/vs/workbench/contrib/chat/test/{common/promptSyntax/service => browser/promptSytntax}/promptValidator.test.ts (75%) diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index e6d1f9460da..1caf67d8ece 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -596,58 +596,73 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } } - toToolEnablementMap(toolOrToolsetNames: Set): Record { - const result: Record = {}; - for (const tool of this._tools.values()) { - if (tool.data.toolReferenceName && toolOrToolsetNames.has(tool.data.toolReferenceName)) { - result[tool.data.id] = true; - } else { - result[tool.data.id] = false; - } - } - - for (const toolSet of this._toolSets) { - if (toolOrToolsetNames.has(toolSet.referenceName)) { - for (const tool of toolSet.getTools()) { - result[tool.id] = true; - } - } - } - - return result; - } - /** * Create a map that contains all tools and toolsets with their enablement state. * @param toolOrToolSetNames A list of tool or toolset names that are enabled. * @returns A map of tool or toolset instances to their enablement state. */ - toToolAndToolSetEnablementMap(enabledToolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap { - const toolOrToolSetNames = new Set(enabledToolOrToolSetNames); + toToolAndToolSetEnablementMap(enabledQualifiedToolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap { + const toolOrToolSetNames = new Set(enabledQualifiedToolOrToolSetNames); const result = new Map(); for (const tool of this.getTools()) { if (tool.canBeReferencedInPrompt) { - result.set(tool, toolOrToolSetNames.has(tool.toolReferenceName ?? tool.displayName)); + const enabled = toolOrToolSetNames.has(getToolReferenceName(tool)) || /* legacy */ toolOrToolSetNames.has(tool.toolReferenceName ?? tool.displayName); + result.set(tool, enabled); } } - for (const toolSet of this._toolSets) { - const enabled = toolOrToolSetNames.has(toolSet.referenceName); - result.set(toolSet, enabled); + for (const toolSet of this.toolSets.get()) { + const toolSetEnabled = toolOrToolSetNames.has(getToolSetReferenceName(toolSet)) || /* legacy */ toolOrToolSetNames.has(toolSet.referenceName); + result.set(toolSet, toolSetEnabled); for (const tool of toolSet.getTools()) { - result.set(tool, enabled || toolOrToolSetNames?.has(tool.toolReferenceName ?? tool.displayName)); + const enabled = toolSetEnabled || toolOrToolSetNames.has(getToolReferenceName(tool, toolSet)) || /* legacy */ toolOrToolSetNames.has(tool.toolReferenceName ?? tool.displayName); + result.set(tool, enabled); } - } return result; } - public toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] { - const toolsOrToolSetByName = new Map(); - for (const toolSet of this.toolSets.get()) { - toolsOrToolSetByName.set(toolSet.referenceName, toolSet); + toQualifiedToolNames(map: IToolAndToolSetEnablementMap): string[] { + const toolsCoveredBySets = new Set(); + for (const item of map.keys()) { + if (item instanceof ToolSet) { + for (const tool of item.getTools()) { + toolsCoveredBySets.add(tool); + } + } } + + const result: string[] = []; for (const tool of this.getTools()) { - toolsOrToolSetByName.set(tool.toolReferenceName ?? tool.displayName, tool); + if (map.get(tool) && !toolsCoveredBySets.has(tool)) { + result.push(getToolReferenceName(tool)); + } + } + for (const toolSet of this.toolSets.get()) { + if (map.get(toolSet)) { + result.push(getToolSetReferenceName(toolSet)); + } else { + for (const tool of toolSet.getTools()) { + if (map.get(tool)) { + result.push(getToolReferenceName(tool, toolSet)); + } + } + } + } + return result; + } + + toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] { + const toolsOrToolSetByName = new Map(); + for (const tool of this.getTools()) { + if (tool.canBeReferencedInPrompt) { + toolsOrToolSetByName.set(getToolReferenceName(tool), tool); + } + } + for (const toolSet of this.toolSets.get()) { + toolsOrToolSetByName.set(getToolSetReferenceName(toolSet), toolSet); + for (const tool of toolSet.getTools()) { + toolsOrToolSetByName.set(getToolReferenceName(tool, toolSet), tool); + } } const result: ChatRequestToolReferenceEntry[] = []; @@ -704,6 +719,93 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo this._toolSets.add(result); return result; } + + *getQualifiedToolNames(): Iterable { + for (const tool of this.getTools()) { + if (tool.canBeReferencedInPrompt) { + yield getToolReferenceName(tool); + } + } + for (const toolSet of this.toolSets.get()) { + yield getToolSetReferenceName(toolSet); + for (const tool of toolSet.getTools()) { + yield getToolReferenceName(tool, toolSet); + } + } + } + + getDeprecatedQualifiedToolNames(): Map { + const result = new Map(); + const add = (name: string, qualifiedName: string) => { + if (name !== qualifiedName) { + result.set(name, qualifiedName); + } + }; + for (const tool of this.getTools()) { + if (tool.canBeReferencedInPrompt) { + add(tool.toolReferenceName ?? tool.displayName, getToolReferenceName(tool)); + } + } + for (const toolSet of this.toolSets.get()) { + add(toolSet.referenceName, getToolSetReferenceName(toolSet)); + for (const tool of toolSet.getTools()) { + add(tool.toolReferenceName ?? tool.displayName, getToolReferenceName(tool, toolSet)); + } + } + return result; + } + + getToolByQualifiedName(qualifiedName: string): IToolData | ToolSet | undefined { + for (const tool of this.getTools()) { + if (tool.canBeReferencedInPrompt) { + if (qualifiedName === getToolReferenceName(tool) || qualifiedName === (tool.toolReferenceName ?? tool.displayName) /* legacy */) { + if (matchesToolReferenceName(tool, qualifiedName)) { + return tool; + } + } + } + for (const toolSet of this.toolSets.get()) { + if (qualifiedName === getToolSetReferenceName(toolSet) || qualifiedName === toolSet.referenceName /* legacy */) { + return toolSet; + } + for (const tool of toolSet.getTools()) { + if (qualifiedName === getToolReferenceName(tool) || qualifiedName === (tool.toolReferenceName ?? tool.displayName) /* legacy */) { + return tool; + } + } + } + } + return undefined; + } + + getQualifiedToolName(tool: IToolData | ToolSet, toolSet?: ToolSet): string { + if (tool instanceof ToolSet) { + return getToolSetReferenceName(tool); + } + return getToolReferenceName(tool, toolSet); + } +} + +function getToolReferenceName(tool: IToolData, toolSet?: ToolSet) { + const toolName = tool.toolReferenceName ?? tool.displayName; + if (toolSet) { + return `${toolSet.referenceName}/${toolName}`; + } else if (tool.source.type === 'extension') { + return `${tool.source.extensionId.value.toLowerCase()}/${toolName}`; + } + return toolName; +} + +function getToolSetReferenceName(toolSet: ToolSet) { + if (toolSet.source.type === 'mcp') { + return `${toolSet.referenceName}/*`; + } + return toolSet.referenceName; +} + +function matchesToolReferenceName(tool: IToolData, name: string, toolSet?: ToolSet) { + const toolName = tool.toolReferenceName ?? tool.displayName; + return name === toolName || (toolSet && name === `${toolSet.referenceName}/${toolName}`); } type LanguageModelToolInvokedEvent = { diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts index 21072444d07..c8b543ef4f1 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts @@ -9,13 +9,14 @@ import { ICodeEditorService } from '../../../../../editor/browser/services/codeE import { EditOperation } from '../../../../../editor/common/core/editOperation.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { ITextModel } from '../../../../../editor/common/model.js'; -import { IToolAndToolSetEnablementMap, IToolData, ToolSet } from '../../common/languageModelToolsService.js'; +import { ILanguageModelToolsService, IToolAndToolSetEnablementMap } from '../../common/languageModelToolsService.js'; import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; export class PromptFileRewriter { constructor( @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IPromptsService private readonly _promptsService: IPromptsService + @IPromptsService private readonly _promptsService: IPromptsService, + @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService ) { } @@ -48,24 +49,7 @@ export class PromptFileRewriter { } public getNewValueString(tools: IToolAndToolSetEnablementMap): string { - const newToolNames: string[] = []; - const toolsCoveredBySets = new Set(); - for (const [item, picked] of tools) { - if (picked && item instanceof ToolSet) { - for (const tool of item.getTools()) { - toolsCoveredBySets.add(tool); - } - } - } - for (const [item, picked] of tools) { - if (picked) { - if (item instanceof ToolSet) { - newToolNames.push(item.referenceName); - } else if (!toolsCoveredBySets.has(item)) { - newToolNames.push(item.toolReferenceName ?? item.displayName); - } - } - } + const newToolNames = this._languageModelToolsService.toQualifiedToolNames(tools); return `[${newToolNames.map(s => `'${s}'`).join(', ')}]`; } } diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index 5dc95aec9ab..bdabd076efc 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -324,14 +324,22 @@ export interface ILanguageModelToolsService { getToolAutoConfirmation(toolId: string): 'workspace' | 'profile' | 'session' | 'never'; resetToolAutoConfirmation(): void; cancelToolCallsForRequest(requestId: string): void; - toToolEnablementMap(toolOrToolSetNames: Set): Record; - toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap; - toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[]; readonly toolSets: IObservable>; getToolSet(id: string): ToolSet | undefined; getToolSetByName(name: string): ToolSet | undefined; createToolSet(source: ToolDataSource, id: string, referenceName: string, options?: { icon?: ThemeIcon; description?: string }): ToolSet & IDisposable; + + // tool names in prompt files handling ('qualified names') + + getQualifiedToolNames(): Iterable; + getToolByQualifiedName(qualifiedName: string): IToolData | ToolSet | undefined; + getQualifiedToolName(tool: IToolData, toolSet?: ToolSet): string; + getDeprecatedQualifiedToolNames(): Map; + + toToolAndToolSetEnablementMap(qualifiedToolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap; + toQualifiedToolNames(map: IToolAndToolSetEnablementMap): string[]; + toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[]; } export function createToolInputUri(toolOrId: IToolData | string): URI { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptBodyAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptBodyAutocompletion.ts index d77e76a5ce1..47e2d89864a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptBodyAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptBodyAutocompletion.ts @@ -70,7 +70,7 @@ export class PromptBodyAutocompletion extends Disposable implements CompletionIt } private async collectToolCompletions(model: ITextModel, position: Position, toolRange: Range, suggestions: CompletionItem[]): Promise { - const addSuggestion = (toolName: string, toolRange: Range) => { + for (const toolName of this.languageModelToolsService.getQualifiedToolNames()) { suggestions.push({ label: toolName, kind: CompletionItemKind.Value, @@ -78,14 +78,6 @@ export class PromptBodyAutocompletion extends Disposable implements CompletionIt insertText: toolName, range: toolRange, }); - }; - for (const tool of this.languageModelToolsService.getTools()) { - if (tool.canBeReferencedInPrompt) { - addSuggestion(tool.toolReferenceName ?? tool.displayName, toolRange); - } - } - for (const toolSet of this.languageModelToolsService.toolSets.get()) { - addSuggestion(toolSet.referenceName, toolRange); } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts new file mode 100644 index 00000000000..abdd3e1b120 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { CodeActionContext, CodeActionList, CodeActionProvider, ProviderResult, TextEdit, WorkspaceEdit } from '../../../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; +import { localize } from '../../../../../../nls.js'; +import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; +import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId } from '../promptTypes.js'; +import { IPromptsService } from '../service/promptsService.js'; +import { IValue } from '../service/newPromptsParser.js'; +import { Selection } from '../../../../../../editor/common/core/selection.js'; + +export class PromptCodeActionProvider extends Disposable implements CodeActionProvider { + /** + * Debug display name for this provider. + */ + public readonly _debugDisplayName: string = 'PromptHoverProvider'; + + constructor( + @IPromptsService private readonly promptsService: IPromptsService, + @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, + @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService + ) { + super(); + + this._register(this.languageService.codeActionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, this)); + } + + provideCodeActions(model: ITextModel, range: Range | Selection, context: CodeActionContext, token: CancellationToken): ProviderResult { + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); + if (!promptType) { + // if the model is not a prompt, we don't provide any hovers + return undefined; + } + + const parser = this.promptsService.getParsedPromptFile(model); + const toolsAttr = parser.header?.getAttribute('tools'); + if (!toolsAttr || toolsAttr.value.type !== 'array' || !toolsAttr.value.range.containsRange(range)) { + return undefined; + } + for (const item of toolsAttr.value.items) { + if (item.range.containsRange(range)) { + return this.getToolCodeActions(item, model); + } + } + return undefined; + } + + private getToolCodeActions(value: IValue, model: ITextModel): CodeActionList | undefined { + if (value.type !== 'string') { + return undefined; + } + const oldName = value.value; + const deprecatedNames = this.languageModelToolsService.getDeprecatedQualifiedToolNames(); + const newName = deprecatedNames.get(oldName); + if (newName) { + const quote = model.getValueInRange(new Range(value.range.startLineNumber, value.range.startColumn, value.range.endLineNumber, value.range.startColumn + 1)); + const text = (quote === `'` || quote === '"') ? (quote + newName + quote) : newName; + return { + actions: [{ + title: localize('replaceWith', "Replace with '{0}'", newName), + edit: asWorkspaceEdit(model, { range: value.range, text: text }) + }], + dispose() { } + }; + } + return undefined; + } + +} +function asWorkspaceEdit(model: ITextModel, textEdit: TextEdit): WorkspaceEdit { + return { + edits: [{ + versionId: model.getVersionId(), + resource: model.uri, + textEdit + }] + }; +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 4ed5bc6869e..e95ca021e91 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -222,7 +222,7 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion } const getSuggestions = (toolRange: Range) => { const suggestions: CompletionItem[] = []; - const addSuggestion = (toolName: string, toolRange: Range) => { + for (const toolName of this.languageModelToolsService.getQualifiedToolNames()) { let insertText: string; if (!toolRange.isEmpty()) { const firstChar = model.getValueInRange(toolRange).charCodeAt(0); @@ -237,14 +237,6 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion insertText: insertText, range: toolRange, }); - }; - for (const tool of this.languageModelToolsService.getTools()) { - if (tool.canBeReferencedInPrompt) { - addSuggestion(tool.toolReferenceName ?? tool.displayName, toolRange); - } - } - for (const toolSet of this.languageModelToolsService.toolSets.get()) { - addSuggestion(toolSet.referenceName, toolRange); } return { suggestions }; }; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index dc77fa2caa7..87723bb7290 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -129,13 +129,13 @@ export class PromptHoverProvider extends Disposable implements HoverProvider { } private getToolHoverByName(toolName: string, range: Range): Hover | undefined { - const tool = this.languageModelToolsService.getToolByName(toolName); - if (tool) { - return this.createHover(tool.modelDescription, range); - } - const toolSet = this.languageModelToolsService.getToolSetByName(toolName); - if (toolSet) { - return this.getToolsetHover(toolSet, range); + const tool = this.languageModelToolsService.getToolByQualifiedName(toolName); + if (tool !== undefined) { + if (tool instanceof ToolSet) { + return this.getToolsetHover(tool, range); + } else { + return this.createHover(tool.modelDescription, range); + } } return undefined; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts index e4f3b5b4e33..aba3b3882ea 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts @@ -14,6 +14,7 @@ import { PromptHoverProvider } from './languageProviders/promptHovers.js'; import { PromptHeaderDefinitionProvider } from './languageProviders/PromptHeaderDefinitionProvider.js'; import { PromptValidatorContribution } from './service/promptValidator.js'; import { PromptDocumentSemanticTokensProvider } from './languageProviders/promptDocumentSemanticTokensProvider.js'; +import { PromptCodeActionProvider } from './languageProviders/promptCodeActions.js'; /** @@ -31,6 +32,7 @@ export function registerPromptFileContributions(): void { registerContribution(PromptHoverProvider); registerContribution(PromptHeaderDefinitionProvider); registerContribution(PromptDocumentSemanticTokensProvider); + registerContribution(PromptCodeActionProvider); registerContribution(ConfigMigration); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptValidator.ts index 763d42960ba..5d78cb41c84 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptValidator.ts @@ -72,10 +72,16 @@ export class PromptValidator { // Validate variable references (tool or toolset names) if (body.variableReferences.length) { - const available = this.getAvailableToolAndToolSetNames(); + const available = new Set(this.languageModelToolsService.getQualifiedToolNames()); + const deprecatedNames = this.languageModelToolsService.getDeprecatedQualifiedToolNames(); for (const variable of body.variableReferences) { if (!available.has(variable.name)) { - report(toMarker(localize('promptValidator.unknownVariableReference', "Unknown tool or toolset '{0}'.", variable.name), variable.range, MarkerSeverity.Warning)); + if (deprecatedNames.has(variable.name)) { + const currentName = deprecatedNames.get(variable.name); + report(toMarker(localize('promptValidator.deprecatedVariableReference', "Tool or toolset '{0}' is deprecated, use '{1}' instead.", variable.name, currentName), variable.range, MarkerSeverity.Warning)); + } else { + report(toMarker(localize('promptValidator.unknownVariableReference', "Unknown tool or toolset '{0}'.", variable.name), variable.range, MarkerSeverity.Error)); + } } } } @@ -236,33 +242,23 @@ export class PromptValidator { private validateToolsArray(valueItem: IArrayValue, report: (markers: IMarkerData) => void) { if (valueItem.items.length > 0) { - const available = this.getAvailableToolAndToolSetNames(); + const available = new Set(this.languageModelToolsService.getQualifiedToolNames()); + const deprecatedNames = this.languageModelToolsService.getDeprecatedQualifiedToolNames(); for (const item of valueItem.items) { if (item.type !== 'string') { report(toMarker(localize('promptValidator.eachToolMustBeString', "Each tool name in the 'tools' attribute must be a string."), item.range, MarkerSeverity.Error)); } else if (item.value && !available.has(item.value)) { - report(toMarker(localize('promptValidator.toolNotFound', "Unknown tool '{0}'.", item.value), item.range, MarkerSeverity.Warning)); + if (deprecatedNames.has(item.value)) { + const currentName = deprecatedNames.get(item.value); + report(toMarker(localize('promptValidator.toolDeprecated', "Tool or toolset '{0}' is deprecated, use '{1}' instead.", item.value, currentName), item.range, MarkerSeverity.Warning)); + } else { + report(toMarker(localize('promptValidator.toolNotFound', "Unknown tool '{0}'.", item.value), item.range, MarkerSeverity.Error)); + } } } } } - private getAvailableToolAndToolSetNames(): Set { - const available = new Set(); - for (const tool of this.languageModelToolsService.getTools()) { - if (tool.canBeReferencedInPrompt) { - available.add(tool.toolReferenceName ?? tool.displayName); - } - } - for (const toolSet of this.languageModelToolsService.toolSets.get()) { - available.add(toolSet.referenceName); - for (const tool of toolSet.getTools()) { - available.add(tool.toolReferenceName ?? tool.displayName); - } - } - return available; - } - private validateApplyTo(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { const attribute = attributes.find(attr => attr.key === 'applyTo'); if (!attribute) { diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 02bb85f80de..af5d6ed6679 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -23,6 +23,8 @@ import { IChatService, IChatToolInputInvocationData } from '../../common/chatSer import { IToolData, IToolImpl, IToolInvocation, ToolDataSource } from '../../common/languageModelToolsService.js'; import { MockChatService } from '../common/mockChatService.js'; import { IConfigurationChangeEvent } from '../../../../../platform/configuration/common/configuration.js'; +import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; +import { ChatConfiguration } from '../../common/constants.js'; // --- Test helpers to reduce repetition and improve readability --- @@ -100,6 +102,7 @@ suite('LanguageModelToolsService', () => { setup(() => { configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); const instaService = workbenchInstantiationService({ contextKeyService: () => store.add(new ContextKeyService(configurationService)), configurationService: () => configurationService @@ -378,13 +381,14 @@ suite('LanguageModelToolsService', () => { }, 'Expected tool call to be cancelled'); }); - test('toToolEnablementMap', () => { + test('toToolAndToolSetEnablementMap', () => { const toolData1: IToolData = { id: 'tool1', toolReferenceName: 'refTool1', modelDescription: 'Test Tool 1', displayName: 'Test Tool 1', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; const toolData2: IToolData = { @@ -393,6 +397,7 @@ suite('LanguageModelToolsService', () => { modelDescription: 'Test Tool 2', displayName: 'Test Tool 2', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; const toolData3: IToolData = { @@ -401,6 +406,7 @@ suite('LanguageModelToolsService', () => { modelDescription: 'Test Tool 3', displayName: 'Test Tool 3', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; store.add(service.registerToolData(toolData1)); @@ -408,31 +414,66 @@ suite('LanguageModelToolsService', () => { store.add(service.registerToolData(toolData3)); // Test with enabled tools - const enabledToolNames = new Set(['refTool1']); - const result1 = service.toToolEnablementMap(enabledToolNames); + const enabledToolNames = [toolData1].map(t => service.getQualifiedToolName(t)); + const result1 = service.toToolAndToolSetEnablementMap(enabledToolNames); + + assert.strictEqual(result1.get(toolData1), true, 'tool1 should be enabled'); + assert.strictEqual(result1.get(toolData2), false, 'tool2 should be disabled'); + assert.strictEqual(result1.get(toolData3), false, 'tool3 should be disabled (no reference name)'); + + const qualifiedNames1 = service.toQualifiedToolNames(result1); + assert.deepStrictEqual(qualifiedNames1.sort(), enabledToolNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - assert.strictEqual(result1['tool1'], true, 'tool1 should be enabled'); - assert.strictEqual(result1['tool2'], false, 'tool2 should be disabled'); - assert.strictEqual(result1['tool3'], false, 'tool3 should be disabled (no reference name)'); // Test with multiple enabled tools - const multipleEnabledToolNames = new Set(['refTool1', 'refTool2']); - const result2 = service.toToolEnablementMap(multipleEnabledToolNames); + const multipleEnabledToolNames = [toolData1, toolData2].map(t => service.getQualifiedToolName(t)); + const result2 = service.toToolAndToolSetEnablementMap(multipleEnabledToolNames); + + assert.strictEqual(result1.get(toolData1), true, 'tool1 should be enabled'); + assert.strictEqual(result2.get(toolData2), true, 'tool2 should be enabled'); + assert.strictEqual(result2.get(toolData3), false, 'tool3 should be disabled'); + + const qualifiedNames2 = service.toQualifiedToolNames(result2); + assert.deepStrictEqual(qualifiedNames2.sort(), multipleEnabledToolNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - assert.strictEqual(result2['tool1'], true, 'tool1 should be enabled'); - assert.strictEqual(result2['tool2'], true, 'tool2 should be enabled'); - assert.strictEqual(result2['tool3'], false, 'tool3 should be disabled'); // Test with no enabled tools - const noEnabledToolNames = new Set(); - const result3 = service.toToolEnablementMap(noEnabledToolNames); + const noEnabledToolNames: string[] = []; + const result3 = service.toToolAndToolSetEnablementMap(noEnabledToolNames); + + assert.strictEqual(result3.get(toolData1), false, 'tool1 should be disabled'); + assert.strictEqual(result3.get(toolData2), false, 'tool2 should be disabled'); + assert.strictEqual(result3.get(toolData3), false, 'tool3 should be disabled'); + + const qualifiedNames3 = service.toQualifiedToolNames(result3); + assert.deepStrictEqual(qualifiedNames3.sort(), noEnabledToolNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - assert.strictEqual(result3['tool1'], false, 'tool1 should be disabled'); - assert.strictEqual(result3['tool2'], false, 'tool2 should be disabled'); - assert.strictEqual(result3['tool3'], false, 'tool3 should be disabled'); }); - test('toToolEnablementMap with tool sets', () => { + test('toToolAndToolSetEnablementMap with extension tool', () => { + // Register individual tools + const toolData1: IToolData = { + id: 'tool1', + toolReferenceName: 'refTool1', + modelDescription: 'Test Tool 1', + displayName: 'Test Tool 1', + source: { type: 'extension', label: "My Extension", extensionId: new ExtensionIdentifier('My.extension') }, + canBeReferencedInPrompt: true, + }; + + store.add(service.registerToolData(toolData1)); + + // Test enabling the tool set + const enabledNames = [toolData1].map(t => service.getQualifiedToolName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames); + + assert.strictEqual(result.get(toolData1), true, 'individual tool should be enabled'); + + const qualifiedNames = service.toQualifiedToolNames(result); + assert.deepStrictEqual(qualifiedNames.sort(), enabledNames.sort(), 'toQualifiedToolNames should return the original enabled names'); + }); + + test('toToolAndToolSetEnablementMap with tool sets', () => { // Register individual tools const toolData1: IToolData = { id: 'tool1', @@ -440,6 +481,7 @@ suite('LanguageModelToolsService', () => { modelDescription: 'Test Tool 1', displayName: 'Test Tool 1', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; const toolData2: IToolData = { @@ -447,6 +489,7 @@ suite('LanguageModelToolsService', () => { modelDescription: 'Test Tool 2', displayName: 'Test Tool 2', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; store.add(service.registerToolData(toolData1)); @@ -481,33 +524,52 @@ suite('LanguageModelToolsService', () => { store.add(toolSet.addTool(toolSetTool2)); // Test enabling the tool set - const enabledNames = new Set(['refToolSet', 'refTool1']); - const result = service.toToolEnablementMap(enabledNames); + const enabledNames = [toolSet, toolData1].map(t => service.getQualifiedToolName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames); - assert.strictEqual(result['tool1'], true, 'individual tool should be enabled'); - assert.strictEqual(result['tool2'], false); - assert.strictEqual(result['toolSetTool1'], true, 'tool set tool 1 should be enabled'); - assert.strictEqual(result['toolSetTool2'], true, 'tool set tool 2 should be enabled'); + assert.strictEqual(result.get(toolData1), true, 'individual tool should be enabled'); + assert.strictEqual(result.get(toolData2), false); + assert.strictEqual(result.get(toolSet), true, 'tool set should be enabled'); + assert.strictEqual(result.get(toolSetTool1), true, 'tool set tool 1 should be enabled'); + assert.strictEqual(result.get(toolSetTool2), true, 'tool set tool 2 should be enabled'); + + const qualifiedNames = service.toQualifiedToolNames(result); + assert.deepStrictEqual(qualifiedNames.sort(), enabledNames.sort(), 'toQualifiedToolNames should return the original enabled names'); }); - test('toToolEnablementMap with non-existent tool names', () => { + test('toToolAndToolSetEnablementMap with non-existent tool names', () => { const toolData: IToolData = { id: 'tool1', toolReferenceName: 'refTool1', modelDescription: 'Test Tool 1', displayName: 'Test Tool 1', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; store.add(service.registerToolData(toolData)); - // Test with non-existent tool names - const enabledNames = new Set(['nonExistentTool', 'refTool1']); - const result = service.toToolEnablementMap(enabledNames); + const unregisteredToolData: IToolData = { + id: 'toolX', + toolReferenceName: 'refToolX', + modelDescription: 'Test Tool X', + displayName: 'Test Tool X', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + }; - assert.strictEqual(result['tool1'], true, 'existing tool should be enabled'); + // Test with non-existent tool names + const enabledNames = [toolData, unregisteredToolData].map(t => service.getQualifiedToolName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames); + + assert.strictEqual(result.get(toolData), true, 'existing tool should be enabled'); // Non-existent tools should not appear in the result map - assert.strictEqual(result['nonExistentTool'], undefined, 'non-existent tool should not be in result'); + assert.strictEqual(result.get(unregisteredToolData), undefined, 'non-existent tool should not be in result'); + + const qualifiedNames = service.toQualifiedToolNames(result); + const expectedNames = [service.getQualifiedToolName(toolData)]; // Only the existing tool + assert.deepStrictEqual(qualifiedNames.sort(), expectedNames.sort(), 'toQualifiedToolNames should return the original enabled names'); + }); test('accessibility signal for tool confirmation', async () => { @@ -1298,21 +1360,28 @@ suite('LanguageModelToolsService', () => { store.add(mcpToolSet.addTool(mcpTool)); // Enable the MCP toolset - const result = service.toToolAndToolSetEnablementMap(['mcpSetRef']); + { + const enabledNames = [mcpToolSet].map(t => service.getQualifiedToolName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames); - let toolSetEnabled = false; - let toolEnabled = false; - for (const [toolOrSet, enabled] of result) { - if ('referenceName' in toolOrSet && toolOrSet.referenceName === 'mcpSetRef') { - toolSetEnabled = enabled; - } - if ('id' in toolOrSet && toolOrSet.id === 'mcpTool') { - toolEnabled = enabled; - } + assert.strictEqual(result.get(mcpToolSet), true, 'MCP toolset should be enabled'); // Ensure the toolset is in the map + assert.strictEqual(result.get(mcpTool), true, 'MCP tool should be enabled when its toolset is enabled'); // Ensure the tool is in the map + + const qualifiedNames = service.toQualifiedToolNames(result); + assert.deepStrictEqual(qualifiedNames.sort(), enabledNames.sort(), 'toQualifiedToolNames should return the original enabled names'); + } + // Enable a tool from the MCP toolset + { + const enabledNames = [mcpTool].map(t => service.getQualifiedToolName(t, mcpToolSet)); + const result = service.toToolAndToolSetEnablementMap(enabledNames); + + assert.strictEqual(result.get(mcpToolSet), false, 'MCP toolset should be disabled'); // Ensure the toolset is in the map + assert.strictEqual(result.get(mcpTool), true, 'MCP tool should be enabled'); // Ensure the tool is in the map + + const qualifiedNames = service.toQualifiedToolNames(result); + assert.deepStrictEqual(qualifiedNames.sort(), enabledNames.sort(), 'toQualifiedToolNames should return the original enabled names'); } - assert.strictEqual(toolSetEnabled, true, 'MCP toolset should be enabled'); - assert.strictEqual(toolEnabled, true, 'MCP tool should be enabled when its toolset is enabled'); }); test('shouldAutoConfirm with workspace-specific tool configuration', async () => { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts similarity index 75% rename from src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptValidator.test.ts rename to src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts index dbdf73dbea4..133a068067e 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts @@ -5,26 +5,30 @@ import assert from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; -import { URI } from '../../../../../../../base/common/uri.js'; -import { NewPromptsParser } from '../../../../common/promptSyntax/service/newPromptsParser.js'; -import { PromptValidator } from '../../../../common/promptSyntax/service/promptValidator.js'; -import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js'; -import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; -import { ILanguageModelToolsService, IToolData, ToolDataSource, ToolSet } from '../../../../common/languageModelToolsService.js'; -import { ObservableSet } from '../../../../../../../base/common/observable.js'; -import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../common/languageModels.js'; -import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; -import { ChatMode, CustomChatMode, IChatModeService } from '../../../../common/chatModes.js'; -import { MockChatModeService } from '../../mockChatModeService.js'; -import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; -import { IMarkerData, MarkerSeverity } from '../../../../../../../platform/markers/common/markers.js'; -import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; -import { IFileService } from '../../../../../../../platform/files/common/files.js'; -import { ResourceSet } from '../../../../../../../base/common/map.js'; -import { ILabelService } from '../../../../../../../platform/label/common/label.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { NewPromptsParser } from '../../../common/promptSyntax/service/newPromptsParser.js'; +import { PromptValidator } from '../../../common/promptSyntax/service/promptValidator.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { PromptsConfig } from '../../../common/promptSyntax/config/config.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../common/languageModelToolsService.js'; +import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../common/languageModels.js'; +import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; +import { ChatMode, CustomChatMode, IChatModeService } from '../../../common/chatModes.js'; +import { MockChatModeService } from '../../common/mockChatModeService.js'; +import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; +import { IMarkerData, MarkerSeverity } from '../../../../../../platform/markers/common/markers.js'; +import { getPromptFileExtension } from '../../../common/promptSyntax/config/promptFileLocations.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { ResourceSet } from '../../../../../../base/common/map.js'; +import { ChatConfiguration } from '../../../common/constants.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { IChatService } from '../../../common/chatService.js'; +import { MockChatService } from '../../common/mockChatService.js'; +import { LanguageModelToolsService } from '../../../browser/languageModelToolsService.js'; +import { ContextKeyService } from '../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; suite('PromptValidator', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -32,22 +36,29 @@ suite('PromptValidator', () => { let instaService: TestInstantiationService; setup(async () => { - instaService = disposables.add(new TestInstantiationService()); const testConfigService = new TestConfigurationService(); testConfigService.setUserConfiguration(PromptsConfig.KEY, true); - - instaService.stub(IConfigurationService, testConfigService); - + testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + instaService = workbenchInstantiationService({ + contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, disposables); + const chatService = new MockChatService(); + instaService.stub(IChatService, chatService); instaService.stub(ILabelService, { getUriLabel: (resource) => resource.path }); + const toolService = disposables.add(instaService.createInstance(LanguageModelToolsService)); + const testTool1 = { id: 'testTool1', displayName: 'tool1', canBeReferencedInPrompt: true, modelDescription: 'Test Tool 1', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + const testTool3 = { id: 'testTool3', displayName: 'tool3', canBeReferencedInPrompt: true, toolReferenceName: 'tool3', modelDescription: 'Test Tool 3', source: { type: 'extension', label: "My Extension", extensionId: new ExtensionIdentifier('My.extension') }, inputSchema: {} } satisfies IToolData; - instaService.stub(ILanguageModelToolsService, { - getTools() { return [testTool1, testTool2]; }, - toolSets: new ObservableSet().observable - }); + disposables.add(toolService.registerToolData(testTool1)); + disposables.add(toolService.registerToolData(testTool2)); + disposables.add(toolService.registerToolData(testTool3)); + + instaService.set(ILanguageModelToolsService, toolService); const testModels: ILanguageModelChatMetadata[] = [ { id: 'mae-4', name: 'MAE 4', vendor: 'olama', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true } } satisfies ILanguageModelChatMetadata, @@ -107,17 +118,16 @@ suite('PromptValidator', () => { /* 01 */"---", /* 02 */`description: ""`, // empty description -> error /* 03 */"model: MAE 4.2", // unknown model -> warning - /* 04 */"tools: ['tool1', 'tool2', 'tool3']", // tool3 unknown -> warning + /* 04 */"tools: ['tool1', 'tool2', 'tool4', 'my.extension/tool3']", // tool4 unknown -> error /* 05 */"---", /* 06 */"Body", ].join('\n'); const markers = await validate(content, PromptsType.mode); - assert.strictEqual(markers.length, 3, 'Expected 3 validation issues'); assert.deepStrictEqual( markers.map(m => ({ severity: m.severity, message: m.message })), [ { severity: MarkerSeverity.Error, message: "The 'description' attribute should not be empty." }, - { severity: MarkerSeverity.Warning, message: "Unknown tool 'tool3'." }, + { severity: MarkerSeverity.Error, message: "Unknown tool 'tool4'." }, { severity: MarkerSeverity.Warning, message: "Unknown model 'MAE 4.2'." }, ] ); @@ -143,8 +153,28 @@ suite('PromptValidator', () => { "---", ].join('\n'); const markers = await validate(content, PromptsType.mode); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].message, "Each tool name in the 'tools' attribute must be a string."); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: "Each tool name in the 'tools' attribute must be a string." }, + ] + ); + }); + + test('old tool reference', async () => { + const content = [ + "---", + "description: \"Test\"", + "tools: ['tool1', 'tool3']", + "---", + ].join('\n'); + const markers = await validate(content, PromptsType.mode); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Warning, message: "Tool or toolset 'tool3' is deprecated, use 'my.extension/tool3' instead." }, + ] + ); }); test('unknown attribute in mode file', async () => { @@ -327,7 +357,7 @@ suite('PromptValidator', () => { ].join('\n'); const markers = await validate(content, PromptsType.prompt); assert.strictEqual(markers.length, 1, 'Expected one warning for unknown tool variable'); - assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); assert.strictEqual(markers[0].message, "Unknown tool or toolset 'toolX'."); }); diff --git a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts index 2d33f4a1169..c795fc769c9 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts @@ -82,10 +82,6 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService throw new Error('Method not implemented.'); } - toToolEnablementMap(toolOrToolSetNames: Set): Record { - throw new Error('Method not implemented.'); - } - toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap { throw new Error('Method not implemented.'); } @@ -93,4 +89,24 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] { throw new Error('Method not implemented.'); } + + getQualifiedToolNames(): Iterable { + throw new Error('Method not implemented.'); + } + + getToolByQualifiedName(qualifiedName: string): IToolData | ToolSet | undefined { + throw new Error('Method not implemented.'); + } + + getQualifiedToolName(tool: IToolData, set?: ToolSet): string { + throw new Error('Method not implemented.'); + } + + toQualifiedToolNames(map: IToolAndToolSetEnablementMap): string[] { + throw new Error('Method not implemented.'); + } + + getDeprecatedQualifiedToolNames(): Map { + throw new Error('Method not implemented.'); + } }