Path: blob/main/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts
5242 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, derivedOpts, IObservable, IReader, observableFromEventOpts, ObservableSet, observableSignal, transaction } 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 { ICommandService } from '../../../../../platform/commands/common/commands.js';26import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';27import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';28import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';29import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';30import * as JSONContributionRegistry from '../../../../../platform/jsonschemas/common/jsonContributionRegistry.js';31import { ILogService } from '../../../../../platform/log/common/log.js';32import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';33import { Registry } from '../../../../../platform/registry/common/platform.js';34import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';35import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';36import { IExtensionService } from '../../../../services/extensions/common/extensions.js';37import { IPostToolUseCallerInput, IPreToolUseCallerInput, IPreToolUseHookResult } from '../../common/hooks/hooksTypes.js';38import { IHooksExecutionService } from '../../common/hooks/hooksExecutionService.js';39import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';40import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js';41import { IVariableReference } from '../../common/chatModes.js';42import { ConfirmedReason, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js';43import { ChatConfiguration } from '../../common/constants.js';44import { ILanguageModelChatMetadata } from '../../common/languageModels.js';45import { IChatModel, IChatRequestModel } from '../../common/model/chatModel.js';46import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToolInvocation.js';47import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js';48import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, isToolSet, ToolDataSource, toolContentToA11yString, toolMatchesModel, ToolSet, VSCodeToolReference, IToolSet, ToolSetForModel, IToolInvokedEvent } from '../../common/tools/languageModelToolsService.js';49import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js';50import { URI } from '../../../../../base/common/uri.js';51import { chatSessionResourceToId } from '../../common/model/chatUri.js';52import { HookType } from '../../common/promptSyntax/hookSchema.js';5354const jsonSchemaRegistry = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);5556interface IToolEntry {57data: IToolData;58impl?: IToolImpl;59}6061interface ITrackedCall {62store: IDisposable;63}6465const enum AutoApproveStorageKeys {66GlobalAutoApproveOptIn = 'chat.tools.global.autoApprove.optIn'67}6869const SkipAutoApproveConfirmationKey = 'vscode.chat.tools.global.autoApprove.testMode';7071// This tool will always require user confirmation even in auto approval mode.72// Users cannot auto approve this tool via settings either, as this is a tool used before the agentic loop.73const toolIdThatCannotBeAutoApproved = 'vscode_get_confirmation_with_options';7475export const globalAutoApproveDescription = localize2(76{77key: 'autoApprove2.markdown',78comment: [79'{Locked=\'](https://github.com/features/codespaces)\'}',80'{Locked=\'](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)\'}',81'{Locked=\'](https://code.visualstudio.com/docs/copilot/security)\'}',82'{Locked=\'**\'}',83]84},85'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.**'86);8788export class LanguageModelToolsService extends Disposable implements ILanguageModelToolsService {89_serviceBrand: undefined;90readonly vscodeToolSet: ToolSet;91readonly executeToolSet: ToolSet;92readonly readToolSet: ToolSet;93readonly agentToolSet: ToolSet;9495private readonly _onDidChangeTools = this._register(new Emitter<void>());96readonly onDidChangeTools = this._onDidChangeTools.event;97private readonly _onDidPrepareToolCallBecomeUnresponsive = this._register(new Emitter<{ sessionResource: URI; toolData: IToolData }>());98readonly onDidPrepareToolCallBecomeUnresponsive = this._onDidPrepareToolCallBecomeUnresponsive.event;99private readonly _onDidInvokeTool = this._register(new Emitter<IToolInvokedEvent>());100readonly onDidInvokeTool = this._onDidInvokeTool.event;101102/** Throttle tools updates because it sends all tools and runs on context key updates */103private readonly _onDidChangeToolsScheduler = new RunOnceScheduler(() => this._onDidChangeTools.fire(), 750);104private readonly _tools = new Map<string, IToolEntry>();105private readonly _toolContextKeys = new Set<string>();106private readonly _ctxToolsCount: IContextKey<number>;107108private readonly _callsByRequestId = new Map<string, ITrackedCall[]>();109110/** Pending tool calls in the streaming phase, keyed by toolCallId */111private readonly _pendingToolCalls = new Map<string, ChatToolInvocation>();112113private readonly _isAgentModeEnabled: IObservable<boolean>;114115constructor(116@IInstantiationService private readonly _instantiationService: IInstantiationService,117@IExtensionService private readonly _extensionService: IExtensionService,118@IContextKeyService private readonly _contextKeyService: IContextKeyService,119@IChatService private readonly _chatService: IChatService,120@IDialogService private readonly _dialogService: IDialogService,121@ITelemetryService private readonly _telemetryService: ITelemetryService,122@ILogService private readonly _logService: ILogService,123@IConfigurationService private readonly _configurationService: IConfigurationService,124@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,125@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,126@IStorageService private readonly _storageService: IStorageService,127@ILanguageModelToolsConfirmationService private readonly _confirmationService: ILanguageModelToolsConfirmationService,128@IHooksExecutionService private readonly _hooksExecutionService: IHooksExecutionService,129@ICommandService private readonly _commandService: ICommandService,130) {131super();132133this._isAgentModeEnabled = observableConfigValue(ChatConfiguration.AgentEnabled, true, this._configurationService);134135this._register(this._contextKeyService.onDidChangeContext(e => {136if (e.affectsSome(this._toolContextKeys)) {137// Not worth it to compute a delta here unless we have many tools changing often138this._onDidChangeToolsScheduler.schedule();139}140}));141142this._register(this._configurationService.onDidChangeConfiguration(e => {143if (e.affectsConfiguration(ChatConfiguration.ExtensionToolsEnabled) || e.affectsConfiguration(ChatConfiguration.AgentEnabled)) {144this._onDidChangeToolsScheduler.schedule();145}146}));147148// Clear out warning accepted state if the setting is disabled149this._register(Event.runAndSubscribe(this._configurationService.onDidChangeConfiguration, e => {150if (!e || e.affectsConfiguration(ChatConfiguration.GlobalAutoApprove)) {151if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) !== true) {152this._storageService.remove(AutoApproveStorageKeys.GlobalAutoApproveOptIn, StorageScope.APPLICATION);153}154}155}));156157this._ctxToolsCount = ChatContextKeys.Tools.toolsCount.bindTo(_contextKeyService);158159// Create the internal VS Code tool set160this.vscodeToolSet = this._register(this.createToolSet(161ToolDataSource.Internal,162'vscode',163VSCodeToolReference.vscode,164{165icon: ThemeIcon.fromId(Codicon.vscode.id),166description: localize('copilot.toolSet.vscode.description', 'Use VS Code features'),167}168));169170// Create the internal Execute tool set171this.executeToolSet = this._register(this.createToolSet(172ToolDataSource.Internal,173'execute',174SpecedToolAliases.execute,175{176icon: ThemeIcon.fromId(Codicon.terminal.id),177description: localize('copilot.toolSet.execute.description', 'Execute code and applications on your machine'),178}179));180181// Create the internal Read tool set182this.readToolSet = this._register(this.createToolSet(183ToolDataSource.Internal,184'read',185SpecedToolAliases.read,186{187icon: ThemeIcon.fromId(Codicon.book.id),188description: localize('copilot.toolSet.read.description', 'Read files in your workspace'),189}190));191192// Create the internal Agent tool set193this.agentToolSet = this._register(this.createToolSet(194ToolDataSource.Internal,195'agent',196SpecedToolAliases.agent,197{198icon: ThemeIcon.fromId(Codicon.agent.id),199description: localize('copilot.toolSet.agent.description', 'Delegate tasks to other agents'),200}201));202}203204/**205* Returns if the given tool or toolset is permitted in the current context.206* When agent mode is enabled, all tools are permitted (no restriction)207* When agent mode is disabled only a subset of read-only tools are permitted in agentic-loop contexts.208*/209private isPermitted(toolOrToolSet: IToolData | ToolSet, reader?: IReader): boolean {210const agentModeEnabled = this._isAgentModeEnabled.read(reader);211if (agentModeEnabled !== false) {212return true;213}214const permittedInternalToolSetIds = [SpecedToolAliases.read, SpecedToolAliases.search, SpecedToolAliases.web];215if (isToolSet(toolOrToolSet)) {216const permitted = toolOrToolSet.source.type === 'internal' && permittedInternalToolSetIds.includes(toolOrToolSet.referenceName);217this._logService.trace(`LanguageModelToolsService#isPermitted: ToolSet ${toolOrToolSet.id} (${toolOrToolSet.referenceName}) permitted=${permitted}`);218return permitted;219}220for (const toolSet of this._toolSets) {221if (toolSet.source.type === 'internal' && permittedInternalToolSetIds.includes(toolSet.referenceName)) {222for (const memberTool of toolSet.getTools()) {223if (memberTool.id === toolOrToolSet.id) {224this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=true (member of ${toolSet.referenceName})`);225return true;226}227}228}229}230231// Special case for 'vscode_fetchWebPage_internal', which is allowed if we allow 'web' tools232// Fetch is implemented with two tools, this one and 'copilot_fetchWebPage'233if (toolOrToolSet.id === 'vscode_fetchWebPage_internal' && permittedInternalToolSetIds.includes(SpecedToolAliases.web)) {234this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=true (special case)`);235return true;236}237238this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=false`);239return false;240}241242override dispose(): void {243super.dispose();244245this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose()));246this._pendingToolCalls.clear();247this._ctxToolsCount.reset();248}249250registerToolData(toolData: IToolData): IDisposable {251if (this._tools.has(toolData.id)) {252throw new Error(`Tool "${toolData.id}" is already registered.`);253}254255this._tools.set(toolData.id, { data: toolData });256this._ctxToolsCount.set(this._tools.size);257if (!this._onDidChangeToolsScheduler.isScheduled()) {258this._onDidChangeToolsScheduler.schedule();259}260261toolData.when?.keys().forEach(key => this._toolContextKeys.add(key));262263let store: DisposableStore | undefined;264if (toolData.inputSchema) {265store = new DisposableStore();266const schemaUrl = createToolSchemaUri(toolData.id).toString();267jsonSchemaRegistry.registerSchema(schemaUrl, toolData.inputSchema, store);268store.add(jsonSchemaRegistry.registerSchemaAssociation(schemaUrl, `/lm/tool/${toolData.id}/tool_input.json`));269}270271return toDisposable(() => {272store?.dispose();273this._tools.delete(toolData.id);274this._ctxToolsCount.set(this._tools.size);275this._refreshAllToolContextKeys();276if (!this._onDidChangeToolsScheduler.isScheduled()) {277this._onDidChangeToolsScheduler.schedule();278}279});280}281282flushToolUpdates(): void {283this._onDidChangeToolsScheduler.flush();284}285286private _refreshAllToolContextKeys() {287this._toolContextKeys.clear();288for (const tool of this._tools.values()) {289tool.data.when?.keys().forEach(key => this._toolContextKeys.add(key));290}291}292293registerToolImplementation(id: string, tool: IToolImpl): IDisposable {294const entry = this._tools.get(id);295if (!entry) {296throw new Error(`Tool "${id}" was not contributed.`);297}298299if (entry.impl) {300throw new Error(`Tool "${id}" already has an implementation.`);301}302303entry.impl = tool;304return toDisposable(() => {305entry.impl = undefined;306});307}308309registerTool(toolData: IToolData, tool: IToolImpl): IDisposable {310return combinedDisposable(311this.registerToolData(toolData),312this.registerToolImplementation(toolData.id, tool)313);314}315316getTools(model: ILanguageModelChatMetadata | undefined): Iterable<IToolData> {317const toolDatas = Iterable.map(this._tools.values(), i => i.data);318const extensionToolsEnabled = this._configurationService.getValue<boolean>(ChatConfiguration.ExtensionToolsEnabled);319return Iterable.filter(320toolDatas,321toolData => {322const satisfiesWhenClause = !toolData.when || this._contextKeyService.contextMatchesRules(toolData.when);323const satisfiesExternalToolCheck = toolData.source.type !== 'extension' || !!extensionToolsEnabled;324const satisfiesPermittedCheck = this.isPermitted(toolData);325const satisfiesModelFilter = toolMatchesModel(toolData, model);326return satisfiesWhenClause && satisfiesExternalToolCheck && satisfiesPermittedCheck && satisfiesModelFilter;327});328}329330observeTools(model: ILanguageModelChatMetadata | undefined): IObservable<readonly IToolData[]> {331const meta = derived(reader => {332const signal = observableSignal('observeToolsContext');333const trigger = () => transaction(tx => signal.trigger(tx));334reader.store.add(this.onDidChangeTools(trigger));335return signal;336});337338return derivedOpts({ equalsFn: arrayEqualsC() }, reader => {339meta.read(reader).read(reader);340return Array.from(this.getTools(model));341});342}343344getAllToolsIncludingDisabled(): Iterable<IToolData> {345const toolDatas = Iterable.map(this._tools.values(), i => i.data);346const extensionToolsEnabled = this._configurationService.getValue<boolean>(ChatConfiguration.ExtensionToolsEnabled);347return Iterable.filter(348toolDatas,349toolData => {350const satisfiesExternalToolCheck = toolData.source.type !== 'extension' || !!extensionToolsEnabled;351const satisfiesPermittedCheck = this.isPermitted(toolData);352return satisfiesExternalToolCheck && satisfiesPermittedCheck;353});354}355356getTool(id: string): IToolData | undefined {357return this._tools.get(id)?.data;358}359360getToolByName(name: string): IToolData | undefined {361for (const tool of this.getAllToolsIncludingDisabled()) {362if (tool.toolReferenceName === name) {363return tool;364}365}366return undefined;367}368369/**370* Execute the preToolUse hook and handle denial.371* Returns an object containing:372* - denialResult: A tool result if the hook denied execution (caller should return early)373* - hookResult: The full hook result for use in auto-approval logic (allow/ask decisions)374* @param pendingInvocation If there's an existing streaming invocation from beginToolCall, pass it here to cancel it instead of creating a new one.375*/376private async _executePreToolUseHook(377dto: IToolInvocation,378toolData: IToolData | undefined,379request: IChatRequestModel | undefined,380pendingInvocation: ChatToolInvocation | undefined,381token: CancellationToken382): Promise<{ denialResult?: IToolResult; hookResult?: IPreToolUseHookResult }> {383// Skip hook if no session context or tool doesn't exist384if (!dto.context?.sessionResource || !toolData) {385return {};386}387388const hookInput: IPreToolUseCallerInput = {389toolName: dto.toolId,390toolInput: dto.parameters,391toolCallId: dto.callId,392};393const hookResult = await this._hooksExecutionService.executePreToolUseHook(dto.context.sessionResource, hookInput, token);394395if (hookResult?.permissionDecision === 'deny') {396const hookReason = hookResult.permissionDecisionReason ?? localize('hookDeniedNoReason', "Hook denied tool execution");397const reason = localize('deniedByPreToolUseHook', "Denied by {0} hook: {1}", HookType.PreToolUse, hookReason);398this._logService.debug(`[LanguageModelToolsService#invokeTool] Tool ${dto.toolId} denied by preToolUse hook: ${hookReason}`);399400// Handle the tool invocation in cancelled state401if (toolData) {402if (pendingInvocation) {403// If there's an existing streaming invocation, cancel it404pendingInvocation.cancelFromStreaming(ToolConfirmKind.Denied, reason);405} else if (request) {406// Otherwise create a new cancelled invocation and add it to the chat model407const toolInvocation = ChatToolInvocation.createCancelled(408{ toolCallId: dto.callId, toolId: dto.toolId, toolData, subagentInvocationId: dto.subAgentInvocationId, chatRequestId: dto.chatRequestId },409dto.parameters,410ToolConfirmKind.Denied,411reason412);413this._chatService.appendProgress(request, toolInvocation);414}415}416417const denialMessage = localize('toolExecutionDenied', "Tool execution denied: {0}", hookReason);418return {419denialResult: {420content: [{ kind: 'text', value: denialMessage }],421toolResultError: hookReason,422},423hookResult,424};425}426427return { hookResult };428}429430/**431* Validate updatedInput from a preToolUse hook against the tool's input schema432* using the json.validate command from the JSON extension.433* @returns An error message string if validation fails, or undefined if valid.434*/435private async _validateUpdatedInput(toolId: string, toolData: IToolData | undefined, updatedInput: object): Promise<string | undefined> {436if (!toolData?.inputSchema) {437return undefined;438}439440type JsonDiagnostic = {441message: string;442range: { line: number; character: number }[];443severity: string;444code?: string | number;445};446447try {448const schemaUri = createToolSchemaUri(toolId);449const inputJson = JSON.stringify(updatedInput);450const diagnostics = await this._commandService.executeCommand<JsonDiagnostic[]>('json.validate', schemaUri, inputJson) || [];451if (diagnostics.length > 0) {452return diagnostics.map(d => d.message).join('; ');453}454} catch (e) {455// json extension may not be available; skip validation456this._logService.debug(`[LanguageModelToolsService#_validateUpdatedInput] json.validate command failed, skipping validation: ${toErrorMessage(e)}`);457}458459return undefined;460}461462/**463* Execute the postToolUse hook after tool completion.464* If the hook returns a "block" decision, additional context is appended to the tool result465* as feedback for the agent indicating the block and reason. The tool has already run,466* so blocking only provides feedback.467*/468private async _executePostToolUseHook(469dto: IToolInvocation,470toolResult: IToolResult,471token: CancellationToken472): Promise<void> {473if (!dto.context?.sessionResource) {474return;475}476477const hookInput: IPostToolUseCallerInput = {478toolName: dto.toolId,479toolInput: dto.parameters,480getToolResponseText: () => toolContentToA11yString(toolResult.content),481toolCallId: dto.callId,482};483const hookResult = await this._hooksExecutionService.executePostToolUseHook(dto.context.sessionResource, hookInput, token);484485if (hookResult?.decision === 'block') {486const hookReason = hookResult.reason ?? localize('postToolUseHookBlockedNoReason', "Hook blocked tool result");487this._logService.debug(`[LanguageModelToolsService#invokeTool] PostToolUse hook blocked for tool ${dto.toolId}: ${hookReason}`);488const blockMessage = localize('postToolUseHookBlockedContext', "The PostToolUse hook blocked this tool result. Reason: {0}", hookReason);489toolResult.content.push({ kind: 'text', value: '\n<PostToolUse-context>\n' + blockMessage + '\n</PostToolUse-context>' });490}491492if (hookResult?.additionalContext) {493// Append additional context from all hooks to the tool result content494for (const context of hookResult.additionalContext) {495toolResult.content.push({ kind: 'text', value: '\n<PostToolUse-context>\n' + context + '\n</PostToolUse-context>' });496}497}498}499500async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise<IToolResult> {501this._logService.trace(`[LanguageModelToolsService#invokeTool] Invoking tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}`);502503const toolData = this._tools.get(dto.toolId)?.data;504let model: IChatModel | undefined;505let request: IChatRequestModel | undefined;506if (dto.context?.sessionResource) {507model = this._chatService.getSession(dto.context.sessionResource);508request = model?.getRequests().at(-1);509}510511// Check if there's an existing pending tool call from streaming phase BEFORE hook check512let pendingToolCallKey: string | undefined;513let toolInvocation: ChatToolInvocation | undefined;514if (this._pendingToolCalls.has(dto.callId)) {515pendingToolCallKey = dto.callId;516toolInvocation = this._pendingToolCalls.get(dto.callId);517} else if (dto.chatStreamToolCallId && this._pendingToolCalls.has(dto.chatStreamToolCallId)) {518pendingToolCallKey = dto.chatStreamToolCallId;519toolInvocation = this._pendingToolCalls.get(dto.chatStreamToolCallId);520}521522let requestId: string | undefined;523let store: DisposableStore | undefined;524if (dto.context && request) {525requestId = request.id;526store = new DisposableStore();527if (!this._callsByRequestId.has(requestId)) {528this._callsByRequestId.set(requestId, []);529}530const trackedCall: ITrackedCall = { store };531this._callsByRequestId.get(requestId)!.push(trackedCall);532533const source = new CancellationTokenSource();534store.add(toDisposable(() => {535source.dispose(true);536}));537store.add(token.onCancellationRequested((() => {538IChatToolInvocation.confirmWith(toolInvocation, { type: ToolConfirmKind.Denied });539source.cancel();540})));541store.add(source.token.onCancellationRequested(() => {542IChatToolInvocation.confirmWith(toolInvocation, { type: ToolConfirmKind.Denied });543}));544token = source.token;545}546547// Execute preToolUse hook - returns early if hook denies execution548const { denialResult: hookDenialResult, hookResult: preToolUseHookResult } = await this._executePreToolUseHook(dto, toolData, request, toolInvocation, token);549if (hookDenialResult) {550// Clean up pending tool call if it exists551if (pendingToolCallKey) {552this._pendingToolCalls.delete(pendingToolCallKey);553}554return hookDenialResult;555}556557// Apply updatedInput from preToolUse hook if provided, after validating against the tool's input schema558if (preToolUseHookResult?.updatedInput) {559const validationError = await this._validateUpdatedInput(dto.toolId, toolData, preToolUseHookResult.updatedInput);560if (validationError) {561this._logService.warn(`[LanguageModelToolsService#invokeTool] Tool ${dto.toolId} updatedInput from preToolUse hook failed schema validation: ${validationError}`);562} else {563this._logService.debug(`[LanguageModelToolsService#invokeTool] Tool ${dto.toolId} input modified by preToolUse hook`);564dto.parameters = preToolUseHookResult.updatedInput;565}566}567568// Fire the event to notify listeners that a tool is being invoked569this._onDidInvokeTool.fire({570toolId: dto.toolId,571sessionResource: dto.context?.sessionResource,572requestId: dto.chatRequestId,573subagentInvocationId: dto.subAgentInvocationId,574});575576// 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.577let tool = this._tools.get(dto.toolId);578if (!tool) {579throw new Error(`Tool ${dto.toolId} was not contributed`);580}581582if (!tool.impl) {583await this._extensionService.activateByEvent(`onLanguageModelTool:${dto.toolId}`);584585// Extension should activate and register the tool implementation586tool = this._tools.get(dto.toolId);587if (!tool?.impl) {588throw new Error(`Tool ${dto.toolId} does not have an implementation registered.`);589}590}591592// Note: pending invocation lookup was already done above for the hook check593const hadPendingInvocation = !!toolInvocation;594if (hadPendingInvocation && pendingToolCallKey) {595// Remove from pending since we're now invoking it596this._pendingToolCalls.delete(pendingToolCallKey);597}598599let toolResult: IToolResult | undefined;600let prepareTimeWatch: StopWatch | undefined;601let invocationTimeWatch: StopWatch | undefined;602let preparedInvocation: IPreparedToolInvocation | undefined;603try {604if (dto.context) {605if (!model) {606throw new Error(`Tool called for unknown chat session`);607}608609if (!request) {610throw new Error(`Tool called for unknown chat request`);611}612dto.modelId = request.modelId;613dto.userSelectedTools = request.userSelectedTools && { ...request.userSelectedTools };614615prepareTimeWatch = StopWatch.create(true);616preparedInvocation = await this.prepareToolInvocationWithHookResult(tool, dto, preToolUseHookResult, token);617prepareTimeWatch.stop();618619const { autoConfirmed, preparedInvocation: updatedPreparedInvocation } = await this.resolveAutoConfirmFromHook(preToolUseHookResult, tool, dto, preparedInvocation, dto.context?.sessionResource);620preparedInvocation = updatedPreparedInvocation;621622623// Important: a tool invocation that will be autoconfirmed should never624// be in the chat response in the `NeedsConfirmation` state, even briefly,625// as that triggers notifications and causes issues in eval.626if (hadPendingInvocation && toolInvocation) {627// Transition from streaming to executing/waiting state628toolInvocation.transitionFromStreaming(preparedInvocation, dto.parameters, autoConfirmed);629} else {630// Create a new tool invocation (no streaming phase)631toolInvocation = new ChatToolInvocation(preparedInvocation, tool.data, dto.callId, dto.subAgentInvocationId, dto.parameters);632if (autoConfirmed) {633IChatToolInvocation.confirmWith(toolInvocation, autoConfirmed);634}635636this._chatService.appendProgress(request, toolInvocation);637}638639dto.toolSpecificData = toolInvocation?.toolSpecificData;640if (preparedInvocation?.confirmationMessages?.title) {641if (!IChatToolInvocation.executionConfirmedOrDenied(toolInvocation) && !autoConfirmed) {642this.playAccessibilitySignal([toolInvocation]);643}644const userConfirmed = await IChatToolInvocation.awaitConfirmation(toolInvocation, token);645if (userConfirmed.type === ToolConfirmKind.Denied) {646throw new CancellationError();647}648if (userConfirmed.type === ToolConfirmKind.Skipped) {649toolResult = {650content: [{651kind: 'text',652value: 'The user chose to skip the tool call, they want to proceed without running it'653}]654};655return toolResult;656}657658if (userConfirmed.type === ToolConfirmKind.UserAction && userConfirmed.selectedButton) {659dto.selectedCustomButton = userConfirmed.selectedButton;660}661662if (dto.toolSpecificData?.kind === 'input') {663dto.parameters = dto.toolSpecificData.rawInput;664dto.toolSpecificData = undefined;665}666}667} else {668prepareTimeWatch = StopWatch.create(true);669preparedInvocation = await this.prepareToolInvocationWithHookResult(tool, dto, preToolUseHookResult, token);670prepareTimeWatch.stop();671672const { autoConfirmed: fallbackAutoConfirmed, preparedInvocation: updatedPreparedInvocation } = await this.resolveAutoConfirmFromHook(preToolUseHookResult, tool, dto, preparedInvocation, undefined);673preparedInvocation = updatedPreparedInvocation;674if (preparedInvocation?.confirmationMessages?.title && !fallbackAutoConfirmed) {675const result = await this._dialogService.confirm({ message: renderAsPlaintext(preparedInvocation.confirmationMessages.title), detail: renderAsPlaintext(preparedInvocation.confirmationMessages.message!) });676if (!result.confirmed) {677throw new CancellationError();678}679}680dto.toolSpecificData = preparedInvocation?.toolSpecificData;681}682683if (token.isCancellationRequested) {684throw new CancellationError();685}686687invocationTimeWatch = StopWatch.create(true);688toolResult = await tool.impl.invoke(dto, countTokens, {689report: step => {690toolInvocation?.acceptProgress(step);691}692}, token);693invocationTimeWatch.stop();694this.ensureToolDetails(dto, toolResult, tool.data);695696const afterExecuteState = await toolInvocation?.didExecuteTool(toolResult, undefined, () =>697this.shouldAutoConfirmPostExecution(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, dto.context?.sessionResource));698699if (toolInvocation && afterExecuteState?.type === IChatToolInvocation.StateKind.WaitingForPostApproval) {700const postConfirm = await IChatToolInvocation.awaitPostConfirmation(toolInvocation, token);701if (postConfirm.type === ToolConfirmKind.Denied) {702throw new CancellationError();703}704if (postConfirm.type === ToolConfirmKind.Skipped) {705toolResult = {706content: [{707kind: 'text',708value: 'The tool executed but the user chose not to share the results'709}]710};711}712}713714// Execute postToolUse hook after successful tool execution715await this._executePostToolUseHook(dto, toolResult, token);716717this._telemetryService.publicLog2<LanguageModelToolInvokedEvent, LanguageModelToolInvokedClassification>(718'languageModelToolInvoked',719{720result: 'success',721chatSessionId: dto.context?.sessionResource ? chatSessionResourceToId(dto.context.sessionResource) : undefined,722toolId: tool.data.id,723toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined,724toolSourceKind: tool.data.source.type,725prepareTimeMs: prepareTimeWatch?.elapsed(),726invocationTimeMs: invocationTimeWatch?.elapsed(),727});728return toolResult;729} catch (err) {730const result = isCancellationError(err) ? 'userCancelled' : 'error';731this._telemetryService.publicLog2<LanguageModelToolInvokedEvent, LanguageModelToolInvokedClassification>(732'languageModelToolInvoked',733{734result,735chatSessionId: dto.context?.sessionId,736toolId: tool.data.id,737toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined,738toolSourceKind: tool.data.source.type,739prepareTimeMs: prepareTimeWatch?.elapsed(),740invocationTimeMs: invocationTimeWatch?.elapsed(),741});742if (!isCancellationError(err)) {743this._logService.error(`[LanguageModelToolsService#invokeTool] Error from tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}:\n${toErrorMessage(err, true)}`);744}745746toolResult ??= { content: [] };747toolResult.toolResultError = err instanceof Error ? err.message : String(err);748if (tool.data.alwaysDisplayInputOutput) {749toolResult.toolResultDetails = { input: this.formatToolInput(dto), output: [{ type: 'embed', isText: true, value: String(err) }], isError: true };750}751752throw err;753} finally {754toolInvocation?.didExecuteTool(toolResult, true);755if (store) {756this.cleanupCallDisposables(requestId, store);757}758}759}760761private async prepareToolInvocationWithHookResult(tool: IToolEntry, dto: IToolInvocation, hookResult: IPreToolUseHookResult | undefined, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {762let forceConfirmationReason: string | undefined;763if (hookResult?.permissionDecision === 'ask') {764const hookMessage = localize('preToolUseHookRequiredConfirmation', "{0} required confirmation", HookType.PreToolUse);765forceConfirmationReason = hookResult.permissionDecisionReason766? `${hookMessage}: ${hookResult.permissionDecisionReason}`767: hookMessage;768}769return this.prepareToolInvocation(tool, dto, forceConfirmationReason, token);770}771772/**773* Determines the auto-confirm decision based on a preToolUse hook result.774* If the hook returned 'allow', auto-approves. If 'ask', forces confirmation775* and ensures confirmation messages exist on `preparedInvocation`. Otherwise776* falls back to normal auto-confirm logic.777*778* Returns the possibly-updated preparedInvocation along with the auto-confirm decision,779* since when the hook returns 'ask' and preparedInvocation was undefined, we create one.780*/781private async resolveAutoConfirmFromHook(782hookResult: IPreToolUseHookResult | undefined,783tool: IToolEntry,784dto: IToolInvocation,785preparedInvocation: IPreparedToolInvocation | undefined,786sessionResource: URI | undefined,787): Promise<{ autoConfirmed: ConfirmedReason | undefined; preparedInvocation: IPreparedToolInvocation | undefined }> {788if (hookResult?.permissionDecision === 'allow') {789this._logService.debug(`[LanguageModelToolsService#invokeTool] Tool ${dto.toolId} auto-approved by preToolUse hook`);790return { autoConfirmed: { type: ToolConfirmKind.ConfirmationNotNeeded, reason: localize('hookAllowed', "Allowed by hook") }, preparedInvocation };791}792793if (hookResult?.permissionDecision === 'ask') {794this._logService.debug(`[LanguageModelToolsService#invokeTool] Tool ${dto.toolId} requires confirmation (preToolUse hook returned 'ask')`);795// Ensure confirmation messages exist when hook requires confirmation796if (!preparedInvocation?.confirmationMessages?.title) {797if (!preparedInvocation) {798preparedInvocation = {};799}800const fullReferenceName = getToolFullReferenceName(tool.data);801const hookReason = hookResult.permissionDecisionReason;802const baseMessage = localize('hookRequiresConfirmation.message', "{0} hook confirmation required", HookType.PreToolUse);803preparedInvocation.confirmationMessages = {804...preparedInvocation.confirmationMessages,805title: localize('hookRequiresConfirmation.title', "Use the '{0}' tool?", fullReferenceName),806message: new MarkdownString(hookReason ? `${baseMessage}\n\n${hookReason}` : baseMessage),807allowAutoConfirm: false,808};809preparedInvocation.toolSpecificData = {810kind: 'input',811rawInput: dto.parameters,812};813}814return { autoConfirmed: undefined, preparedInvocation };815}816817// No hook decision - use normal auto-confirm logic818const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, sessionResource);819return { autoConfirmed, preparedInvocation };820}821822private async prepareToolInvocation(tool: IToolEntry, dto: IToolInvocation, forceConfirmationReason: string | undefined, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {823let prepared: IPreparedToolInvocation | undefined;824if (tool.impl!.prepareToolInvocation) {825const preparePromise = tool.impl!.prepareToolInvocation({826parameters: dto.parameters,827toolCallId: dto.callId,828chatRequestId: dto.chatRequestId,829chatSessionId: dto.context?.sessionId,830chatSessionResource: dto.context?.sessionResource,831chatInteractionId: dto.chatInteractionId,832modelId: dto.modelId,833forceConfirmationReason: forceConfirmationReason834}, token);835836const raceResult = await Promise.race([837timeout(3000, token).then(() => 'timeout'),838preparePromise839]);840if (raceResult === 'timeout' && dto.context) {841this._onDidPrepareToolCallBecomeUnresponsive.fire({842sessionResource: dto.context.sessionResource,843toolData: tool.data844});845}846847prepared = await preparePromise;848}849850const isEligibleForAutoApproval = this.isToolEligibleForAutoApproval(tool.data);851852// Default confirmation messages if tool is not eligible for auto-approval853if (!isEligibleForAutoApproval && !prepared?.confirmationMessages?.title) {854if (!prepared) {855prepared = {};856}857const fullReferenceName = getToolFullReferenceName(tool.data);858859// TODO: This should be more detailed per tool.860prepared.confirmationMessages = {861...prepared.confirmationMessages,862title: localize('defaultToolConfirmation.title', 'Confirm tool execution'),863message: localize('defaultToolConfirmation.message', 'Run the \'{0}\' tool?', fullReferenceName),864disclaimer: tool.data.id === toolIdThatCannotBeAutoApproved ? undefined : 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 }),865allowAutoConfirm: false,866};867}868869if (!isEligibleForAutoApproval && prepared?.confirmationMessages?.title) {870// Always overwrite the disclaimer if not eligible for auto-approval871prepared.confirmationMessages.disclaimer = tool.data.id === toolIdThatCannotBeAutoApproved ? undefined : 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 });872}873874if (prepared?.confirmationMessages?.title) {875if (prepared.toolSpecificData?.kind !== 'terminal' && prepared.confirmationMessages.allowAutoConfirm !== false) {876prepared.confirmationMessages.allowAutoConfirm = isEligibleForAutoApproval;877}878879if (!prepared.toolSpecificData && tool.data.alwaysDisplayInputOutput) {880prepared.toolSpecificData = {881kind: 'input',882rawInput: dto.parameters,883};884}885}886887return prepared;888}889890beginToolCall(options: IBeginToolCallOptions): IChatToolInvocation | undefined {891// First try to look up by tool ID (the package.json "name" field),892// then fall back to looking up by toolReferenceName893const toolEntry = this._tools.get(options.toolId);894if (!toolEntry) {895return undefined;896}897898// Don't create a streaming invocation for tools that don't implement handleToolStream.899// These tools will have their invocation created directly in invokeToolInternal.900if (!toolEntry.impl?.handleToolStream) {901return undefined;902}903904// Create the invocation in streaming state905const invocation = ChatToolInvocation.createStreaming({906toolCallId: options.toolCallId,907toolId: options.toolId,908toolData: toolEntry.data,909subagentInvocationId: options.subagentInvocationId,910chatRequestId: options.chatRequestId,911});912913// Track the pending tool call914this._pendingToolCalls.set(options.toolCallId, invocation);915916// If we have a session, append the invocation to the chat as progress917if (options.sessionResource) {918const model = this._chatService.getSession(options.sessionResource);919if (model) {920// Find the request by chatRequestId if available, otherwise use the last request921const request = (options.chatRequestId922? model.getRequests().find(r => r.id === options.chatRequestId)923: undefined) ?? model.getRequests().at(-1);924if (request) {925this._chatService.appendProgress(request, invocation);926}927}928}929930// Call handleToolStream to get initial streaming message931this._callHandleToolStream(toolEntry, invocation, options.toolCallId, undefined, CancellationToken.None);932933return invocation;934}935936private async _callHandleToolStream(toolEntry: IToolEntry, invocation: ChatToolInvocation, toolCallId: string, rawInput: unknown, token: CancellationToken): Promise<void> {937if (!toolEntry.impl?.handleToolStream) {938return;939}940try {941const result = await toolEntry.impl.handleToolStream({942toolCallId,943rawInput,944chatRequestId: invocation.chatRequestId,945}, token);946947if (result?.invocationMessage) {948invocation.updateStreamingMessage(result.invocationMessage);949}950} catch (error) {951this._logService.error(`[LanguageModelToolsService#_callHandleToolStream] Error calling handleToolStream for tool ${toolEntry.data.id}:`, error);952}953}954955async updateToolStream(toolCallId: string, partialInput: unknown, token: CancellationToken): Promise<void> {956const invocation = this._pendingToolCalls.get(toolCallId);957if (!invocation) {958return;959}960961// Update the partial input on the invocation962invocation.updatePartialInput(partialInput);963964// Call handleToolStream if the tool implements it965const toolEntry = this._tools.get(invocation.toolId);966if (toolEntry) {967await this._callHandleToolStream(toolEntry, invocation, toolCallId, partialInput, token);968}969}970971private playAccessibilitySignal(toolInvocations: ChatToolInvocation[]): void {972const autoApproved = this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove);973if (autoApproved) {974return;975}976977// Filter out any tool invocations that have already been confirmed/denied.978// This is a defensive check - normally the call site should prevent this,979// but tools may be auto-approved through various mechanisms (per-session rules,980// per-workspace rules, etc.) that could cause a race condition.981const pendingInvocations = toolInvocations.filter(inv => !IChatToolInvocation.executionConfirmedOrDenied(inv));982if (pendingInvocations.length === 0) {983return;984}985986const setting: { sound?: 'auto' | 'on' | 'off'; announcement?: 'auto' | 'off' } | undefined = this._configurationService.getValue(AccessibilitySignal.chatUserActionRequired.settingsKey);987if (!setting) {988return;989}990const soundEnabled = setting.sound === 'on' || (setting.sound === 'auto' && (this._accessibilityService.isScreenReaderOptimized()));991const announcementEnabled = this._accessibilityService.isScreenReaderOptimized() && setting.announcement === 'auto';992if (soundEnabled || announcementEnabled) {993this._accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired, { customAlertMessage: this._instantiationService.invokeFunction(getToolConfirmationAlert, pendingInvocations), userGesture: true, modality: !soundEnabled ? 'announcement' : undefined });994}995}996997private ensureToolDetails(dto: IToolInvocation, toolResult: IToolResult, toolData: IToolData): void {998if (!toolResult.toolResultDetails && toolData.alwaysDisplayInputOutput) {999toolResult.toolResultDetails = {1000input: this.formatToolInput(dto),1001output: this.toolResultToIO(toolResult),1002};1003}1004}10051006private formatToolInput(dto: IToolInvocation): string {1007return JSON.stringify(dto.parameters, undefined, 2);1008}10091010private toolResultToIO(toolResult: IToolResult): IToolResultInputOutputDetails['output'] {1011return toolResult.content.map(part => {1012if (part.kind === 'text') {1013return { type: 'embed', isText: true, value: part.value };1014} else if (part.kind === 'promptTsx') {1015return { type: 'embed', isText: true, value: stringifyPromptTsxPart(part) };1016} else if (part.kind === 'data') {1017return { type: 'embed', value: encodeBase64(part.value.data), mimeType: part.value.mimeType };1018} else {1019assertNever(part);1020}1021});1022}10231024private getEligibleForAutoApprovalSpecialCase(toolData: IToolData): string | undefined {1025if (toolData.id === 'vscode_fetchWebPage_internal') {1026return 'fetch';1027}1028return undefined;1029}10301031private isToolEligibleForAutoApproval(toolData: IToolData): boolean {1032const fullReferenceName = this.getEligibleForAutoApprovalSpecialCase(toolData) ?? getToolFullReferenceName(toolData);1033if (toolData.id === 'copilot_fetchWebPage') {1034// Special case, this fetch will call an internal tool 'vscode_fetchWebPage_internal'1035return true;1036}1037if (toolData.id === toolIdThatCannotBeAutoApproved) {1038// Special case, this tool will always require user confirmation as there are multiple options,1039// These aren't LM generated instead are generated by extension before agentic loop starts.1040return false;1041}1042const eligibilityConfig = this._configurationService.getValue<Record<string, boolean>>(ChatConfiguration.EligibleForAutoApproval);1043if (eligibilityConfig && typeof eligibilityConfig === 'object' && fullReferenceName) {1044// Direct match1045if (Object.prototype.hasOwnProperty.call(eligibilityConfig, fullReferenceName)) {1046return eligibilityConfig[fullReferenceName];1047}1048// Back compat with legacy names1049if (toolData.legacyToolReferenceFullNames) {1050for (const legacyName of toolData.legacyToolReferenceFullNames) {1051// Check if the full legacy name is in the config1052if (Object.prototype.hasOwnProperty.call(eligibilityConfig, legacyName)) {1053return eligibilityConfig[legacyName];1054}1055// Some tools may be both renamed and namespaced from a toolset, eg: xxx/yyy -> yyy1056if (legacyName.includes('/')) {1057const trimmedLegacyName = legacyName.split('/').pop();1058if (trimmedLegacyName && Object.prototype.hasOwnProperty.call(eligibilityConfig, trimmedLegacyName)) {1059return eligibilityConfig[trimmedLegacyName];1060}1061}1062}1063}1064}1065return true;1066}10671068private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise<ConfirmedReason | undefined> {1069const tool = this._tools.get(toolId);1070if (!tool) {1071return undefined;1072}10731074if (!this.isToolEligibleForAutoApproval(tool.data)) {1075return undefined;1076}10771078const reason = this._confirmationService.getPreConfirmAction({ toolId, source, parameters, chatSessionResource });1079if (reason) {1080return reason;1081}10821083const config = this._configurationService.inspect<boolean | Record<string, boolean>>(ChatConfiguration.GlobalAutoApprove);10841085// If we know the tool runs at a global level, only consider the global config.1086// If we know the tool runs at a workspace level, use those specific settings when appropriate.1087let value = config.value ?? config.defaultValue;1088if (typeof runsInWorkspace === 'boolean') {1089value = config.userLocalValue ?? config.applicationValue;1090if (runsInWorkspace) {1091value = config.workspaceValue ?? config.workspaceFolderValue ?? config.userRemoteValue ?? value;1092}1093}10941095const autoConfirm = value === true || (typeof value === 'object' && value.hasOwnProperty(toolId) && value[toolId] === true);1096if (autoConfirm) {1097if (await this._checkGlobalAutoApprove()) {1098return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove };1099}1100}11011102return undefined;1103}11041105private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise<ConfirmedReason | undefined> {1106if (this._configurationService.getValue<boolean>(ChatConfiguration.GlobalAutoApprove) && await this._checkGlobalAutoApprove()) {1107return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove };1108}11091110return this._confirmationService.getPostConfirmAction({ toolId, source, parameters, chatSessionResource });1111}11121113private async _checkGlobalAutoApprove(): Promise<boolean> {1114const optedIn = this._storageService.getBoolean(AutoApproveStorageKeys.GlobalAutoApproveOptIn, StorageScope.APPLICATION, false);1115if (optedIn) {1116return true;1117}11181119if (this._contextKeyService.getContextKeyValue(SkipAutoApproveConfirmationKey) === true) {1120return true;1121}11221123const promptResult = await this._dialogService.prompt({1124type: Severity.Warning,1125message: localize('autoApprove2.title', 'Enable global auto approve?'),1126buttons: [1127{1128label: localize('autoApprove2.button.enable', 'Enable'),1129run: () => true1130},1131{1132label: localize('autoApprove2.button.disable', 'Disable'),1133run: () => false1134},1135],1136custom: {1137icon: Codicon.warning,1138disableCloseAction: true,1139markdownDetails: [{1140markdown: new MarkdownString(globalAutoApproveDescription.value),1141}],1142}1143});11441145if (promptResult.result !== true) {1146await this._configurationService.updateValue(ChatConfiguration.GlobalAutoApprove, false);1147return false;1148}11491150this._storageService.store(AutoApproveStorageKeys.GlobalAutoApproveOptIn, true, StorageScope.APPLICATION, StorageTarget.USER);1151return true;1152}11531154private cleanupCallDisposables(requestId: string | undefined, store: DisposableStore): void {1155if (requestId) {1156const disposables = this._callsByRequestId.get(requestId);1157if (disposables) {1158const index = disposables.findIndex(d => d.store === store);1159if (index > -1) {1160disposables.splice(index, 1);1161}1162if (disposables.length === 0) {1163this._callsByRequestId.delete(requestId);1164}1165}1166}11671168store.dispose();1169}11701171cancelToolCallsForRequest(requestId: string): void {1172const calls = this._callsByRequestId.get(requestId);1173if (calls) {1174calls.forEach(call => call.store.dispose());1175this._callsByRequestId.delete(requestId);1176}11771178// Clean up any pending tool calls that belong to this request1179for (const [toolCallId, invocation] of this._pendingToolCalls) {1180if (invocation.chatRequestId === requestId) {1181this._pendingToolCalls.delete(toolCallId);1182}1183}1184}11851186private static readonly githubMCPServerAliases = ['github/github-mcp-server', 'io.github.github/github-mcp-server', 'github-mcp-server'];1187private static readonly playwrightMCPServerAliases = ['microsoft/playwright-mcp', 'com.microsoft/playwright-mcp'];11881189private *getToolSetAliases(toolSet: ToolSet, fullReferenceName: string): Iterable<string> {1190if (fullReferenceName !== toolSet.referenceName) {1191yield toolSet.referenceName; // tool set name without '/*'1192}1193if (toolSet.legacyFullNames) {1194yield* toolSet.legacyFullNames;1195}1196switch (toolSet.referenceName) {1197case 'github':1198for (const alias of LanguageModelToolsService.githubMCPServerAliases) {1199yield alias + '/*';1200}1201break;1202case 'playwright':1203for (const alias of LanguageModelToolsService.playwrightMCPServerAliases) {1204yield alias + '/*';1205}1206break;1207case SpecedToolAliases.execute: // 'execute'1208yield 'shell'; // legacy alias1209break;1210case SpecedToolAliases.agent: // 'agent'1211yield VSCodeToolReference.runSubagent; // prefer the tool set over th old tool name1212yield 'custom-agent'; // legacy alias1213break;1214}1215}12161217private * getToolAliases(toolSet: IToolData, fullReferenceName: string): Iterable<string> {1218const referenceName = toolSet.toolReferenceName ?? toolSet.displayName;1219if (fullReferenceName !== referenceName && referenceName !== VSCodeToolReference.runSubagent) {1220yield referenceName; // simple name, without toolset name1221}1222if (toolSet.legacyToolReferenceFullNames) {1223for (const legacyName of toolSet.legacyToolReferenceFullNames) {1224yield legacyName;1225const lastSlashIndex = legacyName.lastIndexOf('/');1226if (lastSlashIndex !== -1) {1227yield legacyName.substring(lastSlashIndex + 1); // it was also known under the simple name1228}1229}1230}1231const slashIndex = fullReferenceName.lastIndexOf('/');1232if (slashIndex !== -1) {1233switch (fullReferenceName.substring(0, slashIndex)) {1234case 'github':1235for (const alias of LanguageModelToolsService.githubMCPServerAliases) {1236yield alias + fullReferenceName.substring(slashIndex);1237}1238break;1239case 'playwright':1240for (const alias of LanguageModelToolsService.playwrightMCPServerAliases) {1241yield alias + fullReferenceName.substring(slashIndex);1242}1243break;1244}1245}1246}12471248/**1249* Create a map that contains all tools and toolsets with their enablement state.1250* @param fullReferenceNames A list of tool or toolset by their full reference names that are enabled.1251* @returns A map of tool or toolset instances to their enablement state.1252*/1253toToolAndToolSetEnablementMap(fullReferenceNames: readonly string[], _target: string | undefined, model: ILanguageModelChatMetadata | undefined): IToolAndToolSetEnablementMap {1254const toolOrToolSetNames = new Set(fullReferenceNames);1255const result = new Map<IToolSet | IToolData, boolean>();1256for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {1257if (isToolSet(tool)) {1258const enabled = toolOrToolSetNames.has(fullReferenceName) || Iterable.some(this.getToolSetAliases(tool, fullReferenceName), name => toolOrToolSetNames.has(name));1259const scoped = model ? new ToolSetForModel(tool, model) : tool;1260result.set(scoped, enabled);1261if (enabled) {1262for (const memberTool of scoped.getTools()) {1263result.set(memberTool, true);1264}1265}1266} else {1267if (model && !toolMatchesModel(tool, model)) {1268continue;1269}12701271if (!result.has(tool)) { // already set via an enabled toolset1272const enabled = toolOrToolSetNames.has(fullReferenceName)1273|| Iterable.some(this.getToolAliases(tool, fullReferenceName), name => toolOrToolSetNames.has(name))1274|| !!tool.legacyToolReferenceFullNames?.some(toolFullName => {1275// enable tool if just the legacy tool set name is present1276const index = toolFullName.lastIndexOf('/');1277return index !== -1 && toolOrToolSetNames.has(toolFullName.substring(0, index));1278});1279result.set(tool, enabled);1280}1281}1282}12831284// also add all user tool sets (not part of the prompt referencable tools)1285for (const toolSet of this._toolSets) {1286if (toolSet.source.type === 'user') {1287const enabled = Iterable.every(toolSet.getTools(), t => result.get(t) === true);1288result.set(toolSet, enabled);1289}1290}1291return result;1292}12931294toFullReferenceNames(map: IToolAndToolSetEnablementMap): string[] {1295const result: string[] = [];1296const toolsCoveredByEnabledToolSet = new Set<IToolData>();1297for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {1298if (isToolSet(tool)) {1299if (map.get(tool)) {1300result.push(fullReferenceName);1301for (const memberTool of tool.getTools()) {1302toolsCoveredByEnabledToolSet.add(memberTool);1303}1304}1305} else {1306if (map.get(tool) && !toolsCoveredByEnabledToolSet.has(tool)) {1307result.push(fullReferenceName);1308}1309}1310}1311return result;1312}13131314toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] {1315const toolsOrToolSetByName = new Map<string, ToolSet | IToolData>();1316for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {1317toolsOrToolSetByName.set(fullReferenceName, tool);1318}13191320const result: ChatRequestToolReferenceEntry[] = [];1321for (const ref of variableReferences) {1322const toolOrToolSet = toolsOrToolSetByName.get(ref.name);1323if (toolOrToolSet) {1324if (isToolSet(toolOrToolSet)) {1325result.push(toToolSetVariableEntry(toolOrToolSet, ref.range));1326} else {1327result.push(toToolVariableEntry(toolOrToolSet, ref.range));1328}1329}1330}1331return result;1332}133313341335private readonly _toolSets = new ObservableSet<ToolSet>();13361337readonly toolSets: IObservable<Iterable<ToolSet>> = derived(this, reader => {1338const allToolSets = Array.from(this._toolSets.observable.read(reader));1339return allToolSets.filter(toolSet => this.isPermitted(toolSet, reader));1340});13411342getToolSetsForModel(model: ILanguageModelChatMetadata | undefined, reader?: IReader): Iterable<IToolSet> {1343if (!model) {1344return this.toolSets.read(reader);1345}13461347return Iterable.map(this.toolSets.read(reader), ts => new ToolSetForModel(ts, model));1348}13491350getToolSet(id: string): ToolSet | undefined {1351for (const toolSet of this._toolSets) {1352if (toolSet.id === id) {1353return toolSet;1354}1355}1356return undefined;1357}13581359getToolSetByName(name: string): ToolSet | undefined {1360for (const toolSet of this._toolSets) {1361if (toolSet.referenceName === name) {1362return toolSet;1363}1364}1365return undefined;1366}13671368getSpecedToolSetName(referenceName: string): string {1369if (LanguageModelToolsService.githubMCPServerAliases.includes(referenceName)) {1370return 'github';1371}1372if (LanguageModelToolsService.playwrightMCPServerAliases.includes(referenceName)) {1373return 'playwright';1374}1375return referenceName;1376}13771378createToolSet(source: ToolDataSource, id: string, referenceName: string, options?: { icon?: ThemeIcon; description?: string; legacyFullNames?: string[] }): ToolSet & IDisposable {13791380const that = this;13811382referenceName = this.getSpecedToolSetName(referenceName);13831384const result = new class extends ToolSet implements IDisposable {1385dispose(): void {1386if (that._toolSets.has(result)) {1387this._tools.clear();1388that._toolSets.delete(result);1389}13901391}1392}(id, referenceName, options?.icon ?? Codicon.tools, source, options?.description, options?.legacyFullNames, this._contextKeyService);13931394this._toolSets.add(result);1395return result;1396}13971398private readonly allToolsIncludingDisableObs = observableFromEventOpts<readonly IToolData[], void>(1399{ equalsFn: arrayEqualsC() },1400this.onDidChangeTools,1401() => Array.from(this.getAllToolsIncludingDisabled()),1402);14031404private readonly toolsWithFullReferenceName = derived<[IToolData | ToolSet, string][]>(reader => {1405const result: [IToolData | ToolSet, string][] = [];1406const coveredByToolSets = new Set<IToolData>();1407for (const toolSet of this.toolSets.read(reader)) {1408if (toolSet.source.type !== 'user') {1409result.push([toolSet, getToolSetFullReferenceName(toolSet)]);1410for (const tool of toolSet.getTools()) {1411result.push([tool, getToolFullReferenceName(tool, toolSet)]);1412coveredByToolSets.add(tool);1413}1414}1415}1416for (const tool of this.allToolsIncludingDisableObs.read(reader)) {1417// todo@connor4312/aeschil: this effectively hides model-specific tools1418// for prompt referencing. Should we eventually enable this? (If so how?)1419if (tool.when && !this._contextKeyService.contextMatchesRules(tool.when)) {1420continue;1421}14221423if (tool.canBeReferencedInPrompt && !coveredByToolSets.has(tool) && this.isPermitted(tool, reader)) {1424result.push([tool, getToolFullReferenceName(tool)]);1425}1426}1427return result;1428});14291430* getFullReferenceNames(): Iterable<string> {1431for (const [, fullReferenceName] of this.toolsWithFullReferenceName.get()) {1432yield fullReferenceName;1433}1434}14351436getDeprecatedFullReferenceNames(): Map<string, Set<string>> {1437const result = new Map<string, Set<string>>();1438const knownToolSetNames = new Set<string>();1439const add = (name: string, fullReferenceName: string) => {1440if (name !== fullReferenceName) {1441if (!result.has(name)) {1442result.set(name, new Set<string>());1443}1444result.get(name)!.add(fullReferenceName);1445}1446};14471448for (const [tool, _] of this.toolsWithFullReferenceName.get()) {1449if (isToolSet(tool)) {1450knownToolSetNames.add(tool.referenceName);1451if (tool.legacyFullNames) {1452for (const legacyName of tool.legacyFullNames) {1453knownToolSetNames.add(legacyName);1454}1455}1456}1457}14581459for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {1460if (isToolSet(tool)) {1461for (const alias of this.getToolSetAliases(tool, fullReferenceName)) {1462add(alias, fullReferenceName);1463}1464} else {1465for (const alias of this.getToolAliases(tool, fullReferenceName)) {1466add(alias, fullReferenceName);1467}1468if (tool.legacyToolReferenceFullNames) {1469for (const legacyName of tool.legacyToolReferenceFullNames) {1470// for any 'orphaned' toolsets (toolsets that no longer exist and1471// do not have an explicit legacy mapping), we should1472// just point them to the list of tools directly1473if (legacyName.includes('/')) {1474const toolSetFullName = legacyName.substring(0, legacyName.lastIndexOf('/'));1475if (!knownToolSetNames.has(toolSetFullName)) {1476add(toolSetFullName, fullReferenceName);1477}1478}1479}1480}1481}1482}1483return result;1484}14851486getToolByFullReferenceName(fullReferenceName: string): IToolData | ToolSet | undefined {1487for (const [tool, toolFullReferenceName] of this.toolsWithFullReferenceName.get()) {1488if (fullReferenceName === toolFullReferenceName) {1489return tool;1490}1491const aliases = isToolSet(tool) ? this.getToolSetAliases(tool, toolFullReferenceName) : this.getToolAliases(tool, toolFullReferenceName);1492if (Iterable.some(aliases, alias => fullReferenceName === alias)) {1493return tool;1494}1495}1496return undefined;1497}14981499getFullReferenceName(tool: IToolData | IToolSet, toolSet?: IToolSet): string {1500if (isToolSet(tool)) {1501return getToolSetFullReferenceName(tool);1502}1503return getToolFullReferenceName(tool, toolSet);1504}1505}15061507function getToolFullReferenceName(tool: IToolData, toolSet?: IToolSet) {1508const toolName = tool.toolReferenceName ?? tool.displayName;1509if (toolSet) {1510return `${toolSet.referenceName}/${toolName}`;1511} else if (tool.source.type === 'extension') {1512return `${tool.source.extensionId.value.toLowerCase()}/${toolName}`;1513}1514return toolName;1515}15161517function getToolSetFullReferenceName(toolSet: IToolSet) {1518if (toolSet.source.type === 'mcp') {1519return `${toolSet.referenceName}/*`;1520}1521return toolSet.referenceName;1522}152315241525type LanguageModelToolInvokedEvent = {1526result: 'success' | 'error' | 'userCancelled';1527chatSessionId: string | undefined;1528toolId: string;1529toolExtensionId: string | undefined;1530toolSourceKind: string;1531prepareTimeMs?: number;1532invocationTimeMs?: number;1533};15341535type LanguageModelToolInvokedClassification = {1536result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether invoking the LanguageModelTool resulted in an error.' };1537chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat session that the tool was used within, if applicable.' };1538toolId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the tool used.' };1539toolExtensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that contributed the tool.' };1540toolSourceKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source (mcp/extension/internal) of the tool.' };1541prepareTimeMs?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time spent in prepareToolInvocation method in milliseconds.' };1542invocationTimeMs?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time spent in tool invoke method in milliseconds.' };1543owner: 'roblourens';1544comment: 'Provides insight into the usage of language model tools.';1545};154615471548