From f4e50bfd97624788b782a176996881679c89f562 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 18 Aug 2026 17:32:01 -0700 Subject: [PATCH 1/6] Refactor AgentService instantiation Create an agent-host application DI scope, construct AgentService through it, and remove child-to-parent service re-exports. Update tests to use the production construction path. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/node/agentHostApplication.ts | 32 ++ .../platform/agentHost/node/agentHostMain.ts | 69 ++-- .../agentHost/node/agentHostServerMain.ts | 80 ++--- .../platform/agentHost/node/agentService.ts | 86 ++--- .../agentHost/test/node/agentService.test.ts | 331 +++++++++--------- .../test/node/agentServiceTestUtils.ts | 86 +++++ .../test/node/agentSideEffects.test.ts | 8 +- .../agentHost/test/node/claudeAgent.test.ts | 4 +- 8 files changed, 386 insertions(+), 310 deletions(-) create mode 100644 src/vs/platform/agentHost/node/agentHostApplication.ts create mode 100644 src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts diff --git a/src/vs/platform/agentHost/node/agentHostApplication.ts b/src/vs/platform/agentHost/node/agentHostApplication.ts new file mode 100644 index 00000000000000..d624e455087660 --- /dev/null +++ b/src/vs/platform/agentHost/node/agentHostApplication.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService } from '../../instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; +import { AgentService, IAgentServiceOptions } from './agentService.js'; + +export interface IAgentHostApplication { + readonly agentService: T; + readonly instantiationService: IInstantiationService; + readonly services: ServiceCollection; +} + +type AgentServiceFactory = (instantiationService: IInstantiationService, services: ServiceCollection) => T; + +export function createAgentHostApplication(parentInstantiationService: IInstantiationService, options: IAgentServiceOptions): IAgentHostApplication; +export function createAgentHostApplication(parentInstantiationService: IInstantiationService, options: IAgentServiceOptions, factory: AgentServiceFactory): IAgentHostApplication; +export function createAgentHostApplication(parentInstantiationService: IInstantiationService, options: IAgentServiceOptions, factory?: AgentServiceFactory): IAgentHostApplication { + const services = new ServiceCollection(); + const instantiationService = parentInstantiationService.createChild(services); + try { + const agentService = factory + ? factory(instantiationService, services) + : instantiationService.createInstance(AgentService, options, services); + return { agentService, instantiationService, services }; + } catch (error) { + instantiationService.dispose(); + throw error; + } +} diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index 7d05020ec33bd5..4529e59b00f3c3 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -16,23 +16,13 @@ import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import * as os from 'os'; import * as inspector from 'inspector'; -import { AgentHostClaudeAgentEnabledEnvVar, AgentHostCodexAgentEnabledEnvVar, AgentHostIpcChannels, IAgentHostInspectInfo, IAgentHostSocketInfo, IAgentService, IConnectionTrackerService, isAgentEnabled } from '../common/agentService.js'; +import { AgentHostClaudeAgentEnabledEnvVar, AgentHostCodexAgentEnabledEnvVar, AgentHostIpcChannels, IAgentHostInspectInfo, IAgentHostSocketInfo, IConnectionTrackerService, isAgentEnabled } from '../common/agentService.js'; import { AgentHostCodexEnabledConfigKey, platformRootSchema } from '../common/agentHostSchema.js'; import { AgentModelRefreshScheduler, MODEL_REFRESH_INTERVAL_MS } from './agentModelRefreshScheduler.js'; import { AgentService } from './agentService.js'; -import { IAgentHostStateManager } from './agentHostStateManager.js'; -import { IAgentHostPromptCache } from './agentHostPromptCache.js'; -import { IAgentHostSessionTitleSignal } from './agentHostSessionTitleSignal.js'; -import { IAgentConfigurationService } from './agentConfigurationService.js'; -import { IAgentHostStorageService } from './agentHostStorageService.js'; -import { IAgentHostCustomizationEnablementService } from './agentHostCustomizationEnablementService.js'; -import { IAgentHostManagedSettingsService } from './agentHostManagedSettingsService.js'; -import { IAgentHostGitHubEndpointService } from './agentHostGitHubEndpointService.js'; -import { IAgentHostCompletions } from './agentHostCompletions.js'; -import { IAgentHostTerminalManager } from './agentHostTerminalManager.js'; +import { createAgentHostApplication } from './agentHostApplication.js'; import { CopilotAgent } from './copilot/copilotAgent.js'; import { IAgentHostWorktreeIsolation, WorktreeIsolation } from './shared/worktreeIsolation.js'; -import { CopilotApiService, ICopilotApiService } from './shared/copilotApiService.js'; import { ClaudeAgent } from './claude/claudeAgent.js'; import { ClaudeAgentSdkService, ClaudeSdkPackage, IClaudeAgentSdkService } from './claude/claudeAgentSdkService.js'; import { ClaudeProxyService, IClaudeProxyService } from './claude/claudeProxyService.js'; @@ -92,7 +82,6 @@ import { IAgentPluginManager } from '../common/agentPluginManager.js'; import { AgentPluginManager } from './agentPluginManager.js'; import { AgentHostGitService } from './agentHostGitService.js'; import { IAgentHostGitService } from '../common/agentHostGitService.js'; -import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js'; import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js'; import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js'; import { join } from '../../../base/common/path.js'; @@ -160,6 +149,7 @@ async function startAgentHost(): Promise { // Create the real service implementation that lives in this process let agentService: AgentService; let instantiationService: IInstantiationService; + let rootInstantiationService: IInstantiationService | undefined; // Hoisted out of the `try` below so the protocol handlers (constructed // after the block) can forward agent-SDK download progress to clients. let sdkDownloadProgress: Event | undefined; @@ -182,7 +172,8 @@ async function startAgentHost(): Promise { const telemetryService = await createAgentHostTelemetryService({ environmentService, productService, fileService, loggerService, logService, disposables, fetchFn, requestService: networkServices.requestService }); errorTelemetry.value = new ErrorTelemetry(telemetryService); diServices.set(ITelemetryService, telemetryService); - instantiationService = new InstantiationService(diServices); + rootInstantiationService = new InstantiationService(diServices, /*strict*/ true); + instantiationService = rootInstantiationService; const fileMonitorService = disposables.add(instantiationService.createInstance(AgentHostFileMonitorService)); diServices.set(IAgentHostFileMonitorService, fileMonitorService); diServices.set(IWindowsMxcTerminalSandboxRuntime, instantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); @@ -208,50 +199,38 @@ async function startAgentHost(): Promise { diServices.set(IByokLmProxyService, byokLmProxyService); const agentHostOTelService = disposables.add(instantiationService.createInstance(AgentHostOTelService, fetchFn)); diServices.set(IAgentHostOTelService, agentHostOTelService); - agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, rootConfigResource, telemetryService, fileMonitorService, undefined, fetchFn, [createCodexProviderConfiguration(environmentService.userHome)], hostLaunchKind, storageResource); + const application = createAgentHostApplication(instantiationService, { + rootConfigResource, + providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], + hostLaunchKind, + storageResource, + }); + agentService = application.agentService; + instantiationService = application.instantiationService; + const agentServices = application.services; const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); - diServices.set(INetworkDiagnosticsService, networkDiagnosticsService); + agentServices.set(INetworkDiagnosticsService, networkDiagnosticsService); agentService.setNetworkDiagnosticsService(networkDiagnosticsService); - diServices.set(IAgentService, agentService); - diServices.set(IAgentHostStateManager, agentService.stateManager); - // Narrow host seams providers consume instead of the whole state manager. - diServices.set(IAgentHostPromptCache, agentService.promptCache); - diServices.set(IAgentHostSessionTitleSignal, agentService.sessionTitleSignal); const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService); - diServices.set(IAgentPluginManager, pluginManager); + agentServices.set(IAgentPluginManager, pluginManager); const diffComputeService = disposables.add(new NodeWorkerDiffComputeService(logService)); - diServices.set(IDiffComputeService, diffComputeService); + agentServices.set(IDiffComputeService, diffComputeService); const editAttributionService = disposables.add(instantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); - diServices.set(IAgentEditAttributionService, editAttributionService); + agentServices.set(IAgentEditAttributionService, editAttributionService); agentService.setEditAttributionService(editAttributionService); - diServices.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); - - diServices.set(IAgentHostTerminalManager, agentService.terminalManager); - diServices.set(IAgentConfigurationService, agentService.configurationService); - diServices.set(IAgentHostStorageService, agentService.storageService); - diServices.set(IAgentHostCustomizationEnablementService, agentService.customizationEnablementService); - diServices.set(IAgentHostManagedSettingsService, agentService.managedSettingsService); + agentServices.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); const editArcReporterService = disposables.add(instantiationService.createInstance(EditArcReporterService, undefined)); - diServices.set(IEditArcReporterService, editArcReporterService); - diServices.set(IAgentHostGitHubEndpointService, agentService.gitHubEndpointService); - diServices.set(IAgentHostCompletions, agentService.completionsService); - diServices.set(IAgentHostCheckpointService, agentService.checkpointService); - - // CopilotApiService and the proxies that consume it are created AFTER the - // GitHub endpoint service is re-exported (above) so CAPI endpoint discovery - // can target a GitHub Enterprise host. Matches agentHostServerMain ordering. - const copilotApiService = instantiationService.createInstance(CopilotApiService, fetchFn); - diServices.set(ICopilotApiService, copilotApiService); + agentServices.set(IEditArcReporterService, editArcReporterService); // Host-owned worktree isolation controller: a single instance drives folder // / worktree isolation for every agent, so providers stay unaware of it. It // owns its branch-name generator, created from ICopilotApiService. const worktreeIsolation = disposables.add(instantiationService.createInstance(WorktreeIsolation, undefined)); - diServices.set(IAgentHostWorktreeIsolation, worktreeIsolation); + agentServices.set(IAgentHostWorktreeIsolation, worktreeIsolation); agentService.setWorktreeIsolation(worktreeIsolation); const claudeProxyService = disposables.add(instantiationService.createInstance(ClaudeProxyService)); - diServices.set(IClaudeProxyService, claudeProxyService); + agentServices.set(IClaudeProxyService, claudeProxyService); const codexProxyService = disposables.add(instantiationService.createInstance(CodexProxyService)); - diServices.set(ICodexProxyService, codexProxyService); + agentServices.set(ICodexProxyService, codexProxyService); agentService.registerProvider(instantiationService.createInstance(CopilotAgent)); // Claude and Codex providers are gated on two things: // 1. The user-facing enable toggle (`chat.agentHost.Agent.enabled`, @@ -290,6 +269,7 @@ async function startAgentHost(): Promise { disposables.add(agentConfigurationService.onDidRootConfigChange(registerCodexIfEnabled)); } } catch (err) { + rootInstantiationService?.dispose(); logService.error('Failed to create AgentService', err); throw err; } @@ -585,6 +565,7 @@ async function startAgentHost(): Promise { agentService.dispose(); logService.dispose(); disposables.dispose(); + rootInstantiationService?.dispose(); }); } diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index 0e067070e96b5a..f628f50e61c87e 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -40,7 +40,6 @@ import { INetworkDiagnosticsService, NetworkDiagnosticsService } from './network import { IByokLmBridgeRegistry, NullByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; import { IByokLmProxyService, NullByokLmProxyService } from './copilot/byokLmProxyService.js'; import { IAgentHostWorktreeIsolation, WorktreeIsolation } from './shared/worktreeIsolation.js'; -import { CopilotApiService, ICopilotApiService } from './shared/copilotApiService.js'; import { ClaudeAgent } from './claude/claudeAgent.js'; import { ClaudeAgentSdkService, ClaudeSdkPackage, IClaudeAgentSdkService } from './claude/claudeAgentSdkService.js'; import { ClaudeProxyService, IClaudeProxyService } from './claude/claudeProxyService.js'; @@ -52,18 +51,8 @@ import { IAgentHostOTelService } from '../common/otel/agentHostOTelService.js'; import { AgentHostOTelService } from './otel/agentHostOTelService.js'; import { AgentHostCodexEnabledConfigKey, platformRootSchema } from '../common/agentHostSchema.js'; import { AgentModelRefreshScheduler, MODEL_REFRESH_INTERVAL_MS } from './agentModelRefreshScheduler.js'; -import { AgentService } from './agentService.js'; -import { IAgentHostStateManager } from './agentHostStateManager.js'; -import { IAgentHostPromptCache } from './agentHostPromptCache.js'; -import { IAgentHostSessionTitleSignal } from './agentHostSessionTitleSignal.js'; -import { AgentHostClaudeAgentEnabledEnvVar, AgentHostClaudeSdkRootEnvVar, AgentHostCodexAgentEnabledEnvVar, IAgentService, AgentHostCodexAgentSdkRootEnvVar, isAgentEnabled } from '../common/agentService.js'; -import { IAgentConfigurationService } from './agentConfigurationService.js'; -import { IAgentHostStorageService } from './agentHostStorageService.js'; -import { IAgentHostCustomizationEnablementService } from './agentHostCustomizationEnablementService.js'; -import { IAgentHostManagedSettingsService } from './agentHostManagedSettingsService.js'; -import { IAgentHostGitHubEndpointService } from './agentHostGitHubEndpointService.js'; -import { IAgentHostCompletions } from './agentHostCompletions.js'; -import { IAgentHostTerminalManager } from './agentHostTerminalManager.js'; +import { createAgentHostApplication } from './agentHostApplication.js'; +import { AgentHostClaudeAgentEnabledEnvVar, AgentHostClaudeSdkRootEnvVar, AgentHostCodexAgentEnabledEnvVar, AgentHostCodexAgentSdkRootEnvVar, isAgentEnabled } from '../common/agentService.js'; import { WebSocketProtocolServer } from './webSocketTransport.js'; import { ProtocolServerHandler } from './protocolServerHandler.js'; import { AgentHostClientConnectionTelemetryTracker } from './agentHostClientConnectionTelemetry.js'; @@ -90,7 +79,6 @@ import { IAgentPluginManager } from '../common/agentPluginManager.js'; import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js'; import { AgentHostGitService } from './agentHostGitService.js'; import { IAgentHostGitService } from '../common/agentHostGitService.js'; -import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js'; import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js'; import { createAgentHostTelemetryService } from './agentHostTelemetryService.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; @@ -255,57 +243,44 @@ async function main(): Promise { const telemetryService = await createAgentHostTelemetryService({ environmentService, productService, fileService, loggerService, logService, disposables, disableTelemetry: options.quiet, fetchFn, requestService: networkServices.requestService }); errorTelemetry.value = new ErrorTelemetry(telemetryService); diServices.set(ITelemetryService, telemetryService); - const instantiationService = new InstantiationService(diServices); - const fileMonitorService = disposables.add(instantiationService.createInstance(AgentHostFileMonitorService)); + const rootInstantiationService = new InstantiationService(diServices, /*strict*/ true); + const fileMonitorService = disposables.add(rootInstantiationService.createInstance(AgentHostFileMonitorService)); diServices.set(IAgentHostFileMonitorService, fileMonitorService); - diServices.set(IWindowsMxcTerminalSandboxRuntime, instantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); + diServices.set(IWindowsMxcTerminalSandboxRuntime, rootInstantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); diServices.set(ISandboxHelperService, new SandboxHelperService()); - const gitService = instantiationService.createInstance(AgentHostGitService); + const gitService = rootInstantiationService.createInstance(AgentHostGitService); diServices.set(IAgentHostGitService, gitService); - // Create the agent service (owns AgentHostStateManager + AgentSideEffects internally) - const agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, rootConfigResource, telemetryService, fileMonitorService, undefined, fetchFn, [createCodexProviderConfiguration(environmentService.userHome)], AgentHostLaunchKind.VSCodeCLI, storageResource); + const application = createAgentHostApplication(rootInstantiationService, { + rootConfigResource, + providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], + hostLaunchKind: AgentHostLaunchKind.VSCodeCLI, + storageResource, + }); + const { agentService, instantiationService, services: agentServices } = application; disposables.add(agentService); - diServices.set(IAgentService, agentService); - diServices.set(IAgentHostStateManager, agentService.stateManager); - // Narrow host seams providers consume instead of the whole state manager. - diServices.set(IAgentHostPromptCache, agentService.promptCache); - diServices.set(IAgentHostSessionTitleSignal, agentService.sessionTitleSignal); - diServices.set(IAgentHostManagedSettingsService, agentService.managedSettingsService); const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); - diServices.set(INetworkDiagnosticsService, networkDiagnosticsService); + agentServices.set(INetworkDiagnosticsService, networkDiagnosticsService); agentService.setNetworkDiagnosticsService(networkDiagnosticsService); - diServices.set(IAgentHostStorageService, agentService.storageService); - diServices.set(IAgentHostCustomizationEnablementService, agentService.customizationEnablementService); - diServices.set(IAgentHostGitHubEndpointService, agentService.gitHubEndpointService); // Register agents let sdkDownloadProgress: Event | undefined; if (!options.quiet) { // Production agents (require DI) const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService); - diServices.set(IAgentPluginManager, pluginManager); - diServices.set(IDiffComputeService, disposables.add(new NodeWorkerDiffComputeService(logService))); + agentServices.set(IAgentPluginManager, pluginManager); + agentServices.set(IDiffComputeService, disposables.add(new NodeWorkerDiffComputeService(logService))); const editAttributionService = disposables.add(instantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); - diServices.set(IAgentEditAttributionService, editAttributionService); + agentServices.set(IAgentEditAttributionService, editAttributionService); agentService.setEditAttributionService(editAttributionService); - diServices.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); - diServices.set(IAgentHostTerminalManager, agentService.terminalManager); - diServices.set(IAgentConfigurationService, agentService.configurationService); + agentServices.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); const editArcReporterService = disposables.add(instantiationService.createInstance(EditArcReporterService, undefined)); - diServices.set(IEditArcReporterService, editArcReporterService); - diServices.set(IAgentHostCompletions, agentService.completionsService); - diServices.set(IAgentHostCheckpointService, agentService.checkpointService); - diServices.set(IAgentHostGitService, gitService); - // Register `ICopilotApiService` BEFORE `IClaudeProxyService` — - // the proxy service constructor requires it. - const copilotApiService = instantiationService.createInstance(CopilotApiService, fetchFn); - diServices.set(ICopilotApiService, copilotApiService); + agentServices.set(IEditArcReporterService, editArcReporterService); // Host-owned worktree isolation controller: a single instance drives folder // / worktree isolation for every agent, so providers stay unaware of it. It // owns its branch-name generator, created from ICopilotApiService. const worktreeIsolation = disposables.add(instantiationService.createInstance(WorktreeIsolation, undefined)); - diServices.set(IAgentHostWorktreeIsolation, worktreeIsolation); + agentServices.set(IAgentHostWorktreeIsolation, worktreeIsolation); agentService.setWorktreeIsolation(worktreeIsolation); // CLI flags become env vars BEFORE the downloader is constructed so // `isAvailable()` and `loadSdkRoot()` see them as dev overrides. @@ -317,21 +292,21 @@ async function main(): Promise { } // Register the agent SDK downloader BEFORE any service that injects it. const agentSdkDownloader = disposables.add(instantiationService.createInstance(AgentSdkDownloader)); - diServices.set(IAgentSdkDownloader, agentSdkDownloader); + agentServices.set(IAgentSdkDownloader, agentSdkDownloader); sdkDownloadProgress = agentSdkDownloader.onDidDownloadProgress; const claudeProxyService = disposables.add(instantiationService.createInstance(ClaudeProxyService)); - diServices.set(IClaudeProxyService, claudeProxyService); + agentServices.set(IClaudeProxyService, claudeProxyService); const claudeAgentSdkService = instantiationService.createInstance(ClaudeAgentSdkService); - diServices.set(IClaudeAgentSdkService, claudeAgentSdkService); + agentServices.set(IClaudeAgentSdkService, claudeAgentSdkService); const codexProxyService = disposables.add(instantiationService.createInstance(CodexProxyService)); - diServices.set(ICodexProxyService, codexProxyService); + agentServices.set(ICodexProxyService, codexProxyService); const agentHostOTelService = disposables.add(instantiationService.createInstance(AgentHostOTelService, fetchFn)); - diServices.set(IAgentHostOTelService, agentHostOTelService); + agentServices.set(IAgentHostOTelService, agentHostOTelService); // BYOK is unsupported in the remote agent host (no extension host runs // next to it to serve the renderer LM API). Inject null implementations // to satisfy CopilotAgent / CopilotSessionLauncher DI. - diServices.set(IByokLmBridgeRegistry, new NullByokLmBridgeRegistry()); - diServices.set(IByokLmProxyService, new NullByokLmProxyService()); + agentServices.set(IByokLmBridgeRegistry, new NullByokLmBridgeRegistry()); + agentServices.set(IByokLmProxyService, new NullByokLmProxyService()); const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent)); agentService.registerProvider(copilotAgent); log('CopilotAgent registered'); @@ -500,6 +475,7 @@ async function main(): Promise { logService.warn('[AgentHostServer] Timed out waiting for persistence writes to flush; exiting anyway.'); }); disposables.dispose(); + rootInstantiationService.dispose(); loggerService?.dispose(); process.exit(0); } diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 11f8d6b7e91d8f..a960d985285ae4 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -19,7 +19,7 @@ import { generateUuid } from '../../../base/common/uuid.js'; import { hasKey } from '../../../base/common/types.js'; import { localize } from '../../../nls.js'; import { FileChangeType, FileOperationResult, IFileChange, IFileService, toFileOperationResult, type FileChangesEvent } from '../../files/common/files.js'; -import { InstantiationService } from '../../instantiation/common/instantiationService.js'; +import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; import { ILogService } from '../../log/common/log.js'; import { AgentProvider, AgentSession, AgentSignal, IAgent, IAgentChatContext, IAgentChatDataChange, IAgentChatMetadata, IAgentCreateChatOptions, IAgentCreateChatResult, IAgentCreateChatSideChatSelection, IAgentCreateChatSideChatSource, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDiscoveredChat, IAgentHostAuthTokenRequest, IAgentHostNetworkEndpoint, IAgentMaterializeChatEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentChatAdoptionResult, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSpawnChatEvent, AuthenticateParams, AuthenticateResult, IMcpNotification, SubagentChatSignal, subagentChatTitle } from '../common/agent.js'; @@ -42,7 +42,7 @@ import { readToolCallMeta } from '../common/meta/agentToolCallMeta.js'; import { IProductService } from '../../product/common/productService.js'; import { buildBoundedSideChatSourceContext, getSideChatPartialResponse } from './agentPeerChats.js'; import { AgentConfigurationService, getEffectiveWorkingDirectories, IAgentConfigurationService } from './agentConfigurationService.js'; -import { AgentHostManagedSettingsService, type IAgentHostManagedSettingsService } from './agentHostManagedSettingsService.js'; +import { AgentHostManagedSettingsService, IAgentHostManagedSettingsService } from './agentHostManagedSettingsService.js'; import { AgentHostTerminalManager, IAgentHostTerminalManager } from './agentHostTerminalManager.js'; import { ISessionDbUriFields, parseSessionDbUri } from '../common/sessionDbUri.js'; import { IGitBlobUriFields, parseGitBlobUri } from './gitDiffContent.js'; @@ -64,7 +64,6 @@ import { AGENT_HOST_TITLE_SOURCE_AGENT, customChatTitleMetadataKey, customChatTi import { buildWorktreeFailureNotification, WorktreeIsolation, WORKTREE_META_REPOSITORY_ROOT, worktreeProjectFromRepositoryRoot } from './shared/worktreeIsolation.js'; import { AgentHostChangesetService } from './agentHostChangesetService.js'; -import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js'; import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js'; import { IAgentHostReviewService } from '../common/agentHostReviewService.js'; import { AgentHostChangesetCoordinator } from './agentHostChangesetCoordinator.js'; @@ -89,7 +88,6 @@ import { AgentMergeController } from './agentMergeController.js'; import { AgentMergeConfigKey, agentMergeRootConfigSchema } from '../common/agentMerge.js'; import { AgentMergeTools } from './agentMergeTools.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; -import { NullTelemetryService } from '../../telemetry/common/telemetryUtils.js'; import { AgentHostAuthenticationService } from './agentHostAuthenticationService.js'; import { updateAgentHostTelemetryLevelFromConfig } from './agentHostTelemetryService.js'; import { AgentHostActiveAgentTitleGenerationConfigKey, AgentHostEditTelemetryEnabledConfigKey, AgentHostExternalSessionsMode, AgentHostMigrateLegacyCopilotCliEnabledConfigKey, AgentHostShowExternalSessionsConfigKey, platformRootSchema } from '../common/agentHostSchema.js'; @@ -103,6 +101,7 @@ import { AgentHostChangesetSubscriptionService } from './agentHostChangesetSubsc import { GIT_DB_METADATA_KEYS, IAgentHostGitStateService, META_GIT_STATE, META_GITHUB_STATE, META_SOURCE_CONTROL_STATE } from '../common/agentHostGitStateService.js'; import { IAgentHostChangesetOperationService } from '../common/agentHostChangesetOperationService.js'; import { AgentHostCommitOperationContribution } from './agentHostCommitOperationProvider.js'; +import { IAgentHostProxyResolver } from './agentHostProxyResolver.js'; import { AgentHostDiscardChangesOperationContribution } from './agentHostDiscardChangesOperationProvider.js'; import { AgentHostMergeOperationContribution } from './agentHostMergeOperationProvider.js'; import { AgentHostPullRequestOperationContribution } from './agentHostPullRequestOperationProvider.js'; @@ -308,6 +307,16 @@ function reconcileWorkingDirectories(requested: readonly URI[] | undefined, reso return [...resolved, ...tail].map(d => d.toString()); } +export interface IAgentServiceOptions { + readonly rootConfigResource?: URI; + readonly copilotApiService?: ICopilotApiService; + readonly providerConfigurations?: readonly IAgentCustomizationSettingsRegistration[]; + readonly hostLaunchKind?: AgentHostLaunchKind; + readonly storageResource?: URI; + readonly orchestratorDatabase?: IAgentHostDatabase; + readonly now?: () => number; +} + /** * The agent service implementation that runs inside the agent-host utility * process. Dispatches to registered {@link IAgent} instances based @@ -455,12 +464,16 @@ export class AgentService extends Disposable implements IAgentService { private _worktree: WorktreeIsolation | undefined; /** Single source of truth for GitHub (Enterprise) endpoints and protected resources. */ private readonly _gitHubEndpointService: IAgentHostGitHubEndpointService; + private readonly _copilotApiService: ICopilotApiService; /** Pluggable completion item providers (e.g. workspace file completions, agent-specific @-mentions). */ private readonly _completions: IAgentHostCompletions; private _skillCompletionProviderRegistered = false; /** Backs {@link getNetworkDiagnosticsInfo} / {@link diagnosticsFetch}; wired via {@link setNetworkDiagnosticsService}. */ private _networkDiagnostics: INetworkDiagnosticsService | undefined; private _editAttributionService: IAgentEditAttributionService | undefined; + private readonly _rootConfigResource: URI | undefined; + private readonly _hostLaunchKind: AgentHostLaunchKind; + private readonly _now: () => number; /** * Authoritative server-side per-resource subscription refcount, keyed by @@ -543,29 +556,28 @@ export class AgentService extends Disposable implements IAgentService { get completionTriggerCharacters(): readonly string[] { return this._completions.triggerCharacters; } constructor( - private readonly _logService: ILogService, - private readonly _fileService: IFileService, - private readonly _sessionDataService: ISessionDataService, - private readonly _productService: IProductService, - private readonly _gitService: IAgentHostGitService, - private readonly _rootConfigResource?: URI, - private readonly _telemetryService: ITelemetryService = NullTelemetryService, - _fileMonitorService?: IAgentHostFileMonitorService, - copilotApiService?: ICopilotApiService, - fetchFn?: typeof globalThis.fetch, - providerConfigurations: readonly IAgentCustomizationSettingsRegistration[] = [], - private readonly _hostLaunchKind = AgentHostLaunchKind.Unknown, - storageResource?: URI, - orchestratorDatabase?: IAgentHostDatabase, - private readonly _now: () => number = Date.now, + options: IAgentServiceOptions, + services: ServiceCollection, + @IInstantiationService instantiationService: IInstantiationService, + @ILogService private readonly _logService: ILogService, + @IFileService private readonly _fileService: IFileService, + @ISessionDataService private readonly _sessionDataService: ISessionDataService, + @IProductService private readonly _productService: IProductService, + @IAgentHostGitService private readonly _gitService: IAgentHostGitService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IAgentHostProxyResolver proxyResolver: IAgentHostProxyResolver, ) { super(); + this._rootConfigResource = options.rootConfigResource; + this._hostLaunchKind = options.hostLaunchKind ?? AgentHostLaunchKind.Unknown; + this._now = options.now ?? Date.now; + const fetchFn = proxyResolver.fetch.bind(proxyResolver); this._logService.info('AgentService initialized'); this._authService = new AgentHostAuthenticationService(_logService); const databasePath = this._rootConfigResource ? joinPath(resourcesDirname(this._rootConfigResource), 'agent-host.db').fsPath : ':memory:'; - this._orchestratorDatabase = this._register(orchestratorDatabase ?? new AgentHostDatabase(databasePath)); + this._orchestratorDatabase = this._register(options.orchestratorDatabase ?? new AgentHostDatabase(databasePath)); this._sessionRegistry = this._register(new AgentSessionRegistry(this._orchestratorDatabase)); this._stateManager = this._register(new AgentHostStateManager(_logService, { hostBuildInfo: hostBuildInfoFromProduct(this._productService), @@ -593,7 +605,7 @@ export class AgentService extends Disposable implements IAgentService { // Build a local instantiation scope so downstream components can // consume {@link IAgentConfigurationService} (and later {@link ILogService}) // via DI rather than being plumbed plain-class references. - const configurationService = this._register(new AgentConfigurationService(this._stateManager, this._logService, this._rootConfigResource, providerConfigurations)); + const configurationService = this._register(new AgentConfigurationService(this._stateManager, this._logService, this._rootConfigResource, options.providerConfigurations ?? [])); this._configurationService = configurationService; let externalSessionsMode = this._getExternalSessionsMode(); this._lastMigrateLegacyEnabled = this._isMigrateLegacyEnabled(); @@ -617,26 +629,13 @@ export class AgentService extends Disposable implements IAgentService { } this._onMigrateLegacySettingChanged(); })); - const fileMonitorService = _fileMonitorService ?? this._register(new AgentHostFileMonitorService(this._fileService, this._logService)); - this._storageService = this._register(new AgentHostStorageService(storageResource, this._logService)); + this._storageService = this._register(new AgentHostStorageService(options.storageResource, this._logService)); updateAgentHostTelemetryLevelFromConfig(this._telemetryService, this._stateManager.rootState.config?.values); - const services = new ServiceCollection( - [ILogService, this._logService], - [IAgentService, this], - [IProductService, this._productService], - [IAgentConfigurationService, configurationService], - [IAgentHostStateManager, this._stateManager], - [IAgentHostFileMonitorService, fileMonitorService], - [IAgentHostGitService, this._gitService], - [IAgentHostStorageService, this._storageService], - [ITelemetryService, this._telemetryService], - // The outer agent-host process DI registers `ISessionDataService`, - // but this nested strict `InstantiationService` does not inherit it. - // Add it explicitly so `@ISessionDataService` injection into the - // changeset service (and any future sibling) resolves correctly. - [ISessionDataService, this._sessionDataService], - ); - const instantiationService = this._register(new InstantiationService(services, /*strict*/ true)); + services.set(IAgentService, this); + services.set(IAgentConfigurationService, configurationService); + services.set(IAgentHostStateManager, this._stateManager); + services.set(IAgentHostStorageService, this._storageService); + services.set(IAgentHostManagedSettingsService, this._managedSettingsService); this._gitHubEndpointService = this._register(instantiationService.createInstance(AgentHostGitHubEndpointService)); services.set(IAgentHostGitHubEndpointService, this._gitHubEndpointService); // A GitHub Enterprise URI change repoints every agent's GitHub resource @@ -665,8 +664,8 @@ export class AgentService extends Disposable implements IAgentService { fetch: fetchFn, })); services.set(IGitHubService, gitHubService); - const effectiveCopilotApiService = copilotApiService ?? instantiationService.createInstance(CopilotApiService, fetchFn); - services.set(ICopilotApiService, effectiveCopilotApiService); + this._copilotApiService = options.copilotApiService ?? instantiationService.createInstance(CopilotApiService, fetchFn); + services.set(ICopilotApiService, this._copilotApiService); this._customizationEnablementService = this._register(instantiationService.createInstance(AgentHostCustomizationEnablementService)); services.set(IAgentHostCustomizationEnablementService, this._customizationEnablementService); @@ -716,6 +715,7 @@ export class AgentService extends Disposable implements IAgentService { this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostDiscardChangesOperationContribution))); this._completions = this._register(instantiationService.createInstance(AgentHostCompletions)); + services.set(IAgentHostCompletions, this._completions); // Built-in generic provider: completes files in the session's workspace folder. const workspaceFiles = this._register(instantiationService.createInstance(AgentHostWorkspaceFiles)); this._register(this._completions.registerProvider( @@ -756,7 +756,7 @@ export class AgentService extends Disposable implements IAgentService { localTurns: this._localTurns, agents: this._agents, hostLaunchKind: this._hostLaunchKind, - copilotApiService: effectiveCopilotApiService, + copilotApiService: this._copilotApiService, getGitHubCopilotToken: () => { return this.getAuthToken({ resource: this._gitHubEndpointService.getCopilotResource().resource, diff --git a/src/vs/platform/agentHost/test/node/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts index c425b3ed923224..067c951ed81bb6 100644 --- a/src/vs/platform/agentHost/test/node/agentService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentService.test.ts @@ -60,6 +60,7 @@ import type { INetworkDiagnosticsService } from '../../node/networkDiagnosticsSe import type { IAgentServerToolHost } from '../../common/agentServerTools.js'; import { SessionServerToolName } from '../../common/serverToolNames.js'; import { buildMcpChannel } from '../../node/shared/mcpCustomizationController.js'; +import { createTestAgentService } from './agentServiceTestUtils.js'; /** * Replace individual operations on an agent's chat surface, delegating every @@ -354,7 +355,7 @@ suite('AgentService (node dispatcher)', () => { await fileService.createFolder(URI.from({ scheme: Schemas.inMemory, path: '/testDir' })); await fileService.writeFile(URI.from({ scheme: Schemas.inMemory, path: '/testDir/file.txt' }), VSBuffer.fromString('hello')); - service = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + service = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); copilotAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => copilotAgent.dispose())); }); @@ -572,7 +573,7 @@ suite('AgentService (node dispatcher)', () => { gitService.revParse = async () => 'head'; gitService.getCurrentBranch = async () => 'feature'; gitService.getDefaultBranch = async () => ({ name: 'main', startPoint: 'main' }); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); localService.setWorktreeIsolation(disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/test' }, gitService, @@ -618,7 +619,7 @@ suite('AgentService (node dispatcher)', () => { gitService.revParse = async () => 'head'; gitService.getCurrentBranch = async () => 'feature'; gitService.getDefaultBranch = async () => ({ name: 'main', startPoint: 'origin/main' }); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); localService.setWorktreeIsolation(disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/test' }, gitService, @@ -747,7 +748,7 @@ suite('AgentService (node dispatcher)', () => { gitService.revParse = async () => 'head'; gitService.getCurrentBranch = async () => 'feature'; gitService.getDefaultBranch = async () => ({ name: 'main', startPoint: 'main' }); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const isolation = disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/test' }, gitService, @@ -807,7 +808,7 @@ suite('AgentService (node dispatcher)', () => { test('createSession validates, exposes, persists, and inherits multi-root metadata', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); localService.registerProvider(agent); @@ -878,7 +879,7 @@ suite('AgentService (node dispatcher)', () => { } const agent = new RejectingFolderPickerAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(agent); const session = await localService.createSession({ @@ -904,7 +905,7 @@ suite('AgentService (node dispatcher)', () => { } const agent = new PinningFolderPickerAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(agent); const session = await localService.createSession({ @@ -938,7 +939,7 @@ suite('AgentService (node dispatcher)', () => { const db = new TestSessionDatabase(); // Create writes the frozen decision into the session DB (non-provisional). - const creating = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const creating = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const creatingAgent = new DecidingFolderPickerAgent('copilot'); creatingAgent.decision = decision; disposables.add(toDisposable(() => creatingAgent.dispose())); @@ -950,7 +951,7 @@ suite('AgentService (node dispatcher)', () => { // Reopen: a fresh service on the same DB rediscovers the provider-native // session and must restore the persisted decision into `_meta`. - const reopened = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const reopened = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); reopened.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); const reopenedAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => reopenedAgent.dispose())); @@ -994,7 +995,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new TestSessionDatabase(); - const creating = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const creating = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new ProvisionalDecidingAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); creating.registerProvider(agent); @@ -1008,7 +1009,7 @@ suite('AgentService (node dispatcher)', () => { agent.materialize(session, [URI.file('/work/one'), URI.file('/work/two')]); await timeout(0); - const reopened = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const reopened = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); reopened.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); const reopenedAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => reopenedAgent.dispose())); @@ -1049,7 +1050,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new ProvisionalAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); localService.registerProvider(agent); @@ -1099,7 +1100,7 @@ suite('AgentService (node dispatcher)', () => { test('reconciles pending worktree isolation when creating session config changes', async () => { const gitService = createNoopGitService(); const sessionDataService = createSessionDataService(new TestSessionDatabase()); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const isolation = disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/test' }, gitService, @@ -1240,7 +1241,7 @@ suite('AgentService (node dispatcher)', () => { return { provider: this.id, displayName: this.id, description: this.id, capabilities: { multipleWorkingDirectories: { immutablePrimary: true } } }; } } - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const agent = new MultiRootMockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); localService.registerProvider(agent); @@ -1300,7 +1301,7 @@ suite('AgentService (node dispatcher)', () => { const repoA = URI.file('/workspace/repoA'); const showBlobCalls: Array<{ workingDirectory: string; ref: string; repoRelativePath: string }> = []; const gitService = createBlobGitService(new Map([[repoA.toString(), repoA]]), showBlobCalls); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const agent = new MockAgent('copilot'); agent.sessionMetadataOverrides = { workingDirectories: [repoA] }; disposables.add(toDisposable(() => agent.dispose())); @@ -1328,7 +1329,7 @@ suite('AgentService (node dispatcher)', () => { const repoA = URI.file('/workspace/repoA'); const showBlobCalls: Array<{ workingDirectory: string; ref: string; repoRelativePath: string }> = []; const gitService = createBlobGitService(new Map([[repoA.toString(), repoA]]), showBlobCalls); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const agent = new MockAgent('copilot'); agent.sessionMetadataOverrides = { workingDirectories: [repoA] }; disposables.add(toDisposable(() => agent.dispose())); @@ -1420,7 +1421,7 @@ suite('AgentService (node dispatcher)', () => { async function setupTitleGeneration(copilotApiService: TestCopilotApiService, activeAgentTitleGeneration = false): Promise<{ svc: AgentService; agent: MockAgent; session: URI; db: TestSessionDatabase }> { const db = new TestSessionDatabase(); const sessionDataService = createSessionDataService(db); - const svc = disposables.add(new AgentService( + const svc = disposables.add(createTestAgentService( new NullLogService(), fileService, sessionDataService, @@ -1463,7 +1464,7 @@ suite('AgentService (node dispatcher)', () => { } async function createDynamicWorkingDirectorySession(immutablePrimary = true): Promise<{ svc: AgentService; session: URI; primary: URI; secondary: URI }> { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new DynamicWorkingDirectoryAgent('dynamic', immutablePrimary); disposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -1478,7 +1479,7 @@ suite('AgentService (node dispatcher)', () => { } test('rejects a turn id already used by another chat before applying it', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -1522,7 +1523,7 @@ suite('AgentService (node dispatcher)', () => { }); test('rejects a turn id used by an unresolved restored peer before applying it', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -1592,7 +1593,7 @@ suite('AgentService (node dispatcher)', () => { }); test('rejects client writes to host-owned Agent Merge controller state', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -1620,7 +1621,7 @@ suite('AgentService (node dispatcher)', () => { }); test('preserves host-owned Agent Merge controller state across a client config replacement', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -1647,7 +1648,7 @@ suite('AgentService (node dispatcher)', () => { }); test('accepts client writes to the client-owned Agent Merge enablement value', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -1691,7 +1692,7 @@ suite('AgentService (node dispatcher)', () => { test('rejects a failed review update and clears the client dispatch queue', async () => { const db = new TestSessionDatabase(); db.getMetadata = async () => { throw new Error('metadata unavailable'); }; - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -1880,7 +1881,7 @@ suite('AgentService (node dispatcher)', () => { const localDisposables = new DisposableStore(); try { const rootConfigResource = joinPath(tempDir, 'agent-host-config.json'); - const svc = localDisposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService(), rootConfigResource)); + const svc = localDisposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService(), rootConfigResource)); const agent = new MockAgent('copilot'); localDisposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -2180,7 +2181,7 @@ suite('AgentService (node dispatcher)', () => { const logService = new class extends NullLogService { override warn(message: string): void { warnings.push(message); } }; - const svc = disposables.add(new AgentService(logService, fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(logService, fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -2603,7 +2604,7 @@ suite('AgentService (node dispatcher)', () => { test('retries a transient registry registration failure before reporting creation success', async () => { const db = new TransientRegistryWriteDatabase(); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, db)); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, db)); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); await svc.listSessions(); @@ -2644,7 +2645,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new FailingProviderDataDatabase(); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new BackedDefaultChatAgent('copilot')); svc.registerProvider(agent); @@ -2695,7 +2696,7 @@ suite('AgentService (node dispatcher)', () => { ...nullSessionDataService, deleteSessionData: async () => { order.push('deleteSessionData'); }, }; - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.registerProvider(copilotAgent); const session = await svc.createSession({ provider: 'copilot' }); const workingDirectoryPendingChange = disposables.add(new Emitter()); @@ -2721,7 +2722,7 @@ suite('AgentService (node dispatcher)', () => { ...nullSessionDataService, deleteSessionData: async () => { deletedSessionData = true; }, }; - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.registerProvider(copilotAgent); const session = await svc.createSession({ provider: 'copilot' }); svc.setWorktreeIsolation({ @@ -2751,7 +2752,7 @@ suite('AgentService (node dispatcher)', () => { ...createSessionDataService(), deleteSessionData: async () => { deleteSessionDataCalls++; }, }; - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, db)); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, db)); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); const session = await svc.createSession({ provider: 'copilot' }); @@ -2825,7 +2826,7 @@ suite('AgentService (node dispatcher)', () => { } function createExternalSessionService(now: () => number, sessionDataService = createSessionDataService()): AgentService { - return disposables.add(new AgentService( + return disposables.add(createTestAgentService( new NullLogService(), fileService, sessionDataService, @@ -2866,7 +2867,7 @@ suite('AgentService (node dispatcher)', () => { test('listSessions discovers provider-native sessions as external and restore preserves provenance', async () => { const db = new TestSessionDatabase(); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); @@ -2892,7 +2893,7 @@ suite('AgentService (node dispatcher)', () => { test('rediscovery does not overwrite durable unread state for an existing external session', async () => { const db = new TestSessionDatabase(); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); const session = AgentSession.uri('copilot', 'rediscovered-external'); (agent as unknown as { _sessions: Map })._sessions.set(AgentSession.id(session), session); @@ -3225,7 +3226,7 @@ suite('AgentService (node dispatcher)', () => { }); test('discovery registration preserves provider-supplied internal provenance', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); await svc.listSessions(); @@ -3247,7 +3248,7 @@ suite('AgentService (node dispatcher)', () => { }); test('discovery announces a registered session with provider metadata intact', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); svc.configurationService.updateRootConfig({ [AgentHostMigrateLegacyCopilotCliEnabledConfigKey]: true }); @@ -3279,7 +3280,7 @@ suite('AgentService (node dispatcher)', () => { test('rediscovering a registered chat with different provenance performs no per-session database I/O', async () => { const perSession = createPerSessionDataService(); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, perSession.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, perSession.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); const session = AgentSession.uri('copilot', 'known-discovered'); @@ -3303,7 +3304,7 @@ suite('AgentService (node dispatcher)', () => { }); test('the known-sessions filter reports registered sessions only, leaving tombstones to registration', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); const registered = AgentSession.uri('copilot', 'filter-registered'); @@ -3328,7 +3329,7 @@ suite('AgentService (node dispatcher)', () => { }); test('concurrent listSessions calls share one computation and never share their result array', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); await svc.createSession({ provider: 'copilot' }); @@ -3362,7 +3363,7 @@ suite('AgentService (node dispatcher)', () => { }); test('a registry mutation during an in-flight list is not served from the shared computation', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); const gate = new DeferredPromise(); @@ -3392,7 +3393,7 @@ suite('AgentService (node dispatcher)', () => { }); test('provider registration invalidates an in-flight list computation', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const gate = new DeferredPromise(); const inner = svc as unknown as { _computeSessions(mode: AgentHostExternalSessionsMode): Promise }; const original = inner._computeSessions; @@ -3455,7 +3456,7 @@ suite('AgentService (node dispatcher)', () => { const legacy = AgentSession.uri('copilot', 'legacy-catalog'); const sessionData = createPerSessionDataService(); await sessionData.database(legacy).setMetadata(AH_META_WORKSPACELESS_DB_KEY, 'false'); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new SeparateCatalogAgent('copilot')); svc.registerProvider(agent); await svc.listSessions(); @@ -3484,7 +3485,7 @@ suite('AgentService (node dispatcher)', () => { }); test('one invalid discovered chat does not block sibling registration', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); await svc.listSessions(); @@ -3508,7 +3509,7 @@ suite('AgentService (node dispatcher)', () => { }); test('failed discovery announcement releases its deduplication reservation', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); await svc.listSessions(); @@ -3552,7 +3553,7 @@ suite('AgentService (node dispatcher)', () => { const external = AgentSession.uri('copilot', 'migration-external'); const sessionData = createPerSessionDataService(); await sessionData.database(restored).setMetadata(AH_META_WORKSPACELESS_DB_KEY, 'true'); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.registerProvider(disposables.add(new MixedMigrationAgent('copilot'))); await svc.listSessions(); @@ -3575,7 +3576,7 @@ suite('AgentService (node dispatcher)', () => { database.addSessionWithoutExternal({ session: external.toString(), provider: 'claude', startTime: 2, external: false, source: 'explicit' }); const sessionData = createPerSessionDataService(); await sessionData.database(internal).setMetadata(AH_META_WORKSPACELESS_DB_KEY, 'false'); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, database)); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, database)); await svc.getRegisteredSessions(); await svc.getRegisteredSessions(); @@ -3619,7 +3620,7 @@ suite('AgentService (node dispatcher)', () => { legacyDatabase = undefined; database = new AgentHostDatabase(path); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, database)); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, database)); const agent = disposables.add(new MockAgent('copilot')); const session = AgentSession.uri('copilot', 'legacy-real-database'); (agent as unknown as { _sessions: Map })._sessions.set(AgentSession.id(session), session); @@ -3646,7 +3647,7 @@ suite('AgentService (node dispatcher)', () => { return super.listExternalChats(); } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new CountingAgent('copilot')); const native = AgentSession.uri('copilot', 'native-disappeared'); (agent as unknown as { _sessions: Map })._sessions.set(AgentSession.id(native), native); @@ -3677,7 +3678,7 @@ suite('AgentService (node dispatcher)', () => { return super.listExternalChats(); } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); const agent = disposables.add(new GatedListAgent('copilot')); svc.registerProvider(agent); @@ -3727,7 +3728,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); const agent = disposables.add(new TransientListFailureAgent('copilot')); svc.registerProvider(agent); @@ -3748,7 +3749,7 @@ suite('AgentService (node dispatcher)', () => { }); test('a late-registered provider gets its own native discovery pass', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); const early = disposables.add(new MockAgent('copilot')); svc.registerProvider(early); @@ -3791,7 +3792,7 @@ suite('AgentService (node dispatcher)', () => { } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new LateEnumerableAgent('copilot')); svc.registerProvider(agent); @@ -3824,7 +3825,7 @@ suite('AgentService (node dispatcher)', () => { return undefined; } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new AdoptableLegacyAgent('copilot')); const legacy = AgentSession.uri('copilot', 'adoptable-legacy'); (agent as unknown as { _sessions: Map })._sessions.set(AgentSession.id(legacy), legacy); @@ -3846,7 +3847,7 @@ suite('AgentService (node dispatcher)', () => { }); test('does not surface a discovered session that was already deleted', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); const legacy = AgentSession.uri('copilot', 'deleted-adoptable-legacy'); svc.registerProvider(agent); @@ -3886,7 +3887,7 @@ suite('AgentService (node dispatcher)', () => { super.dispose(); } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); const providerA = disposables.add(new CountingAgent('copilot')); const providerB = disposables.add(new FailingThenRecoveringAgent('other')); @@ -3937,7 +3938,7 @@ suite('AgentService (node dispatcher)', () => { super.dispose(); } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); const agent = disposables.add(new NotYetEnumerableAgent('copilot')); const originalListExternalChats = agent.listExternalChats.bind(agent); @@ -3970,7 +3971,7 @@ suite('AgentService (node dispatcher)', () => { migrationCalls = 0; enumerable = false; } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); const agent = disposables.add(new NotYetMigratableAgent('copilot')); const legacy = AgentSession.uri('copilot', 'legacy-migration-not-ready'); @@ -4028,7 +4029,7 @@ suite('AgentService (node dispatcher)', () => { super.dispose(); } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new GatedListAgent('copilot')); svc.registerProvider(agent); @@ -4071,7 +4072,7 @@ suite('AgentService (node dispatcher)', () => { // downgrade to pre-per-provider code reading a prematurely-set // global marker would then silently skip that late provider's // legacy sessions forever. - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const early = disposables.add(new MockAgent('copilot')); svc.registerProvider(early); await svc.listSessions(); @@ -4102,7 +4103,7 @@ suite('AgentService (node dispatcher)', () => { super.dispose(); } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new ChatListChangeAgent('copilot')); svc.registerProvider(agent); @@ -4130,7 +4131,7 @@ suite('AgentService (node dispatcher)', () => { }); test('an explicit create at a previously-deleted session URI clears its tombstone and allows reuse', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); const reusedUri = AgentSession.uri('copilot', 'reused-after-delete'); @@ -4177,7 +4178,7 @@ suite('AgentService (node dispatcher)', () => { super.dispose(); } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new GatedListAgent('copilot')); svc.registerProvider(agent); await svc.listSessions(); @@ -4217,7 +4218,7 @@ suite('AgentService (node dispatcher)', () => { const db = new TransientRegistryWriteDatabase(); // Simulate an old database whose legacy one-time marker is set. await db.markSessionRegistryBackfilled(); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, db)); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, db)); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); const agent = disposables.add(new CountingAgent('copilot')); const legacy = AgentSession.uri('copilot', 'old-db-native-session'); @@ -4260,7 +4261,7 @@ suite('AgentService (node dispatcher)', () => { super.dispose(); } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new SequentiallyGatedListAgent('copilot')); svc.registerProvider(agent); @@ -4320,7 +4321,7 @@ suite('AgentService (node dispatcher)', () => { super.dispose(); } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new SequentiallyGatedListAgent('copilot')); svc.registerProvider(agent); @@ -4360,7 +4361,7 @@ suite('AgentService (node dispatcher)', () => { return this.dropFromList ? undefined : super.getSessionMetadata(session); } } - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new FlakyListAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -4377,7 +4378,7 @@ suite('AgentService (node dispatcher)', () => { }); test('session registry stays in parity with listSessions across create/delete', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); svc.registerProvider(agent); @@ -4431,7 +4432,7 @@ suite('AgentService (node dispatcher)', () => { // Manually add the session to the mock (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); svc.registerProvider(agent); @@ -4477,7 +4478,7 @@ suite('AgentService (node dispatcher)', () => { }; (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); svc.registerProvider(agent); @@ -4500,7 +4501,7 @@ suite('AgentService (node dispatcher)', () => { _meta: { multiRoot: { workspaceFile: 'file:///provider-spoof.code-workspace' } }, }; (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); svc.registerProvider(agent); @@ -4521,7 +4522,7 @@ suite('AgentService (node dispatcher)', () => { const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); svc.registerProvider(agent); @@ -4539,7 +4540,7 @@ suite('AgentService (node dispatcher)', () => { _meta: { multiRoot: { workspaceFile: 'file:///provider-spoof.code-workspace' } }, }; (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); svc.registerProvider(agent); @@ -4565,7 +4566,7 @@ suite('AgentService (node dispatcher)', () => { worktreeRootResolutions++; return []; }; - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, gitService)); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, gitService)); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); svc.registerProvider(agent); @@ -4600,7 +4601,7 @@ suite('AgentService (node dispatcher)', () => { const gitService = createNoopGitService(); gitService.getWorktreeRoots = async () => [primaryRoot, linkedCheckout, sessionWorktree]; const sessionDataService = createSessionDataService(db); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); svc.setWorktreeIsolation(disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/test' }, @@ -4647,7 +4648,7 @@ suite('AgentService (node dispatcher)', () => { gitService.getCurrentBranch = async () => undefined; gitService.getDefaultBranch = async () => ({ name: 'main', startPoint: 'main' }); const sessionDataService = createSessionDataService(db); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.All }); svc.setWorktreeIsolation(disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/test' }, @@ -4879,7 +4880,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.registerProvider(agent); const sessions = await svc.listSessions(); @@ -4922,7 +4923,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.registerProvider(agent); const sessions = await svc.listSessions(); @@ -4958,7 +4959,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.registerProvider(agent); const sessions = await svc.listSessions(); @@ -5013,7 +5014,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.registerProvider(agent); // Seed live changeset state directly: a single file with @@ -5082,7 +5083,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.registerProvider(agent); // Seed a ready (zero-file) live changeset state — this alone @@ -5125,7 +5126,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map })._sessions.set(sessionId, sessionUri); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.registerProvider(agent); // Register a changeset but leave it in the default @@ -5215,7 +5216,7 @@ suite('AgentService (node dispatcher)', () => { getBranchDiffSafetyInfo: async () => undefined, getDiffPatchBetweenRefs: async () => undefined, }; - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); agent.resolvedWorkingDirectory = workingDirectory; @@ -5262,7 +5263,7 @@ suite('AgentService (node dispatcher)', () => { const sessionDb = new SessionDatabase(':memory:'); disposables.add(toDisposable(() => sessionDb.close())); const sessionDataService = createSessionDataService(sessionDb); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); agent.resolvedWorkingDirectory = workingDirectory; @@ -5322,7 +5323,7 @@ suite('AgentService (node dispatcher)', () => { getBranchDiffSafetyInfo: async () => undefined, getDiffPatchBetweenRefs: async () => undefined, }; - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); // No resolvedWorkingDirectory set on the mock. @@ -5346,7 +5347,7 @@ suite('AgentService (node dispatcher)', () => { // Probe runs but reports "not a git repo". gitService.getSessionGitState = async () => undefined; - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); agent.resolvedWorkingDirectory = workingDirectory; @@ -5377,7 +5378,7 @@ suite('AgentService (node dispatcher)', () => { const gitService = createNoopGitService(); gitService.getSessionGitState = async () => gitState; - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); agent.resolvedWorkingDirectory = workingDirectory; @@ -5411,7 +5412,7 @@ suite('AgentService (node dispatcher)', () => { const gitService = createNoopGitService(); gitService.getSessionGitState = async () => gitState; - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); agent.resolvedWorkingDirectory = workingDirectory; @@ -5456,7 +5457,7 @@ suite('AgentService (node dispatcher)', () => { const calls: string[] = []; const gitService = createNoopGitService(); gitService.getSessionGitState = async (uri: URI) => { calls.push(uri.fsPath); return gitState; }; - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); agent.resolvedWorkingDirectory = workingDirectory; @@ -5509,7 +5510,7 @@ suite('AgentService (node dispatcher)', () => { test('annotations survive session state restoration', async () => { const sessionData = createPerSessionDataService(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); localService.registerProvider(agent); @@ -5537,7 +5538,7 @@ suite('AgentService (node dispatcher)', () => { test('annotations subscribe concurrent with session restore returns persisted feedback', async () => { const sessionData = createPerSessionDataService(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); localService.registerProvider(agent); @@ -5570,7 +5571,7 @@ suite('AgentService (node dispatcher)', () => { test('subagent annotations persist in the parent session database', async () => { const sessionData = createPerSessionDataService(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionData.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); localService.registerProvider(agent); @@ -6070,7 +6071,7 @@ suite('AgentService (node dispatcher)', () => { } test('rejects restoring a session that has been explicitly deleted (tombstoned) without resurrecting it', async () => { - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MockAgent('copilot')); svc.registerProvider(agent); @@ -6092,7 +6093,7 @@ suite('AgentService (node dispatcher)', () => { // restore from the central session DB — the agent (MockAgent) re-emits // nothing itself, yet the restored session still carries the tag. const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(copilotAgent); await createAgentSession(copilotAgent); const sessionResource = (await copilotAgent.listSessions())[0].session; @@ -6106,7 +6107,7 @@ suite('AgentService (node dispatcher)', () => { test('restores persisted multi-root metadata', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(copilotAgent); await createAgentSession(copilotAgent); const sessionResource = (await copilotAgent.listSessions())[0].session; @@ -6126,7 +6127,7 @@ suite('AgentService (node dispatcher)', () => { test('restores persisted source-control provenance', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(copilotAgent); await createAgentSession(copilotAgent); const sessionResource = (await copilotAgent.listSessions())[0].session; @@ -6185,7 +6186,7 @@ suite('AgentService (node dispatcher)', () => { // the host-side overlay a reloaded session comes back with no // context-usage gauge and a session cost of 0. const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(copilotAgent); const { session } = await createAgentSession(copilotAgent); const sessionResource = (await copilotAgent.listSessions())[0].session; @@ -6210,7 +6211,7 @@ suite('AgentService (node dispatcher)', () => { // not. Treating that stub as "already has usage" would skip exactly // the turns needing re-attachment — and Auto is the default model. const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const autoModeResolved = { chosenModel: 'claude-opus-4.8', predictedLabel: 'needs_reasoning', confidence: 0.93 }; const agent = disposables.add(new MockAgent('copilot')); agent.turnUsageOverride = { model: 'claude-opus-4.8', _meta: { autoModeResolved } }; @@ -6239,7 +6240,7 @@ suite('AgentService (node dispatcher)', () => { test('interleaves persisted host-injected local turns after their anchor on restore', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(copilotAgent); const { session } = await createAgentSession(copilotAgent); const sessionResource = (await copilotAgent.listSessions())[0].session; @@ -6270,7 +6271,7 @@ suite('AgentService (node dispatcher)', () => { test('restores the default chat\'s independently-renamed title', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(copilotAgent); await createAgentSession(copilotAgent); const sessionResource = (await copilotAgent.listSessions())[0].session; @@ -6289,7 +6290,7 @@ suite('AgentService (node dispatcher)', () => { test('persists chat drafts to session metadata', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(copilotAgent); const session = await localService.createSession({ provider: 'copilot' }); const draft = { @@ -6309,7 +6310,7 @@ suite('AgentService (node dispatcher)', () => { test('restores chat drafts from session metadata', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(copilotAgent); const { session } = await createAgentSession(copilotAgent); const sessionResource = (await copilotAgent.listSessions())[0].session; @@ -6436,7 +6437,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new AdoptOnOpenAgent()); localService.registerProvider(agent); agent.sessionMessages = []; @@ -6485,7 +6486,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new AdoptOnOpenAgent()); localService.registerProvider(agent); agent.sessionMessages = []; @@ -6521,7 +6522,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new AdoptOnOpenAgent()); localService.registerProvider(agent); @@ -6558,7 +6559,7 @@ suite('AgentService (node dispatcher)', () => { test('excludes adoptable-legacy sessions from the list while the migrate setting is off', async () => { // Guards against a refresh re-surfacing a registry entry that can no longer be opened while migration is off. - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const adoptable: IAgentSessionMetadata = { session: AgentSession.uri('copilot', 'adoptable-list-gate'), startTime: Date.now(), @@ -6951,7 +6952,7 @@ suite('AgentService (node dispatcher)', () => { test('legacy subagent reconstruction restores a persisted custom title', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(copilotAgent); const parent = await localService.createSession({ provider: 'copilot' }); const childChat = buildSubagentChatUri(parent.toString(), 'tc-sub'); @@ -7365,7 +7366,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -7487,7 +7488,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new TestSessionDatabase(); const agent = disposables.add(new MultiChatAgent('copilot')); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(agent); const { session } = await createAgentSession(agent); const sessionResource = (await agent.listSessions())[0].session; @@ -7566,7 +7567,7 @@ suite('AgentService (node dispatcher)', () => { } const agent = disposables.add(new LeakyMultiChatAgent('copilot')); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, perSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, perSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); svc.registerProvider(agent); const session = await svc.createSession({ provider: 'copilot' }); const chatUri = URI.parse(buildChatUri(session, 'peer-1')); @@ -7577,7 +7578,7 @@ suite('AgentService (node dispatcher)', () => { // Simulate a host restart: a fresh service over the same persisted // databases, with a fresh agent still leaking the backing session. const restartAgent = disposables.add(new LeakyMultiChatAgent('copilot')); - const restarted = disposables.add(new AgentService(new NullLogService(), fileService, perSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const restarted = disposables.add(createTestAgentService(new NullLogService(), fileService, perSessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); restarted.registerProvider(restartAgent); const afterRestart = await restarted.listSessions(); @@ -7615,7 +7616,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new FailingBackingMarkerDatabase(); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new BackedMultiChatAgent('copilot')); svc.registerProvider(agent); const session = await svc.createSession({ provider: 'copilot' }); @@ -7663,7 +7664,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new FailingBackingMarkerDatabase(); - const svc = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const svc = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new BackedMultiChatAgent('copilot')); svc.registerProvider(agent); const session = await svc.createSession({ provider: 'copilot' }); @@ -7796,7 +7797,7 @@ suite('AgentService (node dispatcher)', () => { test('creates a side chat from a completed local turn without losing its stable source turn identity', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new SideChatAgent('copilot')); localService.registerProvider(agent); const { session } = await createAgentSession(agent); @@ -7912,7 +7913,7 @@ suite('AgentService (node dispatcher)', () => { test('persists and restores the SideChat origin', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new SideChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -7951,7 +7952,7 @@ suite('AgentService (node dispatcher)', () => { test('resolves a restored peer side-chat source without resolving the target chat', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new SideChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -7985,7 +7986,7 @@ suite('AgentService (node dispatcher)', () => { test('hydrates a missing peer chat when resolving a generic Chat attachment', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new SideChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -8120,7 +8121,7 @@ suite('AgentService (node dispatcher)', () => { test('collapsed session creation persists and restores exact default-chat provider data', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const calls: { op: string; providerData?: string }[] = []; class ExactDefaultChatAgent extends MockAgent { override readonly chats: IAgentChats = withChatOverrides(getChatSurface(this), base => ({ @@ -8188,7 +8189,7 @@ suite('AgentService (node dispatcher)', () => { test('host-restore-slice: restoring a legacy default chat recovers before canonical materialization and persists additively', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new RecoveringDefaultChatAgent('copilot')); localService.registerProvider(agent); @@ -8235,7 +8236,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new ExternalRestoreAgent('copilot')); const session = AgentSession.uri('copilot', 'external-restore'); (agent as unknown as { _sessions: Map })._sessions.set(AgentSession.id(session), session); @@ -8260,7 +8261,7 @@ suite('AgentService (node dispatcher)', () => { test('host-restore-slice: a second restore reads the recovered providerData directly and never re-recovers or re-persists it', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new RecoveringDefaultChatAgent('copilot')); localService.registerProvider(agent); @@ -8290,7 +8291,7 @@ suite('AgentService (node dispatcher)', () => { test('host-restore-slice: a canonical default-chat providerData blob is never rewritten by a recovered materializeChat result', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new RecoveringDefaultChatAgent('copilot')); localService.registerProvider(agent); @@ -8317,7 +8318,7 @@ suite('AgentService (node dispatcher)', () => { test('host-restore-slice: a default chat with neither a persisted nor a recovered backing restores its history without binding anything', async () => { const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); // The base mock has no `materializeChat` at all, so restore has // nothing to re-attach and no bind fallback to reach for. const agent = disposables.add(new MockAgent('copilot')); @@ -8383,7 +8384,7 @@ suite('AgentService (node dispatcher)', () => { // A session data service that cannot open a database makes the // default-chat backing write — the last step of provisioning — // throw, which is what drives the create-time rollback. - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createNullSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createNullSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); class BackingChatSurfaceAgent extends ChatSurfaceAgent { override readonly chats: IAgentChats = withChatOverrides(getChatSurface(this), base => ({ createChat: async (chat, context, options) => { @@ -8863,7 +8864,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new FailingPeerCatalogDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); class MultiChatAgent extends MockAgent { readonly disposedPeers: URI[] = []; override async createChat(): Promise { @@ -8905,7 +8906,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new BackedPeerChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -8958,7 +8959,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9039,7 +9040,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9098,7 +9099,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9170,7 +9171,7 @@ suite('AgentService (node dispatcher)', () => { } } - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new ServerToolAgent('copilot')); localService.registerProvider(agent); const sourceSession = await localService.createSession({ provider: 'copilot' }); @@ -9247,7 +9248,7 @@ suite('AgentService (node dispatcher)', () => { })); } - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(new TestSessionDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new ServerToolAgent('copilot')); localService.registerProvider(agent); const sourceSession = await localService.createSession({ provider: 'copilot' }); @@ -9306,7 +9307,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9355,7 +9356,7 @@ suite('AgentService (node dispatcher)', () => { override async getSessionMessages(): Promise { return []; } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9398,7 +9399,7 @@ suite('AgentService (node dispatcher)', () => { override async getSessionMessages(): Promise { return []; } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9439,7 +9440,7 @@ suite('AgentService (node dispatcher)', () => { override async getSessionMessages(): Promise { return []; } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9485,7 +9486,7 @@ suite('AgentService (node dispatcher)', () => { return []; } } - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new RestoringAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9529,7 +9530,7 @@ suite('AgentService (node dispatcher)', () => { return []; } } - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new RestoringAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ @@ -9579,7 +9580,7 @@ suite('AgentService (node dispatcher)', () => { override async getSessionMessages(): Promise { return []; } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = AgentSession.uri('copilot', 'reused-session'); @@ -9631,7 +9632,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9678,7 +9679,7 @@ suite('AgentService (node dispatcher)', () => { override async disposeChat(_session: URI, _chat: URI): Promise { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9718,7 +9719,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new UpdatingDisposeAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9753,7 +9754,7 @@ suite('AgentService (node dispatcher)', () => { override async disposeChat(): Promise { } } const db = new FailingRemovalDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new MultiChatAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9808,7 +9809,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new LegacyAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9856,7 +9857,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new LegacyAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9889,7 +9890,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new LegacyAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9927,7 +9928,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new TestSessionDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new LegacyAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -9971,7 +9972,7 @@ suite('AgentService (node dispatcher)', () => { } } const db = new FailingCatalogDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = disposables.add(new LegacyAgent('copilot')); localService.registerProvider(agent); const session = await localService.createSession({ provider: 'copilot' }); @@ -10020,7 +10021,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new RecordingTitleDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.configurationService.updateRootConfig({ [AgentHostActiveAgentTitleGenerationConfigKey]: true }); const agent = disposables.add(new ServerToolAgent('copilot')); localService.registerProvider(agent); @@ -10107,7 +10108,7 @@ suite('AgentService (node dispatcher)', () => { } const db = new FailingTitleDatabase(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(db), { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.configurationService.updateRootConfig({ [AgentHostActiveAgentTitleGenerationConfigKey]: true }); const agent = disposables.add(new ServerToolAgent('copilot')); localService.registerProvider(agent); @@ -10287,7 +10288,7 @@ suite('AgentService (node dispatcher)', () => { await whenIdle.p; } } - const localService = disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(new DelayedIdleDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(new DelayedIdleDatabase()), { _serviceBrand: undefined } as IProductService, createNoopGitService())); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); localService.registerProvider(agent); @@ -10745,7 +10746,7 @@ suite('AgentService (node dispatcher)', () => { }; const sessionDataService = createSessionDataService(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); localService.registerProvider(copilotAgent); const sessionResource = await localService.createSession({ provider: 'copilot' }); const uncommittedUri = URI.parse(buildUncommittedChangesetUri(sessionResource.toString())); @@ -10786,7 +10787,7 @@ suite('AgentService (node dispatcher)', () => { }; const sessionDataService = createSessionDataService(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); localService.registerProvider(copilotAgent); const sessionResource = await localService.createSession({ provider: 'copilot' }); const sessionChangesetUri = URI.parse(buildSessionChangesetUri(sessionResource.toString())); @@ -10827,7 +10828,7 @@ suite('AgentService (node dispatcher)', () => { }; const sessionDataService = createSessionDataService(); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); localService.registerProvider(copilotAgent); // Seed a session on the agent without calling @@ -11053,7 +11054,7 @@ suite('AgentService (node dispatcher)', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); await localService.createSession({ provider: 'copilot', config: { autoApprove: 'autoApprove' } }); @@ -11071,7 +11072,7 @@ suite('AgentService (node dispatcher)', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); await localService.createSession({ provider: 'copilot' }); @@ -11094,7 +11095,7 @@ suite('AgentService (node dispatcher)', () => { const localAgent = new MockAgent('codex'); localAgent.sessionMetadataOverrides = { workingDirectories: [workingDirectory], project: undefined }; disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); localService.setWorktreeIsolation(disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/test' }, gitService, @@ -11130,7 +11131,7 @@ suite('AgentService (node dispatcher)', () => { const model = { id: 'codex-model:openai:gpt-5.6-sol' }; localAgent.sessionMetadataOverrides = { model } as typeof localAgent.sessionMetadataOverrides; disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); const { session } = await createAgentSession(localAgent); await sessionDb.setChatDraft(URI.parse(buildDefaultChatUri(session)), { @@ -11157,7 +11158,7 @@ suite('AgentService (node dispatcher)', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); // Create a session on the agent backend (no config) so listSessions can find it @@ -11187,7 +11188,7 @@ suite('AgentService (node dispatcher)', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); const { session } = await createAgentSession(localAgent); @@ -11241,7 +11242,7 @@ suite('AgentService (node dispatcher)', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); const { session } = await createAgentSession(localAgent); @@ -11292,7 +11293,7 @@ suite('AgentService (node dispatcher)', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); const session = await localService.createSession({ provider: 'copilot', config: { autoApprove: 'autoApprove' } }); @@ -11319,7 +11320,7 @@ suite('AgentService (node dispatcher)', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); const { session } = await createAgentSession(localAgent); @@ -11463,7 +11464,7 @@ suite('AgentService (node dispatcher)', () => { gitService.addWorktree = async () => { throw new Error('git worktree exited with code 128: git-lfs filter-process: git-lfs: command not found'); }; - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const isolation = disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/failure' }, gitService, @@ -11530,7 +11531,7 @@ suite('AgentService (node dispatcher)', () => { const sessionDataService = createSessionDataService(database); const gitService = createNoopGitService(); gitService.getRepositoryRoot = async () => undefined; - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const isolation = disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/fallback' }, gitService, @@ -11609,7 +11610,7 @@ suite('AgentService (node dispatcher)', () => { }; }; gitService.computeSessionFileDiffs = async () => []; - const localService = disposables.add(new AgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, nullSessionDataService, { _serviceBrand: undefined } as IProductService, gitService)); const provisionalAgent = new ProvisionalMockAgent('provisional'); disposables.add(toDisposable(() => provisionalAgent.dispose())); localService.registerProvider(provisionalAgent); @@ -11788,7 +11789,7 @@ suite('AgentService (node dispatcher)', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); const { session } = await createAgentSession(localAgent); diff --git a/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts b/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts new file mode 100644 index 00000000000000..5b7055d96476a1 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IFileService } from '../../../files/common/files.js'; +import { InstantiationService } from '../../../instantiation/common/instantiationService.js'; +import { ServiceCollection } from '../../../instantiation/common/serviceCollection.js'; +import { ILogService } from '../../../log/common/log.js'; +import { IProductService } from '../../../product/common/productService.js'; +import { ITelemetryService } from '../../../telemetry/common/telemetry.js'; +import { NullTelemetryService } from '../../../telemetry/common/telemetryUtils.js'; +import { type IAgentCustomizationSettingsRegistration } from '../../common/agentCustomizationSettings.js'; +import { IAgentHostGitService } from '../../common/agentHostGitService.js'; +import { AgentHostLaunchKind } from '../../common/agentHostTelemetry.js'; +import { ISessionDataService } from '../../common/sessionDataService.js'; +import { IAgentHostDatabase } from '../../node/agentHostDatabase.js'; +import { createAgentHostApplication } from '../../node/agentHostApplication.js'; +import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from '../../node/agentHostFileMonitorService.js'; +import { IAgentHostProxyResolver } from '../../node/agentHostProxyResolver.js'; +import { AgentService } from '../../node/agentService.js'; +import { ICopilotApiService } from '../../node/shared/copilotApiService.js'; + +class TestAgentService extends AgentService { + registerTestDependency(disposable: IDisposable): void { + this._register(disposable); + } +} + +export function createTestAgentService( + logService: ILogService, + fileService: IFileService, + sessionDataService: ISessionDataService, + productService: IProductService, + gitService: IAgentHostGitService, + rootConfigResource?: URI, + telemetryService: ITelemetryService = NullTelemetryService, + fileMonitorService?: IAgentHostFileMonitorService, + copilotApiService?: ICopilotApiService, + fetchFn: typeof globalThis.fetch = globalThis.fetch, + providerConfigurations: readonly IAgentCustomizationSettingsRegistration[] = [], + hostLaunchKind = AgentHostLaunchKind.Unknown, + storageResource?: URI, + orchestratorDatabase?: IAgentHostDatabase, + now: () => number = Date.now, +): AgentService { + const effectiveFileMonitorService = fileMonitorService ?? new AgentHostFileMonitorService(fileService, logService); + const proxyResolver: IAgentHostProxyResolver = { + _serviceBrand: undefined, + onDidRegisterConnection: Event.None, + register: () => Disposable.None, + resolveProxy: async () => undefined, + fetch: fetchFn, + }; + const instantiationService = new InstantiationService(new ServiceCollection( + [ILogService, logService], + [IFileService, fileService], + [ISessionDataService, sessionDataService], + [IProductService, productService], + [IAgentHostGitService, gitService], + [ITelemetryService, telemetryService], + [IAgentHostFileMonitorService, effectiveFileMonitorService], + [IAgentHostProxyResolver, proxyResolver], + ), /*strict*/ true); + const options = { + rootConfigResource, + copilotApiService, + providerConfigurations, + hostLaunchKind, + storageResource, + orchestratorDatabase, + now, + }; + const application = createAgentHostApplication(instantiationService, options, (applicationInstantiationService, services) => { + return applicationInstantiationService.createInstance(TestAgentService, options, services); + }); + const service = application.agentService; + if (!fileMonitorService) { + service.registerTestDependency(effectiveFileMonitorService); + } + service.registerTestDependency(instantiationService); + return service; +} diff --git a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts index 17ea4d9e462b88..b687d61d61c391 100644 --- a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts +++ b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts @@ -39,7 +39,6 @@ import { AgentHostClientConnectionKind, AgentHostLaunchKind, AgentHostTransportK import { IAgentHostCheckpointService, NULL_CHECKPOINT_SERVICE } from '../../common/agentHostCheckpointService.js'; import { IAgentHostChangesetService, StaticChangesetKind } from '../../common/agentHostChangesetService.js'; import { IAgentHostGitService } from '../../common/agentHostGitService.js'; -import { AgentService } from '../../node/agentService.js'; import { AgentSideEffects, IAgentSideEffectsOptions } from '../../node/agentSideEffects.js'; import { AgentHostLocalTurns } from '../../node/agentHostLocalTurns.js'; import type { IAgentHostAskQuestionsToolInvokedEvent } from '../../node/agentHostTelemetryReporter.js'; @@ -52,6 +51,7 @@ import { applyMcpServerEnablement } from '../../node/shared/mcpCustomizationCont import { createNoopGitService, createNullSessionDataService, createSessionDataService, TestSessionDatabase } from '../common/sessionTestHelpers.js'; import { MockAgent } from './mockAgent.js'; import { TestAgentHostTerminalManager } from './testAgentHostTerminalManager.js'; +import { createTestAgentService } from './agentServiceTestUtils.js'; // ---- Tests ------------------------------------------------------------------ @@ -4887,7 +4887,7 @@ suite('AgentSideEffects', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent(); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); await localService.createSession({ provider: localAgent.id }); @@ -4906,7 +4906,7 @@ suite('AgentSideEffects', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent(); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); const session = await createAgentSession(localAgent); @@ -4933,7 +4933,7 @@ suite('AgentSideEffects', () => { const sessionDataService = createSessionDataService(sessionDb); const localAgent = new MockAgent(); disposables.add(toDisposable(() => localAgent.dispose())); - const localService = disposables.add(new AgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const localService = disposables.add(createTestAgentService(new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService())); localService.registerProvider(localAgent); const session = await createAgentSession(localAgent); diff --git a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts index f00ca2aacecad2..e1cc2a329e55a0 100644 --- a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts @@ -67,6 +67,7 @@ import { IAgentHostCustomizationEnablementService, type IAgentHostCustomizationE import { AgentHostSessionTitleSignal, IAgentHostSessionTitleSignal } from '../../node/agentHostSessionTitleSignal.js'; import { IAgentHostGitHubEndpointService } from '../../node/agentHostGitHubEndpointService.js'; import { createTestGitHubEndpointService } from './testGitHubEndpointService.js'; +import { createTestAgentService } from './agentServiceTestUtils.js'; import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js'; import { makeMcpServerCustomization } from '../../../agentPlugins/common/pluginParsers.js'; import { ClaudeAgent, fromSdkModelInfo } from '../../node/claude/claudeAgent.js'; @@ -82,7 +83,6 @@ import { PendingRequestRegistry } from '../../common/pendingRequestRegistry.js'; import { IClaudeProxyCreditsReport, IClaudeProxyHandle, IClaudeProxyService } from '../../node/claude/claudeProxyService.js'; import { resolvePromptToContentBlocks } from '../../node/claude/claudePromptResolver.js'; import { ICopilotApiService, type ICopilotApiServiceRequestOptions } from '../../node/shared/copilotApiService.js'; -import { AgentService } from '../../node/agentService.js'; import { createAgentChatContext } from '../../node/agentChatContext.js'; import { injectSideChatContext } from '../../node/agentPeerChats.js'; import { createNoopGitService, createNullSessionDataService, createSessionDataService, RecordingCheckpointService, TestSessionDatabase } from '../common/sessionTestHelpers.js'; @@ -2071,7 +2071,7 @@ suite('ClaudeAgent', () => { test('AgentService surfaces the registered ClaudeAgent in the providers map', () => { const { agent } = createTestContext(disposables); const fileService = disposables.add(new FileService(new NullLogService())); - const service = disposables.add(new AgentService( + const service = disposables.add(createTestAgentService( new NullLogService(), fileService, createNullSessionDataService(), From 43ef7427ea0f5e46df86c8f7f47e00276990504f Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 18 Aug 2026 18:41:48 -0700 Subject: [PATCH 2/6] Clarify Agent Host DI scope names Use explicit bootstrap and application names for service collections and instantiation services. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/node/agentHostApplication.ts | 24 ++-- .../platform/agentHost/node/agentHostMain.ts | 125 +++++++++--------- .../agentHost/node/agentHostServerMain.ts | 96 +++++++------- .../platform/agentHost/node/agentService.ts | 98 +++++++------- .../test/node/agentServiceTestUtils.ts | 8 +- 5 files changed, 175 insertions(+), 176 deletions(-) diff --git a/src/vs/platform/agentHost/node/agentHostApplication.ts b/src/vs/platform/agentHost/node/agentHostApplication.ts index d624e455087660..11978a892a0336 100644 --- a/src/vs/platform/agentHost/node/agentHostApplication.ts +++ b/src/vs/platform/agentHost/node/agentHostApplication.ts @@ -9,24 +9,24 @@ import { AgentService, IAgentServiceOptions } from './agentService.js'; export interface IAgentHostApplication { readonly agentService: T; - readonly instantiationService: IInstantiationService; - readonly services: ServiceCollection; + readonly applicationInstantiationService: IInstantiationService; + readonly applicationServices: ServiceCollection; } -type AgentServiceFactory = (instantiationService: IInstantiationService, services: ServiceCollection) => T; +type AgentServiceFactory = (applicationInstantiationService: IInstantiationService, applicationServices: ServiceCollection) => T; -export function createAgentHostApplication(parentInstantiationService: IInstantiationService, options: IAgentServiceOptions): IAgentHostApplication; -export function createAgentHostApplication(parentInstantiationService: IInstantiationService, options: IAgentServiceOptions, factory: AgentServiceFactory): IAgentHostApplication; -export function createAgentHostApplication(parentInstantiationService: IInstantiationService, options: IAgentServiceOptions, factory?: AgentServiceFactory): IAgentHostApplication { - const services = new ServiceCollection(); - const instantiationService = parentInstantiationService.createChild(services); +export function createAgentHostApplication(bootstrapInstantiationService: IInstantiationService, options: IAgentServiceOptions): IAgentHostApplication; +export function createAgentHostApplication(bootstrapInstantiationService: IInstantiationService, options: IAgentServiceOptions, factory: AgentServiceFactory): IAgentHostApplication; +export function createAgentHostApplication(bootstrapInstantiationService: IInstantiationService, options: IAgentServiceOptions, factory?: AgentServiceFactory): IAgentHostApplication { + const applicationServices = new ServiceCollection(); + const applicationInstantiationService = bootstrapInstantiationService.createChild(applicationServices); try { const agentService = factory - ? factory(instantiationService, services) - : instantiationService.createInstance(AgentService, options, services); - return { agentService, instantiationService, services }; + ? factory(applicationInstantiationService, applicationServices) + : applicationInstantiationService.createInstance(AgentService, options, applicationServices); + return { agentService, applicationInstantiationService, applicationServices }; } catch (error) { - instantiationService.dispose(); + applicationInstantiationService.dispose(); throw error; } } diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index 67b5a17aac5ca2..c77b755de5e49b 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -148,8 +148,8 @@ async function startAgentHost(): Promise { // Create the real service implementation that lives in this process let agentService: AgentService; - let instantiationService: IInstantiationService; - let rootInstantiationService: IInstantiationService | undefined; + let applicationInstantiationService: IInstantiationService; + let bootstrapInstantiationService: IInstantiationService | undefined; // Hoisted out of the `try` below so the protocol handlers (constructed // after the block) can forward agent-SDK download progress to clients. let sdkDownloadProgress: Event | undefined; @@ -160,75 +160,74 @@ async function startAgentHost(): Promise { try { // Build the process DI container and network stack before telemetry so every // outbound fetch, including restricted telemetry, uses the same proxy resolver. - const diServices = new ServiceCollection(); - diServices.set(INativeEnvironmentService, environmentService); - diServices.set(ILogService, logService); - diServices.set(IFileService, fileService); - diServices.set(ISessionDataService, sessionDataService); - diServices.set(IProductService, productService); - const networkServices = await registerAgentHostNetworkServices(diServices, fileService, environmentService, logService, disposables); + const bootstrapServices = new ServiceCollection(); + bootstrapServices.set(INativeEnvironmentService, environmentService); + bootstrapServices.set(ILogService, logService); + bootstrapServices.set(IFileService, fileService); + bootstrapServices.set(ISessionDataService, sessionDataService); + bootstrapServices.set(IProductService, productService); + const networkServices = await registerAgentHostNetworkServices(bootstrapServices, fileService, environmentService, logService, disposables); proxyResolver = networkServices.proxyResolver; const fetchFn = proxyResolver.fetch.bind(proxyResolver); const telemetryService = await createAgentHostTelemetryService({ environmentService, productService, fileService, loggerService, logService, disposables, fetchFn, requestService: networkServices.requestService }); errorTelemetry.value = new ErrorTelemetry(telemetryService); - diServices.set(ITelemetryService, telemetryService); - rootInstantiationService = new InstantiationService(diServices, /*strict*/ true); - instantiationService = rootInstantiationService; - const fileMonitorService = disposables.add(instantiationService.createInstance(AgentHostFileMonitorService)); - diServices.set(IAgentHostFileMonitorService, fileMonitorService); - diServices.set(IWindowsMxcTerminalSandboxRuntime, instantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); - diServices.set(ISandboxHelperService, new SandboxHelperService()); - const gitService = instantiationService.createInstance(AgentHostGitService); - diServices.set(IAgentHostGitService, gitService); + bootstrapServices.set(ITelemetryService, telemetryService); + bootstrapInstantiationService = new InstantiationService(bootstrapServices, /*strict*/ true); + const fileMonitorService = disposables.add(bootstrapInstantiationService.createInstance(AgentHostFileMonitorService)); + bootstrapServices.set(IAgentHostFileMonitorService, fileMonitorService); + bootstrapServices.set(IWindowsMxcTerminalSandboxRuntime, bootstrapInstantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); + bootstrapServices.set(ISandboxHelperService, new SandboxHelperService()); + const gitService = bootstrapInstantiationService.createInstance(AgentHostGitService); + bootstrapServices.set(IAgentHostGitService, gitService); // Register the agent SDK downloader BEFORE any service that injects it // (ClaudeAgentSdkService and CodexAgent below). The downloader resolves // dev-override env var → on-disk cache → product.agentSdks download. - const agentSdkDownloader = disposables.add(instantiationService.createInstance(AgentSdkDownloader)); - diServices.set(IAgentSdkDownloader, agentSdkDownloader); + const agentSdkDownloader = disposables.add(bootstrapInstantiationService.createInstance(AgentSdkDownloader)); + bootstrapServices.set(IAgentSdkDownloader, agentSdkDownloader); sdkDownloadProgress = agentSdkDownloader.onDidDownloadProgress; - const claudeAgentSdkService = instantiationService.createInstance(ClaudeAgentSdkService); - diServices.set(IClaudeAgentSdkService, claudeAgentSdkService); + const claudeAgentSdkService = bootstrapInstantiationService.createInstance(ClaudeAgentSdkService); + bootstrapServices.set(IClaudeAgentSdkService, claudeAgentSdkService); // BYOK infrastructure is always wired; synchronized root config gates model // publication and per-session provider configuration. byokLmBridgeRegistry = new ByokLmBridgeRegistry(); - diServices.set(IByokLmBridgeRegistry, byokLmBridgeRegistry); - const byokLmProxyService = disposables.add(instantiationService.createInstance(ByokLmProxyService)); - diServices.set(IByokLmProxyService, byokLmProxyService); - const agentHostOTelService = disposables.add(instantiationService.createInstance(AgentHostOTelService, fetchFn)); - diServices.set(IAgentHostOTelService, agentHostOTelService); - const application = createAgentHostApplication(instantiationService, { + bootstrapServices.set(IByokLmBridgeRegistry, byokLmBridgeRegistry); + const byokLmProxyService = disposables.add(bootstrapInstantiationService.createInstance(ByokLmProxyService)); + bootstrapServices.set(IByokLmProxyService, byokLmProxyService); + const agentHostOTelService = disposables.add(bootstrapInstantiationService.createInstance(AgentHostOTelService, fetchFn)); + bootstrapServices.set(IAgentHostOTelService, agentHostOTelService); + const application = createAgentHostApplication(bootstrapInstantiationService, { rootConfigResource, providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], hostLaunchKind, storageResource, }); agentService = application.agentService; - instantiationService = application.instantiationService; - const agentServices = application.services; - const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); - agentServices.set(INetworkDiagnosticsService, networkDiagnosticsService); + applicationInstantiationService = application.applicationInstantiationService; + const applicationServices = application.applicationServices; + const networkDiagnosticsService = applicationInstantiationService.createInstance(NetworkDiagnosticsService); + applicationServices.set(INetworkDiagnosticsService, networkDiagnosticsService); agentService.setNetworkDiagnosticsService(networkDiagnosticsService); const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService); - agentServices.set(IAgentPluginManager, pluginManager); + applicationServices.set(IAgentPluginManager, pluginManager); const diffComputeService = disposables.add(new NodeWorkerDiffComputeService(logService)); - agentServices.set(IDiffComputeService, diffComputeService); - const editAttributionService = disposables.add(instantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); - agentServices.set(IAgentEditAttributionService, editAttributionService); + applicationServices.set(IDiffComputeService, diffComputeService); + const editAttributionService = disposables.add(applicationInstantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); + applicationServices.set(IAgentEditAttributionService, editAttributionService); agentService.setEditAttributionService(editAttributionService); - agentServices.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); - const editArcReporterService = disposables.add(instantiationService.createInstance(EditArcReporterService, undefined)); - agentServices.set(IEditArcReporterService, editArcReporterService); + applicationServices.set(IEditSurvivalReporterFactory, applicationInstantiationService.createInstance(EditSurvivalReporterFactory)); + const editArcReporterService = disposables.add(applicationInstantiationService.createInstance(EditArcReporterService, undefined)); + applicationServices.set(IEditArcReporterService, editArcReporterService); // Host-owned worktree isolation controller: a single instance drives folder // / worktree isolation for every agent, so providers stay unaware of it. It // owns its branch-name generator, created from ICopilotApiService. - const worktreeIsolation = disposables.add(instantiationService.createInstance(WorktreeIsolation, undefined)); - agentServices.set(IAgentHostWorktreeIsolation, worktreeIsolation); + const worktreeIsolation = disposables.add(applicationInstantiationService.createInstance(WorktreeIsolation, undefined)); + applicationServices.set(IAgentHostWorktreeIsolation, worktreeIsolation); agentService.setWorktreeIsolation(worktreeIsolation); - const claudeProxyService = disposables.add(instantiationService.createInstance(ClaudeProxyService)); - agentServices.set(IClaudeProxyService, claudeProxyService); - const codexProxyService = disposables.add(instantiationService.createInstance(CodexProxyService)); - agentServices.set(ICodexProxyService, codexProxyService); - agentService.registerProvider(instantiationService.createInstance(CopilotAgent)); + const claudeProxyService = disposables.add(applicationInstantiationService.createInstance(ClaudeProxyService)); + applicationServices.set(IClaudeProxyService, claudeProxyService); + const codexProxyService = disposables.add(applicationInstantiationService.createInstance(CodexProxyService)); + applicationServices.set(ICodexProxyService, codexProxyService); + agentService.registerProvider(applicationInstantiationService.createInstance(CopilotAgent)); // Claude and Codex providers are gated on two things: // 1. The user-facing enable toggle (`chat.agentHost.Agent.enabled`, // forwarded as an env var by the starters). Claude defaults to on, @@ -243,7 +242,7 @@ async function startAgentHost(): Promise { // If either gate fails, the provider is not registered and never appears // in the agent picker (matches the pre-CDN UX exactly). if (isAgentEnabled(process.env[AgentHostClaudeAgentEnabledEnvVar], true) && (!environmentService.isBuilt || agentSdkDownloader.isAvailable(ClaudeSdkPackage))) { - agentService.registerProvider(instantiationService.createInstance(ClaudeAgent)); + agentService.registerProvider(applicationInstantiationService.createInstance(ClaudeAgent)); } // Codex registration is one-way (register-on-enable): the env-var toggle // or the renderer-forwarded `codexAgentEnabled` root config enables it. @@ -259,14 +258,14 @@ async function startAgentHost(): Promise { const enabledByRootConfig = agentConfigurationService.getRootValue(platformRootSchema, AgentHostCodexEnabledConfigKey) === true; if (enabledByEnv || enabledByRootConfig) { codexRegistered = true; - agentService.registerProvider(instantiationService.createInstance(CodexAgent)); + agentService.registerProvider(applicationInstantiationService.createInstance(CodexAgent)); } }; registerCodexIfEnabled(); disposables.add(agentConfigurationService.onDidRootConfigChange(registerCodexIfEnabled)); } } catch (err) { - rootInstantiationService?.dispose(); + bootstrapInstantiationService?.dispose(); logService.error('Failed to create AgentService', err); throw err; } @@ -278,7 +277,7 @@ async function startAgentHost(): Promise { // lifetime, rather than inside `AgentHostService`: a service that arms a // recurring timer in its constructor is one that no faked-timer unit test // can ever drain. - disposables.add(instantiationService.createInstance(AgentModelRefreshScheduler, agentService.agents, agentService.onDidStartTurn, MODEL_REFRESH_INTERVAL_MS)); + disposables.add(applicationInstantiationService.createInstance(AgentModelRefreshScheduler, agentService.agents, agentService.onDidStartTurn, MODEL_REFRESH_INTERVAL_MS)); // Surface agent-SDK download progress to clients as generic `progress` // notifications. The downloader fires process-global frames keyed by package @@ -326,7 +325,7 @@ async function startAgentHost(): Promise { }; try { // Handler for the renderer's MessagePort data plane. - const messagePortProtocolHandler = localDataPlaneDisposables.add(instantiationService.createInstance( + const messagePortProtocolHandler = localDataPlaneDisposables.add(applicationInstantiationService.createInstance( ProtocolServerHandler, agentService, agentService.stateManager, @@ -390,7 +389,7 @@ async function startAgentHost(): Promise { const localEndpoint = await startLocalAgentHostEndpoint( environmentService.userDataPath, logService, - instantiationService, + applicationInstantiationService, environmentService.logsHome, ); if (localEndpoint) { @@ -399,7 +398,7 @@ async function startAgentHost(): Promise { // publishing the metadata that advertises it, so a client can't connect // in the gap and be missed. localDataPlaneDisposables.add(localEndpoint.server); - const localEndpointProtocolHandler = localDataPlaneDisposables.add(instantiationService.createInstance( + const localEndpointProtocolHandler = localDataPlaneDisposables.add(applicationInstantiationService.createInstance( ProtocolServerHandler, agentService, agentService.stateManager, @@ -448,7 +447,7 @@ async function startAgentHost(): Promise { const wsServer = await WebSocketProtocolServer.create( { socketPath }, logService, - { instantiationService, logsHome: environmentService.logsHome }, + { instantiationService: applicationInstantiationService, logsHome: environmentService.logsHome }, ); if (protocolIngressDisposables.isDisposed) { wsServer.dispose(); @@ -456,7 +455,7 @@ async function startAgentHost(): Promise { } protocolIngressDisposables.add(wsServer); - const protocolHandler = protocolIngressDisposables.add(instantiationService.createInstance( + const protocolHandler = protocolIngressDisposables.add(applicationInstantiationService.createInstance( ProtocolServerHandler, agentService, agentService.stateManager, @@ -525,7 +524,7 @@ async function startAgentHost(): Promise { } }, }; - server.registerChannel(AgentHostIpcChannels.Management, ProxyChannel.fromService(instantiationService.createInstance( + server.registerChannel(AgentHostIpcChannels.Management, ProxyChannel.fromService(applicationInstantiationService.createInstance( AgentHostManagementService, agentService, connectionTrackerService, @@ -543,7 +542,7 @@ async function startAgentHost(): Promise { const configuredWebSocketServerStart = startWebSocketServer( agentService, clientFileSystemProvider, - instantiationService, + applicationInstantiationService, environmentService.logsHome, logService, otlpLogEmitter, @@ -562,7 +561,7 @@ async function startAgentHost(): Promise { agentService.dispose(); logService.dispose(); disposables.dispose(); - rootInstantiationService?.dispose(); + bootstrapInstantiationService?.dispose(); }); } @@ -574,7 +573,7 @@ interface ILocalAgentHostEndpoint { async function startLocalAgentHostEndpoint( userDataPath: string, logService: ILogService, - instantiationService: IInstantiationService, + applicationInstantiationService: IInstantiationService, logsHome: URI, ): Promise { let metadata: ILocalAgentHostEndpointMetadata | undefined; @@ -592,7 +591,7 @@ async function startLocalAgentHostEndpoint( connectionTokenValidate: token => token === endpointMetadata.connectionToken, }, logService, - { instantiationService, logsHome }, + { instantiationService: applicationInstantiationService, logsHome }, ); await server.whenListening; return { metadata: endpointMetadata, server }; @@ -636,7 +635,7 @@ function cleanupLocalAgentHostEndpoint( async function startWebSocketServer( agentService: AgentService, clientFileSystemProvider: AgentHostClientFileSystemProvider, - instantiationService: IInstantiationService, + applicationInstantiationService: IInstantiationService, logsHome: URI, logService: ILogService, otlpLogEmitter: OtlpLogEmitter, @@ -672,7 +671,7 @@ async function startWebSocketServer( : undefined, }, logService, - { instantiationService, logsHome }, + { instantiationService: applicationInstantiationService, logsHome }, ); if (disposables.isDisposed) { wsServer.dispose(); @@ -680,7 +679,7 @@ async function startWebSocketServer( } disposables.add(wsServer); - const protocolHandler = disposables.add(instantiationService.createInstance( + const protocolHandler = disposables.add(applicationInstantiationService.createInstance( ProtocolServerHandler, agentService, agentService.stateManager, diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index f628f50e61c87e..138574d592c5bb 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -231,36 +231,36 @@ async function main(): Promise { // `createInstance` (it needs IFileService + INativeEnvironmentService). // The git service is shared by AgentService (for diff computation + // showBlob) and the production agent registration path. - const diServices = new ServiceCollection(); - diServices.set(IProductService, productService); - diServices.set(INativeEnvironmentService, environmentService); - diServices.set(ILogService, logService); - diServices.set(IFileService, fileService); - diServices.set(ISessionDataService, sessionDataService); - const networkServices = await registerAgentHostNetworkServices(diServices, fileService, environmentService, logService, disposables); + const bootstrapServices = new ServiceCollection(); + bootstrapServices.set(IProductService, productService); + bootstrapServices.set(INativeEnvironmentService, environmentService); + bootstrapServices.set(ILogService, logService); + bootstrapServices.set(IFileService, fileService); + bootstrapServices.set(ISessionDataService, sessionDataService); + const networkServices = await registerAgentHostNetworkServices(bootstrapServices, fileService, environmentService, logService, disposables); const proxyResolver = networkServices.proxyResolver; const fetchFn = proxyResolver.fetch.bind(proxyResolver); const telemetryService = await createAgentHostTelemetryService({ environmentService, productService, fileService, loggerService, logService, disposables, disableTelemetry: options.quiet, fetchFn, requestService: networkServices.requestService }); errorTelemetry.value = new ErrorTelemetry(telemetryService); - diServices.set(ITelemetryService, telemetryService); - const rootInstantiationService = new InstantiationService(diServices, /*strict*/ true); - const fileMonitorService = disposables.add(rootInstantiationService.createInstance(AgentHostFileMonitorService)); - diServices.set(IAgentHostFileMonitorService, fileMonitorService); - diServices.set(IWindowsMxcTerminalSandboxRuntime, rootInstantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); - diServices.set(ISandboxHelperService, new SandboxHelperService()); - const gitService = rootInstantiationService.createInstance(AgentHostGitService); - diServices.set(IAgentHostGitService, gitService); - - const application = createAgentHostApplication(rootInstantiationService, { + bootstrapServices.set(ITelemetryService, telemetryService); + const bootstrapInstantiationService = new InstantiationService(bootstrapServices, /*strict*/ true); + const fileMonitorService = disposables.add(bootstrapInstantiationService.createInstance(AgentHostFileMonitorService)); + bootstrapServices.set(IAgentHostFileMonitorService, fileMonitorService); + bootstrapServices.set(IWindowsMxcTerminalSandboxRuntime, bootstrapInstantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); + bootstrapServices.set(ISandboxHelperService, new SandboxHelperService()); + const gitService = bootstrapInstantiationService.createInstance(AgentHostGitService); + bootstrapServices.set(IAgentHostGitService, gitService); + + const application = createAgentHostApplication(bootstrapInstantiationService, { rootConfigResource, providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], hostLaunchKind: AgentHostLaunchKind.VSCodeCLI, storageResource, }); - const { agentService, instantiationService, services: agentServices } = application; + const { agentService, applicationInstantiationService, applicationServices } = application; disposables.add(agentService); - const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); - agentServices.set(INetworkDiagnosticsService, networkDiagnosticsService); + const networkDiagnosticsService = applicationInstantiationService.createInstance(NetworkDiagnosticsService); + applicationServices.set(INetworkDiagnosticsService, networkDiagnosticsService); agentService.setNetworkDiagnosticsService(networkDiagnosticsService); // Register agents @@ -268,19 +268,19 @@ async function main(): Promise { if (!options.quiet) { // Production agents (require DI) const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService); - agentServices.set(IAgentPluginManager, pluginManager); - agentServices.set(IDiffComputeService, disposables.add(new NodeWorkerDiffComputeService(logService))); - const editAttributionService = disposables.add(instantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); - agentServices.set(IAgentEditAttributionService, editAttributionService); + applicationServices.set(IAgentPluginManager, pluginManager); + applicationServices.set(IDiffComputeService, disposables.add(new NodeWorkerDiffComputeService(logService))); + const editAttributionService = disposables.add(applicationInstantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); + applicationServices.set(IAgentEditAttributionService, editAttributionService); agentService.setEditAttributionService(editAttributionService); - agentServices.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); - const editArcReporterService = disposables.add(instantiationService.createInstance(EditArcReporterService, undefined)); - agentServices.set(IEditArcReporterService, editArcReporterService); + applicationServices.set(IEditSurvivalReporterFactory, applicationInstantiationService.createInstance(EditSurvivalReporterFactory)); + const editArcReporterService = disposables.add(applicationInstantiationService.createInstance(EditArcReporterService, undefined)); + applicationServices.set(IEditArcReporterService, editArcReporterService); // Host-owned worktree isolation controller: a single instance drives folder // / worktree isolation for every agent, so providers stay unaware of it. It // owns its branch-name generator, created from ICopilotApiService. - const worktreeIsolation = disposables.add(instantiationService.createInstance(WorktreeIsolation, undefined)); - agentServices.set(IAgentHostWorktreeIsolation, worktreeIsolation); + const worktreeIsolation = disposables.add(applicationInstantiationService.createInstance(WorktreeIsolation, undefined)); + applicationServices.set(IAgentHostWorktreeIsolation, worktreeIsolation); agentService.setWorktreeIsolation(worktreeIsolation); // CLI flags become env vars BEFORE the downloader is constructed so // `isAvailable()` and `loadSdkRoot()` see them as dev overrides. @@ -291,23 +291,23 @@ async function main(): Promise { process.env[AgentHostCodexAgentSdkRootEnvVar] = options.codexSdkRoot; } // Register the agent SDK downloader BEFORE any service that injects it. - const agentSdkDownloader = disposables.add(instantiationService.createInstance(AgentSdkDownloader)); - agentServices.set(IAgentSdkDownloader, agentSdkDownloader); + const agentSdkDownloader = disposables.add(applicationInstantiationService.createInstance(AgentSdkDownloader)); + applicationServices.set(IAgentSdkDownloader, agentSdkDownloader); sdkDownloadProgress = agentSdkDownloader.onDidDownloadProgress; - const claudeProxyService = disposables.add(instantiationService.createInstance(ClaudeProxyService)); - agentServices.set(IClaudeProxyService, claudeProxyService); - const claudeAgentSdkService = instantiationService.createInstance(ClaudeAgentSdkService); - agentServices.set(IClaudeAgentSdkService, claudeAgentSdkService); - const codexProxyService = disposables.add(instantiationService.createInstance(CodexProxyService)); - agentServices.set(ICodexProxyService, codexProxyService); - const agentHostOTelService = disposables.add(instantiationService.createInstance(AgentHostOTelService, fetchFn)); - agentServices.set(IAgentHostOTelService, agentHostOTelService); + const claudeProxyService = disposables.add(applicationInstantiationService.createInstance(ClaudeProxyService)); + applicationServices.set(IClaudeProxyService, claudeProxyService); + const claudeAgentSdkService = applicationInstantiationService.createInstance(ClaudeAgentSdkService); + applicationServices.set(IClaudeAgentSdkService, claudeAgentSdkService); + const codexProxyService = disposables.add(applicationInstantiationService.createInstance(CodexProxyService)); + applicationServices.set(ICodexProxyService, codexProxyService); + const agentHostOTelService = disposables.add(applicationInstantiationService.createInstance(AgentHostOTelService, fetchFn)); + applicationServices.set(IAgentHostOTelService, agentHostOTelService); // BYOK is unsupported in the remote agent host (no extension host runs // next to it to serve the renderer LM API). Inject null implementations // to satisfy CopilotAgent / CopilotSessionLauncher DI. - agentServices.set(IByokLmBridgeRegistry, new NullByokLmBridgeRegistry()); - agentServices.set(IByokLmProxyService, new NullByokLmProxyService()); - const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent)); + applicationServices.set(IByokLmBridgeRegistry, new NullByokLmBridgeRegistry()); + applicationServices.set(IByokLmProxyService, new NullByokLmProxyService()); + const copilotAgent = disposables.add(applicationInstantiationService.createInstance(CopilotAgent)); agentService.registerProvider(copilotAgent); log('CopilotAgent registered'); // Claude and Codex providers are gated on two things: @@ -324,7 +324,7 @@ async function main(): Promise { // `node_modules` in dev; built/shipped installs use the env-var // override or `product.agentSdks.codex`. if (isAgentEnabled(process.env[AgentHostClaudeAgentEnabledEnvVar], true) && (!environmentService.isBuilt || agentSdkDownloader.isAvailable(ClaudeSdkPackage))) { - const claudeAgent = disposables.add(instantiationService.createInstance(ClaudeAgent)); + const claudeAgent = disposables.add(applicationInstantiationService.createInstance(ClaudeAgent)); agentService.registerProvider(claudeAgent); log('ClaudeAgent registered'); } @@ -339,7 +339,7 @@ async function main(): Promise { const enabledByRootConfig = agentConfigurationService.getRootValue(platformRootSchema, AgentHostCodexEnabledConfigKey) === true; if (enabledByEnv || enabledByRootConfig) { codexRegistered = true; - const codexAgent = disposables.add(instantiationService.createInstance(CodexAgent)); + const codexAgent = disposables.add(applicationInstantiationService.createInstance(CodexAgent)); agentService.registerProvider(codexAgent); log('CodexAgent registered'); } @@ -383,7 +383,7 @@ async function main(): Promise { // lifetime, rather than inside `AgentHostService`: a service that arms a // recurring timer in its constructor is one that no faked-timer unit test // can ever drain. - disposables.add(instantiationService.createInstance(AgentModelRefreshScheduler, agentService.agents, agentService.onDidStartTurn, MODEL_REFRESH_INTERVAL_MS)); + disposables.add(applicationInstantiationService.createInstance(AgentModelRefreshScheduler, agentService.agents, agentService.onDidStartTurn, MODEL_REFRESH_INTERVAL_MS)); // WebSocket server const wsServer = disposables.add(await WebSocketProtocolServer.create({ @@ -392,7 +392,7 @@ async function main(): Promise { connectionTokenValidate: options.connectionToken ? token => token === options.connectionToken : undefined, - }, logService, { instantiationService, logsHome: environmentService.logsHome })); + }, logService, { instantiationService: applicationInstantiationService, logsHome: environmentService.logsHome })); const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider()); @@ -400,7 +400,7 @@ async function main(): Promise { const connectionTelemetryTracker = disposables.add(new AgentHostClientConnectionTelemetryTracker()); // Wire up protocol handler - disposables.add(instantiationService.createInstance( + disposables.add(applicationInstantiationService.createInstance( ProtocolServerHandler, agentService, agentService.stateManager, @@ -475,7 +475,7 @@ async function main(): Promise { logService.warn('[AgentHostServer] Timed out waiting for persistence writes to flush; exiting anyway.'); }); disposables.dispose(); - rootInstantiationService.dispose(); + bootstrapInstantiationService.dispose(); loggerService?.dispose(); process.exit(0); } diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index b25332ac800f1c..46585d48a1466f 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -559,8 +559,8 @@ export class AgentService extends Disposable implements IAgentService { constructor( options: IAgentServiceOptions, - services: ServiceCollection, - @IInstantiationService instantiationService: IInstantiationService, + applicationServices: ServiceCollection, + @IInstantiationService applicationInstantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @IFileService private readonly _fileService: IFileService, @ISessionDataService private readonly _sessionDataService: ISessionDataService, @@ -632,13 +632,13 @@ export class AgentService extends Disposable implements IAgentService { })); this._storageService = this._register(new AgentHostStorageService(options.storageResource, this._logService)); updateAgentHostTelemetryLevelFromConfig(this._telemetryService, this._stateManager.rootState.config?.values); - services.set(IAgentService, this); - services.set(IAgentConfigurationService, configurationService); - services.set(IAgentHostStateManager, this._stateManager); - services.set(IAgentHostStorageService, this._storageService); - services.set(IAgentHostManagedSettingsService, this._managedSettingsService); - this._gitHubEndpointService = this._register(instantiationService.createInstance(AgentHostGitHubEndpointService)); - services.set(IAgentHostGitHubEndpointService, this._gitHubEndpointService); + applicationServices.set(IAgentService, this); + applicationServices.set(IAgentConfigurationService, configurationService); + applicationServices.set(IAgentHostStateManager, this._stateManager); + applicationServices.set(IAgentHostStorageService, this._storageService); + applicationServices.set(IAgentHostManagedSettingsService, this._managedSettingsService); + this._gitHubEndpointService = this._register(applicationInstantiationService.createInstance(AgentHostGitHubEndpointService)); + applicationServices.set(IAgentHostGitHubEndpointService, this._gitHubEndpointService); // A GitHub Enterprise URI change repoints every agent's GitHub resource // identity to a different authorization server, so the client must obtain a // token for the new resource. One root-channel `auth/required` covers all @@ -649,9 +649,9 @@ export class AgentService extends Disposable implements IAgentService { reason: AuthRequiredReason.Required, }); })); - const agentHostOctoKitService = instantiationService.createInstance(AgentHostOctoKitService, fetchFn); - services.set(IAgentHostOctoKitService, agentHostOctoKitService); - const gitHubService = this._register(instantiationService.createInstance(GitHubService, { + const agentHostOctoKitService = applicationInstantiationService.createInstance(AgentHostOctoKitService, fetchFn); + applicationServices.set(IAgentHostOctoKitService, agentHostOctoKitService); + const gitHubService = this._register(applicationInstantiationService.createInstance(GitHubService, { endpoint: this._gitHubEndpointService, tokenProvider: { getToken: () => { @@ -664,61 +664,61 @@ export class AgentService extends Disposable implements IAgentService { }, fetch: fetchFn, })); - services.set(IGitHubService, gitHubService); - this._copilotApiService = options.copilotApiService ?? instantiationService.createInstance(CopilotApiService, fetchFn); - services.set(ICopilotApiService, this._copilotApiService); - this._customizationEnablementService = this._register(instantiationService.createInstance(AgentHostCustomizationEnablementService)); - services.set(IAgentHostCustomizationEnablementService, this._customizationEnablementService); - - this._gitStateService = this._register(instantiationService.createInstance(AgentHostGitStateService)); - services.set(IAgentHostGitStateService, this._gitStateService); - this._agentMergeController = this._register(instantiationService.createInstance(AgentMergeController, { + applicationServices.set(IGitHubService, gitHubService); + this._copilotApiService = options.copilotApiService ?? applicationInstantiationService.createInstance(CopilotApiService, fetchFn); + applicationServices.set(ICopilotApiService, this._copilotApiService); + this._customizationEnablementService = this._register(applicationInstantiationService.createInstance(AgentHostCustomizationEnablementService)); + applicationServices.set(IAgentHostCustomizationEnablementService, this._customizationEnablementService); + + this._gitStateService = this._register(applicationInstantiationService.createInstance(AgentHostGitStateService)); + applicationServices.set(IAgentHostGitStateService, this._gitStateService); + this._agentMergeController = this._register(applicationInstantiationService.createInstance(AgentMergeController, { startTurn: (session, turnId, prompt) => this._startAgentMergePrompt(session, turnId, prompt), cancelTurn: (session, turnId) => this._cancelAgentMergePrompt(session, turnId), getAutonomousSessionConfig: (session, config) => this._findProviderForSession(session)?.getAutonomousSessionConfig?.(config), })); - this._checkpointService = this._register(instantiationService.createInstance(AgentHostCheckpointService)); - services.set(IAgentHostCheckpointService, this._checkpointService); + this._checkpointService = this._register(applicationInstantiationService.createInstance(AgentHostCheckpointService)); + applicationServices.set(IAgentHostCheckpointService, this._checkpointService); - this._promptCache = instantiationService.createInstance(AgentHostPromptCache); - services.set(IAgentHostPromptCache, this._promptCache); - this._sessionTitleSignal = this._register(instantiationService.createInstance(AgentHostSessionTitleSignal)); - services.set(IAgentHostSessionTitleSignal, this._sessionTitleSignal); + this._promptCache = applicationInstantiationService.createInstance(AgentHostPromptCache); + applicationServices.set(IAgentHostPromptCache, this._promptCache); + this._sessionTitleSignal = this._register(applicationInstantiationService.createInstance(AgentHostSessionTitleSignal)); + applicationServices.set(IAgentHostSessionTitleSignal, this._sessionTitleSignal); // The subscription service manages the lifecycle of changeset subscriptions. The service // is also consulted by other services when refreshing changesets and changeset operations. - this._changesetSubscriptions = instantiationService.createInstance(AgentHostChangesetSubscriptionService); - services.set(IAgentHostChangesetSubscriptionService, this._changesetSubscriptions); + this._changesetSubscriptions = applicationInstantiationService.createInstance(AgentHostChangesetSubscriptionService); + applicationServices.set(IAgentHostChangesetSubscriptionService, this._changesetSubscriptions); // The operation contribution service manages the lifecycle of changeset operations. - this._changesetOperationService = this._register(instantiationService.createInstance(AgentHostChangesetOperationService)); - services.set(IAgentHostChangesetOperationService, this._changesetOperationService); + this._changesetOperationService = this._register(applicationInstantiationService.createInstance(AgentHostChangesetOperationService)); + applicationServices.set(IAgentHostChangesetOperationService, this._changesetOperationService); // The changes review service is responsible for managing review/unreview state for changeset changes. - this._reviewService = this._register(instantiationService.createInstance(AgentHostReviewService)); - services.set(IAgentHostReviewService, this._reviewService); + this._reviewService = this._register(applicationInstantiationService.createInstance(AgentHostReviewService)); + applicationServices.set(IAgentHostReviewService, this._reviewService); // The changeset service is responsible for computing, publishing, and persisting changesets. - this._changesets = this._register(instantiationService.createInstance(AgentHostChangesetService)); - services.set(IAgentHostChangesetService, this._changesets); + this._changesets = this._register(applicationInstantiationService.createInstance(AgentHostChangesetService)); + applicationServices.set(IAgentHostChangesetService, this._changesets); // The coordinator owns all AgentService-side orchestration of the changeset feature: lifecycle // hooks, listSessions overlay, subscription URI routing, and the deferred-refresh state machine. - this._changesetCoordinator = this._register(instantiationService.createInstance(AgentHostChangesetCoordinator)); + this._changesetCoordinator = this._register(applicationInstantiationService.createInstance(AgentHostChangesetCoordinator)); this._register(this._stateManager.onDidChangeSessionActiveTurn(e => this._changesetCoordinator.onSessionTurnActiveChanged(e.session, e.active))); // Register the changeset operation contributions. - this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostCommitOperationContribution))); - this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostPullRequestOperationContribution))); - this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostMergeOperationContribution))); - this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostSyncOperationContribution))); - this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostDiscardChangesOperationContribution))); - - this._completions = this._register(instantiationService.createInstance(AgentHostCompletions)); - services.set(IAgentHostCompletions, this._completions); + this._register(this._changesetOperationService.registerContribution(applicationInstantiationService.createInstance(AgentHostCommitOperationContribution))); + this._register(this._changesetOperationService.registerContribution(applicationInstantiationService.createInstance(AgentHostPullRequestOperationContribution))); + this._register(this._changesetOperationService.registerContribution(applicationInstantiationService.createInstance(AgentHostMergeOperationContribution))); + this._register(this._changesetOperationService.registerContribution(applicationInstantiationService.createInstance(AgentHostSyncOperationContribution))); + this._register(this._changesetOperationService.registerContribution(applicationInstantiationService.createInstance(AgentHostDiscardChangesOperationContribution))); + + this._completions = this._register(applicationInstantiationService.createInstance(AgentHostCompletions)); + applicationServices.set(IAgentHostCompletions, this._completions); // Built-in generic provider: completes files in the session's workspace folder. - const workspaceFiles = this._register(instantiationService.createInstance(AgentHostWorkspaceFiles)); + const workspaceFiles = this._register(applicationInstantiationService.createInstance(AgentHostWorkspaceFiles)); this._register(this._completions.registerProvider( new AgentHostFileCompletionProvider(this._stateManager, workspaceFiles, this._logService), )); @@ -746,12 +746,12 @@ export class AgentService extends Disposable implements IAgentService { // Created before AgentSideEffects and registered in the local scope so // AgentSideEffects can consume it via DI (for inline `!command` // execution). - this._terminalManager = this._register(instantiationService.createInstance(AgentHostTerminalManager)); - services.set(IAgentHostTerminalManager, this._terminalManager); + this._terminalManager = this._register(applicationInstantiationService.createInstance(AgentHostTerminalManager)); + applicationServices.set(IAgentHostTerminalManager, this._terminalManager); this._localTurns = new AgentHostLocalTurns(this._sessionDataService, this._logService); - this._sideEffects = this._register(instantiationService.createInstance(AgentSideEffects, this._stateManager, this._customizationEnablementService, { + this._sideEffects = this._register(applicationInstantiationService.createInstance(AgentSideEffects, this._stateManager, this._customizationEnablementService, { getAgent: session => this._findProviderForSession(session), sessionDataService: this._sessionDataService, localTurns: this._localTurns, @@ -797,7 +797,7 @@ export class AgentService extends Disposable implements IAgentService { // state. The set of groups (and their display) is the single source of // truth in `serverToolGroups.ts`; the session-management group's runtime // dependency (this service) is injected via the accessor. - const agentMergeTools = instantiationService.createInstance( + const agentMergeTools = applicationInstantiationService.createInstance( AgentMergeTools, () => this._agentMergeController.isEnabled(), session => this._agentMergeController.getTurnContext(session), diff --git a/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts b/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts index 5b7055d96476a1..daf073e5dd6275 100644 --- a/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts +++ b/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts @@ -55,7 +55,7 @@ export function createTestAgentService( resolveProxy: async () => undefined, fetch: fetchFn, }; - const instantiationService = new InstantiationService(new ServiceCollection( + const bootstrapInstantiationService = new InstantiationService(new ServiceCollection( [ILogService, logService], [IFileService, fileService], [ISessionDataService, sessionDataService], @@ -74,13 +74,13 @@ export function createTestAgentService( orchestratorDatabase, now, }; - const application = createAgentHostApplication(instantiationService, options, (applicationInstantiationService, services) => { - return applicationInstantiationService.createInstance(TestAgentService, options, services); + const application = createAgentHostApplication(bootstrapInstantiationService, options, (applicationInstantiationService, applicationServices) => { + return applicationInstantiationService.createInstance(TestAgentService, options, applicationServices); }); const service = application.agentService; if (!fileMonitorService) { service.registerTestDependency(effectiveFileMonitorService); } - service.registerTestDependency(instantiationService); + service.registerTestDependency(bootstrapInstantiationService); return service; } From df57be9b51e93dace5205eebb1b97d1a5b23197a Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 18 Aug 2026 19:37:46 -0700 Subject: [PATCH 3/6] Share Agent Host service initialization Use one strict DI scope and centralize common base and provider service setup for both Agent Host entry points. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/node/agentHostApplication.ts | 32 ---- .../agentHost/node/agentHostBootstrap.ts | 154 +++++++++++++++- .../platform/agentHost/node/agentHostMain.ts | 174 ++++++------------ .../agentHost/node/agentHostServerMain.ts | 150 ++++----------- .../platform/agentHost/node/agentService.ts | 98 +++++----- .../test/node/agentServiceTestUtils.ts | 13 +- 6 files changed, 295 insertions(+), 326 deletions(-) delete mode 100644 src/vs/platform/agentHost/node/agentHostApplication.ts diff --git a/src/vs/platform/agentHost/node/agentHostApplication.ts b/src/vs/platform/agentHost/node/agentHostApplication.ts deleted file mode 100644 index 11978a892a0336..00000000000000 --- a/src/vs/platform/agentHost/node/agentHostApplication.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IInstantiationService } from '../../instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; -import { AgentService, IAgentServiceOptions } from './agentService.js'; - -export interface IAgentHostApplication { - readonly agentService: T; - readonly applicationInstantiationService: IInstantiationService; - readonly applicationServices: ServiceCollection; -} - -type AgentServiceFactory = (applicationInstantiationService: IInstantiationService, applicationServices: ServiceCollection) => T; - -export function createAgentHostApplication(bootstrapInstantiationService: IInstantiationService, options: IAgentServiceOptions): IAgentHostApplication; -export function createAgentHostApplication(bootstrapInstantiationService: IInstantiationService, options: IAgentServiceOptions, factory: AgentServiceFactory): IAgentHostApplication; -export function createAgentHostApplication(bootstrapInstantiationService: IInstantiationService, options: IAgentServiceOptions, factory?: AgentServiceFactory): IAgentHostApplication { - const applicationServices = new ServiceCollection(); - const applicationInstantiationService = bootstrapInstantiationService.createChild(applicationServices); - try { - const agentService = factory - ? factory(applicationInstantiationService, applicationServices) - : applicationInstantiationService.createInstance(AgentService, options, applicationServices); - return { agentService, applicationInstantiationService, applicationServices }; - } catch (error) { - applicationInstantiationService.dispose(); - throw error; - } -} diff --git a/src/vs/platform/agentHost/node/agentHostBootstrap.ts b/src/vs/platform/agentHost/node/agentHostBootstrap.ts index 3baef2a365984c..b01bf155243441 100644 --- a/src/vs/platform/agentHost/node/agentHostBootstrap.ts +++ b/src/vs/platform/agentHost/node/agentHostBootstrap.ts @@ -4,23 +4,95 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from '../../../base/common/lifecycle.js'; +import { Event } from '../../../base/common/event.js'; import { joinPath } from '../../../base/common/resources.js'; +import { URI } from '../../../base/common/uri.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { ConfigurationService } from '../../configuration/common/configurationService.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { IFileService } from '../../files/common/files.js'; +import { IInstantiationService } from '../../instantiation/common/instantiation.js'; +import { InstantiationService } from '../../instantiation/common/instantiationService.js'; import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; -import { ILogService } from '../../log/common/log.js'; +import { ILoggerService, ILogService } from '../../log/common/log.js'; import { IPolicyService, NullPolicyService } from '../../policy/common/policy.js'; +import { IProductService } from '../../product/common/productService.js'; import { IRequestService } from '../../request/common/request.js'; +import { ITelemetryService } from '../../telemetry/common/telemetry.js'; +import { ISandboxHelperService } from '../../sandbox/common/sandboxHelperService.js'; +import { SandboxHelperService } from '../../sandbox/node/sandboxHelper.js'; +import { IWindowsMxcTerminalSandboxRuntime, WindowsMxcTerminalSandboxRuntime } from '../../sandbox/common/terminalSandboxMxcRuntime.js'; +import { IAgentPluginManager } from '../common/agentPluginManager.js'; +import { IDiffComputeService } from '../common/diffComputeService.js'; +import { IAgentEditAttributionService } from '../common/fileEditAttribution.js'; +import { IAgentHostGitService } from '../common/agentHostGitService.js'; +import { IAgentHostOTelService } from '../common/otel/agentHostOTelService.js'; +import { ISessionDataService } from '../common/sessionDataService.js'; +import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js'; +import { AgentHostGitService } from './agentHostGitService.js'; +import { AgentHostOTelService } from './otel/agentHostOTelService.js'; import { AgentHostProxyResolver, IAgentHostProxyResolver } from './agentHostProxyResolver.js'; import { AgentHostRequestService } from './agentHostRequestService.js'; +import { createAgentHostTelemetryService, IAgentHostTelemetryService } from './agentHostTelemetryService.js'; +import { AgentService, IAgentServiceOptions } from './agentService.js'; +import { INetworkDiagnosticsService, NetworkDiagnosticsService } from './networkDiagnosticsService.js'; +import { AgentPluginManager } from './agentPluginManager.js'; +import { NodeWorkerDiffComputeService } from './diffComputeService.js'; +import { AgentEditAttributionService } from './shared/agentEditAttributionService.js'; +import { EditArcReporterService, IEditArcReporterService } from './shared/editArcReporter.js'; +import { EditSurvivalReporterFactory, IEditSurvivalReporterFactory } from './shared/editSurvivalReporter.js'; +import { IAgentHostWorktreeIsolation, WorktreeIsolation } from './shared/worktreeIsolation.js'; +import { AgentSdkDownloader, IAgentSdkDownloader, type IAgentSdkDownloadProgress } from './agentSdkDownloader.js'; +import { IClaudeAgentSdkService, ClaudeAgentSdkService } from './claude/claudeAgentSdkService.js'; +import { ClaudeProxyService, IClaudeProxyService } from './claude/claudeProxyService.js'; +import { CodexProxyService, ICodexProxyService } from './codex/codexProxyService.js'; +import { IByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; +import { ByokLmProxyService, IByokLmProxyService } from './copilot/byokLmProxyService.js'; export interface IAgentHostNetworkServices { readonly proxyResolver: IAgentHostProxyResolver; readonly requestService: IRequestService; } +export interface ICreateAgentHostServicesOptions { + readonly environmentService: INativeEnvironmentService; + readonly productService: IProductService; + readonly logService: ILogService; + readonly loggerService: ILoggerService | undefined; + readonly fileService: IFileService; + readonly sessionDataService: ISessionDataService; + readonly disposables: DisposableStore; + readonly disableTelemetry?: boolean; + readonly agentServiceOptions: IAgentServiceOptions; +} + +export interface IAgentHostServices { + readonly services: ServiceCollection; + readonly instantiationService: IInstantiationService; + readonly agentService: AgentService; + readonly proxyResolver: IAgentHostProxyResolver; + readonly telemetryService: IAgentHostTelemetryService; + readonly fetchFn: typeof globalThis.fetch; +} + +export interface IRegisterAgentHostProviderServicesOptions { + readonly services: ServiceCollection; + readonly instantiationService: IInstantiationService; + readonly agentService: AgentService; + readonly environmentService: INativeEnvironmentService; + readonly fileService: IFileService; + readonly logService: ILogService; + readonly disposables: DisposableStore; + readonly fetchFn: typeof globalThis.fetch; + readonly byokBridgeRegistry: IByokLmBridgeRegistry; + readonly byokLmProxyService?: IByokLmProxyService; +} + +export interface IAgentHostProviderServices { + readonly agentSdkDownloader: AgentSdkDownloader; + readonly sdkDownloadProgress: Event; +} + /** * Register `IPolicyService`, `IConfigurationService`, `IAgentHostProxyResolver`, * and `IRequestService` into the agent host's DI container — the services that @@ -42,21 +114,91 @@ export interface IAgentHostNetworkServices { * in `'local'` mode because the agent host runs on the user's machine. */ export async function registerAgentHostNetworkServices( - diServices: ServiceCollection, + services: ServiceCollection, fileService: IFileService, environmentService: INativeEnvironmentService, logService: ILogService, disposables: DisposableStore, ): Promise { const policyService = new NullPolicyService(); - diServices.set(IPolicyService, policyService); + services.set(IPolicyService, policyService); const settingsResource = joinPath(environmentService.appSettingsHome, 'settings.json'); const configurationService = disposables.add(new ConfigurationService(settingsResource, fileService, policyService, logService)); await configurationService.initialize(); - diServices.set(IConfigurationService, configurationService); + services.set(IConfigurationService, configurationService); const proxyResolver = disposables.add(new AgentHostProxyResolver(configurationService, logService)); - diServices.set(IAgentHostProxyResolver, proxyResolver); + services.set(IAgentHostProxyResolver, proxyResolver); const requestService = disposables.add(new AgentHostRequestService(configurationService, environmentService, logService, proxyResolver)); - diServices.set(IRequestService, requestService); + services.set(IRequestService, requestService); return { proxyResolver, requestService }; } + +export async function createAgentHostServices(options: ICreateAgentHostServicesOptions): Promise { + const { environmentService, productService, logService, loggerService, fileService, sessionDataService, disposables } = options; + const services = new ServiceCollection( + [INativeEnvironmentService, environmentService], + [ILogService, logService], + [IFileService, fileService], + [ISessionDataService, sessionDataService], + [IProductService, productService], + ); + const networkServices = await registerAgentHostNetworkServices(services, fileService, environmentService, logService, disposables); + const proxyResolver = networkServices.proxyResolver; + const fetchFn = proxyResolver.fetch.bind(proxyResolver); + const telemetryService = await createAgentHostTelemetryService({ + environmentService, + productService, + fileService, + loggerService, + logService, + disposables, + disableTelemetry: options.disableTelemetry, + fetchFn, + requestService: networkServices.requestService, + }); + services.set(ITelemetryService, telemetryService); + const instantiationService = new InstantiationService(services, /*strict*/ true); + try { + const fileMonitorService = disposables.add(instantiationService.createInstance(AgentHostFileMonitorService)); + services.set(IAgentHostFileMonitorService, fileMonitorService); + services.set(IWindowsMxcTerminalSandboxRuntime, instantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); + services.set(ISandboxHelperService, new SandboxHelperService()); + services.set(IAgentHostGitService, instantiationService.createInstance(AgentHostGitService)); + const agentService = instantiationService.createInstance(AgentService, options.agentServiceOptions, services); + const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); + services.set(INetworkDiagnosticsService, networkDiagnosticsService); + agentService.setNetworkDiagnosticsService(networkDiagnosticsService); + return { services, instantiationService, agentService, proxyResolver, telemetryService, fetchFn }; + } catch (error) { + instantiationService.dispose(); + throw error; + } +} + +export function registerAgentHostProviderServices(options: IRegisterAgentHostProviderServicesOptions): IAgentHostProviderServices { + const { services, instantiationService, agentService, environmentService, fileService, logService, disposables, fetchFn } = options; + services.set(IAgentPluginManager, new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService)); + services.set(IDiffComputeService, disposables.add(instantiationService.createInstance(NodeWorkerDiffComputeService))); + const editAttributionService = disposables.add(instantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); + services.set(IAgentEditAttributionService, editAttributionService); + agentService.setEditAttributionService(editAttributionService); + services.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); + services.set(IEditArcReporterService, disposables.add(instantiationService.createInstance(EditArcReporterService, undefined))); + + const worktreeIsolation = disposables.add(instantiationService.createInstance(WorktreeIsolation, undefined)); + services.set(IAgentHostWorktreeIsolation, worktreeIsolation); + agentService.setWorktreeIsolation(worktreeIsolation); + + const agentSdkDownloader = disposables.add(instantiationService.createInstance(AgentSdkDownloader)); + services.set(IAgentSdkDownloader, agentSdkDownloader); + services.set(IClaudeProxyService, disposables.add(instantiationService.createInstance(ClaudeProxyService))); + services.set(IClaudeAgentSdkService, instantiationService.createInstance(ClaudeAgentSdkService)); + services.set(ICodexProxyService, disposables.add(instantiationService.createInstance(CodexProxyService))); + services.set(IAgentHostOTelService, disposables.add(instantiationService.createInstance(AgentHostOTelService, fetchFn))); + + services.set(IByokLmBridgeRegistry, options.byokBridgeRegistry); + const byokLmProxyService = options.byokLmProxyService ?? disposables.add(instantiationService.createInstance(ByokLmProxyService)); + services.set(IByokLmProxyService, byokLmProxyService); + + return { agentSdkDownloader, sdkDownloadProgress: agentSdkDownloader.onDidDownloadProgress }; +} diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index c77b755de5e49b..f0c9e7ca7e5ddc 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -20,29 +20,20 @@ import { AgentHostClaudeAgentEnabledEnvVar, AgentHostCodexAgentEnabledEnvVar, Ag import { AgentHostCodexEnabledConfigKey, platformRootSchema } from '../common/agentHostSchema.js'; import { AgentModelRefreshScheduler, MODEL_REFRESH_INTERVAL_MS } from './agentModelRefreshScheduler.js'; import { AgentService } from './agentService.js'; -import { createAgentHostApplication } from './agentHostApplication.js'; import { CopilotAgent } from './copilot/copilotAgent.js'; -import { IAgentHostWorktreeIsolation, WorktreeIsolation } from './shared/worktreeIsolation.js'; import { ClaudeAgent } from './claude/claudeAgent.js'; -import { ClaudeAgentSdkService, ClaudeSdkPackage, IClaudeAgentSdkService } from './claude/claudeAgentSdkService.js'; -import { ClaudeProxyService, IClaudeProxyService } from './claude/claudeProxyService.js'; +import { ClaudeSdkPackage } from './claude/claudeAgentSdkService.js'; import { CodexAgent, CodexSdkPackage } from './codex/codexAgent.js'; import { createCodexProviderConfiguration } from './codex/codexProviderConfiguration.js'; -import { CodexProxyService, ICodexProxyService } from './codex/codexProxyService.js'; -import { ByokLmProxyService, IByokLmProxyService } from './copilot/byokLmProxyService.js'; -import { ByokLmBridgeRegistry, IByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; +import { ByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; import { IAgentHostProxyResolver } from './agentHostProxyResolver.js'; -import { INetworkDiagnosticsService, NetworkDiagnosticsService } from './networkDiagnosticsService.js'; -import { AgentSdkDownloader, IAgentSdkDownloader, type IAgentSdkDownloadProgress } from './agentSdkDownloader.js'; -import { IAgentHostOTelService } from '../common/otel/agentHostOTelService.js'; -import { AgentHostOTelService } from './otel/agentHostOTelService.js'; +import { type IAgentSdkDownloadProgress } from './agentSdkDownloader.js'; import { ProtocolServerHandler } from './protocolServerHandler.js'; import { AgentHostClientConnectionTelemetryTracker } from './agentHostClientConnectionTelemetry.js'; import { WebSocketProtocolServer } from './webSocketTransport.js'; import { MessagePortProtocolServer } from './messagePortProtocolServer.js'; import { cleanupLocalAgentHostEndpointMetadataSync, cleanupLocalAgentHostEndpointSocketSync, createLocalAgentHostEndpointMetadata, prepareLocalAgentHostEndpointMetadataDirectory, prepareLocalAgentHostEndpointSocketDirectory, publishLocalAgentHostEndpointMetadata, type ILocalAgentHostEndpointMetadata } from './localAgentHostMetadata.js'; import { AgentHostManagementService } from './agentHostManagementService.js'; -import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { NativeEnvironmentService } from '../../environment/node/environmentService.js'; import { parseArgs, OPTIONS } from '../../environment/node/argv.js'; import { getLogLevel, ILogService, isDevConsoleLogForwardingEnabled, registerDevConsoleLogForwarder } from '../../log/common/log.js'; @@ -55,38 +46,18 @@ import product from '../../product/common/product.js'; import { IProductService } from '../../product/common/productService.js'; import { localize } from '../../../nls.js'; import { FileService } from '../../files/common/fileService.js'; -import { IFileService } from '../../files/common/files.js'; import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.js'; import { Schemas } from '../../../base/common/network.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; -import { InstantiationService } from '../../instantiation/common/instantiationService.js'; -import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; -import { registerAgentHostNetworkServices } from './agentHostBootstrap.js'; +import { createAgentHostServices, registerAgentHostProviderServices } from './agentHostBootstrap.js'; import { BANG_COMMAND_PREFIX } from './agentHostBangCommand.js'; import { SessionDataService } from './sessionDataService.js'; -import { ISessionDataService } from '../common/sessionDataService.js'; -import { IWindowsMxcTerminalSandboxRuntime, WindowsMxcTerminalSandboxRuntime } from '../../sandbox/common/terminalSandboxMxcRuntime.js'; -import { ISandboxHelperService } from '../../sandbox/common/sandboxHelperService.js'; -import { SandboxHelperService } from '../../sandbox/node/sandboxHelper.js'; -import { IDiffComputeService } from '../common/diffComputeService.js'; -import { IAgentEditAttributionService } from '../common/fileEditAttribution.js'; -import { NodeWorkerDiffComputeService } from './diffComputeService.js'; -import { AgentEditAttributionService } from './shared/agentEditAttributionService.js'; -import { IEditSurvivalReporterFactory, EditSurvivalReporterFactory } from './shared/editSurvivalReporter.js'; -import { EditArcReporterService, IEditArcReporterService } from './shared/editArcReporter.js'; import { AgentHostClientFileSystemProvider } from '../common/agentHostClientFileSystemProvider.js'; import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js'; import { AGENT_HOST_CLIENT_BYOK_LM_CHANNEL, createAgentHostClientByokLmConnection } from '../common/agentHostClientByokLmChannel.js'; import { AGENT_HOST_CLIENT_PROXY_CHANNEL, createAgentHostClientProxyConnection } from '../common/agentHostClientProxyChannel.js'; -import { IAgentPluginManager } from '../common/agentPluginManager.js'; -import { AgentPluginManager } from './agentPluginManager.js'; -import { AgentHostGitService } from './agentHostGitService.js'; -import { IAgentHostGitService } from '../common/agentHostGitService.js'; -import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js'; import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js'; import { join } from '../../../base/common/path.js'; -import { createAgentHostTelemetryService } from './agentHostTelemetryService.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import ErrorTelemetry from '../../telemetry/node/errorTelemetry.js'; import { AgentHostLaunchKindEnvVar, readAgentHostLaunchKind, type AgentHostLaunchKind } from '../common/agentHostTelemetry.js'; @@ -148,86 +119,47 @@ async function startAgentHost(): Promise { // Create the real service implementation that lives in this process let agentService: AgentService; - let applicationInstantiationService: IInstantiationService; - let bootstrapInstantiationService: IInstantiationService | undefined; + let instantiationService!: IInstantiationService; // Hoisted out of the `try` below so the protocol handlers (constructed // after the block) can forward agent-SDK download progress to clients. let sdkDownloadProgress: Event | undefined; let byokLmBridgeRegistry: ByokLmBridgeRegistry; - let proxyResolver: IAgentHostProxyResolver | undefined; + let proxyResolver!: IAgentHostProxyResolver; const hostLaunchKind = readAgentHostLaunchKind(process.env[AgentHostLaunchKindEnvVar]); const connectionTelemetryTracker = disposables.add(new AgentHostClientConnectionTelemetryTracker()); try { - // Build the process DI container and network stack before telemetry so every - // outbound fetch, including restricted telemetry, uses the same proxy resolver. - const bootstrapServices = new ServiceCollection(); - bootstrapServices.set(INativeEnvironmentService, environmentService); - bootstrapServices.set(ILogService, logService); - bootstrapServices.set(IFileService, fileService); - bootstrapServices.set(ISessionDataService, sessionDataService); - bootstrapServices.set(IProductService, productService); - const networkServices = await registerAgentHostNetworkServices(bootstrapServices, fileService, environmentService, logService, disposables); - proxyResolver = networkServices.proxyResolver; - const fetchFn = proxyResolver.fetch.bind(proxyResolver); - const telemetryService = await createAgentHostTelemetryService({ environmentService, productService, fileService, loggerService, logService, disposables, fetchFn, requestService: networkServices.requestService }); - errorTelemetry.value = new ErrorTelemetry(telemetryService); - bootstrapServices.set(ITelemetryService, telemetryService); - bootstrapInstantiationService = new InstantiationService(bootstrapServices, /*strict*/ true); - const fileMonitorService = disposables.add(bootstrapInstantiationService.createInstance(AgentHostFileMonitorService)); - bootstrapServices.set(IAgentHostFileMonitorService, fileMonitorService); - bootstrapServices.set(IWindowsMxcTerminalSandboxRuntime, bootstrapInstantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); - bootstrapServices.set(ISandboxHelperService, new SandboxHelperService()); - const gitService = bootstrapInstantiationService.createInstance(AgentHostGitService); - bootstrapServices.set(IAgentHostGitService, gitService); - // Register the agent SDK downloader BEFORE any service that injects it - // (ClaudeAgentSdkService and CodexAgent below). The downloader resolves - // dev-override env var → on-disk cache → product.agentSdks download. - const agentSdkDownloader = disposables.add(bootstrapInstantiationService.createInstance(AgentSdkDownloader)); - bootstrapServices.set(IAgentSdkDownloader, agentSdkDownloader); - sdkDownloadProgress = agentSdkDownloader.onDidDownloadProgress; - const claudeAgentSdkService = bootstrapInstantiationService.createInstance(ClaudeAgentSdkService); - bootstrapServices.set(IClaudeAgentSdkService, claudeAgentSdkService); - // BYOK infrastructure is always wired; synchronized root config gates model - // publication and per-session provider configuration. + const hostServices = await createAgentHostServices({ + environmentService, + productService, + logService, + loggerService, + fileService, + sessionDataService, + disposables, + agentServiceOptions: { + rootConfigResource, + providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], + hostLaunchKind, + storageResource, + }, + }); + agentService = hostServices.agentService; + instantiationService = hostServices.instantiationService; + proxyResolver = hostServices.proxyResolver; + errorTelemetry.value = new ErrorTelemetry(hostServices.telemetryService); + byokLmBridgeRegistry = new ByokLmBridgeRegistry(); - bootstrapServices.set(IByokLmBridgeRegistry, byokLmBridgeRegistry); - const byokLmProxyService = disposables.add(bootstrapInstantiationService.createInstance(ByokLmProxyService)); - bootstrapServices.set(IByokLmProxyService, byokLmProxyService); - const agentHostOTelService = disposables.add(bootstrapInstantiationService.createInstance(AgentHostOTelService, fetchFn)); - bootstrapServices.set(IAgentHostOTelService, agentHostOTelService); - const application = createAgentHostApplication(bootstrapInstantiationService, { - rootConfigResource, - providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], - hostLaunchKind, - storageResource, + const providerServices = registerAgentHostProviderServices({ + ...hostServices, + environmentService, + fileService, + logService, + disposables, + byokBridgeRegistry: byokLmBridgeRegistry, }); - agentService = application.agentService; - applicationInstantiationService = application.applicationInstantiationService; - const applicationServices = application.applicationServices; - const networkDiagnosticsService = applicationInstantiationService.createInstance(NetworkDiagnosticsService); - applicationServices.set(INetworkDiagnosticsService, networkDiagnosticsService); - agentService.setNetworkDiagnosticsService(networkDiagnosticsService); - const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService); - applicationServices.set(IAgentPluginManager, pluginManager); - const diffComputeService = disposables.add(new NodeWorkerDiffComputeService(logService)); - applicationServices.set(IDiffComputeService, diffComputeService); - const editAttributionService = disposables.add(applicationInstantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); - applicationServices.set(IAgentEditAttributionService, editAttributionService); - agentService.setEditAttributionService(editAttributionService); - applicationServices.set(IEditSurvivalReporterFactory, applicationInstantiationService.createInstance(EditSurvivalReporterFactory)); - const editArcReporterService = disposables.add(applicationInstantiationService.createInstance(EditArcReporterService, undefined)); - applicationServices.set(IEditArcReporterService, editArcReporterService); - // Host-owned worktree isolation controller: a single instance drives folder - // / worktree isolation for every agent, so providers stay unaware of it. It - // owns its branch-name generator, created from ICopilotApiService. - const worktreeIsolation = disposables.add(applicationInstantiationService.createInstance(WorktreeIsolation, undefined)); - applicationServices.set(IAgentHostWorktreeIsolation, worktreeIsolation); - agentService.setWorktreeIsolation(worktreeIsolation); - const claudeProxyService = disposables.add(applicationInstantiationService.createInstance(ClaudeProxyService)); - applicationServices.set(IClaudeProxyService, claudeProxyService); - const codexProxyService = disposables.add(applicationInstantiationService.createInstance(CodexProxyService)); - applicationServices.set(ICodexProxyService, codexProxyService); - agentService.registerProvider(applicationInstantiationService.createInstance(CopilotAgent)); + const agentSdkDownloader = providerServices.agentSdkDownloader; + sdkDownloadProgress = providerServices.sdkDownloadProgress; + agentService.registerProvider(instantiationService.createInstance(CopilotAgent)); // Claude and Codex providers are gated on two things: // 1. The user-facing enable toggle (`chat.agentHost.Agent.enabled`, // forwarded as an env var by the starters). Claude defaults to on, @@ -242,7 +174,7 @@ async function startAgentHost(): Promise { // If either gate fails, the provider is not registered and never appears // in the agent picker (matches the pre-CDN UX exactly). if (isAgentEnabled(process.env[AgentHostClaudeAgentEnabledEnvVar], true) && (!environmentService.isBuilt || agentSdkDownloader.isAvailable(ClaudeSdkPackage))) { - agentService.registerProvider(applicationInstantiationService.createInstance(ClaudeAgent)); + agentService.registerProvider(instantiationService.createInstance(ClaudeAgent)); } // Codex registration is one-way (register-on-enable): the env-var toggle // or the renderer-forwarded `codexAgentEnabled` root config enables it. @@ -258,14 +190,14 @@ async function startAgentHost(): Promise { const enabledByRootConfig = agentConfigurationService.getRootValue(platformRootSchema, AgentHostCodexEnabledConfigKey) === true; if (enabledByEnv || enabledByRootConfig) { codexRegistered = true; - agentService.registerProvider(applicationInstantiationService.createInstance(CodexAgent)); + agentService.registerProvider(instantiationService.createInstance(CodexAgent)); } }; registerCodexIfEnabled(); disposables.add(agentConfigurationService.onDidRootConfigChange(registerCodexIfEnabled)); } } catch (err) { - bootstrapInstantiationService?.dispose(); + instantiationService?.dispose(); logService.error('Failed to create AgentService', err); throw err; } @@ -277,7 +209,7 @@ async function startAgentHost(): Promise { // lifetime, rather than inside `AgentHostService`: a service that arms a // recurring timer in its constructor is one that no faked-timer unit test // can ever drain. - disposables.add(applicationInstantiationService.createInstance(AgentModelRefreshScheduler, agentService.agents, agentService.onDidStartTurn, MODEL_REFRESH_INTERVAL_MS)); + disposables.add(instantiationService.createInstance(AgentModelRefreshScheduler, agentService.agents, agentService.onDidStartTurn, MODEL_REFRESH_INTERVAL_MS)); // Surface agent-SDK download progress to clients as generic `progress` // notifications. The downloader fires process-global frames keyed by package @@ -325,7 +257,7 @@ async function startAgentHost(): Promise { }; try { // Handler for the renderer's MessagePort data plane. - const messagePortProtocolHandler = localDataPlaneDisposables.add(applicationInstantiationService.createInstance( + const messagePortProtocolHandler = localDataPlaneDisposables.add(instantiationService.createInstance( ProtocolServerHandler, agentService, agentService.stateManager, @@ -389,7 +321,7 @@ async function startAgentHost(): Promise { const localEndpoint = await startLocalAgentHostEndpoint( environmentService.userDataPath, logService, - applicationInstantiationService, + instantiationService, environmentService.logsHome, ); if (localEndpoint) { @@ -398,7 +330,7 @@ async function startAgentHost(): Promise { // publishing the metadata that advertises it, so a client can't connect // in the gap and be missed. localDataPlaneDisposables.add(localEndpoint.server); - const localEndpointProtocolHandler = localDataPlaneDisposables.add(applicationInstantiationService.createInstance( + const localEndpointProtocolHandler = localDataPlaneDisposables.add(instantiationService.createInstance( ProtocolServerHandler, agentService, agentService.stateManager, @@ -447,7 +379,7 @@ async function startAgentHost(): Promise { const wsServer = await WebSocketProtocolServer.create( { socketPath }, logService, - { instantiationService: applicationInstantiationService, logsHome: environmentService.logsHome }, + { instantiationService, logsHome: environmentService.logsHome }, ); if (protocolIngressDisposables.isDisposed) { wsServer.dispose(); @@ -455,7 +387,7 @@ async function startAgentHost(): Promise { } protocolIngressDisposables.add(wsServer); - const protocolHandler = protocolIngressDisposables.add(applicationInstantiationService.createInstance( + const protocolHandler = protocolIngressDisposables.add(instantiationService.createInstance( ProtocolServerHandler, agentService, agentService.stateManager, @@ -524,7 +456,7 @@ async function startAgentHost(): Promise { } }, }; - server.registerChannel(AgentHostIpcChannels.Management, ProxyChannel.fromService(applicationInstantiationService.createInstance( + server.registerChannel(AgentHostIpcChannels.Management, ProxyChannel.fromService(instantiationService.createInstance( AgentHostManagementService, agentService, connectionTrackerService, @@ -542,7 +474,7 @@ async function startAgentHost(): Promise { const configuredWebSocketServerStart = startWebSocketServer( agentService, clientFileSystemProvider, - applicationInstantiationService, + instantiationService, environmentService.logsHome, logService, otlpLogEmitter, @@ -561,7 +493,7 @@ async function startAgentHost(): Promise { agentService.dispose(); logService.dispose(); disposables.dispose(); - bootstrapInstantiationService?.dispose(); + instantiationService.dispose(); }); } @@ -573,7 +505,7 @@ interface ILocalAgentHostEndpoint { async function startLocalAgentHostEndpoint( userDataPath: string, logService: ILogService, - applicationInstantiationService: IInstantiationService, + instantiationService: IInstantiationService, logsHome: URI, ): Promise { let metadata: ILocalAgentHostEndpointMetadata | undefined; @@ -591,7 +523,7 @@ async function startLocalAgentHostEndpoint( connectionTokenValidate: token => token === endpointMetadata.connectionToken, }, logService, - { instantiationService: applicationInstantiationService, logsHome }, + { instantiationService, logsHome }, ); await server.whenListening; return { metadata: endpointMetadata, server }; @@ -635,7 +567,7 @@ function cleanupLocalAgentHostEndpoint( async function startWebSocketServer( agentService: AgentService, clientFileSystemProvider: AgentHostClientFileSystemProvider, - applicationInstantiationService: IInstantiationService, + instantiationService: IInstantiationService, logsHome: URI, logService: ILogService, otlpLogEmitter: OtlpLogEmitter, @@ -671,7 +603,7 @@ async function startWebSocketServer( : undefined, }, logService, - { instantiationService: applicationInstantiationService, logsHome }, + { instantiationService, logsHome }, ); if (disposables.isDisposed) { wsServer.dispose(); @@ -679,7 +611,7 @@ async function startWebSocketServer( } disposables.add(wsServer); - const protocolHandler = disposables.add(applicationInstantiationService.createInstance( + const protocolHandler = disposables.add(instantiationService.createInstance( ProtocolServerHandler, agentService, agentService.stateManager, diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index 138574d592c5bb..a602ad973811f0 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -23,7 +23,6 @@ import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { localize } from '../../../nls.js'; import { NativeEnvironmentService } from '../../environment/node/environmentService.js'; -import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { parseArgs, OPTIONS } from '../../environment/node/argv.js'; import { getLogLevel, ILogService } from '../../log/common/log.js'; import { LogService } from '../../log/common/logService.js'; @@ -31,57 +30,30 @@ import { LoggerService } from '../../log/node/loggerService.js'; import { OtlpEmitterLogger, OtlpLogEmitter } from '../common/otlp/otlpLogEmitter.js'; import product from '../../product/common/product.js'; import { IProductService } from '../../product/common/productService.js'; -import { InstantiationService } from '../../instantiation/common/instantiationService.js'; -import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; -import { registerAgentHostNetworkServices } from './agentHostBootstrap.js'; +import { createAgentHostServices, registerAgentHostProviderServices } from './agentHostBootstrap.js'; import { BANG_COMMAND_PREFIX } from './agentHostBangCommand.js'; import { CopilotAgent } from './copilot/copilotAgent.js'; -import { INetworkDiagnosticsService, NetworkDiagnosticsService } from './networkDiagnosticsService.js'; -import { IByokLmBridgeRegistry, NullByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; -import { IByokLmProxyService, NullByokLmProxyService } from './copilot/byokLmProxyService.js'; -import { IAgentHostWorktreeIsolation, WorktreeIsolation } from './shared/worktreeIsolation.js'; +import { NullByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; +import { NullByokLmProxyService } from './copilot/byokLmProxyService.js'; import { ClaudeAgent } from './claude/claudeAgent.js'; -import { ClaudeAgentSdkService, ClaudeSdkPackage, IClaudeAgentSdkService } from './claude/claudeAgentSdkService.js'; -import { ClaudeProxyService, IClaudeProxyService } from './claude/claudeProxyService.js'; +import { ClaudeSdkPackage } from './claude/claudeAgentSdkService.js'; import { CodexAgent, CodexSdkPackage } from './codex/codexAgent.js'; import { createCodexProviderConfiguration } from './codex/codexProviderConfiguration.js'; -import { CodexProxyService, ICodexProxyService } from './codex/codexProxyService.js'; -import { AgentSdkDownloader, IAgentSdkDownloader, type IAgentSdkDownloadProgress } from './agentSdkDownloader.js'; -import { IAgentHostOTelService } from '../common/otel/agentHostOTelService.js'; -import { AgentHostOTelService } from './otel/agentHostOTelService.js'; +import { type IAgentSdkDownloadProgress } from './agentSdkDownloader.js'; import { AgentHostCodexEnabledConfigKey, platformRootSchema } from '../common/agentHostSchema.js'; import { AgentModelRefreshScheduler, MODEL_REFRESH_INTERVAL_MS } from './agentModelRefreshScheduler.js'; -import { createAgentHostApplication } from './agentHostApplication.js'; import { AgentHostClaudeAgentEnabledEnvVar, AgentHostClaudeSdkRootEnvVar, AgentHostCodexAgentEnabledEnvVar, AgentHostCodexAgentSdkRootEnvVar, isAgentEnabled } from '../common/agentService.js'; import { WebSocketProtocolServer } from './webSocketTransport.js'; import { ProtocolServerHandler } from './protocolServerHandler.js'; import { AgentHostClientConnectionTelemetryTracker } from './agentHostClientConnectionTelemetry.js'; import { FileService } from '../../files/common/fileService.js'; -import { IFileService } from '../../files/common/files.js'; import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.js'; import { Schemas } from '../../../base/common/network.js'; -import { ISessionDataService } from '../common/sessionDataService.js'; -import { IDiffComputeService } from '../common/diffComputeService.js'; -import { IAgentEditAttributionService } from '../common/fileEditAttribution.js'; -import { NodeWorkerDiffComputeService } from './diffComputeService.js'; -import { AgentEditAttributionService } from './shared/agentEditAttributionService.js'; -import { IEditSurvivalReporterFactory, EditSurvivalReporterFactory } from './shared/editSurvivalReporter.js'; -import { EditArcReporterService, IEditArcReporterService } from './shared/editArcReporter.js'; import { SessionDataService } from './sessionDataService.js'; -import { IWindowsMxcTerminalSandboxRuntime, WindowsMxcTerminalSandboxRuntime } from '../../sandbox/common/terminalSandboxMxcRuntime.js'; -import { ISandboxHelperService } from '../../sandbox/common/sandboxHelperService.js'; -import { SandboxHelperService } from '../../sandbox/node/sandboxHelper.js'; import { AgentHostClientFileSystemProvider } from '../common/agentHostClientFileSystemProvider.js'; import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js'; import { resolveServerUrls } from './serverUrls.js'; -import { AgentPluginManager } from './agentPluginManager.js'; -import { IAgentPluginManager } from '../common/agentPluginManager.js'; import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js'; -import { AgentHostGitService } from './agentHostGitService.js'; -import { IAgentHostGitService } from '../common/agentHostGitService.js'; -import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from './agentHostFileMonitorService.js'; -import { createAgentHostTelemetryService } from './agentHostTelemetryService.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import ErrorTelemetry from '../../telemetry/node/errorTelemetry.js'; import { AgentHostLaunchKind } from '../common/agentHostTelemetry.js'; @@ -203,13 +175,9 @@ async function main(): Promise { if (options.quiet) { logService = disposables.add(new LogService(otlpLogger)); } else { - const services = new ServiceCollection(); - services.set(IProductService, productService); - services.set(INativeEnvironmentService, environmentService); loggerService = new LoggerService(getLogLevel(environmentService), environmentService.logsHome); const logger = loggerService.createLogger('agenthost-server', { name: localize('agentHostServer', "Agent Host Server") }); logService = disposables.add(new LogService(logger, [otlpLogger])); - services.set(ILogService, logService); log('Starting standalone agent host server'); } @@ -227,61 +195,29 @@ async function main(): Promise { const rootConfigResource = joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-config.json'); const storageResource = joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-storage.json'); - // Build the DI container early so the git service can be created via - // `createInstance` (it needs IFileService + INativeEnvironmentService). - // The git service is shared by AgentService (for diff computation + - // showBlob) and the production agent registration path. - const bootstrapServices = new ServiceCollection(); - bootstrapServices.set(IProductService, productService); - bootstrapServices.set(INativeEnvironmentService, environmentService); - bootstrapServices.set(ILogService, logService); - bootstrapServices.set(IFileService, fileService); - bootstrapServices.set(ISessionDataService, sessionDataService); - const networkServices = await registerAgentHostNetworkServices(bootstrapServices, fileService, environmentService, logService, disposables); - const proxyResolver = networkServices.proxyResolver; - const fetchFn = proxyResolver.fetch.bind(proxyResolver); - const telemetryService = await createAgentHostTelemetryService({ environmentService, productService, fileService, loggerService, logService, disposables, disableTelemetry: options.quiet, fetchFn, requestService: networkServices.requestService }); - errorTelemetry.value = new ErrorTelemetry(telemetryService); - bootstrapServices.set(ITelemetryService, telemetryService); - const bootstrapInstantiationService = new InstantiationService(bootstrapServices, /*strict*/ true); - const fileMonitorService = disposables.add(bootstrapInstantiationService.createInstance(AgentHostFileMonitorService)); - bootstrapServices.set(IAgentHostFileMonitorService, fileMonitorService); - bootstrapServices.set(IWindowsMxcTerminalSandboxRuntime, bootstrapInstantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); - bootstrapServices.set(ISandboxHelperService, new SandboxHelperService()); - const gitService = bootstrapInstantiationService.createInstance(AgentHostGitService); - bootstrapServices.set(IAgentHostGitService, gitService); - - const application = createAgentHostApplication(bootstrapInstantiationService, { - rootConfigResource, - providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], - hostLaunchKind: AgentHostLaunchKind.VSCodeCLI, - storageResource, + const hostServices = await createAgentHostServices({ + environmentService, + productService, + logService, + loggerService, + fileService, + sessionDataService, + disposables, + disableTelemetry: options.quiet, + agentServiceOptions: { + rootConfigResource, + providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], + hostLaunchKind: AgentHostLaunchKind.VSCodeCLI, + storageResource, + }, }); - const { agentService, applicationInstantiationService, applicationServices } = application; + const { agentService, instantiationService } = hostServices; disposables.add(agentService); - const networkDiagnosticsService = applicationInstantiationService.createInstance(NetworkDiagnosticsService); - applicationServices.set(INetworkDiagnosticsService, networkDiagnosticsService); - agentService.setNetworkDiagnosticsService(networkDiagnosticsService); + errorTelemetry.value = new ErrorTelemetry(hostServices.telemetryService); // Register agents let sdkDownloadProgress: Event | undefined; if (!options.quiet) { - // Production agents (require DI) - const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService); - applicationServices.set(IAgentPluginManager, pluginManager); - applicationServices.set(IDiffComputeService, disposables.add(new NodeWorkerDiffComputeService(logService))); - const editAttributionService = disposables.add(applicationInstantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); - applicationServices.set(IAgentEditAttributionService, editAttributionService); - agentService.setEditAttributionService(editAttributionService); - applicationServices.set(IEditSurvivalReporterFactory, applicationInstantiationService.createInstance(EditSurvivalReporterFactory)); - const editArcReporterService = disposables.add(applicationInstantiationService.createInstance(EditArcReporterService, undefined)); - applicationServices.set(IEditArcReporterService, editArcReporterService); - // Host-owned worktree isolation controller: a single instance drives folder - // / worktree isolation for every agent, so providers stay unaware of it. It - // owns its branch-name generator, created from ICopilotApiService. - const worktreeIsolation = disposables.add(applicationInstantiationService.createInstance(WorktreeIsolation, undefined)); - applicationServices.set(IAgentHostWorktreeIsolation, worktreeIsolation); - agentService.setWorktreeIsolation(worktreeIsolation); // CLI flags become env vars BEFORE the downloader is constructed so // `isAvailable()` and `loadSdkRoot()` see them as dev overrides. if (options.claudeSdkRoot) { @@ -290,24 +226,18 @@ async function main(): Promise { if (options.codexSdkRoot) { process.env[AgentHostCodexAgentSdkRootEnvVar] = options.codexSdkRoot; } - // Register the agent SDK downloader BEFORE any service that injects it. - const agentSdkDownloader = disposables.add(applicationInstantiationService.createInstance(AgentSdkDownloader)); - applicationServices.set(IAgentSdkDownloader, agentSdkDownloader); - sdkDownloadProgress = agentSdkDownloader.onDidDownloadProgress; - const claudeProxyService = disposables.add(applicationInstantiationService.createInstance(ClaudeProxyService)); - applicationServices.set(IClaudeProxyService, claudeProxyService); - const claudeAgentSdkService = applicationInstantiationService.createInstance(ClaudeAgentSdkService); - applicationServices.set(IClaudeAgentSdkService, claudeAgentSdkService); - const codexProxyService = disposables.add(applicationInstantiationService.createInstance(CodexProxyService)); - applicationServices.set(ICodexProxyService, codexProxyService); - const agentHostOTelService = disposables.add(applicationInstantiationService.createInstance(AgentHostOTelService, fetchFn)); - applicationServices.set(IAgentHostOTelService, agentHostOTelService); - // BYOK is unsupported in the remote agent host (no extension host runs - // next to it to serve the renderer LM API). Inject null implementations - // to satisfy CopilotAgent / CopilotSessionLauncher DI. - applicationServices.set(IByokLmBridgeRegistry, new NullByokLmBridgeRegistry()); - applicationServices.set(IByokLmProxyService, new NullByokLmProxyService()); - const copilotAgent = disposables.add(applicationInstantiationService.createInstance(CopilotAgent)); + const providerServices = registerAgentHostProviderServices({ + ...hostServices, + environmentService, + fileService, + logService, + disposables, + byokBridgeRegistry: new NullByokLmBridgeRegistry(), + byokLmProxyService: new NullByokLmProxyService(), + }); + const agentSdkDownloader = providerServices.agentSdkDownloader; + sdkDownloadProgress = providerServices.sdkDownloadProgress; + const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent)); agentService.registerProvider(copilotAgent); log('CopilotAgent registered'); // Claude and Codex providers are gated on two things: @@ -324,7 +254,7 @@ async function main(): Promise { // `node_modules` in dev; built/shipped installs use the env-var // override or `product.agentSdks.codex`. if (isAgentEnabled(process.env[AgentHostClaudeAgentEnabledEnvVar], true) && (!environmentService.isBuilt || agentSdkDownloader.isAvailable(ClaudeSdkPackage))) { - const claudeAgent = disposables.add(applicationInstantiationService.createInstance(ClaudeAgent)); + const claudeAgent = disposables.add(instantiationService.createInstance(ClaudeAgent)); agentService.registerProvider(claudeAgent); log('ClaudeAgent registered'); } @@ -339,7 +269,7 @@ async function main(): Promise { const enabledByRootConfig = agentConfigurationService.getRootValue(platformRootSchema, AgentHostCodexEnabledConfigKey) === true; if (enabledByEnv || enabledByRootConfig) { codexRegistered = true; - const codexAgent = disposables.add(applicationInstantiationService.createInstance(CodexAgent)); + const codexAgent = disposables.add(instantiationService.createInstance(CodexAgent)); agentService.registerProvider(codexAgent); log('CodexAgent registered'); } @@ -383,7 +313,7 @@ async function main(): Promise { // lifetime, rather than inside `AgentHostService`: a service that arms a // recurring timer in its constructor is one that no faked-timer unit test // can ever drain. - disposables.add(applicationInstantiationService.createInstance(AgentModelRefreshScheduler, agentService.agents, agentService.onDidStartTurn, MODEL_REFRESH_INTERVAL_MS)); + disposables.add(instantiationService.createInstance(AgentModelRefreshScheduler, agentService.agents, agentService.onDidStartTurn, MODEL_REFRESH_INTERVAL_MS)); // WebSocket server const wsServer = disposables.add(await WebSocketProtocolServer.create({ @@ -392,7 +322,7 @@ async function main(): Promise { connectionTokenValidate: options.connectionToken ? token => token === options.connectionToken : undefined, - }, logService, { instantiationService: applicationInstantiationService, logsHome: environmentService.logsHome })); + }, logService, { instantiationService, logsHome: environmentService.logsHome })); const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider()); @@ -400,7 +330,7 @@ async function main(): Promise { const connectionTelemetryTracker = disposables.add(new AgentHostClientConnectionTelemetryTracker()); // Wire up protocol handler - disposables.add(applicationInstantiationService.createInstance( + disposables.add(instantiationService.createInstance( ProtocolServerHandler, agentService, agentService.stateManager, @@ -475,7 +405,7 @@ async function main(): Promise { logService.warn('[AgentHostServer] Timed out waiting for persistence writes to flush; exiting anyway.'); }); disposables.dispose(); - bootstrapInstantiationService.dispose(); + instantiationService.dispose(); loggerService?.dispose(); process.exit(0); } diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 46585d48a1466f..b25332ac800f1c 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -559,8 +559,8 @@ export class AgentService extends Disposable implements IAgentService { constructor( options: IAgentServiceOptions, - applicationServices: ServiceCollection, - @IInstantiationService applicationInstantiationService: IInstantiationService, + services: ServiceCollection, + @IInstantiationService instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @IFileService private readonly _fileService: IFileService, @ISessionDataService private readonly _sessionDataService: ISessionDataService, @@ -632,13 +632,13 @@ export class AgentService extends Disposable implements IAgentService { })); this._storageService = this._register(new AgentHostStorageService(options.storageResource, this._logService)); updateAgentHostTelemetryLevelFromConfig(this._telemetryService, this._stateManager.rootState.config?.values); - applicationServices.set(IAgentService, this); - applicationServices.set(IAgentConfigurationService, configurationService); - applicationServices.set(IAgentHostStateManager, this._stateManager); - applicationServices.set(IAgentHostStorageService, this._storageService); - applicationServices.set(IAgentHostManagedSettingsService, this._managedSettingsService); - this._gitHubEndpointService = this._register(applicationInstantiationService.createInstance(AgentHostGitHubEndpointService)); - applicationServices.set(IAgentHostGitHubEndpointService, this._gitHubEndpointService); + services.set(IAgentService, this); + services.set(IAgentConfigurationService, configurationService); + services.set(IAgentHostStateManager, this._stateManager); + services.set(IAgentHostStorageService, this._storageService); + services.set(IAgentHostManagedSettingsService, this._managedSettingsService); + this._gitHubEndpointService = this._register(instantiationService.createInstance(AgentHostGitHubEndpointService)); + services.set(IAgentHostGitHubEndpointService, this._gitHubEndpointService); // A GitHub Enterprise URI change repoints every agent's GitHub resource // identity to a different authorization server, so the client must obtain a // token for the new resource. One root-channel `auth/required` covers all @@ -649,9 +649,9 @@ export class AgentService extends Disposable implements IAgentService { reason: AuthRequiredReason.Required, }); })); - const agentHostOctoKitService = applicationInstantiationService.createInstance(AgentHostOctoKitService, fetchFn); - applicationServices.set(IAgentHostOctoKitService, agentHostOctoKitService); - const gitHubService = this._register(applicationInstantiationService.createInstance(GitHubService, { + const agentHostOctoKitService = instantiationService.createInstance(AgentHostOctoKitService, fetchFn); + services.set(IAgentHostOctoKitService, agentHostOctoKitService); + const gitHubService = this._register(instantiationService.createInstance(GitHubService, { endpoint: this._gitHubEndpointService, tokenProvider: { getToken: () => { @@ -664,61 +664,61 @@ export class AgentService extends Disposable implements IAgentService { }, fetch: fetchFn, })); - applicationServices.set(IGitHubService, gitHubService); - this._copilotApiService = options.copilotApiService ?? applicationInstantiationService.createInstance(CopilotApiService, fetchFn); - applicationServices.set(ICopilotApiService, this._copilotApiService); - this._customizationEnablementService = this._register(applicationInstantiationService.createInstance(AgentHostCustomizationEnablementService)); - applicationServices.set(IAgentHostCustomizationEnablementService, this._customizationEnablementService); - - this._gitStateService = this._register(applicationInstantiationService.createInstance(AgentHostGitStateService)); - applicationServices.set(IAgentHostGitStateService, this._gitStateService); - this._agentMergeController = this._register(applicationInstantiationService.createInstance(AgentMergeController, { + services.set(IGitHubService, gitHubService); + this._copilotApiService = options.copilotApiService ?? instantiationService.createInstance(CopilotApiService, fetchFn); + services.set(ICopilotApiService, this._copilotApiService); + this._customizationEnablementService = this._register(instantiationService.createInstance(AgentHostCustomizationEnablementService)); + services.set(IAgentHostCustomizationEnablementService, this._customizationEnablementService); + + this._gitStateService = this._register(instantiationService.createInstance(AgentHostGitStateService)); + services.set(IAgentHostGitStateService, this._gitStateService); + this._agentMergeController = this._register(instantiationService.createInstance(AgentMergeController, { startTurn: (session, turnId, prompt) => this._startAgentMergePrompt(session, turnId, prompt), cancelTurn: (session, turnId) => this._cancelAgentMergePrompt(session, turnId), getAutonomousSessionConfig: (session, config) => this._findProviderForSession(session)?.getAutonomousSessionConfig?.(config), })); - this._checkpointService = this._register(applicationInstantiationService.createInstance(AgentHostCheckpointService)); - applicationServices.set(IAgentHostCheckpointService, this._checkpointService); + this._checkpointService = this._register(instantiationService.createInstance(AgentHostCheckpointService)); + services.set(IAgentHostCheckpointService, this._checkpointService); - this._promptCache = applicationInstantiationService.createInstance(AgentHostPromptCache); - applicationServices.set(IAgentHostPromptCache, this._promptCache); - this._sessionTitleSignal = this._register(applicationInstantiationService.createInstance(AgentHostSessionTitleSignal)); - applicationServices.set(IAgentHostSessionTitleSignal, this._sessionTitleSignal); + this._promptCache = instantiationService.createInstance(AgentHostPromptCache); + services.set(IAgentHostPromptCache, this._promptCache); + this._sessionTitleSignal = this._register(instantiationService.createInstance(AgentHostSessionTitleSignal)); + services.set(IAgentHostSessionTitleSignal, this._sessionTitleSignal); // The subscription service manages the lifecycle of changeset subscriptions. The service // is also consulted by other services when refreshing changesets and changeset operations. - this._changesetSubscriptions = applicationInstantiationService.createInstance(AgentHostChangesetSubscriptionService); - applicationServices.set(IAgentHostChangesetSubscriptionService, this._changesetSubscriptions); + this._changesetSubscriptions = instantiationService.createInstance(AgentHostChangesetSubscriptionService); + services.set(IAgentHostChangesetSubscriptionService, this._changesetSubscriptions); // The operation contribution service manages the lifecycle of changeset operations. - this._changesetOperationService = this._register(applicationInstantiationService.createInstance(AgentHostChangesetOperationService)); - applicationServices.set(IAgentHostChangesetOperationService, this._changesetOperationService); + this._changesetOperationService = this._register(instantiationService.createInstance(AgentHostChangesetOperationService)); + services.set(IAgentHostChangesetOperationService, this._changesetOperationService); // The changes review service is responsible for managing review/unreview state for changeset changes. - this._reviewService = this._register(applicationInstantiationService.createInstance(AgentHostReviewService)); - applicationServices.set(IAgentHostReviewService, this._reviewService); + this._reviewService = this._register(instantiationService.createInstance(AgentHostReviewService)); + services.set(IAgentHostReviewService, this._reviewService); // The changeset service is responsible for computing, publishing, and persisting changesets. - this._changesets = this._register(applicationInstantiationService.createInstance(AgentHostChangesetService)); - applicationServices.set(IAgentHostChangesetService, this._changesets); + this._changesets = this._register(instantiationService.createInstance(AgentHostChangesetService)); + services.set(IAgentHostChangesetService, this._changesets); // The coordinator owns all AgentService-side orchestration of the changeset feature: lifecycle // hooks, listSessions overlay, subscription URI routing, and the deferred-refresh state machine. - this._changesetCoordinator = this._register(applicationInstantiationService.createInstance(AgentHostChangesetCoordinator)); + this._changesetCoordinator = this._register(instantiationService.createInstance(AgentHostChangesetCoordinator)); this._register(this._stateManager.onDidChangeSessionActiveTurn(e => this._changesetCoordinator.onSessionTurnActiveChanged(e.session, e.active))); // Register the changeset operation contributions. - this._register(this._changesetOperationService.registerContribution(applicationInstantiationService.createInstance(AgentHostCommitOperationContribution))); - this._register(this._changesetOperationService.registerContribution(applicationInstantiationService.createInstance(AgentHostPullRequestOperationContribution))); - this._register(this._changesetOperationService.registerContribution(applicationInstantiationService.createInstance(AgentHostMergeOperationContribution))); - this._register(this._changesetOperationService.registerContribution(applicationInstantiationService.createInstance(AgentHostSyncOperationContribution))); - this._register(this._changesetOperationService.registerContribution(applicationInstantiationService.createInstance(AgentHostDiscardChangesOperationContribution))); - - this._completions = this._register(applicationInstantiationService.createInstance(AgentHostCompletions)); - applicationServices.set(IAgentHostCompletions, this._completions); + this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostCommitOperationContribution))); + this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostPullRequestOperationContribution))); + this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostMergeOperationContribution))); + this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostSyncOperationContribution))); + this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostDiscardChangesOperationContribution))); + + this._completions = this._register(instantiationService.createInstance(AgentHostCompletions)); + services.set(IAgentHostCompletions, this._completions); // Built-in generic provider: completes files in the session's workspace folder. - const workspaceFiles = this._register(applicationInstantiationService.createInstance(AgentHostWorkspaceFiles)); + const workspaceFiles = this._register(instantiationService.createInstance(AgentHostWorkspaceFiles)); this._register(this._completions.registerProvider( new AgentHostFileCompletionProvider(this._stateManager, workspaceFiles, this._logService), )); @@ -746,12 +746,12 @@ export class AgentService extends Disposable implements IAgentService { // Created before AgentSideEffects and registered in the local scope so // AgentSideEffects can consume it via DI (for inline `!command` // execution). - this._terminalManager = this._register(applicationInstantiationService.createInstance(AgentHostTerminalManager)); - applicationServices.set(IAgentHostTerminalManager, this._terminalManager); + this._terminalManager = this._register(instantiationService.createInstance(AgentHostTerminalManager)); + services.set(IAgentHostTerminalManager, this._terminalManager); this._localTurns = new AgentHostLocalTurns(this._sessionDataService, this._logService); - this._sideEffects = this._register(applicationInstantiationService.createInstance(AgentSideEffects, this._stateManager, this._customizationEnablementService, { + this._sideEffects = this._register(instantiationService.createInstance(AgentSideEffects, this._stateManager, this._customizationEnablementService, { getAgent: session => this._findProviderForSession(session), sessionDataService: this._sessionDataService, localTurns: this._localTurns, @@ -797,7 +797,7 @@ export class AgentService extends Disposable implements IAgentService { // state. The set of groups (and their display) is the single source of // truth in `serverToolGroups.ts`; the session-management group's runtime // dependency (this service) is injected via the accessor. - const agentMergeTools = applicationInstantiationService.createInstance( + const agentMergeTools = instantiationService.createInstance( AgentMergeTools, () => this._agentMergeController.isEnabled(), session => this._agentMergeController.getTurnContext(session), diff --git a/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts b/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts index daf073e5dd6275..cd402c51bf625a 100644 --- a/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts +++ b/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts @@ -18,7 +18,6 @@ import { IAgentHostGitService } from '../../common/agentHostGitService.js'; import { AgentHostLaunchKind } from '../../common/agentHostTelemetry.js'; import { ISessionDataService } from '../../common/sessionDataService.js'; import { IAgentHostDatabase } from '../../node/agentHostDatabase.js'; -import { createAgentHostApplication } from '../../node/agentHostApplication.js'; import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from '../../node/agentHostFileMonitorService.js'; import { IAgentHostProxyResolver } from '../../node/agentHostProxyResolver.js'; import { AgentService } from '../../node/agentService.js'; @@ -55,7 +54,7 @@ export function createTestAgentService( resolveProxy: async () => undefined, fetch: fetchFn, }; - const bootstrapInstantiationService = new InstantiationService(new ServiceCollection( + const services = new ServiceCollection( [ILogService, logService], [IFileService, fileService], [ISessionDataService, sessionDataService], @@ -64,7 +63,8 @@ export function createTestAgentService( [ITelemetryService, telemetryService], [IAgentHostFileMonitorService, effectiveFileMonitorService], [IAgentHostProxyResolver, proxyResolver], - ), /*strict*/ true); + ); + const instantiationService = new InstantiationService(services, /*strict*/ true); const options = { rootConfigResource, copilotApiService, @@ -74,13 +74,10 @@ export function createTestAgentService( orchestratorDatabase, now, }; - const application = createAgentHostApplication(bootstrapInstantiationService, options, (applicationInstantiationService, applicationServices) => { - return applicationInstantiationService.createInstance(TestAgentService, options, applicationServices); - }); - const service = application.agentService; + const service = instantiationService.createInstance(TestAgentService, options, services); if (!fileMonitorService) { service.registerTestDependency(effectiveFileMonitorService); } - service.registerTestDependency(bootstrapInstantiationService); + service.registerTestDependency(instantiationService); return service; } From f499f83c43035ffbdd5bbd4925e7f7787aad6aa4 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 19 Aug 2026 09:50:02 -0700 Subject: [PATCH 4/6] Streamline Agent Host runtime creation Expose one runtime factory that owns common file, session, DI, AgentService, diagnostics, and optional provider infrastructure initialization. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/node/agentHostBootstrap.ts | 108 +++++++++++++----- .../platform/agentHost/node/agentHostMain.ts | 63 +++------- .../agentHost/node/agentHostServerMain.ts | 72 ++++-------- 3 files changed, 114 insertions(+), 129 deletions(-) diff --git a/src/vs/platform/agentHost/node/agentHostBootstrap.ts b/src/vs/platform/agentHost/node/agentHostBootstrap.ts index b01bf155243441..8a3548b9ad026b 100644 --- a/src/vs/platform/agentHost/node/agentHostBootstrap.ts +++ b/src/vs/platform/agentHost/node/agentHostBootstrap.ts @@ -7,10 +7,13 @@ import { DisposableStore } from '../../../base/common/lifecycle.js'; import { Event } from '../../../base/common/event.js'; import { joinPath } from '../../../base/common/resources.js'; import { URI } from '../../../base/common/uri.js'; +import { Schemas } from '../../../base/common/network.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { ConfigurationService } from '../../configuration/common/configurationService.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { IFileService } from '../../files/common/files.js'; +import { FileService } from '../../files/common/fileService.js'; +import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { InstantiationService } from '../../instantiation/common/instantiationService.js'; import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; @@ -48,49 +51,42 @@ import { ClaudeProxyService, IClaudeProxyService } from './claude/claudeProxySer import { CodexProxyService, ICodexProxyService } from './codex/codexProxyService.js'; import { IByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; import { ByokLmProxyService, IByokLmProxyService } from './copilot/byokLmProxyService.js'; +import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js'; +import { SessionDataService } from './sessionDataService.js'; +import { IAgentCustomizationSettingsRegistration } from '../common/agentCustomizationSettings.js'; +import { AgentHostLaunchKind } from '../common/agentHostTelemetry.js'; export interface IAgentHostNetworkServices { readonly proxyResolver: IAgentHostProxyResolver; readonly requestService: IRequestService; } -export interface ICreateAgentHostServicesOptions { +export interface IAgentHostProviderInfrastructureOptions { + readonly byokBridgeRegistry: IByokLmBridgeRegistry; + readonly byokLmProxyService?: IByokLmProxyService; +} + +export interface ICreateAgentHostRuntimeOptions { readonly environmentService: INativeEnvironmentService; readonly productService: IProductService; readonly logService: ILogService; readonly loggerService: ILoggerService | undefined; - readonly fileService: IFileService; - readonly sessionDataService: ISessionDataService; readonly disposables: DisposableStore; readonly disableTelemetry?: boolean; - readonly agentServiceOptions: IAgentServiceOptions; + readonly hostLaunchKind: AgentHostLaunchKind; + readonly providerConfigurations: readonly IAgentCustomizationSettingsRegistration[]; + readonly providerInfrastructure?: IAgentHostProviderInfrastructureOptions; } -export interface IAgentHostServices { - readonly services: ServiceCollection; +export interface IAgentHostRuntime { readonly instantiationService: IInstantiationService; readonly agentService: AgentService; + readonly fileService: IFileService; + readonly sessionDataService: ISessionDataService; readonly proxyResolver: IAgentHostProxyResolver; readonly telemetryService: IAgentHostTelemetryService; - readonly fetchFn: typeof globalThis.fetch; -} - -export interface IRegisterAgentHostProviderServicesOptions { - readonly services: ServiceCollection; - readonly instantiationService: IInstantiationService; - readonly agentService: AgentService; - readonly environmentService: INativeEnvironmentService; - readonly fileService: IFileService; - readonly logService: ILogService; - readonly disposables: DisposableStore; - readonly fetchFn: typeof globalThis.fetch; - readonly byokBridgeRegistry: IByokLmBridgeRegistry; - readonly byokLmProxyService?: IByokLmProxyService; -} - -export interface IAgentHostProviderServices { - readonly agentSdkDownloader: AgentSdkDownloader; - readonly sdkDownloadProgress: Event; + readonly agentSdkDownloader: AgentSdkDownloader | undefined; + readonly sdkDownloadProgress: Event | undefined; } /** @@ -133,8 +129,12 @@ export async function registerAgentHostNetworkServices( return { proxyResolver, requestService }; } -export async function createAgentHostServices(options: ICreateAgentHostServicesOptions): Promise { - const { environmentService, productService, logService, loggerService, fileService, sessionDataService, disposables } = options; +export async function createAgentHostRuntime(options: ICreateAgentHostRuntimeOptions): Promise { + const { environmentService, productService, logService, loggerService, disposables } = options; + const fileService = disposables.add(new FileService(logService)); + disposables.add(fileService.registerProvider(Schemas.file, disposables.add(new DiskFileSystemProvider(logService)))); + disposables.add(registerPendingEditContentProvider(fileService)); + const sessionDataService = new SessionDataService(URI.file(environmentService.userDataPath), fileService, logService); const services = new ServiceCollection( [INativeEnvironmentService, environmentService], [ILogService, logService], @@ -164,18 +164,66 @@ export async function createAgentHostServices(options: ICreateAgentHostServicesO services.set(IWindowsMxcTerminalSandboxRuntime, instantiationService.createInstance(WindowsMxcTerminalSandboxRuntime)); services.set(ISandboxHelperService, new SandboxHelperService()); services.set(IAgentHostGitService, instantiationService.createInstance(AgentHostGitService)); - const agentService = instantiationService.createInstance(AgentService, options.agentServiceOptions, services); + const agentServiceOptions: IAgentServiceOptions = { + rootConfigResource: joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-config.json'), + providerConfigurations: options.providerConfigurations, + hostLaunchKind: options.hostLaunchKind, + storageResource: joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-storage.json'), + debugLogsEnvironment: { + logsHome: environmentService.logsHome, + tmpDir: environmentService.tmpDir, + }, + }; + const agentService = instantiationService.createInstance(AgentService, agentServiceOptions, services); const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); services.set(INetworkDiagnosticsService, networkDiagnosticsService); agentService.setNetworkDiagnosticsService(networkDiagnosticsService); - return { services, instantiationService, agentService, proxyResolver, telemetryService, fetchFn }; + const providerInfrastructure = options.providerInfrastructure + ? registerProviderInfrastructure({ + services, + instantiationService, + agentService, + environmentService, + fileService, + logService, + disposables, + fetchFn, + ...options.providerInfrastructure, + }) + : undefined; + return { + instantiationService, + agentService, + fileService, + sessionDataService, + proxyResolver, + telemetryService, + agentSdkDownloader: providerInfrastructure?.agentSdkDownloader, + sdkDownloadProgress: providerInfrastructure?.sdkDownloadProgress, + }; } catch (error) { instantiationService.dispose(); throw error; } } -export function registerAgentHostProviderServices(options: IRegisterAgentHostProviderServicesOptions): IAgentHostProviderServices { +interface IRegisterProviderInfrastructureOptions extends IAgentHostProviderInfrastructureOptions { + readonly services: ServiceCollection; + readonly instantiationService: IInstantiationService; + readonly agentService: AgentService; + readonly environmentService: INativeEnvironmentService; + readonly fileService: IFileService; + readonly logService: ILogService; + readonly disposables: DisposableStore; + readonly fetchFn: typeof globalThis.fetch; +} + +interface IProviderInfrastructure { + readonly agentSdkDownloader: AgentSdkDownloader; + readonly sdkDownloadProgress: Event; +} + +function registerProviderInfrastructure(options: IRegisterProviderInfrastructureOptions): IProviderInfrastructure { const { services, instantiationService, agentService, environmentService, fileService, logService, disposables, fetchFn } = options; services.set(IAgentPluginManager, new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService)); services.set(IDiffComputeService, disposables.add(instantiationService.createInstance(NodeWorkerDiffComputeService))); diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index cddd4b5c0a0e3f..5e9cfae9d19721 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -10,7 +10,6 @@ import { Server as UtilityProcessServer } from '../../../base/parts/ipc/node/ipc import { isUtilityProcess } from '../../../base/parts/sandbox/node/electronTypes.js'; import { Emitter, type Event } from '../../../base/common/event.js'; import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js'; -import { joinPath } from '../../../base/common/resources.js'; import { isWindows } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; @@ -45,18 +44,14 @@ import { DefaultURITransformer } from '../../../base/common/uriIpc.js'; import product from '../../product/common/product.js'; import { IProductService } from '../../product/common/productService.js'; import { localize } from '../../../nls.js'; -import { FileService } from '../../files/common/fileService.js'; -import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.js'; -import { Schemas } from '../../../base/common/network.js'; +import { IFileService } from '../../files/common/files.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; -import { createAgentHostServices, registerAgentHostProviderServices } from './agentHostBootstrap.js'; +import { createAgentHostRuntime } from './agentHostBootstrap.js'; import { BANG_COMMAND_PREFIX } from './agentHostBangCommand.js'; -import { SessionDataService } from './sessionDataService.js'; import { AgentHostClientFileSystemProvider } from '../common/agentHostClientFileSystemProvider.js'; import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js'; import { AGENT_HOST_CLIENT_BYOK_LM_CHANNEL, createAgentHostClientByokLmConnection } from '../common/agentHostClientByokLmChannel.js'; import { AGENT_HOST_CLIENT_PROXY_CHANNEL, createAgentHostClientProxyConnection } from '../common/agentHostClientProxyChannel.js'; -import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js'; import { join } from '../../../base/common/path.js'; import ErrorTelemetry from '../../telemetry/node/errorTelemetry.js'; import { AgentHostLaunchKindEnvVar, readAgentHostLaunchKind, type AgentHostLaunchKind } from '../common/agentHostTelemetry.js'; @@ -105,21 +100,10 @@ async function startAgentHost(): Promise { } logService.info('Agent Host process started successfully'); - // File service - const fileService = disposables.add(new FileService(logService)); - disposables.add(fileService.registerProvider(Schemas.file, disposables.add(new DiskFileSystemProvider(logService)))); - // In-memory filesystem backing transient file-edit previews shown during - // tool-call confirmations. - disposables.add(registerPendingEditContentProvider(fileService)); - - // Session data service - const sessionDataService = new SessionDataService(URI.file(environmentService.userDataPath), fileService, logService); - const rootConfigResource = joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-config.json'); - const storageResource = joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-storage.json'); - // Create the real service implementation that lives in this process let agentService: AgentService; let instantiationService!: IInstantiationService; + let fileService!: IFileService; // Hoisted out of the `try` below so the protocol handlers (constructed // after the block) can forward agent-SDK download progress to clients. let sdkDownloadProgress: Event | undefined; @@ -128,41 +112,26 @@ async function startAgentHost(): Promise { const hostLaunchKind = readAgentHostLaunchKind(process.env[AgentHostLaunchKindEnvVar]); const connectionTelemetryTracker = disposables.add(new AgentHostClientConnectionTelemetryTracker()); try { - const hostServices = await createAgentHostServices({ + byokLmBridgeRegistry = new ByokLmBridgeRegistry(); + const runtime = await createAgentHostRuntime({ environmentService, productService, logService, loggerService, - fileService, - sessionDataService, disposables, - agentServiceOptions: { - rootConfigResource, - providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], - hostLaunchKind, - storageResource, - debugLogsEnvironment: { - logsHome: environmentService.logsHome, - tmpDir: environmentService.tmpDir, - }, + hostLaunchKind, + providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], + providerInfrastructure: { + byokBridgeRegistry: byokLmBridgeRegistry, }, }); - agentService = hostServices.agentService; - instantiationService = hostServices.instantiationService; - proxyResolver = hostServices.proxyResolver; - errorTelemetry.value = new ErrorTelemetry(hostServices.telemetryService); - - byokLmBridgeRegistry = new ByokLmBridgeRegistry(); - const providerServices = registerAgentHostProviderServices({ - ...hostServices, - environmentService, - fileService, - logService, - disposables, - byokBridgeRegistry: byokLmBridgeRegistry, - }); - const agentSdkDownloader = providerServices.agentSdkDownloader; - sdkDownloadProgress = providerServices.sdkDownloadProgress; + agentService = runtime.agentService; + instantiationService = runtime.instantiationService; + fileService = runtime.fileService; + proxyResolver = runtime.proxyResolver; + errorTelemetry.value = new ErrorTelemetry(runtime.telemetryService); + const agentSdkDownloader = runtime.agentSdkDownloader!; + sdkDownloadProgress = runtime.sdkDownloadProgress; agentService.registerProvider(instantiationService.createInstance(CopilotAgent)); // Claude and Codex providers are gated on two things: // 1. The user-facing enable toggle (`chat.agentHost.Agent.enabled`, diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index acd8db7c386462..78260a24aed9f3 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -18,7 +18,6 @@ import * as os from 'os'; import type { Event } from '../../../base/common/event.js'; import { DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js'; import { raceTimeout } from '../../../base/common/async.js'; -import { joinPath } from '../../../base/common/resources.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { localize } from '../../../nls.js'; @@ -30,7 +29,7 @@ import { LoggerService } from '../../log/node/loggerService.js'; import { OtlpEmitterLogger, OtlpLogEmitter } from '../common/otlp/otlpLogEmitter.js'; import product from '../../product/common/product.js'; import { IProductService } from '../../product/common/productService.js'; -import { createAgentHostServices, registerAgentHostProviderServices } from './agentHostBootstrap.js'; +import { createAgentHostRuntime } from './agentHostBootstrap.js'; import { BANG_COMMAND_PREFIX } from './agentHostBangCommand.js'; import { CopilotAgent } from './copilot/copilotAgent.js'; import { NullByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; @@ -46,14 +45,9 @@ import { AgentHostClaudeAgentEnabledEnvVar, AgentHostClaudeSdkRootEnvVar, AgentH import { WebSocketProtocolServer } from './webSocketTransport.js'; import { ProtocolServerHandler } from './protocolServerHandler.js'; import { AgentHostClientConnectionTelemetryTracker } from './agentHostClientConnectionTelemetry.js'; -import { FileService } from '../../files/common/fileService.js'; -import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.js'; -import { Schemas } from '../../../base/common/network.js'; -import { SessionDataService } from './sessionDataService.js'; import { AgentHostClientFileSystemProvider } from '../common/agentHostClientFileSystemProvider.js'; import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js'; import { resolveServerUrls } from './serverUrls.js'; -import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js'; import ErrorTelemetry from '../../telemetry/node/errorTelemetry.js'; import { AgentHostLaunchKind } from '../common/agentHostTelemetry.js'; @@ -183,64 +177,38 @@ async function main(): Promise { logService.info('[AgentHostServer] Starting standalone agent host server'); - // File service - const fileService = disposables.add(new FileService(logService)); - disposables.add(fileService.registerProvider(Schemas.file, disposables.add(new DiskFileSystemProvider(logService)))); - // In-memory filesystem backing transient file-edit previews shown during - // tool-call confirmations. - disposables.add(registerPendingEditContentProvider(fileService)); - - // Session data service - const sessionDataService = new SessionDataService(URI.file(environmentService.userDataPath), fileService, logService); - const rootConfigResource = joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-config.json'); - const storageResource = joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-storage.json'); + if (!options.quiet) { + if (options.claudeSdkRoot) { + process.env[AgentHostClaudeSdkRootEnvVar] = options.claudeSdkRoot; + } + if (options.codexSdkRoot) { + process.env[AgentHostCodexAgentSdkRootEnvVar] = options.codexSdkRoot; + } + } - const hostServices = await createAgentHostServices({ + const runtime = await createAgentHostRuntime({ environmentService, productService, logService, loggerService, - fileService, - sessionDataService, disposables, disableTelemetry: options.quiet, - agentServiceOptions: { - rootConfigResource, - providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], - hostLaunchKind: AgentHostLaunchKind.VSCodeCLI, - storageResource, - debugLogsEnvironment: { - logsHome: environmentService.logsHome, - tmpDir: environmentService.tmpDir, - }, - }, + hostLaunchKind: AgentHostLaunchKind.VSCodeCLI, + providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], + providerInfrastructure: options.quiet ? undefined : { + byokBridgeRegistry: new NullByokLmBridgeRegistry(), + byokLmProxyService: new NullByokLmProxyService(), + } }); - const { agentService, instantiationService } = hostServices; + const { agentService, instantiationService, fileService, sessionDataService } = runtime; disposables.add(agentService); - errorTelemetry.value = new ErrorTelemetry(hostServices.telemetryService); + errorTelemetry.value = new ErrorTelemetry(runtime.telemetryService); // Register agents let sdkDownloadProgress: Event | undefined; if (!options.quiet) { - // CLI flags become env vars BEFORE the downloader is constructed so - // `isAvailable()` and `loadSdkRoot()` see them as dev overrides. - if (options.claudeSdkRoot) { - process.env[AgentHostClaudeSdkRootEnvVar] = options.claudeSdkRoot; - } - if (options.codexSdkRoot) { - process.env[AgentHostCodexAgentSdkRootEnvVar] = options.codexSdkRoot; - } - const providerServices = registerAgentHostProviderServices({ - ...hostServices, - environmentService, - fileService, - logService, - disposables, - byokBridgeRegistry: new NullByokLmBridgeRegistry(), - byokLmProxyService: new NullByokLmProxyService(), - }); - const agentSdkDownloader = providerServices.agentSdkDownloader; - sdkDownloadProgress = providerServices.sdkDownloadProgress; + const agentSdkDownloader = runtime.agentSdkDownloader!; + sdkDownloadProgress = runtime.sdkDownloadProgress; const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent)); agentService.registerProvider(copilotAgent); log('CopilotAgent registered'); From d88e40a3fa5e8efcc7c9b6b07cddb07aed0d46ac Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 19 Aug 2026 16:36:50 -0700 Subject: [PATCH 5/6] Move AgentService composition to runtime Construct and register the AgentService core and collaborator graph outside AgentService, use one guarded initialization step for genuine back-references, and replace the test-only clock injection with virtual timers. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/node/agentHostBootstrap.ts | 5 +- .../agentHost/node/agentMergeController.ts | 2 +- .../platform/agentHost/node/agentService.ts | 448 ++++++++---------- .../agentHost/node/agentServiceComposition.ts | 236 +++++++++ .../agentHost/test/node/agentService.test.ts | 52 +- .../test/node/agentServiceTestUtils.ts | 25 +- 6 files changed, 462 insertions(+), 306 deletions(-) create mode 100644 src/vs/platform/agentHost/node/agentServiceComposition.ts diff --git a/src/vs/platform/agentHost/node/agentHostBootstrap.ts b/src/vs/platform/agentHost/node/agentHostBootstrap.ts index e58b1be287391c..7020896f7c4c4d 100644 --- a/src/vs/platform/agentHost/node/agentHostBootstrap.ts +++ b/src/vs/platform/agentHost/node/agentHostBootstrap.ts @@ -35,6 +35,7 @@ import { AgentHostProxyResolver, IAgentHostProxyResolver } from './agentHostProx import { AgentHostRequestService } from './agentHostRequestService.js'; import { createAgentHostTelemetryService, IAgentHostTelemetryService } from './agentHostTelemetryService.js'; import { AgentService, IAgentServiceOptions } from './agentService.js'; +import { createAgentService } from './agentServiceComposition.js'; import { INetworkDiagnosticsService, NetworkDiagnosticsService } from './networkDiagnosticsService.js'; import { AgentPluginManager } from './agentPluginManager.js'; import { NodeWorkerDiffComputeService } from './diffComputeService.js'; @@ -140,6 +141,7 @@ export async function createAgentHostRuntime(options: ICreateAgentHostRuntimeOpt }); services.set(ITelemetryService, telemetryService); const instantiationService = new InstantiationService(services, /*strict*/ true); + let agentService: AgentService | undefined; try { const fileMonitorService = disposables.add(instantiationService.createInstance(AgentHostFileMonitorService)); services.set(IAgentHostFileMonitorService, fileMonitorService); @@ -156,7 +158,7 @@ export async function createAgentHostRuntime(options: ICreateAgentHostRuntimeOpt tmpDir: environmentService.tmpDir, }, }; - const agentService = instantiationService.createInstance(AgentService, agentServiceOptions, services); + agentService = createAgentService(agentServiceOptions, services, instantiationService, fetchFn, logService, productService); proxyResolver.bindConfigurationService(agentService.configurationService, options.transientProxyConfiguration); const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); services.set(INetworkDiagnosticsService, networkDiagnosticsService); @@ -185,6 +187,7 @@ export async function createAgentHostRuntime(options: ICreateAgentHostRuntimeOpt sdkDownloadProgress: providerInfrastructure?.sdkDownloadProgress, }; } catch (error) { + agentService?.dispose(); instantiationService.dispose(); throw error; } diff --git a/src/vs/platform/agentHost/node/agentMergeController.ts b/src/vs/platform/agentHost/node/agentMergeController.ts index 711c04b4bbe8d8..1cc11c0114b4d8 100644 --- a/src/vs/platform/agentHost/node/agentMergeController.ts +++ b/src/vs/platform/agentHost/node/agentMergeController.ts @@ -31,7 +31,7 @@ const backstopInterval = 10 * 60_000; const maximumRepeatedPromptCount = 3; const maximumTotalPromptCount = 6; -interface IAgentMergeControllerOptions { +export interface IAgentMergeControllerOptions { readonly startTurn: (session: string, turnId: string, prompt: string) => boolean; readonly cancelTurn: (session: string, turnId: string) => void; readonly getAutonomousSessionConfig: (session: string, config: Readonly>) => Record | undefined; diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 36c07b30f898c8..5c065f361fb2c2 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -19,8 +19,6 @@ import { generateUuid } from '../../../base/common/uuid.js'; import { hasKey } from '../../../base/common/types.js'; import { localize } from '../../../nls.js'; import { FileChangeType, FileOperationResult, IFileChange, IFileService, toFileOperationResult, type FileChangesEvent } from '../../files/common/files.js'; -import { IInstantiationService } from '../../instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; import { ILogService } from '../../log/common/log.js'; import { AgentProvider, AgentSession, AgentSignal, IAgent, IAgentChatContext, IAgentChatDataChange, IAgentChatMetadata, IAgentCreateChatOptions, IAgentCreateChatResult, IAgentCreateChatSideChatSelection, IAgentCreateChatSideChatSource, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDiscoveredChat, IAgentHostAuthTokenRequest, IAgentHostNetworkEndpoint, IAgentMaterializeChatEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentChatAdoptionResult, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSpawnChatEvent, AuthenticateParams, AuthenticateResult, IMcpNotification, SubagentChatSignal, subagentChatTitle } from '../common/agent.js'; import { AgentHostSessionReleaseGraceMsEnvVar, type AgentHostDebugLogsArtifactKind, type IAgentHostDebugLogsArtifact, type IAgentHostDebugLogsChunk, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult, IAgentService } from '../common/agentService.js'; @@ -37,14 +35,13 @@ import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } f import { AhpErrorCodes, AHP_SESSION_NOT_FOUND, ContentEncoding, JSON_RPC_INTERNAL_ERROR, ProtocolError, ResourceChangeType, ResourceType, ResourceWriteMode, type CreateResourceWatchParams, type CreateResourceWatchResult, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMkdirParams, type ResourceMkdirResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceResolveParams, type ResourceResolveResult, type ResourceWatchState, type ResourceWriteParams, type ResourceWriteResult, type IStateSnapshot } from '../common/state/sessionProtocol.js'; import { ChangesSummary, ChatInteractivity, ChatOriginKind, MessageAttachmentKind, type Annotation, type AnnotationEntry, type AnnotationsState, type ChatOrigin, type Customization, type Message, type MessageAttachment, type MessageResourceAttachment } from '../common/state/protocol/state.js'; import type { ChatPendingMessageSetAction, ChatTurnStartedAction, SessionConfigChangedAction } from '../common/state/protocol/actions.js'; -import { ISessionGitHubState, ISessionGitState, MessageKind, ResponsePartKind, SESSION_META_GITHUB_KEY, SESSION_META_GIT_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, AH_META_ORCHESTRATION_DB_KEY, readSessionSpawnDepth, parseSessionOrchestration, withSessionSpawnDepth, withSessionOrchestration, SessionLifecycle, SessionStatus, ToolCallStatus, ToolResultContentType, AH_META_WORKSPACELESS_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, AH_META_IS_READ_DB_KEY, buildChatUri, buildDefaultChatUri, buildResourceWatchChannelUri, buildSubagentChatUri, buildSubagentSessionUriPrefix, hostBuildInfoFromProduct, isAhpChatChannel, isDefaultChatUri, isSubagentChatUri, isSubagentSession, parseChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseResourceWatchChannelUri, parseSessionMultiRootMetadata, parseSubagentSessionUri, readSessionExternal, readSessionGitHubState, readSessionGitState, readSessionMultiRootMetadata, readSessionSourceControlState, readSessionWorkspaceless, withSessionExternal, withSessionGitHubState, withSessionGitState, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionStatusFlag, withSessionWorkspaceless, withSessionFolderPickerDecision, readSessionFolderPickerDecision, parseSessionFolderPickerDecision, SESSION_META_FOLDER_PICKER_KEY, readSessionEhcliAdoptable, type ISessionSourceControlState, type SessionConfigState, type SessionSummary, type ToolResultSubagentContent, type Turn, type UsageInfo, chatStorageUri, hasReportedUsage } from '../common/state/sessionState.js'; +import { ISessionGitHubState, ISessionGitState, MessageKind, ResponsePartKind, SESSION_META_GITHUB_KEY, SESSION_META_GIT_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, AH_META_ORCHESTRATION_DB_KEY, readSessionSpawnDepth, parseSessionOrchestration, withSessionSpawnDepth, withSessionOrchestration, SessionLifecycle, SessionStatus, ToolCallStatus, ToolResultContentType, AH_META_WORKSPACELESS_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, AH_META_IS_READ_DB_KEY, buildChatUri, buildDefaultChatUri, buildResourceWatchChannelUri, buildSubagentChatUri, buildSubagentSessionUriPrefix, isAhpChatChannel, isDefaultChatUri, isSubagentChatUri, isSubagentSession, parseChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseResourceWatchChannelUri, parseSessionMultiRootMetadata, parseSubagentSessionUri, readSessionExternal, readSessionGitHubState, readSessionGitState, readSessionMultiRootMetadata, readSessionSourceControlState, readSessionWorkspaceless, withSessionExternal, withSessionGitHubState, withSessionGitState, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionStatusFlag, withSessionWorkspaceless, withSessionFolderPickerDecision, readSessionFolderPickerDecision, parseSessionFolderPickerDecision, SESSION_META_FOLDER_PICKER_KEY, readSessionEhcliAdoptable, type ISessionSourceControlState, type SessionConfigState, type SessionSummary, type ToolResultSubagentContent, type Turn, type UsageInfo, chatStorageUri, hasReportedUsage } from '../common/state/sessionState.js'; import { readToolCallMeta } from '../common/meta/agentToolCallMeta.js'; import { readEphemeralSessionMeta, withEphemeralSessionMeta } from '../common/meta/agentEphemeralSessionMeta.js'; import { readChatSurfaceMeta, withChatSurfaceMeta } from '../common/meta/agentChatSurfaceMeta.js'; -import { IProductService } from '../../product/common/productService.js'; import { buildBoundedSideChatSourceContext, getSideChatPartialResponse } from './agentPeerChats.js'; import { AgentConfigurationService, getEffectiveWorkingDirectories, IAgentConfigurationService } from './agentConfigurationService.js'; -import { AgentHostManagedSettingsService, IAgentHostManagedSettingsService } from './agentHostManagedSettingsService.js'; +import { IAgentHostManagedSettingsService } from './agentHostManagedSettingsService.js'; import { AgentHostTerminalManager, IAgentHostTerminalManager } from './agentHostTerminalManager.js'; import { ISessionDbUriFields, parseSessionDbUri } from '../common/sessionDbUri.js'; import { IGitBlobUriFields, parseGitBlobUri } from './gitDiffContent.js'; @@ -52,44 +49,34 @@ import { resolveSessionRepositories } from './agentHostSessionRepositories.js'; import { findDeepestContainingWorkingDirectory, isMultiRootSession } from '../common/agentHostWorkingDirectories.js'; import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js'; import { createAgentChatContext } from './agentChatContext.js'; -import { AgentHostPromptCache, IAgentHostPromptCache } from './agentHostPromptCache.js'; -import { AgentHostSessionTitleSignal, IAgentHostSessionTitleSignal } from './agentHostSessionTitleSignal.js'; +import { IAgentHostPromptCache } from './agentHostPromptCache.js'; +import { IAgentHostSessionTitleSignal } from './agentHostSessionTitleSignal.js'; import { AgentHostDebugLogsCollector, type IAgentHostDebugLogsEnvironment } from './agentHostDebugLogs.js'; -import { AgentHostDatabase, IAgentHostDatabase } from './agentHostDatabase.js'; +import { IAgentHostDatabase } from './agentHostDatabase.js'; import { AgentSessionRegistry, IRegisteredSession, IStoredRegisteredSession } from './agentSessionRegistry.js'; import { IAgentHostGitService } from '../common/agentHostGitService.js'; -import { AgentSideEffects } from './agentSideEffects.js'; +import { AgentSideEffects, type IAgentSideEffectsOptions } from './agentSideEffects.js'; import { AgentHostLocalTurns } from './agentHostLocalTurns.js'; import { AgentServerToolHost } from './shared/agentServerToolHost.js'; -import { buildServerToolGroups } from './shared/serverToolGroups.js'; import { type IChatContextSnapshot, type IRenameTitleResult, type ISessionCreationDefaults, type ISessionServerToolAccessor, validateRenameTitle } from './shared/sessionServerTools.js'; import { AGENT_HOST_TITLE_SOURCE_AGENT, customChatTitleMetadataKey, customChatTitleSourceMetadataKey, persistSessionMetadataValues, SESSION_CUSTOM_TITLE_KEY, SESSION_CUSTOM_TITLE_SOURCE_KEY } from './shared/persistSessionMetadata.js'; import { buildWorktreeFailureNotification, WorktreeIsolation, WORKTREE_META_REPOSITORY_ROOT, worktreeProjectFromRepositoryRoot } from './shared/worktreeIsolation.js'; -import { AgentHostChangesetService } from './agentHostChangesetService.js'; import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js'; import { IAgentHostReviewService } from '../common/agentHostReviewService.js'; import { AgentHostChangesetCoordinator } from './agentHostChangesetCoordinator.js'; -import { AgentHostCompletions, IAgentHostCompletions } from './agentHostCompletions.js'; -import { AgentHostChatCompletionProvider } from './agentHostChatCompletionProvider.js'; -import { AgentHostFileCompletionProvider } from './agentHostFileCompletionProvider.js'; -import { AgentHostRenameCompletionProvider } from './agentHostRenameCommand.js'; +import { IAgentHostCompletions } from './agentHostCompletions.js'; import { AgentHostSkillCompletionProvider } from './agentHostSkillCompletionProvider.js'; -import { AgentHostWorkspaceFiles } from './agentHostWorkspaceFiles.js'; import { SessionServerToolName } from '../common/serverToolNames.js'; -import { CodexCompactCompletionProvider } from './codexCompactCommand.js'; -import { CopilotApiService, ICopilotApiService } from './shared/copilotApiService.js'; +import { ICopilotApiService } from './shared/copilotApiService.js'; import { INetworkDiagnosticsService } from './networkDiagnosticsService.js'; import { parseMcpChannelUri } from './shared/mcpCustomizationController.js'; import { toAgentClientUri } from '../common/agentClientUri.js'; import { AgentHostClientType } from '../common/agentHostClientInfo.js'; import { AgentHostLaunchKind, createUnknownAgentHostClientTelemetryContext, type IAgentHostClientTelemetryContext } from '../common/agentHostTelemetry.js'; -import { AgentHostChangesetOperationService } from './agentHostChangesetOperationService.js'; -import { AgentHostGitStateService } from './agentHostGitStateService.js'; -import { AgentHostGitHubEndpointService, IAgentHostGitHubEndpointService } from './agentHostGitHubEndpointService.js'; -import { AgentMergeController } from './agentMergeController.js'; +import { IAgentHostGitHubEndpointService } from './agentHostGitHubEndpointService.js'; +import { AgentMergeController, type IAgentMergeControllerOptions } from './agentMergeController.js'; import { AgentMergeConfigKey, agentMergeRootConfigSchema } from '../common/agentMerge.js'; -import { AgentMergeTools } from './agentMergeTools.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AgentHostAuthenticationService } from './agentHostAuthenticationService.js'; import { updateAgentHostTelemetryLevelFromConfig } from './agentHostTelemetryService.js'; @@ -97,21 +84,10 @@ import { AgentHostActiveAgentTitleGenerationConfigKey, AgentHostEditTelemetryEna import { AgentHostCustomizationEnablementService, IAgentHostCustomizationEnablementService } from './agentHostCustomizationEnablementService.js'; import { AgentHostStorageService, IAgentHostStorageService } from './agentHostStorageService.js'; import { SessionCoordinationService } from './sessionCoordination.js'; -import { AgentHostOctoKitService, IAgentHostOctoKitService } from './shared/agentHostOctoKitService.js'; -import { GitHubService, IGitHubService } from '../../github/common/githubService.js'; +import { IAgentHostOctoKitService } from './shared/agentHostOctoKitService.js'; import { IAgentHostChangesetService, CHANGESET_DB_METADATA_KEYS, META_CHANGES_SUMMARY } from '../common/agentHostChangesetService.js'; -import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChangesetSubscriptionService.js'; -import { AgentHostChangesetSubscriptionService } from './agentHostChangesetSubscriptionService.js'; import { GIT_DB_METADATA_KEYS, IAgentHostGitStateService, META_GIT_STATE, META_GITHUB_STATE, META_SOURCE_CONTROL_STATE } from '../common/agentHostGitStateService.js'; import { IAgentHostChangesetOperationService } from '../common/agentHostChangesetOperationService.js'; -import { AgentHostCommitOperationContribution } from './agentHostCommitOperationProvider.js'; -import { IAgentHostProxyResolver } from './agentHostProxyResolver.js'; -import { AgentHostDiscardChangesOperationContribution } from './agentHostDiscardChangesOperationProvider.js'; -import { AgentHostMergeOperationContribution } from './agentHostMergeOperationProvider.js'; -import { AgentHostPullRequestOperationContribution } from './agentHostPullRequestOperationProvider.js'; -import { AgentHostSyncOperationContribution } from './agentHostSyncOperationProvider.js'; -import { AgentHostReviewService } from './agentHostReviewService.js'; -import { AgentHostCheckpointService } from './agentHostCheckpointService.js'; /** * Grace period before an empty, unsubscribed session is garbage-collected @@ -318,10 +294,68 @@ export interface IAgentServiceOptions { readonly hostLaunchKind?: AgentHostLaunchKind; readonly storageResource?: URI; readonly orchestratorDatabase?: IAgentHostDatabase; - readonly now?: () => number; readonly debugLogsEnvironment?: IAgentHostDebugLogsEnvironment; } +/** Core state and callbacks exposed only to the Agent Host composition root. */ +export interface IAgentServiceCompositionContext { + readonly stateManager: AgentHostStateManager; + readonly configurationService: AgentConfigurationService; + readonly storageService: AgentHostStorageService; + readonly managedSettingsService: IAgentHostManagedSettingsService; + readonly sessionDataService: ISessionDataService; + readonly agents: IObservable; + readonly hostLaunchKind: AgentHostLaunchKind; + readonly copilotApiServiceOverride: ICopilotApiService | undefined; + readonly getAuthToken: (request: IAgentHostAuthTokenRequest) => string | undefined; + readonly createAgentMergeControllerOptions: () => IAgentMergeControllerOptions; + readonly createSideEffectsOptions: (services: { + readonly localTurns: AgentHostLocalTurns; + readonly copilotApiService: ICopilotApiService; + readonly octoKitService: IAgentHostOctoKitService; + readonly gitStateService: IAgentHostGitStateService; + }) => IAgentSideEffectsOptions; + readonly getSessionMetadata: (session: URI) => Promise; + readonly restoreSession: (session: URI) => Promise; + readonly createSessionServerToolAccessor: () => ISessionServerToolAccessor; +} + +/** Collaborators constructed by the composition root after registering {@link IAgentService}. */ +export interface IAgentServiceInitialization { + readonly gitHubEndpointService: IAgentHostGitHubEndpointService; + readonly customizationEnablementService: AgentHostCustomizationEnablementService; + readonly gitStateService: IAgentHostGitStateService; + readonly agentMergeController: AgentMergeController; + readonly checkpointService: IAgentHostCheckpointService; + readonly promptCache: IAgentHostPromptCache; + readonly sessionTitleSignal: IAgentHostSessionTitleSignal; + readonly changesetOperationService: IAgentHostChangesetOperationService; + readonly reviewService: IAgentHostReviewService; + readonly changesets: IAgentHostChangesetService; + readonly changesetCoordinator: AgentHostChangesetCoordinator; + readonly completions: IAgentHostCompletions; + readonly terminalManager: AgentHostTerminalManager; + readonly localTurns: AgentHostLocalTurns; + readonly sideEffects: AgentSideEffects; + readonly sessionCoordination: SessionCoordinationService; + readonly serverToolHost: AgentServerToolHost; +} + +/** Core services that must exist before {@link AgentService} can be constructed. */ +export interface IAgentServiceCore { + readonly disposables: DisposableStore; + readonly authenticationService: AgentHostAuthenticationService; + readonly orchestratorDatabase: IAgentHostDatabase; + readonly debugLogsCollector: AgentHostDebugLogsCollector | undefined; + readonly sessionRegistry: AgentSessionRegistry; + readonly stateManager: AgentHostStateManager; + readonly configurationService: AgentConfigurationService; + readonly storageService: AgentHostStorageService; + readonly managedSettingsService: IAgentHostManagedSettingsService; + readonly hostLaunchKind: AgentHostLaunchKind; + readonly copilotApiServiceOverride: ICopilotApiService | undefined; +} + /** * The agent service implementation that runs inside the agent-host utility * process. Dispatches to registered {@link IAgent} instances based @@ -346,8 +380,8 @@ export class AgentService extends Disposable implements IAgentService { /** Authoritative state manager for the sessions process protocol. */ private readonly _stateManager: AgentHostStateManager; - private readonly _sessionCoordination: SessionCoordinationService; - private readonly _managedSettingsService = this._register(new AgentHostManagedSettingsService()); + private _sessionCoordination!: SessionCoordinationService; + private readonly _managedSettingsService: IAgentHostManagedSettingsService; /** * Orchestrator-owned durable index of known sessions. Populated alongside @@ -433,33 +467,32 @@ export class AgentService extends Disposable implements IAgentService { /** Observable registered agents, drives `root/agentsChanged` via {@link AgentSideEffects}. */ private readonly _agents = observableValue('agents', []); /** Shared side-effect handler for action dispatch and session lifecycle. */ - private readonly _sideEffects: AgentSideEffects; - private readonly _agentMergeController: AgentMergeController; + private _sideEffects!: AgentSideEffects; + private _agentMergeController!: AgentMergeController; /** Owns static / per-turn changeset compute, publish, persist, restore. */ - private readonly _changesets: IAgentHostChangesetService; + private _changesets!: IAgentHostChangesetService; /** Shared active changeset subscription registry. */ - private readonly _changesetSubscriptions: IAgentHostChangesetSubscriptionService; /** Owns changeset operation contributions and handler activation. */ - private readonly _changesetOperationService: IAgentHostChangesetOperationService; - private readonly _reviewService: IAgentHostReviewService; + private _changesetOperationService!: IAgentHostChangesetOperationService; + private _reviewService!: IAgentHostReviewService; /** Owns AgentService-side orchestration of the changeset feature. */ - private readonly _changesetCoordinator: AgentHostChangesetCoordinator; + private _changesetCoordinator!: AgentHostChangesetCoordinator; /** Owns session git-state probing and git-backed catalogue decoration. */ - private readonly _gitStateService: IAgentHostGitStateService; + private _gitStateService!: IAgentHostGitStateService; /** Manages PTY-backed terminals for the agent host protocol. */ - private readonly _terminalManager: AgentHostTerminalManager; + private _terminalManager!: AgentHostTerminalManager; /** Persists host-injected `/rename` / `!command` turns for restore & fork/truncate. */ - private readonly _localTurns: AgentHostLocalTurns; + private _localTurns!: AgentHostLocalTurns; /** Server-side host for the agent host's server tools. */ - private readonly _serverToolHost: AgentServerToolHost; + private _serverToolHost!: AgentServerToolHost; private readonly _debugLogsCollector: AgentHostDebugLogsCollector | undefined; private readonly _configurationService: AgentConfigurationService; private readonly _storageService: AgentHostStorageService; - private readonly _customizationEnablementService: AgentHostCustomizationEnablementService; + private _customizationEnablementService!: AgentHostCustomizationEnablementService; /** Captures baseline / per-turn git checkpoints backing the changeset pipeline. */ - private readonly _checkpointService: IAgentHostCheckpointService; - private readonly _promptCache: IAgentHostPromptCache; - private readonly _sessionTitleSignal: IAgentHostSessionTitleSignal; + private _checkpointService!: IAgentHostCheckpointService; + private _promptCache!: IAgentHostPromptCache; + private _sessionTitleSignal!: IAgentHostSessionTitleSignal; /** * Host-owned worktree isolation controller. Set post-construction via * {@link setWorktreeIsolation} after host startup constructs the Copilot API @@ -470,17 +503,16 @@ export class AgentService extends Disposable implements IAgentService { */ private _worktree: WorktreeIsolation | undefined; /** Single source of truth for GitHub (Enterprise) endpoints and protected resources. */ - private readonly _gitHubEndpointService: IAgentHostGitHubEndpointService; - private readonly _copilotApiService: ICopilotApiService; + private _gitHubEndpointService!: IAgentHostGitHubEndpointService; /** Pluggable completion item providers (e.g. workspace file completions, agent-specific @-mentions). */ - private readonly _completions: IAgentHostCompletions; + private _completions!: IAgentHostCompletions; + private _initialized = false; private _skillCompletionProviderRegistered = false; /** Backs {@link getNetworkDiagnosticsInfo} / {@link diagnosticsFetch}; wired via {@link setNetworkDiagnosticsService}. */ private _networkDiagnostics: INetworkDiagnosticsService | undefined; private _editAttributionService: IAgentEditAttributionService | undefined; - private readonly _rootConfigResource: URI | undefined; private readonly _hostLaunchKind: AgentHostLaunchKind; - private readonly _now: () => number; + private readonly _copilotApiServiceOverride: ICopilotApiService | undefined; /** * Authoritative server-side per-resource subscription refcount, keyed by @@ -563,39 +595,23 @@ export class AgentService extends Disposable implements IAgentService { get completionTriggerCharacters(): readonly string[] { return this._completions.triggerCharacters; } constructor( - options: IAgentServiceOptions, - services: ServiceCollection, - @IInstantiationService instantiationService: IInstantiationService, + core: IAgentServiceCore, @ILogService private readonly _logService: ILogService, @IFileService private readonly _fileService: IFileService, @ISessionDataService private readonly _sessionDataService: ISessionDataService, - @IProductService private readonly _productService: IProductService, @IAgentHostGitService private readonly _gitService: IAgentHostGitService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IAgentHostProxyResolver proxyResolver: IAgentHostProxyResolver, ) { super(); - this._rootConfigResource = options.rootConfigResource; - this._hostLaunchKind = options.hostLaunchKind ?? AgentHostLaunchKind.Unknown; - this._now = options.now ?? Date.now; - const fetchFn = proxyResolver.fetch.bind(proxyResolver); + this._register(core.disposables); + this._hostLaunchKind = core.hostLaunchKind; + this._copilotApiServiceOverride = core.copilotApiServiceOverride; this._logService.info('AgentService initialized'); - this._authService = new AgentHostAuthenticationService(_logService); - const databasePath = this._rootConfigResource - ? joinPath(resourcesDirname(this._rootConfigResource), 'agent-host.db').fsPath - : ':memory:'; - this._orchestratorDatabase = this._register(options.orchestratorDatabase ?? new AgentHostDatabase(databasePath)); - this._debugLogsCollector = options.debugLogsEnvironment ? this._register(new AgentHostDebugLogsCollector(options.debugLogsEnvironment, this._logService)) : undefined; - this._sessionRegistry = this._register(new AgentSessionRegistry(this._orchestratorDatabase)); - this._stateManager = this._register(new AgentHostStateManager(_logService, { - hostBuildInfo: hostBuildInfoFromProduct(this._productService), - changesetStateRetention: { - // The cache calls this lazily after construction. If a future state-manager - // initialization path registers changesets before `_changesets` is assigned, - // keep the entry pinned rather than evicting with incomplete liveness data. - canEvict: changeset => this._changesets ? this._isChangesetEvictable(changeset) : false, - }, - })); + this._authService = core.authenticationService; + this._orchestratorDatabase = core.orchestratorDatabase; + this._debugLogsCollector = core.debugLogsCollector; + this._sessionRegistry = core.sessionRegistry; + this._stateManager = core.stateManager; this._register(this._stateManager.onDidEmitEnvelope(e => this._onDidAction.fire(e))); this._register(this._stateManager.onDidEmitEnvelope(e => this._trackPendingSubagentChatFromEnvelope(e))); this._register(this._stateManager.onDidEmitEnvelope(e => this._persistAnnotations(e))); @@ -609,15 +625,90 @@ export class AgentService extends Disposable implements IAgentService { this._queueSessionListReconciliation(); } })); - // Build a local instantiation scope so downstream components can - // consume {@link IAgentConfigurationService} (and later {@link ILogService}) - // via DI rather than being plumbed plain-class references. - const configurationService = this._register(new AgentConfigurationService(this._stateManager, this._logService, this._rootConfigResource, options.providerConfigurations ?? [])); - this._configurationService = configurationService; + this._configurationService = core.configurationService; + this._storageService = core.storageService; + this._managedSettingsService = core.managedSettingsService; + updateAgentHostTelemetryLevelFromConfig(this._telemetryService, this._stateManager.rootState.config?.values); + } + + /** Returns the narrow state and callback surface needed to compose collaborators. */ + getCompositionContext(): IAgentServiceCompositionContext { + return { + stateManager: this._stateManager, + configurationService: this._configurationService, + storageService: this._storageService, + managedSettingsService: this._managedSettingsService, + sessionDataService: this._sessionDataService, + agents: this._agents, + hostLaunchKind: this._hostLaunchKind, + copilotApiServiceOverride: this._copilotApiServiceOverride, + getAuthToken: request => this._authService.getAuthToken(request), + createAgentMergeControllerOptions: () => ({ + startTurn: (session, turnId, prompt) => this._startAgentMergePrompt(session, turnId, prompt), + cancelTurn: (session, turnId) => this._cancelAgentMergePrompt(session, turnId), + getAutonomousSessionConfig: (session, config) => this._findProviderForSession(session)?.getAutonomousSessionConfig?.(config), + }), + createSideEffectsOptions: services => ({ + getAgent: session => this._findProviderForSession(session), + sessionDataService: this._sessionDataService, + localTurns: services.localTurns, + agents: this._agents, + hostLaunchKind: this._hostLaunchKind, + copilotApiService: services.copilotApiService, + getGitHubCopilotToken: () => { + const resource = this._gitHubEndpointService.getCopilotResource(); + return this._authService.getAuthToken({ resource: resource.resource, scopes: resource.scopes_supported }); + }, + getGitHubToken: () => { + const resource = this._gitHubEndpointService.getRepoResource(); + return this._authService.getAuthToken({ resource: resource.resource, scopes: resource.scopes_supported }); + }, + getGitHubHost: () => this._gitHubEndpointService.getEnterpriseHost() ?? 'github.com', + octoKitService: services.octoKitService, + resolveWorkingDirectoryBeforeSend: params => this._resolveWorkingDirectoryBeforeSend(params), + resolveChatAttachmentTurns: resource => this._resolveChatAttachmentTurns(resource), + onTurnComplete: session => { + const workingDirStr = this._stateManager.getSessionState(session)?.workingDirectories?.[0]; + void services.gitStateService.attachSessionGitHubPullRequest(session, workingDirStr ? URI.parse(workingDirStr) : undefined); + }, + onUserMessage: (session, text) => { + void services.gitStateService.attachSessionGitHubReferences(session.toString(), text); + }, + }), + getSessionMetadata: session => this._getSessionMetadata(session), + restoreSession: session => this.restoreSession(session), + createSessionServerToolAccessor: () => this._createSessionServerToolAccessor(), + }; + } + + /** Completes the one-time wiring of collaborators that depend on {@link IAgentService}. */ + initialize(initialization: IAgentServiceInitialization): void { + if (this._initialized) { + throw new Error('AgentService has already been initialized'); + } + this._initialized = true; + this._gitHubEndpointService = initialization.gitHubEndpointService; + this._customizationEnablementService = initialization.customizationEnablementService; + this._gitStateService = initialization.gitStateService; + this._agentMergeController = initialization.agentMergeController; + this._checkpointService = initialization.checkpointService; + this._promptCache = initialization.promptCache; + this._sessionTitleSignal = initialization.sessionTitleSignal; + this._changesetOperationService = initialization.changesetOperationService; + this._reviewService = initialization.reviewService; + this._changesets = initialization.changesets; + this._changesetCoordinator = initialization.changesetCoordinator; + this._completions = initialization.completions; + this._terminalManager = initialization.terminalManager; + this._localTurns = initialization.localTurns; + this._sideEffects = initialization.sideEffects; + this._sessionCoordination = initialization.sessionCoordination; + this._serverToolHost = initialization.serverToolHost; + let externalSessionsMode = this._getExternalSessionsMode(); this._lastMigrateLegacyEnabled = this._isMigrateLegacyEnabled(); let agentMergeEnabled = this._isAgentMergeEnabled(); - this._register(configurationService.onDidRootConfigChange(() => { + this._register(this._configurationService.onDidRootConfigChange(() => { const nextMode = this._getExternalSessionsMode(); if (nextMode !== externalSessionsMode) { const previousMode = externalSessionsMode; @@ -625,8 +716,6 @@ export class AgentService extends Disposable implements IAgentService { this._logService.info(`[AgentService] ${AgentHostShowExternalSessionsConfigKey} changed '${previousMode}' -> '${nextMode}'; queueing session list reconciliation`); this._queueSessionListReconciliation(previousMode); } - // Agent Merge tools are only advertised while the feature is on, so a - // toggle has to reach sessions that were advertised under the old value. const nextAgentMergeEnabled = this._isAgentMergeEnabled(); if (nextAgentMergeEnabled !== agentMergeEnabled) { agentMergeEnabled = nextAgentMergeEnabled; @@ -636,179 +725,12 @@ export class AgentService extends Disposable implements IAgentService { } this._onMigrateLegacySettingChanged(); })); - this._storageService = this._register(new AgentHostStorageService(options.storageResource, this._logService)); - updateAgentHostTelemetryLevelFromConfig(this._telemetryService, this._stateManager.rootState.config?.values); - services.set(IAgentService, this); - services.set(IAgentConfigurationService, configurationService); - services.set(IAgentHostStateManager, this._stateManager); - services.set(IAgentHostStorageService, this._storageService); - services.set(IAgentHostManagedSettingsService, this._managedSettingsService); - this._gitHubEndpointService = this._register(instantiationService.createInstance(AgentHostGitHubEndpointService)); - services.set(IAgentHostGitHubEndpointService, this._gitHubEndpointService); - // A GitHub Enterprise URI change repoints every agent's GitHub resource - // identity to a different authorization server, so the client must obtain a - // token for the new resource. One root-channel `auth/required` covers all - // agents (the URI is host-level config). this._register(this._gitHubEndpointService.onDidChange(() => { this._stateManager.emitAuthRequired({ resource: this._gitHubEndpointService.getCopilotResource(), reason: AuthRequiredReason.Required, }); })); - const agentHostOctoKitService = instantiationService.createInstance(AgentHostOctoKitService, fetchFn); - services.set(IAgentHostOctoKitService, agentHostOctoKitService); - const gitHubService = this._register(instantiationService.createInstance(GitHubService, { - endpoint: this._gitHubEndpointService, - tokenProvider: { - getToken: () => { - const resource = this._gitHubEndpointService.getRepoResource(); - return this._authService.getAuthToken({ - resource: resource.resource, - scopes: resource.scopes_supported, - }); - }, - }, - fetch: fetchFn, - })); - services.set(IGitHubService, gitHubService); - this._copilotApiService = options.copilotApiService ?? instantiationService.createInstance(CopilotApiService, fetchFn); - services.set(ICopilotApiService, this._copilotApiService); - this._customizationEnablementService = this._register(instantiationService.createInstance(AgentHostCustomizationEnablementService)); - services.set(IAgentHostCustomizationEnablementService, this._customizationEnablementService); - - this._gitStateService = this._register(instantiationService.createInstance(AgentHostGitStateService)); - services.set(IAgentHostGitStateService, this._gitStateService); - this._agentMergeController = this._register(instantiationService.createInstance(AgentMergeController, { - startTurn: (session, turnId, prompt) => this._startAgentMergePrompt(session, turnId, prompt), - cancelTurn: (session, turnId) => this._cancelAgentMergePrompt(session, turnId), - getAutonomousSessionConfig: (session, config) => this._findProviderForSession(session)?.getAutonomousSessionConfig?.(config), - })); - - this._checkpointService = this._register(instantiationService.createInstance(AgentHostCheckpointService)); - services.set(IAgentHostCheckpointService, this._checkpointService); - - this._promptCache = instantiationService.createInstance(AgentHostPromptCache); - services.set(IAgentHostPromptCache, this._promptCache); - this._sessionTitleSignal = this._register(instantiationService.createInstance(AgentHostSessionTitleSignal)); - services.set(IAgentHostSessionTitleSignal, this._sessionTitleSignal); - - // The subscription service manages the lifecycle of changeset subscriptions. The service - // is also consulted by other services when refreshing changesets and changeset operations. - this._changesetSubscriptions = instantiationService.createInstance(AgentHostChangesetSubscriptionService); - services.set(IAgentHostChangesetSubscriptionService, this._changesetSubscriptions); - - // The operation contribution service manages the lifecycle of changeset operations. - this._changesetOperationService = this._register(instantiationService.createInstance(AgentHostChangesetOperationService)); - services.set(IAgentHostChangesetOperationService, this._changesetOperationService); - - // The changes review service is responsible for managing review/unreview state for changeset changes. - this._reviewService = this._register(instantiationService.createInstance(AgentHostReviewService)); - services.set(IAgentHostReviewService, this._reviewService); - - // The changeset service is responsible for computing, publishing, and persisting changesets. - this._changesets = this._register(instantiationService.createInstance(AgentHostChangesetService)); - services.set(IAgentHostChangesetService, this._changesets); - - // The coordinator owns all AgentService-side orchestration of the changeset feature: lifecycle - // hooks, listSessions overlay, subscription URI routing, and the deferred-refresh state machine. - this._changesetCoordinator = this._register(instantiationService.createInstance(AgentHostChangesetCoordinator)); - this._register(this._stateManager.onDidChangeSessionActiveTurn(e => this._changesetCoordinator.onSessionTurnActiveChanged(e.session, e.active))); - - // Register the changeset operation contributions. - this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostCommitOperationContribution))); - this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostPullRequestOperationContribution))); - this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostMergeOperationContribution))); - this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostSyncOperationContribution))); - this._register(this._changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostDiscardChangesOperationContribution))); - - this._completions = this._register(instantiationService.createInstance(AgentHostCompletions)); - services.set(IAgentHostCompletions, this._completions); - // Built-in generic provider: completes files in the session's workspace folder. - const workspaceFiles = this._register(instantiationService.createInstance(AgentHostWorkspaceFiles)); - this._register(this._completions.registerProvider( - new AgentHostFileCompletionProvider(this._stateManager, workspaceFiles, this._logService), - )); - // Built-in generic provider: completes `#chat:` references to other - // chats in the same session, attaching a chat transcript attachment. - this._register(this._completions.registerProvider( - new AgentHostChatCompletionProvider(this._stateManager), - )); - // Built-in generic provider: offers the `/rename` slash command for any - // session that already has history. Execution is handled server-side in - // AgentSideEffects (redirected to a SessionTitleChanged action). - this._register(this._completions.registerProvider( - new AgentHostRenameCompletionProvider( - session => (this._stateManager.getSessionState(session)?.turns.length ?? 0) > 0, - ), - )); - this._register(this._completions.registerProvider( - new CodexCompactCompletionProvider( - session => (this._stateManager.getSessionState(session)?.turns.length ?? 0) > 0, - ), - )); - - // Terminal management — the terminal manager listens to the state - // manager's action stream and dispatches PTY output back through it. - // Created before AgentSideEffects and registered in the local scope so - // AgentSideEffects can consume it via DI (for inline `!command` - // execution). - this._terminalManager = this._register(instantiationService.createInstance(AgentHostTerminalManager)); - services.set(IAgentHostTerminalManager, this._terminalManager); - - this._localTurns = new AgentHostLocalTurns(this._sessionDataService, this._logService); - - this._sideEffects = this._register(instantiationService.createInstance(AgentSideEffects, this._stateManager, this._customizationEnablementService, { - getAgent: session => this._findProviderForSession(session), - sessionDataService: this._sessionDataService, - localTurns: this._localTurns, - agents: this._agents, - hostLaunchKind: this._hostLaunchKind, - copilotApiService: this._copilotApiService, - getGitHubCopilotToken: () => { - return this.getAuthToken({ - resource: this._gitHubEndpointService.getCopilotResource().resource, - scopes: this._gitHubEndpointService.getCopilotResource().scopes_supported, - }); - }, - getGitHubToken: () => { - return this.getAuthToken({ - resource: this._gitHubEndpointService.getRepoResource().resource, - scopes: this._gitHubEndpointService.getRepoResource().scopes_supported, - }); - }, - getGitHubHost: () => this._gitHubEndpointService.getEnterpriseHost() ?? 'github.com', - octoKitService: agentHostOctoKitService, - resolveWorkingDirectoryBeforeSend: params => this._resolveWorkingDirectoryBeforeSend(params), - resolveChatAttachmentTurns: resource => this._resolveChatAttachmentTurns(resource), - onTurnComplete: session => { - const workingDirStr = this._stateManager.getSessionState(session)?.workingDirectories?.[0]; - void this._gitStateService.attachSessionGitHubPullRequest(session, workingDirStr ? URI.parse(workingDirStr) : undefined); - }, - onUserMessage: (session, text) => { - void this._gitStateService.attachSessionGitHubReferences(session.toString(), text); - }, - })); - this._sessionCoordination = this._register(new SessionCoordinationService( - this._stateManager, - this._sessionDataService, - this._logService, - { - getSessionMetadata: session => this._getSessionMetadata(session), - restoreSession: session => this.restoreSession(session), - handleAction: (chat, action) => this._sideEffects.handleAction(chat, action), - }, - )); - - // Server-side tools, executed in-process against each session's own - // state. The set of groups (and their display) is the single source of - // truth in `serverToolGroups.ts`; the session-management group's runtime - // dependency (this service) is injected via the accessor. - const agentMergeTools = instantiationService.createInstance( - AgentMergeTools, - () => this._agentMergeController.isEnabled(), - session => this._agentMergeController.getTurnContext(session), - ); - this._serverToolHost = new AgentServerToolHost(this._stateManager, buildServerToolGroups(this._createSessionServerToolAccessor(), agentMergeTools)); } /** @@ -1852,7 +1774,7 @@ export class AgentService extends Disposable implements IAgentService { }); } const combined = additions.length > 0 ? [...withStatus, ...additions] : withStatus; - const now = this._now(); + const now = Date.now(); const recentSessionKeys = mode === AgentHostExternalSessionsMode.Recent ? this._getRecentSessionKeys(combined, now) : undefined; @@ -1925,7 +1847,7 @@ export class AgentService extends Disposable implements IAgentService { private _shouldIncludeSession( session: IAgentSessionMetadata, mode = this._getExternalSessionsMode(), - now = this._now(), + now = Date.now(), recentSessionKeys?: ReadonlySet<string>, ): boolean { // While migration is off, un-adopted adoptable-legacy sessions belong to the extension-host provider — exclude so a refresh cannot re-surface an unopenable row. @@ -2080,7 +2002,7 @@ export class AgentService extends Disposable implements IAgentService { previousMode: AgentHostExternalSessionsMode, previouslyBroadcast: Set<string>, ): IAgentSessionMetadata[] { - const now = this._now(); + const now = Date.now(); const recentKeysFor = (mode: AgentHostExternalSessionsMode) => mode === AgentHostExternalSessionsMode.Recent ? this._getRecentSessionKeys(superset, now) : undefined; @@ -3946,8 +3868,8 @@ export class AgentService extends Disposable implements IAgentService { this._stateManager.removeSession(evictionTargetKey); } - // Returns true when a changeset is safe to drop from the in-memory cache. - private _isChangesetEvictable(changeset: string): boolean { + /** Returns true when a changeset is safe to drop from the in-memory cache. */ + canEvictChangeset(changeset: string): boolean { const changesetUri = URI.parse(changeset); // A direct changeset subscriber is rendering this expanded URI. Keep // the state alive so future envelopes still target an existing object. diff --git a/src/vs/platform/agentHost/node/agentServiceComposition.ts b/src/vs/platform/agentHost/node/agentServiceComposition.ts new file mode 100644 index 00000000000000..8c111f5a23c11e --- /dev/null +++ b/src/vs/platform/agentHost/node/agentServiceComposition.ts @@ -0,0 +1,236 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore, type IDisposable } from '../../../base/common/lifecycle.js'; +import { dirname, joinPath } from '../../../base/common/resources.js'; +import { GitHubService, IGitHubService } from '../../github/common/githubService.js'; +import { IInstantiationService } from '../../instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../instantiation/common/serviceCollection.js'; +import { ILogService } from '../../log/common/log.js'; +import { IProductService } from '../../product/common/productService.js'; +import { IAgentHostChangesetOperationService } from '../common/agentHostChangesetOperationService.js'; +import { IAgentHostChangesetService } from '../common/agentHostChangesetService.js'; +import { IAgentHostChangesetSubscriptionService } from '../common/agentHostChangesetSubscriptionService.js'; +import { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.js'; +import { IAgentHostGitStateService } from '../common/agentHostGitStateService.js'; +import { IAgentHostReviewService } from '../common/agentHostReviewService.js'; +import { IAgentService } from '../common/agentService.js'; +import { AgentHostLaunchKind } from '../common/agentHostTelemetry.js'; +import { AgentConfigurationService, IAgentConfigurationService } from './agentConfigurationService.js'; +import { AgentHostAuthenticationService } from './agentHostAuthenticationService.js'; +import { AgentHostChangesetCoordinator } from './agentHostChangesetCoordinator.js'; +import { AgentHostChangesetOperationService } from './agentHostChangesetOperationService.js'; +import { AgentHostChangesetService } from './agentHostChangesetService.js'; +import { AgentHostChangesetSubscriptionService } from './agentHostChangesetSubscriptionService.js'; +import { AgentHostChatCompletionProvider } from './agentHostChatCompletionProvider.js'; +import { AgentHostCheckpointService } from './agentHostCheckpointService.js'; +import { AgentHostCommitOperationContribution } from './agentHostCommitOperationProvider.js'; +import { AgentHostCompletions, IAgentHostCompletions } from './agentHostCompletions.js'; +import { AgentHostCustomizationEnablementService, IAgentHostCustomizationEnablementService } from './agentHostCustomizationEnablementService.js'; +import { AgentHostDiscardChangesOperationContribution } from './agentHostDiscardChangesOperationProvider.js'; +import { AgentHostDebugLogsCollector } from './agentHostDebugLogs.js'; +import { AgentHostDatabase } from './agentHostDatabase.js'; +import { AgentHostFileCompletionProvider } from './agentHostFileCompletionProvider.js'; +import { AgentHostGitStateService } from './agentHostGitStateService.js'; +import { AgentHostGitHubEndpointService, IAgentHostGitHubEndpointService } from './agentHostGitHubEndpointService.js'; +import { AgentHostLocalTurns } from './agentHostLocalTurns.js'; +import { AgentHostManagedSettingsService, IAgentHostManagedSettingsService } from './agentHostManagedSettingsService.js'; +import { AgentHostMergeOperationContribution } from './agentHostMergeOperationProvider.js'; +import { AgentHostPromptCache, IAgentHostPromptCache } from './agentHostPromptCache.js'; +import { AgentHostPullRequestOperationContribution } from './agentHostPullRequestOperationProvider.js'; +import { AgentHostRenameCompletionProvider } from './agentHostRenameCommand.js'; +import { AgentHostReviewService } from './agentHostReviewService.js'; +import { AgentHostSessionTitleSignal, IAgentHostSessionTitleSignal } from './agentHostSessionTitleSignal.js'; +import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js'; +import { AgentHostStorageService, IAgentHostStorageService } from './agentHostStorageService.js'; +import { AgentHostSyncOperationContribution } from './agentHostSyncOperationProvider.js'; +import { AgentHostTerminalManager, IAgentHostTerminalManager } from './agentHostTerminalManager.js'; +import { AgentHostWorkspaceFiles } from './agentHostWorkspaceFiles.js'; +import { AgentMergeController } from './agentMergeController.js'; +import { AgentMergeTools } from './agentMergeTools.js'; +import { AgentService, type IAgentServiceCore, type IAgentServiceOptions } from './agentService.js'; +import { AgentSessionRegistry } from './agentSessionRegistry.js'; +import { AgentSideEffects } from './agentSideEffects.js'; +import { CodexCompactCompletionProvider } from './codexCompactCommand.js'; +import { SessionCoordinationService } from './sessionCoordination.js'; +import { AgentServerToolHost } from './shared/agentServerToolHost.js'; +import { buildServerToolGroups } from './shared/serverToolGroups.js'; +import { AgentHostOctoKitService, IAgentHostOctoKitService } from './shared/agentHostOctoKitService.js'; +import { CopilotApiService, ICopilotApiService } from './shared/copilotApiService.js'; +import { hostBuildInfoFromProduct } from '../common/state/sessionState.js'; + +/** Constructs, registers, and initializes the complete {@link AgentService} collaborator graph. */ +export function createAgentService( + options: IAgentServiceOptions, + services: ServiceCollection, + instantiationService: IInstantiationService, + fetchFn: typeof globalThis.fetch, + logService: ILogService, + productService: IProductService, + additionalDisposables: readonly IDisposable[] = [], +): AgentService { + const owned = new DisposableStore(); + let agentService: AgentService | undefined; + try { + for (const disposable of additionalDisposables) { + owned.add(disposable); + } + const databasePath = options.rootConfigResource + ? joinPath(dirname(options.rootConfigResource), 'agent-host.db').fsPath + : ':memory:'; + const orchestratorDatabase = owned.add(options.orchestratorDatabase ?? new AgentHostDatabase(databasePath)); + const debugLogsCollector = options.debugLogsEnvironment + ? owned.add(new AgentHostDebugLogsCollector(options.debugLogsEnvironment, logService)) + : undefined; + const sessionRegistry = owned.add(new AgentSessionRegistry(orchestratorDatabase)); + const stateManager = owned.add(new AgentHostStateManager(logService, { + hostBuildInfo: hostBuildInfoFromProduct(productService), + changesetStateRetention: { + canEvict: changeset => agentService?.canEvictChangeset(changeset) ?? false, + }, + })); + const configurationService = owned.add(new AgentConfigurationService( + stateManager, + logService, + options.rootConfigResource, + options.providerConfigurations ?? [], + )); + const storageService = owned.add(new AgentHostStorageService(options.storageResource, logService)); + const managedSettingsService = owned.add(new AgentHostManagedSettingsService()); + const core: IAgentServiceCore = { + disposables: owned, + authenticationService: new AgentHostAuthenticationService(logService), + orchestratorDatabase, + debugLogsCollector, + sessionRegistry, + stateManager, + configurationService, + storageService, + managedSettingsService, + hostLaunchKind: options.hostLaunchKind ?? AgentHostLaunchKind.Unknown, + copilotApiServiceOverride: options.copilotApiService, + }; + agentService = instantiationService.createInstance(AgentService, core); + const context = agentService.getCompositionContext(); + services.set(IAgentService, agentService); + services.set(IAgentConfigurationService, context.configurationService); + services.set(IAgentHostStateManager, context.stateManager); + services.set(IAgentHostStorageService, context.storageService); + services.set(IAgentHostManagedSettingsService, context.managedSettingsService); + + const gitHubEndpointService = owned.add(instantiationService.createInstance(AgentHostGitHubEndpointService)); + services.set(IAgentHostGitHubEndpointService, gitHubEndpointService); + const octoKitService = instantiationService.createInstance(AgentHostOctoKitService, fetchFn); + services.set(IAgentHostOctoKitService, octoKitService); + const gitHubService = owned.add(instantiationService.createInstance(GitHubService, { + endpoint: gitHubEndpointService, + tokenProvider: { + getToken: () => { + const resource = gitHubEndpointService.getRepoResource(); + return context.getAuthToken({ resource: resource.resource, scopes: resource.scopes_supported }); + }, + }, + fetch: fetchFn, + })); + services.set(IGitHubService, gitHubService); + const copilotApiService = context.copilotApiServiceOverride ?? instantiationService.createInstance(CopilotApiService, fetchFn); + services.set(ICopilotApiService, copilotApiService); + const customizationEnablementService = owned.add(instantiationService.createInstance(AgentHostCustomizationEnablementService)); + services.set(IAgentHostCustomizationEnablementService, customizationEnablementService); + const gitStateService = owned.add(instantiationService.createInstance(AgentHostGitStateService)); + services.set(IAgentHostGitStateService, gitStateService); + const agentMergeController = owned.add(instantiationService.createInstance(AgentMergeController, context.createAgentMergeControllerOptions())); + const checkpointService = owned.add(instantiationService.createInstance(AgentHostCheckpointService)); + services.set(IAgentHostCheckpointService, checkpointService); + const promptCache = instantiationService.createInstance(AgentHostPromptCache); + services.set(IAgentHostPromptCache, promptCache); + const sessionTitleSignal = owned.add(instantiationService.createInstance(AgentHostSessionTitleSignal)); + services.set(IAgentHostSessionTitleSignal, sessionTitleSignal); + const changesetSubscriptions = instantiationService.createInstance(AgentHostChangesetSubscriptionService); + services.set(IAgentHostChangesetSubscriptionService, changesetSubscriptions); + const changesetOperationService = owned.add(instantiationService.createInstance(AgentHostChangesetOperationService)); + services.set(IAgentHostChangesetOperationService, changesetOperationService); + const reviewService = owned.add(instantiationService.createInstance(AgentHostReviewService)); + services.set(IAgentHostReviewService, reviewService); + const changesets = owned.add(instantiationService.createInstance(AgentHostChangesetService)); + services.set(IAgentHostChangesetService, changesets); + const changesetCoordinator = owned.add(instantiationService.createInstance(AgentHostChangesetCoordinator)); + owned.add(context.stateManager.onDidChangeSessionActiveTurn(event => changesetCoordinator.onSessionTurnActiveChanged(event.session, event.active))); + owned.add(changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostCommitOperationContribution))); + owned.add(changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostPullRequestOperationContribution))); + owned.add(changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostMergeOperationContribution))); + owned.add(changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostSyncOperationContribution))); + owned.add(changesetOperationService.registerContribution(instantiationService.createInstance(AgentHostDiscardChangesOperationContribution))); + + const completions = owned.add(instantiationService.createInstance(AgentHostCompletions)); + services.set(IAgentHostCompletions, completions); + const workspaceFiles = owned.add(instantiationService.createInstance(AgentHostWorkspaceFiles)); + owned.add(completions.registerProvider(new AgentHostFileCompletionProvider(context.stateManager, workspaceFiles, logService))); + owned.add(completions.registerProvider(new AgentHostChatCompletionProvider(context.stateManager))); + owned.add(completions.registerProvider(new AgentHostRenameCompletionProvider( + session => (context.stateManager.getSessionState(session)?.turns.length ?? 0) > 0, + ))); + owned.add(completions.registerProvider(new CodexCompactCompletionProvider( + session => (context.stateManager.getSessionState(session)?.turns.length ?? 0) > 0, + ))); + + const terminalManager = owned.add(instantiationService.createInstance(AgentHostTerminalManager)); + services.set(IAgentHostTerminalManager, terminalManager); + const localTurns = new AgentHostLocalTurns(context.sessionDataService, logService); + const sideEffects = owned.add(instantiationService.createInstance( + AgentSideEffects, + context.stateManager, + customizationEnablementService, + context.createSideEffectsOptions({ localTurns, copilotApiService, octoKitService, gitStateService }), + )); + const sessionCoordination = owned.add(new SessionCoordinationService( + context.stateManager, + context.sessionDataService, + logService, + { + getSessionMetadata: context.getSessionMetadata, + restoreSession: context.restoreSession, + handleAction: (chat, action) => sideEffects.handleAction(chat, action), + }, + )); + const agentMergeTools = instantiationService.createInstance( + AgentMergeTools, + () => agentMergeController.isEnabled(), + session => agentMergeController.getTurnContext(session), + ); + const serverToolHost = new AgentServerToolHost( + context.stateManager, + buildServerToolGroups(context.createSessionServerToolAccessor(), agentMergeTools), + ); + + agentService.initialize({ + gitHubEndpointService, + customizationEnablementService, + gitStateService, + agentMergeController, + checkpointService, + promptCache, + sessionTitleSignal, + changesetOperationService, + reviewService, + changesets, + changesetCoordinator, + completions, + terminalManager, + localTurns, + sideEffects, + sessionCoordination, + serverToolHost, + }); + return agentService; + } catch (error) { + if (agentService) { + agentService.dispose(); + } else { + owned.dispose(); + } + throw error; + } +} diff --git a/src/vs/platform/agentHost/test/node/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts index 9eeb857f273b75..6eaf2838158bd1 100644 --- a/src/vs/platform/agentHost/test/node/agentService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentService.test.ts @@ -2838,26 +2838,24 @@ suite('AgentService (node dispatcher)', () => { } } - function createExternalSessionService(now: () => number, sessionDataService = createSessionDataService()): AgentService { + function createExternalSessionService(sessionDataService = createSessionDataService()): AgentService { return disposables.add(createTestAgentService( new NullLogService(), fileService, sessionDataService, { _serviceBrand: undefined } as IProductService, createNoopGitService(), - undefined, - undefined, - undefined, - undefined, - undefined, - [], - undefined, - undefined, - undefined, - now, )); } + function testWithExternalSessionClock(name: string, fn: () => Promise<void>): void { + test(name, () => runWithFakedTimers({ + useFakeTimers: true, + startTime: Date.UTC(2026, 0, 1), + maxTaskCount: 10_000, + }, fn)); + } + function setExternalSessionsMode(service: AgentService, mode: AgentHostExternalSessionsMode, clientSeq: number): void { service.dispatchAction(ROOT_STATE_URI, { type: ActionType.RootConfigChanged, @@ -2919,10 +2917,10 @@ suite('AgentService (node dispatcher)', () => { assert.strictEqual(await db.getMetadata(AH_META_IS_READ_DB_KEY), ''); }); - test('filters external sessions in every mode with inclusive time boundaries', async () => { + testWithExternalSessionClock('filters external sessions in every mode with inclusive time boundaries', async () => { const day = 24 * 60 * 60 * 1000; const now = Date.now(); - const svc = createExternalSessionService(() => now); + const svc = createExternalSessionService(); const agent = disposables.add(new TimedExternalAgent('copilot')); agent.addSession('recent', now); agent.addSession('at-24-hours', now - day); @@ -2958,10 +2956,10 @@ suite('AgentService (node dispatcher)', () => { }); }); - test('a mode change reconciles with a single catalog pass', async () => { + testWithExternalSessionClock('a mode change reconciles with a single catalog pass', async () => { const day = 24 * 60 * 60 * 1000; const now = Date.now(); - const svc = createExternalSessionService(() => now); + const svc = createExternalSessionService(); const agent = disposables.add(new TimedExternalAgent('copilot')); agent.addSession('recent', now); agent.addSession('yesterday', now - day); @@ -2998,9 +2996,9 @@ suite('AgentService (node dispatcher)', () => { }); }); - test('recent replaces the oldest visible external session when a newer session is discovered', async () => { + testWithExternalSessionClock('recent replaces the oldest visible external session when a newer session is discovered', async () => { const now = Date.now(); - const svc = createExternalSessionService(() => now); + const svc = createExternalSessionService(); setExternalSessionsMode(svc, AgentHostExternalSessionsMode.Recent, 1); await waitForSessionListReconciliation(svc); const agent = disposables.add(new TimedExternalAgent('copilot')); @@ -3037,9 +3035,9 @@ suite('AgentService (node dispatcher)', () => { }); }); - test('external discovery reconciles against a mode change that completes while registration is in flight', async () => { + testWithExternalSessionClock('external discovery reconciles against a mode change that completes while registration is in flight', async () => { const now = Date.now(); - const svc = createExternalSessionService(() => now); + const svc = createExternalSessionService(); setExternalSessionsMode(svc, AgentHostExternalSessionsMode.All, 1); await waitForSessionListReconciliation(svc); const agent = disposables.add(new TimedExternalAgent('copilot')); @@ -3089,9 +3087,9 @@ suite('AgentService (node dispatcher)', () => { }); }); - test('recent reconciles clients when a hidden external session becomes more recent', async () => { + testWithExternalSessionClock('recent reconciles clients when a hidden external session becomes more recent', async () => { const now = Date.now(); - const svc = createExternalSessionService(() => now); + const svc = createExternalSessionService(); setExternalSessionsMode(svc, AgentHostExternalSessionsMode.Recent, 1); await waitForSessionListReconciliation(svc); const agent = disposables.add(new TimedExternalAgent('copilot')); @@ -3130,9 +3128,9 @@ suite('AgentService (node dispatcher)', () => { }); }); - test('configuration changes add and remove non-live external sessions immediately', async () => { + testWithExternalSessionClock('configuration changes add and remove non-live external sessions immediately', async () => { const now = Date.now(); - const svc = createExternalSessionService(() => now); + const svc = createExternalSessionService(); const agent = disposables.add(new TimedExternalAgent('copilot')); const session = agent.addSession('config-visible', now); const notifications: string[] = []; @@ -3160,9 +3158,9 @@ suite('AgentService (node dispatcher)', () => { assert.deepStrictEqual(notifications, [`add:${session.toString()}`, `remove:${session.toString()}`]); }); - test('unpublishes and republishes a restored external session as the configured mode changes', async () => { + testWithExternalSessionClock('unpublishes and republishes a restored external session as the configured mode changes', async () => { const now = Date.now(); - const svc = createExternalSessionService(() => now); + const svc = createExternalSessionService(); setExternalSessionsMode(svc, AgentHostExternalSessionsMode.All, 1); await waitForSessionListReconciliation(svc); const agent = disposables.add(new TimedExternalAgent('copilot')); @@ -3197,7 +3195,7 @@ suite('AgentService (node dispatcher)', () => { }); }); - test('publishes an external session restored while hidden when the configured mode includes it', async () => { + testWithExternalSessionClock('publishes an external session restored while hidden when the configured mode includes it', async () => { class ExternalOnlyAgent extends TimedExternalAgent { override async listSessions(): Promise<IAgentSessionMetadata[]> { return []; @@ -3205,7 +3203,7 @@ suite('AgentService (node dispatcher)', () => { } const now = Date.now(); - const svc = createExternalSessionService(() => now); + const svc = createExternalSessionService(); const agent = disposables.add(new ExternalOnlyAgent('copilot')); const session = agent.addSession('hidden-then-restored', now); const notifications: string[] = []; diff --git a/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts b/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts index c69308825350ee..633deb82c32a41 100644 --- a/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts +++ b/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IFileService } from '../../../files/common/files.js'; import { InstantiationService } from '../../../instantiation/common/instantiationService.js'; @@ -21,14 +21,9 @@ import { IAgentHostDatabase } from '../../node/agentHostDatabase.js'; import { AgentHostFileMonitorService, IAgentHostFileMonitorService } from '../../node/agentHostFileMonitorService.js'; import { IAgentHostProxyResolver } from '../../node/agentHostProxyResolver.js'; import { AgentService } from '../../node/agentService.js'; +import { createAgentService } from '../../node/agentServiceComposition.js'; import { ICopilotApiService } from '../../node/shared/copilotApiService.js'; -class TestAgentService extends AgentService { - registerTestDependency(disposable: IDisposable): void { - this._register(disposable); - } -} - export function createTestAgentService( logService: ILogService, fileService: IFileService, @@ -44,7 +39,6 @@ export function createTestAgentService( hostLaunchKind = AgentHostLaunchKind.Unknown, storageResource?: URI, orchestratorDatabase?: IAgentHostDatabase, - now: () => number = Date.now, ): AgentService { const effectiveFileMonitorService = fileMonitorService ?? new AgentHostFileMonitorService(fileService, logService); const proxyResolver: IAgentHostProxyResolver = { @@ -75,12 +69,15 @@ export function createTestAgentService( hostLaunchKind, storageResource, orchestratorDatabase, - now, }; - const service = instantiationService.createInstance(TestAgentService, options, services); - if (!fileMonitorService) { - service.registerTestDependency(effectiveFileMonitorService); - } - service.registerTestDependency(instantiationService); + const service = createAgentService( + options, + services, + instantiationService, + fetchFn, + logService, + productService, + fileMonitorService ? [instantiationService] : [effectiveFileMonitorService, instantiationService], + ); return service; } From 154dd71ae3d328351928dca38c5b3ce31c620c46 Mon Sep 17 00:00:00 2001 From: Rob Lourens <roblourens@gmail.com> Date: Wed, 19 Aug 2026 20:56:21 -0700 Subject: [PATCH 6/6] Use one complete Agent Host runtime graph Remove optional provider-infrastructure setup, make BYOK policy explicit, and defer Claude SDK environment mutation until first use. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/node/agentHostBootstrap.ts | 105 ++++++------------ .../platform/agentHost/node/agentHostMain.ts | 6 +- .../agentHost/node/agentHostServerMain.ts | 9 +- .../node/claude/claudeAgentSdkService.ts | 15 ++- .../test/node/agentHostBootstrap.test.ts | 38 ++++++- .../agentHost/test/node/claudeAgent.test.ts | 12 ++ 6 files changed, 95 insertions(+), 90 deletions(-) diff --git a/src/vs/platform/agentHost/node/agentHostBootstrap.ts b/src/vs/platform/agentHost/node/agentHostBootstrap.ts index 7020896f7c4c4d..a90826809bd493 100644 --- a/src/vs/platform/agentHost/node/agentHostBootstrap.ts +++ b/src/vs/platform/agentHost/node/agentHostBootstrap.ts @@ -47,8 +47,8 @@ import { AgentSdkDownloader, IAgentSdkDownloader, type IAgentSdkDownloadProgress import { IClaudeAgentSdkService, ClaudeAgentSdkService } from './claude/claudeAgentSdkService.js'; import { ClaudeProxyService, IClaudeProxyService } from './claude/claudeProxyService.js'; import { CodexProxyService, ICodexProxyService } from './codex/codexProxyService.js'; -import { IByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; -import { ByokLmProxyService, IByokLmProxyService } from './copilot/byokLmProxyService.js'; +import { IByokLmBridgeRegistry, NullByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; +import { ByokLmProxyService, IByokLmProxyService, NullByokLmProxyService } from './copilot/byokLmProxyService.js'; import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js'; import { SessionDataService } from './sessionDataService.js'; import { IAgentCustomizationSettingsRegistration } from '../common/agentCustomizationSettings.js'; @@ -59,11 +59,6 @@ export interface IAgentHostNetworkServices { readonly requestService: IRequestService; } -export interface IAgentHostProviderInfrastructureOptions { - readonly byokBridgeRegistry: IByokLmBridgeRegistry; - readonly byokLmProxyService?: IByokLmProxyService; -} - export interface ICreateAgentHostRuntimeOptions { readonly environmentService: INativeEnvironmentService; readonly productService: IProductService; @@ -74,7 +69,11 @@ export interface ICreateAgentHostRuntimeOptions { readonly transientProxyConfiguration: boolean; readonly hostLaunchKind: AgentHostLaunchKind; readonly providerConfigurations: readonly IAgentCustomizationSettingsRegistration[]; - readonly providerInfrastructure?: IAgentHostProviderInfrastructureOptions; + /** + * The utility-process host has a renderer bridge; standalone hosts use the + * unavailable variant but still register the same complete service graph. + */ + readonly byok: { readonly kind: 'renderer'; readonly bridgeRegistry: IByokLmBridgeRegistry } | { readonly kind: 'unavailable' }; } export interface IAgentHostRuntime { @@ -84,8 +83,8 @@ export interface IAgentHostRuntime { readonly sessionDataService: ISessionDataService; readonly proxyResolver: IAgentHostProxyResolver; readonly telemetryService: IAgentHostTelemetryService; - readonly agentSdkDownloader: AgentSdkDownloader | undefined; - readonly sdkDownloadProgress: Event<IAgentSdkDownloadProgress> | undefined; + readonly agentSdkDownloader: AgentSdkDownloader; + readonly sdkDownloadProgress: Event<IAgentSdkDownloadProgress>; } /** @@ -163,19 +162,31 @@ export async function createAgentHostRuntime(options: ICreateAgentHostRuntimeOpt const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); services.set(INetworkDiagnosticsService, networkDiagnosticsService); agentService.setNetworkDiagnosticsService(networkDiagnosticsService); - const providerInfrastructure = options.providerInfrastructure - ? registerProviderInfrastructure({ - services, - instantiationService, - agentService, - environmentService, - fileService, - logService, - disposables, - fetchFn, - ...options.providerInfrastructure, - }) - : undefined; + services.set(IAgentPluginManager, new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService)); + services.set(IDiffComputeService, disposables.add(instantiationService.createInstance(NodeWorkerDiffComputeService))); + const editAttributionService = disposables.add(instantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); + services.set(IAgentEditAttributionService, editAttributionService); + agentService.setEditAttributionService(editAttributionService); + services.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); + services.set(IEditArcReporterService, disposables.add(instantiationService.createInstance(EditArcReporterService, undefined))); + + const worktreeIsolation = disposables.add(instantiationService.createInstance(WorktreeIsolation, undefined)); + services.set(IAgentHostWorktreeIsolation, worktreeIsolation); + agentService.setWorktreeIsolation(worktreeIsolation); + + const agentSdkDownloader = disposables.add(instantiationService.createInstance(AgentSdkDownloader)); + services.set(IAgentSdkDownloader, agentSdkDownloader); + services.set(IClaudeProxyService, disposables.add(instantiationService.createInstance(ClaudeProxyService))); + services.set(IClaudeAgentSdkService, instantiationService.createInstance(ClaudeAgentSdkService)); + services.set(ICodexProxyService, disposables.add(instantiationService.createInstance(CodexProxyService))); + services.set(IAgentHostOTelService, disposables.add(instantiationService.createInstance(AgentHostOTelService, fetchFn))); + const byokBridgeRegistry = options.byok.kind === 'renderer' ? options.byok.bridgeRegistry : new NullByokLmBridgeRegistry(); + services.set(IByokLmBridgeRegistry, byokBridgeRegistry); + const byokLmProxyService: IByokLmProxyService = options.byok.kind === 'renderer' + ? disposables.add(instantiationService.createInstance(ByokLmProxyService)) + : new NullByokLmProxyService(); + services.set(IByokLmProxyService, byokLmProxyService); + return { instantiationService, agentService, @@ -183,8 +194,8 @@ export async function createAgentHostRuntime(options: ICreateAgentHostRuntimeOpt sessionDataService, proxyResolver, telemetryService, - agentSdkDownloader: providerInfrastructure?.agentSdkDownloader, - sdkDownloadProgress: providerInfrastructure?.sdkDownloadProgress, + agentSdkDownloader, + sdkDownloadProgress: agentSdkDownloader.onDidDownloadProgress, }; } catch (error) { agentService?.dispose(); @@ -192,47 +203,3 @@ export async function createAgentHostRuntime(options: ICreateAgentHostRuntimeOpt throw error; } } - -interface IRegisterProviderInfrastructureOptions extends IAgentHostProviderInfrastructureOptions { - readonly services: ServiceCollection; - readonly instantiationService: IInstantiationService; - readonly agentService: AgentService; - readonly environmentService: INativeEnvironmentService; - readonly fileService: IFileService; - readonly logService: ILogService; - readonly disposables: DisposableStore; - readonly fetchFn: typeof globalThis.fetch; -} - -interface IProviderInfrastructure { - readonly agentSdkDownloader: AgentSdkDownloader; - readonly sdkDownloadProgress: Event<IAgentSdkDownloadProgress>; -} - -function registerProviderInfrastructure(options: IRegisterProviderInfrastructureOptions): IProviderInfrastructure { - const { services, instantiationService, agentService, environmentService, fileService, logService, disposables, fetchFn } = options; - services.set(IAgentPluginManager, new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService)); - services.set(IDiffComputeService, disposables.add(instantiationService.createInstance(NodeWorkerDiffComputeService))); - const editAttributionService = disposables.add(instantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); - services.set(IAgentEditAttributionService, editAttributionService); - agentService.setEditAttributionService(editAttributionService); - services.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); - services.set(IEditArcReporterService, disposables.add(instantiationService.createInstance(EditArcReporterService, undefined))); - - const worktreeIsolation = disposables.add(instantiationService.createInstance(WorktreeIsolation, undefined)); - services.set(IAgentHostWorktreeIsolation, worktreeIsolation); - agentService.setWorktreeIsolation(worktreeIsolation); - - const agentSdkDownloader = disposables.add(instantiationService.createInstance(AgentSdkDownloader)); - services.set(IAgentSdkDownloader, agentSdkDownloader); - services.set(IClaudeProxyService, disposables.add(instantiationService.createInstance(ClaudeProxyService))); - services.set(IClaudeAgentSdkService, instantiationService.createInstance(ClaudeAgentSdkService)); - services.set(ICodexProxyService, disposables.add(instantiationService.createInstance(CodexProxyService))); - services.set(IAgentHostOTelService, disposables.add(instantiationService.createInstance(AgentHostOTelService, fetchFn))); - - services.set(IByokLmBridgeRegistry, options.byokBridgeRegistry); - const byokLmProxyService = options.byokLmProxyService ?? disposables.add(instantiationService.createInstance(ByokLmProxyService)); - services.set(IByokLmProxyService, byokLmProxyService); - - return { agentSdkDownloader, sdkDownloadProgress: agentSdkDownloader.onDidDownloadProgress }; -} diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index 7a904080a482a3..af19c42c4dcfd5 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -122,16 +122,14 @@ async function startAgentHost(): Promise<void> { transientProxyConfiguration: true, hostLaunchKind, providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], - providerInfrastructure: { - byokBridgeRegistry: byokLmBridgeRegistry, - }, + byok: { kind: 'renderer', bridgeRegistry: byokLmBridgeRegistry }, }); agentService = runtime.agentService; instantiationService = runtime.instantiationService; fileService = runtime.fileService; proxyResolver = runtime.proxyResolver; errorTelemetry.value = new ErrorTelemetry(runtime.telemetryService); - const agentSdkDownloader = runtime.agentSdkDownloader!; + const agentSdkDownloader = runtime.agentSdkDownloader; sdkDownloadProgress = runtime.sdkDownloadProgress; agentService.registerProvider(instantiationService.createInstance(CopilotAgent)); // Claude and Codex providers are gated on two things: diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index fdd50e545d15c7..3162e25175def1 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -32,8 +32,6 @@ import { IProductService } from '../../product/common/productService.js'; import { createAgentHostRuntime } from './agentHostBootstrap.js'; import { BANG_COMMAND_PREFIX } from './agentHostBangCommand.js'; import { CopilotAgent } from './copilot/copilotAgent.js'; -import { NullByokLmBridgeRegistry } from './byokLmBridgeRegistry.js'; -import { NullByokLmProxyService } from './copilot/byokLmProxyService.js'; import { ClaudeAgent } from './claude/claudeAgent.js'; import { ClaudeSdkPackage } from './claude/claudeAgentSdkService.js'; import { CodexAgent, CodexSdkPackage } from './codex/codexAgent.js'; @@ -196,10 +194,7 @@ async function main(): Promise<void> { transientProxyConfiguration: false, hostLaunchKind: AgentHostLaunchKind.VSCodeCLI, providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], - providerInfrastructure: options.quiet ? undefined : { - byokBridgeRegistry: new NullByokLmBridgeRegistry(), - byokLmProxyService: new NullByokLmProxyService(), - } + byok: { kind: 'unavailable' }, }); const { agentService, instantiationService, fileService, sessionDataService } = runtime; disposables.add(agentService); @@ -208,7 +203,7 @@ async function main(): Promise<void> { // Register agents let sdkDownloadProgress: Event<IAgentSdkDownloadProgress> | undefined; if (!options.quiet) { - const agentSdkDownloader = runtime.agentSdkDownloader!; + const agentSdkDownloader = runtime.agentSdkDownloader; sdkDownloadProgress = runtime.sdkDownloadProgress; const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent)); agentService.registerProvider(copilotAgent); diff --git a/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts b/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts index 46a2df473581e7..a10926fb41da0b 100644 --- a/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts +++ b/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts @@ -153,14 +153,7 @@ export class ClaudeAgentSdkService implements IClaudeAgentSdkService { constructor( @ILogService private readonly _logService: ILogService, @IAgentSdkDownloader private readonly _downloader: IAgentSdkDownloader, - ) { - // Set before any SDK call so full transcripts are always read back. - // An explicit value from the environment wins so the optimization can - // still be re-enabled from outside. - if (process.env[ClaudeDisablePrecompactSkipEnvVar] === undefined) { - process.env[ClaudeDisablePrecompactSkipEnvVar] = '1'; - } - } + ) { } async listSessions(): Promise<readonly SDKSessionInfo[]> { const sdk = await this._getSdk(); @@ -245,6 +238,12 @@ export class ClaudeAgentSdkService implements IClaudeAgentSdkService { } private async _getSdk(): Promise<IClaudeSdkBindings> { + // Set before the first SDK call so full transcripts are always read back. + // An explicit value from the environment wins so the optimization can + // still be re-enabled from outside. + if (process.env[ClaudeDisablePrecompactSkipEnvVar] === undefined) { + process.env[ClaudeDisablePrecompactSkipEnvVar] = '1'; + } if (this._sdkModule) { return this._sdkModule; } diff --git a/src/vs/platform/agentHost/test/node/agentHostBootstrap.test.ts b/src/vs/platform/agentHost/test/node/agentHostBootstrap.test.ts index a44d3b0b6a7201..aea89b8ad49262 100644 --- a/src/vs/platform/agentHost/test/node/agentHostBootstrap.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostBootstrap.test.ts @@ -4,13 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { mkdirSync, mkdtempSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from '../../../../base/common/path.js'; +import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { parseArgs, OPTIONS } from '../../../environment/node/argv.js'; +import { NativeEnvironmentService } from '../../../environment/node/environmentService.js'; import { NullLogService } from '../../../log/common/log.js'; +import product from '../../../product/common/product.js'; import { ServiceCollection } from '../../../instantiation/common/serviceCollection.js'; import { IRequestService } from '../../../request/common/request.js'; -import { registerAgentHostNetworkServices } from '../../node/agentHostBootstrap.js'; +import { createAgentHostRuntime, registerAgentHostNetworkServices } from '../../node/agentHostBootstrap.js'; import { IAgentHostProxyResolver } from '../../node/agentHostProxyResolver.js'; +import { NullByokLmBridgeRegistry } from '../../node/byokLmBridgeRegistry.js'; +import { AgentHostLaunchKind } from '../../common/agentHostTelemetry.js'; suite('agentHostBootstrap', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -28,4 +36,30 @@ suite('agentHostBootstrap', () => { requestService: true, }); }); + + test('constructs the renderer BYOK runtime with strict dependency injection', async () => { + const testDisposables = disposables.add(new DisposableStore()); + const userDataPath = mkdtempSync(join(tmpdir(), 'agent-host-bootstrap-')); + mkdirSync(join(userDataPath, 'User', 'globalStorage'), { recursive: true }); + testDisposables.add(toDisposable(() => rmSync(userDataPath, { recursive: true, force: true }))); + const productService = { _serviceBrand: undefined, ...product }; + const environmentService = new NativeEnvironmentService(parseArgs(['--user-data-dir', userDataPath, '--force-disable-user-env'], OPTIONS), productService); + + const runtime = await createAgentHostRuntime({ + environmentService, + productService, + logService: new NullLogService(), + loggerService: undefined, + disposables: testDisposables, + disableTelemetry: true, + transientProxyConfiguration: true, + hostLaunchKind: AgentHostLaunchKind.Unknown, + providerConfigurations: [], + byok: { kind: 'renderer', bridgeRegistry: new NullByokLmBridgeRegistry() }, + }); + testDisposables.add(runtime.agentService); + testDisposables.add(runtime.instantiationService); + + assert.ok(runtime.agentSdkDownloader); + }); }); diff --git a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts index c29ebc00e7c085..0a7b59f5be949c 100644 --- a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts @@ -5309,11 +5309,23 @@ suite('ClaudeAgent', () => { [ILogService, new RecordingLogService()], [IAgentSdkDownloader, stubAgentSdkDownloader()], ); + const precompactSkipEnvVar = 'CLAUDE_CODE_DISABLE_PRECOMPACT_SKIP'; + const previousPrecompactSkip = process.env[precompactSkipEnvVar]; + delete process.env[precompactSkipEnvVar]; + disposables.add(toDisposable(() => { + if (previousPrecompactSkip === undefined) { + delete process.env[precompactSkipEnvVar]; + } else { + process.env[precompactSkipEnvVar] = previousPrecompactSkip; + } + })); const inst = disposables.add(new InstantiationService(services)); const svc = inst.createInstance(TestableClaudeAgentSdkService); + assert.strictEqual(process.env[precompactSkipEnvVar], undefined, 'constructing the SDK service must not mutate the environment'); // First two calls fault → exactly one log entry; both retry the import. await assert.rejects(() => svc.listSessions(), /simulated SDK load failure/); + assert.strictEqual(process.env[precompactSkipEnvVar], '1', 'the environment must be configured before the first SDK call'); await assert.rejects(() => svc.listSessions(), /simulated SDK load failure/); const failuresLogged = errorCalls.length; const importInvocationsAfterFailures = importInvocations;