Path: blob/main/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts
5251 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 { Separator } from '../../../../../base/common/actions.js';6import { VSBuffer } from '../../../../../base/common/buffer.js';7import { CancellationToken } from '../../../../../base/common/cancellation.js';8import { Event } from '../../../../../base/common/event.js';9import { IMarkdownString } from '../../../../../base/common/htmlContent.js';10import { Iterable } from '../../../../../base/common/iterator.js';11import { IJSONSchema } from '../../../../../base/common/jsonSchema.js';12import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';13import { Schemas } from '../../../../../base/common/network.js';14import { derived, IObservable, IReader, ITransaction, ObservableSet } from '../../../../../base/common/observable.js';15import { ThemeIcon } from '../../../../../base/common/themables.js';16import { URI } from '../../../../../base/common/uri.js';17import { Location } from '../../../../../editor/common/languages.js';18import { localize } from '../../../../../nls.js';19import { ContextKeyExpression, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';20import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';21import { ByteSize } from '../../../../../platform/files/common/files.js';22import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';23import { IProgress } from '../../../../../platform/progress/common/progress.js';24import { ChatRequestToolReferenceEntry } from '../attachments/chatVariableEntries.js';25import { IVariableReference } from '../chatModes.js';26import { IChatExtensionsContent, IChatSimpleToolInvocationData, IChatSubagentToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, type IChatTerminalToolInvocationData } from '../chatService/chatService.js';27import { ILanguageModelChatMetadata, LanguageModelPartAudience } from '../languageModels.js';28import { UserSelectedTools } from '../participants/chatAgents.js';29import { PromptElementJSON, stringifyPromptElementJSON } from './promptTsxTypes.js';3031/**32* Selector for matching language models by vendor, family, version, or id.33* Used to filter tools to specific models or model families.34*/35export interface ILanguageModelChatSelector {36readonly vendor?: string;37readonly family?: string;38readonly version?: string;39readonly id?: string;40}4142export interface IToolData {43readonly id: string;44readonly source: ToolDataSource;45readonly toolReferenceName?: string;46readonly legacyToolReferenceFullNames?: readonly string[];47readonly icon?: { dark: URI; light?: URI } | ThemeIcon;48readonly when?: ContextKeyExpression;49readonly tags?: readonly string[];50readonly displayName: string;51readonly userDescription?: string;52readonly modelDescription: string;53readonly inputSchema?: IJSONSchema;54readonly canBeReferencedInPrompt?: boolean;55/**56* True if the tool runs in the (possibly remote) workspace, false if it runs57* on the host, undefined if known.58*/59readonly runsInWorkspace?: boolean;60readonly alwaysDisplayInputOutput?: boolean;61/** True if this tool might ask for pre-approval */62readonly canRequestPreApproval?: boolean;63/** True if this tool might ask for post-approval */64readonly canRequestPostApproval?: boolean;65/**66* Model selectors that this tool is available for.67* If defined, the tool is only available when the selected model matches one of the selectors.68*/69readonly models?: readonly ILanguageModelChatSelector[];70}7172/**73* Check if a tool matches the given model metadata based on the tool's `models` selectors.74* If the tool has no `models` defined, it matches all models.75* If model is undefined, model-specific filtering is skipped (tool is included).76*/77export function toolMatchesModel(toolData: IToolData, model: ILanguageModelChatMetadata | undefined): boolean {78// If no model selectors are defined, the tool is available for all models79if (!toolData.models || toolData.models.length === 0) {80return true;81}82// If model is undefined, skip model-specific filtering83if (!model) {84return true;85}86// Check if any selector matches the model (OR logic)87return toolData.models.some(selector =>88(!selector.id || selector.id === model.id) &&89(!selector.vendor || selector.vendor === model.vendor) &&90(!selector.family || selector.family === model.family) &&91(!selector.version || selector.version === model.version)92);93}9495export interface IToolProgressStep {96readonly message: string | IMarkdownString | undefined;97/** 0-1 progress of the tool call */98readonly progress?: number;99}100101export type ToolProgress = IProgress<IToolProgressStep>;102103export type ToolDataSource =104| {105type: 'extension';106label: string;107extensionId: ExtensionIdentifier;108}109| {110type: 'mcp';111label: string;112serverLabel: string | undefined;113instructions: string | undefined;114collectionId: string;115definitionId: string;116}117| {118type: 'user';119label: string;120file: URI;121}122| {123type: 'internal';124label: string;125} | {126type: 'external';127label: string;128};129130export namespace ToolDataSource {131132export const Internal: ToolDataSource = { type: 'internal', label: 'Built-In' };133134/** External tools may not be contributed or invoked, but may be invoked externally and described in an IChatToolInvocationSerialized */135export const External: ToolDataSource = { type: 'external', label: 'External' };136137export function toKey(source: ToolDataSource): string {138switch (source.type) {139case 'extension': return `extension:${source.extensionId.value}`;140case 'mcp': return `mcp:${source.collectionId}:${source.definitionId}`;141case 'user': return `user:${source.file.toString()}`;142case 'internal': return 'internal';143case 'external': return 'external';144}145}146147export function equals(a: ToolDataSource, b: ToolDataSource): boolean {148return toKey(a) === toKey(b);149}150151export function classify(source: ToolDataSource): { readonly ordinal: number; readonly label: string } {152if (source.type === 'internal') {153return { ordinal: 1, label: localize('builtin', 'Built-In') };154} else if (source.type === 'mcp') {155return { ordinal: 2, label: source.label };156} else if (source.type === 'user') {157return { ordinal: 0, label: localize('user', 'User Defined') };158} else {159return { ordinal: 3, label: source.label };160}161}162}163164export interface IToolInvocation {165callId: string;166toolId: string;167// eslint-disable-next-line @typescript-eslint/no-explicit-any168parameters: Record<string, any>;169tokenBudget?: number;170context: IToolInvocationContext | undefined;171chatRequestId?: string;172chatInteractionId?: string;173/**174* Optional tool call ID from the chat stream, used to correlate with pending streaming tool calls.175*/176chatStreamToolCallId?: string;177/**178* 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 groups179*/180subAgentInvocationId?: string;181toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData;182modelId?: string;183userSelectedTools?: UserSelectedTools;184/** The label of the custom button selected by the user during confirmation, if custom buttons were used. */185selectedCustomButton?: string;186}187188export interface IToolInvocationContext {189/** @deprecated Use {@link sessionResource} instead */190readonly sessionId: string;191readonly sessionResource: URI;192}193194// eslint-disable-next-line @typescript-eslint/no-explicit-any195export function isToolInvocationContext(obj: any): obj is IToolInvocationContext {196return typeof obj === 'object' && typeof obj.sessionId === 'string' && URI.isUri(obj.sessionResource);197}198199export interface IToolInvocationPreparationContext {200// eslint-disable-next-line @typescript-eslint/no-explicit-any201parameters: any;202toolCallId: string;203chatRequestId?: string;204/** @deprecated Use {@link chatSessionResource} instead */205chatSessionId?: string;206chatSessionResource: URI | undefined;207chatInteractionId?: string;208modelId?: string;209/** If set, tells the tool that it should include confirmation messages. */210forceConfirmationReason?: string;211}212213export type ToolInputOutputBase = {214/** Mimetype of the value, optional */215mimeType?: string;216/** URI of the resource on the MCP server. */217uri?: URI;218/** If true, this part came in as a resource reference rather than direct data. */219asResource?: boolean;220/** Audience of the data part */221audience?: LanguageModelPartAudience[];222};223224export type ToolInputOutputEmbedded = ToolInputOutputBase & {225type: 'embed';226value: string;227/** If true, value is text. If false or not given, value is base64 */228isText?: boolean;229};230231export type ToolInputOutputReference = ToolInputOutputBase & { type: 'ref'; uri: URI };232233export interface IToolResultInputOutputDetails {234readonly input: string;235readonly output: (ToolInputOutputEmbedded | ToolInputOutputReference)[];236readonly isError?: boolean;237/** Raw MCP tool result for MCP App UI rendering */238readonly mcpOutput?: unknown;239}240241export interface IToolResultOutputDetails {242readonly output: { type: 'data'; mimeType: string; value: VSBuffer };243}244245// eslint-disable-next-line @typescript-eslint/no-explicit-any246export function isToolResultInputOutputDetails(obj: any): obj is IToolResultInputOutputDetails {247return typeof obj === 'object' && typeof obj?.input === 'string' && (typeof obj?.output === 'string' || Array.isArray(obj?.output));248}249250// eslint-disable-next-line @typescript-eslint/no-explicit-any251export function isToolResultOutputDetails(obj: any): obj is IToolResultOutputDetails {252return typeof obj === 'object' && typeof obj?.output === 'object' && typeof obj?.output?.mimeType === 'string' && obj?.output?.type === 'data';253}254255export interface IToolResult {256content: (IToolResultPromptTsxPart | IToolResultTextPart | IToolResultDataPart)[];257toolResultMessage?: string | IMarkdownString;258toolResultDetails?: Array<URI | Location> | IToolResultInputOutputDetails | IToolResultOutputDetails;259toolResultError?: string;260toolMetadata?: unknown;261/** Whether to ask the user to confirm these tool results. Overrides {@link IToolConfirmationMessages.confirmResults}. */262confirmResults?: boolean;263}264265export function toolContentToA11yString(part: IToolResult['content']) {266return part.map(p => {267switch (p.kind) {268case 'promptTsx':269return stringifyPromptTsxPart(p);270case 'text':271return p.value;272case 'data':273return localize('toolResultDataPartA11y', "{0} of {1} binary data", ByteSize.formatSize(p.value.data.byteLength), p.value.mimeType || 'unknown');274}275}).join(', ');276}277278export function toolResultHasBuffers(result: IToolResult): boolean {279return result.content.some(part => part.kind === 'data');280}281282export interface IToolResultPromptTsxPart {283kind: 'promptTsx';284value: unknown;285}286287export function stringifyPromptTsxPart(part: IToolResultPromptTsxPart): string {288return stringifyPromptElementJSON(part.value as PromptElementJSON);289}290291export interface IToolResultTextPart {292kind: 'text';293value: string;294audience?: LanguageModelPartAudience[];295title?: string;296}297298export interface IToolResultDataPart {299kind: 'data';300value: {301mimeType: string;302data: VSBuffer;303};304audience?: LanguageModelPartAudience[];305title?: string;306}307308export interface IToolConfirmationMessages {309/** Title for the confirmation. If set, the user will be asked to confirm execution of the tool */310title?: string | IMarkdownString;311/** MUST be set if `title` is also set */312message?: string | IMarkdownString;313disclaimer?: string | IMarkdownString;314allowAutoConfirm?: boolean;315terminalCustomActions?: ToolConfirmationAction[];316/** If true, confirmation will be requested after the tool executes and before results are sent to the model */317confirmResults?: boolean;318/** If title is not set (no confirmation needed), this reason will be shown to explain why confirmation was not needed */319confirmationNotNeededReason?: string | IMarkdownString;320/** Custom button labels to display instead of the default Allow/Skip buttons. */321customButtons?: string[];322}323324export interface IToolConfirmationAction {325label: string;326disabled?: boolean;327tooltip?: string;328// eslint-disable-next-line @typescript-eslint/no-explicit-any329data: any;330}331332export type ToolConfirmationAction = IToolConfirmationAction | Separator;333334export enum ToolInvocationPresentation {335Hidden = 'hidden',336HiddenAfterComplete = 'hiddenAfterComplete'337}338339export interface IToolInvocationStreamContext {340toolCallId: string;341rawInput: unknown;342chatRequestId?: string;343/** @deprecated Use {@link chatSessionResource} instead */344chatSessionId?: string;345chatSessionResource?: URI;346chatInteractionId?: string;347}348349export interface IStreamedToolInvocation {350invocationMessage?: string | IMarkdownString;351}352353export interface IPreparedToolInvocation {354invocationMessage?: string | IMarkdownString;355pastTenseMessage?: string | IMarkdownString;356originMessage?: string | IMarkdownString;357confirmationMessages?: IToolConfirmationMessages;358presentation?: ToolInvocationPresentation;359toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData;360}361362export interface IToolImpl {363invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise<IToolResult>;364prepareToolInvocation?(context: IToolInvocationPreparationContext, token: CancellationToken): Promise<IPreparedToolInvocation | undefined>;365handleToolStream?(context: IToolInvocationStreamContext, token: CancellationToken): Promise<IStreamedToolInvocation | undefined>;366}367368export interface IToolSet {369readonly id: string;370readonly referenceName: string;371readonly icon: ThemeIcon;372readonly source: ToolDataSource;373readonly description?: string;374readonly legacyFullNames?: string[];375376getTools(r?: IReader): Iterable<IToolData>;377}378379export type IToolAndToolSetEnablementMap = ReadonlyMap<IToolData | IToolSet, boolean>;380381export function isToolSet(obj: IToolData | IToolSet | undefined): obj is IToolSet {382return !!obj && (obj as IToolSet).getTools !== undefined;383}384385export class ToolSet implements IToolSet {386387protected readonly _tools = new ObservableSet<IToolData>();388389protected readonly _toolSets = new ObservableSet<IToolSet>();390391/**392* A homogenous tool set only contains tools from the same source as the tool set itself393*/394readonly isHomogenous: IObservable<boolean>;395396constructor(397readonly id: string,398readonly referenceName: string,399readonly icon: ThemeIcon,400readonly source: ToolDataSource,401readonly description: string | undefined,402readonly legacyFullNames: string[] | undefined,403private readonly _contextKeyService: IContextKeyService,404) {405406this.isHomogenous = derived(r => {407return !Iterable.some(this._tools.observable.read(r), tool => !ToolDataSource.equals(tool.source, this.source))408&& !Iterable.some(this._toolSets.observable.read(r), toolSet => !ToolDataSource.equals(toolSet.source, this.source));409});410}411412addTool(data: IToolData, tx?: ITransaction): IDisposable {413this._tools.add(data, tx);414return toDisposable(() => {415this._tools.delete(data);416});417}418419addToolSet(toolSet: IToolSet, tx?: ITransaction): IDisposable {420if (toolSet === this) {421return Disposable.None;422}423this._toolSets.add(toolSet, tx);424return toDisposable(() => {425this._toolSets.delete(toolSet);426});427}428429getTools(r?: IReader): Iterable<IToolData> {430return Iterable.concat(431Iterable.filter(this._tools.observable.read(r), toolData => this._contextKeyService.contextMatchesRules(toolData.when)),432...Iterable.map(this._toolSets.observable.read(r), toolSet => toolSet.getTools(r))433);434}435}436437export class ToolSetForModel {438public get id() {439return this._toolSet.id;440}441442public get referenceName() {443return this._toolSet.referenceName;444}445446public get icon() {447return this._toolSet.icon;448}449450public get source() {451return this._toolSet.source;452}453454public get description() {455return this._toolSet.description;456}457458public get legacyFullNames() {459return this._toolSet.legacyFullNames;460}461462constructor(463private readonly _toolSet: IToolSet,464private readonly model: ILanguageModelChatMetadata | undefined,465) { }466467public getTools(r?: IReader): Iterable<IToolData> {468return Iterable.filter(this._toolSet.getTools(r), toolData => toolMatchesModel(toolData, this.model));469}470}471472473export interface IBeginToolCallOptions {474toolCallId: string;475toolId: string;476chatRequestId?: string;477sessionResource?: URI;478subagentInvocationId?: string;479}480481export interface IToolInvokedEvent {482readonly toolId: string;483readonly sessionResource: URI | undefined;484readonly requestId: string | undefined;485readonly subagentInvocationId: string | undefined;486}487488export const ILanguageModelToolsService = createDecorator<ILanguageModelToolsService>('ILanguageModelToolsService');489490export type CountTokensCallback = (input: string, token: CancellationToken) => Promise<number>;491492export interface ILanguageModelToolsService {493_serviceBrand: undefined;494readonly vscodeToolSet: ToolSet;495readonly executeToolSet: ToolSet;496readonly readToolSet: ToolSet;497readonly agentToolSet: ToolSet;498readonly onDidChangeTools: Event<void>;499readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ readonly sessionResource: URI; readonly toolData: IToolData }>;500readonly onDidInvokeTool: Event<IToolInvokedEvent>;501registerToolData(toolData: IToolData): IDisposable;502registerToolImplementation(id: string, tool: IToolImpl): IDisposable;503registerTool(toolData: IToolData, tool: IToolImpl): IDisposable;504505/**506* Get all tools currently enabled (matching `when` clauses and model).507* @param model The language model metadata to filter tools by. If undefined, model-specific filtering is skipped.508*/509getTools(model: ILanguageModelChatMetadata | undefined): Iterable<IToolData>;510511/**512* Creats an observable of enabled tools in the context. Note the observable513* should be created and reused, not created per reader, for example:514*515* ```516* const toolsObs = toolsService.observeTools(model);517* autorun(reader => {518* const tools = toolsObs.read(reader);519* ...520* });521* ```522* @param model The language model metadata to filter tools by. If undefined, model-specific filtering is skipped.523*/524observeTools(model: ILanguageModelChatMetadata | undefined): IObservable<readonly IToolData[]>;525526/**527* Get all registered tools regardless of enablement state.528* Use this for configuration UIs, completions, etc. where all tools should be visible.529*/530getAllToolsIncludingDisabled(): Iterable<IToolData>;531532/**533* Get a tool by its ID. Does not check when clauses.534*/535getTool(id: string): IToolData | undefined;536537/**538* Get a tool by its reference name. Does not check when clauses.539*/540getToolByName(name: string): IToolData | undefined;541542/**543* Begin a tool call in the streaming phase.544* Creates a ChatToolInvocation in the Streaming state and appends it to the chat.545* Returns the invocation so it can be looked up later when invokeTool is called.546*/547beginToolCall(options: IBeginToolCallOptions): IChatToolInvocation | undefined;548549/**550* Update the streaming state of a pending tool call.551* Calls the tool's handleToolStream method to get a custom invocation message.552*/553updateToolStream(toolCallId: string, partialInput: unknown, token: CancellationToken): Promise<void>;554555invokeTool(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise<IToolResult>;556cancelToolCallsForRequest(requestId: string): void;557/** Flush any pending tool updates to the extension hosts. */558flushToolUpdates(): void;559560readonly toolSets: IObservable<Iterable<IToolSet>>;561getToolSetsForModel(model: ILanguageModelChatMetadata | undefined, reader?: IReader): Iterable<IToolSet>;562getToolSet(id: string): IToolSet | undefined;563getToolSetByName(name: string): IToolSet | undefined;564createToolSet(source: ToolDataSource, id: string, referenceName: string, options?: { icon?: ThemeIcon; description?: string; legacyFullNames?: string[] }): ToolSet & IDisposable;565566// tool names in prompt and agent files ('full reference names')567getFullReferenceNames(): Iterable<string>;568getFullReferenceName(tool: IToolData, toolSet?: IToolSet): string;569getToolByFullReferenceName(fullReferenceName: string): IToolData | IToolSet | undefined;570getDeprecatedFullReferenceNames(): Map<string, Set<string>>;571572/**573* Gets the enablement maps based on the given set of references.574* @param fullReferenceNames The full reference names of the tools and tool sets to enable.575* @param model Optional language model metadata to filter tools by.576* If undefined is passed, all tools will be returned, even if normally disabled.577*/578toToolAndToolSetEnablementMap(fullReferenceNames: readonly string[], model: ILanguageModelChatMetadata | undefined): IToolAndToolSetEnablementMap;579580toFullReferenceNames(map: IToolAndToolSetEnablementMap): string[];581toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[];582}583584585export function createToolInputUri(toolCallId: string): URI {586return URI.from({ scheme: Schemas.inMemory, path: `/lm/tool/${toolCallId}/tool_input.json` });587}588589export function createToolSchemaUri(toolOrId: IToolData | string): URI {590if (typeof toolOrId !== 'string') {591toolOrId = toolOrId.id;592}593return URI.from({ scheme: Schemas.vscode, authority: 'schemas', path: `/lm/tool/${toolOrId}` });594}595596export namespace SpecedToolAliases {597export const execute = 'execute';598export const edit = 'edit';599export const search = 'search';600export const agent = 'agent';601export const read = 'read';602export const web = 'web';603export const todo = 'todo';604}605606export namespace VSCodeToolReference {607export const runSubagent = 'runSubagent';608export const vscode = 'vscode';609610}611612613