Files
vscode/src/vs/platform/agentHost/node/agentHostServerMain.ts
Connor Peet 3e15c2da57 agentHost: hello world from server
Exposes --agent-host-port/--agent-host-path to specify an additional
port for them to listen on. This runs as a separate process mirroring
the architecture from main.

Does a 'hello world', I need to keep testing it some more. I honestly
don't know a lot about the import changes Opus is doing here. Without
them, there is an error:

```
import { CopilotClient } from "@github/copilot-sdk";
         ^^^^^^^^^^^^^
SyntaxError: The requested module '@github/copilot-sdk' does not provide an export named 'CopilotClient'
```

This was Copilot's explanation for why they're needed here but not in Electron:

>The agent host process itself didn't change — the issue is the **module resolution environment** differs between Electron and the server.
>
>**Electron (desktop):** `ElectronAgentHostStarter` spawns a utility process that inherits the main process's module resolution. It resolves packages from the **root** node_modules, where `@github/copilot-sdk`, `ws`, etc. are all installed. Everything just works.
>
>**Server:** `NodeAgentHostStarter` spawns a child via `bootstrap-fork.js`, which registers a custom ESM resolver hook (bootstrap-import.ts) that **redirects** all module lookups to node_modules instead of root node_modules. That's the `[bootstrap-import] Initialized node_modules redirector for: ...\remote\node_modules` log line. The remote folder has its own package.json with a curated set of server-specific dependencies.
>
>The problems were:
>1. **Missing deps** — `@github/copilot-sdk`, `@github/copilot`, and `ws` weren't in package.json because the agent host had never run in the server context before
>2. **Resolver bugs** — bootstrap-import.ts was written when all remote deps were CJS. It hardcoded `format: 'commonjs'`, didn't handle `exports` maps, didn't handle `.mjs` files, and didn't resolve subpath imports like `vscode-jsonrpc/node`. These are pre-existing limitations that never mattered until now because no server component previously depended on ESM-only npm packages.
>
>So in short: the agent host code is identical — it's the server's module resolution plumbing that needed updating to support the ESM packages the agent host depends on.

cc @bpasero as the expert in this area
2026-03-12 09:00:40 -07:00

170 lines
6.8 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Standalone agent host server with WebSocket protocol transport.
// Start with: node out/vs/platform/agentHost/node/agentHostServerMain.js [--port <port>] [--enable-mock-agent]
import { DisposableStore } from '../../../base/common/lifecycle.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, NullLogService } from '../../log/common/log.js';
import { LogService } from '../../log/common/logService.js';
import { LoggerService } from '../../log/node/loggerService.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 { CopilotAgent } from './copilot/copilotAgent.js';
import { type AgentProvider } from '../common/agentService.js';
import { SessionStatus } from '../common/state/sessionState.js';
import { AgentService } from './agentService.js';
import { WebSocketProtocolServer } from './webSocketTransport.js';
import { ProtocolServerHandler, type IProtocolSideEffectHandler } from './protocolServerHandler.js';
// ---- Options ----------------------------------------------------------------
interface IServerOptions {
readonly port: number;
readonly enableMockAgent: boolean;
readonly quiet: boolean;
}
function parseServerOptions(): IServerOptions {
const argv = process.argv.slice(2);
const envPort = parseInt(process.env['VSCODE_AGENT_HOST_PORT'] ?? '8081', 10);
const portIdx = argv.indexOf('--port');
const port = portIdx >= 0 ? parseInt(argv[portIdx + 1], 10) : envPort;
const enableMockAgent = argv.includes('--enable-mock-agent');
const quiet = argv.includes('--quiet');
return { port, enableMockAgent, quiet };
}
// ---- Main -------------------------------------------------------------------
async function main(): Promise<void> {
const options = parseServerOptions();
const disposables = new DisposableStore();
// Services — production logging unless --quiet
let logService: ILogService;
let loggerService: LoggerService | undefined;
if (options.quiet) {
logService = new NullLogService();
} else {
const services = new ServiceCollection();
const productService: IProductService = { _serviceBrand: undefined, ...product };
services.set(IProductService, productService);
const args = parseArgs(process.argv.slice(2), OPTIONS);
const environmentService = new NativeEnvironmentService(args, 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));
services.set(ILogService, logService);
}
logService.info('[AgentHostServer] Starting standalone agent host server');
// Create agent service — handles agent registration, session lifecycle,
// event mapping, and state management (reuses the same logic as the
// utility-process entry point in agentHostMain.ts).
const agentService = disposables.add(new AgentService(logService));
// Register agents
if (!options.quiet) {
// Production agents (require DI)
const services = new ServiceCollection();
const productService: IProductService = { _serviceBrand: undefined, ...product };
services.set(IProductService, productService);
const args = parseArgs(process.argv.slice(2), OPTIONS);
const environmentService = new NativeEnvironmentService(args, productService);
services.set(INativeEnvironmentService, environmentService);
services.set(ILogService, logService);
const instantiationService = new InstantiationService(services);
const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent));
agentService.registerProvider(copilotAgent);
}
if (options.enableMockAgent) {
// Dynamic import to avoid bundling test code in production
import('../test/node/mockAgent.js').then(({ ScriptedMockAgent }) => {
const mockAgent = disposables.add(new ScriptedMockAgent());
agentService.registerProvider(mockAgent);
}).catch(err => {
logService.error('[AgentHostServer] Failed to load mock agent', err);
});
}
// WebSocket server
const wsServer = disposables.add(await WebSocketProtocolServer.create(options.port, logService));
// Side-effect handler — delegates to AgentService for all orchestration
const sideEffects: IProtocolSideEffectHandler = {
handleAction(action) {
agentService.dispatchAction(action, 'ws-server', 0);
},
async handleCreateSession(command) {
await agentService.createSession({
provider: command.provider as AgentProvider | undefined,
model: command.model,
workingDirectory: command.workingDirectory,
});
},
handleDisposeSession(session) {
agentService.disposeSession(session);
},
async handleListSessions() {
const sessions = await agentService.listSessions();
return sessions.map(s => ({
resource: s.session,
provider: '' as AgentProvider,
title: s.summary ?? 'Session',
status: SessionStatus.Idle,
createdAt: s.startTime,
modifiedAt: s.modifiedTime,
}));
},
};
// Wire up protocol handler
disposables.add(new ProtocolServerHandler(agentService.stateManager, wsServer, sideEffects, logService));
// Report ready
const address = wsServer.address;
if (address) {
const listeningPort = address.split(':').pop();
process.stdout.write(`READY:${listeningPort}\n`);
logService.info(`[AgentHostServer] WebSocket server listening on ws://${address}`);
} else {
const interval = setInterval(() => {
const addr = wsServer.address;
if (addr) {
clearInterval(interval);
const listeningPort = addr.split(':').pop();
process.stdout.write(`READY:${listeningPort}\n`);
logService.info(`[AgentHostServer] WebSocket server listening on ws://${addr}`);
}
}, 10);
}
// Keep alive until stdin closes or signal
process.stdin.resume();
process.stdin.on('end', shutdown);
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
function shutdown(): void {
logService.info('[AgentHostServer] Shutting down...');
disposables.dispose();
loggerService?.dispose();
process.exit(0);
}
}
main();