mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-18 15:55:59 +01:00
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
170 lines
6.8 KiB
TypeScript
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();
|