Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/node/defaultIntentRequestHandler.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 type { ChatRequest, ChatResponseReferencePart, ChatResponseStream, ChatResult, LanguageModelToolInformation, Progress } from 'vscode';
9
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
10
import { IAuthenticationChatUpgradeService } from '../../../platform/authentication/common/authenticationUpgrade';
11
import { IChatHookService, UserPromptSubmitHookInput, UserPromptSubmitHookOutput } from '../../../platform/chat/common/chatHookService';
12
import { CanceledResult, ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError } from '../../../platform/chat/common/commonTypes';
13
import { IConversationOptions } from '../../../platform/chat/common/conversationOptions';
14
import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService';
15
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
16
import { IEditSurvivalTrackerService, IEditSurvivalTrackingSession, NullEditSurvivalTrackingSession } from '../../../platform/editSurvivalTracking/common/editSurvivalTrackerService';
17
import { isAnthropicFamily } from '../../../platform/endpoint/common/chatModelCapabilities';
18
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
19
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
20
import { IGitService } from '../../../platform/git/common/gitService';
21
import { IOctoKitService } from '../../../platform/github/common/githubService';
22
import { HAS_IGNORED_FILES_MESSAGE } from '../../../platform/ignore/common/ignoreService';
23
import { ILogService } from '../../../platform/log/common/logService';
24
import { isAnthropicContextEditingEnabled } from '../../../platform/networking/common/anthropic';
25
import { FilterReason } from '../../../platform/networking/common/openai';
26
import { IOTelService } from '../../../platform/otel/common/otelService';
27
import { CapturingToken } from '../../../platform/requestLogger/common/capturingToken';
28
import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger';
29
import { ISurveyService } from '../../../platform/survey/common/surveyService';
30
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
31
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
32
import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl';
33
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
34
import { isCancellationError } from '../../../util/vs/base/common/errors';
35
import { Event } from '../../../util/vs/base/common/event';
36
import { Iterable } from '../../../util/vs/base/common/iterator';
37
import { DisposableStore } from '../../../util/vs/base/common/lifecycle';
38
import { mixin } from '../../../util/vs/base/common/objects';
39
import { assertType, Mutable } from '../../../util/vs/base/common/types';
40
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
41
import { ChatResponseMarkdownPart, ChatResponseProgressPart, ChatResponseTextEditPart, LanguageModelToolResult2 } from '../../../vscodeTypes';
42
import { CodeBlocksMetadata, CodeBlockTrackingChatResponseStream } from '../../codeBlocks/node/codeBlockProcessor';
43
import { CopilotInteractiveEditorResponse, InteractionOutcomeComputer } from '../../inlineChat/node/promptCraftingTypes';
44
import { formatHookErrorMessage, HookAbortError, isHookAbortError, processHookResults } from '../../intents/node/hookResultProcessor';
45
import { EmptyPromptError, IToolCallingBuiltPromptEvent, IToolCallingLoopOptions, IToolCallingResponseEvent, IToolCallLoopResult, ToolCallingLoop, ToolCallingLoopFetchOptions, ToolCallLimitBehavior } from '../../intents/node/toolCallingLoop';
46
import { UnknownIntent } from '../../intents/node/unknownIntent';
47
import { ResponseStreamWithLinkification } from '../../linkify/common/responseStreamWithLinkification';
48
import { SummarizedConversationHistoryMetadata } from '../../prompts/node/agent/summarizedConversationHistory';
49
import { normalizeToolSchema } from '../../tools/common/toolSchemaNormalizer';
50
import { ToolCallCancelledError } from '../../tools/common/toolsService';
51
import { IToolGrouping, IToolGroupingService } from '../../tools/common/virtualTools/virtualToolTypes';
52
import { ChatVariablesCollection } from '../common/chatVariablesCollection';
53
import { AnthropicTokenUsageMetadata, Conversation, getUniqueReferences, GlobalContextMessageMetadata, IResultMetadata, RenderedUserMessageMetadata, RequestDebugInformation, ResponseStreamParticipant, Turn, TurnStatus } from '../common/conversation';
54
import { IBuildPromptContext, IToolCallRound } from '../common/intents';
55
import { isToolCallLimitCancellation, ISwitchToAutoOnRateLimitConfirmation } from '../common/specialRequestTypes';
56
import { ChatTelemetry, ChatTelemetryBuilder } from './chatParticipantTelemetry';
57
import { IntentInvocationMetadata } from './conversation';
58
import { IDocumentContext } from './documentContext';
59
import { IBuildPromptResult, IIntent, IIntentInvocation, IResponseProcessor, TelemetryData } from './intents';
60
import { ConversationalBaseTelemetryData, createTelemetryWithId, sendModelMessageTelemetry } from './telemetry';
61
62
export interface IDefaultIntentRequestHandlerOptions {
63
maxToolCallIterations: number;
64
/**
65
* Whether to ask the user if they want to continue when the tool call limit
66
* is exceeded. Defaults to true.
67
*/
68
confirmOnMaxToolIterations?: boolean;
69
temperature?: number;
70
overrideRequestLocation?: ChatLocation;
71
}
72
73
/*
74
* Handles a single chat-request via an intent-invocation.
75
*/
76
export class DefaultIntentRequestHandler {
77
78
private readonly turn: Turn;
79
80
private _editSurvivalTracker: IEditSurvivalTrackingSession = new NullEditSurvivalTrackingSession();
81
private _loop!: DefaultToolCallingLoop;
82
83
constructor(
84
private readonly intent: IIntent,
85
private readonly conversation: Conversation,
86
protected readonly request: ChatRequest,
87
protected readonly stream: ChatResponseStream,
88
private readonly token: CancellationToken,
89
protected readonly documentContext: IDocumentContext | undefined,
90
private readonly location: ChatLocation,
91
private readonly chatTelemetryBuilder: ChatTelemetryBuilder,
92
private readonly handlerOptions: IDefaultIntentRequestHandlerOptions = { maxToolCallIterations: 15 },
93
private readonly yieldRequested: (() => boolean) | undefined,
94
@IInstantiationService private readonly _instantiationService: IInstantiationService,
95
@IConversationOptions private readonly options: IConversationOptions,
96
@ITelemetryService private readonly _telemetryService: ITelemetryService,
97
@ILogService private readonly _logService: ILogService,
98
@ISurveyService private readonly _surveyService: ISurveyService,
99
@IRequestLogger private readonly _requestLogger: IRequestLogger,
100
@IEditSurvivalTrackerService private readonly _editSurvivalTrackerService: IEditSurvivalTrackerService,
101
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
102
@IChatHookService private readonly _chatHookService: IChatHookService,
103
@IOctoKitService private readonly _octoKitService: IOctoKitService,
104
@IConfigurationService private readonly _configurationService: IConfigurationService,
105
) {
106
// Initialize properties
107
this.turn = conversation.getLatestTurn();
108
}
109
110
async getResult(): Promise<ChatResult> {
111
if (isToolCallLimitCancellation(this.request)) {
112
// Just some friendly text instead of an empty message on cancellation:
113
this.stream.markdown(l10n.t("Let me know if there's anything else I can help with!"));
114
return {};
115
}
116
117
try {
118
if (this.token.isCancellationRequested) {
119
return CanceledResult;
120
}
121
122
this._logService.trace('Processing intent');
123
const intentInvocation = await this.intent.invoke({ location: this.location, documentContext: this.documentContext, request: this.request });
124
if (this.token.isCancellationRequested) {
125
return CanceledResult;
126
}
127
this._logService.trace('Processed intent');
128
129
this.turn.setMetadata(new IntentInvocationMetadata(intentInvocation));
130
131
const confirmationResult = await this.handleConfirmationsIfNeeded();
132
if (confirmationResult) {
133
return confirmationResult;
134
}
135
136
// For subagent requests, use the subAgentInvocationId as the session ID.
137
// This enables explicit linking between the parent's runSubagent tool call and the subagent trajectory.
138
// For main requests, use the VS Code chat sessionId directly as the trajectory session ID.
139
const isSubagent = !!this.request.subAgentInvocationId;
140
const capturingToken = new CapturingToken(
141
this.request.prompt,
142
'comment',
143
this.request.subAgentInvocationId,
144
this.request.subAgentName,
145
// For subagents, use invocation ID as chatSessionId so spans get their own log file
146
isSubagent ? this.request.subAgentInvocationId : this.request.sessionId,
147
// For subagents, link back to the parent session
148
isSubagent ? this.request.sessionId : undefined,
149
isSubagent ? `runSubagent-${this.request.subAgentName ?? 'default'}` : undefined,
150
);
151
const resultDetails = await this._requestLogger.captureInvocation(capturingToken, () => this.runWithToolCalling(intentInvocation));
152
153
let chatResult = resultDetails.chatResult || {};
154
this._surveyService.signalUsage(`${this.location === ChatLocation.Editor ? 'inline' : 'panel'}.${this.intent.id}`, this.documentContext?.document.languageId);
155
156
const responseMessage = resultDetails.toolCallRounds.at(-1)?.response ?? '';
157
const metadataFragment: Partial<IResultMetadata> = {
158
toolCallRounds: resultDetails.toolCallRounds,
159
toolCallResults: this._collectRelevantToolCallResults(resultDetails.toolCallRounds, resultDetails.toolCallResults),
160
resolvedModel: resultDetails.response.type === ChatFetchResponseType.Success ? resultDetails.response.resolvedModel : undefined,
161
};
162
mixin(chatResult, { metadata: metadataFragment }, true);
163
const baseModelTelemetry = createTelemetryWithId();
164
chatResult = await this.processResult(resultDetails.response, responseMessage, chatResult, metadataFragment, baseModelTelemetry, resultDetails.toolCallRounds);
165
if (chatResult.errorDetails && intentInvocation.modifyErrorDetails) {
166
chatResult.errorDetails = intentInvocation.modifyErrorDetails(chatResult.errorDetails, resultDetails.response);
167
}
168
169
if (resultDetails.hadIgnoredFiles) {
170
this.stream.markdown(HAS_IGNORED_FILES_MESSAGE);
171
}
172
173
return chatResult;
174
} catch (err) {
175
if (err instanceof ToolCallCancelledError) {
176
this.turn.setResponse(TurnStatus.Cancelled, { message: err.message, type: 'meta' }, undefined, {});
177
return {};
178
} else if (isCancellationError(err)) {
179
return CanceledResult;
180
} else if (err instanceof EmptyPromptError) {
181
return {};
182
} else if (isHookAbortError(err)) {
183
this._logService.info(`[DefaultIntentRequestHandler] Hook ${err.hookType} aborted: ${err.stopReason}`);
184
return {};
185
}
186
187
this._logService.error(err);
188
this._telemetryService.sendGHTelemetryException(err, 'Error');
189
const errorMessage = (<Error>err).message;
190
const chatResult = { errorDetails: { message: errorMessage } };
191
this.turn.setResponse(TurnStatus.Error, { message: errorMessage, type: 'meta' }, undefined, chatResult);
192
return chatResult;
193
}
194
}
195
196
private _collectRelevantToolCallResults(toolCallRounds: IToolCallRound[], toolCallResults: Record<string, LanguageModelToolResult2>): Record<string, LanguageModelToolResult2> | undefined {
197
const resultsUsedInThisTurn: Record<string, LanguageModelToolResult2> = {};
198
for (const round of toolCallRounds) {
199
for (const toolCall of round.toolCalls) {
200
resultsUsedInThisTurn[toolCall.id] = toolCallResults[toolCall.id];
201
}
202
}
203
204
return Object.keys(resultsUsedInThisTurn).length ? resultsUsedInThisTurn : undefined;
205
}
206
207
private _sendInitialChatReferences({ result: buildPromptResult }: IToolCallingBuiltPromptEvent) {
208
const [includedVariableReferences, ignoredVariableReferences] = [getUniqueReferences(buildPromptResult.references), getUniqueReferences(buildPromptResult.omittedReferences)].map((refs) => refs.reduce((acc, ref) => {
209
if ('variableName' in ref.anchor) {
210
acc.add(ref.anchor.variableName);
211
}
212
return acc;
213
}, new Set<string>()));
214
for (const reference of buildPromptResult.references) {
215
// Report variables which were partially sent to the model
216
const options = reference.options ?? ('variableName' in reference.anchor && ignoredVariableReferences.has(reference.anchor.variableName)
217
? { status: { kind: 2, description: l10n.t('Part of this attachment was not sent to the model due to context window limitations.') } }
218
: undefined);
219
if (!reference.options?.isFromTool) {
220
// References reported by a tool result will be shown in a separate list, don't need to be reported as references
221
this.stream.reference2(reference.anchor, undefined, options);
222
}
223
}
224
for (const omittedReference of buildPromptResult.omittedReferences) {
225
if ('variableName' in omittedReference.anchor && !includedVariableReferences.has(omittedReference.anchor.variableName)) {
226
this.stream.reference2(omittedReference.anchor, undefined, { status: { kind: 3, description: l10n.t('This attachment was not sent to the model due to context window limitations.') } });
227
}
228
}
229
}
230
231
private makeResponseStreamParticipants(intentInvocation: IIntentInvocation): ResponseStreamParticipant[] {
232
const participants: ResponseStreamParticipant[] = [];
233
234
// 1. Tracking of code blocks. Currently used in stests. todo@connor4312:
235
// can we simplify this so it's not used otherwise?
236
participants.push(stream => {
237
const codeBlockTrackingResponseStream = this._instantiationService.createInstance(CodeBlockTrackingChatResponseStream, stream, intentInvocation.codeblocksRepresentEdits);
238
return ChatResponseStreamImpl.spy(
239
codeBlockTrackingResponseStream,
240
v => v,
241
() => {
242
const codeBlocksMetaData = codeBlockTrackingResponseStream.finish();
243
this.turn.setMetadata(codeBlocksMetaData);
244
}
245
);
246
});
247
248
// 2. Track the survival of edits made in the editor
249
if (this.documentContext && this.location === ChatLocation.Editor) {
250
participants.push(stream => {
251
const firstTurnWithAIEditCollector = this.conversation.turns.find(turn => turn.getMetadata(CopilotInteractiveEditorResponse)?.editSurvivalTracker);
252
this._editSurvivalTracker = firstTurnWithAIEditCollector?.getMetadata(CopilotInteractiveEditorResponse)?.editSurvivalTracker ?? this._editSurvivalTrackerService.initialize(this.documentContext!.document.document);
253
return ChatResponseStreamImpl.spy(stream, value => {
254
if (value instanceof ChatResponseTextEditPart) {
255
this._editSurvivalTracker.collectAIEdits(value.edits);
256
}
257
});
258
});
259
}
260
261
262
// 3. Track the survival of other(?) interactions
263
// todo@connor4312: can these two streams be combined?
264
const interactionOutcomeComputer = new InteractionOutcomeComputer(this.documentContext?.document.uri);
265
participants.push(stream => interactionOutcomeComputer.spyOnStream(stream));
266
267
// 4. Linkify the stream unless told otherwise, or if this is a subagent request
268
if (!intentInvocation.linkification?.disable && !this.request.subAgentInvocationId) {
269
participants.push(stream => {
270
const linkStream = this._instantiationService.createInstance(ResponseStreamWithLinkification, { requestId: this.turn.id, references: this.turn.references }, stream, intentInvocation.linkification?.additionaLinkifiers ?? [], this.token);
271
return ChatResponseStreamImpl.spy(linkStream, p => p, () => {
272
this._loop.telemetry.markAddedLinks(linkStream.totalAddedLinkCount);
273
});
274
});
275
}
276
277
// 5. General telemetry on emitted components
278
participants.push(stream => ChatResponseStreamImpl.spy(stream, (part) => {
279
if (part instanceof ChatResponseMarkdownPart) {
280
this._loop.telemetry.markEmittedMarkdown(part.value);
281
}
282
if (part instanceof ChatResponseTextEditPart) {
283
this._loop.telemetry.markEmittedEdits(part.uri, part.edits);
284
}
285
}));
286
287
return participants;
288
}
289
290
private async _onDidReceiveResponse({ response, toolCalls, interactionOutcome }: IToolCallingResponseEvent) {
291
const responseMessage = (response.type === ChatFetchResponseType.Success ? response.value : '');
292
await this._loop.telemetry.sendTelemetry(response.requestId, response.type, responseMessage, interactionOutcome.interactionOutcome, toolCalls);
293
294
if (this.documentContext) {
295
this.turn.setMetadata(new CopilotInteractiveEditorResponse(
296
interactionOutcome.store,
297
{ ...this.documentContext, intent: this.intent, query: this.request.prompt },
298
this.chatTelemetryBuilder.telemetryMessageId,
299
this._loop.telemetry,
300
this._editSurvivalTracker,
301
));
302
303
const documentText = this.documentContext?.document.getText();
304
this.turn.setMetadata(new RequestDebugInformation(
305
this.documentContext.document.uri,
306
this.intent.id,
307
this.documentContext.document.languageId,
308
documentText!,
309
this.request.prompt,
310
this.documentContext.selection
311
));
312
}
313
}
314
315
private async runWithToolCalling(intentInvocation: IIntentInvocation): Promise<IInternalRequestResult> {
316
const store = new DisposableStore();
317
const loop = this._loop = store.add(this._instantiationService.createInstance(
318
DefaultToolCallingLoop,
319
{
320
conversation: this.conversation,
321
intent: this.intent,
322
invocation: intentInvocation,
323
toolCallLimit: this.handlerOptions.maxToolCallIterations,
324
onHitToolCallLimit: this.handlerOptions.confirmOnMaxToolIterations !== false
325
? ToolCallLimitBehavior.Confirm : ToolCallLimitBehavior.Stop,
326
request: this.request,
327
documentContext: this.documentContext,
328
streamParticipants: this.makeResponseStreamParticipants(intentInvocation),
329
temperature: this.handlerOptions.temperature ?? this.options.temperature,
330
location: this.location,
331
overrideRequestLocation: this.handlerOptions.overrideRequestLocation,
332
interactionContext: this.documentContext?.document.uri,
333
responseProcessor: typeof intentInvocation.processResponse === 'function' ? intentInvocation as IResponseProcessor : undefined,
334
yieldRequested: this.yieldRequested,
335
},
336
this.chatTelemetryBuilder,
337
));
338
339
store.add(Event.once(loop.onDidBuildPrompt)(this._sendInitialChatReferences, this));
340
341
// We need to wait for all response handlers to finish before
342
// we can dispose the store. This is because the telemetry machine
343
// still needs the tokenizers to count tokens. There was a case in vitests
344
// in which the store, and the tokenizers, were disposed before the telemetry
345
// machine could count the tokens, which resulted in an error.
346
// src/extension/prompt/node/chatParticipantTelemetry.ts#L521-L522
347
//
348
// cc @lramos15
349
const responseHandlers: Promise<unknown>[] = [];
350
store.add(loop.onDidReceiveResponse(res => {
351
const promise = this._onDidReceiveResponse(res);
352
responseHandlers.push(promise);
353
return promise;
354
}, this));
355
356
try {
357
// Execute start hooks first (SessionStart/SubagentStart), then UserPromptSubmit
358
await loop.runStartHooks(this.stream, this.token);
359
360
const userPromptSubmitResults = await this._chatHookService.executeHook('UserPromptSubmit', this.request.hooks, { prompt: this.request.prompt } satisfies UserPromptSubmitHookInput, this.conversation.sessionId, this.token);
361
const additionalContexts: string[] = [];
362
processHookResults({
363
hookType: 'UserPromptSubmit',
364
results: userPromptSubmitResults,
365
outputStream: this.stream,
366
logService: this._logService,
367
onSuccess: (output) => {
368
if (typeof output === 'object' && output !== null) {
369
const typedOutput = output as UserPromptSubmitHookOutput & { additionalContext?: string };
370
const additionalContext = typedOutput.hookSpecificOutput?.additionalContext ?? typedOutput.additionalContext;
371
if (additionalContext) {
372
additionalContexts.push(additionalContext);
373
}
374
// Check for block decision output
375
if (typedOutput.decision === 'block') {
376
const blockReason = typedOutput.reason || l10n.t('No reason provided');
377
this._logService.info(`[DefaultIntentRequestHandler] UserPromptSubmit hook block decision: ${blockReason}`);
378
this.stream.hookProgress('UserPromptSubmit', formatHookErrorMessage(blockReason));
379
throw new HookAbortError('UserPromptSubmit', blockReason);
380
}
381
}
382
},
383
});
384
385
if (additionalContexts.length > 0) {
386
loop.appendAdditionalHookContext(additionalContexts.join('\n'));
387
}
388
389
const result = await loop.run(this.stream, this.token);
390
if (!result.round.toolCalls.length || result.response.type !== ChatFetchResponseType.Success) {
391
loop.telemetry.sendToolCallingTelemetry(result.toolCallRounds, result.availableTools, this.token.isCancellationRequested ? 'cancelled' : result.response.type);
392
}
393
result.chatResult ??= {};
394
if ((result.chatResult.metadata as IResultMetadata)?.maxToolCallsExceeded) {
395
loop.telemetry.sendToolCallingTelemetry(result.toolCallRounds, result.availableTools, 'maxToolCalls');
396
}
397
398
// TODO need proper typing for all chat metadata and a better pattern to build it up from random places
399
result.chatResult = this.resultWithMetadatas(result.chatResult);
400
return { ...result, lastRequestTelemetry: loop.telemetry };
401
} finally {
402
await Promise.allSettled(responseHandlers);
403
store.dispose();
404
}
405
}
406
407
private resultWithMetadatas(chatResult: ChatResult | undefined): ChatResult | undefined {
408
const codeBlocks = this.turn.getMetadata(CodeBlocksMetadata);
409
const allSummarizedConversationHistory = this.turn.getAllMetadata(SummarizedConversationHistoryMetadata);
410
const renderedUserMessageMetadata = this.turn.getMetadata(RenderedUserMessageMetadata);
411
const globalContextMetadata = this.turn.getMetadata(GlobalContextMessageMetadata);
412
const anthropicTokenUsageMetadata = this.turn.getMetadata(AnthropicTokenUsageMetadata);
413
return codeBlocks || allSummarizedConversationHistory?.length || renderedUserMessageMetadata || globalContextMetadata || anthropicTokenUsageMetadata ?
414
{
415
...chatResult,
416
metadata: {
417
...chatResult?.metadata,
418
...codeBlocks,
419
...allSummarizedConversationHistory && allSummarizedConversationHistory.length > 0 && { summaries: allSummarizedConversationHistory },
420
...renderedUserMessageMetadata,
421
...globalContextMetadata,
422
...anthropicTokenUsageMetadata,
423
} satisfies Partial<IResultMetadata>,
424
} : chatResult;
425
}
426
427
private async handleConfirmationsIfNeeded(): Promise<ChatResult | undefined> {
428
const intentInvocation = this.turn.getMetadata(IntentInvocationMetadata)?.value;
429
assertType(intentInvocation);
430
if ((this.request.acceptedConfirmationData?.length || this.request.rejectedConfirmationData?.length) && intentInvocation.confirmationHandler) {
431
await intentInvocation.confirmationHandler(this.request.acceptedConfirmationData, this.request.rejectedConfirmationData, this.stream);
432
return {};
433
}
434
}
435
436
private async processSuccessfulFetchResult(appliedText: string, requestId: string, chatResult: ChatResult, baseModelTelemetry: ConversationalBaseTelemetryData, rounds: IToolCallRound[]): Promise<ChatResult> {
437
if (appliedText.length === 0 && !rounds.some(r => r.toolCalls.length)) {
438
const message = l10n.t('The model unexpectedly did not return a response. Request ID: {0}', requestId);
439
this.turn.setResponse(TurnStatus.Error, { type: 'meta', message }, baseModelTelemetry.properties.messageId, chatResult);
440
return {
441
errorDetails: {
442
message
443
},
444
};
445
}
446
447
this.turn.setResponse(TurnStatus.Success, { type: 'model', message: appliedText }, baseModelTelemetry.properties.messageId, chatResult);
448
baseModelTelemetry.markAsDisplayed();
449
sendModelMessageTelemetry(
450
this._telemetryService,
451
this.conversation,
452
this.location,
453
appliedText,
454
requestId,
455
this.documentContext?.document,
456
baseModelTelemetry,
457
this.getModeNameForTelemetry()
458
);
459
460
return chatResult;
461
}
462
463
private getModeNameForTelemetry(): string {
464
const modeInstructionsName = this.request.modeInstructions2?.name?.toLowerCase();
465
if (modeInstructionsName) {
466
return this.request.modeInstructions2?.isBuiltin ? this.request.modeInstructions2.name.toLowerCase() : 'custom';
467
}
468
469
if (this.intent.id === 'editAgent') {
470
return 'agent';
471
}
472
473
if (this.intent.id === 'edit') {
474
return 'edit';
475
}
476
477
return 'ask';
478
}
479
480
private processOffTopicFetchResult(baseModelTelemetry: ConversationalBaseTelemetryData): ChatResult {
481
// Create starting off topic telemetry and mark event as issued and displayed
482
this.stream.markdown(this.options.rejectionMessage);
483
this.turn.setResponse(TurnStatus.OffTopic, { message: this.options.rejectionMessage, type: 'offtopic-detection' }, baseModelTelemetry.properties.messageId, {});
484
return {};
485
}
486
487
private async processResult(fetchResult: ChatResponse, responseMessage: string, chatResult: ChatResult | void, metadataFragment: Partial<IResultMetadata>, baseModelTelemetry: ConversationalBaseTelemetryData, rounds: IToolCallRound[]): Promise<ChatResult> {
488
switch (fetchResult.type) {
489
case ChatFetchResponseType.Success:
490
return await this.processSuccessfulFetchResult(responseMessage, fetchResult.requestId, chatResult ?? {}, baseModelTelemetry, rounds);
491
case ChatFetchResponseType.OffTopic:
492
return this.processOffTopicFetchResult(baseModelTelemetry);
493
case ChatFetchResponseType.Canceled: {
494
const outageStatus = await this._octoKitService.getGitHubOutageStatus();
495
const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);
496
const chatResult = { errorDetails, metadata: metadataFragment };
497
this.turn.setResponse(TurnStatus.Cancelled, { message: errorDetails.message, type: 'user' }, baseModelTelemetry.properties.messageId, chatResult);
498
return chatResult;
499
}
500
case ChatFetchResponseType.QuotaExceeded:
501
case ChatFetchResponseType.RateLimited: {
502
const outageStatus = await this._octoKitService.getGitHubOutageStatus();
503
const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);
504
if (fetchResult.type === ChatFetchResponseType.RateLimited
505
&& fetchResult.capiError?.code?.startsWith('user_model_rate_limited')
506
&& !fetchResult.isAuto) {
507
if (this._configurationService.getConfig(ConfigKey.RateLimitAutoSwitchToAuto)) {
508
metadataFragment.shouldAutoSwitchToAuto = true;
509
} else {
510
errorDetails.confirmationButtons = [
511
{ data: { copilotSwitchToAutoOnRateLimit: true, alwaysSwitchToAuto: true } satisfies ISwitchToAutoOnRateLimitConfirmation, label: l10n.t('Switch to Auto (always)') },
512
{ data: { copilotSwitchToAutoOnRateLimit: true, alwaysSwitchToAuto: false } satisfies ISwitchToAutoOnRateLimitConfirmation, label: l10n.t('Switch to Auto') },
513
];
514
}
515
}
516
const chatResult = { errorDetails, metadata: metadataFragment };
517
this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult);
518
return chatResult;
519
}
520
case ChatFetchResponseType.BadRequest:
521
case ChatFetchResponseType.NetworkError:
522
case ChatFetchResponseType.Failed: {
523
const outageStatus = await this._octoKitService.getGitHubOutageStatus();
524
const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);
525
const chatResult = { errorDetails, metadata: metadataFragment };
526
this.turn.setResponse(TurnStatus.Error, { message: errorDetails.message, type: 'server' }, baseModelTelemetry.properties.messageId, chatResult);
527
return chatResult;
528
}
529
case ChatFetchResponseType.Filtered: {
530
const outageStatus = await this._octoKitService.getGitHubOutageStatus();
531
const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);
532
const chatResult = { errorDetails, metadata: { ...metadataFragment, filterReason: fetchResult.category } };
533
this.turn.setResponse(TurnStatus.Filtered, undefined, baseModelTelemetry.properties.messageId, chatResult);
534
return chatResult;
535
}
536
case ChatFetchResponseType.PromptFiltered: {
537
const outageStatus = await this._octoKitService.getGitHubOutageStatus();
538
const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);
539
const chatResult = { errorDetails, metadata: { ...metadataFragment, filterReason: FilterReason.Prompt } };
540
this.turn.setResponse(TurnStatus.PromptFiltered, undefined, baseModelTelemetry.properties.messageId, chatResult);
541
return chatResult;
542
}
543
case ChatFetchResponseType.AgentUnauthorized: {
544
const chatResult = {};
545
this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult);
546
return chatResult;
547
}
548
case ChatFetchResponseType.AgentFailedDependency: {
549
const outageStatus = await this._octoKitService.getGitHubOutageStatus();
550
const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);
551
const chatResult = { errorDetails, metadata: metadataFragment };
552
this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult);
553
return chatResult;
554
}
555
case ChatFetchResponseType.Length: {
556
const outageStatus = await this._octoKitService.getGitHubOutageStatus();
557
const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);
558
const chatResult = { errorDetails, metadata: metadataFragment };
559
this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult);
560
return chatResult;
561
}
562
case ChatFetchResponseType.NotFound: // before we had `NotFound`, it would fall into Unknown, so behavior should be consistent
563
case ChatFetchResponseType.Unknown: {
564
const outageStatus = await this._octoKitService.getGitHubOutageStatus();
565
const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);
566
const chatResult = { errorDetails, metadata: metadataFragment };
567
this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult);
568
return chatResult;
569
}
570
case ChatFetchResponseType.ExtensionBlocked: {
571
const outageStatus = await this._octoKitService.getGitHubOutageStatus();
572
const errorDetails = getErrorDetailsFromChatFetchError(fetchResult, (await this._authenticationService.getCopilotToken()).copilotPlan, outageStatus);
573
const chatResult = { errorDetails, metadata: metadataFragment };
574
// This shouldn't happen, only 3rd party extensions should be blocked
575
this.turn.setResponse(TurnStatus.Error, undefined, baseModelTelemetry.properties.messageId, chatResult);
576
return chatResult;
577
}
578
case ChatFetchResponseType.InvalidStatefulMarker:
579
throw new Error('unreachable'); // retried within the endpoint
580
}
581
}
582
}
583
584
interface IInternalRequestResult {
585
response: ChatResponse;
586
round: IToolCallRound;
587
chatResult?: ChatResult; // TODO should just be metadata
588
hadIgnoredFiles: boolean;
589
lastRequestMessages: Raw.ChatMessage[];
590
lastRequestTelemetry: ChatTelemetry;
591
}
592
593
interface IDefaultToolLoopOptions extends IToolCallingLoopOptions {
594
invocation: IIntentInvocation;
595
intent: IIntent;
596
documentContext: IDocumentContext | undefined;
597
location: ChatLocation;
598
temperature: number;
599
overrideRequestLocation?: ChatLocation;
600
}
601
602
class DefaultToolCallingLoop extends ToolCallingLoop<IDefaultToolLoopOptions> {
603
public telemetry!: ChatTelemetry;
604
private toolGrouping?: IToolGrouping;
605
606
constructor(
607
options: IDefaultToolLoopOptions,
608
telemetryBuilder: ChatTelemetryBuilder,
609
@IInstantiationService instantiationService: IInstantiationService,
610
@ILogService logService: ILogService,
611
@IRequestLogger requestLogger: IRequestLogger,
612
@IEndpointProvider endpointProvider: IEndpointProvider,
613
@IAuthenticationChatUpgradeService authenticationChatUpgradeService: IAuthenticationChatUpgradeService,
614
@ITelemetryService telemetryService: ITelemetryService,
615
@IExperimentationService experimentationService: IExperimentationService,
616
@IConfigurationService configurationService: IConfigurationService,
617
@IToolGroupingService private readonly toolGroupingService: IToolGroupingService,
618
@IChatHookService chatHookService: IChatHookService,
619
@ISessionTranscriptService sessionTranscriptService: ISessionTranscriptService,
620
@IFileSystemService fileSystemService: IFileSystemService,
621
@IOTelService otelService: IOTelService,
622
@IGitService gitService: IGitService,
623
) {
624
super(options, instantiationService, endpointProvider, logService, requestLogger, authenticationChatUpgradeService, telemetryService, configurationService, experimentationService, chatHookService, sessionTranscriptService, fileSystemService, otelService, gitService);
625
626
this._register(this.onDidBuildPrompt(({ result, tools, promptTokenLength, toolTokenCount }) => {
627
if (result.metadata.get(SummarizedConversationHistoryMetadata)) {
628
this.toolGrouping?.didInvalidateCache();
629
}
630
631
this.telemetry = telemetryBuilder.makeRequest(
632
options.intent!,
633
options.location,
634
options.conversation,
635
result.messages,
636
promptTokenLength,
637
result.references,
638
options.invocation.endpoint,
639
result.metadata.getAll(TelemetryData) ?? [],
640
tools.length,
641
toolTokenCount
642
);
643
}));
644
645
this._register(this.onDidReceiveResponse(() => {
646
this.toolGrouping?.didTakeTurn();
647
}));
648
}
649
650
protected override createPromptContext(availableTools: LanguageModelToolInformation[], outputStream: ChatResponseStream | undefined): Mutable<IBuildPromptContext> {
651
const context = super.createPromptContext(availableTools, outputStream);
652
this._handleVirtualCalls(context);
653
654
const extraVars = this.options.invocation.getAdditionalVariables?.(context);
655
if (extraVars?.hasVariables()) {
656
return {
657
...context,
658
chatVariables: ChatVariablesCollection.merge(context.chatVariables, extraVars),
659
};
660
}
661
662
return context;
663
}
664
665
private _handleVirtualCalls(context: Mutable<IBuildPromptContext>) {
666
if (!this.toolGrouping) {
667
return;
668
}
669
670
for (const call of context.toolCallRounds?.at(-1)?.toolCalls || Iterable.empty()) {
671
if (context.toolCallResults?.[call.id]) {
672
continue;
673
}
674
const expanded = this.toolGrouping.didCall(context.toolCallRounds!.length, call.name);
675
if (expanded) {
676
context.toolCallResults ??= {};
677
context.toolCallResults[call.id] = expanded;
678
}
679
}
680
}
681
682
protected override async buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress<ChatResponseReferencePart | ChatResponseProgressPart>, token: CancellationToken): Promise<IBuildPromptResult> {
683
const buildPromptResult = await this.options.invocation.buildPrompt(buildPromptContext, progress, token);
684
this.fixMessageNames(buildPromptResult.messages);
685
return buildPromptResult;
686
}
687
688
protected override async fetch(opts: ToolCallingLoopFetchOptions, token: CancellationToken): Promise<ChatResponse> {
689
const messageSourcePrefix = this.options.location === ChatLocation.Editor ? 'inline' : 'chat';
690
const debugName = this.options.request.subAgentInvocationId ?
691
`tool/runSubagent${this.options.request.subAgentName ? `-${this.options.request.subAgentName}` : ''}` :
692
`${ChatLocation.toStringShorter(this.options.location)}/${this.options.intent?.id}`;
693
const location = this.options.overrideRequestLocation ?? this.options.location;
694
const isThinkingLocation = location === ChatLocation.Agent || location === ChatLocation.MessagesProxy;
695
const rawEffort = this.options.request.modelConfiguration?.reasoningEffort;
696
const reasoningEffort = typeof rawEffort === 'string' ? rawEffort : undefined;
697
const isSubagent = !!this.options.request.subAgentInvocationId;
698
const modeChanged = this.didModeChangeSincePreviousRequest();
699
return this.options.invocation.endpoint.makeChatRequest2({
700
...opts,
701
modeChanged,
702
modelCapabilities: {
703
...opts.modelCapabilities,
704
enableThinking: isThinkingLocation && opts.modelCapabilities?.enableThinking,
705
reasoningEffort,
706
enableToolSearch: !isSubagent && !!this.options.invocation.endpoint.supportsToolSearch,
707
enableContextEditing: !isSubagent && isAnthropicContextEditingEnabled(this.options.invocation.endpoint, this._configurationService, this._experimentationService),
708
},
709
debugName,
710
conversationId: this.options.conversation.sessionId,
711
turnId: opts.turnId,
712
finishedCb: (text, index, delta) => {
713
this.telemetry.markReceivedToken();
714
return opts.finishedCb!(text, index, delta);
715
},
716
location,
717
requestOptions: {
718
...opts.requestOptions,
719
tools: normalizeToolSchema(
720
this.options.invocation.endpoint.family,
721
opts.requestOptions.tools,
722
(tool, rule) => {
723
this._logService.warn(`Tool ${tool} failed validation: ${rule}`);
724
},
725
),
726
temperature: this.calculateTemperature(),
727
},
728
telemetryProperties: {
729
messageId: this.telemetry.telemetryMessageId,
730
conversationId: this.options.conversation.sessionId,
731
messageSource: this.options.intent?.id && this.options.intent.id !== UnknownIntent.ID ? `${messageSourcePrefix}.${this.options.intent.id}` : `${messageSourcePrefix}.user`,
732
subType: this.options.request.subAgentInvocationId ? `subagent` : this.options.request.isSystemInitiated ? 'system-initiated' : undefined,
733
parentRequestId: this.options.request.parentRequestId,
734
},
735
requestKindOptions: this.options.request.subAgentInvocationId
736
? { kind: 'subagent' }
737
: undefined,
738
enableRetryOnFilter: true
739
}, token);
740
}
741
742
private didModeChangeSincePreviousRequest(): boolean {
743
if (this.options.invocation.endpoint.apiType !== 'responses') {
744
return false;
745
}
746
747
const previousTurn = this.options.conversation.turns.at(-2);
748
if (!previousTurn) {
749
return false;
750
}
751
752
// Once a mode-switched turn has successfully produced a fresh responses-api
753
// stateful marker, later requests in the same turn should resume from that
754
// new chain instead of continuing to invalidate previous_response_id.
755
// This is especially important for websocket follow-up requests after tool
756
// calls: keeping modeChanged=true for the entire turn would force the full
757
// pre-switch history back into every follow-up request, which can pull the
758
// model back toward the prior mode (for example implementation after
759
// switching into Plan mode).
760
if (this.currentToolCallRounds.some(round => !!round.statefulMarker)) {
761
return false;
762
}
763
764
const previousModeInstructions = previousTurn.modeInstructions;
765
if (!previousModeInstructions && !this.options.request.modeInstructions2) {
766
return false;
767
}
768
769
const modeChanged = !areModeInstructionsEqual(previousModeInstructions, this.options.request.modeInstructions2);
770
if (modeChanged) {
771
this._logService.trace('[DefaultIntentRequestHandler] Detected mode instructions changed between requests');
772
}
773
774
return modeChanged;
775
}
776
777
protected override async getAvailableTools(outputStream: ChatResponseStream | undefined, token: CancellationToken): Promise<LanguageModelToolInformation[]> {
778
const tools = await this.options.invocation.getAvailableTools?.() ?? [];
779
780
// Skip tool grouping when Anthropic tool search is enabled
781
if (isAnthropicFamily(this.options.invocation.endpoint) && this.options.invocation.endpoint.supportsToolSearch) {
782
return tools;
783
}
784
785
if (this.toolGrouping) {
786
this.toolGrouping.tools = tools;
787
} else {
788
this.toolGrouping = this.toolGroupingService.create(this.options.conversation.sessionId, tools);
789
for (const ref of this.options.request.toolReferences) {
790
this.toolGrouping.ensureExpanded(ref.name);
791
}
792
}
793
794
const computePromise = this.toolGrouping.compute(this.options.request.prompt, token); // Show progress if this takes a moment...
795
const timeout = setTimeout(() => {
796
outputStream?.progress(l10n.t('Optimizing tool selection...'), async () => {
797
await computePromise;
798
});
799
}, 1000);
800
801
try {
802
return await computePromise;
803
} finally {
804
clearTimeout(timeout);
805
}
806
}
807
808
private fixMessageNames(messages: Raw.ChatMessage[]): void {
809
messages.forEach(m => {
810
if (m.role !== Raw.ChatRole.System && 'name' in m && m.name === this.options.intent?.id) {
811
// Assistant messages from the current intent should not have 'name' set.
812
// It's not well-documented how this works in OpenAI models but this seems to be the expectation
813
m.name = undefined;
814
}
815
});
816
}
817
818
private calculateTemperature(): number {
819
if (this.options.request.attempt > 0) {
820
return Math.min(
821
this.options.temperature * (this.options.request.attempt + 1),
822
2 /* MAX temperature - https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature */
823
);
824
} else {
825
return this.options.temperature;
826
}
827
}
828
}
829
830
interface IInternalRequestResult extends IToolCallLoopResult {
831
lastRequestTelemetry: ChatTelemetry;
832
}
833
834
type ModeInstructions = NonNullable<ChatRequest['modeInstructions2']>;
835
type ModeInstructionMetadata = ModeInstructions['metadata'];
836
837
function areModeInstructionsEqual(a: ChatRequest['modeInstructions2'], b: ChatRequest['modeInstructions2']): boolean {
838
if (!a || !b) {
839
return a === b;
840
}
841
842
return a.uri?.toString() === b.uri?.toString()
843
&& a.name === b.name
844
&& a.content === b.content
845
&& a.isBuiltin === b.isBuiltin
846
&& serializeModeInstructionMetadata(a.metadata) === serializeModeInstructionMetadata(b.metadata);
847
}
848
849
function normalizeModeInstructionMetadata(metadata: ModeInstructionMetadata): Record<string, boolean | string | number> | undefined {
850
if (!metadata) {
851
return undefined;
852
}
853
854
const entries = Object.entries(metadata).sort(([left], [right]) => left.localeCompare(right));
855
if (entries.length === 0) {
856
return undefined;
857
}
858
859
return entries.reduce<Record<string, boolean | string | number>>((result, [key, value]) => {
860
result[key] = value;
861
return result;
862
}, {});
863
}
864
865
function serializeModeInstructionMetadata(metadata: ModeInstructionMetadata): string | undefined {
866
const normalizedMetadata = normalizeModeInstructionMetadata(metadata);
867
return normalizedMetadata ? JSON.stringify(normalizedMetadata) : undefined;
868
}
869
870