diff --git a/src/vs/platform/agentHost/node/agentHostBootstrap.ts b/src/vs/platform/agentHost/node/agentHostBootstrap.ts index 2e76e8adcc16c0..a90826809bd493 100644 --- a/src/vs/platform/agentHost/node/agentHostBootstrap.ts +++ b/src/vs/platform/agentHost/node/agentHostBootstrap.ts @@ -4,17 +4,89 @@ *--------------------------------------------------------------------------------------------*/ 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 { 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'; -import { ILogService } from '../../log/common/log.js'; +import { ILoggerService, ILogService } from '../../log/common/log.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 { createAgentService } from './agentServiceComposition.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, 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'; +import { AgentHostLaunchKind } from '../common/agentHostTelemetry.js'; export interface IAgentHostNetworkServices { readonly proxyResolver: IAgentHostProxyResolver; readonly requestService: IRequestService; } +export interface ICreateAgentHostRuntimeOptions { + readonly environmentService: INativeEnvironmentService; + readonly productService: IProductService; + readonly logService: ILogService; + readonly loggerService: ILoggerService | undefined; + readonly disposables: DisposableStore; + readonly disableTelemetry?: boolean; + readonly transientProxyConfiguration: boolean; + readonly hostLaunchKind: AgentHostLaunchKind; + readonly providerConfigurations: readonly IAgentCustomizationSettingsRegistration[]; + /** + * 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 { + readonly instantiationService: IInstantiationService; + readonly agentService: AgentService; + readonly fileService: IFileService; + readonly sessionDataService: ISessionDataService; + readonly proxyResolver: IAgentHostProxyResolver; + readonly telemetryService: IAgentHostTelemetryService; + readonly agentSdkDownloader: AgentSdkDownloader; + readonly sdkDownloadProgress: Event; +} + /** * Register `IAgentHostProxyResolver` and `IRequestService` into the agent host's * DI container — the services that `IAgentSdkDownloader` (and proxy-aware @@ -28,13 +100,106 @@ export interface IAgentHostNetworkServices { * configuration service. */ export function registerAgentHostNetworkServices( - diServices: ServiceCollection, + services: ServiceCollection, logService: ILogService, disposables: DisposableStore, ): IAgentHostNetworkServices { const proxyResolver = disposables.add(new AgentHostProxyResolver(logService)); - diServices.set(IAgentHostProxyResolver, proxyResolver); + services.set(IAgentHostProxyResolver, proxyResolver); const requestService = disposables.add(new AgentHostRequestService(logService, proxyResolver)); - diServices.set(IRequestService, requestService); + services.set(IRequestService, requestService); return { proxyResolver, requestService }; } + +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], + [IFileService, fileService], + [ISessionDataService, sessionDataService], + [IProductService, productService], + ); + const networkServices = registerAgentHostNetworkServices(services, 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); + let agentService: AgentService | undefined; + 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 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, + }, + }; + agentService = createAgentService(agentServiceOptions, services, instantiationService, fetchFn, logService, productService); + proxyResolver.bindConfigurationService(agentService.configurationService, options.transientProxyConfiguration); + const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); + services.set(INetworkDiagnosticsService, networkDiagnosticsService); + agentService.setNetworkDiagnosticsService(networkDiagnosticsService); + 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, + fileService, + sessionDataService, + proxyResolver, + telemetryService, + agentSdkDownloader, + sdkDownloadProgress: agentSdkDownloader.onDidDownloadProgress, + }; + } catch (error) { + agentService?.dispose(); + instantiationService.dispose(); + throw error; + } +} diff --git a/src/vs/platform/agentHost/node/agentHostMain.ts b/src/vs/platform/agentHost/node/agentHostMain.ts index ee8737a8590406..af19c42c4dcfd5 100644 --- a/src/vs/platform/agentHost/node/agentHostMain.ts +++ b/src/vs/platform/agentHost/node/agentHostMain.ts @@ -10,50 +10,29 @@ 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'; 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 { IAgentHostAuthenticationService } from './agentHostAuthenticationService.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 { 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'; +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'; @@ -65,40 +44,15 @@ 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 { 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 { createAgentHostRuntime } 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 { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.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'; @@ -146,115 +100,37 @@ 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 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; 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 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 = registerAgentHostNetworkServices(diServices, 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); - instantiationService = new InstantiationService(diServices); - 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); - // 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); - sdkDownloadProgress = agentSdkDownloader.onDidDownloadProgress; - const claudeAgentSdkService = instantiationService.createInstance(ClaudeAgentSdkService); - diServices.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); - agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, rootConfigResource, telemetryService, fileMonitorService, undefined, fetchFn, [createCodexProviderConfiguration(environmentService.userHome)], hostLaunchKind, storageResource, undefined, undefined, { - logsHome: environmentService.logsHome, - tmpDir: environmentService.tmpDir, + const runtime = await createAgentHostRuntime({ + environmentService, + productService, + logService, + loggerService, + disposables, + transientProxyConfiguration: true, + hostLaunchKind, + providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], + byok: { kind: 'renderer', bridgeRegistry: byokLmBridgeRegistry }, }); - diServices.set(IAgentService, agentService); - diServices.set(IAgentHostAuthenticationService, agentService.authenticationService); - 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(IAgentConfigurationService, agentService.configurationService); - proxyResolver.bindConfigurationService(agentService.configurationService, true); - const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); - diServices.set(INetworkDiagnosticsService, networkDiagnosticsService); - agentService.setNetworkDiagnosticsService(networkDiagnosticsService); - const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService); - diServices.set(IAgentPluginManager, pluginManager); - const diffComputeService = disposables.add(new NodeWorkerDiffComputeService(logService)); - diServices.set(IDiffComputeService, diffComputeService); - const editAttributionService = disposables.add(instantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); - diServices.set(IAgentEditAttributionService, editAttributionService); - agentService.setEditAttributionService(editAttributionService); - diServices.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); - - diServices.set(IAgentHostTerminalManager, agentService.terminalManager); - diServices.set(IAgentHostStorageService, agentService.storageService); - diServices.set(IAgentHostCustomizationEnablementService, agentService.customizationEnablementService); - diServices.set(IAgentHostManagedSettingsService, agentService.managedSettingsService); - 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); - // 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); - agentService.setWorktreeIsolation(worktreeIsolation); - const claudeProxyService = disposables.add(instantiationService.createInstance(ClaudeProxyService)); - diServices.set(IClaudeProxyService, claudeProxyService); - const codexProxyService = disposables.add(instantiationService.createInstance(CodexProxyService)); - diServices.set(ICodexProxyService, codexProxyService); + 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`, @@ -293,6 +169,7 @@ async function startAgentHost(): Promise { disposables.add(agentConfigurationService.onDidRootConfigChange(registerCodexIfEnabled)); } } catch (err) { + instantiationService?.dispose(); logService.error('Failed to create AgentService', err); throw err; } @@ -588,6 +465,7 @@ async function startAgentHost(): Promise { agentService.dispose(); logService.dispose(); disposables.dispose(); + instantiationService.dispose(); }); } diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index 9d360559a41cc4..3162e25175def1 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -18,12 +18,10 @@ 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'; 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,70 +29,23 @@ 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 { createAgentHostRuntime } 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 { 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'; +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 { AgentService } from './agentService.js'; -import { IAgentHostAuthenticationService } from './agentHostAuthenticationService.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 { 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 { IAgentHostCheckpointService } from '../common/agentHostCheckpointService.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'; @@ -216,128 +167,44 @@ 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'); } 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'); - - // 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 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 = registerAgentHostNetworkServices(diServices, 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 instantiationService = new InstantiationService(diServices); - 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); - - // 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, undefined, undefined, { - logsHome: environmentService.logsHome, - tmpDir: environmentService.tmpDir, - }); - disposables.add(agentService); - diServices.set(IAgentService, agentService); - diServices.set(IAgentHostAuthenticationService, agentService.authenticationService); - 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); - diServices.set(IAgentConfigurationService, agentService.configurationService); - proxyResolver.bindConfigurationService(agentService.configurationService, false); - const networkDiagnosticsService = instantiationService.createInstance(NetworkDiagnosticsService); - diServices.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))); - const editAttributionService = disposables.add(instantiationService.createInstance(AgentEditAttributionService, undefined, undefined)); - diServices.set(IAgentEditAttributionService, editAttributionService); - agentService.setEditAttributionService(editAttributionService); - diServices.set(IEditSurvivalReporterFactory, instantiationService.createInstance(EditSurvivalReporterFactory)); - diServices.set(IAgentHostTerminalManager, agentService.terminalManager); - 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); - // 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); - 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) { process.env[AgentHostClaudeSdkRootEnvVar] = options.claudeSdkRoot; } if (options.codexSdkRoot) { process.env[AgentHostCodexAgentSdkRootEnvVar] = options.codexSdkRoot; } - // Register the agent SDK downloader BEFORE any service that injects it. - const agentSdkDownloader = disposables.add(instantiationService.createInstance(AgentSdkDownloader)); - diServices.set(IAgentSdkDownloader, agentSdkDownloader); - sdkDownloadProgress = agentSdkDownloader.onDidDownloadProgress; - const claudeProxyService = disposables.add(instantiationService.createInstance(ClaudeProxyService)); - diServices.set(IClaudeProxyService, claudeProxyService); - const claudeAgentSdkService = instantiationService.createInstance(ClaudeAgentSdkService); - diServices.set(IClaudeAgentSdkService, claudeAgentSdkService); - const codexProxyService = disposables.add(instantiationService.createInstance(CodexProxyService)); - diServices.set(ICodexProxyService, codexProxyService); - const agentHostOTelService = disposables.add(instantiationService.createInstance(AgentHostOTelService, fetchFn)); - diServices.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()); + } + + const runtime = await createAgentHostRuntime({ + environmentService, + productService, + logService, + loggerService, + disposables, + disableTelemetry: options.quiet, + transientProxyConfiguration: false, + hostLaunchKind: AgentHostLaunchKind.VSCodeCLI, + providerConfigurations: [createCodexProviderConfiguration(environmentService.userHome)], + byok: { kind: 'unavailable' }, + }); + const { agentService, instantiationService, fileService, sessionDataService } = runtime; + disposables.add(agentService); + errorTelemetry.value = new ErrorTelemetry(runtime.telemetryService); + + // Register agents + let sdkDownloadProgress: Event | undefined; + if (!options.quiet) { + const agentSdkDownloader = runtime.agentSdkDownloader; + sdkDownloadProgress = runtime.sdkDownloadProgress; const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent)); agentService.registerProvider(copilotAgent); log('CopilotAgent registered'); @@ -506,6 +373,7 @@ async function main(): Promise { logService.warn('[AgentHostServer] Timed out waiting for persistence writes to flush; exiting anyway.'); }); disposables.dispose(); + instantiationService.dispose(); loggerService?.dispose(); process.exit(0); } diff --git a/src/vs/platform/agentHost/node/agentMergeController.ts b/src/vs/platform/agentHost/node/agentMergeController.ts index 7563e195929d97..9143038d687ded 100644 --- a/src/vs/platform/agentHost/node/agentMergeController.ts +++ b/src/vs/platform/agentHost/node/agentMergeController.ts @@ -32,7 +32,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 984c6724e05a64..951991acec1c27 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 { InstantiationService } from '../../instantiation/common/instantiationService.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, type 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,69 +49,47 @@ 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, persistSessionMetadata, persistSessionMetadataValues, SESSION_ARTIFACTS_KEY, SESSION_CUSTOM_TITLE_KEY, SESSION_CUSTOM_TITLE_SOURCE_KEY } from './shared/persistSessionMetadata.js'; import { type IArtifactServerToolAccessor } from './shared/artifactServerTools.js'; import { parseSessionArtifacts, stringifySessionArtifacts, withSessionArtifacts } from '../common/sessionArtifacts.js'; 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'; -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, readAgentMergeSessionState } 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, type IAgentHostAuthenticationService } from './agentHostAuthenticationService.js'; import { updateAgentHostTelemetryLevelFromConfig } from './agentHostTelemetryService.js'; import { AgentHostActiveAgentTitleGenerationConfigKey, AgentHostArtifactToolsConfigKey, AgentHostEditTelemetryEnabledConfigKey, AgentHostExternalSessionsMode, AgentHostMigrateLegacyCopilotCliEnabledConfigKey, AgentHostShowExternalSessionsConfigKey, platformRootSchema } from '../common/agentHostSchema.js'; 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 { 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 @@ -323,6 +298,76 @@ 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 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; + readonly createArtifactServerToolAccessor: () => IArtifactServerToolAccessor; +} + +/** 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 @@ -347,8 +392,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 @@ -435,33 +480,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 @@ -472,13 +516,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 _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 _hostLaunchKind: AgentHostLaunchKind; + private readonly _copilotApiServiceOverride: ICopilotApiService | undefined; /** * Authoritative server-side per-resource subscription refcount, keyed by @@ -561,41 +608,23 @@ 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, - debugLogsEnvironment?: IAgentHostDebugLogsEnvironment, + core: IAgentServiceCore, + @ILogService private readonly _logService: ILogService, + @IFileService private readonly _fileService: IFileService, + @ISessionDataService private readonly _sessionDataService: ISessionDataService, + @IAgentHostGitService private readonly _gitService: IAgentHostGitService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { super(); + this._register(core.disposables); + this._hostLaunchKind = core.hostLaunchKind; + this._copilotApiServiceOverride = core.copilotApiServiceOverride; this._logService.info('AgentService initialized'); - this._authService = this._register(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._debugLogsCollector = debugLogsEnvironment ? this._register(new AgentHostDebugLogsCollector(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))); @@ -616,15 +645,98 @@ 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, 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(), + createArtifactServerToolAccessor: () => this._createArtifactServerToolAccessor(), + }; + } + + /** 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; + this._register(this._stateManager.onDidChangeSessionConfig(({ session, previous, current }) => this._syncAgentMergeIndex(URI.parse(session), previous, current))); + this._register(this._agentMergeController.onDidReleaseHold(session => { + const resource = URI.parse(session); + if (!this._hasSessionSubscribers(resource) && this._stateManager.getSessionState(session)) { + this._scheduleSessionRelease(resource); + } + })); + 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; @@ -632,8 +744,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; @@ -650,199 +760,12 @@ 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)); - 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)); - 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); - const effectiveCopilotApiService = copilotApiService ?? instantiationService.createInstance(CopilotApiService, fetchFn); - services.set(ICopilotApiService, effectiveCopilotApiService); - 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._register(this._stateManager.onDidChangeSessionConfig(({ session, previous, current }) => this._syncAgentMergeIndex(URI.parse(session), previous, current))); - // A held session skipped its idle release; re-arm it once the hold ends. - this._register(this._agentMergeController.onDidReleaseHold(session => { - const resource = URI.parse(session); - if (!this._hasSessionSubscribers(resource) && this._stateManager.getSessionState(session)) { - this._scheduleSessionRelease(resource); - } - })); - - 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)); - // 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: effectiveCopilotApiService, - 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, this._createArtifactServerToolAccessor())); this._scheduleExternalSessionPrune(); } @@ -873,7 +796,7 @@ export class AgentService extends Disposable implements IAgentService { } private async _pruneStaleExternalSessions(): Promise<void> { - const now = this._now(); + const now = Date.now(); const registered = await this._listRegisteredSessions(); const staleExternalSessions: URI[] = []; for (const entry of registered) { @@ -916,9 +839,8 @@ export class AgentService extends Disposable implements IAgentService { /** * Injects the host-owned {@link WorktreeIsolation} controller and forwards it - * to the collaborators that consult it. Called once at startup (from - * agentHostMain / agentHostServerMain) after the Copilot API dependencies - * have been wired. + * to the collaborators that consult it. Called by provider-infrastructure + * composition after the Copilot API dependencies have been wired. */ setWorktreeIsolation(worktree: WorktreeIsolation): void { this._worktree = worktree; @@ -1676,7 +1598,7 @@ export class AgentService extends Disposable implements IAgentService { suppressed++; return false; } - if (external && !readSessionEhcliAdoptable(sessionMetadata._meta) && this._isExternalSessionOlderThanMaxAge(sessionMetadata.modifiedTime, this._now())) { + if (external && !readSessionEhcliAdoptable(sessionMetadata._meta) && this._isExternalSessionOlderThanMaxAge(sessionMetadata.modifiedTime, Date.now())) { skippedAsStale++; return false; } @@ -1746,7 +1668,7 @@ export class AgentService extends Disposable implements IAgentService { continue; } const metadata = sessions[index]; - if (identity.external && !readSessionEhcliAdoptable(metadata._meta) && this._isExternalSessionOlderThanMaxAge(metadata.modifiedTime, this._now())) { + if (identity.external && !readSessionEhcliAdoptable(metadata._meta) && this._isExternalSessionOlderThanMaxAge(metadata.modifiedTime, Date.now())) { continue; } const registered = await this._sessionRegistry.register(identity.session, identity, { checkTombstone: true }); @@ -2106,7 +2028,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; @@ -2192,7 +2114,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. @@ -2402,7 +2324,7 @@ export class AgentService extends Disposable implements IAgentService { previousMode: AgentHostExternalSessionsMode, previouslyExposed: Set<string>, ): IAgentSessionMetadata[] { - const now = this._now(); + const now = Date.now(); const recentKeysFor = (mode: AgentHostExternalSessionsMode) => mode === AgentHostExternalSessionsMode.Recent ? this._getRecentSessionKeys(superset, now) : undefined; @@ -4284,8 +4206,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..2895a90c533ffe --- /dev/null +++ b/src/vs/platform/agentHost/node/agentServiceComposition.ts @@ -0,0 +1,237 @@ +/*--------------------------------------------------------------------------------------------- + * 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, IAgentHostAuthenticationService } 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: owned.add(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(IAgentHostAuthenticationService, core.authenticationService); + 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, context.createArtifactServerToolAccessor()), + ); + + 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/node/claude/claudeAgentSdkService.ts b/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts index d53bc976c3879a..8543ebef2f52de 100644 --- a/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts +++ b/src/vs/platform/agentHost/node/claude/claudeAgentSdkService.ts @@ -154,14 +154,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(); @@ -246,6 +239,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/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts index 55e878fee42d06..611d8fc83998bd 100644 --- a/src/vs/platform/agentHost/test/node/agentService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentService.test.ts @@ -40,7 +40,7 @@ import { SessionConfigKey } from '../../common/sessionConfigKeys.js'; import { AgentMergeConfigKey, readAgentMergeSessionState } from '../../common/agentMerge.js'; import { SessionDatabase } from '../../node/sessionDatabase.js'; import { ActionType, ActionEnvelope, NotificationType } from '../../common/state/sessionActions.js'; -import { AH_META_IS_READ_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_ORCHESTRATION_DB_KEY, AH_META_WORKSPACELESS_DB_KEY, ChangesetStatus, CustomizationType, MessageAttachmentKind, MessageKind, SessionActiveClient, ResponsePartKind, ROOT_STATE_URI, SESSION_META_FOLDER_PICKER_KEY, SESSION_META_MULTI_ROOT_KEY, SessionLifecycle, SessionSourceControlOutcome, SessionStatus, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, buildChatUri, buildDefaultChatUri, buildSubagentChatUri, buildSubagentSessionUri, customizationId, isDefaultChatUri, isSubagentSession, parseChatUri, parseSubagentSessionUri, readSessionEhcliAdoptable, readSessionExternal, readSessionGitHubState, readSessionMultiRootMetadata, readSessionFolderPickerDecision, readSessionOrchestration, readSessionSourceControlState, withSessionEhcliAdoptable, withSessionMultiRootMetadata, ChatOriginKind, type ChangesetState, type ISessionFolderPickerDecision, type ISessionOrchestration, type ISessionWithDefaultChat, type MarkdownResponsePart, type SessionState, type SessionSummary, type ToolCallCompletedState, type ToolCallResponsePart, type Turn } from '../../common/state/sessionState.js'; +import { AH_META_IS_READ_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_ORCHESTRATION_DB_KEY, AH_META_WORKSPACELESS_DB_KEY, ChangesetStatus, CustomizationType, MessageAttachmentKind, MessageKind, SessionActiveClient, ResponsePartKind, ROOT_STATE_URI, SESSION_META_FOLDER_PICKER_KEY, SESSION_META_MULTI_ROOT_KEY, SessionLifecycle, SessionSourceControlOutcome, SessionStatus, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, buildChatUri, buildDefaultChatUri, buildSubagentChatUri, buildSubagentSessionUri, customizationId, isDefaultChatUri, isSubagentSession, parseChatUri, parseSubagentSessionUri, readSessionEhcliAdoptable, readSessionExternal, readSessionGitHubState, readSessionMultiRootMetadata, readSessionFolderPickerDecision, readSessionOrchestration, readSessionSourceControlState, withSessionEhcliAdoptable, withSessionExternal, withSessionMultiRootMetadata, ChatOriginKind, type ChangesetState, type ISessionFolderPickerDecision, type ISessionOrchestration, type ISessionWithDefaultChat, type MarkdownResponsePart, type SessionState, type SessionSummary, type ToolCallCompletedState, type ToolCallResponsePart, type Turn } from '../../common/state/sessionState.js'; import { ChatInteractivity, type MessageAttachment } from '../../common/state/protocol/state.js'; import { IProductService } from '../../../product/common/productService.js'; import { AgentService } from '../../node/agentService.js'; @@ -63,6 +63,7 @@ import { SessionServerToolName } from '../../common/serverToolNames.js'; import { buildMcpChannel } from '../../node/shared/mcpCustomizationController.js'; import { readEphemeralSessionMeta, withEphemeralSessionMeta } from '../../common/meta/agentEphemeralSessionMeta.js'; import { readChatSurfaceMeta, withChatSurfaceMeta } from '../../common/meta/agentChatSurfaceMeta.js'; +import { createTestAgentService } from './agentServiceTestUtils.js'; /** * Replace individual operations on an agent's chat surface, delegating every @@ -462,7 +463,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())); }); @@ -680,7 +681,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, @@ -726,7 +727,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, @@ -855,7 +856,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, @@ -915,7 +916,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); @@ -986,7 +987,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({ @@ -1012,7 +1013,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({ @@ -1046,7 +1047,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())); @@ -1058,7 +1059,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.Last30Days }); const reopenedAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => reopenedAgent.dispose())); @@ -1102,7 +1103,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); @@ -1116,7 +1117,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.Last30Days }); const reopenedAgent = new MockAgent('copilot'); disposables.add(toDisposable(() => reopenedAgent.dispose())); @@ -1157,7 +1158,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); @@ -1207,7 +1208,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, @@ -1359,7 +1360,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); @@ -1419,7 +1420,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())); @@ -1447,7 +1448,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())); @@ -1539,7 +1540,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, @@ -1582,7 +1583,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); @@ -1597,7 +1598,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); @@ -1641,7 +1642,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); @@ -1711,7 +1712,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); @@ -1739,7 +1740,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); @@ -1766,7 +1767,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); @@ -1810,7 +1811,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); @@ -1999,7 +2000,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); @@ -2299,7 +2300,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); @@ -2737,7 +2738,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(); @@ -2778,7 +2779,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); @@ -2829,7 +2830,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<string>()); @@ -2855,7 +2856,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({ @@ -2885,7 +2886,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' }); @@ -2959,8 +2960,8 @@ suite('AgentService (node dispatcher)', () => { } } - function createExternalSessionService(now: () => number, sessionDataService = createSessionDataService(), orchestratorDatabase?: IAgentHostDatabase): AgentService { - return disposables.add(new AgentService( + function createExternalSessionService(sessionDataService = createSessionDataService(), orchestratorDatabase?: IAgentHostDatabase): AgentService { + return disposables.add(createTestAgentService( new NullLogService(), fileService, sessionDataService, @@ -2975,10 +2976,17 @@ suite('AgentService (node dispatcher)', () => { undefined, undefined, orchestratorDatabase, - 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, @@ -3024,7 +3032,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.Last30Days }); const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); @@ -3050,7 +3058,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<string, URI> })._sessions.set(AgentSession.id(session), session); @@ -3063,10 +3071,10 @@ suite('AgentService (node dispatcher)', () => { assert.strictEqual(await db.getMetadata(AH_META_IS_READ_DB_KEY), ''); }); - test('discovery does not ingest external sessions older than 30 days', async () => { + testWithExternalSessionClock('discovery does not ingest external sessions older than 30 days', 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')); const stale = agent.addSession('stale', now - 30 * day - 1); const fresh = agent.addSession('fresh', now - 30 * day + 60_000); @@ -3091,14 +3099,14 @@ suite('AgentService (node dispatcher)', () => { assert.ok(!registered.has(stale.toString())); }); - test('prune removes stale external sessions but keeps adoptable-legacy sessions', async () => { + testWithExternalSessionClock('prune removes stale external sessions but keeps adoptable-legacy sessions', 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')); const stale = agent.addSession('stale-prune', now - 30 * day - 1); const staleAdoptable = agent.addSession('stale-adoptable', now - 30 * day - 1, withSessionEhcliAdoptable(undefined)); - const fresh = agent.addSession('fresh-prune', now - 30 * day); + const fresh = agent.addSession('fresh-prune', now - 29 * day); svc.registerProvider(agent); const sessionRegistry = (svc as unknown as { _sessionRegistry: AgentSessionRegistry })._sessionRegistry; await sessionRegistry.register(stale, { provider: 'copilot', startTime: now - 30 * day - 1, source: 'discovery' }, { checkTombstone: true }); @@ -3111,17 +3119,48 @@ suite('AgentService (node dispatcher)', () => { assert.deepStrictEqual(registered, [fresh.toString(), staleAdoptable.toString()].sort()); }); - test('filters external sessions in every mode with inclusive time boundaries', async () => { + test('external session mode time boundaries are inclusive', () => { + const day = 24 * 60 * 60 * 1000; + const now = Date.UTC(2026, 0, 1); + const svc = createExternalSessionService(); + const shouldIncludeSession = (svc as unknown as { + _shouldIncludeSession(session: IAgentSessionMetadata, mode: AgentHostExternalSessionsMode, now: number): boolean; + })._shouldIncludeSession.bind(svc); + const metadata = (age: number): IAgentSessionMetadata => ({ + session: AgentSession.uri('copilot', `age-${age}`), + startTime: now - age, + modifiedTime: now - age, + _meta: withSessionExternal(undefined, true), + }); + + assert.deepStrictEqual({ + at24Hours: shouldIncludeSession(metadata(day), AgentHostExternalSessionsMode.Last24Hours, now), + olderThan24Hours: shouldIncludeSession(metadata(day + 1), AgentHostExternalSessionsMode.Last24Hours, now), + at7Days: shouldIncludeSession(metadata(7 * day), AgentHostExternalSessionsMode.Last7Days, now), + olderThan7Days: shouldIncludeSession(metadata(7 * day + 1), AgentHostExternalSessionsMode.Last7Days, now), + at30Days: shouldIncludeSession(metadata(30 * day), AgentHostExternalSessionsMode.Last30Days, now), + olderThan30Days: shouldIncludeSession(metadata(30 * day + 1), AgentHostExternalSessionsMode.Last30Days, now), + }, { + at24Hours: true, + olderThan24Hours: false, + at7Days: true, + olderThan7Days: false, + at30Days: true, + olderThan30Days: false, + }); + }); + + testWithExternalSessionClock('filters external sessions in every mode', 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); + agent.addSession('within-24-hours', now - day + day / 2); agent.addSession('older-than-24-hours', now - day - 1); - agent.addSession('at-7-days', now - 7 * day); + agent.addSession('within-7-days', now - 7 * day + day / 2); agent.addSession('older-than-7-days', now - 7 * day - 1); - agent.addSession('at-30-days', now - 30 * day); + agent.addSession('within-30-days', now - 30 * day + day / 2); agent.addSession('older-than-30-days', now - 30 * day - 1); svc.registerProvider(agent); @@ -3143,19 +3182,19 @@ suite('AgentService (node dispatcher)', () => { assert.deepStrictEqual({ listedByDefault, listedByMode }, { listedByDefault: [], listedByMode: { - [AgentHostExternalSessionsMode.Recent]: ['at-24-hours', 'recent'], + [AgentHostExternalSessionsMode.Recent]: ['recent', 'within-24-hours'], [AgentHostExternalSessionsMode.None]: [], - [AgentHostExternalSessionsMode.Last30Days]: ['at-24-hours', 'at-30-days', 'at-7-days', 'older-than-24-hours', 'older-than-7-days', 'recent'], - [AgentHostExternalSessionsMode.Last24Hours]: ['at-24-hours', 'recent'], - [AgentHostExternalSessionsMode.Last7Days]: ['at-24-hours', 'at-7-days', 'older-than-24-hours', 'recent'], + [AgentHostExternalSessionsMode.Last30Days]: ['older-than-24-hours', 'older-than-7-days', 'recent', 'within-24-hours', 'within-30-days', 'within-7-days'], + [AgentHostExternalSessionsMode.Last24Hours]: ['recent', 'within-24-hours'], + [AgentHostExternalSessionsMode.Last7Days]: ['older-than-24-hours', 'recent', 'within-24-hours', 'within-7-days'], }, }); }); - test('a mode that hides every external session skips the catalog work for them', async () => { + testWithExternalSessionClock('a mode that hides every external session skips the catalog work for them', async () => { const now = Date.now(); const perSession = createPerSessionDataService(); - const svc = createExternalSessionService(() => now, perSession.service); + const svc = createExternalSessionService(perSession.service); const agent = disposables.add(new TimedExternalAgent('copilot')); agent.addSession('external-one', now); agent.addSession('external-two', now); @@ -3188,10 +3227,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); @@ -3228,9 +3267,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')); @@ -3267,7 +3306,7 @@ suite('AgentService (node dispatcher)', () => { }); }); - test('recent re-adds a registry-known external session after restart list visibility rotates', async () => { + testWithExternalSessionClock('recent re-adds a registry-known external session after restart list visibility rotates', async () => { const now = Date.now(); const database = new TransientRegistryWriteDatabase(); const first = AgentSession.uri('copilot', 'first'); @@ -3278,7 +3317,7 @@ suite('AgentService (node dispatcher)', () => { } await database.markProviderBackfilled('copilot'); - const svc = createExternalSessionService(() => now, createSessionDataService(), database); + const svc = createExternalSessionService(createSessionDataService(), database); setExternalSessionsMode(svc, AgentHostExternalSessionsMode.Recent, 1); await waitForSessionListReconciliation(svc); const agent = disposables.add(new TimedExternalAgent('copilot')); @@ -3316,9 +3355,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.Last30Days, 1); await waitForSessionListReconciliation(svc); const agent = disposables.add(new TimedExternalAgent('copilot')); @@ -3368,9 +3407,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')); @@ -3409,9 +3448,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[] = []; @@ -3439,9 +3478,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.Last30Days, 1); await waitForSessionListReconciliation(svc); const agent = disposables.add(new TimedExternalAgent('copilot')); @@ -3476,7 +3515,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 []; @@ -3484,7 +3523,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[] = []; @@ -3518,7 +3557,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(); @@ -3540,7 +3579,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 }); @@ -3571,7 +3610,7 @@ suite('AgentService (node dispatcher)', () => { }); test('an adoptable chat retracted by disabling migration is re-surfaced when it is re-enabled', 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 }); @@ -3604,7 +3643,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'); @@ -3628,7 +3667,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'); @@ -3653,7 +3692,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' }); @@ -3687,7 +3726,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); await svc.listSessions(); @@ -3722,7 +3761,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<void>(); const inner = svc as unknown as { _computeSessions(mode: AgentHostExternalSessionsMode): Promise<readonly IAgentSessionMetadata[]> }; const original = inner._computeSessions; @@ -3785,7 +3824,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(); @@ -3814,7 +3853,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(); @@ -3838,7 +3877,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(); @@ -3882,7 +3921,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(); @@ -3905,7 +3944,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(); @@ -3949,7 +3988,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<string, URI> })._sessions.set(AgentSession.id(session), session); @@ -3976,7 +4015,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<string, URI> })._sessions.set(AgentSession.id(native), native); @@ -4007,7 +4046,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.Last30Days }); const agent = disposables.add(new GatedListAgent('copilot')); svc.registerProvider(agent); @@ -4057,7 +4096,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.Last30Days }); const agent = disposables.add(new TransientListFailureAgent('copilot')); svc.registerProvider(agent); @@ -4078,7 +4117,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.Last30Days }); const early = disposables.add(new MockAgent('copilot')); svc.registerProvider(early); @@ -4121,7 +4160,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); @@ -4154,7 +4193,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<string, URI> })._sessions.set(AgentSession.id(legacy), legacy); @@ -4176,7 +4215,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); @@ -4216,7 +4255,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.Last30Days }); const providerA = disposables.add(new CountingAgent('copilot')); const providerB = disposables.add(new FailingThenRecoveringAgent('other')); @@ -4267,7 +4306,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.Last30Days }); const agent = disposables.add(new NotYetEnumerableAgent('copilot')); const originalListExternalChats = agent.listExternalChats.bind(agent); @@ -4305,7 +4344,7 @@ suite('AgentService (node dispatcher)', () => { const existing = AgentSession.uri('copilot', 'existing-before-unavailable'); await db.registerSession(existing.toString(), { provider: 'copilot', startTime: 1, source: 'explicit' }, { checkTombstone: false }); const writesBeforeUnavailable = db.registryWriteAttempts; - 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.Last30Days }); const agent = disposables.add(new NotYetMigratableAgent('copilot')); const legacy = AgentSession.uri('copilot', 'legacy-migration-not-ready'); @@ -4365,7 +4404,7 @@ suite('AgentService (node dispatcher)', () => { return []; } } - 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 SingleFlightRetryAgent('copilot')); svc.registerProvider(agent); for (let i = 0; i < 20 && agent.catalogCalls === 0; i++) { @@ -4395,7 +4434,7 @@ suite('AgentService (node dispatcher)', () => { } } 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)); svc.configurationService.updateRootConfig({ [AgentHostShowExternalSessionsConfigKey]: AgentHostExternalSessionsMode.Last30Days }); svc.configurationService.updateRootConfig({ [AgentHostMigrateLegacyCopilotCliEnabledConfigKey]: true }); const copilot = disposables.add(new CatalogAgent('copilot')); @@ -4460,7 +4499,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); @@ -4503,7 +4542,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(); @@ -4534,7 +4573,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); @@ -4562,7 +4601,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'); @@ -4609,7 +4648,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(); @@ -4649,7 +4688,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.Last30Days }); const agent = disposables.add(new CountingAgent('copilot')); const legacy = AgentSession.uri('copilot', 'old-db-native-session'); @@ -4692,7 +4731,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); @@ -4752,7 +4791,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); @@ -4792,7 +4831,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); @@ -4809,7 +4848,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); @@ -4863,7 +4902,7 @@ suite('AgentService (node dispatcher)', () => { // Manually add the session to the mock (agent as unknown as { _sessions: Map<string, URI> })._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.Last30Days }); svc.registerProvider(agent); @@ -4909,7 +4948,7 @@ suite('AgentService (node dispatcher)', () => { }; (agent as unknown as { _sessions: Map<string, URI> })._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.Last30Days }); svc.registerProvider(agent); @@ -4932,7 +4971,7 @@ suite('AgentService (node dispatcher)', () => { _meta: { multiRoot: { workspaceFile: 'file:///provider-spoof.code-workspace' } }, }; (agent as unknown as { _sessions: Map<string, URI> })._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.Last30Days }); svc.registerProvider(agent); @@ -4953,7 +4992,7 @@ suite('AgentService (node dispatcher)', () => { const agent = new MockAgent('copilot'); disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map<string, URI> })._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.Last30Days }); svc.registerProvider(agent); @@ -4971,7 +5010,7 @@ suite('AgentService (node dispatcher)', () => { _meta: { multiRoot: { workspaceFile: 'file:///provider-spoof.code-workspace' } }, }; (agent as unknown as { _sessions: Map<string, URI> })._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.Last30Days }); svc.registerProvider(agent); @@ -4997,7 +5036,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.Last30Days }); svc.registerProvider(agent); @@ -5032,7 +5071,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.Last30Days }); svc.setWorktreeIsolation(disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/test' }, @@ -5079,7 +5118,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.Last30Days }); svc.setWorktreeIsolation(disposables.add(new WorktreeIsolation( { generateBranchName: async () => 'agents/test' }, @@ -5312,7 +5351,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map<string, URI> })._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(); @@ -5355,7 +5394,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map<string, URI> })._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(); @@ -5391,7 +5430,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map<string, URI> })._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(); @@ -5446,7 +5485,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map<string, URI> })._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 @@ -5515,7 +5554,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map<string, URI> })._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 @@ -5558,7 +5597,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(toDisposable(() => agent.dispose())); (agent as unknown as { _sessions: Map<string, URI> })._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 @@ -5648,7 +5687,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; @@ -5695,7 +5734,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; @@ -5755,7 +5794,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. @@ -5779,7 +5818,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; @@ -5810,7 +5849,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; @@ -5844,7 +5883,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; @@ -5889,7 +5928,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; @@ -5942,7 +5981,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); @@ -5970,7 +6009,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); @@ -6003,7 +6042,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); @@ -6523,7 +6562,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 DelayedMigrationAgent('copilot')); const { session } = await createAgentSession(agent); svc.registerProvider(agent); @@ -6548,7 +6587,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); @@ -6592,7 +6631,7 @@ suite('AgentService (node dispatcher)', () => { } function makeService(): AgentService { - return disposables.add(new AgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); + return disposables.add(createTestAgentService(new NullLogService(), fileService, createSessionDataService(), { _serviceBrand: undefined } as IProductService, createNoopGitService())); } function seedSession(agent: MockAgent, session: URI): void { @@ -6716,7 +6755,7 @@ suite('AgentService (node dispatcher)', () => { const session = AgentSession.uri('copilot', 'registered-but-unavailable'); await db.registerSession(session.toString(), { provider: 'copilot', startTime: 1, source: 'restore' }, { checkTombstone: false }); await db.markProviderBackfilled('copilot'); - 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 StartupRaceAgent('copilot')); agent.migrationGate.complete(); svc.registerProvider(agent); @@ -6769,7 +6808,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; @@ -6783,7 +6822,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; @@ -6803,7 +6842,7 @@ suite('AgentService (node dispatcher)', () => { test('restores persisted orchestration 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; @@ -6823,7 +6862,7 @@ suite('AgentService (node dispatcher)', () => { test('does not consume a child notification when its creator cannot be resolved', 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())); localService.registerProvider(copilotAgent); const child = await localService.createSession({ provider: 'copilot' }); const orchestration: ISessionOrchestration = { @@ -6848,7 +6887,7 @@ suite('AgentService (node dispatcher)', () => { test('restores a cold creator before delivering and consuming a child notification', 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())); localService.registerProvider(copilotAgent); const creator = await localService.createSession({ provider: 'copilot' }); const child = await localService.createSession({ provider: 'copilot' }); @@ -6887,7 +6926,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; @@ -6946,7 +6985,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; @@ -6971,7 +7010,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 } }; @@ -7000,7 +7039,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; @@ -7031,7 +7070,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; @@ -7050,7 +7089,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 = { @@ -7070,7 +7109,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; @@ -7197,7 +7236,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 = []; @@ -7240,7 +7279,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())); localService.registerProvider(disposables.add(new NotAdoptableAgent())); localService.configurationService.updateRootConfig({ [AgentHostMigrateLegacyCopilotCliEnabledConfigKey]: true }); @@ -7267,7 +7306,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 = []; @@ -7303,7 +7342,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); @@ -7340,7 +7379,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(), @@ -7734,7 +7773,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'); @@ -8148,7 +8187,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' }); @@ -8270,7 +8309,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; @@ -8349,7 +8388,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')); @@ -8360,7 +8399,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(); @@ -8378,7 +8417,7 @@ suite('AgentService (node dispatcher)', () => { test('createSession carries client-owned _meta slots and drops unknown ones', async () => { const perSession = createPerSessionDataService(); const agent = disposables.add(new MockAgent('copilot')); - 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())); svc.registerProvider(agent); const session = await svc.createSession({ @@ -8406,7 +8445,7 @@ suite('AgentService (node dispatcher)', () => { test('ephemeral session teardown clears its discovery tombstone', async () => { const perSession = createPerSessionDataService(); const agent = disposables.add(new MockAgent('copilot')); - 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())); svc.registerProvider(agent); const registry = (svc as unknown as { _sessionRegistry: AgentSessionRegistry })._sessionRegistry; @@ -8440,7 +8479,7 @@ suite('AgentService (node dispatcher)', () => { } const agent = disposables.add(new LeakyAgent('copilot')); - 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())); svc.registerProvider(agent); await svc.createSession({ provider: 'copilot', @@ -8457,7 +8496,7 @@ suite('AgentService (node dispatcher)', () => { const registeredBeforeRestart = await svc.getRegisteredSessions(); const restartedAgent = disposables.add(new LeakyAgent('copilot')); - const restarted = disposables.add(new AgentService(new NullLogService(), fileService, perSession.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); + const restarted = disposables.add(createTestAgentService(new NullLogService(), fileService, perSession.service, { _serviceBrand: undefined } as IProductService, createNoopGitService())); restarted.registerProvider(restartedAgent); const afterRestart = await restarted.listSessions(); @@ -8499,7 +8538,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' }); @@ -8547,7 +8586,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' }); @@ -8680,7 +8719,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); @@ -8796,7 +8835,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' }); @@ -8835,7 +8874,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' }); @@ -8869,7 +8908,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' }); @@ -9004,7 +9043,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 => ({ @@ -9072,7 +9111,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); @@ -9119,7 +9158,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<string, URI> })._sessions.set(AgentSession.id(session), session); @@ -9144,7 +9183,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); @@ -9174,7 +9213,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); @@ -9201,7 +9240,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')); @@ -9267,7 +9306,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) => { @@ -9747,7 +9786,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<IAgentCreateChatResult> { @@ -9789,7 +9828,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' }); @@ -9842,7 +9881,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' }); @@ -9923,7 +9962,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' }); @@ -9982,7 +10021,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' }); @@ -10054,7 +10093,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' }); @@ -10131,7 +10170,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' }); @@ -10190,7 +10229,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' }); @@ -10239,7 +10278,7 @@ suite('AgentService (node dispatcher)', () => { override async getSessionMessages(): Promise<readonly Turn[]> { 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' }); @@ -10282,7 +10321,7 @@ suite('AgentService (node dispatcher)', () => { override async getSessionMessages(): Promise<readonly Turn[]> { 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' }); @@ -10323,7 +10362,7 @@ suite('AgentService (node dispatcher)', () => { override async getSessionMessages(): Promise<readonly Turn[]> { 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' }); @@ -10369,7 +10408,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' }); @@ -10413,7 +10452,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({ @@ -10463,7 +10502,7 @@ suite('AgentService (node dispatcher)', () => { override async getSessionMessages(): Promise<readonly Turn[]> { 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'); @@ -10515,7 +10554,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' }); @@ -10562,7 +10601,7 @@ suite('AgentService (node dispatcher)', () => { override async disposeChat(_session: URI, _chat: URI): Promise<void> { } } 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' }); @@ -10602,7 +10641,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' }); @@ -10637,7 +10676,7 @@ suite('AgentService (node dispatcher)', () => { override async disposeChat(): Promise<void> { } } 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' }); @@ -10692,7 +10731,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' }); @@ -10740,7 +10779,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' }); @@ -10773,7 +10812,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' }); @@ -10811,7 +10850,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' }); @@ -10855,7 +10894,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' }); @@ -10904,7 +10943,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); @@ -10991,7 +11030,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); @@ -11171,7 +11210,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); @@ -11629,7 +11668,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())); @@ -11670,7 +11709,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())); @@ -11711,7 +11750,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 @@ -11937,7 +11976,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' } }); @@ -11955,7 +11994,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' }); @@ -11978,7 +12017,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, @@ -12014,7 +12053,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)), { @@ -12041,7 +12080,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 @@ -12071,7 +12110,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); @@ -12125,7 +12164,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); @@ -12176,7 +12215,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' } }); @@ -12203,7 +12242,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); @@ -12347,7 +12386,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, @@ -12414,7 +12453,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, @@ -12493,7 +12532,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); @@ -12672,7 +12711,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); @@ -12697,7 +12736,7 @@ suite('AgentService (node dispatcher)', () => { suite('Agent Merge durable session monitoring', () => { function createAgentMergeService(sessionDb: TestSessionDatabase, orchestratorDb: IAgentHostDatabase): AgentService { - const localService = disposables.add(new AgentService( + const localService = disposables.add(createTestAgentService( new NullLogService(), fileService, createSessionDataService(sessionDb), { _serviceBrand: undefined } as IProductService, createNoopGitService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, orchestratorDb, 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..633deb82c32a41 --- /dev/null +++ b/src/vs/platform/agentHost/test/node/agentServiceTestUtils.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } 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 { 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'; + +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, +): AgentService { + const effectiveFileMonitorService = fileMonitorService ?? new AgentHostFileMonitorService(fileService, logService); + const proxyResolver: IAgentHostProxyResolver = { + _serviceBrand: undefined, + onDidRegisterConnection: Event.None, + onDidChangeConfiguration: Event.None, + register: () => Disposable.None, + bindConfigurationService: () => { }, + getConfigurationValue: () => undefined, + resolveProxy: async () => undefined, + fetch: fetchFn, + }; + const services = new ServiceCollection( + [ILogService, logService], + [IFileService, fileService], + [ISessionDataService, sessionDataService], + [IProductService, productService], + [IAgentHostGitService, gitService], + [ITelemetryService, telemetryService], + [IAgentHostFileMonitorService, effectiveFileMonitorService], + [IAgentHostProxyResolver, proxyResolver], + ); + const instantiationService = new InstantiationService(services, /*strict*/ true); + const options = { + rootConfigResource, + copilotApiService, + providerConfigurations, + hostLaunchKind, + storageResource, + orchestratorDatabase, + }; + const service = createAgentService( + options, + services, + instantiationService, + fetchFn, + logService, + productService, + fileMonitorService ? [instantiationService] : [effectiveFileMonitorService, 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 f2cc47addcc4c4..b377c8785525e9 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'; @@ -53,6 +52,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 ------------------------------------------------------------------ @@ -4917,7 +4917,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 }); @@ -4936,7 +4936,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); @@ -4963,7 +4963,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 aa093bd0ce65a4..641c7fb26ed1cc 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 { AgentHostSessionTitleSignal, IAgentHostSessionTitleSignal } from '../.. import { IAgentHostGitHubEndpointService } from '../../node/agentHostGitHubEndpointService.js'; import { IAgentHostAuthenticationService, type IAgentHostAuthTokenChangeEvent } from '../../node/agentHostAuthenticationService.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'; @@ -84,7 +85,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'; @@ -2155,7 +2155,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(), @@ -5356,11 +5356,23 @@ suite('ClaudeAgent', () => { [ILogService, new RecordingLogService()], [IAgentSdkDownloader, new RecordingAgentSdkDownloader(false)], ); + 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; diff --git a/src/vs/workbench/browser/chatDropdownPill.ts b/src/vs/workbench/browser/chatDropdownPill.ts index 5d2803d1d34297..e109915d35567e 100644 --- a/src/vs/workbench/browser/chatDropdownPill.ts +++ b/src/vs/workbench/browser/chatDropdownPill.ts @@ -243,4 +243,3 @@ export function createChatSectionPill( ? { action, createActionViewItem: viewItemOptions => new ChatResourcePillActionViewItem(action, viewItemOptions, singleResourceEntry, resourceLabels) } : { action, createActionViewItem: viewItemOptions => instantiationService.createInstance(ChatDropdownPillActionViewItem, action, viewItemOptions, sections, options) }); } -