Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js';
7
import { assertNever } from '../../../../base/common/assert.js';
8
import { RunOnceScheduler } from '../../../../base/common/async.js';
9
import { encodeBase64 } from '../../../../base/common/buffer.js';
10
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
11
import { Codicon } from '../../../../base/common/codicons.js';
12
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
13
import { CancellationError, isCancellationError } from '../../../../base/common/errors.js';
14
import { Emitter, Event } from '../../../../base/common/event.js';
15
import { MarkdownString } from '../../../../base/common/htmlContent.js';
16
import { Iterable } from '../../../../base/common/iterator.js';
17
import { Lazy } from '../../../../base/common/lazy.js';
18
import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
19
import { LRUCache } from '../../../../base/common/map.js';
20
import { IObservable, ObservableSet } from '../../../../base/common/observable.js';
21
import Severity from '../../../../base/common/severity.js';
22
import { ThemeIcon } from '../../../../base/common/themables.js';
23
import { localize, localize2 } from '../../../../nls.js';
24
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
25
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
26
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
27
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
28
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
29
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
30
import * as JSONContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';
31
import { ILogService } from '../../../../platform/log/common/log.js';
32
import { Registry } from '../../../../platform/registry/common/platform.js';
33
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
34
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
35
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
36
import { ChatContextKeys } from '../common/chatContextKeys.js';
37
import { ChatModel } from '../common/chatModel.js';
38
import { IVariableReference } from '../common/chatModes.js';
39
import { ChatToolInvocation } from '../common/chatProgressTypes/chatToolInvocation.js';
40
import { ConfirmedReason, IChatService, ToolConfirmKind } from '../common/chatService.js';
41
import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../common/chatVariableEntries.js';
42
import { ChatConfiguration } from '../common/constants.js';
43
import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, stringifyPromptTsxPart, ToolDataSource, ToolSet, IToolAndToolSetEnablementMap } from '../common/languageModelToolsService.js';
44
import { getToolConfirmationAlert } from './chatAccessibilityProvider.js';
45
46
const jsonSchemaRegistry = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);
47
48
interface IToolEntry {
49
data: IToolData;
50
impl?: IToolImpl;
51
}
52
53
interface ITrackedCall {
54
invocation?: ChatToolInvocation;
55
store: IDisposable;
56
}
57
58
const enum AutoApproveStorageKeys {
59
GlobalAutoApproveOptIn = 'chat.tools.global.autoApprove.optIn'
60
}
61
62
export const globalAutoApproveDescription = localize2(
63
{
64
key: 'autoApprove2.markdown',
65
comment: [
66
'{Locked=\'](https://github.com/features/codespaces)\'}',
67
'{Locked=\'](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)\'}',
68
'{Locked=\'](https://code.visualstudio.com/docs/copilot/security)\'}',
69
'{Locked=\'**\'}',
70
]
71
},
72
'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.**'
73
);
74
75
export class LanguageModelToolsService extends Disposable implements ILanguageModelToolsService {
76
_serviceBrand: undefined;
77
78
private _onDidChangeTools = new Emitter<void>();
79
readonly onDidChangeTools = this._onDidChangeTools.event;
80
81
/** Throttle tools updates because it sends all tools and runs on context key updates */
82
private _onDidChangeToolsScheduler = new RunOnceScheduler(() => this._onDidChangeTools.fire(), 750);
83
84
private _tools = new Map<string, IToolEntry>();
85
private _toolContextKeys = new Set<string>();
86
private readonly _ctxToolsCount: IContextKey<number>;
87
88
private _callsByRequestId = new Map<string, ITrackedCall[]>();
89
90
private _workspaceToolConfirmStore: Lazy<ToolConfirmStore>;
91
private _profileToolConfirmStore: Lazy<ToolConfirmStore>;
92
private _memoryToolConfirmStore = new Set<string>();
93
94
constructor(
95
@IInstantiationService private readonly _instantiationService: IInstantiationService,
96
@IExtensionService private readonly _extensionService: IExtensionService,
97
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
98
@IChatService private readonly _chatService: IChatService,
99
@IDialogService private readonly _dialogService: IDialogService,
100
@ITelemetryService private readonly _telemetryService: ITelemetryService,
101
@ILogService private readonly _logService: ILogService,
102
@IConfigurationService private readonly _configurationService: IConfigurationService,
103
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
104
@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,
105
@IStorageService private readonly _storageService: IStorageService,
106
) {
107
super();
108
109
this._workspaceToolConfirmStore = new Lazy(() => this._register(this._instantiationService.createInstance(ToolConfirmStore, StorageScope.WORKSPACE)));
110
this._profileToolConfirmStore = new Lazy(() => this._register(this._instantiationService.createInstance(ToolConfirmStore, StorageScope.PROFILE)));
111
112
this._register(this._contextKeyService.onDidChangeContext(e => {
113
if (e.affectsSome(this._toolContextKeys)) {
114
// Not worth it to compute a delta here unless we have many tools changing often
115
this._onDidChangeToolsScheduler.schedule();
116
}
117
}));
118
119
this._register(this._configurationService.onDidChangeConfiguration(e => {
120
if (e.affectsConfiguration(ChatConfiguration.ExtensionToolsEnabled)) {
121
this._onDidChangeToolsScheduler.schedule();
122
}
123
}));
124
125
// Clear out warning accepted state if the setting is disabled
126
this._register(Event.runAndSubscribe(this._configurationService.onDidChangeConfiguration, e => {
127
if (!e || e.affectsConfiguration(ChatConfiguration.GlobalAutoApprove)) {
128
if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) !== true) {
129
this._storageService.remove(AutoApproveStorageKeys.GlobalAutoApproveOptIn, StorageScope.APPLICATION);
130
}
131
}
132
}));
133
134
this._ctxToolsCount = ChatContextKeys.Tools.toolsCount.bindTo(_contextKeyService);
135
}
136
override dispose(): void {
137
super.dispose();
138
139
this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose()));
140
this._ctxToolsCount.reset();
141
}
142
143
registerToolData(toolData: IToolData): IDisposable {
144
if (this._tools.has(toolData.id)) {
145
throw new Error(`Tool "${toolData.id}" is already registered.`);
146
}
147
148
this._tools.set(toolData.id, { data: toolData });
149
this._ctxToolsCount.set(this._tools.size);
150
this._onDidChangeToolsScheduler.schedule();
151
152
toolData.when?.keys().forEach(key => this._toolContextKeys.add(key));
153
154
let store: DisposableStore | undefined;
155
if (toolData.inputSchema) {
156
store = new DisposableStore();
157
const schemaUrl = createToolSchemaUri(toolData.id).toString();
158
jsonSchemaRegistry.registerSchema(schemaUrl, toolData.inputSchema, store);
159
store.add(jsonSchemaRegistry.registerSchemaAssociation(schemaUrl, `/lm/tool/${toolData.id}/tool_input.json`));
160
}
161
162
return toDisposable(() => {
163
store?.dispose();
164
this._tools.delete(toolData.id);
165
this._ctxToolsCount.set(this._tools.size);
166
this._refreshAllToolContextKeys();
167
this._onDidChangeToolsScheduler.schedule();
168
});
169
}
170
171
private _refreshAllToolContextKeys() {
172
this._toolContextKeys.clear();
173
for (const tool of this._tools.values()) {
174
tool.data.when?.keys().forEach(key => this._toolContextKeys.add(key));
175
}
176
}
177
178
registerToolImplementation(id: string, tool: IToolImpl): IDisposable {
179
const entry = this._tools.get(id);
180
if (!entry) {
181
throw new Error(`Tool "${id}" was not contributed.`);
182
}
183
184
if (entry.impl) {
185
throw new Error(`Tool "${id}" already has an implementation.`);
186
}
187
188
entry.impl = tool;
189
return toDisposable(() => {
190
entry.impl = undefined;
191
});
192
}
193
194
registerTool(toolData: IToolData, tool: IToolImpl): IDisposable {
195
return combinedDisposable(
196
this.registerToolData(toolData),
197
this.registerToolImplementation(toolData.id, tool)
198
);
199
}
200
201
getTools(includeDisabled?: boolean): Iterable<Readonly<IToolData>> {
202
const toolDatas = Iterable.map(this._tools.values(), i => i.data);
203
const extensionToolsEnabled = this._configurationService.getValue<boolean>(ChatConfiguration.ExtensionToolsEnabled);
204
return Iterable.filter(
205
toolDatas,
206
toolData => {
207
const satisfiesWhenClause = includeDisabled || !toolData.when || this._contextKeyService.contextMatchesRules(toolData.when);
208
const satisfiesExternalToolCheck = toolData.source.type !== 'extension' || !!extensionToolsEnabled;
209
return satisfiesWhenClause && satisfiesExternalToolCheck;
210
});
211
}
212
213
getTool(id: string): IToolData | undefined {
214
return this._getToolEntry(id)?.data;
215
}
216
217
private _getToolEntry(id: string): IToolEntry | undefined {
218
const entry = this._tools.get(id);
219
if (entry && (!entry.data.when || this._contextKeyService.contextMatchesRules(entry.data.when))) {
220
return entry;
221
} else {
222
return undefined;
223
}
224
}
225
226
getToolByName(name: string, includeDisabled?: boolean): IToolData | undefined {
227
for (const tool of this.getTools(!!includeDisabled)) {
228
if (tool.toolReferenceName === name) {
229
return tool;
230
}
231
}
232
return undefined;
233
}
234
235
setToolAutoConfirmation(toolId: string, scope: 'workspace' | 'profile' | 'session' | 'never'): void {
236
this._workspaceToolConfirmStore.value.setAutoConfirm(toolId, scope === 'workspace');
237
this._profileToolConfirmStore.value.setAutoConfirm(toolId, scope === 'profile');
238
239
if (scope === 'session') {
240
this._memoryToolConfirmStore.add(toolId);
241
} else {
242
this._memoryToolConfirmStore.delete(toolId);
243
}
244
}
245
246
getToolAutoConfirmation(toolId: string): 'workspace' | 'profile' | 'session' | 'never' {
247
if (this._workspaceToolConfirmStore.value.getAutoConfirm(toolId)) {
248
return 'workspace';
249
}
250
if (this._profileToolConfirmStore.value.getAutoConfirm(toolId)) {
251
return 'profile';
252
}
253
if (this._memoryToolConfirmStore.has(toolId)) {
254
return 'session';
255
}
256
return 'never';
257
}
258
259
resetToolAutoConfirmation(): void {
260
this._workspaceToolConfirmStore.value.reset();
261
this._profileToolConfirmStore.value.reset();
262
this._memoryToolConfirmStore.clear();
263
}
264
265
async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise<IToolResult> {
266
this._logService.trace(`[LanguageModelToolsService#invokeTool] Invoking tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}`);
267
268
// 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.
269
let tool = this._tools.get(dto.toolId);
270
if (!tool) {
271
throw new Error(`Tool ${dto.toolId} was not contributed`);
272
}
273
274
if (!tool.impl) {
275
await this._extensionService.activateByEvent(`onLanguageModelTool:${dto.toolId}`);
276
277
// Extension should activate and register the tool implementation
278
tool = this._tools.get(dto.toolId);
279
if (!tool?.impl) {
280
throw new Error(`Tool ${dto.toolId} does not have an implementation registered.`);
281
}
282
}
283
284
// Shortcut to write to the model directly here, but could call all the way back to use the real stream.
285
let toolInvocation: ChatToolInvocation | undefined;
286
287
let requestId: string | undefined;
288
let store: DisposableStore | undefined;
289
let toolResult: IToolResult | undefined;
290
try {
291
if (dto.context) {
292
store = new DisposableStore();
293
const model = this._chatService.getSession(dto.context?.sessionId) as ChatModel | undefined;
294
if (!model) {
295
throw new Error(`Tool called for unknown chat session`);
296
}
297
298
const request = model.getRequests().at(-1)!;
299
requestId = request.id;
300
dto.modelId = request.modelId;
301
302
// Replace the token with a new token that we can cancel when cancelToolCallsForRequest is called
303
if (!this._callsByRequestId.has(requestId)) {
304
this._callsByRequestId.set(requestId, []);
305
}
306
const trackedCall: ITrackedCall = { store };
307
this._callsByRequestId.get(requestId)!.push(trackedCall);
308
309
const source = new CancellationTokenSource();
310
store.add(toDisposable(() => {
311
source.dispose(true);
312
}));
313
store.add(token.onCancellationRequested(() => {
314
toolInvocation?.confirmed.complete({ type: ToolConfirmKind.Denied });
315
source.cancel();
316
}));
317
store.add(source.token.onCancellationRequested(() => {
318
toolInvocation?.confirmed.complete({ type: ToolConfirmKind.Denied });
319
}));
320
token = source.token;
321
322
const prepared = await this.prepareToolInvocation(tool, dto, token);
323
toolInvocation = new ChatToolInvocation(prepared, tool.data, dto.callId);
324
trackedCall.invocation = toolInvocation;
325
const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace);
326
if (autoConfirmed) {
327
toolInvocation.confirmed.complete(autoConfirmed);
328
}
329
330
model.acceptResponseProgress(request, toolInvocation);
331
332
dto.toolSpecificData = toolInvocation?.toolSpecificData;
333
334
if (prepared?.confirmationMessages) {
335
if (!toolInvocation.isConfirmed?.type && !autoConfirmed) {
336
this.playAccessibilitySignal([toolInvocation]);
337
}
338
const userConfirmed = await toolInvocation.confirmed.p;
339
if (userConfirmed.type === ToolConfirmKind.Denied) {
340
throw new CancellationError();
341
}
342
if (userConfirmed.type === ToolConfirmKind.Skipped) {
343
toolResult = {
344
content: [{
345
kind: 'text',
346
value: 'The user chose to skip the tool call, they want to proceed without running it'
347
}]
348
};
349
return toolResult;
350
}
351
352
if (dto.toolSpecificData?.kind === 'input') {
353
dto.parameters = dto.toolSpecificData.rawInput;
354
dto.toolSpecificData = undefined;
355
}
356
}
357
} else {
358
const prepared = await this.prepareToolInvocation(tool, dto, token);
359
if (prepared?.confirmationMessages && !(await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace))) {
360
const result = await this._dialogService.confirm({ message: renderAsPlaintext(prepared.confirmationMessages.title), detail: renderAsPlaintext(prepared.confirmationMessages.message) });
361
if (!result.confirmed) {
362
throw new CancellationError();
363
}
364
}
365
366
dto.toolSpecificData = prepared?.toolSpecificData;
367
}
368
369
if (token.isCancellationRequested) {
370
throw new CancellationError();
371
}
372
373
toolResult = await tool.impl.invoke(dto, countTokens, {
374
report: step => {
375
toolInvocation?.acceptProgress(step);
376
}
377
}, token);
378
this.ensureToolDetails(dto, toolResult, tool.data);
379
380
this._telemetryService.publicLog2<LanguageModelToolInvokedEvent, LanguageModelToolInvokedClassification>(
381
'languageModelToolInvoked',
382
{
383
result: 'success',
384
chatSessionId: dto.context?.sessionId,
385
toolId: tool.data.id,
386
toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined,
387
toolSourceKind: tool.data.source.type,
388
});
389
return toolResult;
390
} catch (err) {
391
const result = isCancellationError(err) ? 'userCancelled' : 'error';
392
this._telemetryService.publicLog2<LanguageModelToolInvokedEvent, LanguageModelToolInvokedClassification>(
393
'languageModelToolInvoked',
394
{
395
result,
396
chatSessionId: dto.context?.sessionId,
397
toolId: tool.data.id,
398
toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined,
399
toolSourceKind: tool.data.source.type,
400
});
401
this._logService.error(`[LanguageModelToolsService#invokeTool] Error from tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}:\n${toErrorMessage(err, true)}`);
402
403
toolResult ??= { content: [] };
404
toolResult.toolResultError = err instanceof Error ? err.message : String(err);
405
if (tool.data.alwaysDisplayInputOutput) {
406
toolResult.toolResultDetails = { input: this.formatToolInput(dto), output: [{ type: 'embed', isText: true, value: String(err) }], isError: true };
407
}
408
409
throw err;
410
} finally {
411
toolInvocation?.complete(toolResult);
412
413
if (store) {
414
this.cleanupCallDisposables(requestId, store);
415
}
416
}
417
}
418
419
private async prepareToolInvocation(tool: IToolEntry, dto: IToolInvocation, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
420
const prepared = tool.impl!.prepareToolInvocation ?
421
await tool.impl!.prepareToolInvocation({
422
parameters: dto.parameters,
423
chatRequestId: dto.chatRequestId,
424
chatSessionId: dto.context?.sessionId,
425
chatInteractionId: dto.chatInteractionId
426
}, token)
427
: undefined;
428
429
if (prepared?.confirmationMessages) {
430
if (prepared.toolSpecificData?.kind !== 'terminal' && typeof prepared.confirmationMessages.allowAutoConfirm !== 'boolean') {
431
prepared.confirmationMessages.allowAutoConfirm = true;
432
}
433
434
if (!prepared.toolSpecificData && tool.data.alwaysDisplayInputOutput) {
435
prepared.toolSpecificData = {
436
kind: 'input',
437
rawInput: dto.parameters,
438
};
439
}
440
}
441
442
return prepared;
443
}
444
445
private playAccessibilitySignal(toolInvocations: ChatToolInvocation[]): void {
446
const autoApproved = this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove);
447
if (autoApproved) {
448
return;
449
}
450
const setting: { sound?: 'auto' | 'on' | 'off'; announcement?: 'auto' | 'off' } | undefined = this._configurationService.getValue(AccessibilitySignal.chatUserActionRequired.settingsKey);
451
if (!setting) {
452
return;
453
}
454
const soundEnabled = setting.sound === 'on' || (setting.sound === 'auto' && (this._accessibilityService.isScreenReaderOptimized()));
455
const announcementEnabled = this._accessibilityService.isScreenReaderOptimized() && setting.announcement === 'auto';
456
if (soundEnabled || announcementEnabled) {
457
this._accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired, { customAlertMessage: this._instantiationService.invokeFunction(getToolConfirmationAlert, toolInvocations), userGesture: true, modality: !soundEnabled ? 'announcement' : undefined });
458
}
459
}
460
461
private ensureToolDetails(dto: IToolInvocation, toolResult: IToolResult, toolData: IToolData): void {
462
if (!toolResult.toolResultDetails && toolData.alwaysDisplayInputOutput) {
463
toolResult.toolResultDetails = {
464
input: this.formatToolInput(dto),
465
output: this.toolResultToIO(toolResult),
466
};
467
}
468
}
469
470
private formatToolInput(dto: IToolInvocation): string {
471
return JSON.stringify(dto.parameters, undefined, 2);
472
}
473
474
private toolResultToIO(toolResult: IToolResult): IToolResultInputOutputDetails['output'] {
475
return toolResult.content.map(part => {
476
if (part.kind === 'text') {
477
return { type: 'embed', isText: true, value: part.value };
478
} else if (part.kind === 'promptTsx') {
479
return { type: 'embed', isText: true, value: stringifyPromptTsxPart(part) };
480
} else if (part.kind === 'data') {
481
return { type: 'embed', value: encodeBase64(part.value.data), mimeType: part.value.mimeType };
482
} else {
483
assertNever(part);
484
}
485
});
486
}
487
488
private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined): Promise<ConfirmedReason | undefined> {
489
if (this._workspaceToolConfirmStore.value.getAutoConfirm(toolId)) {
490
return { type: ToolConfirmKind.LmServicePerTool, scope: 'workspace' };
491
}
492
if (this._profileToolConfirmStore.value.getAutoConfirm(toolId)) {
493
return { type: ToolConfirmKind.LmServicePerTool, scope: 'profile' };
494
}
495
if (this._memoryToolConfirmStore.has(toolId)) {
496
return { type: ToolConfirmKind.LmServicePerTool, scope: 'session' };
497
}
498
499
const config = this._configurationService.inspect<boolean | Record<string, boolean>>(ChatConfiguration.GlobalAutoApprove);
500
501
// If we know the tool runs at a global level, only consider the global config.
502
// If we know the tool runs at a workspace level, use those specific settings when appropriate.
503
let value = config.value ?? config.defaultValue;
504
if (typeof runsInWorkspace === 'boolean') {
505
value = config.userLocalValue ?? config.applicationValue;
506
if (runsInWorkspace) {
507
value = config.workspaceValue ?? config.workspaceFolderValue ?? config.userRemoteValue ?? value;
508
}
509
}
510
511
const autoConfirm = value === true || (typeof value === 'object' && value.hasOwnProperty(toolId) && value[toolId] === true);
512
if (autoConfirm) {
513
if (await this._checkGlobalAutoApprove()) {
514
return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove };
515
}
516
}
517
518
return undefined;
519
}
520
521
private async _checkGlobalAutoApprove(): Promise<boolean> {
522
const optedIn = this._storageService.getBoolean(AutoApproveStorageKeys.GlobalAutoApproveOptIn, StorageScope.APPLICATION, false);
523
if (optedIn) {
524
return true;
525
}
526
527
const promptResult = await this._dialogService.prompt({
528
type: Severity.Warning,
529
message: localize('autoApprove2.title', 'Enable global auto approve?'),
530
buttons: [
531
{
532
label: localize('autoApprove2.button.enable', 'Enable'),
533
run: () => true
534
},
535
{
536
label: localize('autoApprove2.button.disable', 'Disable'),
537
run: () => false
538
},
539
],
540
custom: {
541
icon: Codicon.warning,
542
disableCloseAction: true,
543
markdownDetails: [{
544
markdown: new MarkdownString(globalAutoApproveDescription.value),
545
}],
546
}
547
});
548
549
if (promptResult.result !== true) {
550
await this._configurationService.updateValue(ChatConfiguration.GlobalAutoApprove, false);
551
return false;
552
}
553
554
this._storageService.store(AutoApproveStorageKeys.GlobalAutoApproveOptIn, true, StorageScope.APPLICATION, StorageTarget.USER);
555
return true;
556
}
557
558
private cleanupCallDisposables(requestId: string | undefined, store: DisposableStore): void {
559
if (requestId) {
560
const disposables = this._callsByRequestId.get(requestId);
561
if (disposables) {
562
const index = disposables.findIndex(d => d.store === store);
563
if (index > -1) {
564
disposables.splice(index, 1);
565
}
566
if (disposables.length === 0) {
567
this._callsByRequestId.delete(requestId);
568
}
569
}
570
}
571
572
store.dispose();
573
}
574
575
cancelToolCallsForRequest(requestId: string): void {
576
const calls = this._callsByRequestId.get(requestId);
577
if (calls) {
578
calls.forEach(call => call.store.dispose());
579
this._callsByRequestId.delete(requestId);
580
}
581
}
582
583
toToolEnablementMap(toolOrToolsetNames: Set<string>): Record<string, boolean> {
584
const result: Record<string, boolean> = {};
585
for (const tool of this._tools.values()) {
586
if (tool.data.toolReferenceName && toolOrToolsetNames.has(tool.data.toolReferenceName)) {
587
result[tool.data.id] = true;
588
} else {
589
result[tool.data.id] = false;
590
}
591
}
592
593
for (const toolSet of this._toolSets) {
594
if (toolOrToolsetNames.has(toolSet.referenceName)) {
595
for (const tool of toolSet.getTools()) {
596
result[tool.id] = true;
597
}
598
}
599
}
600
601
return result;
602
}
603
604
/**
605
* Create a map that contains all tools and toolsets with their enablement state.
606
* @param toolOrToolSetNames A list of tool or toolset names that are enabled.
607
* @returns A map of tool or toolset instances to their enablement state.
608
*/
609
toToolAndToolSetEnablementMap(enabledToolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap {
610
const toolOrToolSetNames = new Set(enabledToolOrToolSetNames);
611
const result = new Map<ToolSet | IToolData, boolean>();
612
for (const tool of this.getTools()) {
613
if (tool.canBeReferencedInPrompt) {
614
result.set(tool, toolOrToolSetNames.has(tool.toolReferenceName ?? tool.displayName));
615
}
616
}
617
for (const toolSet of this._toolSets) {
618
const enabled = toolOrToolSetNames.has(toolSet.referenceName);
619
result.set(toolSet, enabled);
620
for (const tool of toolSet.getTools()) {
621
result.set(tool, enabled || toolOrToolSetNames?.has(tool.toolReferenceName ?? tool.displayName));
622
}
623
624
}
625
return result;
626
}
627
628
public toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] {
629
const toolsOrToolSetByName = new Map<string, ToolSet | IToolData>();
630
for (const toolSet of this.toolSets.get()) {
631
toolsOrToolSetByName.set(toolSet.referenceName, toolSet);
632
}
633
for (const tool of this.getTools()) {
634
toolsOrToolSetByName.set(tool.toolReferenceName ?? tool.displayName, tool);
635
}
636
637
const result: ChatRequestToolReferenceEntry[] = [];
638
for (const ref of variableReferences) {
639
const toolOrToolSet = toolsOrToolSetByName.get(ref.name);
640
if (toolOrToolSet) {
641
if (toolOrToolSet instanceof ToolSet) {
642
result.push(toToolSetVariableEntry(toolOrToolSet, ref.range));
643
} else {
644
result.push(toToolVariableEntry(toolOrToolSet, ref.range));
645
}
646
}
647
}
648
return result;
649
}
650
651
652
private readonly _toolSets = new ObservableSet<ToolSet>();
653
654
readonly toolSets: IObservable<Iterable<ToolSet>> = this._toolSets.observable;
655
656
getToolSet(id: string): ToolSet | undefined {
657
for (const toolSet of this._toolSets) {
658
if (toolSet.id === id) {
659
return toolSet;
660
}
661
}
662
return undefined;
663
}
664
665
getToolSetByName(name: string): ToolSet | undefined {
666
for (const toolSet of this._toolSets) {
667
if (toolSet.referenceName === name) {
668
return toolSet;
669
}
670
}
671
return undefined;
672
}
673
674
createToolSet(source: ToolDataSource, id: string, referenceName: string, options?: { icon?: ThemeIcon; description?: string }): ToolSet & IDisposable {
675
676
const that = this;
677
678
const result = new class extends ToolSet implements IDisposable {
679
dispose(): void {
680
if (that._toolSets.has(result)) {
681
this._tools.clear();
682
that._toolSets.delete(result);
683
}
684
685
}
686
}(id, referenceName, options?.icon ?? Codicon.tools, source, options?.description);
687
688
this._toolSets.add(result);
689
return result;
690
}
691
}
692
693
type LanguageModelToolInvokedEvent = {
694
result: 'success' | 'error' | 'userCancelled';
695
chatSessionId: string | undefined;
696
toolId: string;
697
toolExtensionId: string | undefined;
698
toolSourceKind: string;
699
};
700
701
type LanguageModelToolInvokedClassification = {
702
result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether invoking the LanguageModelTool resulted in an error.' };
703
chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat session that the tool was used within, if applicable.' };
704
toolId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the tool used.' };
705
toolExtensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that contributed the tool.' };
706
toolSourceKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source (mcp/extension/internal) of the tool.' };
707
owner: 'roblourens';
708
comment: 'Provides insight into the usage of language model tools.';
709
};
710
711
class ToolConfirmStore extends Disposable {
712
private static readonly STORED_KEY = 'chat/autoconfirm';
713
714
private _autoConfirmTools: LRUCache<string, boolean> = new LRUCache<string, boolean>(100);
715
private _didChange = false;
716
717
constructor(
718
private readonly _scope: StorageScope,
719
@IStorageService private readonly storageService: IStorageService,
720
) {
721
super();
722
723
const stored = storageService.getObject<string[]>(ToolConfirmStore.STORED_KEY, this._scope);
724
if (stored) {
725
for (const key of stored) {
726
this._autoConfirmTools.set(key, true);
727
}
728
}
729
730
this._register(storageService.onWillSaveState(() => {
731
if (this._didChange) {
732
this.storageService.store(ToolConfirmStore.STORED_KEY, [...this._autoConfirmTools.keys()], this._scope, StorageTarget.MACHINE);
733
this._didChange = false;
734
}
735
}));
736
}
737
738
public reset() {
739
this._autoConfirmTools.clear();
740
this._didChange = true;
741
}
742
743
public getAutoConfirm(toolId: string): boolean {
744
if (this._autoConfirmTools.get(toolId)) {
745
this._didChange = true;
746
return true;
747
}
748
749
return false;
750
}
751
752
public setAutoConfirm(toolId: string, autoConfirm: boolean): void {
753
if (autoConfirm) {
754
this._autoConfirmTools.set(toolId, true);
755
} else {
756
this._autoConfirmTools.delete(toolId);
757
}
758
this._didChange = true;
759
}
760
}
761
762