Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts
5251 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 { Separator } from '../../../../../base/common/actions.js';
7
import { VSBuffer } from '../../../../../base/common/buffer.js';
8
import { CancellationToken } from '../../../../../base/common/cancellation.js';
9
import { Event } from '../../../../../base/common/event.js';
10
import { IMarkdownString } from '../../../../../base/common/htmlContent.js';
11
import { Iterable } from '../../../../../base/common/iterator.js';
12
import { IJSONSchema } from '../../../../../base/common/jsonSchema.js';
13
import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
14
import { Schemas } from '../../../../../base/common/network.js';
15
import { derived, IObservable, IReader, ITransaction, ObservableSet } from '../../../../../base/common/observable.js';
16
import { ThemeIcon } from '../../../../../base/common/themables.js';
17
import { URI } from '../../../../../base/common/uri.js';
18
import { Location } from '../../../../../editor/common/languages.js';
19
import { localize } from '../../../../../nls.js';
20
import { ContextKeyExpression, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
21
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
22
import { ByteSize } from '../../../../../platform/files/common/files.js';
23
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
24
import { IProgress } from '../../../../../platform/progress/common/progress.js';
25
import { ChatRequestToolReferenceEntry } from '../attachments/chatVariableEntries.js';
26
import { IVariableReference } from '../chatModes.js';
27
import { IChatExtensionsContent, IChatSimpleToolInvocationData, IChatSubagentToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, type IChatTerminalToolInvocationData } from '../chatService/chatService.js';
28
import { ILanguageModelChatMetadata, LanguageModelPartAudience } from '../languageModels.js';
29
import { UserSelectedTools } from '../participants/chatAgents.js';
30
import { PromptElementJSON, stringifyPromptElementJSON } from './promptTsxTypes.js';
31
32
/**
33
* Selector for matching language models by vendor, family, version, or id.
34
* Used to filter tools to specific models or model families.
35
*/
36
export interface ILanguageModelChatSelector {
37
readonly vendor?: string;
38
readonly family?: string;
39
readonly version?: string;
40
readonly id?: string;
41
}
42
43
export interface IToolData {
44
readonly id: string;
45
readonly source: ToolDataSource;
46
readonly toolReferenceName?: string;
47
readonly legacyToolReferenceFullNames?: readonly string[];
48
readonly icon?: { dark: URI; light?: URI } | ThemeIcon;
49
readonly when?: ContextKeyExpression;
50
readonly tags?: readonly string[];
51
readonly displayName: string;
52
readonly userDescription?: string;
53
readonly modelDescription: string;
54
readonly inputSchema?: IJSONSchema;
55
readonly canBeReferencedInPrompt?: boolean;
56
/**
57
* True if the tool runs in the (possibly remote) workspace, false if it runs
58
* on the host, undefined if known.
59
*/
60
readonly runsInWorkspace?: boolean;
61
readonly alwaysDisplayInputOutput?: boolean;
62
/** True if this tool might ask for pre-approval */
63
readonly canRequestPreApproval?: boolean;
64
/** True if this tool might ask for post-approval */
65
readonly canRequestPostApproval?: boolean;
66
/**
67
* Model selectors that this tool is available for.
68
* If defined, the tool is only available when the selected model matches one of the selectors.
69
*/
70
readonly models?: readonly ILanguageModelChatSelector[];
71
}
72
73
/**
74
* Check if a tool matches the given model metadata based on the tool's `models` selectors.
75
* If the tool has no `models` defined, it matches all models.
76
* If model is undefined, model-specific filtering is skipped (tool is included).
77
*/
78
export function toolMatchesModel(toolData: IToolData, model: ILanguageModelChatMetadata | undefined): boolean {
79
// If no model selectors are defined, the tool is available for all models
80
if (!toolData.models || toolData.models.length === 0) {
81
return true;
82
}
83
// If model is undefined, skip model-specific filtering
84
if (!model) {
85
return true;
86
}
87
// Check if any selector matches the model (OR logic)
88
return toolData.models.some(selector =>
89
(!selector.id || selector.id === model.id) &&
90
(!selector.vendor || selector.vendor === model.vendor) &&
91
(!selector.family || selector.family === model.family) &&
92
(!selector.version || selector.version === model.version)
93
);
94
}
95
96
export interface IToolProgressStep {
97
readonly message: string | IMarkdownString | undefined;
98
/** 0-1 progress of the tool call */
99
readonly progress?: number;
100
}
101
102
export type ToolProgress = IProgress<IToolProgressStep>;
103
104
export type ToolDataSource =
105
| {
106
type: 'extension';
107
label: string;
108
extensionId: ExtensionIdentifier;
109
}
110
| {
111
type: 'mcp';
112
label: string;
113
serverLabel: string | undefined;
114
instructions: string | undefined;
115
collectionId: string;
116
definitionId: string;
117
}
118
| {
119
type: 'user';
120
label: string;
121
file: URI;
122
}
123
| {
124
type: 'internal';
125
label: string;
126
} | {
127
type: 'external';
128
label: string;
129
};
130
131
export namespace ToolDataSource {
132
133
export const Internal: ToolDataSource = { type: 'internal', label: 'Built-In' };
134
135
/** External tools may not be contributed or invoked, but may be invoked externally and described in an IChatToolInvocationSerialized */
136
export const External: ToolDataSource = { type: 'external', label: 'External' };
137
138
export function toKey(source: ToolDataSource): string {
139
switch (source.type) {
140
case 'extension': return `extension:${source.extensionId.value}`;
141
case 'mcp': return `mcp:${source.collectionId}:${source.definitionId}`;
142
case 'user': return `user:${source.file.toString()}`;
143
case 'internal': return 'internal';
144
case 'external': return 'external';
145
}
146
}
147
148
export function equals(a: ToolDataSource, b: ToolDataSource): boolean {
149
return toKey(a) === toKey(b);
150
}
151
152
export function classify(source: ToolDataSource): { readonly ordinal: number; readonly label: string } {
153
if (source.type === 'internal') {
154
return { ordinal: 1, label: localize('builtin', 'Built-In') };
155
} else if (source.type === 'mcp') {
156
return { ordinal: 2, label: source.label };
157
} else if (source.type === 'user') {
158
return { ordinal: 0, label: localize('user', 'User Defined') };
159
} else {
160
return { ordinal: 3, label: source.label };
161
}
162
}
163
}
164
165
export interface IToolInvocation {
166
callId: string;
167
toolId: string;
168
// eslint-disable-next-line @typescript-eslint/no-explicit-any
169
parameters: Record<string, any>;
170
tokenBudget?: number;
171
context: IToolInvocationContext | undefined;
172
chatRequestId?: string;
173
chatInteractionId?: string;
174
/**
175
* Optional tool call ID from the chat stream, used to correlate with pending streaming tool calls.
176
*/
177
chatStreamToolCallId?: string;
178
/**
179
* Lets us add some nicer UI to toolcalls that came from a sub-agent, but in the long run, this should probably just be rendered in a similar way to thinking text + tool call groups
180
*/
181
subAgentInvocationId?: string;
182
toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData;
183
modelId?: string;
184
userSelectedTools?: UserSelectedTools;
185
/** The label of the custom button selected by the user during confirmation, if custom buttons were used. */
186
selectedCustomButton?: string;
187
}
188
189
export interface IToolInvocationContext {
190
/** @deprecated Use {@link sessionResource} instead */
191
readonly sessionId: string;
192
readonly sessionResource: URI;
193
}
194
195
// eslint-disable-next-line @typescript-eslint/no-explicit-any
196
export function isToolInvocationContext(obj: any): obj is IToolInvocationContext {
197
return typeof obj === 'object' && typeof obj.sessionId === 'string' && URI.isUri(obj.sessionResource);
198
}
199
200
export interface IToolInvocationPreparationContext {
201
// eslint-disable-next-line @typescript-eslint/no-explicit-any
202
parameters: any;
203
toolCallId: string;
204
chatRequestId?: string;
205
/** @deprecated Use {@link chatSessionResource} instead */
206
chatSessionId?: string;
207
chatSessionResource: URI | undefined;
208
chatInteractionId?: string;
209
modelId?: string;
210
/** If set, tells the tool that it should include confirmation messages. */
211
forceConfirmationReason?: string;
212
}
213
214
export type ToolInputOutputBase = {
215
/** Mimetype of the value, optional */
216
mimeType?: string;
217
/** URI of the resource on the MCP server. */
218
uri?: URI;
219
/** If true, this part came in as a resource reference rather than direct data. */
220
asResource?: boolean;
221
/** Audience of the data part */
222
audience?: LanguageModelPartAudience[];
223
};
224
225
export type ToolInputOutputEmbedded = ToolInputOutputBase & {
226
type: 'embed';
227
value: string;
228
/** If true, value is text. If false or not given, value is base64 */
229
isText?: boolean;
230
};
231
232
export type ToolInputOutputReference = ToolInputOutputBase & { type: 'ref'; uri: URI };
233
234
export interface IToolResultInputOutputDetails {
235
readonly input: string;
236
readonly output: (ToolInputOutputEmbedded | ToolInputOutputReference)[];
237
readonly isError?: boolean;
238
/** Raw MCP tool result for MCP App UI rendering */
239
readonly mcpOutput?: unknown;
240
}
241
242
export interface IToolResultOutputDetails {
243
readonly output: { type: 'data'; mimeType: string; value: VSBuffer };
244
}
245
246
// eslint-disable-next-line @typescript-eslint/no-explicit-any
247
export function isToolResultInputOutputDetails(obj: any): obj is IToolResultInputOutputDetails {
248
return typeof obj === 'object' && typeof obj?.input === 'string' && (typeof obj?.output === 'string' || Array.isArray(obj?.output));
249
}
250
251
// eslint-disable-next-line @typescript-eslint/no-explicit-any
252
export function isToolResultOutputDetails(obj: any): obj is IToolResultOutputDetails {
253
return typeof obj === 'object' && typeof obj?.output === 'object' && typeof obj?.output?.mimeType === 'string' && obj?.output?.type === 'data';
254
}
255
256
export interface IToolResult {
257
content: (IToolResultPromptTsxPart | IToolResultTextPart | IToolResultDataPart)[];
258
toolResultMessage?: string | IMarkdownString;
259
toolResultDetails?: Array<URI | Location> | IToolResultInputOutputDetails | IToolResultOutputDetails;
260
toolResultError?: string;
261
toolMetadata?: unknown;
262
/** Whether to ask the user to confirm these tool results. Overrides {@link IToolConfirmationMessages.confirmResults}. */
263
confirmResults?: boolean;
264
}
265
266
export function toolContentToA11yString(part: IToolResult['content']) {
267
return part.map(p => {
268
switch (p.kind) {
269
case 'promptTsx':
270
return stringifyPromptTsxPart(p);
271
case 'text':
272
return p.value;
273
case 'data':
274
return localize('toolResultDataPartA11y', "{0} of {1} binary data", ByteSize.formatSize(p.value.data.byteLength), p.value.mimeType || 'unknown');
275
}
276
}).join(', ');
277
}
278
279
export function toolResultHasBuffers(result: IToolResult): boolean {
280
return result.content.some(part => part.kind === 'data');
281
}
282
283
export interface IToolResultPromptTsxPart {
284
kind: 'promptTsx';
285
value: unknown;
286
}
287
288
export function stringifyPromptTsxPart(part: IToolResultPromptTsxPart): string {
289
return stringifyPromptElementJSON(part.value as PromptElementJSON);
290
}
291
292
export interface IToolResultTextPart {
293
kind: 'text';
294
value: string;
295
audience?: LanguageModelPartAudience[];
296
title?: string;
297
}
298
299
export interface IToolResultDataPart {
300
kind: 'data';
301
value: {
302
mimeType: string;
303
data: VSBuffer;
304
};
305
audience?: LanguageModelPartAudience[];
306
title?: string;
307
}
308
309
export interface IToolConfirmationMessages {
310
/** Title for the confirmation. If set, the user will be asked to confirm execution of the tool */
311
title?: string | IMarkdownString;
312
/** MUST be set if `title` is also set */
313
message?: string | IMarkdownString;
314
disclaimer?: string | IMarkdownString;
315
allowAutoConfirm?: boolean;
316
terminalCustomActions?: ToolConfirmationAction[];
317
/** If true, confirmation will be requested after the tool executes and before results are sent to the model */
318
confirmResults?: boolean;
319
/** If title is not set (no confirmation needed), this reason will be shown to explain why confirmation was not needed */
320
confirmationNotNeededReason?: string | IMarkdownString;
321
/** Custom button labels to display instead of the default Allow/Skip buttons. */
322
customButtons?: string[];
323
}
324
325
export interface IToolConfirmationAction {
326
label: string;
327
disabled?: boolean;
328
tooltip?: string;
329
// eslint-disable-next-line @typescript-eslint/no-explicit-any
330
data: any;
331
}
332
333
export type ToolConfirmationAction = IToolConfirmationAction | Separator;
334
335
export enum ToolInvocationPresentation {
336
Hidden = 'hidden',
337
HiddenAfterComplete = 'hiddenAfterComplete'
338
}
339
340
export interface IToolInvocationStreamContext {
341
toolCallId: string;
342
rawInput: unknown;
343
chatRequestId?: string;
344
/** @deprecated Use {@link chatSessionResource} instead */
345
chatSessionId?: string;
346
chatSessionResource?: URI;
347
chatInteractionId?: string;
348
}
349
350
export interface IStreamedToolInvocation {
351
invocationMessage?: string | IMarkdownString;
352
}
353
354
export interface IPreparedToolInvocation {
355
invocationMessage?: string | IMarkdownString;
356
pastTenseMessage?: string | IMarkdownString;
357
originMessage?: string | IMarkdownString;
358
confirmationMessages?: IToolConfirmationMessages;
359
presentation?: ToolInvocationPresentation;
360
toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData;
361
}
362
363
export interface IToolImpl {
364
invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise<IToolResult>;
365
prepareToolInvocation?(context: IToolInvocationPreparationContext, token: CancellationToken): Promise<IPreparedToolInvocation | undefined>;
366
handleToolStream?(context: IToolInvocationStreamContext, token: CancellationToken): Promise<IStreamedToolInvocation | undefined>;
367
}
368
369
export interface IToolSet {
370
readonly id: string;
371
readonly referenceName: string;
372
readonly icon: ThemeIcon;
373
readonly source: ToolDataSource;
374
readonly description?: string;
375
readonly legacyFullNames?: string[];
376
377
getTools(r?: IReader): Iterable<IToolData>;
378
}
379
380
export type IToolAndToolSetEnablementMap = ReadonlyMap<IToolData | IToolSet, boolean>;
381
382
export function isToolSet(obj: IToolData | IToolSet | undefined): obj is IToolSet {
383
return !!obj && (obj as IToolSet).getTools !== undefined;
384
}
385
386
export class ToolSet implements IToolSet {
387
388
protected readonly _tools = new ObservableSet<IToolData>();
389
390
protected readonly _toolSets = new ObservableSet<IToolSet>();
391
392
/**
393
* A homogenous tool set only contains tools from the same source as the tool set itself
394
*/
395
readonly isHomogenous: IObservable<boolean>;
396
397
constructor(
398
readonly id: string,
399
readonly referenceName: string,
400
readonly icon: ThemeIcon,
401
readonly source: ToolDataSource,
402
readonly description: string | undefined,
403
readonly legacyFullNames: string[] | undefined,
404
private readonly _contextKeyService: IContextKeyService,
405
) {
406
407
this.isHomogenous = derived(r => {
408
return !Iterable.some(this._tools.observable.read(r), tool => !ToolDataSource.equals(tool.source, this.source))
409
&& !Iterable.some(this._toolSets.observable.read(r), toolSet => !ToolDataSource.equals(toolSet.source, this.source));
410
});
411
}
412
413
addTool(data: IToolData, tx?: ITransaction): IDisposable {
414
this._tools.add(data, tx);
415
return toDisposable(() => {
416
this._tools.delete(data);
417
});
418
}
419
420
addToolSet(toolSet: IToolSet, tx?: ITransaction): IDisposable {
421
if (toolSet === this) {
422
return Disposable.None;
423
}
424
this._toolSets.add(toolSet, tx);
425
return toDisposable(() => {
426
this._toolSets.delete(toolSet);
427
});
428
}
429
430
getTools(r?: IReader): Iterable<IToolData> {
431
return Iterable.concat(
432
Iterable.filter(this._tools.observable.read(r), toolData => this._contextKeyService.contextMatchesRules(toolData.when)),
433
...Iterable.map(this._toolSets.observable.read(r), toolSet => toolSet.getTools(r))
434
);
435
}
436
}
437
438
export class ToolSetForModel {
439
public get id() {
440
return this._toolSet.id;
441
}
442
443
public get referenceName() {
444
return this._toolSet.referenceName;
445
}
446
447
public get icon() {
448
return this._toolSet.icon;
449
}
450
451
public get source() {
452
return this._toolSet.source;
453
}
454
455
public get description() {
456
return this._toolSet.description;
457
}
458
459
public get legacyFullNames() {
460
return this._toolSet.legacyFullNames;
461
}
462
463
constructor(
464
private readonly _toolSet: IToolSet,
465
private readonly model: ILanguageModelChatMetadata | undefined,
466
) { }
467
468
public getTools(r?: IReader): Iterable<IToolData> {
469
return Iterable.filter(this._toolSet.getTools(r), toolData => toolMatchesModel(toolData, this.model));
470
}
471
}
472
473
474
export interface IBeginToolCallOptions {
475
toolCallId: string;
476
toolId: string;
477
chatRequestId?: string;
478
sessionResource?: URI;
479
subagentInvocationId?: string;
480
}
481
482
export interface IToolInvokedEvent {
483
readonly toolId: string;
484
readonly sessionResource: URI | undefined;
485
readonly requestId: string | undefined;
486
readonly subagentInvocationId: string | undefined;
487
}
488
489
export const ILanguageModelToolsService = createDecorator<ILanguageModelToolsService>('ILanguageModelToolsService');
490
491
export type CountTokensCallback = (input: string, token: CancellationToken) => Promise<number>;
492
493
export interface ILanguageModelToolsService {
494
_serviceBrand: undefined;
495
readonly vscodeToolSet: ToolSet;
496
readonly executeToolSet: ToolSet;
497
readonly readToolSet: ToolSet;
498
readonly agentToolSet: ToolSet;
499
readonly onDidChangeTools: Event<void>;
500
readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ readonly sessionResource: URI; readonly toolData: IToolData }>;
501
readonly onDidInvokeTool: Event<IToolInvokedEvent>;
502
registerToolData(toolData: IToolData): IDisposable;
503
registerToolImplementation(id: string, tool: IToolImpl): IDisposable;
504
registerTool(toolData: IToolData, tool: IToolImpl): IDisposable;
505
506
/**
507
* Get all tools currently enabled (matching `when` clauses and model).
508
* @param model The language model metadata to filter tools by. If undefined, model-specific filtering is skipped.
509
*/
510
getTools(model: ILanguageModelChatMetadata | undefined): Iterable<IToolData>;
511
512
/**
513
* Creats an observable of enabled tools in the context. Note the observable
514
* should be created and reused, not created per reader, for example:
515
*
516
* ```
517
* const toolsObs = toolsService.observeTools(model);
518
* autorun(reader => {
519
* const tools = toolsObs.read(reader);
520
* ...
521
* });
522
* ```
523
* @param model The language model metadata to filter tools by. If undefined, model-specific filtering is skipped.
524
*/
525
observeTools(model: ILanguageModelChatMetadata | undefined): IObservable<readonly IToolData[]>;
526
527
/**
528
* Get all registered tools regardless of enablement state.
529
* Use this for configuration UIs, completions, etc. where all tools should be visible.
530
*/
531
getAllToolsIncludingDisabled(): Iterable<IToolData>;
532
533
/**
534
* Get a tool by its ID. Does not check when clauses.
535
*/
536
getTool(id: string): IToolData | undefined;
537
538
/**
539
* Get a tool by its reference name. Does not check when clauses.
540
*/
541
getToolByName(name: string): IToolData | undefined;
542
543
/**
544
* Begin a tool call in the streaming phase.
545
* Creates a ChatToolInvocation in the Streaming state and appends it to the chat.
546
* Returns the invocation so it can be looked up later when invokeTool is called.
547
*/
548
beginToolCall(options: IBeginToolCallOptions): IChatToolInvocation | undefined;
549
550
/**
551
* Update the streaming state of a pending tool call.
552
* Calls the tool's handleToolStream method to get a custom invocation message.
553
*/
554
updateToolStream(toolCallId: string, partialInput: unknown, token: CancellationToken): Promise<void>;
555
556
invokeTool(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise<IToolResult>;
557
cancelToolCallsForRequest(requestId: string): void;
558
/** Flush any pending tool updates to the extension hosts. */
559
flushToolUpdates(): void;
560
561
readonly toolSets: IObservable<Iterable<IToolSet>>;
562
getToolSetsForModel(model: ILanguageModelChatMetadata | undefined, reader?: IReader): Iterable<IToolSet>;
563
getToolSet(id: string): IToolSet | undefined;
564
getToolSetByName(name: string): IToolSet | undefined;
565
createToolSet(source: ToolDataSource, id: string, referenceName: string, options?: { icon?: ThemeIcon; description?: string; legacyFullNames?: string[] }): ToolSet & IDisposable;
566
567
// tool names in prompt and agent files ('full reference names')
568
getFullReferenceNames(): Iterable<string>;
569
getFullReferenceName(tool: IToolData, toolSet?: IToolSet): string;
570
getToolByFullReferenceName(fullReferenceName: string): IToolData | IToolSet | undefined;
571
getDeprecatedFullReferenceNames(): Map<string, Set<string>>;
572
573
/**
574
* Gets the enablement maps based on the given set of references.
575
* @param fullReferenceNames The full reference names of the tools and tool sets to enable.
576
* @param model Optional language model metadata to filter tools by.
577
* If undefined is passed, all tools will be returned, even if normally disabled.
578
*/
579
toToolAndToolSetEnablementMap(fullReferenceNames: readonly string[], model: ILanguageModelChatMetadata | undefined): IToolAndToolSetEnablementMap;
580
581
toFullReferenceNames(map: IToolAndToolSetEnablementMap): string[];
582
toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[];
583
}
584
585
586
export function createToolInputUri(toolCallId: string): URI {
587
return URI.from({ scheme: Schemas.inMemory, path: `/lm/tool/${toolCallId}/tool_input.json` });
588
}
589
590
export function createToolSchemaUri(toolOrId: IToolData | string): URI {
591
if (typeof toolOrId !== 'string') {
592
toolOrId = toolOrId.id;
593
}
594
return URI.from({ scheme: Schemas.vscode, authority: 'schemas', path: `/lm/tool/${toolOrId}` });
595
}
596
597
export namespace SpecedToolAliases {
598
export const execute = 'execute';
599
export const edit = 'edit';
600
export const search = 'search';
601
export const agent = 'agent';
602
export const read = 'read';
603
export const web = 'web';
604
export const todo = 'todo';
605
}
606
607
export namespace VSCodeToolReference {
608
export const runSubagent = 'runSubagent';
609
export const vscode = 'vscode';
610
611
}
612
613