From ceec79e5d25c8d634749fa0ba22d7cc7322a99a3 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 4 Jun 2026 10:52:17 -0700 Subject: [PATCH 1/6] Run BG Todo Agent only if github copilot is signed in --- .../src/extension/intents/node/agentIntent.ts | 21 ++++++++++++++----- .../platform/networking/common/networking.ts | 20 ++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/extensions/copilot/src/extension/intents/node/agentIntent.ts b/extensions/copilot/src/extension/intents/node/agentIntent.ts index 166658b9c460fc..4e49430fd851b5 100644 --- a/extensions/copilot/src/extension/intents/node/agentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/agentIntent.ts @@ -69,6 +69,7 @@ import { getAgentMaxRequests } from '../common/agentConfig'; import { addCacheBreakpoints } from './cacheBreakpoints'; import { EditCodeIntent, EditCodeIntentInvocation, EditCodeIntentInvocationOptions, mergeMetadata, toNewChatReferences } from './editCodeIntent'; import { ToolCallingLoop } from './toolCallingLoop'; +import { IAuthenticationService } from '../../../lib/node/chatLibMain'; function isResponsesCompactionContextManagementEnabled(endpoint: IChatEndpoint, configurationService: IConfigurationService, experimentationService: IExperimentationService): boolean { return endpoint.apiType === 'responses' @@ -99,8 +100,14 @@ export function isTodoToolExplicitlyEnabled(request: vscode.ChatRequest): boolea * * @internal - exported for testing */ -export function isBackgroundTodoAgentEnabled(configurationService: IConfigurationService, experimentationService: IExperimentationService, request: vscode.ChatRequest): boolean { - return configurationService.getExperimentBasedConfig(ConfigKey.Advanced.BackgroundTodoAgentEnabled, experimentationService) +export function isBackgroundTodoAgentEnabled( + configurationService: IConfigurationService, + experimentationService: IExperimentationService, + authenticationService: IAuthenticationService, + request: vscode.ChatRequest): boolean { + const token = authenticationService.copilotToken; + const isEnabledForToken = token !== undefined && !token.isFreeUser && !token.isNoAuthUser; + return isEnabledForToken && configurationService.getExperimentBasedConfig(ConfigKey.Advanced.BackgroundTodoAgentEnabled, experimentationService) && !isTodoToolExplicitlyEnabled(request); } @@ -148,6 +155,8 @@ export const getAgentTools = async (accessor: ServicesAccessor, request: vscode. const experimentationService = accessor.get(IExperimentationService); const endpointProvider = accessor.get(IEndpointProvider); const editToolLearningService = accessor.get(IEditToolLearningService); + const authenticationService = accessor.get(IAuthenticationService); + model ??= await endpointProvider.getChatEndpoint(request); const allowTools: Record = {}; @@ -222,7 +231,7 @@ export const getAgentTools = async (accessor: ServicesAccessor, request: vscode. allowTools[ToolName.CoreManageTodoList] = false; } - if (isBackgroundTodoAgentEnabled(configurationService, experimentationService, request)) { + if (isBackgroundTodoAgentEnabled(configurationService, experimentationService, authenticationService, request)) { allowTools[ToolName.CoreManageTodoList] = false; } @@ -290,6 +299,7 @@ export class AgentIntent extends EditCodeIntent { @ICodeMapperService codeMapperService: ICodeMapperService, @IWorkspaceService workspaceService: IWorkspaceService, @IChatSessionService chatSessionService: IChatSessionService, + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, @IAutomodeService private readonly _automodeService: IAutomodeService, @ILogService private readonly _logService: ILogService, ) { @@ -383,7 +393,7 @@ export class AgentIntent extends EditCodeIntent { // Do NOT pass the request `token` as parentToken — it may be cancelled // by the framework after the turn ends, which would immediately abort // the background pass even on a normal completion. - if (isBackgroundTodoAgentEnabled(this.configurationService, this.expService, request)) { + if (isBackgroundTodoAgentEnabled(this.configurationService, this.expService, this._authenticationService, request)) { const todoProcessor = this._backgroundTodoProcessors.get(conversation.sessionId); const currentTurn = conversation.getLatestTurn(); const invocation = currentTurn.getMetadata(IntentInvocationMetadata)?.value; @@ -567,6 +577,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I @IAutomodeService private readonly automodeService: IAutomodeService, @IOTelService protected override readonly otelService: IOTelService, @ISessionTranscriptService private readonly sessionTranscriptService: ISessionTranscriptService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, ) { super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, otelService); } @@ -1376,7 +1387,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I this._backgroundTodoExecutionContext = executionContext; const { decision, reason, delta } = processor.shouldRun({ - backgroundTodoAgentEnabled: isBackgroundTodoAgentEnabled(this.configurationService, this.expService, this.request), + backgroundTodoAgentEnabled: isBackgroundTodoAgentEnabled(this.configurationService, this.expService, this.authenticationService, this.request), todoToolExplicitlyEnabled: isTodoToolExplicitlyEnabled(this.request), isAgentPrompt: this.prompt === AgentPrompt, promptContext, diff --git a/extensions/copilot/src/platform/networking/common/networking.ts b/extensions/copilot/src/platform/networking/common/networking.ts index e752a88eae7387..16a9d0c1847e3b 100644 --- a/extensions/copilot/src/platform/networking/common/networking.ts +++ b/extensions/copilot/src/platform/networking/common/networking.ts @@ -150,6 +150,26 @@ export function stringifyUrlOrRequestMetadata(urlOrRequestMetadata: string | Req return JSON.stringify(urlOrRequestMetadata); } +/** + * Whether the given value is {@link RequestMetadata} (routed through CAPI) rather + * than a literal URL string (fetched directly, e.g. BYOK / custom endpoints). + * + * This is the exact discriminant used by `networkRequest`: a `RequestMetadata` + * object is dispatched via {@link ICAPIClientService.makeRequest}, whereas a + * `string` URL is sent straight to {@link IFetcherService.fetch}. + */ +export function isCAPIRequestMetadata(urlOrRequestMetadata: string | RequestMetadata): urlOrRequestMetadata is RequestMetadata { + return typeof urlOrRequestMetadata !== 'string'; +} + +/** + * Whether requests for this endpoint are routed through CAPI (the Copilot proxy) + * rather than fetched directly from a literal URL (BYOK / custom endpoints). + */ +export function isCAPIEndpoint(endpoint: IEndpoint): boolean { + return isCAPIRequestMetadata(endpoint.urlOrRequestMetadata); +} + export interface IEmbeddingsEndpoint extends IEndpoint { readonly maxBatchSize: number; } From 4e68bfe6928cc4123b109044e67d6bd508622727 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 4 Jun 2026 11:05:25 -0700 Subject: [PATCH 2/6] move away from incorrectly using the `ownsAuthorization` property --- .../src/extension/intents/node/agentIntent.ts | 28 ++++++++++--------- .../endpoint/vscode-node/extChatEndpoint.ts | 2 -- .../platform/networking/common/networking.ts | 4 +-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions/copilot/src/extension/intents/node/agentIntent.ts b/extensions/copilot/src/extension/intents/node/agentIntent.ts index 4e49430fd851b5..d59464c7273f79 100644 --- a/extensions/copilot/src/extension/intents/node/agentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/agentIntent.ts @@ -19,7 +19,7 @@ import { IEnvService } from '../../../platform/env/common/envService'; import { ILogService } from '../../../platform/log/common/logService'; import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogService'; import { CUSTOM_TOOL_SEARCH_NAME, isAnthropicContextEditingEnabled } from '../../../platform/networking/common/anthropic'; -import { IChatEndpoint } from '../../../platform/networking/common/networking'; +import { IChatEndpoint, isCAPIEndpoint } from '../../../platform/networking/common/networking'; import { modelsWithoutResponsesContextManagement } from '../../../platform/networking/common/openai'; import { INotebookService } from '../../../platform/notebook/common/notebookService'; import { GenAiMetrics } from '../../../platform/otel/common/genAiMetrics'; @@ -101,12 +101,15 @@ export function isTodoToolExplicitlyEnabled(request: vscode.ChatRequest): boolea * @internal - exported for testing */ export function isBackgroundTodoAgentEnabled( + endpoint: IChatEndpoint, configurationService: IConfigurationService, experimentationService: IExperimentationService, authenticationService: IAuthenticationService, request: vscode.ChatRequest): boolean { const token = authenticationService.copilotToken; - const isEnabledForToken = token !== undefined && !token.isFreeUser && !token.isNoAuthUser; + + // Only enable for a signed in no-free plan user talking to the CAPI endpoint. + const isEnabledForToken = token !== undefined && !token.isFreeUser && !token.isNoAuthUser && isCAPIEndpoint(endpoint); return isEnabledForToken && configurationService.getExperimentBasedConfig(ConfigKey.Advanced.BackgroundTodoAgentEnabled, experimentationService) && !isTodoToolExplicitlyEnabled(request); } @@ -189,10 +192,9 @@ export const getAgentTools = async (accessor: ServicesAccessor, request: vscode. allowTools[ToolName.CoreRunTest] = await testService.hasAnyTests(); allowTools[ToolName.CoreRunTask] = tasksService.getTasks().length > 0; - // BYOK endpoints that own their `Authorization` have no Copilot token for the - // agentic proxy or override models the subagents rely on, so disable them - // entirely and skip the (otherwise unnecessary) config and endpoint lookups. - if (model.ownsAuthorization) { + // The specialized subagents must only run when + // the main agent is on CAPI. + if (!isCAPIEndpoint(model)) { allowTools[ToolName.SearchSubagent] = false; allowTools[ToolName.ExploreSubagent] = false; allowTools[ToolName.ExecutionSubagent] = false; @@ -231,7 +233,7 @@ export const getAgentTools = async (accessor: ServicesAccessor, request: vscode. allowTools[ToolName.CoreManageTodoList] = false; } - if (isBackgroundTodoAgentEnabled(configurationService, experimentationService, authenticationService, request)) { + if (isBackgroundTodoAgentEnabled(model, configurationService, experimentationService, authenticationService, request)) { allowTools[ToolName.CoreManageTodoList] = false; } @@ -299,7 +301,6 @@ export class AgentIntent extends EditCodeIntent { @ICodeMapperService codeMapperService: ICodeMapperService, @IWorkspaceService workspaceService: IWorkspaceService, @IChatSessionService chatSessionService: IChatSessionService, - @IAuthenticationService private readonly _authenticationService: IAuthenticationService, @IAutomodeService private readonly _automodeService: IAutomodeService, @ILogService private readonly _logService: ILogService, ) { @@ -393,12 +394,12 @@ export class AgentIntent extends EditCodeIntent { // Do NOT pass the request `token` as parentToken — it may be cancelled // by the framework after the turn ends, which would immediately abort // the background pass even on a normal completion. - if (isBackgroundTodoAgentEnabled(this.configurationService, this.expService, this._authenticationService, request)) { - const todoProcessor = this._backgroundTodoProcessors.get(conversation.sessionId); + const todoProcessor = this._backgroundTodoProcessors.get(conversation.sessionId); + if (todoProcessor !== undefined) { const currentTurn = conversation.getLatestTurn(); const invocation = currentTurn.getMetadata(IntentInvocationMetadata)?.value; const executionContext = invocation instanceof AgentIntentInvocation ? invocation.getBackgroundTodoExecutionContext() : undefined; - if (todoProcessor && executionContext) { + if (executionContext) { todoProcessor.requestFinalReview(currentTurn.id, executionContext); await todoProcessor.waitForCompletion(); } @@ -939,7 +940,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I } // ── Background todo processing ────────────────────────────────── - this._maybeStartBackgroundTodoPass(promptContext, token); + this._maybeStartBackgroundTodoPass(endpoint, promptContext, token); const lastMessage = result.messages.at(-1); if (lastMessage?.role === Raw.ChatRole.User) { @@ -1360,6 +1361,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I * Kick off a background todo pass if the policy says to run. */ private _maybeStartBackgroundTodoPass( + endpoint: IChatEndpoint, promptContext: IBuildPromptContext, token: vscode.CancellationToken, ): void { @@ -1387,7 +1389,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I this._backgroundTodoExecutionContext = executionContext; const { decision, reason, delta } = processor.shouldRun({ - backgroundTodoAgentEnabled: isBackgroundTodoAgentEnabled(this.configurationService, this.expService, this.authenticationService, this.request), + backgroundTodoAgentEnabled: isBackgroundTodoAgentEnabled(endpoint, this.configurationService, this.expService, this.authenticationService, this.request), todoToolExplicitlyEnabled: isTodoToolExplicitlyEnabled(this.request), isAgentPrompt: this.prompt === AgentPrompt, promptContext, diff --git a/extensions/copilot/src/platform/endpoint/vscode-node/extChatEndpoint.ts b/extensions/copilot/src/platform/endpoint/vscode-node/extChatEndpoint.ts index 2609b4a898cfc4..48bac293bf733c 100644 --- a/extensions/copilot/src/platform/endpoint/vscode-node/extChatEndpoint.ts +++ b/extensions/copilot/src/platform/endpoint/vscode-node/extChatEndpoint.ts @@ -45,8 +45,6 @@ export class ExtensionContributedChatEndpoint implements IChatEndpoint { public readonly multiplier: number | undefined = undefined; public readonly isExtensionContributed = true; public readonly supportedEditTools?: readonly EndpointEditToolName[] | undefined; - // Extension-contributed endpoints are not backed by the CAPI Copilot token; treat them as owning auth to opt out of token fallback and related tool gating. - public readonly ownsAuthorization = true; constructor( private readonly languageModel: vscode.LanguageModelChat, diff --git a/extensions/copilot/src/platform/networking/common/networking.ts b/extensions/copilot/src/platform/networking/common/networking.ts index 16a9d0c1847e3b..dea73c986edcee 100644 --- a/extensions/copilot/src/platform/networking/common/networking.ts +++ b/extensions/copilot/src/platform/networking/common/networking.ts @@ -522,7 +522,7 @@ function networkRequest( // pass the controller abort signal to the request request.signal = abort.signal; } - if (typeof endpoint.urlOrRequestMetadata === 'string') { + if (!isCAPIRequestMetadata(endpoint.urlOrRequestMetadata)) { const requestPromise = fetcher.fetch(endpoint.urlOrRequestMetadata, request).catch(reason => { if (canRetryOnce && canRetryOnceNetworkError(reason)) { // disconnect and retry the request once if the connection was reset @@ -538,7 +538,7 @@ function networkRequest( }); return requestPromise; } else { - return capiClientService.makeRequest(request, endpoint.urlOrRequestMetadata as RequestMetadata); + return capiClientService.makeRequest(request, endpoint.urlOrRequestMetadata); } } From e559424e7aaf7079213a2242ccaa3ff3ad77b188 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 4 Jun 2026 11:23:33 -0700 Subject: [PATCH 3/6] add tests --- .../test/backgroundTodoEnablement.spec.ts | 79 +++++++++++++++---- .../networking/test/node/networking.spec.ts | 29 ++++++- 2 files changed, 92 insertions(+), 16 deletions(-) diff --git a/extensions/copilot/src/extension/intents/node/test/backgroundTodoEnablement.spec.ts b/extensions/copilot/src/extension/intents/node/test/backgroundTodoEnablement.spec.ts index 6bf701fe644277..d6b80899f07a9d 100644 --- a/extensions/copilot/src/extension/intents/node/test/backgroundTodoEnablement.spec.ts +++ b/extensions/copilot/src/extension/intents/node/test/backgroundTodoEnablement.spec.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { RequestType } from '@vscode/copilot-api'; import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest'; +import { IAuthenticationService } from '../../../../platform/authentication/common/authentication'; +import { CopilotToken, createTestExtendedTokenInfo } from '../../../../platform/authentication/common/copilotToken'; import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService'; import { MockEndpoint } from '../../../../platform/endpoint/test/node/mockEndpoint'; import { IChatEndpoint } from '../../../../platform/networking/common/networking'; @@ -63,13 +66,74 @@ describe('isTodoToolExplicitlyEnabled', () => { }); }); +// ─── isBackgroundTodoAgentEnabled unit tests ───────────────────── + +// The gate only opens for a signed-in, paid user whose request is routed +// through CAPI. Each test flips exactly one of those factors away from an +// otherwise-enabled baseline to confirm it is sufficient to close the gate. + +describe('isBackgroundTodoAgentEnabled', () => { + + // CAPI endpoints carry a `RequestMetadata` object; BYOK/custom endpoints are + // fetched from a literal URL string. + const capiEndpoint = { urlOrRequestMetadata: { type: RequestType.ChatCompletions } } as unknown as IChatEndpoint; + const byokEndpoint = { urlOrRequestMetadata: 'https://api.example.com/v1/chat' } as unknown as IChatEndpoint; + + const paidToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'copilot_individual', copilot_plan: 'individual' })); + const freeToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'free_limited_copilot' })); + const noAuthToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'no_auth_limited_copilot' })); + + function auth(copilotToken: CopilotToken | undefined): IAuthenticationService { + return { copilotToken } as unknown as IAuthenticationService; + } + + function config(experimentEnabled: boolean): IConfigurationService { + return { getExperimentBasedConfig: () => experimentEnabled } as unknown as IConfigurationService; + } + + const expService = {} as IExperimentationService; + + function isEnabled(endpoint: IChatEndpoint, copilotToken: CopilotToken | undefined, experimentEnabled: boolean, request: TestChatRequest = new TestChatRequest('fix the bug')): boolean { + return isBackgroundTodoAgentEnabled(endpoint, config(experimentEnabled), expService, auth(copilotToken), request); + } + + test('enabled for a signed-in paid user on a CAPI endpoint with the experiment on', () => { + expect(isEnabled(capiEndpoint, paidToken, true)).toBe(true); + }); + + test('disabled when the experiment is off', () => { + expect(isEnabled(capiEndpoint, paidToken, false)).toBe(false); + }); + + test('disabled when there is no Copilot token (signed out)', () => { + expect(isEnabled(capiEndpoint, undefined, true)).toBe(false); + }); + + test('disabled for a free-plan user', () => { + expect(isEnabled(capiEndpoint, freeToken, true)).toBe(false); + }); + + test('disabled for a no-auth user', () => { + expect(isEnabled(capiEndpoint, noAuthToken, true)).toBe(false); + }); + + test('disabled on a non-CAPI (BYOK) endpoint', () => { + expect(isEnabled(byokEndpoint, paidToken, true)).toBe(false); + }); + + test('disabled when #todo is explicitly referenced', () => { + const request = new TestChatRequest('fix the bug'); + (request as any).toolReferences = [{ name: 'todo' }]; + expect(isEnabled(capiEndpoint, paidToken, true, request)).toBe(false); + }); +}); + // ─── getAgentTools integration tests for background todo gate ──── describe('getAgentTools background todo enablement', () => { let accessor: ITestingServicesAccessor; let instantiationService: IInstantiationService; let configService: IConfigurationService; - let experimentationService: IExperimentationService; let mockEndpoint: IChatEndpoint; beforeAll(() => { @@ -85,7 +149,6 @@ describe('getAgentTools background todo enablement', () => { accessor = services.createTestingAccessor(); instantiationService = accessor.get(IInstantiationService); configService = accessor.get(IConfigurationService); - experimentationService = accessor.get(IExperimentationService); mockEndpoint = instantiationService.createInstance(MockEndpoint, undefined); }); @@ -102,18 +165,6 @@ describe('getAgentTools background todo enablement', () => { return tools.some(t => t.name === ToolName.CoreManageTodoList); } - test('background todo agent is enabled only when experiment is on and todo is not explicit', () => { - const request = new TestChatRequest('fix the bug'); - configService.setConfig(ConfigKey.Advanced.BackgroundTodoAgentEnabled, false); - expect(isBackgroundTodoAgentEnabled(configService, experimentationService, request)).toBe(false); - - configService.setConfig(ConfigKey.Advanced.BackgroundTodoAgentEnabled, true); - expect(isBackgroundTodoAgentEnabled(configService, experimentationService, request)).toBe(true); - - (request as any).toolReferences = [{ name: 'todo' }]; - expect(isBackgroundTodoAgentEnabled(configService, experimentationService, request)).toBe(false); - }); - test('todo tool is not in enabled tools when experiment is on', async () => { configService.setConfig(ConfigKey.Advanced.BackgroundTodoAgentEnabled, true); const request = new TestChatRequest('fix the bug'); diff --git a/extensions/copilot/src/platform/networking/test/node/networking.spec.ts b/extensions/copilot/src/platform/networking/test/node/networking.spec.ts index 044901f9d78219..de022dd7b233c9 100644 --- a/extensions/copilot/src/platform/networking/test/node/networking.spec.ts +++ b/extensions/copilot/src/platform/networking/test/node/networking.spec.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RequestType } from '@vscode/copilot-api'; +import { RequestType, type RequestMetadata } from '@vscode/copilot-api'; import assert from 'assert'; import { suite, test } from 'vitest'; import { Event } from '../../../../util/vs/base/common/event'; @@ -11,7 +11,7 @@ import { IInstantiationService } from '../../../../util/vs/platform/instantiatio import { createFakeResponse } from '../../../test/node/fetcher'; import { createPlatformServices } from '../../../test/node/services'; import { FetchOptions, IAbortController, IFetcherService, PaginationOptions, Response, WebSocketConnection } from '../../common/fetcherService'; -import { postRequest } from '../../common/networking'; +import { IEndpoint, isCAPIEndpoint, isCAPIRequestMetadata, postRequest } from '../../common/networking'; suite('Networking test Suite', function () { @@ -102,3 +102,28 @@ suite('Networking test Suite', function () { assert.strictEqual('Authorization' in headerBuffer!, false); }); }); + +suite('isCAPIRequestMetadata / isCAPIEndpoint', function () { + + const capiMetadata: RequestMetadata = { type: RequestType.ChatCompletions }; + + function endpointWith(urlOrRequestMetadata: string | RequestMetadata): IEndpoint { + return { urlOrRequestMetadata } as unknown as IEndpoint; + } + + test('isCAPIRequestMetadata is false for a literal URL string', function () { + assert.strictEqual(isCAPIRequestMetadata('https://api.example.com/v1/chat'), false); + }); + + test('isCAPIRequestMetadata is true for RequestMetadata routed through CAPI', function () { + assert.strictEqual(isCAPIRequestMetadata(capiMetadata), true); + }); + + test('isCAPIEndpoint is false for an endpoint fetched from a literal URL (BYOK)', function () { + assert.strictEqual(isCAPIEndpoint(endpointWith('https://api.example.com/v1/chat')), false); + }); + + test('isCAPIEndpoint is true for an endpoint routed through CAPI', function () { + assert.strictEqual(isCAPIEndpoint(endpointWith(capiMetadata)), true); + }); +}); From 94e3ea5887a509866a7bc59c169553dc9a950e0d Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 4 Jun 2026 11:47:05 -0700 Subject: [PATCH 4/6] fix type errors --- .../copilot/src/extension/intents/node/askAgentIntent.ts | 4 +++- .../copilot/src/extension/intents/node/editCodeIntent2.ts | 4 +++- .../src/extension/intents/node/notebookEditorIntent.ts | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/extensions/copilot/src/extension/intents/node/askAgentIntent.ts b/extensions/copilot/src/extension/intents/node/askAgentIntent.ts index 8a8cd2ecdbd726..9736905a4d319d 100644 --- a/extensions/copilot/src/extension/intents/node/askAgentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/askAgentIntent.ts @@ -34,6 +34,7 @@ import { ICodeMapperService } from '../../prompts/node/codeMapper/codeMapperServ import { IToolsService } from '../../tools/common/toolsService'; import { getAgentMaxRequests } from '../common/agentConfig'; import { AgentIntentInvocation } from './agentIntent'; +import { IAuthenticationService } from '../../../lib/node/chatLibMain'; const getTools = (instaService: IInstantiationService, request: vscode.ChatRequest): Promise => @@ -130,8 +131,9 @@ export class AskAgentIntentInvocation extends AgentIntentInvocation { @IAutomodeService automodeService: IAutomodeService, @IOTelService otelService: IOTelService, @ISessionTranscriptService sessionTranscriptService: ISessionTranscriptService, + @IAuthenticationService authenticationService: IAuthenticationService, ) { - super(intent, location, endpoint, request, { processCodeblocks: true }, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, sessionTranscriptService); + super(intent, location, endpoint, request, { processCodeblocks: true }, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, sessionTranscriptService, authenticationService); } public override async getAvailableTools(): Promise { diff --git a/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts b/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts index 1c9063e066b746..052f0f26a4c5fd 100644 --- a/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts +++ b/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts @@ -31,6 +31,7 @@ import { ToolName } from '../../tools/common/toolNames'; import { IToolsService } from '../../tools/common/toolsService'; import { AgentIntentInvocation } from './agentIntent'; import { EditCodeIntentOptions } from './editCodeIntent'; +import { IAuthenticationService } from '../../../lib/node/chatLibMain'; const getTools = (instaService: IInstantiationService, request: vscode.ChatRequest): Promise => instaService.invokeFunction(async accessor => { @@ -91,8 +92,9 @@ export class EditCode2IntentInvocation extends AgentIntentInvocation { @IAutomodeService automodeService: IAutomodeService, @IOTelService otelService: IOTelService, @ISessionTranscriptService sessionTranscriptService: ISessionTranscriptService, + @IAuthenticationService authenticationService: IAuthenticationService, ) { - super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, sessionTranscriptService); + super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, sessionTranscriptService, authenticationService); } public override async getAvailableTools(): Promise { diff --git a/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts b/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts index a0e71c9757101c..f3242ca0c95f37 100644 --- a/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts +++ b/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts @@ -38,6 +38,7 @@ import { IToolsService } from '../../tools/common/toolsService'; import { getAgentMaxRequests } from '../common/agentConfig'; import { EditCodeIntent, EditCodeIntentOptions } from './editCodeIntent'; import { EditCode2IntentInvocation } from './editCodeIntent2'; +import { IAuthenticationService } from '../../../lib/node/chatLibMain'; const getTools = (instaService: IInstantiationService, request: vscode.ChatRequest): Promise => instaService.invokeFunction(async accessor => { @@ -109,8 +110,9 @@ export class NotebookEditorIntentInvocation extends EditCode2IntentInvocation { @IAutomodeService automodeService: IAutomodeService, @IOTelService otelService: IOTelService, @ISessionTranscriptService sessionTranscriptService: ISessionTranscriptService, + @IAuthenticationService authenticationService: IAuthenticationService, ) { - super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, sessionTranscriptService); + super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, sessionTranscriptService, authenticationService); } protected override prompt = NotebookInlinePrompt; From b61d67bf4e12e07873bdad1dc46230a1ae0cb2ab Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 4 Jun 2026 12:29:59 -0700 Subject: [PATCH 5/6] address PR comments --- extensions/copilot/src/extension/intents/node/agentIntent.ts | 5 +++-- .../copilot/src/extension/intents/node/askAgentIntent.ts | 2 +- .../copilot/src/extension/intents/node/editCodeIntent2.ts | 2 +- .../src/extension/intents/node/notebookEditorIntent.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/extensions/copilot/src/extension/intents/node/agentIntent.ts b/extensions/copilot/src/extension/intents/node/agentIntent.ts index d59464c7273f79..b9ae19c55c7c0f 100644 --- a/extensions/copilot/src/extension/intents/node/agentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/agentIntent.ts @@ -69,7 +69,7 @@ import { getAgentMaxRequests } from '../common/agentConfig'; import { addCacheBreakpoints } from './cacheBreakpoints'; import { EditCodeIntent, EditCodeIntentInvocation, EditCodeIntentInvocationOptions, mergeMetadata, toNewChatReferences } from './editCodeIntent'; import { ToolCallingLoop } from './toolCallingLoop'; -import { IAuthenticationService } from '../../../lib/node/chatLibMain'; +import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; function isResponsesCompactionContextManagementEnabled(endpoint: IChatEndpoint, configurationService: IConfigurationService, experimentationService: IExperimentationService): boolean { return endpoint.apiType === 'responses' @@ -1386,7 +1386,6 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I telemetryService: this.telemetryService, promptContext, }; - this._backgroundTodoExecutionContext = executionContext; const { decision, reason, delta } = processor.shouldRun({ backgroundTodoAgentEnabled: isBackgroundTodoAgentEnabled(endpoint, this.configurationService, this.expService, this.authenticationService, this.request), @@ -1400,6 +1399,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I if (decision === BackgroundTodoDecision.Wait && reason === 'processorInProgress' && delta) { // Coalesce into the queue so the latest context is not lost. + this._backgroundTodoExecutionContext = executionContext; processor.requestRegularPass(delta, executionContext, token, turnId); return; } @@ -1408,6 +1408,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I return; } + this._backgroundTodoExecutionContext = executionContext; processor.requestRegularPass(delta, executionContext, token, turnId); } diff --git a/extensions/copilot/src/extension/intents/node/askAgentIntent.ts b/extensions/copilot/src/extension/intents/node/askAgentIntent.ts index 9736905a4d319d..3a7e1e1a8bcdca 100644 --- a/extensions/copilot/src/extension/intents/node/askAgentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/askAgentIntent.ts @@ -34,7 +34,7 @@ import { ICodeMapperService } from '../../prompts/node/codeMapper/codeMapperServ import { IToolsService } from '../../tools/common/toolsService'; import { getAgentMaxRequests } from '../common/agentConfig'; import { AgentIntentInvocation } from './agentIntent'; -import { IAuthenticationService } from '../../../lib/node/chatLibMain'; +import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; const getTools = (instaService: IInstantiationService, request: vscode.ChatRequest): Promise => diff --git a/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts b/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts index 052f0f26a4c5fd..9faff95436a9f8 100644 --- a/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts +++ b/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts @@ -31,7 +31,7 @@ import { ToolName } from '../../tools/common/toolNames'; import { IToolsService } from '../../tools/common/toolsService'; import { AgentIntentInvocation } from './agentIntent'; import { EditCodeIntentOptions } from './editCodeIntent'; -import { IAuthenticationService } from '../../../lib/node/chatLibMain'; +import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; const getTools = (instaService: IInstantiationService, request: vscode.ChatRequest): Promise => instaService.invokeFunction(async accessor => { diff --git a/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts b/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts index f3242ca0c95f37..b20d95ad7b3785 100644 --- a/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts +++ b/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts @@ -38,7 +38,7 @@ import { IToolsService } from '../../tools/common/toolsService'; import { getAgentMaxRequests } from '../common/agentConfig'; import { EditCodeIntent, EditCodeIntentOptions } from './editCodeIntent'; import { EditCode2IntentInvocation } from './editCodeIntent2'; -import { IAuthenticationService } from '../../../lib/node/chatLibMain'; +import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; const getTools = (instaService: IInstantiationService, request: vscode.ChatRequest): Promise => instaService.invokeFunction(async accessor => { From 160dee22668917d2e1c1fd4ef9da4b238f0c2cc4 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Thu, 4 Jun 2026 12:38:42 -0700 Subject: [PATCH 6/6] fix tests --- .../test/backgroundTodoEnablement.spec.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/extensions/copilot/src/extension/intents/node/test/backgroundTodoEnablement.spec.ts b/extensions/copilot/src/extension/intents/node/test/backgroundTodoEnablement.spec.ts index d6b80899f07a9d..cad2bd40d80069 100644 --- a/extensions/copilot/src/extension/intents/node/test/backgroundTodoEnablement.spec.ts +++ b/extensions/copilot/src/extension/intents/node/test/backgroundTodoEnablement.spec.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RequestType } from '@vscode/copilot-api'; +import { RequestType, type RequestMetadata } from '@vscode/copilot-api'; import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest'; import { IAuthenticationService } from '../../../../platform/authentication/common/authentication'; import { CopilotToken, createTestExtendedTokenInfo } from '../../../../platform/authentication/common/copilotToken'; +import { setCopilotToken } from '../../../../platform/authentication/common/staticGitHubAuthenticationService'; import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService'; import { MockEndpoint } from '../../../../platform/endpoint/test/node/mockEndpoint'; import { IChatEndpoint } from '../../../../platform/networking/common/networking'; @@ -149,7 +150,14 @@ describe('getAgentTools background todo enablement', () => { accessor = services.createTestingAccessor(); instantiationService = accessor.get(IInstantiationService); configService = accessor.get(IConfigurationService); + + // The background-todo gate only opens for a signed-in paid user whose + // request is routed through CAPI, so set up both for this harness. + const paidToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'copilot_individual', copilot_plan: 'individual' })); + setCopilotToken(accessor.get(IAuthenticationService), paidToken); + mockEndpoint = instantiationService.createInstance(MockEndpoint, undefined); + (mockEndpoint as unknown as { urlOrRequestMetadata: string | RequestMetadata }).urlOrRequestMetadata = { type: RequestType.ChatCompletions }; }); afterAll(() => { @@ -200,10 +208,14 @@ describe('getAgentTools background todo enablement', () => { describe('AgentIntentInvocation._maybeStartBackgroundTodoPass subagent guard', () => { - function getMethod(): (this: unknown, promptContext: unknown, token: unknown) => void { - return (AgentIntentInvocation.prototype as unknown as { _maybeStartBackgroundTodoPass: (this: unknown, promptContext: unknown, token: unknown) => void })._maybeStartBackgroundTodoPass; + function getMethod(): (this: unknown, endpoint: unknown, promptContext: unknown, token: unknown) => void { + return (AgentIntentInvocation.prototype as unknown as { _maybeStartBackgroundTodoPass: (this: unknown, endpoint: unknown, promptContext: unknown, token: unknown) => void })._maybeStartBackgroundTodoPass; } + // The guard short-circuits on `request.subAgentInvocationId` before the + // endpoint is inspected, so a placeholder endpoint suffices for these tests. + const endpoint = {} as unknown as IChatEndpoint; + function makeStub(request: TestChatRequest, processorLookup: () => unknown) { return { request, @@ -227,7 +239,7 @@ describe('AgentIntentInvocation._maybeStartBackgroundTodoPass subagent guard', ( return undefined; }); - getMethod().call(stub, { conversation: { sessionId: 'sess-1' } }, {}); + getMethod().call(stub, endpoint, { conversation: { sessionId: 'sess-1' } }, {}); expect(processorLookups).toBe(0); }); @@ -241,7 +253,7 @@ describe('AgentIntentInvocation._maybeStartBackgroundTodoPass subagent guard', ( return undefined; }); - getMethod().call(stub, { conversation: { sessionId: 'sess-1' } }, {}); + getMethod().call(stub, endpoint, { conversation: { sessionId: 'sess-1' } }, {}); expect(processorLookups).toBe(1); }); @@ -256,7 +268,7 @@ describe('AgentIntentInvocation._maybeStartBackgroundTodoPass subagent guard', ( return undefined; }); - getMethod().call(stub, { conversation: { sessionId: 'sess-1' } }, {}); + getMethod().call(stub, endpoint, { conversation: { sessionId: 'sess-1' } }, {}); expect(processorLookups).toBe(1); });