Path: blob/main/extensions/copilot/src/extension/conversation/vscode-node/remoteAgents.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*--------------------------------------------------------------------------------------------*/4import { RequestType } from '@vscode/copilot-api';5import { Raw } from '@vscode/prompt-tsx';6import { ChatCompletionItem, ChatContext, ChatPromptReference, ChatRequest, ChatRequestTurn, ChatResponseMarkdownPart, ChatResponseReferencePart, ChatResponseTurn, ChatResponseWarningPart, ChatVariableLevel, Disposable, DynamicChatParticipantProps, Location, MarkdownString, Position, Progress, Range, TextDocument, TextEditor, ThemeIcon, chat, commands, l10n } from 'vscode';7import { IAuthenticationService } from '../../../platform/authentication/common/authentication';8import { IAuthenticationChatUpgradeService } from '../../../platform/authentication/common/authenticationUpgrade';9import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes';10import { ICAPIClientService, } from '../../../platform/endpoint/common/capiClient';11import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';12import { RemoteAgentChatEndpoint } from '../../../platform/endpoint/node/chatEndpoint';13import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';14import { IGitService, getGitHubRepoInfoFromContext, toGithubNwo } from '../../../platform/git/common/gitService';15import { IGithubRepositoryService } from '../../../platform/github/common/githubService';16import { HAS_IGNORED_FILES_MESSAGE, IIgnoreService } from '../../../platform/ignore/common/ignoreService';17import { ILogService } from '../../../platform/log/common/logService';18import { ICopilotReference } from '../../../platform/networking/common/fetch';19import { Response } from '../../../platform/networking/common/fetcherService';20import { ITabsAndEditorsService } from '../../../platform/tabs/common/tabsAndEditorsService';21import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';22import { DeferredPromise } from '../../../util/vs/base/common/async';23import { DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';24import * as path from '../../../util/vs/base/common/path';25import { URI } from '../../../util/vs/base/common/uri';26import { generateUuid } from '../../../util/vs/base/common/uuid';27import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';28import { ChatResponseReferencePart2, Uri } from '../../../vscodeTypes';29import { ICopilotChatResult, ICopilotChatResultIn } from '../../prompt/common/conversation';30import { IPromptVariablesService } from '../../prompt/node/promptVariablesService';31import { IUserFeedbackService } from './userActions';3233interface IAgent {34name: string;35avatar_url: string;36owner_login: string;37owner_avatar_url: string;38description: string;39slug: string;40editor_context: boolean;41}4243interface IAgentsResponse {44agents: IAgent[];45}4647const agentRegistrations = new Map<string, Disposable>();4849const GITHUB_PLATFORM_AGENT_NAME = 'github';50const GITHUB_PLATFORM_AGENT_ID = 'platform';51const GITHUB_PLATFORM_AGENT_SKILLS: { [key: string]: string } = {52web: 'bing-search',53};5455type IPlatformReference = IFileReference | ISelectionReference | IGitHubRepositoryReference;5657interface IFileReference {58type: 'client.file';59data: {60language: string;61content: string;62};63is_implicit: boolean;64id: string;65}6667interface ISelectionReference {68type: 'client.selection';69data: {70start: { line: number; col: number };71end: { line: number; col: number };72content: string;73};74is_implicit: boolean;75id: string;76}7778interface IGitHubRepositoryReference {79type: 'github.repository';80data: {81type: 'repository';82name: string; // name of the repository83ownerLogin: string; // owner of the repository84id: number;85};86id: string; // e.g. "microsoft/vscode"87}8889export class RemoteAgentContribution implements IDisposable {90private readonly disposables = new DisposableStore();91private refreshRemoteAgentsP: Promise<void> | undefined;92private enabledSkillsPromise: Promise<Set<string>> | undefined;9394constructor(95@ILogService private readonly logService: ILogService,96@IEndpointProvider private readonly endpointProvider: IEndpointProvider,97@ICAPIClientService private readonly capiClientService: ICAPIClientService,98@IPromptVariablesService private readonly promptVariablesService: IPromptVariablesService,99@IWorkspaceService private readonly workspaceService: IWorkspaceService,100@ITabsAndEditorsService private readonly tabsAndEditorsService: ITabsAndEditorsService,101@IIgnoreService private readonly ignoreService: IIgnoreService,102@IGitService private readonly gitService: IGitService,103@IGithubRepositoryService private readonly githubRepositoryService: IGithubRepositoryService,104@IVSCodeExtensionContext private readonly vscodeExtensionContext: IVSCodeExtensionContext,105@IAuthenticationService private readonly authenticationService: IAuthenticationService,106@IUserFeedbackService private readonly userFeedbackService: IUserFeedbackService,107@IInstantiationService private readonly instantiationService: IInstantiationService,108@IAuthenticationChatUpgradeService private readonly authenticationChatUpgradeService: IAuthenticationChatUpgradeService,109) {110this.disposables.add(new Disposable(() => agentRegistrations.forEach(agent => agent.dispose())));111112this.refreshRemoteAgents();113// Refresh remote agents whenever auth changes, e.g. in case the user was initially not signed in114this.disposables.add(this.authenticationService.onDidAccessTokenChange(() => {115this.refreshRemoteAgents();116}));117}118119dispose() {120this.disposables.dispose();121}122123private async refreshRemoteAgents(): Promise<void> {124if (!this.refreshRemoteAgentsP) {125this.refreshRemoteAgentsP = this._doRefreshRemoteAgents();126}127128return this.refreshRemoteAgentsP.finally(() => this.refreshRemoteAgentsP = undefined);129}130131private async _doRefreshRemoteAgents(): Promise<void> {132const existingAgents = new Set(agentRegistrations.keys());133134try {135const authToken = this.authenticationService.anyGitHubSession?.accessToken;136if (!authToken) {137// We have to silently wait for auth to become available so we can fetch remote agents138this.logService.warn('Unable to fetch remote agents because user is not signed in.');139return;140}141try {142// First try to register the default platform agent143if (!existingAgents.delete(GITHUB_PLATFORM_AGENT_ID)) { // Don't reregister it144this.logService.info('Registering default platform agent...');145agentRegistrations.set(GITHUB_PLATFORM_AGENT_ID, this.registerAgent(null));146}147} catch (ex) {148this.logService.info(`Encountered error while registering platform agent: ${JSON.stringify(ex)}`);149}150const response = await this.capiClientService.makeRequest<Response>({151method: 'GET',152headers: {153Authorization: `Bearer ${authToken}`154}155}, { type: RequestType.RemoteAgent });156const text = await response.text();157let newAgents: IAgent[];158try {159newAgents = (<IAgentsResponse>JSON.parse(text)).agents;160if (!Array.isArray(newAgents)) {161throw new Error(`Expected 'agents' to be an array`);162}163} catch (e) {164if (!text.includes('access denied')) {165this.logService.warn(`Invalid remote agent response: ${text} (${e})`);166}167return;168}169170for (const agent of newAgents) {171if (!existingAgents.delete(agent.slug)) {172// only register if we haven't seen them yet173agentRegistrations.set(agent.slug, this.registerAgent(agent));174}175}176} catch (e) {177this.logService.error(e, 'Failed to load remote copilot agents');178}179180for (const item of existingAgents) {181agentRegistrations.get(item)!.dispose();182agentRegistrations.delete(item);183}184}185186private checkAuthorized(agent: string) {187if (agent === GITHUB_PLATFORM_AGENT_NAME) {188return true;189}190const key = `copilot.agent.${agent}.authorized`;191return this.vscodeExtensionContext.globalState.get<boolean>(key, false) || this.vscodeExtensionContext.workspaceState.get<boolean>(key, false);192}193194private async setAuthorized(agent: string, isGlobal = false) {195const memento = isGlobal ? this.vscodeExtensionContext.globalState : this.vscodeExtensionContext.workspaceState;196await memento.update(`copilot.agent.${agent}.authorized`, true);197}198199private registerAgent(agentData: IAgent | null): Disposable {200const store = new DisposableStore();201const participantId = `github.copilot-dynamic.${agentData?.slug ?? GITHUB_PLATFORM_AGENT_ID}`;202const slug = agentData?.slug ?? GITHUB_PLATFORM_AGENT_NAME;203const description = agentData?.description ?? l10n.t("Get answers grounded in web search and code search");204const dynamicProps: DynamicChatParticipantProps = {205name: slug,206description,207publisherName: agentData?.owner_login ?? 'GitHub',208fullName: agentData?.name ?? 'GitHub',209};210let hasShownImplicitContextAuthorizationForSession = false;211const agent = store.add(chat.createDynamicChatParticipant(participantId, dynamicProps, async (request, context, responseStream, token): Promise<ICopilotChatResult> => {212const sessionId = getOrCreateSessionId(context);213const responseId = generateUuid();214// This isn't used anywhere but is needed to fix the IChatResult shape which the remote agents follow215const modelMessageId = generateUuid();216const metadata: ICopilotChatResult['metadata'] & Record<string, unknown> = {217sessionId,218modelMessageId,219responseId,220agentId: participantId,221command: request.command,222};223224let accessToken: string | undefined;225if (request.acceptedConfirmationData) {226for (const data of request.acceptedConfirmationData) {227if (data?.url) {228// Store that the user has authorized the agent229await this.setAuthorized(slug, request.prompt.startsWith(l10n.t('Authorize for all workspaces')));230await commands.executeCommand('vscode.open', Uri.parse(data.url));231responseStream.markdown(l10n.t('Please complete authorization in your browser and resend your question.'));232return { metadata } satisfies ICopilotChatResult;233} else if (data?.hasAcknowledgedImplicitReferences) {234// Store that the user has acknowledged implicit references235await this.setAuthorized(slug, request.prompt.startsWith(l10n.t('Allow for All Workspaces')));236responseStream.markdown(l10n.t('Your preference has been saved.'));237return { metadata } satisfies ICopilotChatResult;238// This property is set by the confirmation in the Upgrade service239} else if (data?.authPermissionPrompted) {240request = await this.authenticationChatUpgradeService.handleConfirmationRequest(responseStream, request, context.history);241metadata.command = request.command;242accessToken = (await this.authenticationService.getGitHubSession('permissive', { silent: true }))?.accessToken;243if (!accessToken) {244responseStream.markdown(l10n.t('The additional permissions are required for this feature.'));245return { metadata } satisfies ICopilotChatResult;246}247}248}249}250251// Slugless means it's the platform agent252if (!agentData?.slug) {253accessToken = this.authenticationService.permissiveGitHubSession?.accessToken;254if (!accessToken) {255if (this.authenticationService.isMinimalMode) {256responseStream.markdown(l10n.t('Minimal mode is enabled. You will need to change `github.copilot.advanced.authPermissions` to `default` to use this feature.'));257responseStream.button({258title: l10n.t('Open Settings (JSON)'),259command: 'workbench.action.openSettingsJson',260arguments: [{ revealSetting: { key: 'github.copilot.advanced.authPermissions' } }]261});262} else {263// Otherwise, show the permissive session upgrade prompt because it's required264this.authenticationChatUpgradeService.showPermissiveSessionUpgradeInChat(265responseStream,266request,267l10n.t('`@github` requires access to your repositories on GitHub for handling requests.')268);269}270return { metadata } satisfies ICopilotChatResult;271}272}273274// Use the basic access token as a fallback275if (!accessToken) {276accessToken = this.authenticationService.anyGitHubSession?.accessToken;277}278279try {280const selectedEndpoint = await this.endpointProvider.getChatEndpoint(request);281// Converts the selected endpoint to a remote agent endpoint so we can request the model the user selected to the agent282const endpoint = this.instantiationService.createInstance(RemoteAgentChatEndpoint, {283model_picker_enabled: false,284is_chat_default: false,285vendor: selectedEndpoint.modelProvider,286billing: selectedEndpoint.isPremium !== undefined || selectedEndpoint.multiplier !== undefined ? { is_premium: selectedEndpoint.isPremium, multiplier: selectedEndpoint.multiplier, restricted_to: selectedEndpoint.restrictedToSkus } : undefined,287is_chat_fallback: false,288capabilities: {289supports: { tool_calls: selectedEndpoint.supportsToolCalls, vision: selectedEndpoint.supportsVision, streaming: true },290type: 'chat',291tokenizer: selectedEndpoint.tokenizer,292family: selectedEndpoint.family,293},294id: selectedEndpoint.model,295name: selectedEndpoint.name,296version: selectedEndpoint.version,297}, agentData ? { type: RequestType.RemoteAgentChat, slug: agentData.slug } : { type: RequestType.RemoteAgentChat });298299// This flattens the docs agent's variables and ignores other variable values for now300const resolved = await this.promptVariablesService.resolveVariablesInPrompt(request.prompt, request.references);301302// Collect copilot skills and references to be sent in the request303const copilotReferences = [];304const { copilot_skills } = await this.resolveCopilotSkills(slug, request);305306let hasIgnoredFiles = false;307try {308const result = await this.prepareClientPlatformReferences([...request.references], slug);309hasIgnoredFiles = result.hasIgnoredFiles;310311if (result.clientReferences) {312copilotReferences.push(...result.clientReferences);313}314for (const ref of result.vscodeReferences) {315responseStream.reference(ref);316}317} catch (ex) {318if (ex instanceof Error && ex.message.includes('File seems to be binary and cannot be opened as text')) {319responseStream.markdown(l10n.t("Sorry, binary files are not currently supported."));320return { metadata } satisfies ICopilotChatResult;321} else {322return {323errorDetails: { message: (ex.message) },324metadata325};326}327}328329// Note: the platform agent will deal with token counting for us330const reportedReferences = new Map<string, ICopilotReference>();331const agentReferences: ICopilotReference[] = [];332const confirmations = prepareConfirmations(request);333let reportedProgress: Progress<ChatResponseWarningPart | ChatResponseReferencePart2> | undefined = undefined;334let pendingProgress: { resolvedMessage: string; deferred: DeferredPromise<string> } | undefined;335let hadCopilotErrorsOrConfirmations = false;336337const response = await endpoint.makeChatRequest(338'remoteAgent',339[340...prepareRemoteAgentHistory(participantId, context),341{342role: Raw.ChatRole.User,343content: (request.acceptedConfirmationData?.length || request.rejectedConfirmationData?.length)344? []345: [{ type: Raw.ChatCompletionContentPartKind.Text, text: resolved.message }],346...(copilotReferences.length ? { copilot_references: copilotReferences } : undefined),347...(confirmations?.length ? { copilot_confirmations: confirmations } : undefined),348}349],350async (result, _, delta) => {351if (delta.copilotReferences) {352353const processReference = (reference: ICopilotReference, parentReference?: ICopilotReference) => {354const url = 'url' in reference ? reference.url : 'url' in reference.data ? reference.data.url : 'html_url' in reference.data ? reference.data.html_url : undefined;355if (url && typeof url === 'string') {356if (!reportedReferences.has(url)) {357let icon: ChatResponseReferencePart['iconPath'] = undefined;358const parsed = new URL(url);359if (parsed.hostname === 'github.com') {360icon = new ThemeIcon('github');361} else {362icon = new ThemeIcon('globe');363}364if (reportedProgress) {365reportedProgress?.report(new ChatResponseReferencePart(Uri.parse(url), icon));366} else {367responseStream.reference(Uri.parse(url), icon);368}369370// Keep track of the parent reference and not the individual URL used, as this will be sent again in history371reportedReferences.set(url, parentReference ?? reference);372}373} else if (reference.metadata) {374const icon = reference.metadata.display_icon ? Uri.parse(reference.metadata.display_icon) : new ThemeIcon('globe');375const value = reference.metadata.display_url ? Uri.parse(reference.metadata.display_url) : reference.metadata.display_name;376if (reportedProgress) {377reportedProgress.report(new ChatResponseReferencePart2(value, icon));378} else {379responseStream.reference2(value, icon);380}381reportedReferences.set(reference.metadata.display_url ?? reference.metadata.display_name, parentReference ?? reference);382}383};384385// Report web references386for (const reference of delta.copilotReferences) {387if (Array.isArray(reference.data.results)) {388reference.data.results.forEach((r) => {389processReference(r, reference);390});391} else if (reference.data.type === 'github.agent') {392agentReferences.push(reference);393} else if (reference.type === 'github.text') {394continue;395} else if ('html_url' in reference.data || 'url' in reference.data && typeof reference.data.url === 'string' || reference.metadata) {396processReference(reference);397}398}399}400401const reportProgress = (progress: Progress<ChatResponseWarningPart | ChatResponseReferencePart>, resolvedMessage: string) => {402pendingProgress?.deferred.complete(pendingProgress.resolvedMessage);403reportedProgress = progress;404const deferred = new DeferredPromise<string>();405pendingProgress = { deferred, resolvedMessage };406return deferred.p;407};408409if (delta._deprecatedCopilotFunctionCalls) {410for (const call of delta._deprecatedCopilotFunctionCalls) {411switch (call.name) {412case 'bing-search': {413try {414const data: { query: string } = JSON.parse(call.arguments);415responseStream.progress(l10n.t('Searching Bing for "{0}"...', data.query), async (progress) => reportProgress(progress, l10n.t('Bing search results for "{0}"', data.query)));416} catch (ex) { }417break;418}419case 'codesearch': {420try {421const data: { query: string; scopingQuery: string } = JSON.parse(call.arguments);422responseStream.progress(l10n.t('Searching {0} for "{1}"...', data.scopingQuery, data.query), async (progress) =>423reportProgress(progress, l10n.t('Code search results for "{0}" in {1}', data.query, data.scopingQuery)));424} catch (ex) { }425break;426}427}428}429}430431if (delta.copilotErrors && typeof responseStream.warning === 'function') {432hadCopilotErrorsOrConfirmations = true;433for (const error of delta.copilotErrors) {434if (reportedProgress) {435reportedProgress?.report(new ChatResponseWarningPart(error.message));436} else {437responseStream.warning(error.message);438}439}440}441442if (delta.copilotConfirmation) {443hadCopilotErrorsOrConfirmations = true;444const confirm = delta.copilotConfirmation;445responseStream.confirmation(confirm.title, confirm.message, confirm.confirmation);446}447448if (delta.text) {449pendingProgress?.deferred.complete(pendingProgress.resolvedMessage);450const md = new MarkdownString(delta.text);451md.supportHtml = true;452responseStream.markdown(md);453}454return undefined;455},456token,457ChatLocation.Panel,458undefined,459{460secretKey: accessToken,461copilot_thread_id: sessionId,462...(copilot_skills ? { copilot_skills } : undefined)463},464true,465{466messageSource: `serverAgent.${agentData?.slug ?? GITHUB_PLATFORM_AGENT_ID}`,467}468);469470metadata['copilot_references'] = [...new Set(reportedReferences.values()).values(), ...agentReferences];471if (response.type === ChatFetchResponseType.Success && hasIgnoredFiles) {472responseStream.markdown(HAS_IGNORED_FILES_MESSAGE);473}474475if (response.type !== ChatFetchResponseType.Success) {476this.logService.warn(`Bad response from remote agent "${slug}": ${response.type} ${response.reason}`);477if (response.reason.includes('400 no docs found')) {478return {479errorDetails: { message: 'No docs found' },480metadata481};482} else if (response.type === ChatFetchResponseType.AgentUnauthorized) {483const url = new URL(response.authorizationUrl);484const editorContext = agentData?.editor_context ? l10n.t('**@{0}** will read your active file and selection.', slug) : '';485responseStream.confirmation(486l10n.t('Authorize agent'),487editorContext + '\n' +488l10n.t({489message: 'Please authorize usage of **@{0}** on {1} and resend your question. [Learn more]({2}).',490args: [slug, url.hostname, 'https://aka.ms/vscode-github-chat-extension-editor-context'],491comment: [`{Locked=']({'}`]492}),493{ url: response.authorizationUrl },494[l10n.t("Authorize"), l10n.t('Authorize for All Workspaces')]495);496return { metadata, nextQuestion: { prompt: request.prompt, participant: participantId, command: request.command } } satisfies ICopilotChatResult;497} else if (response.type === ChatFetchResponseType.AgentFailedDependency) {498return {499errorDetails: { message: l10n.t('Sorry, an error occurred: {0}', response.reason) },500metadata501};502} else if (response.type !== ChatFetchResponseType.Unknown || !hadCopilotErrorsOrConfirmations) {503return {504errorDetails: { message: response.reason },505metadata506};507}508}509510// Ask the user to authorize implicit context511if (!this.checkAuthorized(slug) && agentData?.editor_context && !hasShownImplicitContextAuthorizationForSession) {512responseStream.confirmation(513l10n.t('Grant access to editor context'),514l10n.t({515message: '**@{0}** would like to read your active file and selection. [Learn More]({1})',516args: [slug, 'https://aka.ms/vscode-github-chat-extension-editor-context'],517comment: [`{Locked=']({'}`]518}),519{ hasAcknowledgedImplicitReferences: true },520[l10n.t("Allow"), l10n.t("Allow for All Workspaces")]521);522hasShownImplicitContextAuthorizationForSession = true;523}524525return { metadata } satisfies ICopilotChatResult;526} catch (e) {527this.logService.error(`/agents/${slug} failed: ${e}`);528return { metadata };529}530}));531agent.iconPath = agentData ? Uri.parse(agentData.avatar_url) : new ThemeIcon('github');532533if (slug === GITHUB_PLATFORM_AGENT_NAME) {534agent.participantVariableProvider = {535triggerCharacters: ['#'],536provider: {537provideCompletionItems: async (query, token) => {538const items = await this.getPlatformAgentSkills();539return items.map<ChatCompletionItem>(i => {540const item = new ChatCompletionItem(`copilot.${i.name}`, '#' + i.name, [{ value: i.insertText, level: ChatVariableLevel.Full, description: i.description }]);541item.command = i.command;542item.detail = i.description;543return item;544});545},546}547};548}549550store.add(551agent.onDidReceiveFeedback(e => this.userFeedbackService.handleFeedback(e, participantId)));552553return store;554}555556private async prepareClientPlatformReferences(variables: ChatPromptReference[], slug: string) {557const clientReferences: IPlatformReference[] = [];558const vscodeReferences: ({559variableName: string;560value?: Uri | Location | undefined;561} | Location | Uri)[] = [];562let hasIgnoredFiles = false;563let hasSentImplicitSelectionReference = false;564565const redactFileContents = async (document: TextDocument, range?: Range) => {566const filename = path.basename(document.uri.toString());567let content = document.getText(range);568if (await this.ignoreService.isCopilotIgnored(document.uri)) {569hasIgnoredFiles = true;570content = 'content-exclusion';571} else if (filename.startsWith('.')) {572content = 'hidden-file'; // e.g. .env573} else if (Buffer.byteLength(content, 'utf8') > 1024 ** 3) {574content = 'file-too-large'; // exceeds 1GB575}576return content;577};578579const getImplicitContextId = async (uri: Uri) => {580// The ID of the file should be relative to the root of the repository if we're in a repository581// falling back to a workspace folder-relative path if we're not in a repository582// and finally falling back to the file basename e.g. if it's an untracked file that doesn't belong to the open workspace or repo583const repository = await this.gitService.getRepository(uri);584const baseUri = repository ? repository.rootUri.toString() : this.workspaceService.getWorkspaceFolder(uri)?.toString();585return baseUri ? path.relative(baseUri, uri.toString()) : path.basename(uri.path);586};587588const addFileReference = async (document: TextDocument, variableName?: string, isImplicit?: boolean) => {589clientReferences.push({590type: 'client.file',591data: {592language: document.languageId,593content: await redactFileContents(document)594},595is_implicit: Boolean(isImplicit),596id: await getImplicitContextId(document.uri)597});598599vscodeReferences.push(variableName600? { variableName, value: document.uri }601: document.uri602);603};604605const addSelectionReference = async (activeTextEditor: TextEditor, variableName?: string, reportReference = false, isImplicit?: boolean) => {606const selectionStart = activeTextEditor.selection.start.line;607const selection = activeTextEditor.selection.isEmpty ? new Range(new Position(selectionStart, 0), new Position(selectionStart + 1, 0)) : activeTextEditor.selection;608609clientReferences.push({610type: 'client.selection',611data: {612start: { line: selection.start.line, col: selection.start.character },613end: { line: selection.end.line, col: selection.end.character },614content: await redactFileContents(activeTextEditor.document, selection)615},616is_implicit: Boolean(isImplicit),617id: await getImplicitContextId(activeTextEditor.document.uri)618});619620if (reportReference) {621vscodeReferences.push(variableName622? { variableName, value: new Location(activeTextEditor.document.uri, selection) }623: new Location(activeTextEditor.document.uri, selection)624);625}626};627628// Check whether we can send file and selection data implicitly629if (this.checkAuthorized(slug)) {630const { activeTextEditor } = this.tabsAndEditorsService;631if (activeTextEditor && variables.find(v => v.id.startsWith('vscode.implicit'))) {632await addFileReference(activeTextEditor.document, undefined, true);633await addSelectionReference(activeTextEditor, undefined, undefined, true);634hasSentImplicitSelectionReference = true;635}636}637638for (const variable of variables) {639if (URI.isUri(variable.value)) {640const textDocument = await this.workspaceService.openTextDocument(variable.value);641await addFileReference(textDocument, variable.name);642} else if (variable.name === 'selection') {643const { activeTextEditor } = this.tabsAndEditorsService;644if (!activeTextEditor) {645throw new Error(l10n.t({ message: 'Please open a text editor to use the `#selection` variable.', comment: '{Locked=\'`#selection`\'}' }));646}647if (!hasSentImplicitSelectionReference) {648await addSelectionReference(activeTextEditor, variable.name, true);649}650} else if (variable.name === 'editor' && this.tabsAndEditorsService.activeTextEditor) {651await addFileReference(this.tabsAndEditorsService.activeTextEditor.document, variable.name);652}653}654655// Always send the open GitHub repositories656if (!this.gitService.isInitialized) {657await this.gitService.initialize();658}659const repositories = this.gitService.repositories;660for (const repository of repositories) {661const repoId = getGitHubRepoInfoFromContext(repository)?.id;662if (!repoId) {663continue; // Not a GitHub repository664}665666try {667const repo = await this.githubRepositoryService.getRepositoryInfo(repoId.org, repoId.repo);668clientReferences.push({669type: 'github.repository',670id: toGithubNwo(repoId),671data: {672type: 'repository',673name: repoId.repo,674ownerLogin: repoId.org,675id: repo.id676}677});678} catch (ex) {679if (ex instanceof Error && ex.message.includes('Failed to fetch repository info')) {680// TODO display a merged confirmation to reauthorize with the repo scope681// For now, raise a reauth badge so the user has a way out of this state682void this.authenticationService.getGitHubSession('permissive', { silent: true });683}684this.logService.error(ex, 'Failed to fetch info about current GitHub repository');685}686}687688return { clientReferences, vscodeReferences, hasIgnoredFiles };689}690691private async listEnabledSkills(authToken: string) {692if (!this.enabledSkillsPromise) {693this.enabledSkillsPromise = this.capiClientService.makeRequest<Response>({694method: 'GET',695headers: {696Authorization: `Bearer ${authToken}`,697}698}, { type: RequestType.ListSkills })699.then(response => response.json())700.then((json) => json?.['skills'].reduce((acc: Set<string>, skill: { slug: string }) => acc.add(skill.slug), new Set()));701}702return this.enabledSkillsPromise;703}704705private async resolveCopilotSkills(agent: string, request: ChatRequest): Promise<{ copilot_skills: string[] }> {706if (agent === GITHUB_PLATFORM_AGENT_NAME) {707const skills = new Set<string>();708for (const variable of request.references) {709if (GITHUB_PLATFORM_AGENT_SKILLS[variable.name]) {710skills.add(GITHUB_PLATFORM_AGENT_SKILLS[variable.name]);711}712}713return { copilot_skills: [...skills] };714}715716return { copilot_skills: [] };717}718719private async getPlatformAgentSkills() {720const authToken = this.authenticationService.anyGitHubSession?.accessToken;721if (!authToken) {722return [];723}724725// Register platform agent-specific native skills726const skills = await this.listEnabledSkills(authToken);727728return [729{ name: 'web', insertText: `#web`, description: 'Search Bing for real-time context', kind: 'bing-search', command: undefined },730].filter((skill) => skills.has(skill.kind));731}732}733734function prepareConfirmations(request: ChatRequest) {735const confirmations = [736...(request.acceptedConfirmationData?.map(c => ({ state: 'accepted', confirmation: c })) ?? []),737...(request.rejectedConfirmationData?.map(c => ({ state: 'dismissed', confirmation: c })) ?? []),738];739740return confirmations;741}742743function prepareRemoteAgentHistory(agentId: string, context: ChatContext): Raw.ChatMessage[] {744745const result: Raw.ChatMessage[] = [];746747for (const h of context.history) {748749if (h.participant !== agentId) {750continue;751}752753if (h instanceof ChatRequestTurn) {754result.push({755role: Raw.ChatRole.User,756content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: h.prompt }],757});758}759760if (h instanceof ChatResponseTurn) {761const copilot_references = h.result.metadata?.['copilot_references'];762const content = h.response.map(r => {763if (r instanceof ChatResponseMarkdownPart) {764return r.value.value;765} else if ('content' in r) {766return r.content;767} else {768return null;769}770}).filter(r => !!r).join('');771result.push({772role: Raw.ChatRole.Assistant,773content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: content }],774...(copilot_references ? { copilot_references } : undefined)775});776}777}778779return result;780}781782function getOrCreateSessionId(context: ChatContext): string {783let sessionId: string | undefined;784for (const h of context.history) {785if (h instanceof ChatResponseTurn) {786const maybeSessionId = (h.result as ICopilotChatResultIn).metadata?.sessionId;787if (typeof maybeSessionId === 'string') {788sessionId = maybeSessionId;789break;790}791}792}793794return sessionId ?? generateUuid();795}796797798