Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts
13399 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 * as l10n from '@vscode/l10n';
7
import { Raw } from '@vscode/prompt-tsx';
8
import { BudgetExceededError } from '@vscode/prompt-tsx/dist/base/materialized';
9
import type * as vscode from 'vscode';
10
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
11
import { CanceledResult, ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError } from '../../../platform/chat/common/commonTypes';
12
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
13
import { IEditSurvivalTrackerService } from '../../../platform/editSurvivalTracking/common/editSurvivalTrackerService';
14
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
15
import { IOctoKitService } from '../../../platform/github/common/githubService';
16
import { IIgnoreService } from '../../../platform/ignore/common/ignoreService';
17
import { ILogService } from '../../../platform/log/common/logService';
18
import { IChatEndpoint, IMakeChatRequestOptions } from '../../../platform/networking/common/networking';
19
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
20
import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl';
21
import { toErrorMessage } from '../../../util/common/errorMessage';
22
import { isNonEmptyArray } from '../../../util/vs/base/common/arrays';
23
import { timeout } from '../../../util/vs/base/common/async';
24
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
25
import { ResourceSet } from '../../../util/vs/base/common/map';
26
import { assertType, isDefined } from '../../../util/vs/base/common/types';
27
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
28
import { ChatRequestEditorData, ChatResponseTextEditPart, LanguageModelTextPart, LanguageModelToolResult } from '../../../vscodeTypes';
29
import { Intent } from '../../common/constants';
30
import { getAgentTools } from '../../intents/node/agentIntent';
31
import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection';
32
import { Conversation } from '../../prompt/common/conversation';
33
import { IToolCall } from '../../prompt/common/intents';
34
import { ToolCallRound } from '../../prompt/common/toolCallRound';
35
import { ChatTelemetryBuilder, InlineChatTelemetry } from '../../prompt/node/chatParticipantTelemetry';
36
import { IDocumentContext } from '../../prompt/node/documentContext';
37
import { IIntent } from '../../prompt/node/intents';
38
import { PromptRenderer } from '../../prompts/node/base/promptRenderer';
39
import { ICompletedToolCallRound, InlineChat2Prompt, LARGE_FILE_LINE_THRESHOLD } from './inlineChatPrompt';
40
import { ToolName } from '../../tools/common/toolNames';
41
import { CopilotToolMode } from '../../tools/common/toolsRegistry';
42
import { isToolValidationError, isValidatedToolInput, IToolsService } from '../../tools/common/toolsService';
43
import { InlineChatProgressMessages } from './progressMessages';
44
import { CopilotInteractiveEditorResponse, InteractionOutcome } from '../../inlineChat/node/promptCraftingTypes';
45
46
47
const INLINE_CHAT_EXIT_TOOL_NAME = 'inline_chat_exit';
48
49
interface IInlineChatEditResult {
50
telemetry: InlineChatTelemetry;
51
lastResponse: ChatResponse;
52
needsExitTool: boolean;
53
errorMessage?: string;
54
}
55
56
57
export class InlineChatIntent implements IIntent {
58
59
static readonly ID = Intent.InlineChat;
60
61
62
63
readonly id = InlineChatIntent.ID;
64
65
readonly locations = [ChatLocation.Editor];
66
67
readonly description: string = '';
68
69
private readonly _progressMessages: InlineChatProgressMessages;
70
71
constructor(
72
@IInstantiationService private readonly _instantiationService: IInstantiationService,
73
@IEndpointProvider private readonly _endpointProvider: IEndpointProvider,
74
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
75
@ILogService private readonly _logService: ILogService,
76
@IToolsService private readonly _toolsService: IToolsService,
77
@IIgnoreService private readonly _ignoreService: IIgnoreService,
78
@IEditSurvivalTrackerService private readonly _editSurvivalTrackerService: IEditSurvivalTrackerService,
79
@IOctoKitService private readonly _octoKitService: IOctoKitService,
80
) {
81
this._progressMessages = this._instantiationService.createInstance(InlineChatProgressMessages);
82
}
83
84
async handleRequest(conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext | undefined, _agentName: string, _location: ChatLocation, chatTelemetry: ChatTelemetryBuilder): Promise<vscode.ChatResult> {
85
86
assertType(request.location2 instanceof ChatRequestEditorData);
87
assertType(documentContext);
88
89
if (await this._ignoreService.isCopilotIgnored(request.location2.document.uri, token)) {
90
return {
91
errorDetails: {
92
message: l10n.t('inlineChat.ignored', 'Copilot is disabled for this file.'),
93
}
94
};
95
}
96
97
const endpoint = await this._endpointProvider.getChatEndpoint(request);
98
99
if (!endpoint.supportsToolCalls) {
100
return {
101
errorDetails: {
102
message: l10n.t('inlineChat.model', '{0} cannot be used for inline chat', endpoint.name),
103
}
104
};
105
}
106
107
108
const editSurvivalTracker = this._editSurvivalTrackerService.initialize(request.location2.document);
109
110
stream = ChatResponseStreamImpl.spy(stream, part => {
111
if (part instanceof ChatResponseTextEditPart) {
112
editSurvivalTracker.collectAIEdits(part.edits);
113
}
114
});
115
116
// Start generating contextual message immediately
117
const contextualMessagePromise = this._progressMessages.getContextualMessage(request.prompt, documentContext, token);
118
119
// Show progress message after ~1 second delay (unless request completes first)
120
timeout(1000, token).then(async () => {
121
const message = await contextualMessagePromise;
122
stream.progress(message);
123
});
124
125
let result: IInlineChatEditResult;
126
try {
127
const inlineToolLoop = this._instantiationService.createInstance(InlineChatToolCalling, this);
128
129
result = await inlineToolLoop.run(endpoint, conversation, request, stream, token, documentContext, chatTelemetry);
130
} catch (err) {
131
this._logService.error(err, 'InlineChatIntent: prompt rendering failed');
132
return {
133
errorDetails: {
134
message: err instanceof BudgetExceededError
135
? l10n.t('Sorry, this document is too large for inline chat.')
136
: toErrorMessage(err),
137
}
138
};
139
}
140
141
if (token.isCancellationRequested) {
142
return CanceledResult;
143
}
144
145
if (result.needsExitTool) {
146
this._logService.warn('[InlineChat], BAIL_OUT because of needsExitTool');
147
// BAILOUT: when no edits were emitted, invoke the exit tool manually
148
await this._toolsService.invokeTool(INLINE_CHAT_EXIT_TOOL_NAME, {
149
toolInvocationToken: request.toolInvocationToken, input: {
150
response: result.lastResponse.type === ChatFetchResponseType.Success ? result.lastResponse.value : undefined,
151
}
152
}, token);
153
}
154
155
156
// store metadata for telemetry sending
157
const turn = conversation.getLatestTurn();
158
turn.setMetadata(new CopilotInteractiveEditorResponse(
159
undefined,
160
{ ...documentContext, query: request.prompt, intent: this },
161
result.telemetry.telemetryMessageId, result.telemetry, editSurvivalTracker
162
));
163
164
if (result.errorMessage) {
165
return {
166
errorDetails: {
167
message: result.errorMessage,
168
}
169
};
170
}
171
172
if (result.lastResponse.type !== ChatFetchResponseType.Success) {
173
const outageStatus = await this._octoKitService.getGitHubOutageStatus();
174
const details = getErrorDetailsFromChatFetchError(result.lastResponse, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);
175
return {
176
errorDetails: {
177
message: details.message,
178
responseIsFiltered: details.responseIsFiltered
179
}
180
};
181
}
182
183
return {};
184
}
185
186
invoke(): Promise<never> {
187
throw new TypeError();
188
}
189
}
190
191
class InlineChatToolCalling {
192
193
private static readonly _EDIT_TOOLS = new Set<string>([
194
ToolName.ApplyPatch,
195
ToolName.EditFile,
196
ToolName.ReplaceString,
197
ToolName.MultiReplaceString,
198
]);
199
200
constructor(
201
private readonly _intent: InlineChatIntent,
202
@IInstantiationService private readonly _instantiationService: IInstantiationService,
203
@ILogService private readonly _logService: ILogService,
204
@IToolsService private readonly _toolsService: IToolsService,
205
@IConfigurationService private readonly _configurationService: IConfigurationService,
206
@IExperimentationService private readonly _experimentationService: IExperimentationService,
207
) { }
208
209
async run(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise<IInlineChatEditResult> {
210
assertType(request.location2 instanceof ChatRequestEditorData);
211
assertType(documentContext);
212
213
const isLargeFile = documentContext.document.lineCount > LARGE_FILE_LINE_THRESHOLD;
214
const availableTools = await this._getAvailableTools(request, endpoint, isLargeFile);
215
216
const previousRounds: ICompletedToolCallRound[] = [];
217
let failedEditCount = 0;
218
const toolCallRounds: ToolCallRound[] = [];
219
let readOnlyRounds = 0;
220
let telemetry: InlineChatTelemetry;
221
let lastResponse: ChatResponse;
222
let lastInteractionOutcome: InteractionOutcome;
223
224
while (true) {
225
226
const renderer = PromptRenderer.create(this._instantiationService, endpoint, InlineChat2Prompt, {
227
request,
228
previousRounds,
229
hasFailedEdits: failedEditCount > 0,
230
snapshotAtRequest: documentContext.document,
231
data: request.location2,
232
exitToolName: INLINE_CHAT_EXIT_TOOL_NAME,
233
isLargeFile,
234
readToolName: isLargeFile ? ToolName.ReadFile : undefined,
235
});
236
237
const renderResult = await renderer.render(undefined, token, { trace: true });
238
239
const toolTokenCount = availableTools.length > 0 ? await endpoint.acquireTokenizer().countToolTokens(availableTools) : 0;
240
telemetry = chatTelemetry.makeRequest(this._intent, ChatLocation.Editor, conversation, renderResult.messages, renderResult.tokenCount, renderResult.references, endpoint, [], availableTools.length, toolTokenCount);
241
242
stream = ChatResponseStreamImpl.spy(stream, part => {
243
if (part instanceof ChatResponseTextEditPart) {
244
telemetry.markEmittedEdits(part.uri, part.edits);
245
}
246
});
247
248
249
const result = await this._makeRequestAndRunTools(endpoint, request, stream, renderResult.messages, availableTools, telemetry, token);
250
251
lastInteractionOutcome = new InteractionOutcome(telemetry.editCount > 0 ? 'inlineEdit' : 'none', []);
252
lastResponse = result.fetchResult;
253
254
// telemetry
255
{
256
const responseText = lastResponse.type === ChatFetchResponseType.Success ? lastResponse.value : '';
257
telemetry.sendTelemetry(
258
lastResponse.requestId, lastResponse.type, responseText,
259
lastInteractionOutcome,
260
result.toolCalls
261
);
262
263
toolCallRounds.push(ToolCallRound.create({
264
response: responseText,
265
toolCalls: result.toolCalls,
266
toolInputRetry: failedEditCount
267
}));
268
}
269
270
if (result.toolCalls.length === 0) {
271
// BAILOUT: when no tools have been used
272
break;
273
}
274
275
// Build a completed round from all tool calls in their original order
276
const roundCalls: [IToolCall, vscode.ExtendedLanguageModelToolResult][] = [];
277
for (const toolCall of result.toolCalls) {
278
const toolResult = result.allCallResults.get(toolCall.id);
279
if (toolResult) {
280
roundCalls.push([toolCall, toolResult]);
281
}
282
}
283
previousRounds.push({ calls: roundCalls });
284
285
// Check if this round was read-only (only read_file calls, no edit tool calls)
286
const hasEditToolCalls = result.toolCalls.some(tc => tc.name !== ToolName.ReadFile);
287
288
if (!hasEditToolCalls) {
289
// Read-only round: the model used read_file to gather more context.
290
// Continue the loop so it can make edits with the new info.
291
readOnlyRounds++;
292
if (readOnlyRounds > 9) {
293
this._logService.warn('Aborting inline chat edit: too many read-only rounds');
294
break;
295
}
296
continue;
297
}
298
299
if (result.failedEdits.length === 0 || token.isCancellationRequested) {
300
// DONE
301
break;
302
}
303
304
failedEditCount += result.failedEdits.length;
305
if (failedEditCount > 5) {
306
// TOO MANY FAILED ATTEMPTS
307
this._logService.error(`Aborting inline chat edit: too many failed edit attempts`);
308
break;
309
}
310
}
311
312
telemetry.sendToolCallingTelemetry(toolCallRounds, availableTools, token.isCancellationRequested ? 'cancelled' : lastResponse.type);
313
314
const needsExitTool = lastResponse.type === ChatFetchResponseType.Success
315
&& (toolCallRounds.length === 0 || (toolCallRounds.length > 0 && toolCallRounds[toolCallRounds.length - 1].toolCalls.length === 0));
316
317
if (!needsExitTool && failedEditCount > 0 && telemetry.editCount === 0 && lastResponse.type === ChatFetchResponseType.Success) {
318
return {
319
lastResponse,
320
telemetry,
321
needsExitTool: false,
322
errorMessage: l10n.t('Failed to edit the file. The requested change could not be applied.'),
323
};
324
}
325
326
return { lastResponse, telemetry, needsExitTool };
327
}
328
329
private async _makeRequestAndRunTools(endpoint: IChatEndpoint, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, messages: Raw.ChatMessage[], inlineChatTools: vscode.LanguageModelToolInformation[], telemetry: InlineChatTelemetry, token: CancellationToken) {
330
331
const requestOptions: IMakeChatRequestOptions['requestOptions'] = {
332
tool_choice: 'auto',
333
// Inline chat only uses internal tools with known-good schemas,
334
// skip expensive normalizeToolSchema validation
335
tools: inlineChatTools.map(tool => ({
336
type: 'function' as const,
337
function: {
338
name: tool.name,
339
description: tool.description,
340
parameters: tool.inputSchema && Object.keys(tool.inputSchema).length ? tool.inputSchema : undefined
341
},
342
})),
343
};
344
345
const toolCalls: IToolCall[] = [];
346
const failedEdits: [IToolCall, vscode.ExtendedLanguageModelToolResult][] = [];
347
const allCallResults = new Map<string, vscode.ExtendedLanguageModelToolResult>();
348
349
const toolExecutions: Promise<unknown>[] = [];
350
351
const fetchResult = await endpoint.makeChatRequest2({
352
debugName: 'InlineChat2Intent',
353
messages,
354
userInitiatedRequest: true,
355
location: ChatLocation.Editor,
356
requestOptions,
357
modelCapabilities: {
358
enableThinking: this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatEnableThinking, this._experimentationService),
359
reasoningEffort: typeof request.modelConfiguration?.reasoningEffort === 'string'
360
? request.modelConfiguration.reasoningEffort
361
: this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatReasoningEffort, this._experimentationService),
362
},
363
telemetryProperties: {
364
messageId: telemetry.telemetryMessageId,
365
conversationId: telemetry.sessionId,
366
messageSource: this._intent.id
367
},
368
finishedCb: async (_text, _index, delta) => {
369
370
telemetry.markReceivedToken();
371
372
if (!isNonEmptyArray(delta.copilotToolCalls)) {
373
return undefined;
374
}
375
376
const exitToolCall = delta.copilotToolCalls.find(candidate => candidate.name === INLINE_CHAT_EXIT_TOOL_NAME);
377
const copilotToolCalls = exitToolCall ? [exitToolCall] : delta.copilotToolCalls;
378
379
for (const toolCall of copilotToolCalls) {
380
381
toolCalls.push(toolCall);
382
383
const validationResult = this._toolsService.validateToolInput(toolCall.name, toolCall.arguments);
384
385
if (isToolValidationError(validationResult)) {
386
this._logService.warn(`Tool ${toolCall.name} invocation failed validation: ${validationResult}`);
387
const errorResult = new LanguageModelToolResult([new LanguageModelTextPart(validationResult.error)]);
388
allCallResults.set(toolCall.id, errorResult);
389
failedEdits.push([toolCall, errorResult]);
390
continue;
391
}
392
393
toolExecutions.push((async () => {
394
try {
395
let input = isValidatedToolInput(validationResult)
396
? validationResult.inputObj
397
: JSON.parse(toolCall.arguments);
398
399
const copilotTool = this._toolsService.getCopilotTool(toolCall.name as ToolName);
400
if (copilotTool?.resolveInput) {
401
input = await copilotTool.resolveInput(input, {
402
request,
403
stream,
404
query: request.prompt,
405
chatVariables: new ChatVariablesCollection([...request.references]),
406
history: [],
407
allowedEditUris: request.location2 instanceof ChatRequestEditorData ? new ResourceSet([request.location2.document.uri]) : undefined,
408
}, CopilotToolMode.FullContext);
409
}
410
411
const result = await this._toolsService.invokeToolWithEndpoint(toolCall.name, {
412
input,
413
toolInvocationToken: request.toolInvocationToken,
414
// Split on `__vscode` so it's the chat stream id
415
// TODO @lramos15 - This is a gross hack
416
chatStreamToolCallId: toolCall.id.split('__vscode')[0],
417
}, endpoint, token) as vscode.ExtendedLanguageModelToolResult;
418
419
allCallResults.set(toolCall.id, result);
420
421
if (result.hasError) {
422
failedEdits.push([toolCall, result]);
423
stream.progress(l10n.t('Looking not yet good, trying again...'));
424
}
425
426
this._logService.trace(`Tool ${toolCall.name} invocation result: ${JSON.stringify(result)}`);
427
428
} catch (err) {
429
this._logService.error(err, `Tool ${toolCall.name} invocation failed`);
430
const errorResult = new LanguageModelToolResult([new LanguageModelTextPart(toErrorMessage(err))]);
431
allCallResults.set(toolCall.id, errorResult);
432
failedEdits.push([toolCall, errorResult]);
433
}
434
})());
435
}
436
437
return undefined;
438
}
439
}, token);
440
441
await Promise.allSettled(toolExecutions);
442
443
return { fetchResult, toolCalls, failedEdits, allCallResults };
444
}
445
446
private async _getAvailableTools(request: vscode.ChatRequest, model: IChatEndpoint, isLargeFile: boolean): Promise<vscode.LanguageModelToolInformation[]> {
447
assertType(request.location2 instanceof ChatRequestEditorData);
448
449
450
const enabledTools = new Set(InlineChatToolCalling._EDIT_TOOLS);
451
if (!request.location2.selection.isEmpty) {
452
// only used the multi-replace when there is no selection
453
enabledTools.delete(ToolName.MultiReplaceString);
454
}
455
456
// ALWAYS enable editing tools (only) and ignore what the client did send
457
const fakeRequest: vscode.ChatRequest = {
458
...request,
459
tools: new Map(
460
Array.from(enabledTools)
461
.map(t => this._toolsService.getTool(t))
462
.filter(isDefined)
463
.map(tool => [tool, true])
464
),
465
};
466
467
const agentTools = await this._instantiationService.invokeFunction(getAgentTools, fakeRequest, model);
468
let editTools = agentTools.filter(tool => enabledTools.has(tool.name));
469
470
if (editTools.length === 0) {
471
this._logService.error('MISSING inline chat edit tools');
472
throw new Error('MISSING inline chat edit tools');
473
}
474
475
// EditFile is a poor performer, prefer other edit tools when available
476
if (editTools.length > 1) {
477
editTools = editTools.filter(tool => tool.name !== ToolName.EditFile);
478
}
479
// const result = [exitTool, ...editTools];
480
const result = [...editTools];
481
482
// For large files, also include the read tool so the model can read more of the file
483
if (isLargeFile) {
484
const readTool = this._toolsService.getTool(ToolName.ReadFile);
485
if (readTool) {
486
result.push(readTool);
487
} else {
488
this._logService.error('MISSING inline chat read tool for large file');
489
throw new Error('MISSING inline chat read tool for large file');
490
}
491
}
492
493
return result;
494
}
495
}
496
497