Path: blob/main/src/vs/workbench/api/common/extHostLanguageModelTools.ts
5231 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type * as vscode from 'vscode';6import { raceCancellation } from '../../../base/common/async.js';7import { CancellationToken } from '../../../base/common/cancellation.js';8import { CancellationError } from '../../../base/common/errors.js';9import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js';10import { revive } from '../../../base/common/marshalling.js';11import { generateUuid } from '../../../base/common/uuid.js';12import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';13import { IPreparedToolInvocation, IStreamedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolInvocationPreparationContext, IToolInvocationStreamContext, IToolResult, ToolInvocationPresentation } from '../../contrib/chat/common/tools/languageModelToolsService.js';14import { ExtensionEditToolId, InternalEditToolId } from '../../contrib/chat/common/tools/builtinTools/editFileTool.js';15import { InternalFetchWebPageToolId } from '../../contrib/chat/common/tools/builtinTools/tools.js';16import { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js';17import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';18import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js';19import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, IToolDefinitionDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js';20import { ExtHostLanguageModels } from './extHostLanguageModels.js';21import * as typeConvert from './extHostTypeConverters.js';22import { URI } from '../../../base/common/uri.js';2324class Tool {2526private _data: IToolDataDto;27private _apiObject: vscode.LanguageModelToolInformation | undefined;28private _apiObjectWithChatParticipantAdditions: vscode.LanguageModelToolInformation | undefined;2930constructor(data: IToolDataDto) {31this._data = data;32}3334update(newData: IToolDataDto): void {35this._data = newData;36this._apiObject = undefined;37this._apiObjectWithChatParticipantAdditions = undefined;38}3940get data(): IToolDataDto {41return this._data;42}4344get apiObject(): vscode.LanguageModelToolInformation {45if (!this._apiObject) {46this._apiObject = Object.freeze({47name: this._data.id,48description: this._data.modelDescription,49inputSchema: this._data.inputSchema,50tags: this._data.tags ?? [],51source: undefined52});53}54return this._apiObject;55}5657get apiObjectWithChatParticipantAdditions() {58if (!this._apiObjectWithChatParticipantAdditions) {59this._apiObjectWithChatParticipantAdditions = Object.freeze({60name: this._data.id,61description: this._data.modelDescription,62inputSchema: this._data.inputSchema,63tags: this._data.tags ?? [],64source: typeConvert.LanguageModelToolSource.to(this._data.source)65});66}67return this._apiObjectWithChatParticipantAdditions;68}69}7071export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape {72/** A map of tools that were registered in this EH */73private readonly _registeredTools = new Map<string, { extension: IExtensionDescription; tool: vscode.LanguageModelTool<Object> }>();74private readonly _proxy: MainThreadLanguageModelToolsShape;75private readonly _tokenCountFuncs = new Map</* call ID */string, (text: string, token?: vscode.CancellationToken) => Thenable<number>>();7677/** A map of all known tools, from other EHs or registered in vscode core */78private readonly _allTools = new Map<string, Tool>();7980constructor(81mainContext: IMainContext,82private readonly _languageModels: ExtHostLanguageModels,83) {84this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageModelTools);8586this._proxy.$getTools().then(tools => {87for (const tool of tools) {88this._allTools.set(tool.id, new Tool(revive(tool)));89}90});91}9293async $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise<number> {94const fn = this._tokenCountFuncs.get(callId);95if (!fn) {96throw new Error(`Tool invocation call ${callId} not found`);97}9899return await fn(input, token);100}101102async invokeTool(extension: IExtensionDescription, toolIdOrInfo: string | vscode.LanguageModelToolInformation, options: vscode.LanguageModelToolInvocationOptions<any>, token?: CancellationToken): Promise<vscode.LanguageModelToolResult> {103const toolId = typeof toolIdOrInfo === 'string' ? toolIdOrInfo : toolIdOrInfo.name;104const callId = generateUuid();105if (options.tokenizationOptions) {106this._tokenCountFuncs.set(callId, options.tokenizationOptions.countTokens);107}108109try {110if (options.toolInvocationToken && !isToolInvocationContext(options.toolInvocationToken)) {111throw new Error(`Invalid tool invocation token`);112}113114if ((toolId === InternalEditToolId || toolId === ExtensionEditToolId) && !isProposedApiEnabled(extension, 'chatParticipantPrivate')) {115throw new Error(`Invalid tool: ${toolId}`);116}117118// Making the round trip here because not all tools were necessarily registered in this EH119const result = await this._proxy.$invokeTool({120toolId,121callId,122parameters: options.input,123tokenBudget: options.tokenizationOptions?.tokenBudget,124context: options.toolInvocationToken as IToolInvocationContext | undefined,125chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined,126chatInteractionId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatInteractionId : undefined,127subAgentInvocationId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.subAgentInvocationId : undefined,128chatStreamToolCallId: isProposedApiEnabled(extension, 'chatParticipantAdditions') ? options.chatStreamToolCallId : undefined,129}, token);130131const dto: Dto<IToolResult> = result instanceof SerializableObjectWithBuffers ? result.value : result;132return typeConvert.LanguageModelToolResult.to(revive(dto));133} finally {134this._tokenCountFuncs.delete(callId);135}136}137138$onDidChangeTools(tools: IToolDataDto[]): void {139140const oldTools = new Set(this._allTools.keys());141142for (const tool of tools) {143oldTools.delete(tool.id);144const existing = this._allTools.get(tool.id);145if (existing) {146existing.update(tool);147} else {148this._allTools.set(tool.id, new Tool(revive(tool)));149}150}151152for (const id of oldTools) {153this._allTools.delete(id);154}155}156157getTools(extension: IExtensionDescription): vscode.LanguageModelToolInformation[] {158const hasParticipantAdditions = isProposedApiEnabled(extension, 'chatParticipantPrivate');159return Array.from(this._allTools.values())160.map(tool => hasParticipantAdditions ? tool.apiObjectWithChatParticipantAdditions : tool.apiObject)161.filter(tool => {162switch (tool.name) {163case InternalEditToolId:164case ExtensionEditToolId:165case InternalFetchWebPageToolId:166case SearchExtensionsToolId:167return isProposedApiEnabled(extension, 'chatParticipantPrivate');168default:169return true;170}171});172}173174async $invokeTool(dto: Dto<IToolInvocation>, token: CancellationToken): Promise<Dto<IToolResult> | SerializableObjectWithBuffers<Dto<IToolResult>>> {175const item = this._registeredTools.get(dto.toolId);176if (!item) {177throw new Error(`Unknown tool ${dto.toolId}`);178}179180const options: vscode.LanguageModelToolInvocationOptions<Object> = {181input: dto.parameters,182toolInvocationToken: revive(dto.context) as unknown as vscode.ChatParticipantToolToken | undefined,183};184if (isProposedApiEnabled(item.extension, 'chatParticipantPrivate')) {185options.chatRequestId = dto.chatRequestId;186options.chatInteractionId = dto.chatInteractionId;187options.chatSessionId = dto.context?.sessionId;188options.chatSessionResource = URI.revive(dto.context?.sessionResource);189options.subAgentInvocationId = dto.subAgentInvocationId;190}191192if (isProposedApiEnabled(item.extension, 'chatParticipantAdditions') && dto.modelId) {193options.model = await this.getModel(dto.modelId, item.extension);194}195if (isProposedApiEnabled(item.extension, 'chatParticipantAdditions') && dto.chatStreamToolCallId) {196options.chatStreamToolCallId = dto.chatStreamToolCallId;197}198199if (dto.tokenBudget !== undefined) {200options.tokenizationOptions = {201tokenBudget: dto.tokenBudget,202countTokens: this._tokenCountFuncs.get(dto.callId) || ((value, token = CancellationToken.None) =>203this._proxy.$countTokensForInvocation(dto.callId, value, token))204};205}206207let progress: vscode.Progress<{ message?: string | vscode.MarkdownString; increment?: number }> | undefined;208if (isProposedApiEnabled(item.extension, 'toolProgress')) {209let lastProgress: number | undefined;210progress = {211report: value => {212if (value.increment !== undefined) {213lastProgress = (lastProgress ?? 0) + value.increment;214}215216this._proxy.$acceptToolProgress(dto.callId, {217message: typeConvert.MarkdownString.fromStrict(value.message),218progress: lastProgress === undefined ? undefined : lastProgress / 100,219});220}221};222}223224// todo: 'any' cast because TS can't handle the overloads225// eslint-disable-next-line local/code-no-any-casts226const extensionResult = await raceCancellation(Promise.resolve((item.tool.invoke as any)(options, token, progress!)), token);227if (!extensionResult) {228throw new CancellationError();229}230231return typeConvert.LanguageModelToolResult.from(extensionResult, item.extension);232}233234private async getModel(modelId: string, extension: IExtensionDescription): Promise<vscode.LanguageModelChat> {235let model: vscode.LanguageModelChat | undefined;236if (modelId) {237model = await this._languageModels.getLanguageModelByIdentifier(extension, modelId);238}239if (!model) {240model = await this._languageModels.getDefaultLanguageModel(extension);241if (!model) {242throw new Error('Language model unavailable');243}244}245246return model;247}248249async $handleToolStream(toolId: string, context: IToolInvocationStreamContext, token: CancellationToken): Promise<IStreamedToolInvocation | undefined> {250const item = this._registeredTools.get(toolId);251if (!item) {252throw new Error(`Unknown tool ${toolId}`);253}254255// Only call handleToolStream if it's defined on the tool256if (!item.tool.handleToolStream) {257return undefined;258}259260// Ensure the chatParticipantAdditions API is enabled261checkProposedApiEnabled(item.extension, 'chatParticipantAdditions');262263const options: vscode.LanguageModelToolInvocationStreamOptions<any> = {264rawInput: context.rawInput,265chatRequestId: context.chatRequestId,266chatSessionId: context.chatSessionId,267chatSessionResource: context.chatSessionResource,268chatInteractionId: context.chatInteractionId269};270271const result = await item.tool.handleToolStream(options, token);272if (!result) {273return undefined;274}275276return {277invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage)278};279}280281async $prepareToolInvocation(toolId: string, context: IToolInvocationPreparationContext, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {282const item = this._registeredTools.get(toolId);283if (!item) {284throw new Error(`Unknown tool ${toolId}`);285}286287const options: vscode.LanguageModelToolInvocationPrepareOptions<any> = {288input: context.parameters,289chatRequestId: context.chatRequestId,290chatSessionId: context.chatSessionId,291chatSessionResource: context.chatSessionResource,292chatInteractionId: context.chatInteractionId,293forceConfirmationReason: context.forceConfirmationReason294};295if (context.forceConfirmationReason) {296checkProposedApiEnabled(item.extension, 'chatParticipantPrivate');297}298if (item.tool.prepareInvocation) {299const result = await item.tool.prepareInvocation(options, token);300if (!result) {301return undefined;302}303304if (result.pastTenseMessage || result.presentation) {305checkProposedApiEnabled(item.extension, 'chatParticipantPrivate');306}307308return {309confirmationMessages: result.confirmationMessages ? {310title: typeof result.confirmationMessages.title === 'string' ? result.confirmationMessages.title : typeConvert.MarkdownString.from(result.confirmationMessages.title),311message: typeof result.confirmationMessages.message === 'string' ? result.confirmationMessages.message : typeConvert.MarkdownString.from(result.confirmationMessages.message),312} : undefined,313invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage),314pastTenseMessage: typeConvert.MarkdownString.fromStrict(result.pastTenseMessage),315presentation: result.presentation as ToolInvocationPresentation | undefined,316};317}318319return undefined;320}321322registerTool(extension: IExtensionDescription, id: string, tool: vscode.LanguageModelTool<any>): IDisposable {323this._registeredTools.set(id, { extension, tool });324this._proxy.$registerTool(id, typeof tool.handleToolStream === 'function');325326return toDisposable(() => {327this._registeredTools.delete(id);328this._proxy.$unregisterTool(id);329});330}331332registerToolDefinition(extension: IExtensionDescription, definition: vscode.LanguageModelToolDefinition, tool: vscode.LanguageModelTool<any>): IDisposable {333checkProposedApiEnabled(extension, 'languageModelToolSupportsModel');334335const id = definition.name;336337// Convert the definition to a DTO338const dto: IToolDefinitionDto = {339id,340displayName: definition.displayName,341toolReferenceName: definition.toolReferenceName,342userDescription: definition.userDescription,343modelDescription: definition.description,344inputSchema: definition.inputSchema as object,345source: {346type: 'extension',347label: extension.displayName ?? extension.name,348extensionId: extension.identifier,349},350icon: typeConvert.IconPath.from(definition.icon),351models: definition.models,352toolSet: definition.toolSet,353};354355this._registeredTools.set(id, { extension, tool });356this._proxy.$registerToolWithDefinition(extension.identifier, dto, typeof tool.handleToolStream === 'function');357358return toDisposable(() => {359this._registeredTools.delete(id);360this._proxy.$unregisterTool(id);361});362}363}364365366