Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostLanguageModelTools.ts
5231 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 type * as vscode from 'vscode';
7
import { raceCancellation } from '../../../base/common/async.js';
8
import { CancellationToken } from '../../../base/common/cancellation.js';
9
import { CancellationError } from '../../../base/common/errors.js';
10
import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
11
import { revive } from '../../../base/common/marshalling.js';
12
import { generateUuid } from '../../../base/common/uuid.js';
13
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
14
import { IPreparedToolInvocation, IStreamedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolInvocationPreparationContext, IToolInvocationStreamContext, IToolResult, ToolInvocationPresentation } from '../../contrib/chat/common/tools/languageModelToolsService.js';
15
import { ExtensionEditToolId, InternalEditToolId } from '../../contrib/chat/common/tools/builtinTools/editFileTool.js';
16
import { InternalFetchWebPageToolId } from '../../contrib/chat/common/tools/builtinTools/tools.js';
17
import { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js';
18
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
19
import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js';
20
import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, IToolDefinitionDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js';
21
import { ExtHostLanguageModels } from './extHostLanguageModels.js';
22
import * as typeConvert from './extHostTypeConverters.js';
23
import { URI } from '../../../base/common/uri.js';
24
25
class Tool {
26
27
private _data: IToolDataDto;
28
private _apiObject: vscode.LanguageModelToolInformation | undefined;
29
private _apiObjectWithChatParticipantAdditions: vscode.LanguageModelToolInformation | undefined;
30
31
constructor(data: IToolDataDto) {
32
this._data = data;
33
}
34
35
update(newData: IToolDataDto): void {
36
this._data = newData;
37
this._apiObject = undefined;
38
this._apiObjectWithChatParticipantAdditions = undefined;
39
}
40
41
get data(): IToolDataDto {
42
return this._data;
43
}
44
45
get apiObject(): vscode.LanguageModelToolInformation {
46
if (!this._apiObject) {
47
this._apiObject = Object.freeze({
48
name: this._data.id,
49
description: this._data.modelDescription,
50
inputSchema: this._data.inputSchema,
51
tags: this._data.tags ?? [],
52
source: undefined
53
});
54
}
55
return this._apiObject;
56
}
57
58
get apiObjectWithChatParticipantAdditions() {
59
if (!this._apiObjectWithChatParticipantAdditions) {
60
this._apiObjectWithChatParticipantAdditions = Object.freeze({
61
name: this._data.id,
62
description: this._data.modelDescription,
63
inputSchema: this._data.inputSchema,
64
tags: this._data.tags ?? [],
65
source: typeConvert.LanguageModelToolSource.to(this._data.source)
66
});
67
}
68
return this._apiObjectWithChatParticipantAdditions;
69
}
70
}
71
72
export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape {
73
/** A map of tools that were registered in this EH */
74
private readonly _registeredTools = new Map<string, { extension: IExtensionDescription; tool: vscode.LanguageModelTool<Object> }>();
75
private readonly _proxy: MainThreadLanguageModelToolsShape;
76
private readonly _tokenCountFuncs = new Map</* call ID */string, (text: string, token?: vscode.CancellationToken) => Thenable<number>>();
77
78
/** A map of all known tools, from other EHs or registered in vscode core */
79
private readonly _allTools = new Map<string, Tool>();
80
81
constructor(
82
mainContext: IMainContext,
83
private readonly _languageModels: ExtHostLanguageModels,
84
) {
85
this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageModelTools);
86
87
this._proxy.$getTools().then(tools => {
88
for (const tool of tools) {
89
this._allTools.set(tool.id, new Tool(revive(tool)));
90
}
91
});
92
}
93
94
async $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise<number> {
95
const fn = this._tokenCountFuncs.get(callId);
96
if (!fn) {
97
throw new Error(`Tool invocation call ${callId} not found`);
98
}
99
100
return await fn(input, token);
101
}
102
103
async invokeTool(extension: IExtensionDescription, toolIdOrInfo: string | vscode.LanguageModelToolInformation, options: vscode.LanguageModelToolInvocationOptions<any>, token?: CancellationToken): Promise<vscode.LanguageModelToolResult> {
104
const toolId = typeof toolIdOrInfo === 'string' ? toolIdOrInfo : toolIdOrInfo.name;
105
const callId = generateUuid();
106
if (options.tokenizationOptions) {
107
this._tokenCountFuncs.set(callId, options.tokenizationOptions.countTokens);
108
}
109
110
try {
111
if (options.toolInvocationToken && !isToolInvocationContext(options.toolInvocationToken)) {
112
throw new Error(`Invalid tool invocation token`);
113
}
114
115
if ((toolId === InternalEditToolId || toolId === ExtensionEditToolId) && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) {
116
throw new Error(`Invalid tool: ${toolId}`);
117
}
118
119
// Making the round trip here because not all tools were necessarily registered in this EH
120
const result = await this._proxy.$invokeTool({
121
toolId,
122
callId,
123
parameters: options.input,
124
tokenBudget: options.tokenizationOptions?.tokenBudget,
125
context: options.toolInvocationToken as IToolInvocationContext | undefined,
126
chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined,
127
chatInteractionId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatInteractionId : undefined,
128
subAgentInvocationId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.subAgentInvocationId : undefined,
129
chatStreamToolCallId: isProposedApiEnabled(extension, 'chatParticipantAdditions') ? options.chatStreamToolCallId : undefined,
130
}, token);
131
132
const dto: Dto<IToolResult> = result instanceof SerializableObjectWithBuffers ? result.value : result;
133
return typeConvert.LanguageModelToolResult.to(revive(dto));
134
} finally {
135
this._tokenCountFuncs.delete(callId);
136
}
137
}
138
139
$onDidChangeTools(tools: IToolDataDto[]): void {
140
141
const oldTools = new Set(this._allTools.keys());
142
143
for (const tool of tools) {
144
oldTools.delete(tool.id);
145
const existing = this._allTools.get(tool.id);
146
if (existing) {
147
existing.update(tool);
148
} else {
149
this._allTools.set(tool.id, new Tool(revive(tool)));
150
}
151
}
152
153
for (const id of oldTools) {
154
this._allTools.delete(id);
155
}
156
}
157
158
getTools(extension: IExtensionDescription): vscode.LanguageModelToolInformation[] {
159
const hasParticipantAdditions = isProposedApiEnabled(extension, 'chatParticipantPrivate');
160
return Array.from(this._allTools.values())
161
.map(tool => hasParticipantAdditions ? tool.apiObjectWithChatParticipantAdditions : tool.apiObject)
162
.filter(tool => {
163
switch (tool.name) {
164
case InternalEditToolId:
165
case ExtensionEditToolId:
166
case InternalFetchWebPageToolId:
167
case SearchExtensionsToolId:
168
return isProposedApiEnabled(extension, 'chatParticipantPrivate');
169
default:
170
return true;
171
}
172
});
173
}
174
175
async $invokeTool(dto: Dto<IToolInvocation>, token: CancellationToken): Promise<Dto<IToolResult> | SerializableObjectWithBuffers<Dto<IToolResult>>> {
176
const item = this._registeredTools.get(dto.toolId);
177
if (!item) {
178
throw new Error(`Unknown tool ${dto.toolId}`);
179
}
180
181
const options: vscode.LanguageModelToolInvocationOptions<Object> = {
182
input: dto.parameters,
183
toolInvocationToken: revive(dto.context) as unknown as vscode.ChatParticipantToolToken | undefined,
184
};
185
if (isProposedApiEnabled(item.extension, 'chatParticipantPrivate')) {
186
options.chatRequestId = dto.chatRequestId;
187
options.chatInteractionId = dto.chatInteractionId;
188
options.chatSessionId = dto.context?.sessionId;
189
options.chatSessionResource = URI.revive(dto.context?.sessionResource);
190
options.subAgentInvocationId = dto.subAgentInvocationId;
191
}
192
193
if (isProposedApiEnabled(item.extension, 'chatParticipantAdditions') && dto.modelId) {
194
options.model = await this.getModel(dto.modelId, item.extension);
195
}
196
if (isProposedApiEnabled(item.extension, 'chatParticipantAdditions') && dto.chatStreamToolCallId) {
197
options.chatStreamToolCallId = dto.chatStreamToolCallId;
198
}
199
200
if (dto.tokenBudget !== undefined) {
201
options.tokenizationOptions = {
202
tokenBudget: dto.tokenBudget,
203
countTokens: this._tokenCountFuncs.get(dto.callId) || ((value, token = CancellationToken.None) =>
204
this._proxy.$countTokensForInvocation(dto.callId, value, token))
205
};
206
}
207
208
let progress: vscode.Progress<{ message?: string | vscode.MarkdownString; increment?: number }> | undefined;
209
if (isProposedApiEnabled(item.extension, 'toolProgress')) {
210
let lastProgress: number | undefined;
211
progress = {
212
report: value => {
213
if (value.increment !== undefined) {
214
lastProgress = (lastProgress ?? 0) + value.increment;
215
}
216
217
this._proxy.$acceptToolProgress(dto.callId, {
218
message: typeConvert.MarkdownString.fromStrict(value.message),
219
progress: lastProgress === undefined ? undefined : lastProgress / 100,
220
});
221
}
222
};
223
}
224
225
// todo: 'any' cast because TS can't handle the overloads
226
// eslint-disable-next-line local/code-no-any-casts
227
const extensionResult = await raceCancellation(Promise.resolve((item.tool.invoke as any)(options, token, progress!)), token);
228
if (!extensionResult) {
229
throw new CancellationError();
230
}
231
232
return typeConvert.LanguageModelToolResult.from(extensionResult, item.extension);
233
}
234
235
private async getModel(modelId: string, extension: IExtensionDescription): Promise<vscode.LanguageModelChat> {
236
let model: vscode.LanguageModelChat | undefined;
237
if (modelId) {
238
model = await this._languageModels.getLanguageModelByIdentifier(extension, modelId);
239
}
240
if (!model) {
241
model = await this._languageModels.getDefaultLanguageModel(extension);
242
if (!model) {
243
throw new Error('Language model unavailable');
244
}
245
}
246
247
return model;
248
}
249
250
async $handleToolStream(toolId: string, context: IToolInvocationStreamContext, token: CancellationToken): Promise<IStreamedToolInvocation | undefined> {
251
const item = this._registeredTools.get(toolId);
252
if (!item) {
253
throw new Error(`Unknown tool ${toolId}`);
254
}
255
256
// Only call handleToolStream if it's defined on the tool
257
if (!item.tool.handleToolStream) {
258
return undefined;
259
}
260
261
// Ensure the chatParticipantAdditions API is enabled
262
checkProposedApiEnabled(item.extension, 'chatParticipantAdditions');
263
264
const options: vscode.LanguageModelToolInvocationStreamOptions<any> = {
265
rawInput: context.rawInput,
266
chatRequestId: context.chatRequestId,
267
chatSessionId: context.chatSessionId,
268
chatSessionResource: context.chatSessionResource,
269
chatInteractionId: context.chatInteractionId
270
};
271
272
const result = await item.tool.handleToolStream(options, token);
273
if (!result) {
274
return undefined;
275
}
276
277
return {
278
invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage)
279
};
280
}
281
282
async $prepareToolInvocation(toolId: string, context: IToolInvocationPreparationContext, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
283
const item = this._registeredTools.get(toolId);
284
if (!item) {
285
throw new Error(`Unknown tool ${toolId}`);
286
}
287
288
const options: vscode.LanguageModelToolInvocationPrepareOptions<any> = {
289
input: context.parameters,
290
chatRequestId: context.chatRequestId,
291
chatSessionId: context.chatSessionId,
292
chatSessionResource: context.chatSessionResource,
293
chatInteractionId: context.chatInteractionId,
294
forceConfirmationReason: context.forceConfirmationReason
295
};
296
if (context.forceConfirmationReason) {
297
checkProposedApiEnabled(item.extension, 'chatParticipantPrivate');
298
}
299
if (item.tool.prepareInvocation) {
300
const result = await item.tool.prepareInvocation(options, token);
301
if (!result) {
302
return undefined;
303
}
304
305
if (result.pastTenseMessage || result.presentation) {
306
checkProposedApiEnabled(item.extension, 'chatParticipantPrivate');
307
}
308
309
return {
310
confirmationMessages: result.confirmationMessages ? {
311
title: typeof result.confirmationMessages.title === 'string' ? result.confirmationMessages.title : typeConvert.MarkdownString.from(result.confirmationMessages.title),
312
message: typeof result.confirmationMessages.message === 'string' ? result.confirmationMessages.message : typeConvert.MarkdownString.from(result.confirmationMessages.message),
313
} : undefined,
314
invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage),
315
pastTenseMessage: typeConvert.MarkdownString.fromStrict(result.pastTenseMessage),
316
presentation: result.presentation as ToolInvocationPresentation | undefined,
317
};
318
}
319
320
return undefined;
321
}
322
323
registerTool(extension: IExtensionDescription, id: string, tool: vscode.LanguageModelTool<any>): IDisposable {
324
this._registeredTools.set(id, { extension, tool });
325
this._proxy.$registerTool(id, typeof tool.handleToolStream === 'function');
326
327
return toDisposable(() => {
328
this._registeredTools.delete(id);
329
this._proxy.$unregisterTool(id);
330
});
331
}
332
333
registerToolDefinition(extension: IExtensionDescription, definition: vscode.LanguageModelToolDefinition, tool: vscode.LanguageModelTool<any>): IDisposable {
334
checkProposedApiEnabled(extension, 'languageModelToolSupportsModel');
335
336
const id = definition.name;
337
338
// Convert the definition to a DTO
339
const dto: IToolDefinitionDto = {
340
id,
341
displayName: definition.displayName,
342
toolReferenceName: definition.toolReferenceName,
343
userDescription: definition.userDescription,
344
modelDescription: definition.description,
345
inputSchema: definition.inputSchema as object,
346
source: {
347
type: 'extension',
348
label: extension.displayName ?? extension.name,
349
extensionId: extension.identifier,
350
},
351
icon: typeConvert.IconPath.from(definition.icon),
352
models: definition.models,
353
toolSet: definition.toolSet,
354
};
355
356
this._registeredTools.set(id, { extension, tool });
357
this._proxy.$registerToolWithDefinition(extension.identifier, dto, typeof tool.handleToolStream === 'function');
358
359
return toDisposable(() => {
360
this._registeredTools.delete(id);
361
this._proxy.$unregisterTool(id);
362
});
363
}
364
}
365
366