Path: blob/main/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts
13399 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 * as l10n from '@vscode/l10n';6import { Raw } from '@vscode/prompt-tsx';7import { BudgetExceededError } from '@vscode/prompt-tsx/dist/base/materialized';8import type * as vscode from 'vscode';9import { IAuthenticationService } from '../../../platform/authentication/common/authentication';10import { CanceledResult, ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError } from '../../../platform/chat/common/commonTypes';11import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';12import { IEditSurvivalTrackerService } from '../../../platform/editSurvivalTracking/common/editSurvivalTrackerService';13import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';14import { IOctoKitService } from '../../../platform/github/common/githubService';15import { IIgnoreService } from '../../../platform/ignore/common/ignoreService';16import { ILogService } from '../../../platform/log/common/logService';17import { IChatEndpoint, IMakeChatRequestOptions } from '../../../platform/networking/common/networking';18import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';19import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl';20import { toErrorMessage } from '../../../util/common/errorMessage';21import { isNonEmptyArray } from '../../../util/vs/base/common/arrays';22import { timeout } from '../../../util/vs/base/common/async';23import { CancellationToken } from '../../../util/vs/base/common/cancellation';24import { ResourceSet } from '../../../util/vs/base/common/map';25import { assertType, isDefined } from '../../../util/vs/base/common/types';26import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';27import { ChatRequestEditorData, ChatResponseTextEditPart, LanguageModelTextPart, LanguageModelToolResult } from '../../../vscodeTypes';28import { Intent } from '../../common/constants';29import { getAgentTools } from '../../intents/node/agentIntent';30import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection';31import { Conversation } from '../../prompt/common/conversation';32import { IToolCall } from '../../prompt/common/intents';33import { ToolCallRound } from '../../prompt/common/toolCallRound';34import { ChatTelemetryBuilder, InlineChatTelemetry } from '../../prompt/node/chatParticipantTelemetry';35import { IDocumentContext } from '../../prompt/node/documentContext';36import { IIntent } from '../../prompt/node/intents';37import { PromptRenderer } from '../../prompts/node/base/promptRenderer';38import { ICompletedToolCallRound, InlineChat2Prompt, LARGE_FILE_LINE_THRESHOLD } from './inlineChatPrompt';39import { ToolName } from '../../tools/common/toolNames';40import { CopilotToolMode } from '../../tools/common/toolsRegistry';41import { isToolValidationError, isValidatedToolInput, IToolsService } from '../../tools/common/toolsService';42import { InlineChatProgressMessages } from './progressMessages';43import { CopilotInteractiveEditorResponse, InteractionOutcome } from '../../inlineChat/node/promptCraftingTypes';444546const INLINE_CHAT_EXIT_TOOL_NAME = 'inline_chat_exit';4748interface IInlineChatEditResult {49telemetry: InlineChatTelemetry;50lastResponse: ChatResponse;51needsExitTool: boolean;52errorMessage?: string;53}545556export class InlineChatIntent implements IIntent {5758static readonly ID = Intent.InlineChat;59606162readonly id = InlineChatIntent.ID;6364readonly locations = [ChatLocation.Editor];6566readonly description: string = '';6768private readonly _progressMessages: InlineChatProgressMessages;6970constructor(71@IInstantiationService private readonly _instantiationService: IInstantiationService,72@IEndpointProvider private readonly _endpointProvider: IEndpointProvider,73@IAuthenticationService private readonly _authenticationService: IAuthenticationService,74@ILogService private readonly _logService: ILogService,75@IToolsService private readonly _toolsService: IToolsService,76@IIgnoreService private readonly _ignoreService: IIgnoreService,77@IEditSurvivalTrackerService private readonly _editSurvivalTrackerService: IEditSurvivalTrackerService,78@IOctoKitService private readonly _octoKitService: IOctoKitService,79) {80this._progressMessages = this._instantiationService.createInstance(InlineChatProgressMessages);81}8283async handleRequest(conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext | undefined, _agentName: string, _location: ChatLocation, chatTelemetry: ChatTelemetryBuilder): Promise<vscode.ChatResult> {8485assertType(request.location2 instanceof ChatRequestEditorData);86assertType(documentContext);8788if (await this._ignoreService.isCopilotIgnored(request.location2.document.uri, token)) {89return {90errorDetails: {91message: l10n.t('inlineChat.ignored', 'Copilot is disabled for this file.'),92}93};94}9596const endpoint = await this._endpointProvider.getChatEndpoint(request);9798if (!endpoint.supportsToolCalls) {99return {100errorDetails: {101message: l10n.t('inlineChat.model', '{0} cannot be used for inline chat', endpoint.name),102}103};104}105106107const editSurvivalTracker = this._editSurvivalTrackerService.initialize(request.location2.document);108109stream = ChatResponseStreamImpl.spy(stream, part => {110if (part instanceof ChatResponseTextEditPart) {111editSurvivalTracker.collectAIEdits(part.edits);112}113});114115// Start generating contextual message immediately116const contextualMessagePromise = this._progressMessages.getContextualMessage(request.prompt, documentContext, token);117118// Show progress message after ~1 second delay (unless request completes first)119timeout(1000, token).then(async () => {120const message = await contextualMessagePromise;121stream.progress(message);122});123124let result: IInlineChatEditResult;125try {126const inlineToolLoop = this._instantiationService.createInstance(InlineChatToolCalling, this);127128result = await inlineToolLoop.run(endpoint, conversation, request, stream, token, documentContext, chatTelemetry);129} catch (err) {130this._logService.error(err, 'InlineChatIntent: prompt rendering failed');131return {132errorDetails: {133message: err instanceof BudgetExceededError134? l10n.t('Sorry, this document is too large for inline chat.')135: toErrorMessage(err),136}137};138}139140if (token.isCancellationRequested) {141return CanceledResult;142}143144if (result.needsExitTool) {145this._logService.warn('[InlineChat], BAIL_OUT because of needsExitTool');146// BAILOUT: when no edits were emitted, invoke the exit tool manually147await this._toolsService.invokeTool(INLINE_CHAT_EXIT_TOOL_NAME, {148toolInvocationToken: request.toolInvocationToken, input: {149response: result.lastResponse.type === ChatFetchResponseType.Success ? result.lastResponse.value : undefined,150}151}, token);152}153154155// store metadata for telemetry sending156const turn = conversation.getLatestTurn();157turn.setMetadata(new CopilotInteractiveEditorResponse(158undefined,159{ ...documentContext, query: request.prompt, intent: this },160result.telemetry.telemetryMessageId, result.telemetry, editSurvivalTracker161));162163if (result.errorMessage) {164return {165errorDetails: {166message: result.errorMessage,167}168};169}170171if (result.lastResponse.type !== ChatFetchResponseType.Success) {172const outageStatus = await this._octoKitService.getGitHubOutageStatus();173const details = getErrorDetailsFromChatFetchError(result.lastResponse, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);174return {175errorDetails: {176message: details.message,177responseIsFiltered: details.responseIsFiltered178}179};180}181182return {};183}184185invoke(): Promise<never> {186throw new TypeError();187}188}189190class InlineChatToolCalling {191192private static readonly _EDIT_TOOLS = new Set<string>([193ToolName.ApplyPatch,194ToolName.EditFile,195ToolName.ReplaceString,196ToolName.MultiReplaceString,197]);198199constructor(200private readonly _intent: InlineChatIntent,201@IInstantiationService private readonly _instantiationService: IInstantiationService,202@ILogService private readonly _logService: ILogService,203@IToolsService private readonly _toolsService: IToolsService,204@IConfigurationService private readonly _configurationService: IConfigurationService,205@IExperimentationService private readonly _experimentationService: IExperimentationService,206) { }207208async run(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise<IInlineChatEditResult> {209assertType(request.location2 instanceof ChatRequestEditorData);210assertType(documentContext);211212const isLargeFile = documentContext.document.lineCount > LARGE_FILE_LINE_THRESHOLD;213const availableTools = await this._getAvailableTools(request, endpoint, isLargeFile);214215const previousRounds: ICompletedToolCallRound[] = [];216let failedEditCount = 0;217const toolCallRounds: ToolCallRound[] = [];218let readOnlyRounds = 0;219let telemetry: InlineChatTelemetry;220let lastResponse: ChatResponse;221let lastInteractionOutcome: InteractionOutcome;222223while (true) {224225const renderer = PromptRenderer.create(this._instantiationService, endpoint, InlineChat2Prompt, {226request,227previousRounds,228hasFailedEdits: failedEditCount > 0,229snapshotAtRequest: documentContext.document,230data: request.location2,231exitToolName: INLINE_CHAT_EXIT_TOOL_NAME,232isLargeFile,233readToolName: isLargeFile ? ToolName.ReadFile : undefined,234});235236const renderResult = await renderer.render(undefined, token, { trace: true });237238const toolTokenCount = availableTools.length > 0 ? await endpoint.acquireTokenizer().countToolTokens(availableTools) : 0;239telemetry = chatTelemetry.makeRequest(this._intent, ChatLocation.Editor, conversation, renderResult.messages, renderResult.tokenCount, renderResult.references, endpoint, [], availableTools.length, toolTokenCount);240241stream = ChatResponseStreamImpl.spy(stream, part => {242if (part instanceof ChatResponseTextEditPart) {243telemetry.markEmittedEdits(part.uri, part.edits);244}245});246247248const result = await this._makeRequestAndRunTools(endpoint, request, stream, renderResult.messages, availableTools, telemetry, token);249250lastInteractionOutcome = new InteractionOutcome(telemetry.editCount > 0 ? 'inlineEdit' : 'none', []);251lastResponse = result.fetchResult;252253// telemetry254{255const responseText = lastResponse.type === ChatFetchResponseType.Success ? lastResponse.value : '';256telemetry.sendTelemetry(257lastResponse.requestId, lastResponse.type, responseText,258lastInteractionOutcome,259result.toolCalls260);261262toolCallRounds.push(ToolCallRound.create({263response: responseText,264toolCalls: result.toolCalls,265toolInputRetry: failedEditCount266}));267}268269if (result.toolCalls.length === 0) {270// BAILOUT: when no tools have been used271break;272}273274// Build a completed round from all tool calls in their original order275const roundCalls: [IToolCall, vscode.ExtendedLanguageModelToolResult][] = [];276for (const toolCall of result.toolCalls) {277const toolResult = result.allCallResults.get(toolCall.id);278if (toolResult) {279roundCalls.push([toolCall, toolResult]);280}281}282previousRounds.push({ calls: roundCalls });283284// Check if this round was read-only (only read_file calls, no edit tool calls)285const hasEditToolCalls = result.toolCalls.some(tc => tc.name !== ToolName.ReadFile);286287if (!hasEditToolCalls) {288// Read-only round: the model used read_file to gather more context.289// Continue the loop so it can make edits with the new info.290readOnlyRounds++;291if (readOnlyRounds > 9) {292this._logService.warn('Aborting inline chat edit: too many read-only rounds');293break;294}295continue;296}297298if (result.failedEdits.length === 0 || token.isCancellationRequested) {299// DONE300break;301}302303failedEditCount += result.failedEdits.length;304if (failedEditCount > 5) {305// TOO MANY FAILED ATTEMPTS306this._logService.error(`Aborting inline chat edit: too many failed edit attempts`);307break;308}309}310311telemetry.sendToolCallingTelemetry(toolCallRounds, availableTools, token.isCancellationRequested ? 'cancelled' : lastResponse.type);312313const needsExitTool = lastResponse.type === ChatFetchResponseType.Success314&& (toolCallRounds.length === 0 || (toolCallRounds.length > 0 && toolCallRounds[toolCallRounds.length - 1].toolCalls.length === 0));315316if (!needsExitTool && failedEditCount > 0 && telemetry.editCount === 0 && lastResponse.type === ChatFetchResponseType.Success) {317return {318lastResponse,319telemetry,320needsExitTool: false,321errorMessage: l10n.t('Failed to edit the file. The requested change could not be applied.'),322};323}324325return { lastResponse, telemetry, needsExitTool };326}327328private async _makeRequestAndRunTools(endpoint: IChatEndpoint, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, messages: Raw.ChatMessage[], inlineChatTools: vscode.LanguageModelToolInformation[], telemetry: InlineChatTelemetry, token: CancellationToken) {329330const requestOptions: IMakeChatRequestOptions['requestOptions'] = {331tool_choice: 'auto',332// Inline chat only uses internal tools with known-good schemas,333// skip expensive normalizeToolSchema validation334tools: inlineChatTools.map(tool => ({335type: 'function' as const,336function: {337name: tool.name,338description: tool.description,339parameters: tool.inputSchema && Object.keys(tool.inputSchema).length ? tool.inputSchema : undefined340},341})),342};343344const toolCalls: IToolCall[] = [];345const failedEdits: [IToolCall, vscode.ExtendedLanguageModelToolResult][] = [];346const allCallResults = new Map<string, vscode.ExtendedLanguageModelToolResult>();347348const toolExecutions: Promise<unknown>[] = [];349350const fetchResult = await endpoint.makeChatRequest2({351debugName: 'InlineChat2Intent',352messages,353userInitiatedRequest: true,354location: ChatLocation.Editor,355requestOptions,356modelCapabilities: {357enableThinking: this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatEnableThinking, this._experimentationService),358reasoningEffort: typeof request.modelConfiguration?.reasoningEffort === 'string'359? request.modelConfiguration.reasoningEffort360: this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatReasoningEffort, this._experimentationService),361},362telemetryProperties: {363messageId: telemetry.telemetryMessageId,364conversationId: telemetry.sessionId,365messageSource: this._intent.id366},367finishedCb: async (_text, _index, delta) => {368369telemetry.markReceivedToken();370371if (!isNonEmptyArray(delta.copilotToolCalls)) {372return undefined;373}374375const exitToolCall = delta.copilotToolCalls.find(candidate => candidate.name === INLINE_CHAT_EXIT_TOOL_NAME);376const copilotToolCalls = exitToolCall ? [exitToolCall] : delta.copilotToolCalls;377378for (const toolCall of copilotToolCalls) {379380toolCalls.push(toolCall);381382const validationResult = this._toolsService.validateToolInput(toolCall.name, toolCall.arguments);383384if (isToolValidationError(validationResult)) {385this._logService.warn(`Tool ${toolCall.name} invocation failed validation: ${validationResult}`);386const errorResult = new LanguageModelToolResult([new LanguageModelTextPart(validationResult.error)]);387allCallResults.set(toolCall.id, errorResult);388failedEdits.push([toolCall, errorResult]);389continue;390}391392toolExecutions.push((async () => {393try {394let input = isValidatedToolInput(validationResult)395? validationResult.inputObj396: JSON.parse(toolCall.arguments);397398const copilotTool = this._toolsService.getCopilotTool(toolCall.name as ToolName);399if (copilotTool?.resolveInput) {400input = await copilotTool.resolveInput(input, {401request,402stream,403query: request.prompt,404chatVariables: new ChatVariablesCollection([...request.references]),405history: [],406allowedEditUris: request.location2 instanceof ChatRequestEditorData ? new ResourceSet([request.location2.document.uri]) : undefined,407}, CopilotToolMode.FullContext);408}409410const result = await this._toolsService.invokeToolWithEndpoint(toolCall.name, {411input,412toolInvocationToken: request.toolInvocationToken,413// Split on `__vscode` so it's the chat stream id414// TODO @lramos15 - This is a gross hack415chatStreamToolCallId: toolCall.id.split('__vscode')[0],416}, endpoint, token) as vscode.ExtendedLanguageModelToolResult;417418allCallResults.set(toolCall.id, result);419420if (result.hasError) {421failedEdits.push([toolCall, result]);422stream.progress(l10n.t('Looking not yet good, trying again...'));423}424425this._logService.trace(`Tool ${toolCall.name} invocation result: ${JSON.stringify(result)}`);426427} catch (err) {428this._logService.error(err, `Tool ${toolCall.name} invocation failed`);429const errorResult = new LanguageModelToolResult([new LanguageModelTextPart(toErrorMessage(err))]);430allCallResults.set(toolCall.id, errorResult);431failedEdits.push([toolCall, errorResult]);432}433})());434}435436return undefined;437}438}, token);439440await Promise.allSettled(toolExecutions);441442return { fetchResult, toolCalls, failedEdits, allCallResults };443}444445private async _getAvailableTools(request: vscode.ChatRequest, model: IChatEndpoint, isLargeFile: boolean): Promise<vscode.LanguageModelToolInformation[]> {446assertType(request.location2 instanceof ChatRequestEditorData);447448449const enabledTools = new Set(InlineChatToolCalling._EDIT_TOOLS);450if (!request.location2.selection.isEmpty) {451// only used the multi-replace when there is no selection452enabledTools.delete(ToolName.MultiReplaceString);453}454455// ALWAYS enable editing tools (only) and ignore what the client did send456const fakeRequest: vscode.ChatRequest = {457...request,458tools: new Map(459Array.from(enabledTools)460.map(t => this._toolsService.getTool(t))461.filter(isDefined)462.map(tool => [tool, true])463),464};465466const agentTools = await this._instantiationService.invokeFunction(getAgentTools, fakeRequest, model);467let editTools = agentTools.filter(tool => enabledTools.has(tool.name));468469if (editTools.length === 0) {470this._logService.error('MISSING inline chat edit tools');471throw new Error('MISSING inline chat edit tools');472}473474// EditFile is a poor performer, prefer other edit tools when available475if (editTools.length > 1) {476editTools = editTools.filter(tool => tool.name !== ToolName.EditFile);477}478// const result = [exitTool, ...editTools];479const result = [...editTools];480481// For large files, also include the read tool so the model can read more of the file482if (isLargeFile) {483const readTool = this._toolsService.getTool(ToolName.ReadFile);484if (readTool) {485result.push(readTool);486} else {487this._logService.error('MISSING inline chat read tool for large file');488throw new Error('MISSING inline chat read tool for large file');489}490}491492return result;493}494}495496497