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