Path: blob/main/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js';6import { assertNever } from '../../../../base/common/assert.js';7import { RunOnceScheduler } from '../../../../base/common/async.js';8import { encodeBase64 } from '../../../../base/common/buffer.js';9import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';10import { Codicon } from '../../../../base/common/codicons.js';11import { toErrorMessage } from '../../../../base/common/errorMessage.js';12import { CancellationError, isCancellationError } from '../../../../base/common/errors.js';13import { Emitter, Event } from '../../../../base/common/event.js';14import { MarkdownString } from '../../../../base/common/htmlContent.js';15import { Iterable } from '../../../../base/common/iterator.js';16import { Lazy } from '../../../../base/common/lazy.js';17import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';18import { LRUCache } from '../../../../base/common/map.js';19import { IObservable, ObservableSet } from '../../../../base/common/observable.js';20import Severity from '../../../../base/common/severity.js';21import { ThemeIcon } from '../../../../base/common/themables.js';22import { localize, localize2 } from '../../../../nls.js';23import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';24import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';25import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';26import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';27import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';28import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';29import * as JSONContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';30import { ILogService } from '../../../../platform/log/common/log.js';31import { Registry } from '../../../../platform/registry/common/platform.js';32import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';33import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';34import { IExtensionService } from '../../../services/extensions/common/extensions.js';35import { ChatContextKeys } from '../common/chatContextKeys.js';36import { ChatModel } from '../common/chatModel.js';37import { IVariableReference } from '../common/chatModes.js';38import { ChatToolInvocation } from '../common/chatProgressTypes/chatToolInvocation.js';39import { ConfirmedReason, IChatService, ToolConfirmKind } from '../common/chatService.js';40import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../common/chatVariableEntries.js';41import { ChatConfiguration } from '../common/constants.js';42import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, stringifyPromptTsxPart, ToolDataSource, ToolSet, IToolAndToolSetEnablementMap } from '../common/languageModelToolsService.js';43import { getToolConfirmationAlert } from './chatAccessibilityProvider.js';4445const jsonSchemaRegistry = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);4647interface IToolEntry {48data: IToolData;49impl?: IToolImpl;50}5152interface ITrackedCall {53invocation?: ChatToolInvocation;54store: IDisposable;55}5657const enum AutoApproveStorageKeys {58GlobalAutoApproveOptIn = 'chat.tools.global.autoApprove.optIn'59}6061export const globalAutoApproveDescription = localize2(62{63key: 'autoApprove2.markdown',64comment: [65'{Locked=\'](https://github.com/features/codespaces)\'}',66'{Locked=\'](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)\'}',67'{Locked=\'](https://code.visualstudio.com/docs/copilot/security)\'}',68'{Locked=\'**\'}',69]70},71'Global auto approve also known as "YOLO mode" disables manual approval completely for _all tools in all workspaces_, allowing the agent to act fully autonomously. This is extremely dangerous and is *never* recommended, even containerized environments like [Codespaces](https://github.com/features/codespaces) and [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) have user keys forwarded into the container that could be compromised.\n\n**This feature disables [critical security protections](https://code.visualstudio.com/docs/copilot/security) and makes it much easier for an attacker to compromise the machine.**'72);7374export class LanguageModelToolsService extends Disposable implements ILanguageModelToolsService {75_serviceBrand: undefined;7677private _onDidChangeTools = new Emitter<void>();78readonly onDidChangeTools = this._onDidChangeTools.event;7980/** Throttle tools updates because it sends all tools and runs on context key updates */81private _onDidChangeToolsScheduler = new RunOnceScheduler(() => this._onDidChangeTools.fire(), 750);8283private _tools = new Map<string, IToolEntry>();84private _toolContextKeys = new Set<string>();85private readonly _ctxToolsCount: IContextKey<number>;8687private _callsByRequestId = new Map<string, ITrackedCall[]>();8889private _workspaceToolConfirmStore: Lazy<ToolConfirmStore>;90private _profileToolConfirmStore: Lazy<ToolConfirmStore>;91private _memoryToolConfirmStore = new Set<string>();9293constructor(94@IInstantiationService private readonly _instantiationService: IInstantiationService,95@IExtensionService private readonly _extensionService: IExtensionService,96@IContextKeyService private readonly _contextKeyService: IContextKeyService,97@IChatService private readonly _chatService: IChatService,98@IDialogService private readonly _dialogService: IDialogService,99@ITelemetryService private readonly _telemetryService: ITelemetryService,100@ILogService private readonly _logService: ILogService,101@IConfigurationService private readonly _configurationService: IConfigurationService,102@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,103@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,104@IStorageService private readonly _storageService: IStorageService,105) {106super();107108this._workspaceToolConfirmStore = new Lazy(() => this._register(this._instantiationService.createInstance(ToolConfirmStore, StorageScope.WORKSPACE)));109this._profileToolConfirmStore = new Lazy(() => this._register(this._instantiationService.createInstance(ToolConfirmStore, StorageScope.PROFILE)));110111this._register(this._contextKeyService.onDidChangeContext(e => {112if (e.affectsSome(this._toolContextKeys)) {113// Not worth it to compute a delta here unless we have many tools changing often114this._onDidChangeToolsScheduler.schedule();115}116}));117118this._register(this._configurationService.onDidChangeConfiguration(e => {119if (e.affectsConfiguration(ChatConfiguration.ExtensionToolsEnabled)) {120this._onDidChangeToolsScheduler.schedule();121}122}));123124// Clear out warning accepted state if the setting is disabled125this._register(Event.runAndSubscribe(this._configurationService.onDidChangeConfiguration, e => {126if (!e || e.affectsConfiguration(ChatConfiguration.GlobalAutoApprove)) {127if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) !== true) {128this._storageService.remove(AutoApproveStorageKeys.GlobalAutoApproveOptIn, StorageScope.APPLICATION);129}130}131}));132133this._ctxToolsCount = ChatContextKeys.Tools.toolsCount.bindTo(_contextKeyService);134}135override dispose(): void {136super.dispose();137138this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose()));139this._ctxToolsCount.reset();140}141142registerToolData(toolData: IToolData): IDisposable {143if (this._tools.has(toolData.id)) {144throw new Error(`Tool "${toolData.id}" is already registered.`);145}146147this._tools.set(toolData.id, { data: toolData });148this._ctxToolsCount.set(this._tools.size);149this._onDidChangeToolsScheduler.schedule();150151toolData.when?.keys().forEach(key => this._toolContextKeys.add(key));152153let store: DisposableStore | undefined;154if (toolData.inputSchema) {155store = new DisposableStore();156const schemaUrl = createToolSchemaUri(toolData.id).toString();157jsonSchemaRegistry.registerSchema(schemaUrl, toolData.inputSchema, store);158store.add(jsonSchemaRegistry.registerSchemaAssociation(schemaUrl, `/lm/tool/${toolData.id}/tool_input.json`));159}160161return toDisposable(() => {162store?.dispose();163this._tools.delete(toolData.id);164this._ctxToolsCount.set(this._tools.size);165this._refreshAllToolContextKeys();166this._onDidChangeToolsScheduler.schedule();167});168}169170private _refreshAllToolContextKeys() {171this._toolContextKeys.clear();172for (const tool of this._tools.values()) {173tool.data.when?.keys().forEach(key => this._toolContextKeys.add(key));174}175}176177registerToolImplementation(id: string, tool: IToolImpl): IDisposable {178const entry = this._tools.get(id);179if (!entry) {180throw new Error(`Tool "${id}" was not contributed.`);181}182183if (entry.impl) {184throw new Error(`Tool "${id}" already has an implementation.`);185}186187entry.impl = tool;188return toDisposable(() => {189entry.impl = undefined;190});191}192193registerTool(toolData: IToolData, tool: IToolImpl): IDisposable {194return combinedDisposable(195this.registerToolData(toolData),196this.registerToolImplementation(toolData.id, tool)197);198}199200getTools(includeDisabled?: boolean): Iterable<Readonly<IToolData>> {201const toolDatas = Iterable.map(this._tools.values(), i => i.data);202const extensionToolsEnabled = this._configurationService.getValue<boolean>(ChatConfiguration.ExtensionToolsEnabled);203return Iterable.filter(204toolDatas,205toolData => {206const satisfiesWhenClause = includeDisabled || !toolData.when || this._contextKeyService.contextMatchesRules(toolData.when);207const satisfiesExternalToolCheck = toolData.source.type !== 'extension' || !!extensionToolsEnabled;208return satisfiesWhenClause && satisfiesExternalToolCheck;209});210}211212getTool(id: string): IToolData | undefined {213return this._getToolEntry(id)?.data;214}215216private _getToolEntry(id: string): IToolEntry | undefined {217const entry = this._tools.get(id);218if (entry && (!entry.data.when || this._contextKeyService.contextMatchesRules(entry.data.when))) {219return entry;220} else {221return undefined;222}223}224225getToolByName(name: string, includeDisabled?: boolean): IToolData | undefined {226for (const tool of this.getTools(!!includeDisabled)) {227if (tool.toolReferenceName === name) {228return tool;229}230}231return undefined;232}233234setToolAutoConfirmation(toolId: string, scope: 'workspace' | 'profile' | 'session' | 'never'): void {235this._workspaceToolConfirmStore.value.setAutoConfirm(toolId, scope === 'workspace');236this._profileToolConfirmStore.value.setAutoConfirm(toolId, scope === 'profile');237238if (scope === 'session') {239this._memoryToolConfirmStore.add(toolId);240} else {241this._memoryToolConfirmStore.delete(toolId);242}243}244245getToolAutoConfirmation(toolId: string): 'workspace' | 'profile' | 'session' | 'never' {246if (this._workspaceToolConfirmStore.value.getAutoConfirm(toolId)) {247return 'workspace';248}249if (this._profileToolConfirmStore.value.getAutoConfirm(toolId)) {250return 'profile';251}252if (this._memoryToolConfirmStore.has(toolId)) {253return 'session';254}255return 'never';256}257258resetToolAutoConfirmation(): void {259this._workspaceToolConfirmStore.value.reset();260this._profileToolConfirmStore.value.reset();261this._memoryToolConfirmStore.clear();262}263264async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise<IToolResult> {265this._logService.trace(`[LanguageModelToolsService#invokeTool] Invoking tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}`);266267// When invoking a tool, don't validate the "when" clause. An extension may have invoked a tool just as it was becoming disabled, and just let it go through rather than throw and break the chat.268let tool = this._tools.get(dto.toolId);269if (!tool) {270throw new Error(`Tool ${dto.toolId} was not contributed`);271}272273if (!tool.impl) {274await this._extensionService.activateByEvent(`onLanguageModelTool:${dto.toolId}`);275276// Extension should activate and register the tool implementation277tool = this._tools.get(dto.toolId);278if (!tool?.impl) {279throw new Error(`Tool ${dto.toolId} does not have an implementation registered.`);280}281}282283// Shortcut to write to the model directly here, but could call all the way back to use the real stream.284let toolInvocation: ChatToolInvocation | undefined;285286let requestId: string | undefined;287let store: DisposableStore | undefined;288let toolResult: IToolResult | undefined;289try {290if (dto.context) {291store = new DisposableStore();292const model = this._chatService.getSession(dto.context?.sessionId) as ChatModel | undefined;293if (!model) {294throw new Error(`Tool called for unknown chat session`);295}296297const request = model.getRequests().at(-1)!;298requestId = request.id;299dto.modelId = request.modelId;300301// Replace the token with a new token that we can cancel when cancelToolCallsForRequest is called302if (!this._callsByRequestId.has(requestId)) {303this._callsByRequestId.set(requestId, []);304}305const trackedCall: ITrackedCall = { store };306this._callsByRequestId.get(requestId)!.push(trackedCall);307308const source = new CancellationTokenSource();309store.add(toDisposable(() => {310source.dispose(true);311}));312store.add(token.onCancellationRequested(() => {313toolInvocation?.confirmed.complete({ type: ToolConfirmKind.Denied });314source.cancel();315}));316store.add(source.token.onCancellationRequested(() => {317toolInvocation?.confirmed.complete({ type: ToolConfirmKind.Denied });318}));319token = source.token;320321const prepared = await this.prepareToolInvocation(tool, dto, token);322toolInvocation = new ChatToolInvocation(prepared, tool.data, dto.callId);323trackedCall.invocation = toolInvocation;324const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace);325if (autoConfirmed) {326toolInvocation.confirmed.complete(autoConfirmed);327}328329model.acceptResponseProgress(request, toolInvocation);330331dto.toolSpecificData = toolInvocation?.toolSpecificData;332333if (prepared?.confirmationMessages) {334if (!toolInvocation.isConfirmed?.type && !autoConfirmed) {335this.playAccessibilitySignal([toolInvocation]);336}337const userConfirmed = await toolInvocation.confirmed.p;338if (userConfirmed.type === ToolConfirmKind.Denied) {339throw new CancellationError();340}341if (userConfirmed.type === ToolConfirmKind.Skipped) {342toolResult = {343content: [{344kind: 'text',345value: 'The user chose to skip the tool call, they want to proceed without running it'346}]347};348return toolResult;349}350351if (dto.toolSpecificData?.kind === 'input') {352dto.parameters = dto.toolSpecificData.rawInput;353dto.toolSpecificData = undefined;354}355}356} else {357const prepared = await this.prepareToolInvocation(tool, dto, token);358if (prepared?.confirmationMessages && !(await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace))) {359const result = await this._dialogService.confirm({ message: renderAsPlaintext(prepared.confirmationMessages.title), detail: renderAsPlaintext(prepared.confirmationMessages.message) });360if (!result.confirmed) {361throw new CancellationError();362}363}364365dto.toolSpecificData = prepared?.toolSpecificData;366}367368if (token.isCancellationRequested) {369throw new CancellationError();370}371372toolResult = await tool.impl.invoke(dto, countTokens, {373report: step => {374toolInvocation?.acceptProgress(step);375}376}, token);377this.ensureToolDetails(dto, toolResult, tool.data);378379this._telemetryService.publicLog2<LanguageModelToolInvokedEvent, LanguageModelToolInvokedClassification>(380'languageModelToolInvoked',381{382result: 'success',383chatSessionId: dto.context?.sessionId,384toolId: tool.data.id,385toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined,386toolSourceKind: tool.data.source.type,387});388return toolResult;389} catch (err) {390const result = isCancellationError(err) ? 'userCancelled' : 'error';391this._telemetryService.publicLog2<LanguageModelToolInvokedEvent, LanguageModelToolInvokedClassification>(392'languageModelToolInvoked',393{394result,395chatSessionId: dto.context?.sessionId,396toolId: tool.data.id,397toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined,398toolSourceKind: tool.data.source.type,399});400this._logService.error(`[LanguageModelToolsService#invokeTool] Error from tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}:\n${toErrorMessage(err, true)}`);401402toolResult ??= { content: [] };403toolResult.toolResultError = err instanceof Error ? err.message : String(err);404if (tool.data.alwaysDisplayInputOutput) {405toolResult.toolResultDetails = { input: this.formatToolInput(dto), output: [{ type: 'embed', isText: true, value: String(err) }], isError: true };406}407408throw err;409} finally {410toolInvocation?.complete(toolResult);411412if (store) {413this.cleanupCallDisposables(requestId, store);414}415}416}417418private async prepareToolInvocation(tool: IToolEntry, dto: IToolInvocation, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {419const prepared = tool.impl!.prepareToolInvocation ?420await tool.impl!.prepareToolInvocation({421parameters: dto.parameters,422chatRequestId: dto.chatRequestId,423chatSessionId: dto.context?.sessionId,424chatInteractionId: dto.chatInteractionId425}, token)426: undefined;427428if (prepared?.confirmationMessages) {429if (prepared.toolSpecificData?.kind !== 'terminal' && typeof prepared.confirmationMessages.allowAutoConfirm !== 'boolean') {430prepared.confirmationMessages.allowAutoConfirm = true;431}432433if (!prepared.toolSpecificData && tool.data.alwaysDisplayInputOutput) {434prepared.toolSpecificData = {435kind: 'input',436rawInput: dto.parameters,437};438}439}440441return prepared;442}443444private playAccessibilitySignal(toolInvocations: ChatToolInvocation[]): void {445const autoApproved = this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove);446if (autoApproved) {447return;448}449const setting: { sound?: 'auto' | 'on' | 'off'; announcement?: 'auto' | 'off' } | undefined = this._configurationService.getValue(AccessibilitySignal.chatUserActionRequired.settingsKey);450if (!setting) {451return;452}453const soundEnabled = setting.sound === 'on' || (setting.sound === 'auto' && (this._accessibilityService.isScreenReaderOptimized()));454const announcementEnabled = this._accessibilityService.isScreenReaderOptimized() && setting.announcement === 'auto';455if (soundEnabled || announcementEnabled) {456this._accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired, { customAlertMessage: this._instantiationService.invokeFunction(getToolConfirmationAlert, toolInvocations), userGesture: true, modality: !soundEnabled ? 'announcement' : undefined });457}458}459460private ensureToolDetails(dto: IToolInvocation, toolResult: IToolResult, toolData: IToolData): void {461if (!toolResult.toolResultDetails && toolData.alwaysDisplayInputOutput) {462toolResult.toolResultDetails = {463input: this.formatToolInput(dto),464output: this.toolResultToIO(toolResult),465};466}467}468469private formatToolInput(dto: IToolInvocation): string {470return JSON.stringify(dto.parameters, undefined, 2);471}472473private toolResultToIO(toolResult: IToolResult): IToolResultInputOutputDetails['output'] {474return toolResult.content.map(part => {475if (part.kind === 'text') {476return { type: 'embed', isText: true, value: part.value };477} else if (part.kind === 'promptTsx') {478return { type: 'embed', isText: true, value: stringifyPromptTsxPart(part) };479} else if (part.kind === 'data') {480return { type: 'embed', value: encodeBase64(part.value.data), mimeType: part.value.mimeType };481} else {482assertNever(part);483}484});485}486487private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined): Promise<ConfirmedReason | undefined> {488if (this._workspaceToolConfirmStore.value.getAutoConfirm(toolId)) {489return { type: ToolConfirmKind.LmServicePerTool, scope: 'workspace' };490}491if (this._profileToolConfirmStore.value.getAutoConfirm(toolId)) {492return { type: ToolConfirmKind.LmServicePerTool, scope: 'profile' };493}494if (this._memoryToolConfirmStore.has(toolId)) {495return { type: ToolConfirmKind.LmServicePerTool, scope: 'session' };496}497498const config = this._configurationService.inspect<boolean | Record<string, boolean>>(ChatConfiguration.GlobalAutoApprove);499500// If we know the tool runs at a global level, only consider the global config.501// If we know the tool runs at a workspace level, use those specific settings when appropriate.502let value = config.value ?? config.defaultValue;503if (typeof runsInWorkspace === 'boolean') {504value = config.userLocalValue ?? config.applicationValue;505if (runsInWorkspace) {506value = config.workspaceValue ?? config.workspaceFolderValue ?? config.userRemoteValue ?? value;507}508}509510const autoConfirm = value === true || (typeof value === 'object' && value.hasOwnProperty(toolId) && value[toolId] === true);511if (autoConfirm) {512if (await this._checkGlobalAutoApprove()) {513return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove };514}515}516517return undefined;518}519520private async _checkGlobalAutoApprove(): Promise<boolean> {521const optedIn = this._storageService.getBoolean(AutoApproveStorageKeys.GlobalAutoApproveOptIn, StorageScope.APPLICATION, false);522if (optedIn) {523return true;524}525526const promptResult = await this._dialogService.prompt({527type: Severity.Warning,528message: localize('autoApprove2.title', 'Enable global auto approve?'),529buttons: [530{531label: localize('autoApprove2.button.enable', 'Enable'),532run: () => true533},534{535label: localize('autoApprove2.button.disable', 'Disable'),536run: () => false537},538],539custom: {540icon: Codicon.warning,541disableCloseAction: true,542markdownDetails: [{543markdown: new MarkdownString(globalAutoApproveDescription.value),544}],545}546});547548if (promptResult.result !== true) {549await this._configurationService.updateValue(ChatConfiguration.GlobalAutoApprove, false);550return false;551}552553this._storageService.store(AutoApproveStorageKeys.GlobalAutoApproveOptIn, true, StorageScope.APPLICATION, StorageTarget.USER);554return true;555}556557private cleanupCallDisposables(requestId: string | undefined, store: DisposableStore): void {558if (requestId) {559const disposables = this._callsByRequestId.get(requestId);560if (disposables) {561const index = disposables.findIndex(d => d.store === store);562if (index > -1) {563disposables.splice(index, 1);564}565if (disposables.length === 0) {566this._callsByRequestId.delete(requestId);567}568}569}570571store.dispose();572}573574cancelToolCallsForRequest(requestId: string): void {575const calls = this._callsByRequestId.get(requestId);576if (calls) {577calls.forEach(call => call.store.dispose());578this._callsByRequestId.delete(requestId);579}580}581582toToolEnablementMap(toolOrToolsetNames: Set<string>): Record<string, boolean> {583const result: Record<string, boolean> = {};584for (const tool of this._tools.values()) {585if (tool.data.toolReferenceName && toolOrToolsetNames.has(tool.data.toolReferenceName)) {586result[tool.data.id] = true;587} else {588result[tool.data.id] = false;589}590}591592for (const toolSet of this._toolSets) {593if (toolOrToolsetNames.has(toolSet.referenceName)) {594for (const tool of toolSet.getTools()) {595result[tool.id] = true;596}597}598}599600return result;601}602603/**604* Create a map that contains all tools and toolsets with their enablement state.605* @param toolOrToolSetNames A list of tool or toolset names that are enabled.606* @returns A map of tool or toolset instances to their enablement state.607*/608toToolAndToolSetEnablementMap(enabledToolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap {609const toolOrToolSetNames = new Set(enabledToolOrToolSetNames);610const result = new Map<ToolSet | IToolData, boolean>();611for (const tool of this.getTools()) {612if (tool.canBeReferencedInPrompt) {613result.set(tool, toolOrToolSetNames.has(tool.toolReferenceName ?? tool.displayName));614}615}616for (const toolSet of this._toolSets) {617const enabled = toolOrToolSetNames.has(toolSet.referenceName);618result.set(toolSet, enabled);619for (const tool of toolSet.getTools()) {620result.set(tool, enabled || toolOrToolSetNames?.has(tool.toolReferenceName ?? tool.displayName));621}622623}624return result;625}626627public toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] {628const toolsOrToolSetByName = new Map<string, ToolSet | IToolData>();629for (const toolSet of this.toolSets.get()) {630toolsOrToolSetByName.set(toolSet.referenceName, toolSet);631}632for (const tool of this.getTools()) {633toolsOrToolSetByName.set(tool.toolReferenceName ?? tool.displayName, tool);634}635636const result: ChatRequestToolReferenceEntry[] = [];637for (const ref of variableReferences) {638const toolOrToolSet = toolsOrToolSetByName.get(ref.name);639if (toolOrToolSet) {640if (toolOrToolSet instanceof ToolSet) {641result.push(toToolSetVariableEntry(toolOrToolSet, ref.range));642} else {643result.push(toToolVariableEntry(toolOrToolSet, ref.range));644}645}646}647return result;648}649650651private readonly _toolSets = new ObservableSet<ToolSet>();652653readonly toolSets: IObservable<Iterable<ToolSet>> = this._toolSets.observable;654655getToolSet(id: string): ToolSet | undefined {656for (const toolSet of this._toolSets) {657if (toolSet.id === id) {658return toolSet;659}660}661return undefined;662}663664getToolSetByName(name: string): ToolSet | undefined {665for (const toolSet of this._toolSets) {666if (toolSet.referenceName === name) {667return toolSet;668}669}670return undefined;671}672673createToolSet(source: ToolDataSource, id: string, referenceName: string, options?: { icon?: ThemeIcon; description?: string }): ToolSet & IDisposable {674675const that = this;676677const result = new class extends ToolSet implements IDisposable {678dispose(): void {679if (that._toolSets.has(result)) {680this._tools.clear();681that._toolSets.delete(result);682}683684}685}(id, referenceName, options?.icon ?? Codicon.tools, source, options?.description);686687this._toolSets.add(result);688return result;689}690}691692type LanguageModelToolInvokedEvent = {693result: 'success' | 'error' | 'userCancelled';694chatSessionId: string | undefined;695toolId: string;696toolExtensionId: string | undefined;697toolSourceKind: string;698};699700type LanguageModelToolInvokedClassification = {701result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether invoking the LanguageModelTool resulted in an error.' };702chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat session that the tool was used within, if applicable.' };703toolId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the tool used.' };704toolExtensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that contributed the tool.' };705toolSourceKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source (mcp/extension/internal) of the tool.' };706owner: 'roblourens';707comment: 'Provides insight into the usage of language model tools.';708};709710class ToolConfirmStore extends Disposable {711private static readonly STORED_KEY = 'chat/autoconfirm';712713private _autoConfirmTools: LRUCache<string, boolean> = new LRUCache<string, boolean>(100);714private _didChange = false;715716constructor(717private readonly _scope: StorageScope,718@IStorageService private readonly storageService: IStorageService,719) {720super();721722const stored = storageService.getObject<string[]>(ToolConfirmStore.STORED_KEY, this._scope);723if (stored) {724for (const key of stored) {725this._autoConfirmTools.set(key, true);726}727}728729this._register(storageService.onWillSaveState(() => {730if (this._didChange) {731this.storageService.store(ToolConfirmStore.STORED_KEY, [...this._autoConfirmTools.keys()], this._scope, StorageTarget.MACHINE);732this._didChange = false;733}734}));735}736737public reset() {738this._autoConfirmTools.clear();739this._didChange = true;740}741742public getAutoConfirm(toolId: string): boolean {743if (this._autoConfirmTools.get(toolId)) {744this._didChange = true;745return true;746}747748return false;749}750751public setAutoConfirm(toolId: string, autoConfirm: boolean): void {752if (autoConfirm) {753this._autoConfirmTools.set(toolId, true);754} else {755this._autoConfirmTools.delete(toolId);756}757this._didChange = true;758}759}760761762