Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts
4780 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, timeout } 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 { arrayEqualsC } from '../../../../../base/common/equals.js';
13
import { toErrorMessage } from '../../../../../base/common/errorMessage.js';
14
import { CancellationError, isCancellationError } from '../../../../../base/common/errors.js';
15
import { Emitter, Event } from '../../../../../base/common/event.js';
16
import { createMarkdownCommandLink, MarkdownString } from '../../../../../base/common/htmlContent.js';
17
import { Iterable } from '../../../../../base/common/iterator.js';
18
import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
19
import { derived, IObservable, IReader, observableFromEventOpts, ObservableSet } from '../../../../../base/common/observable.js';
20
import Severity from '../../../../../base/common/severity.js';
21
import { StopWatch } from '../../../../../base/common/stopwatch.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 { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';
33
import { Registry } from '../../../../../platform/registry/common/platform.js';
34
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
35
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
36
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
37
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
38
import { IVariableReference } from '../../common/chatModes.js';
39
import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToolInvocation.js';
40
import { ConfirmedReason, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js';
41
import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js';
42
import { ChatConfiguration } from '../../common/constants.js';
43
import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js';
44
import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, ToolSet, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js';
45
import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js';
46
47
const jsonSchemaRegistry = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);
48
49
interface IToolEntry {
50
data: IToolData;
51
impl?: IToolImpl;
52
}
53
54
interface ITrackedCall {
55
invocation?: ChatToolInvocation;
56
store: IDisposable;
57
}
58
59
const enum AutoApproveStorageKeys {
60
GlobalAutoApproveOptIn = 'chat.tools.global.autoApprove.optIn'
61
}
62
63
const SkipAutoApproveConfirmationKey = 'vscode.chat.tools.global.autoApprove.testMode';
64
65
export const globalAutoApproveDescription = localize2(
66
{
67
key: 'autoApprove2.markdown',
68
comment: [
69
'{Locked=\'](https://github.com/features/codespaces)\'}',
70
'{Locked=\'](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)\'}',
71
'{Locked=\'](https://code.visualstudio.com/docs/copilot/security)\'}',
72
'{Locked=\'**\'}',
73
]
74
},
75
'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.**'
76
);
77
78
export class LanguageModelToolsService extends Disposable implements ILanguageModelToolsService {
79
_serviceBrand: undefined;
80
readonly vscodeToolSet: ToolSet;
81
readonly executeToolSet: ToolSet;
82
readonly readToolSet: ToolSet;
83
84
private readonly _onDidChangeTools = this._register(new Emitter<void>());
85
readonly onDidChangeTools = this._onDidChangeTools.event;
86
private readonly _onDidPrepareToolCallBecomeUnresponsive = this._register(new Emitter<{ sessionId: string; toolData: IToolData }>());
87
readonly onDidPrepareToolCallBecomeUnresponsive = this._onDidPrepareToolCallBecomeUnresponsive.event;
88
89
/** Throttle tools updates because it sends all tools and runs on context key updates */
90
private readonly _onDidChangeToolsScheduler = new RunOnceScheduler(() => this._onDidChangeTools.fire(), 750);
91
private readonly _tools = new Map<string, IToolEntry>();
92
private readonly _toolContextKeys = new Set<string>();
93
private readonly _ctxToolsCount: IContextKey<number>;
94
95
private readonly _callsByRequestId = new Map<string, ITrackedCall[]>();
96
97
private readonly _isAgentModeEnabled: IObservable<boolean>;
98
99
constructor(
100
@IInstantiationService private readonly _instantiationService: IInstantiationService,
101
@IExtensionService private readonly _extensionService: IExtensionService,
102
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
103
@IChatService private readonly _chatService: IChatService,
104
@IDialogService private readonly _dialogService: IDialogService,
105
@ITelemetryService private readonly _telemetryService: ITelemetryService,
106
@ILogService private readonly _logService: ILogService,
107
@IConfigurationService private readonly _configurationService: IConfigurationService,
108
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
109
@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,
110
@IStorageService private readonly _storageService: IStorageService,
111
@ILanguageModelToolsConfirmationService private readonly _confirmationService: ILanguageModelToolsConfirmationService,
112
) {
113
super();
114
115
this._isAgentModeEnabled = observableConfigValue(ChatConfiguration.AgentEnabled, true, this._configurationService);
116
117
this._register(this._contextKeyService.onDidChangeContext(e => {
118
if (e.affectsSome(this._toolContextKeys)) {
119
// Not worth it to compute a delta here unless we have many tools changing often
120
this._onDidChangeToolsScheduler.schedule();
121
}
122
}));
123
124
this._register(this._configurationService.onDidChangeConfiguration(e => {
125
if (e.affectsConfiguration(ChatConfiguration.ExtensionToolsEnabled) || e.affectsConfiguration(ChatConfiguration.AgentEnabled)) {
126
this._onDidChangeToolsScheduler.schedule();
127
}
128
}));
129
130
// Clear out warning accepted state if the setting is disabled
131
this._register(Event.runAndSubscribe(this._configurationService.onDidChangeConfiguration, e => {
132
if (!e || e.affectsConfiguration(ChatConfiguration.GlobalAutoApprove)) {
133
if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) !== true) {
134
this._storageService.remove(AutoApproveStorageKeys.GlobalAutoApproveOptIn, StorageScope.APPLICATION);
135
}
136
}
137
}));
138
139
this._ctxToolsCount = ChatContextKeys.Tools.toolsCount.bindTo(_contextKeyService);
140
141
// Create the internal VS Code tool set
142
this.vscodeToolSet = this._register(this.createToolSet(
143
ToolDataSource.Internal,
144
'vscode',
145
VSCodeToolReference.vscode,
146
{
147
icon: ThemeIcon.fromId(Codicon.vscode.id),
148
description: localize('copilot.toolSet.vscode.description', 'Use VS Code features'),
149
}
150
));
151
152
// Create the internal Execute tool set
153
this.executeToolSet = this._register(this.createToolSet(
154
ToolDataSource.Internal,
155
'execute',
156
SpecedToolAliases.execute,
157
{
158
icon: ThemeIcon.fromId(Codicon.terminal.id),
159
description: localize('copilot.toolSet.execute.description', 'Execute code and applications on your machine'),
160
}
161
));
162
163
// Create the internal Read tool set
164
this.readToolSet = this._register(this.createToolSet(
165
ToolDataSource.Internal,
166
'read',
167
SpecedToolAliases.read,
168
{
169
icon: ThemeIcon.fromId(Codicon.eye.id),
170
description: localize('copilot.toolSet.read.description', 'Read files in your workspace'),
171
}
172
));
173
}
174
175
/**
176
* Returns if the given tool or toolset is permitted in the current context.
177
* When agent mode is enabled, all tools are permitted (no restriction)
178
* When agent mode is disabled only a subset of read-only tools are permitted in agentic-loop contexts.
179
*/
180
private isPermitted(toolOrToolSet: IToolData | ToolSet, reader?: IReader): boolean {
181
const agentModeEnabled = this._isAgentModeEnabled.read(reader);
182
if (agentModeEnabled !== false) {
183
return true;
184
}
185
const permittedInternalToolSetIds = [SpecedToolAliases.read, SpecedToolAliases.search, SpecedToolAliases.web];
186
if (toolOrToolSet instanceof ToolSet) {
187
const permitted = toolOrToolSet.source.type === 'internal' && permittedInternalToolSetIds.includes(toolOrToolSet.referenceName);
188
this._logService.trace(`LanguageModelToolsService#isPermitted: ToolSet ${toolOrToolSet.id} (${toolOrToolSet.referenceName}) permitted=${permitted}`);
189
return permitted;
190
}
191
this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=false`);
192
return false;
193
}
194
195
override dispose(): void {
196
super.dispose();
197
198
this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose()));
199
this._ctxToolsCount.reset();
200
}
201
202
registerToolData(toolData: IToolData): IDisposable {
203
if (this._tools.has(toolData.id)) {
204
throw new Error(`Tool "${toolData.id}" is already registered.`);
205
}
206
207
this._tools.set(toolData.id, { data: toolData });
208
this._ctxToolsCount.set(this._tools.size);
209
this._onDidChangeToolsScheduler.schedule();
210
211
toolData.when?.keys().forEach(key => this._toolContextKeys.add(key));
212
213
let store: DisposableStore | undefined;
214
if (toolData.inputSchema) {
215
store = new DisposableStore();
216
const schemaUrl = createToolSchemaUri(toolData.id).toString();
217
jsonSchemaRegistry.registerSchema(schemaUrl, toolData.inputSchema, store);
218
store.add(jsonSchemaRegistry.registerSchemaAssociation(schemaUrl, `/lm/tool/${toolData.id}/tool_input.json`));
219
}
220
221
return toDisposable(() => {
222
store?.dispose();
223
this._tools.delete(toolData.id);
224
this._ctxToolsCount.set(this._tools.size);
225
this._refreshAllToolContextKeys();
226
this._onDidChangeToolsScheduler.schedule();
227
});
228
}
229
230
flushToolUpdates(): void {
231
this._onDidChangeToolsScheduler.flush();
232
}
233
234
private _refreshAllToolContextKeys() {
235
this._toolContextKeys.clear();
236
for (const tool of this._tools.values()) {
237
tool.data.when?.keys().forEach(key => this._toolContextKeys.add(key));
238
}
239
}
240
241
registerToolImplementation(id: string, tool: IToolImpl): IDisposable {
242
const entry = this._tools.get(id);
243
if (!entry) {
244
throw new Error(`Tool "${id}" was not contributed.`);
245
}
246
247
if (entry.impl) {
248
throw new Error(`Tool "${id}" already has an implementation.`);
249
}
250
251
entry.impl = tool;
252
return toDisposable(() => {
253
entry.impl = undefined;
254
});
255
}
256
257
registerTool(toolData: IToolData, tool: IToolImpl): IDisposable {
258
return combinedDisposable(
259
this.registerToolData(toolData),
260
this.registerToolImplementation(toolData.id, tool)
261
);
262
}
263
264
getTools(includeDisabled?: boolean): Iterable<IToolData> {
265
const toolDatas = Iterable.map(this._tools.values(), i => i.data);
266
const extensionToolsEnabled = this._configurationService.getValue<boolean>(ChatConfiguration.ExtensionToolsEnabled);
267
return Iterable.filter(
268
toolDatas,
269
toolData => {
270
const satisfiesWhenClause = includeDisabled || !toolData.when || this._contextKeyService.contextMatchesRules(toolData.when);
271
const satisfiesExternalToolCheck = toolData.source.type !== 'extension' || !!extensionToolsEnabled;
272
const satisfiesPermittedCheck = includeDisabled || this.isPermitted(toolData);
273
return satisfiesWhenClause && satisfiesExternalToolCheck && satisfiesPermittedCheck;
274
});
275
}
276
277
readonly toolsObservable = observableFromEventOpts<readonly IToolData[], void>({ equalsFn: arrayEqualsC() }, this.onDidChangeTools, () => Array.from(this.getTools()));
278
279
getTool(id: string): IToolData | undefined {
280
return this._getToolEntry(id)?.data;
281
}
282
283
private _getToolEntry(id: string): IToolEntry | undefined {
284
const entry = this._tools.get(id);
285
if (entry && (!entry.data.when || this._contextKeyService.contextMatchesRules(entry.data.when))) {
286
return entry;
287
} else {
288
return undefined;
289
}
290
}
291
292
getToolByName(name: string, includeDisabled?: boolean): IToolData | undefined {
293
for (const tool of this.getTools(!!includeDisabled)) {
294
if (tool.toolReferenceName === name) {
295
return tool;
296
}
297
}
298
return undefined;
299
}
300
301
async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise<IToolResult> {
302
this._logService.trace(`[LanguageModelToolsService#invokeTool] Invoking tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}`);
303
304
// 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.
305
let tool = this._tools.get(dto.toolId);
306
if (!tool) {
307
throw new Error(`Tool ${dto.toolId} was not contributed`);
308
}
309
310
if (!tool.impl) {
311
await this._extensionService.activateByEvent(`onLanguageModelTool:${dto.toolId}`);
312
313
// Extension should activate and register the tool implementation
314
tool = this._tools.get(dto.toolId);
315
if (!tool?.impl) {
316
throw new Error(`Tool ${dto.toolId} does not have an implementation registered.`);
317
}
318
}
319
320
// Shortcut to write to the model directly here, but could call all the way back to use the real stream.
321
let toolInvocation: ChatToolInvocation | undefined;
322
323
let requestId: string | undefined;
324
let store: DisposableStore | undefined;
325
let toolResult: IToolResult | undefined;
326
let prepareTimeWatch: StopWatch | undefined;
327
let invocationTimeWatch: StopWatch | undefined;
328
let preparedInvocation: IPreparedToolInvocation | undefined;
329
try {
330
if (dto.context) {
331
store = new DisposableStore();
332
const model = this._chatService.getSession(dto.context.sessionResource);
333
if (!model) {
334
throw new Error(`Tool called for unknown chat session`);
335
}
336
337
const request = model.getRequests().at(-1)!;
338
requestId = request.id;
339
dto.modelId = request.modelId;
340
dto.userSelectedTools = request.userSelectedTools && { ...request.userSelectedTools };
341
342
// Replace the token with a new token that we can cancel when cancelToolCallsForRequest is called
343
if (!this._callsByRequestId.has(requestId)) {
344
this._callsByRequestId.set(requestId, []);
345
}
346
const trackedCall: ITrackedCall = { store };
347
this._callsByRequestId.get(requestId)!.push(trackedCall);
348
349
const source = new CancellationTokenSource();
350
store.add(toDisposable(() => {
351
source.dispose(true);
352
}));
353
store.add(token.onCancellationRequested(() => {
354
IChatToolInvocation.confirmWith(toolInvocation, { type: ToolConfirmKind.Denied });
355
source.cancel();
356
}));
357
store.add(source.token.onCancellationRequested(() => {
358
IChatToolInvocation.confirmWith(toolInvocation, { type: ToolConfirmKind.Denied });
359
}));
360
token = source.token;
361
362
prepareTimeWatch = StopWatch.create(true);
363
preparedInvocation = await this.prepareToolInvocation(tool, dto, token);
364
prepareTimeWatch.stop();
365
366
toolInvocation = new ChatToolInvocation(preparedInvocation, tool.data, dto.callId, dto.fromSubAgent, dto.parameters);
367
trackedCall.invocation = toolInvocation;
368
const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters);
369
if (autoConfirmed) {
370
IChatToolInvocation.confirmWith(toolInvocation, autoConfirmed);
371
}
372
373
this._chatService.appendProgress(request, toolInvocation);
374
375
dto.toolSpecificData = toolInvocation?.toolSpecificData;
376
if (preparedInvocation?.confirmationMessages?.title) {
377
if (!IChatToolInvocation.executionConfirmedOrDenied(toolInvocation) && !autoConfirmed) {
378
this.playAccessibilitySignal([toolInvocation]);
379
}
380
const userConfirmed = await IChatToolInvocation.awaitConfirmation(toolInvocation, token);
381
if (userConfirmed.type === ToolConfirmKind.Denied) {
382
throw new CancellationError();
383
}
384
if (userConfirmed.type === ToolConfirmKind.Skipped) {
385
toolResult = {
386
content: [{
387
kind: 'text',
388
value: 'The user chose to skip the tool call, they want to proceed without running it'
389
}]
390
};
391
return toolResult;
392
}
393
394
if (dto.toolSpecificData?.kind === 'input') {
395
dto.parameters = dto.toolSpecificData.rawInput;
396
dto.toolSpecificData = undefined;
397
}
398
}
399
} else {
400
prepareTimeWatch = StopWatch.create(true);
401
preparedInvocation = await this.prepareToolInvocation(tool, dto, token);
402
prepareTimeWatch.stop();
403
if (preparedInvocation?.confirmationMessages?.title && !(await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters))) {
404
const result = await this._dialogService.confirm({ message: renderAsPlaintext(preparedInvocation.confirmationMessages.title), detail: renderAsPlaintext(preparedInvocation.confirmationMessages.message!) });
405
if (!result.confirmed) {
406
throw new CancellationError();
407
}
408
}
409
dto.toolSpecificData = preparedInvocation?.toolSpecificData;
410
}
411
412
if (token.isCancellationRequested) {
413
throw new CancellationError();
414
}
415
416
invocationTimeWatch = StopWatch.create(true);
417
toolResult = await tool.impl.invoke(dto, countTokens, {
418
report: step => {
419
toolInvocation?.acceptProgress(step);
420
}
421
}, token);
422
invocationTimeWatch.stop();
423
this.ensureToolDetails(dto, toolResult, tool.data);
424
425
if (toolInvocation?.didExecuteTool(toolResult).type === IChatToolInvocation.StateKind.WaitingForPostApproval) {
426
const autoConfirmedPost = await this.shouldAutoConfirmPostExecution(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters);
427
if (autoConfirmedPost) {
428
IChatToolInvocation.confirmWith(toolInvocation, autoConfirmedPost);
429
}
430
431
const postConfirm = await IChatToolInvocation.awaitPostConfirmation(toolInvocation, token);
432
if (postConfirm.type === ToolConfirmKind.Denied) {
433
throw new CancellationError();
434
}
435
if (postConfirm.type === ToolConfirmKind.Skipped) {
436
toolResult = {
437
content: [{
438
kind: 'text',
439
value: 'The tool executed but the user chose not to share the results'
440
}]
441
};
442
}
443
}
444
445
this._telemetryService.publicLog2<LanguageModelToolInvokedEvent, LanguageModelToolInvokedClassification>(
446
'languageModelToolInvoked',
447
{
448
result: 'success',
449
chatSessionId: dto.context?.sessionId,
450
toolId: tool.data.id,
451
toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined,
452
toolSourceKind: tool.data.source.type,
453
prepareTimeMs: prepareTimeWatch?.elapsed(),
454
invocationTimeMs: invocationTimeWatch?.elapsed(),
455
});
456
return toolResult;
457
} catch (err) {
458
const result = isCancellationError(err) ? 'userCancelled' : 'error';
459
this._telemetryService.publicLog2<LanguageModelToolInvokedEvent, LanguageModelToolInvokedClassification>(
460
'languageModelToolInvoked',
461
{
462
result,
463
chatSessionId: dto.context?.sessionId,
464
toolId: tool.data.id,
465
toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined,
466
toolSourceKind: tool.data.source.type,
467
prepareTimeMs: prepareTimeWatch?.elapsed(),
468
invocationTimeMs: invocationTimeWatch?.elapsed(),
469
});
470
this._logService.error(`[LanguageModelToolsService#invokeTool] Error from tool ${dto.toolId} with parameters ${JSON.stringify(dto.parameters)}:\n${toErrorMessage(err, true)}`);
471
472
toolResult ??= { content: [] };
473
toolResult.toolResultError = err instanceof Error ? err.message : String(err);
474
if (tool.data.alwaysDisplayInputOutput) {
475
toolResult.toolResultDetails = { input: this.formatToolInput(dto), output: [{ type: 'embed', isText: true, value: String(err) }], isError: true };
476
}
477
478
throw err;
479
} finally {
480
toolInvocation?.didExecuteTool(toolResult, true);
481
if (store) {
482
this.cleanupCallDisposables(requestId, store);
483
}
484
}
485
}
486
487
private async prepareToolInvocation(tool: IToolEntry, dto: IToolInvocation, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
488
let prepared: IPreparedToolInvocation | undefined;
489
if (tool.impl!.prepareToolInvocation) {
490
const preparePromise = tool.impl!.prepareToolInvocation({
491
parameters: dto.parameters,
492
chatRequestId: dto.chatRequestId,
493
chatSessionId: dto.context?.sessionId,
494
chatSessionResource: dto.context?.sessionResource,
495
chatInteractionId: dto.chatInteractionId
496
}, token);
497
498
const raceResult = await Promise.race([
499
timeout(3000, token).then(() => 'timeout'),
500
preparePromise
501
]);
502
if (raceResult === 'timeout') {
503
this._onDidPrepareToolCallBecomeUnresponsive.fire({
504
sessionId: dto.context?.sessionId ?? '',
505
toolData: tool.data
506
});
507
}
508
509
prepared = await preparePromise;
510
}
511
512
const isEligibleForAutoApproval = this.isToolEligibleForAutoApproval(tool.data);
513
514
// Default confirmation messages if tool is not eligible for auto-approval
515
if (!isEligibleForAutoApproval && !prepared?.confirmationMessages?.title) {
516
if (!prepared) {
517
prepared = {};
518
}
519
const fullReferenceName = getToolFullReferenceName(tool.data);
520
521
// TODO: This should be more detailed per tool.
522
prepared.confirmationMessages = {
523
...prepared.confirmationMessages,
524
title: localize('defaultToolConfirmation.title', 'Confirm tool execution'),
525
message: localize('defaultToolConfirmation.message', 'Run the \'{0}\' tool?', fullReferenceName),
526
disclaimer: new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ title: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval] }, false)), { isTrusted: true }),
527
allowAutoConfirm: false,
528
};
529
}
530
531
if (!isEligibleForAutoApproval && prepared?.confirmationMessages?.title) {
532
// Always overwrite the disclaimer if not eligible for auto-approval
533
prepared.confirmationMessages.disclaimer = new MarkdownString(localize('defaultToolConfirmation.disclaimer', 'Auto approval for \'{0}\' is restricted via {1}.', getToolFullReferenceName(tool.data), createMarkdownCommandLink({ title: '`' + ChatConfiguration.EligibleForAutoApproval + '`', id: 'workbench.action.openSettings', arguments: [ChatConfiguration.EligibleForAutoApproval] }, false)), { isTrusted: true });
534
}
535
536
if (prepared?.confirmationMessages?.title) {
537
if (prepared.toolSpecificData?.kind !== 'terminal' && prepared.confirmationMessages.allowAutoConfirm !== false) {
538
prepared.confirmationMessages.allowAutoConfirm = isEligibleForAutoApproval;
539
}
540
541
if (!prepared.toolSpecificData && tool.data.alwaysDisplayInputOutput) {
542
prepared.toolSpecificData = {
543
kind: 'input',
544
rawInput: dto.parameters,
545
};
546
}
547
}
548
549
return prepared;
550
}
551
552
private playAccessibilitySignal(toolInvocations: ChatToolInvocation[]): void {
553
const autoApproved = this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove);
554
if (autoApproved) {
555
return;
556
}
557
const setting: { sound?: 'auto' | 'on' | 'off'; announcement?: 'auto' | 'off' } | undefined = this._configurationService.getValue(AccessibilitySignal.chatUserActionRequired.settingsKey);
558
if (!setting) {
559
return;
560
}
561
const soundEnabled = setting.sound === 'on' || (setting.sound === 'auto' && (this._accessibilityService.isScreenReaderOptimized()));
562
const announcementEnabled = this._accessibilityService.isScreenReaderOptimized() && setting.announcement === 'auto';
563
if (soundEnabled || announcementEnabled) {
564
this._accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired, { customAlertMessage: this._instantiationService.invokeFunction(getToolConfirmationAlert, toolInvocations), userGesture: true, modality: !soundEnabled ? 'announcement' : undefined });
565
}
566
}
567
568
private ensureToolDetails(dto: IToolInvocation, toolResult: IToolResult, toolData: IToolData): void {
569
if (!toolResult.toolResultDetails && toolData.alwaysDisplayInputOutput) {
570
toolResult.toolResultDetails = {
571
input: this.formatToolInput(dto),
572
output: this.toolResultToIO(toolResult),
573
};
574
}
575
}
576
577
private formatToolInput(dto: IToolInvocation): string {
578
return JSON.stringify(dto.parameters, undefined, 2);
579
}
580
581
private toolResultToIO(toolResult: IToolResult): IToolResultInputOutputDetails['output'] {
582
return toolResult.content.map(part => {
583
if (part.kind === 'text') {
584
return { type: 'embed', isText: true, value: part.value };
585
} else if (part.kind === 'promptTsx') {
586
return { type: 'embed', isText: true, value: stringifyPromptTsxPart(part) };
587
} else if (part.kind === 'data') {
588
return { type: 'embed', value: encodeBase64(part.value.data), mimeType: part.value.mimeType };
589
} else {
590
assertNever(part);
591
}
592
});
593
}
594
595
private getEligibleForAutoApprovalSpecialCase(toolData: IToolData): string | undefined {
596
if (toolData.id === 'vscode_fetchWebPage_internal') {
597
return 'fetch';
598
}
599
return undefined;
600
}
601
602
private isToolEligibleForAutoApproval(toolData: IToolData): boolean {
603
const fullReferenceName = this.getEligibleForAutoApprovalSpecialCase(toolData) ?? getToolFullReferenceName(toolData);
604
if (toolData.id === 'copilot_fetchWebPage') {
605
// Special case, this fetch will call an internal tool 'vscode_fetchWebPage_internal'
606
return true;
607
}
608
const eligibilityConfig = this._configurationService.getValue<Record<string, boolean>>(ChatConfiguration.EligibleForAutoApproval);
609
if (eligibilityConfig && typeof eligibilityConfig === 'object' && fullReferenceName) {
610
// Direct match
611
if (Object.prototype.hasOwnProperty.call(eligibilityConfig, fullReferenceName)) {
612
return eligibilityConfig[fullReferenceName];
613
}
614
// Back compat with legacy names
615
if (toolData.legacyToolReferenceFullNames) {
616
for (const legacyName of toolData.legacyToolReferenceFullNames) {
617
// Check if the full legacy name is in the config
618
if (Object.prototype.hasOwnProperty.call(eligibilityConfig, legacyName)) {
619
return eligibilityConfig[legacyName];
620
}
621
// Some tools may be both renamed and namespaced from a toolset, eg: xxx/yyy -> yyy
622
if (legacyName.includes('/')) {
623
const trimmedLegacyName = legacyName.split('/').pop();
624
if (trimmedLegacyName && Object.prototype.hasOwnProperty.call(eligibilityConfig, trimmedLegacyName)) {
625
return eligibilityConfig[trimmedLegacyName];
626
}
627
}
628
}
629
}
630
}
631
return true;
632
}
633
634
private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown): Promise<ConfirmedReason | undefined> {
635
const tool = this._tools.get(toolId);
636
if (!tool) {
637
return undefined;
638
}
639
640
if (!this.isToolEligibleForAutoApproval(tool.data)) {
641
return undefined;
642
}
643
644
const reason = this._confirmationService.getPreConfirmAction({ toolId, source, parameters });
645
if (reason) {
646
return reason;
647
}
648
649
const config = this._configurationService.inspect<boolean | Record<string, boolean>>(ChatConfiguration.GlobalAutoApprove);
650
651
// If we know the tool runs at a global level, only consider the global config.
652
// If we know the tool runs at a workspace level, use those specific settings when appropriate.
653
let value = config.value ?? config.defaultValue;
654
if (typeof runsInWorkspace === 'boolean') {
655
value = config.userLocalValue ?? config.applicationValue;
656
if (runsInWorkspace) {
657
value = config.workspaceValue ?? config.workspaceFolderValue ?? config.userRemoteValue ?? value;
658
}
659
}
660
661
const autoConfirm = value === true || (typeof value === 'object' && value.hasOwnProperty(toolId) && value[toolId] === true);
662
if (autoConfirm) {
663
if (await this._checkGlobalAutoApprove()) {
664
return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove };
665
}
666
}
667
668
return undefined;
669
}
670
671
private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown): Promise<ConfirmedReason | undefined> {
672
if (this._configurationService.getValue<boolean>(ChatConfiguration.GlobalAutoApprove) && await this._checkGlobalAutoApprove()) {
673
return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove };
674
}
675
676
return this._confirmationService.getPostConfirmAction({ toolId, source, parameters });
677
}
678
679
private async _checkGlobalAutoApprove(): Promise<boolean> {
680
const optedIn = this._storageService.getBoolean(AutoApproveStorageKeys.GlobalAutoApproveOptIn, StorageScope.APPLICATION, false);
681
if (optedIn) {
682
return true;
683
}
684
685
if (this._contextKeyService.getContextKeyValue(SkipAutoApproveConfirmationKey) === true) {
686
return true;
687
}
688
689
const promptResult = await this._dialogService.prompt({
690
type: Severity.Warning,
691
message: localize('autoApprove2.title', 'Enable global auto approve?'),
692
buttons: [
693
{
694
label: localize('autoApprove2.button.enable', 'Enable'),
695
run: () => true
696
},
697
{
698
label: localize('autoApprove2.button.disable', 'Disable'),
699
run: () => false
700
},
701
],
702
custom: {
703
icon: Codicon.warning,
704
disableCloseAction: true,
705
markdownDetails: [{
706
markdown: new MarkdownString(globalAutoApproveDescription.value),
707
}],
708
}
709
});
710
711
if (promptResult.result !== true) {
712
await this._configurationService.updateValue(ChatConfiguration.GlobalAutoApprove, false);
713
return false;
714
}
715
716
this._storageService.store(AutoApproveStorageKeys.GlobalAutoApproveOptIn, true, StorageScope.APPLICATION, StorageTarget.USER);
717
return true;
718
}
719
720
private cleanupCallDisposables(requestId: string | undefined, store: DisposableStore): void {
721
if (requestId) {
722
const disposables = this._callsByRequestId.get(requestId);
723
if (disposables) {
724
const index = disposables.findIndex(d => d.store === store);
725
if (index > -1) {
726
disposables.splice(index, 1);
727
}
728
if (disposables.length === 0) {
729
this._callsByRequestId.delete(requestId);
730
}
731
}
732
}
733
734
store.dispose();
735
}
736
737
cancelToolCallsForRequest(requestId: string): void {
738
const calls = this._callsByRequestId.get(requestId);
739
if (calls) {
740
calls.forEach(call => call.store.dispose());
741
this._callsByRequestId.delete(requestId);
742
}
743
}
744
745
private static readonly githubMCPServerAliases = ['github/github-mcp-server', 'io.github.github/github-mcp-server', 'github-mcp-server'];
746
private static readonly playwrightMCPServerAliases = ['microsoft/playwright-mcp', 'com.microsoft/playwright-mcp'];
747
748
private * getToolSetAliases(toolSet: ToolSet, fullReferenceName: string): Iterable<string> {
749
if (fullReferenceName !== toolSet.referenceName) {
750
yield toolSet.referenceName; // tool set name without '/*'
751
}
752
if (toolSet.legacyFullNames) {
753
yield* toolSet.legacyFullNames;
754
}
755
switch (toolSet.referenceName) {
756
case 'github':
757
for (const alias of LanguageModelToolsService.githubMCPServerAliases) {
758
yield alias + '/*';
759
}
760
break;
761
case 'playwright':
762
for (const alias of LanguageModelToolsService.playwrightMCPServerAliases) {
763
yield alias + '/*';
764
}
765
break;
766
case SpecedToolAliases.execute: // 'execute'
767
yield 'shell'; // legacy alias
768
break;
769
case SpecedToolAliases.agent: // 'agent'
770
yield VSCodeToolReference.runSubagent; // prefer the tool set over th old tool name
771
yield 'custom-agent'; // legacy alias
772
break;
773
}
774
}
775
776
private * getToolAliases(toolSet: IToolData, fullReferenceName: string): Iterable<string> {
777
const referenceName = toolSet.toolReferenceName ?? toolSet.displayName;
778
if (fullReferenceName !== referenceName && referenceName !== VSCodeToolReference.runSubagent) {
779
yield referenceName; // simple name, without toolset name
780
}
781
if (toolSet.legacyToolReferenceFullNames) {
782
for (const legacyName of toolSet.legacyToolReferenceFullNames) {
783
yield legacyName;
784
const lastSlashIndex = legacyName.lastIndexOf('/');
785
if (lastSlashIndex !== -1) {
786
yield legacyName.substring(lastSlashIndex + 1); // it was also known under the simple name
787
}
788
}
789
}
790
const slashIndex = fullReferenceName.lastIndexOf('/');
791
if (slashIndex !== -1) {
792
switch (fullReferenceName.substring(0, slashIndex)) {
793
case 'github':
794
for (const alias of LanguageModelToolsService.githubMCPServerAliases) {
795
yield alias + fullReferenceName.substring(slashIndex);
796
}
797
break;
798
case 'playwright':
799
for (const alias of LanguageModelToolsService.playwrightMCPServerAliases) {
800
yield alias + fullReferenceName.substring(slashIndex);
801
}
802
break;
803
}
804
}
805
}
806
807
/**
808
* Create a map that contains all tools and toolsets with their enablement state.
809
* @param fullReferenceNames A list of tool or toolset by their full reference names that are enabled.
810
* @returns A map of tool or toolset instances to their enablement state.
811
*/
812
toToolAndToolSetEnablementMap(fullReferenceNames: readonly string[], _target: string | undefined): IToolAndToolSetEnablementMap {
813
const toolOrToolSetNames = new Set(fullReferenceNames);
814
const result = new Map<ToolSet | IToolData, boolean>();
815
for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {
816
if (tool instanceof ToolSet) {
817
const enabled = toolOrToolSetNames.has(fullReferenceName) || Iterable.some(this.getToolSetAliases(tool, fullReferenceName), name => toolOrToolSetNames.has(name));
818
result.set(tool, enabled);
819
if (enabled) {
820
for (const memberTool of tool.getTools()) {
821
result.set(memberTool, true);
822
}
823
}
824
} else {
825
if (!result.has(tool)) { // already set via an enabled toolset
826
const enabled = toolOrToolSetNames.has(fullReferenceName)
827
|| Iterable.some(this.getToolAliases(tool, fullReferenceName), name => toolOrToolSetNames.has(name))
828
|| !!tool.legacyToolReferenceFullNames?.some(toolFullName => {
829
// enable tool if just the legacy tool set name is present
830
const index = toolFullName.lastIndexOf('/');
831
return index !== -1 && toolOrToolSetNames.has(toolFullName.substring(0, index));
832
});
833
result.set(tool, enabled);
834
}
835
}
836
}
837
838
// also add all user tool sets (not part of the prompt referencable tools)
839
for (const toolSet of this._toolSets) {
840
if (toolSet.source.type === 'user') {
841
const enabled = Iterable.every(toolSet.getTools(), t => result.get(t) === true);
842
result.set(toolSet, enabled);
843
}
844
}
845
return result;
846
}
847
848
toFullReferenceNames(map: IToolAndToolSetEnablementMap): string[] {
849
const result: string[] = [];
850
const toolsCoveredByEnabledToolSet = new Set<IToolData>();
851
for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {
852
if (tool instanceof ToolSet) {
853
if (map.get(tool)) {
854
result.push(fullReferenceName);
855
for (const memberTool of tool.getTools()) {
856
toolsCoveredByEnabledToolSet.add(memberTool);
857
}
858
}
859
} else {
860
if (map.get(tool) && !toolsCoveredByEnabledToolSet.has(tool)) {
861
result.push(fullReferenceName);
862
}
863
}
864
}
865
return result;
866
}
867
868
toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] {
869
const toolsOrToolSetByName = new Map<string, ToolSet | IToolData>();
870
for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {
871
toolsOrToolSetByName.set(fullReferenceName, tool);
872
}
873
874
const result: ChatRequestToolReferenceEntry[] = [];
875
for (const ref of variableReferences) {
876
const toolOrToolSet = toolsOrToolSetByName.get(ref.name);
877
if (toolOrToolSet) {
878
if (toolOrToolSet instanceof ToolSet) {
879
result.push(toToolSetVariableEntry(toolOrToolSet, ref.range));
880
} else {
881
result.push(toToolVariableEntry(toolOrToolSet, ref.range));
882
}
883
}
884
}
885
return result;
886
}
887
888
889
private readonly _toolSets = new ObservableSet<ToolSet>();
890
891
readonly toolSets: IObservable<Iterable<ToolSet>> = derived(this, reader => {
892
const allToolSets = Array.from(this._toolSets.observable.read(reader));
893
return allToolSets.filter(toolSet => this.isPermitted(toolSet, reader));
894
});
895
896
getToolSet(id: string): ToolSet | undefined {
897
for (const toolSet of this._toolSets) {
898
if (toolSet.id === id) {
899
return toolSet;
900
}
901
}
902
return undefined;
903
}
904
905
getToolSetByName(name: string): ToolSet | undefined {
906
for (const toolSet of this._toolSets) {
907
if (toolSet.referenceName === name) {
908
return toolSet;
909
}
910
}
911
return undefined;
912
}
913
914
getSpecedToolSetName(referenceName: string): string {
915
if (LanguageModelToolsService.githubMCPServerAliases.includes(referenceName)) {
916
return 'github';
917
}
918
if (LanguageModelToolsService.playwrightMCPServerAliases.includes(referenceName)) {
919
return 'playwright';
920
}
921
return referenceName;
922
}
923
924
createToolSet(source: ToolDataSource, id: string, referenceName: string, options?: { icon?: ThemeIcon; description?: string; legacyFullNames?: string[] }): ToolSet & IDisposable {
925
926
const that = this;
927
928
referenceName = this.getSpecedToolSetName(referenceName);
929
930
const result = new class extends ToolSet implements IDisposable {
931
dispose(): void {
932
if (that._toolSets.has(result)) {
933
this._tools.clear();
934
that._toolSets.delete(result);
935
}
936
937
}
938
}(id, referenceName, options?.icon ?? Codicon.tools, source, options?.description, options?.legacyFullNames);
939
940
this._toolSets.add(result);
941
return result;
942
}
943
944
readonly toolsWithFullReferenceName = derived<[IToolData | ToolSet, string][]>(reader => {
945
const result: [IToolData | ToolSet, string][] = [];
946
const coveredByToolSets = new Set<IToolData>();
947
for (const toolSet of this.toolSets.read(reader)) {
948
if (toolSet.source.type !== 'user') {
949
result.push([toolSet, getToolSetFullReferenceName(toolSet)]);
950
for (const tool of toolSet.getTools()) {
951
result.push([tool, getToolFullReferenceName(tool, toolSet)]);
952
coveredByToolSets.add(tool);
953
}
954
}
955
}
956
for (const tool of this.toolsObservable.read(reader)) {
957
if (tool.canBeReferencedInPrompt && !coveredByToolSets.has(tool) && this.isPermitted(tool, reader)) {
958
result.push([tool, getToolFullReferenceName(tool)]);
959
}
960
}
961
return result;
962
});
963
964
* getFullReferenceNames(): Iterable<string> {
965
for (const [, fullReferenceName] of this.toolsWithFullReferenceName.get()) {
966
yield fullReferenceName;
967
}
968
}
969
970
getDeprecatedFullReferenceNames(): Map<string, Set<string>> {
971
const result = new Map<string, Set<string>>();
972
const knownToolSetNames = new Set<string>();
973
const add = (name: string, fullReferenceName: string) => {
974
if (name !== fullReferenceName) {
975
if (!result.has(name)) {
976
result.set(name, new Set<string>());
977
}
978
result.get(name)!.add(fullReferenceName);
979
}
980
};
981
982
for (const [tool, _] of this.toolsWithFullReferenceName.get()) {
983
if (tool instanceof ToolSet) {
984
knownToolSetNames.add(tool.referenceName);
985
if (tool.legacyFullNames) {
986
for (const legacyName of tool.legacyFullNames) {
987
knownToolSetNames.add(legacyName);
988
}
989
}
990
}
991
}
992
993
for (const [tool, fullReferenceName] of this.toolsWithFullReferenceName.get()) {
994
if (tool instanceof ToolSet) {
995
for (const alias of this.getToolSetAliases(tool, fullReferenceName)) {
996
add(alias, fullReferenceName);
997
}
998
} else {
999
for (const alias of this.getToolAliases(tool, fullReferenceName)) {
1000
add(alias, fullReferenceName);
1001
}
1002
if (tool.legacyToolReferenceFullNames) {
1003
for (const legacyName of tool.legacyToolReferenceFullNames) {
1004
// for any 'orphaned' toolsets (toolsets that no longer exist and
1005
// do not have an explicit legacy mapping), we should
1006
// just point them to the list of tools directly
1007
if (legacyName.includes('/')) {
1008
const toolSetFullName = legacyName.substring(0, legacyName.lastIndexOf('/'));
1009
if (!knownToolSetNames.has(toolSetFullName)) {
1010
add(toolSetFullName, fullReferenceName);
1011
}
1012
}
1013
}
1014
}
1015
}
1016
}
1017
return result;
1018
}
1019
1020
getToolByFullReferenceName(fullReferenceName: string): IToolData | ToolSet | undefined {
1021
for (const [tool, toolFullReferenceName] of this.toolsWithFullReferenceName.get()) {
1022
if (fullReferenceName === toolFullReferenceName) {
1023
return tool;
1024
}
1025
const aliases = tool instanceof ToolSet ? this.getToolSetAliases(tool, toolFullReferenceName) : this.getToolAliases(tool, toolFullReferenceName);
1026
if (Iterable.some(aliases, alias => fullReferenceName === alias)) {
1027
return tool;
1028
}
1029
}
1030
return undefined;
1031
}
1032
1033
getFullReferenceName(tool: IToolData | ToolSet, toolSet?: ToolSet): string {
1034
if (tool instanceof ToolSet) {
1035
return getToolSetFullReferenceName(tool);
1036
}
1037
return getToolFullReferenceName(tool, toolSet);
1038
}
1039
}
1040
1041
function getToolFullReferenceName(tool: IToolData, toolSet?: ToolSet) {
1042
const toolName = tool.toolReferenceName ?? tool.displayName;
1043
if (toolSet) {
1044
return `${toolSet.referenceName}/${toolName}`;
1045
} else if (tool.source.type === 'extension') {
1046
return `${tool.source.extensionId.value.toLowerCase()}/${toolName}`;
1047
}
1048
return toolName;
1049
}
1050
1051
function getToolSetFullReferenceName(toolSet: ToolSet) {
1052
if (toolSet.source.type === 'mcp') {
1053
return `${toolSet.referenceName}/*`;
1054
}
1055
return toolSet.referenceName;
1056
}
1057
1058
1059
type LanguageModelToolInvokedEvent = {
1060
result: 'success' | 'error' | 'userCancelled';
1061
chatSessionId: string | undefined;
1062
toolId: string;
1063
toolExtensionId: string | undefined;
1064
toolSourceKind: string;
1065
prepareTimeMs?: number;
1066
invocationTimeMs?: number;
1067
};
1068
1069
type LanguageModelToolInvokedClassification = {
1070
result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether invoking the LanguageModelTool resulted in an error.' };
1071
chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat session that the tool was used within, if applicable.' };
1072
toolId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the tool used.' };
1073
toolExtensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that contributed the tool.' };
1074
toolSourceKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source (mcp/extension/internal) of the tool.' };
1075
prepareTimeMs?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time spent in prepareToolInvocation method in milliseconds.' };
1076
invocationTimeMs?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time spent in tool invoke method in milliseconds.' };
1077
owner: 'roblourens';
1078
comment: 'Provides insight into the usage of language model tools.';
1079
};
1080
1081