Files
vscode/src/vs/platform/agentHost/node/agentHostMain.ts
Connor Peet b7e5e30020 wip
2026-03-31 09:14:16 -07:00

170 lines
8.0 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
import { Server as ChildProcessServer } from '../../../base/parts/ipc/node/ipc.cp.js';
import { Server as UtilityProcessServer } from '../../../base/parts/ipc/node/ipc.mp.js';
import { isUtilityProcess } from '../../../base/parts/sandbox/node/electronTypes.js';
import { Emitter } from '../../../base/common/event.js';
import { DisposableStore } from '../../../base/common/lifecycle.js';
import { URI } from '../../../base/common/uri.js';
import * as os from 'os';
import { AgentHostIpcChannels } from '../common/agentService.js';
import { AgentService } from './agentService.js';
import { CopilotAgent } from './copilot/copilotAgent.js';
import { ProtocolServerHandler } from './protocolServerHandler.js';
import { WebSocketProtocolServer } from './webSocketTransport.js';
import { NativeEnvironmentService } from '../../environment/node/environmentService.js';
import { parseArgs, OPTIONS } from '../../environment/node/argv.js';
import { getLogLevel, ILogService } from '../../log/common/log.js';
import { LogService } from '../../log/common/logService.js';
import { LoggerService } from '../../log/node/loggerService.js';
import { LoggerChannel } from '../../log/common/logIpc.js';
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 { InstantiationService } from '../../instantiation/common/instantiationService.js';
import { ServiceCollection } from '../../instantiation/common/serviceCollection.js';
import { SessionDataService } from './sessionDataService.js';
import { ISessionDataService } from '../common/sessionDataService.js';
import { AgentHostClientFileSystemProvider } from '../common/agentHostClientFileSystemProvider.js';
import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js';
import { IAgentPluginManager } from '../common/agentPluginManager.js';
import { AgentPluginManager } from './agentPluginManager.js';
// Entry point for the agent host utility process.
// Sets up IPC, logging, and registers agent providers (Copilot).
// When VSCODE_AGENT_HOST_PORT or VSCODE_AGENT_HOST_SOCKET_PATH env vars
// are set, also starts a WebSocket server for external clients.
startAgentHost();
function startAgentHost(): void {
// Setup RPC - supports both Electron utility process and Node child process
let server: ChildProcessServer<string> | UtilityProcessServer;
if (isUtilityProcess(process)) {
server = new UtilityProcessServer();
} else {
server = new ChildProcessServer(AgentHostIpcChannels.AgentHost);
}
const disposables = new DisposableStore();
// Services
const productService: IProductService = { _serviceBrand: undefined, ...product };
const environmentService = new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService);
const loggerService = new LoggerService(getLogLevel(environmentService), environmentService.logsHome);
server.registerChannel(AgentHostIpcChannels.Logger, new LoggerChannel(loggerService, () => DefaultURITransformer));
const logger = loggerService.createLogger('agenthost', { name: localize('agentHost', "Agent Host") });
const logService = new LogService(logger);
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))));
// Session data service
const sessionDataService = new SessionDataService(URI.file(environmentService.userDataPath), fileService, logService);
// Create the real service implementation that lives in this process
let agentService: AgentService;
try {
agentService = new AgentService(logService, fileService, sessionDataService);
const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService);
const diServices = new ServiceCollection();
diServices.set(ILogService, logService);
diServices.set(IFileService, fileService);
diServices.set(ISessionDataService, sessionDataService);
diServices.set(IAgentPluginManager, pluginManager);
const instantiationService = new InstantiationService(diServices);
agentService.registerProvider(instantiationService.createInstance(CopilotAgent));
} catch (err) {
logService.error('Failed to create AgentService', err);
throw err;
}
const agentChannel = ProxyChannel.fromService(agentService, disposables);
server.registerChannel(AgentHostIpcChannels.AgentHost, agentChannel);
// Expose the WebSocket client connection count to the parent process via IPC.
// This is NOT part of the agent host protocol -- it is only used by the
// server process to manage the agent host process lifetime.
const connectionCountEmitter = disposables.add(new Emitter<number>());
const connectionTrackerChannel = ProxyChannel.fromService(
{ onDidChangeConnectionCount: connectionCountEmitter.event },
disposables,
);
server.registerChannel(AgentHostIpcChannels.ConnectionTracker, connectionTrackerChannel);
// Start WebSocket server for external clients if configured
startWebSocketServer(agentService, fileService, logService, disposables, count => connectionCountEmitter.fire(count)).catch(err => {
logService.error('Failed to start WebSocket server', err);
});
process.once('exit', () => {
agentService.dispose();
logService.dispose();
disposables.dispose();
});
}
/**
* When the parent process passes WebSocket configuration via environment
* variables, start a protocol server that external clients can connect to.
* This reuses the same {@link AgentService} and {@link SessionStateManager}
* that the IPC channel uses, so both IPC and WebSocket clients share state.
*/
async function startWebSocketServer(agentService: AgentService, fileService: IFileService, logService: ILogService, disposables: DisposableStore, onConnectionCountChanged: (count: number) => void): Promise<void> {
const port = process.env['VSCODE_AGENT_HOST_PORT'];
const socketPath = process.env['VSCODE_AGENT_HOST_SOCKET_PATH'];
if (!port && !socketPath) {
return;
}
const connectionToken = process.env['VSCODE_AGENT_HOST_CONNECTION_TOKEN'];
const host = process.env['VSCODE_AGENT_HOST_HOST'] || 'localhost';
const wsServer = disposables.add(await WebSocketProtocolServer.create(
socketPath
? {
socketPath,
connectionTokenValidate: connectionToken
? (token) => token === connectionToken
: undefined,
}
: {
port: parseInt(port!, 10),
host,
connectionTokenValidate: connectionToken
? (token) => token === connectionToken
: undefined,
},
logService,
));
const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider());
disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, clientFileSystemProvider));
const protocolHandler = disposables.add(new ProtocolServerHandler(
agentService,
agentService.stateManager,
wsServer,
{ defaultDirectory: URI.file(os.homedir()).toString() },
clientFileSystemProvider,
logService,
));
disposables.add(protocolHandler.onDidChangeConnectionCount(onConnectionCountChanged));
const listenTarget = socketPath ?? `${host}:${port}`;
logService.info(`[AgentHost] WebSocket server listening on ${listenTarget}`);
// Do not change this line. The CLI looks for this in the output.
console.log(`Agent host server listening on ${listenTarget}`);
}