Path: blob/main/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts
4780 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, timeout } 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 { arrayEqualsC } from '../../../../../base/common/equals.js';12import { toErrorMessage } from '../../../../../base/common/errorMessage.js';13import { CancellationError, isCancellationError } from '../../../../../base/common/errors.js';14import { Emitter, Event } from '../../../../../base/common/event.js';15import { createMarkdownCommandLink, MarkdownString } from '../../../../../base/common/htmlContent.js';16import { Iterable } from '../../../../../base/common/iterator.js';17import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';18import { derived, IObservable, IReader, observableFromEventOpts, ObservableSet } from '../../../../../base/common/observable.js';19import Severity from '../../../../../base/common/severity.js';20import { StopWatch } from '../../../../../base/common/stopwatch.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 { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';32import { Registry } from '../../../../../platform/registry/common/platform.js';33import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';34import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';35import { IExtensionService } from '../../../../services/extensions/common/extensions.js';36import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';37import { IVariableReference } from '../../common/chatModes.js';38import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToolInvocation.js';39import { ConfirmedReason, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js';40import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js';41import { ChatConfiguration } from '../../common/constants.js';42import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js';43import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, ToolSet, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js';44import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js';4546const jsonSchemaRegistry = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);4748interface IToolEntry {49data: IToolData;50impl?: IToolImpl;51}5253interface ITrackedCall {54invocation?: ChatToolInvocation;55store: IDisposable;56}5758const enum AutoApproveStorageKeys {59GlobalAutoApproveOptIn = 'chat.tools.global.autoApprove.optIn'60}6162const SkipAutoApproveConfirmationKey = 'vscode.chat.tools.global.autoApprove.testMode';6364export const globalAutoApproveDescription = localize2(65{66key: 'autoApprove2.markdown',67comment: [68'{Locked=\'](https://github.com/features/codespaces)\'}',69'{Locked=\'](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)\'}',70'{Locked=\'](https://code.visualstudio.com/docs/copilot/security)\'}',71'{Locked=\'**\'}',72]73},74'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.**'75);7677export class LanguageModelToolsService extends Disposable implements ILanguageModelToolsService {78_serviceBrand: undefined;79readonly vscodeToolSet: ToolSet;80readonly executeToolSet: ToolSet;81readonly readToolSet: ToolSet;8283private readonly _onDidChangeTools = this._register(new Emitter<void>());84readonly onDidChangeTools = this._onDidChangeTools.event;85private readonly _onDidPrepareToolCallBecomeUnresponsive = this._register(new Emitter<{ sessionId: string; toolData: IToolData }>());86readonly onDidPrepareToolCallBecomeUnresponsive = this._onDidPrepareToolCallBecomeUnresponsive.event;8788/** Throttle tools updates because it sends all tools and runs on context key updates */89private readonly _onDidChangeToolsScheduler = new RunOnceScheduler(() => this._onDidChangeTools.fire(), 750);90private readonly _tools = new Map<string, IToolEntry>();91private readonly _toolContextKeys = new Set<string>();92private readonly _ctxToolsCount: IContextKey<number>;9394private readonly _callsByRequestId = new Map<string, ITrackedCall[]>();9596private readonly _isAgentModeEnabled: IObservable<boolean>;9798constructor(99@IInstantiationService private readonly _instantiationService: IInstantiationService,100@IExtensionService private readonly _extensionService: IExtensionService,101@IContextKeyService private readonly _contextKeyService: IContextKeyService,102@IChatService private readonly _chatService: IChatService,103@IDialogService private readonly _dialogService: IDialogService,104@ITelemetryService private readonly _telemetryService: ITelemetryService,105@ILogService private readonly _logService: ILogService,106@IConfigurationService private readonly _configurationService: IConfigurationService,107@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,108@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,109@IStorageService private readonly _storageService: IStorageService,110@ILanguageModelToolsConfirmationService private readonly _confirmationService: ILanguageModelToolsConfirmationService,111) {112super();113114this._isAgentModeEnabled = observableConfigValue(ChatConfiguration.AgentEnabled, true, this._configurationService);115116this._register(this._contextKeyService.onDidChangeContext(e => {117if (e.affectsSome(this._toolContextKeys)) {118// Not worth it to compute a delta here unless we have many tools changing often119this._onDidChangeToolsScheduler.schedule();120}121}));122123this._register(this._configurationService.onDidChangeConfiguration(e => {124if (e.affectsConfiguration(ChatConfiguration.ExtensionToolsEnabled) || e.affectsConfiguration(ChatConfiguration.AgentEnabled)) {125this._onDidChangeToolsScheduler.schedule();126}127}));128129// Clear out warning accepted state if the setting is disabled130this._register(Event.runAndSubscribe(this._configurationService.onDidChangeConfiguration, e => {131if (!e || e.affectsConfiguration(ChatConfiguration.GlobalAutoApprove)) {132if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) !== true) {133this._storageService.remove(AutoApproveStorageKeys.GlobalAutoApproveOptIn, StorageScope.APPLICATION);134}135}136}));137138this._ctxToolsCount = ChatContextKeys.Tools.toolsCount.bindTo(_contextKeyService);139140// Create the internal VS Code tool set141this.vscodeToolSet = this._register(this.createToolSet(142ToolDataSource.Internal,143'vscode',144VSCodeToolReference.vscode,145{146icon: ThemeIcon.fromId(Codicon.vscode.id),147description: localize('copilot.toolSet.vscode.description', 'Use VS Code features'),148}149));150151// Create the internal Execute tool set152this.executeToolSet = this._register(this.createToolSet(153ToolDataSource.Internal,154'execute',155SpecedToolAliases.execute,156{157icon: ThemeIcon.fromId(Codicon.terminal.id),158description: localize('copilot.toolSet.execute.description', 'Execute code and applications on your machine'),159}160));161162// Create the internal Read tool set163this.readToolSet = this._register(this.createToolSet(164ToolDataSource.Internal,165'read',166SpecedToolAliases.read,167{168icon: ThemeIcon.fromId(Codicon.eye.id),169description: localize('copilot.toolSet.read.description', 'Read files in your workspace'),170}171));172}173174/**175* Returns if the given tool or toolset is permitted in the current context.176* When agent mode is enabled, all tools are permitted (no restriction)177* When agent mode is disabled only a subset of read-only tools are permitted in agentic-loop contexts.178*/179private isPermitted(toolOrToolSet: IToolData | ToolSet, reader?: IReader): boolean {180const agentModeEnabled = this._isAgentModeEnabled.read(reader);181if (agentModeEnabled !== false) {182return true;183}184const permittedInternalToolSetIds = [SpecedToolAliases.read, SpecedToolAliases.search, SpecedToolAliases.web];185if (toolOrToolSet instanceof ToolSet) {186const permitted = toolOrToolSet.source.type === 'internal' && permittedInternalToolSetIds.includes(toolOrToolSet.referenceName);187this._logService.trace(`LanguageModelToolsService#isPermitted: ToolSet ${toolOrToolSet.id} (${toolOrToolSet.referenceName}) permitted=${permitted}`);188return permitted;189}190this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=false`);191return false;192}193194override dispose(): void {195super.dispose();196197this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose()));198this._ctxToolsCount.reset();199}200201registerToolData(toolData: IToolData): IDisposable {202if (this._tools.has(toolData.id)) {203throw new Error(`Tool "${toolData.id}" is already registered.`);204}205206this._tools.set(toolData.id, { data: toolData });207this._ctxToolsCount.set(this._tools.size);208this._onDidChangeToolsScheduler.schedule();209210toolData.when?.keys().forEach(key => this._toolContextKeys.add(key));211212let store: DisposableStore | undefined;213if (toolData.inputSchema) {214store = new DisposableStore();215const schemaUrl = createToolSchemaUri(toolData.id).toString();216jsonSchemaRegistry.registerSchema(schemaUrl, toolData.inputSchema, store);217store.add(jsonSchemaRegistry.registerSchemaAssociation(schemaUrl, `/lm/tool/${toolData.id}/tool_input.json`));218}219220return toDisposable(() => {221store?.dispose();222this._tools.delete(toolData.id);223this._ctxToolsCount.set(this._tools.size);224this._refreshAllToolContextKeys();225this._onDidChangeToolsScheduler.schedule();226});227}228229flushToolUpdates(): void {230this._onDidChangeToolsScheduler.flush();231}232233private _refreshAllToolContextKeys() {234this._toolContextKeys.clear();235for (const tool of this._tools.values()) {236tool.data.when?.keys().forEach(key => this._toolContextKeys.add(key));237}238}239240registerToolImplementation(id: string, tool: IToolImpl): IDisposable {241const entry = this._tools.get(id);242if (!entry) {243throw new Error(`Tool "${id}" was not contributed.`);244}245246if (entry.impl) {247throw new Error(`Tool "${id}" already has an implementation.`);248}249250entry.impl = tool;251return toDisposable(() => {252entry.impl = undefined;253});254}255256registerTool(toolData: IToolData, tool: IToolImpl): IDisposable {257return combinedDisposable(258this.registerToolData(toolData),259this.registerToolImplementation(toolData.id, tool)260);261}262263getTools(includeDisabled?: boolean): Iterable<IToolData> {264const toolDatas = Iterable.map(this._tools.values(), i => i.data);265const extensionToolsEnabled = this._configurationService.getValue<boolean>(ChatConfiguration.ExtensionToolsEnabled);266return Iterable.filter(267toolDatas,268toolData => {269const satisfiesWhenClause = includeDisabled || !toolData.when || this._contextKeyService.contextMatchesRules(toolData.when);270const satisfiesExternalToolCheck = toolData.source.type !== 'extension' || !!extensionToolsEnabled;271const satisfiesPermittedCheck = includeDisabled || this.isPermitted(toolData);272return satisfiesWhenClause && satisfiesExternalToolCheck && satisfiesPermittedCheck;273});274}275276readonly toolsObservable = observableFromEventOpts<readonly IToolData[], void>({ equalsFn: arrayEqualsC() }, this.onDidChangeTools, () => Array.from(this.getTools()));277278getTool(id: string): IToolData | undefined {279return this._getToolEntry(id)?.data;280}281282private _getToolEntry(id: string): IToolEntry | undefined {283const entry = this._tools.get(id);284if (entry && (!entry.data.when || this._contextKeyService.contextMatchesRules(entry.data.when))) {285return entry;286} else {287return undefined;288}289}290291getToolByName(name: string, includeDisabled?: boolean): IToolData | undefined {292for (const tool of this.getTools(!!includeDisabled)) {293if (tool.toolReferenceName === name) {294return tool;295}296}297return undefined;298}299300async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise<IToolResult> {301this._logService.trace(`[LanguageModelToolsService#invokeTool] Invoking tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}`);302303// 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.304let tool = this._tools.get(dto.toolId);305if (!tool) {306throw new Error(`Tool ${dto.toolId} was not contributed`);307}308309if (!tool.impl) {310await this._extensionService.activateByEvent(`onLanguageModelTool:${dto.toolId}`);311312// Extension should activate and register the tool implementation313tool = this._tools.get(dto.toolId);314if (!tool?.impl) {315throw new Error(`Tool ${dto.toolId} does not have an implementation registered.`);316}317}318319// Shortcut to write to the model directly here, but could call all the way back to use the real stream.320let toolInvocation: ChatToolInvocation | undefined;321322let requestId: string | undefined;323let store: DisposableStore | undefined;324let toolResult: IToolResult | undefined;325let prepareTimeWatch: StopWatch | undefined;326let invocationTimeWatch: StopWatch | undefined;327let preparedInvocation: IPreparedToolInvocation | undefined;328try {329if (dto.context) {330store = new DisposableStore();331const model = this._chatService.getSession(dto.context.sessionResource);332if (!model) {333throw new Error(`Tool called for unknown chat session`);334}335336const request = model.getRequests().at(-1)!;337requestId = request.id;338dto.modelId = request.modelId;339dto.userSelectedTools = request.userSelectedTools && { ...request.userSelectedTools };340341// Replace the token with a new token that we can cancel when cancelToolCallsForRequest is called342if (!this._callsByRequestId.has(requestId)) {343this._callsByRequestId.set(requestId, []);344}345const trackedCall: ITrackedCall = { store };346this._callsByRequestId.get(requestId)!.push(trackedCall);347348const source = new CancellationTokenSource();349store.add(toDisposable(() => {350source.dispose(true);351}));352store.add(token.onCancellationRequested(() => {353IChatToolInvocation.confirmWith(toolInvocation, { type: ToolConfirmKind.Denied });354source.cancel();355}));356store.add(source.token.onCancellationRequested(() => {357IChatToolInvocation.confirmWith(toolInvocation, { type: ToolConfirmKind.Denied });358}));359token = source.token;360361prepareTimeWatch = StopWatch.create(true);362preparedInvocation = await this.prepareToolInvocation(tool, dto, token);363prepareTimeWatch.stop();364365toolInvocation = new ChatToolInvocation(preparedInvocation, tool.data, dto.callId, dto.fromSubAgent, dto.parameters);366trackedCall.invocation = toolInvocation;367const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters);368if (autoConfirmed) {369IChatToolInvocation.confirmWith(toolInvocation, autoConfirmed);370}371372this._chatService.appendProgress(request, toolInvocation);373374dto.toolSpecificData = toolInvocation?.toolSpecificData;375if (preparedInvocation?.confirmationMessages?.title) {376if (!IChatToolInvocation.executionConfirmedOrDenied(toolInvocation) && !autoConfirmed) {377this.playAccessibilitySignal([toolInvocation]);378}379const userConfirmed = await IChatToolInvocation.awaitConfirmation(toolInvocation, token);380if (userConfirmed.type === ToolConfirmKind.Denied) {381throw new CancellationError();382}383if (userConfirmed.type === ToolConfirmKind.Skipped) {384toolResult = {385content: [{386kind: 'text',387value: 'The user chose to skip the tool call, they want to proceed without running it'388}]389};390return toolResult;391}392393if (dto.toolSpecificData?.kind === 'input') {394dto.parameters = dto.toolSpecificData.rawInput;395dto.toolSpecificData = undefined;396}397}398} else {399prepareTimeWatch = StopWatch.create(true);400preparedInvocation = await this.prepareToolInvocation(tool, dto, token);401prepareTimeWatch.stop();402if (preparedInvocation?.confirmationMessages?.title && !(await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters))) {403const result = await this._dialogService.confirm({ message: renderAsPlaintext(preparedInvocation.confirmationMessages.title), detail: renderAsPlaintext(preparedInvocation.confirmationMessages.message!) });404if (!result.confirmed) {405throw new CancellationError();406}407}408dto.toolSpecificData = preparedInvocation?.toolSpecificData;409}410411if (token.isCancellationRequested) {412throw new CancellationError();413}414415invocationTimeWatch = StopWatch.create(true);416toolResult = await tool.impl.invoke(dto, countTokens, {417report: step => {418toolInvocation?.acceptProgress(step);419}420}, token);421invocationTimeWatch.stop();422this.ensureToolDetails(dto, toolResult, tool.data);423424if (toolInvocation?.didExecuteTool(toolResult).type === IChatToolInvocation.StateKind.WaitingForPostApproval) {425const autoConfirmedPost = await this.shouldAutoConfirmPostExecution(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters);426if (autoConfirmedPost) {427IChatToolInvocation.confirmWith(toolInvocation, autoConfirmedPost);428}429430const postConfirm = await IChatToolInvocation.awaitPostConfirmation(toolInvocation, token);431if (postConfirm.type === ToolConfirmKind.Denied) {432throw new CancellationError();433}434if (postConfirm.type === ToolConfirmKind.Skipped) {435toolResult = {436content: [{437kind: 'text',438value: 'The tool executed but the user chose not to share the results'439}]440};441}442}443444this._telemetryService.publicLog2<LanguageModelToolInvokedEvent, LanguageModelToolInvokedClassification>(445'languageModelToolInvoked',446{447result: 'success',448chatSessionId: dto.context?.sessionId,449toolId: tool.data.id,450toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined,451toolSourceKind: tool.data.source.type,452prepareTimeMs: prepareTimeWatch?.elapsed(),453invocationTimeMs: invocationTimeWatch?.elapsed(),454});455return toolResult;456} catch (err) {457const result = isCancellationError(err) ? 'userCancelled' : 'error';458this._telemetryService.publicLog2<LanguageModelToolInvokedEvent, LanguageModelToolInvokedClassification>(459'languageModelToolInvoked',460{461result,462chatSessionId: dto.context?.sessionId,463toolId: tool.data.id,464toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined,465toolSourceKind: tool.data.source.type,466prepareTimeMs: prepareTimeWatch?.elapsed(),467invocationTimeMs: invocationTimeWatch?.elapsed(),468});469this._logService.error(`[LanguageModelToolsService#invokeTool] Error from tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}:\n${toErrorMessage(err, true)}`);470471toolResult ??= { content: [] };472toolResult.toolResultError = err instanceof Error ? err.message : String(err);473if (tool.data.alwaysDisplayInputOutput) {474toolResult.toolResultDetails = { input: this.formatToolInput(dto), output: [{ type: 'embed', isText: true, value: String(err) }], isError: true };475}476477throw err;478} finally {479toolInvocation?.didExecuteTool(toolResult, true);480if (store) {481this.cleanupCallDisposables(requestId, store);482}483}484}485486private async prepareToolInvocation(tool: IToolEntry, dto: IToolInvocation, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {487let prepared: IPreparedToolInvocation | undefined;488if (tool.impl!.prepareToolInvocation) {489const preparePromise = tool.impl!.prepareToolInvocation({490parameters: dto.parameters,491chatRequestId: dto.chatRequestId,492chatSessionId: dto.context?.sessionId,493chatSessionResource: dto.context?.sessionResource,494chatInteractionId: dto.chatInteractionId495}, token);496497const raceResult = await Promise.race([498timeout(3000, token).then(() => 'timeout'),499preparePromise500]);501if (raceResult === 'timeout') {502this._onDidPrepareToolCallBecomeUnresponsive.fire({503sessionId: dto.context?.sessionId ?? '',504toolData: tool.data505});506}507508prepared = await preparePromise;509}510511const isEligibleForAutoApproval = this.isToolEligibleForAutoApproval(tool.data);512513// Default confirmation messages if tool is not eligible for auto-approval514if (!isEligibleForAutoApproval && !prepared?.confirmationMessages?.title) {515if (!prepared) {516prepared = {};517}518const fullReferenceName = getToolFullReferenceName(tool.data);519520// TODO: This should be more detailed per tool.521prepared.confirmationMessages = {522...prepared.confirmationMessages,523title: localize('defaultToolConfirmation.title', 'Confirm tool execution'),524message: localize('defaultToolConfirmation.message', 'Run the \'{0}\' tool?', fullReferenceName),525disclaimer: new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ title: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval] }, false)), { isTrusted: true }),526allowAutoConfirm: false,527};528}529530if (!isEligibleForAutoApproval && prepared?.confirmationMessages?.title) {531// Always overwrite the disclaimer if not eligible for auto-approval532prepared.confirmationMessages.disclaimer = new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ title: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval] }, false)), { isTrusted: true });533}534535if (prepared?.confirmationMessages?.title) {536if (prepared.toolSpecificData?.kind !== 'terminal' && prepared.confirmationMessages.allowAutoConfirm !== false) {537prepared.confirmationMessages.allowAutoConfirm = isEligibleForAutoApproval;538}539540if (!prepared.toolSpecificData && tool.data.alwaysDisplayInputOutput) {541prepared.toolSpecificData = {542kind: 'input',543rawInput: dto.parameters,544};545}546}547548return prepared;549}550551private playAccessibilitySignal(toolInvocations: ChatToolInvocation[]): void {552const autoApproved = this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove);553if (autoApproved) {554return;555}556const setting: { sound?: 'auto' | 'on' | 'off'; announcement?: 'auto' | 'off' } | undefined = this._configurationService.getValue(AccessibilitySignal.chatUserActionRequired.settingsKey);557if (!setting) {558return;559}560const soundEnabled = setting.sound === 'on' || (setting.sound === 'auto' && (this._accessibilityService.isScreenReaderOptimized()));561const announcementEnabled = this._accessibilityService.isScreenReaderOptimized() && setting.announcement === 'auto';562if (soundEnabled || announcementEnabled) {563this._accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired, { customAlertMessage: this._instantiationService.invokeFunction(getToolConfirmationAlert, toolInvocations), userGesture: true, modality: !soundEnabled ? 'announcement' : undefined });564}565}566567private ensureToolDetails(dto: IToolInvocation, toolResult: IToolResult, toolData: IToolData): void {568if (!toolResult.toolResultDetails && toolData.alwaysDisplayInputOutput) {569toolResult.toolResultDetails = {570input: this.formatToolInput(dto),571output: this.toolResultToIO(toolResult),572};573}574}575576private formatToolInput(dto: IToolInvocation): string {577return JSON.stringify(dto.parameters, undefined, 2);578}579580private toolResultToIO(toolResult: IToolResult): IToolResultInputOutputDetails['output'] {581return toolResult.content.map(part => {582if (part.kind === 'text') {583return { type: 'embed', isText: true, value: part.value };584} else if (part.kind === 'promptTsx') {585return { type: 'embed', isText: true, value: stringifyPromptTsxPart(part) };586} else if (part.kind === 'data') {587return { type: 'embed', value: encodeBase64(part.value.data), mimeType: part.value.mimeType };588} else {589assertNever(part);590}591});592}593594private getEligibleForAutoApprovalSpecialCase(toolData: IToolData): string | undefined {595if (toolData.id === 'vscode_fetchWebPage_internal') {596return 'fetch';597}598return undefined;599}600601private isToolEligibleForAutoApproval(toolData: IToolData): boolean {602const fullReferenceName = this.getEligibleForAutoApprovalSpecialCase(toolData) ?? getToolFullReferenceName(toolData);603if (toolData.id === 'copilot_fetchWebPage') {604// Special case, this fetch will call an internal tool 'vscode_fetchWebPage_internal'605return true;606}607const eligibilityConfig = this._configurationService.getValue<Record<string, boolean>>(ChatConfiguration.EligibleForAutoApproval);608if (eligibilityConfig && typeof eligibilityConfig === 'object' && fullReferenceName) {609// Direct match610if (Object.prototype.hasOwnProperty.call(eligibilityConfig, fullReferenceName)) {611return eligibilityConfig[fullReferenceName];612}613// Back compat with legacy names614if (toolData.legacyToolReferenceFullNames) {615for (const legacyName of toolData.legacyToolReferenceFullNames) {616// Check if the full legacy name is in the config617if (Object.prototype.hasOwnProperty.call(eligibilityConfig, legacyName)) {618return eligibilityConfig[legacyName];619}620// Some tools may be both renamed and namespaced from a toolset, eg: xxx/yyy -> yyy621if (legacyName.includes('/')) {622const trimmedLegacyName = legacyName.split('/').pop();623if (trimmedLegacyName && Object.prototype.hasOwnProperty.call(eligibilityConfig, trimmedLegacyName)) {624return eligibilityConfig[trimmedLegacyName];625}626}627}628}629}630return true;631}632633private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown): Promise<ConfirmedReason | undefined> {634const tool = this._tools.get(toolId);635if (!tool) {636return undefined;637}638639if (!this.isToolEligibleForAutoApproval(tool.data)) {640return undefined;641}642643const reason = this._confirmationService.getPreConfirmAction({ toolId, source, parameters });644if (reason) {645return reason;646}647648const config = this._configurationService.inspect<boolean | Record<string, boolean>>(ChatConfiguration.GlobalAutoApprove);649650// If we know the tool runs at a global level, only consider the global config.651// If we know the tool runs at a workspace level, use those specific settings when appropriate.652let value = config.value ?? config.defaultValue;653if (typeof runsInWorkspace === 'boolean') {654value = config.userLocalValue ?? config.applicationValue;655if (runsInWorkspace) {656value = config.workspaceValue ?? config.workspaceFolderValue ?? config.userRemoteValue ?? value;657}658}659660const autoConfirm = value === true || (typeof value === 'object' && value.hasOwnProperty(toolId) && value[toolId] === true);661if (autoConfirm) {662if (await this._checkGlobalAutoApprove()) {663return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove };664}665}666667return undefined;668}669670private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown): Promise<ConfirmedReason | undefined> {671if (this._configurationService.getValue<boolean>(ChatConfiguration.GlobalAutoApprove) && await this._checkGlobalAutoApprove()) {672return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove };673}674675return this._confirmationService.getPostConfirmAction({ toolId, source, parameters });676}677678private async _checkGlobalAutoApprove(): Promise<boolean> {679const optedIn = this._storageService.getBoolean(AutoApproveStorageKeys.GlobalAutoApproveOptIn, StorageScope.APPLICATION, false);680if (optedIn) {681return true;682}683684if (this._contextKeyService.getContextKeyValue(SkipAutoApproveConfirmationKey) === true) {685return true;686}687688const promptResult = await this._dialogService.prompt({689type: Severity.Warning,690message: localize('autoApprove2.title', 'Enable global auto approve?'),691buttons: [692{693label: localize('autoApprove2.button.enable', 'Enable'),694run: () => true695},696{697label: localize('autoApprove2.button.disable', 'Disable'),698run: () => false699},700],701custom: {702icon: Codicon.warning,703disableCloseAction: true,704markdownDetails: [{705markdown: new MarkdownString(globalAutoApproveDescription.value),706}],707}708});709710if (promptResult.result !== true) {711await this._configurationService.updateValue(ChatConfiguration.GlobalAutoApprove, false);712return false;713}714715this._storageService.store(AutoApproveStorageKeys.GlobalAutoApproveOptIn, true, StorageScope.APPLICATION, StorageTarget.USER);716return true;717}718719private cleanupCallDisposables(requestId: string | undefined, store: DisposableStore): void {720if (requestId) {721const disposables = this._callsByRequestId.get(requestId);722if (disposables) {723const index = disposables.findIndex(d => d.store === store);724if (index > -1) {725disposables.splice(index, 1);726}727if (disposables.length === 0) {728this._callsByRequestId.delete(requestId);729}730}731}732733store.dispose();734}735736cancelToolCallsForRequest(requestId: string): void {737const calls = this._callsByRequestId.get(requestId);738if (calls) {739calls.forEach(call => call.store.dispose());740this._callsByRequestId.delete(requestId);741}742}743744private static readonly githubMCPServerAliases = ['github/github-mcp-server', 'io.github.github/github-mcp-server', 'github-mcp-server'];745private static readonly playwrightMCPServerAliases = ['microsoft/playwright-mcp', 'com.microsoft/playwright-mcp'];746747private * getToolSetAliases(toolSet: ToolSet, fullReferenceName: string): Iterable<string> {748if (fullReferenceName !== toolSet.referenceName) {749yield toolSet.referenceName; // tool set name without '/*'750}751if (toolSet.legacyFullNames) {752yield* toolSet.legacyFullNames;753}754switch (toolSet.referenceName) {755case 'github':756for (const alias of LanguageModelToolsService.githubMCPServerAliases) {757yield alias + '/*';758}759break;760case 'playwright':761for (const alias of LanguageModelToolsService.playwrightMCPServerAliases) {762yield alias + '/*';763}764break;765case SpecedToolAliases.execute: // 'execute'766yield 'shell'; // legacy alias767break;768case SpecedToolAliases.agent: // 'agent'769yield VSCodeToolReference.runSubagent; // prefer the tool set over th old tool name770yield 'custom-agent'; // legacy alias771break;772}773}774775private * getToolAliases(toolSet: IToolData, fullReferenceName: string): Iterable<string> {776const referenceName = toolSet.toolReferenceName ?? toolSet.displayName;777if (fullReferenceName !== referenceName && referenceName !== VSCodeToolReference.runSubagent) {778yield referenceName; // simple name, without toolset name779}780if (toolSet.legacyToolReferenceFullNames) {781for (const legacyName of toolSet.legacyToolReferenceFullNames) {782yield legacyName;783const lastSlashIndex = legacyName.lastIndexOf('/');784if (lastSlashIndex !== -1) {785yield legacyName.substring(lastSlashIndex + 1); // it was also known under the simple name786}787}788}789const slashIndex = fullReferenceName.lastIndexOf('/');790if (slashIndex !== -1) {791switch (fullReferenceName.substring(0, slashIndex)) {792case 'github':793for (const alias of LanguageModelToolsService.githubMCPServerAliases) {794yield alias + fullReferenceName.substring(slashIndex);795}796break;797case 'playwright':798for (const alias of LanguageModelToolsService.playwrightMCPServerAliases) {799yield alias + fullReferenceName.substring(slashIndex);800}801break;802}803}804}805806/**807* Create a map that contains all tools and toolsets with their enablement state.808* @param fullReferenceNames A list of tool or toolset by their full reference names that are enabled.809* @returns A map of tool or toolset instances to their enablement state.810*/811toToolAndToolSetEnablementMap(fullReferenceNames: readonly string[], _target: string | undefined): IToolAndToolSetEnablementMap {812const toolOrToolSetNames = new Set(fullReferenceNames);813const result = new Map<ToolSet | IToolData, boolean>();814for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {815if (tool instanceof ToolSet) {816const enabled = toolOrToolSetNames.has(fullReferenceName) || Iterable.some(this.getToolSetAliases(tool, fullReferenceName), name => toolOrToolSetNames.has(name));817result.set(tool, enabled);818if (enabled) {819for (const memberTool of tool.getTools()) {820result.set(memberTool, true);821}822}823} else {824if (!result.has(tool)) { // already set via an enabled toolset825const enabled = toolOrToolSetNames.has(fullReferenceName)826|| Iterable.some(this.getToolAliases(tool, fullReferenceName), name => toolOrToolSetNames.has(name))827|| !!tool.legacyToolReferenceFullNames?.some(toolFullName => {828// enable tool if just the legacy tool set name is present829const index = toolFullName.lastIndexOf('/');830return index !== -1 && toolOrToolSetNames.has(toolFullName.substring(0, index));831});832result.set(tool, enabled);833}834}835}836837// also add all user tool sets (not part of the prompt referencable tools)838for (const toolSet of this._toolSets) {839if (toolSet.source.type === 'user') {840const enabled = Iterable.every(toolSet.getTools(), t => result.get(t) === true);841result.set(toolSet, enabled);842}843}844return result;845}846847toFullReferenceNames(map: IToolAndToolSetEnablementMap): string[] {848const result: string[] = [];849const toolsCoveredByEnabledToolSet = new Set<IToolData>();850for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {851if (tool instanceof ToolSet) {852if (map.get(tool)) {853result.push(fullReferenceName);854for (const memberTool of tool.getTools()) {855toolsCoveredByEnabledToolSet.add(memberTool);856}857}858} else {859if (map.get(tool) && !toolsCoveredByEnabledToolSet.has(tool)) {860result.push(fullReferenceName);861}862}863}864return result;865}866867toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] {868const toolsOrToolSetByName = new Map<string, ToolSet | IToolData>();869for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {870toolsOrToolSetByName.set(fullReferenceName, tool);871}872873const result: ChatRequestToolReferenceEntry[] = [];874for (const ref of variableReferences) {875const toolOrToolSet = toolsOrToolSetByName.get(ref.name);876if (toolOrToolSet) {877if (toolOrToolSet instanceof ToolSet) {878result.push(toToolSetVariableEntry(toolOrToolSet, ref.range));879} else {880result.push(toToolVariableEntry(toolOrToolSet, ref.range));881}882}883}884return result;885}886887888private readonly _toolSets = new ObservableSet<ToolSet>();889890readonly toolSets: IObservable<Iterable<ToolSet>> = derived(this, reader => {891const allToolSets = Array.from(this._toolSets.observable.read(reader));892return allToolSets.filter(toolSet => this.isPermitted(toolSet, reader));893});894895getToolSet(id: string): ToolSet | undefined {896for (const toolSet of this._toolSets) {897if (toolSet.id === id) {898return toolSet;899}900}901return undefined;902}903904getToolSetByName(name: string): ToolSet | undefined {905for (const toolSet of this._toolSets) {906if (toolSet.referenceName === name) {907return toolSet;908}909}910return undefined;911}912913getSpecedToolSetName(referenceName: string): string {914if (LanguageModelToolsService.githubMCPServerAliases.includes(referenceName)) {915return 'github';916}917if (LanguageModelToolsService.playwrightMCPServerAliases.includes(referenceName)) {918return 'playwright';919}920return referenceName;921}922923createToolSet(source: ToolDataSource, id: string, referenceName: string, options?: { icon?: ThemeIcon; description?: string; legacyFullNames?: string[] }): ToolSet & IDisposable {924925const that = this;926927referenceName = this.getSpecedToolSetName(referenceName);928929const result = new class extends ToolSet implements IDisposable {930dispose(): void {931if (that._toolSets.has(result)) {932this._tools.clear();933that._toolSets.delete(result);934}935936}937}(id, referenceName, options?.icon ?? Codicon.tools, source, options?.description, options?.legacyFullNames);938939this._toolSets.add(result);940return result;941}942943readonly toolsWithFullReferenceName = derived<[IToolData | ToolSet, string][]>(reader => {944const result: [IToolData | ToolSet, string][] = [];945const coveredByToolSets = new Set<IToolData>();946for (const toolSet of this.toolSets.read(reader)) {947if (toolSet.source.type !== 'user') {948result.push([toolSet, getToolSetFullReferenceName(toolSet)]);949for (const tool of toolSet.getTools()) {950result.push([tool, getToolFullReferenceName(tool, toolSet)]);951coveredByToolSets.add(tool);952}953}954}955for (const tool of this.toolsObservable.read(reader)) {956if (tool.canBeReferencedInPrompt && !coveredByToolSets.has(tool) && this.isPermitted(tool, reader)) {957result.push([tool, getToolFullReferenceName(tool)]);958}959}960return result;961});962963* getFullReferenceNames(): Iterable<string> {964for (const [, fullReferenceName] of this.toolsWithFullReferenceName.get()) {965yield fullReferenceName;966}967}968969getDeprecatedFullReferenceNames(): Map<string, Set<string>> {970const result = new Map<string, Set<string>>();971const knownToolSetNames = new Set<string>();972const add = (name: string, fullReferenceName: string) => {973if (name !== fullReferenceName) {974if (!result.has(name)) {975result.set(name, new Set<string>());976}977result.get(name)!.add(fullReferenceName);978}979};980981for (const [tool, _] of this.toolsWithFullReferenceName.get()) {982if (tool instanceof ToolSet) {983knownToolSetNames.add(tool.referenceName);984if (tool.legacyFullNames) {985for (const legacyName of tool.legacyFullNames) {986knownToolSetNames.add(legacyName);987}988}989}990}991992for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {993if (tool instanceof ToolSet) {994for (const alias of this.getToolSetAliases(tool, fullReferenceName)) {995add(alias, fullReferenceName);996}997} else {998for (const alias of this.getToolAliases(tool, fullReferenceName)) {999add(alias, fullReferenceName);1000}1001if (tool.legacyToolReferenceFullNames) {1002for (const legacyName of tool.legacyToolReferenceFullNames) {1003// for any 'orphaned' toolsets (toolsets that no longer exist and1004// do not have an explicit legacy mapping), we should1005// just point them to the list of tools directly1006if (legacyName.includes('/')) {1007const toolSetFullName = legacyName.substring(0, legacyName.lastIndexOf('/'));1008if (!knownToolSetNames.has(toolSetFullName)) {1009add(toolSetFullName, fullReferenceName);1010}1011}1012}1013}1014}1015}1016return result;1017}10181019getToolByFullReferenceName(fullReferenceName: string): IToolData | ToolSet | undefined {1020for (const [tool, toolFullReferenceName] of this.toolsWithFullReferenceName.get()) {1021if (fullReferenceName === toolFullReferenceName) {1022return tool;1023}1024const aliases = tool instanceof ToolSet ? this.getToolSetAliases(tool, toolFullReferenceName) : this.getToolAliases(tool, toolFullReferenceName);1025if (Iterable.some(aliases, alias => fullReferenceName === alias)) {1026return tool;1027}1028}1029return undefined;1030}10311032getFullReferenceName(tool: IToolData | ToolSet, toolSet?: ToolSet): string {1033if (tool instanceof ToolSet) {1034return getToolSetFullReferenceName(tool);1035}1036return getToolFullReferenceName(tool, toolSet);1037}1038}10391040function getToolFullReferenceName(tool: IToolData, toolSet?: ToolSet) {1041const toolName = tool.toolReferenceName ?? tool.displayName;1042if (toolSet) {1043return `${toolSet.referenceName}/${toolName}`;1044} else if (tool.source.type === 'extension') {1045return `${tool.source.extensionId.value.toLowerCase()}/${toolName}`;1046}1047return toolName;1048}10491050function getToolSetFullReferenceName(toolSet: ToolSet) {1051if (toolSet.source.type === 'mcp') {1052return `${toolSet.referenceName}/*`;1053}1054return toolSet.referenceName;1055}105610571058type LanguageModelToolInvokedEvent = {1059result: 'success' | 'error' | 'userCancelled';1060chatSessionId: string | undefined;1061toolId: string;1062toolExtensionId: string | undefined;1063toolSourceKind: string;1064prepareTimeMs?: number;1065invocationTimeMs?: number;1066};10671068type LanguageModelToolInvokedClassification = {1069result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether invoking the LanguageModelTool resulted in an error.' };1070chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat session that the tool was used within, if applicable.' };1071toolId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the tool used.' };1072toolExtensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that contributed the tool.' };1073toolSourceKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source (mcp/extension/internal) of the tool.' };1074prepareTimeMs?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time spent in prepareToolInvocation method in milliseconds.' };1075invocationTimeMs?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time spent in tool invoke method in milliseconds.' };1076owner: 'roblourens';1077comment: 'Provides insight into the usage of language model tools.';1078};107910801081