Skip to content
125 changes: 68 additions & 57 deletions src/extension/conversation/vscode-node/chatParticipants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import * as vscode from 'vscode';
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
import { IChatAgentService, defaultAgentName, editingSessionAgentEditorName, editingSessionAgentName, editsAgentName, getChatParticipantIdFromName, notebookEditorAgentName, terminalAgentName, vscodeAgentName } from '../../../platform/chat/common/chatAgents';
import { IChatQuotaService } from '../../../platform/chat/common/chatQuotaService';
import { IChatSessionService } from '../../../platform/chat/common/chatSessionService';
import { IInteractionService } from '../../../platform/chat/common/interactionService';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
import { ChatExtPerfMark, clearChatExtMarks, markChatExt } from '../../../util/common/performance';
import { DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';
import { autorun } from '../../../util/vs/base/common/observableInternal';
import { generateUuid } from '../../../util/vs/base/common/uuid';
Expand Down Expand Up @@ -69,7 +71,10 @@ class ChatAgents implements IDisposable {
@IExperimentationService private readonly experimentationService: IExperimentationService,
@IPromptCategorizerService private readonly promptCategorizerService: IPromptCategorizerService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
) { }
@IChatSessionService chatSessionService: IChatSessionService,
) {
this._disposables.add(chatSessionService.onDidDisposeChatSession(sessionId => clearChatExtMarks(sessionId)));
}

dispose() {
this._disposables.dispose();
Expand Down Expand Up @@ -198,68 +203,74 @@ Learn more about [GitHub Copilot](https://docs.github.com/copilot/using-github-c

private getChatParticipantHandler(id: string, name: string, defaultIntentIdOrGetter: IntentOrGetter): vscode.ChatExtendedRequestHandler {
return async (request, context, stream, token): Promise<vscode.ChatResult> => {
// If we need to switch to the base model, this function will handle it
// Otherwise it just returns the same request passed into it
request = await this.switchToBaseModel(request, stream);

// Handle switch-to-auto confirmation button clicks from rate limit errors
const switchToAutoConfirmation = getSwitchToAutoOnRateLimitConfirmation(request);
if (switchToAutoConfirmation) {
const action = switchToAutoConfirmation.alwaysSwitchToAuto ? 'switchToAutoAlways' : 'switchToAuto';
/* __GDPR__
"chatRateLimitAction" : {
"owner": "lramos15",
"comment": "Tracks which action users take when rate limited",
"action": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The action taken: switchToAuto, switchToAutoAlways, tryAgain, or autoSwitch." },
"modelId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model ID the user was rate limited on." }
}
*/
this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action, modelId: request.model?.id });
request = await this.switchToAutoModel(request, stream, switchToAutoConfirmation.alwaysSwitchToAuto);
} else if (isContinueOnError(request)) {
this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action: 'tryAgain', modelId: request.model?.id });
}
markChatExt(request.sessionId, ChatExtPerfMark.WillHandleParticipant);
try {
// If we need to switch to the base model, this function will handle it
// Otherwise it just returns the same request passed into it
request = await this.switchToBaseModel(request, stream);

// Handle switch-to-auto confirmation button clicks from rate limit errors
const switchToAutoConfirmation = getSwitchToAutoOnRateLimitConfirmation(request);
if (switchToAutoConfirmation) {
const action = switchToAutoConfirmation.alwaysSwitchToAuto ? 'switchToAutoAlways' : 'switchToAuto';
/* __GDPR__
"chatRateLimitAction" : {
"owner": "lramos15",
"comment": "Tracks which action users take when rate limited",
"action": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The action taken: switchToAuto, switchToAutoAlways, tryAgain, or autoSwitch." },
"modelId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model ID the user was rate limited on." }
}
*/
this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action, modelId: request.model?.id });
request = await this.switchToAutoModel(request, stream, switchToAutoConfirmation.alwaysSwitchToAuto);
} else if (isContinueOnError(request)) {
this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action: 'tryAgain', modelId: request.model?.id });
}

// The user is starting an interaction with the chat
if (!request.subAgentInvocationId) {
this.interactionService.startInteraction();
}
// The user is starting an interaction with the chat
if (!request.subAgentInvocationId) {
this.interactionService.startInteraction();
}

// Generate a shared telemetry message ID on the first turn only — subsequent turns have no
// categorization event to join and ChatTelemetryBuilder will generate its own ID.
const telemetryMessageId = context.history.length === 0 ? generateUuid() : undefined;
// Generate a shared telemetry message ID on the first turn only — subsequent turns have no
// categorization event to join and ChatTelemetryBuilder will generate its own ID.
const telemetryMessageId = context.history.length === 0 ? generateUuid() : undefined;

// Categorize the first prompt (fire-and-forget)
if (telemetryMessageId !== undefined) {
this.promptCategorizerService.categorizePrompt(request, context, telemetryMessageId);
}
// Categorize the first prompt (fire-and-forget)
if (telemetryMessageId !== undefined) {
this.promptCategorizerService.categorizePrompt(request, context, telemetryMessageId);
}

const defaultIntentId = typeof defaultIntentIdOrGetter === 'function' ?
defaultIntentIdOrGetter(request) :
defaultIntentIdOrGetter;

// empty chatAgentArgs will force InteractiveSession to not use a command or try to parse one out of the query
const commandsForAgent = agentsToCommands[defaultIntentId];
const intentId = request.command && commandsForAgent ?
commandsForAgent[request.command] :
defaultIntentId;

const handler = this.instantiationService.createInstance(ChatParticipantRequestHandler, context.history, request, stream, token, { agentName: name, agentId: id, intentId }, () => context.yieldRequested, telemetryMessageId);
let result = await handler.getResult();

// Auto-retry with Auto model when the setting is enabled and the handler signals it
if ((result as ICopilotChatResultIn).metadata?.shouldAutoSwitchToAuto) {
const previousModelId = request.model?.id;
const switchedRequest = await this.switchToAutoModel(request, stream, false);
if (switchedRequest.model?.id !== previousModelId) {
this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action: 'autoSwitch', modelId: previousModelId });
request = switchedRequest;
const retryHandler = this.instantiationService.createInstance(ChatParticipantRequestHandler, context.history, request, stream, token, { agentName: name, agentId: id, intentId }, () => context.yieldRequested, telemetryMessageId);
result = await retryHandler.getResult();
const defaultIntentId = typeof defaultIntentIdOrGetter === 'function' ?
defaultIntentIdOrGetter(request) :
defaultIntentIdOrGetter;

// empty chatAgentArgs will force InteractiveSession to not use a command or try to parse one out of the query
const commandsForAgent = agentsToCommands[defaultIntentId];
const intentId = request.command && commandsForAgent ?
commandsForAgent[request.command] :
defaultIntentId;

const handler = this.instantiationService.createInstance(ChatParticipantRequestHandler, context.history, request, stream, token, { agentName: name, agentId: id, intentId }, () => context.yieldRequested, telemetryMessageId);
let result = await handler.getResult();

// Auto-retry with Auto model when the setting is enabled and the handler signals it
if ((result as ICopilotChatResultIn).metadata?.shouldAutoSwitchToAuto) {
const previousModelId = request.model?.id;
const switchedRequest = await this.switchToAutoModel(request, stream, false);
if (switchedRequest.model?.id !== previousModelId) {
this.telemetryService.sendMSFTTelemetryEvent('chatRateLimitAction', { action: 'autoSwitch', modelId: previousModelId });
request = switchedRequest;
const retryHandler = this.instantiationService.createInstance(ChatParticipantRequestHandler, context.history, request, stream, token, { agentName: name, agentId: id, intentId }, () => context.yieldRequested, telemetryMessageId);
result = await retryHandler.getResult();
}
}
}

return result;
return result;
} finally {
markChatExt(request.sessionId, ChatExtPerfMark.DidHandleParticipant);
clearChatExtMarks(request.sessionId);
}
};
}

Expand Down
3 changes: 3 additions & 0 deletions src/extension/conversation/vscode-node/conversationFeature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { IGitCommitMessageService } from '../../../platform/git/common/gitCommit
import { ILogService } from '../../../platform/log/common/logService';
import { ISettingsEditorSearchService } from '../../../platform/settingsEditor/common/settingsEditorSearchService';
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
import { ChatExtGlobalPerfMark, markChatExtGlobal } from '../../../util/common/performance';
import { isUri } from '../../../util/common/types';
import { DeferredPromise } from '../../../util/vs/base/common/async';
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
Expand Down Expand Up @@ -94,13 +95,15 @@ export class ConversationFeature implements IExtensionContribution {
this.activated = true;
activationBlockerDeferred.complete();
} else {
markChatExtGlobal(ChatExtGlobalPerfMark.WillWaitForCopilotToken);
this.logService.info(`ConversationFeature: Waiting for copilot token to activate conversation feature`);
}

this._disposables.add(authenticationService.onDidAuthenticationChange(async () => {
const hasSession = !!authenticationService.copilotToken;
this.logService.info(`ConversationFeature: onDidAuthenticationChange has token: ${hasSession}`);
if (hasSession) {
markChatExtGlobal(ChatExtGlobalPerfMark.DidWaitForCopilotToken);
this.activated = true;
} else {
this.activated = false;
Expand Down
6 changes: 5 additions & 1 deletion src/extension/extension/vscode/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { isScenarioAutomation } from '../../../platform/env/common/envService';
import { isProduction } from '../../../platform/env/common/packagejson';
import { IIgnoreService } from '../../../platform/ignore/common/ignoreService';
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
import { ChatExtGlobalPerfMark, markChatExtGlobal } from '../../../util/common/performance';
import { IInstantiationServiceBuilder, InstantiationServiceBuilder } from '../../../util/common/services';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { CopilotExtensionApi } from '../../api/vscode/extensionApi';
Expand All @@ -31,6 +32,7 @@ export interface IExtensionActivationConfiguration {
}

export async function baseActivate(configuration: IExtensionActivationConfiguration) {
markChatExtGlobal(ChatExtGlobalPerfMark.WillActivate);
const context = configuration.context;
if (context.extensionMode === ExtensionMode.Test && !configuration.forceActivation && !isScenarioAutomation) {
// FIXME Running in tests, don't activate the extension
Expand Down Expand Up @@ -78,7 +80,7 @@ export async function baseActivate(configuration: IExtensionActivationConfigurat
return instantiationService; // The returned accessor is used in tests
}

return {
const result = {
getAPI(version: number) {
if (version > CopilotExtensionApi.version) {
throw new Error('Invalid Copilot Chat extension API version. Please upgrade Copilot Chat.');
Expand All @@ -87,6 +89,8 @@ export async function baseActivate(configuration: IExtensionActivationConfigurat
return instantiationService.createInstance(CopilotExtensionApi);
}
};
markChatExtGlobal(ChatExtGlobalPerfMark.DidActivate);
return result;
}

export function createInstantiationService(configuration: IExtensionActivationConfiguration): IInstantiationService {
Expand Down
11 changes: 10 additions & 1 deletion src/extension/intents/node/toolCallingLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { IExperimentationService } from '../../../platform/telemetry/common/null
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
import { computePromptTokenDetails } from '../../../platform/tokenizer/node/promptTokenDetails';
import { tryFinalizeResponseStream } from '../../../util/common/chatResponseStreamImpl';
import { ChatExtPerfMark, markChatExt } from '../../../util/common/performance';
import { DeferredPromise, timeout } from '../../../util/vs/base/common/async';
import { CancellationError, isCancellationError } from '../../../util/vs/base/common/errors';
import { Emitter } from '../../../util/vs/base/common/event';
Expand Down Expand Up @@ -1094,7 +1095,13 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
let availableTools = await this.getAvailableTools(outputStream, token);
const context = this.createPromptContext(availableTools, outputStream);
const isContinuation = context.isContinuation || false;
const buildPromptResult: IBuildPromptResult = await this.buildPrompt2(context, outputStream, token);
markChatExt(this.options.conversation.sessionId, ChatExtPerfMark.WillBuildPrompt);
let buildPromptResult: IBuildPromptResult;
try {
buildPromptResult = await this.buildPrompt2(context, outputStream, token);
} finally {
markChatExt(this.options.conversation.sessionId, ChatExtPerfMark.DidBuildPrompt);
}
this.throwIfCancelled(token);
this.turn.addReferences(buildPromptResult.references);
// Possible the tool call resulted in new tools getting added.
Expand Down Expand Up @@ -1204,6 +1211,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
const enableThinking = !shouldDisableThinking;
let phase: string | undefined;
let compaction: OpenAIContextManagementResponse | undefined;
markChatExt(this.options.conversation.sessionId, ChatExtPerfMark.WillFetch);
const fetchResult = await this.fetch({
messages: this.applyMessagePostProcessing(effectiveBuildPromptResult.messages, { stripOrphanedToolCalls: isGeminiFamily(endpoint) }),
turnId: this.turn.id,
Expand Down Expand Up @@ -1254,6 +1262,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
}, token).finally(() => {
this.stopHookUserInitiated = false;
});
markChatExt(this.options.conversation.sessionId, ChatExtPerfMark.DidFetch);

const promptTokenDetails = await computePromptTokenDetails({
messages: effectiveBuildPromptResult.messages,
Expand Down
4 changes: 3 additions & 1 deletion src/extension/prompts/node/agent/agentPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,11 @@ export class AgentPrompt extends PromptElement<AgentPromptProps> {
const isNewChat = this.props.promptContext.history?.length === 0;
// TODO:@bhavyau find a better way to extract session resource
const sessionResource = (this.props.promptContext.tools?.toolInvocationToken as any)?.sessionResource as string | undefined;
return globalContext ?
const result = globalContext ?
renderedMessageToTsxChildren(globalContext, !!this.props.enableCacheBreakpoints) :
<GlobalAgentContext enableCacheBreakpoints={!!this.props.enableCacheBreakpoints} availableTools={this.props.promptContext.tools?.availableTools} isNewChat={isNewChat} sessionResource={sessionResource} />;

return result;
}

private async getOrCreateGlobalAgentContextContent(endpoint: IChatEndpoint): Promise<Raw.ChatCompletionContentPart[] | undefined> {
Expand Down
Loading
Loading