@agentxjs/server 1.9.1-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/server.ts ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * AgentX Server - Standalone server startup script
4
+ *
5
+ * Usage:
6
+ * ANTHROPIC_API_KEY=sk-xxx bun run bin/server.ts
7
+ *
8
+ * Environment variables:
9
+ * ANTHROPIC_API_KEY - Required: Claude API key
10
+ * ANTHROPIC_BASE_URL - Optional: Custom API endpoint
11
+ * PORT - Server port (default: 5200)
12
+ * HOST - Server host (default: 0.0.0.0)
13
+ * DATA_PATH - Data storage path (default: ./data)
14
+ * LOG_LEVEL - Log level: debug/info/warn/error (default: info)
15
+ */
16
+
17
+ import { createServer } from "../src";
18
+ import { nodeProvider } from "@agentxjs/node-provider";
19
+ import { createClaudeDriverFactory } from "@agentxjs/claude-driver";
20
+ import { createLogger } from "commonxjs/logger";
21
+
22
+ const logger = createLogger("server/bin");
23
+
24
+ async function main() {
25
+ // Validate API key
26
+ const apiKey = process.env.ANTHROPIC_API_KEY;
27
+ if (!apiKey) {
28
+ console.error("Error: ANTHROPIC_API_KEY environment variable is required");
29
+ console.error("");
30
+ console.error("Usage:");
31
+ console.error(" ANTHROPIC_API_KEY=sk-xxx bun run bin/server.ts");
32
+ process.exit(1);
33
+ }
34
+
35
+ // Configuration from environment
36
+ const port = parseInt(process.env.PORT ?? "5200", 10);
37
+ const host = process.env.HOST ?? "0.0.0.0";
38
+ const dataPath = process.env.DATA_PATH ?? "./data";
39
+ const logDir = process.env.LOG_DIR ?? `${dataPath}/logs`;
40
+ const debug = process.env.LOG_LEVEL === "debug";
41
+
42
+ logger.info("Starting AgentX Server", {
43
+ port,
44
+ host,
45
+ dataPath,
46
+ logDir,
47
+ debug,
48
+ });
49
+
50
+ // Create driver factory
51
+ const driverFactory = createClaudeDriverFactory();
52
+
53
+ // Create server with nodeProvider
54
+ const server = await createServer({
55
+ provider: nodeProvider({
56
+ dataPath,
57
+ driverFactory,
58
+ logDir,
59
+ }),
60
+ port,
61
+ host,
62
+ debug,
63
+ });
64
+
65
+ // Start listening
66
+ await server.listen();
67
+
68
+ logger.info("AgentX Server started", {
69
+ url: `ws://${host}:${port}`,
70
+ });
71
+
72
+ // Handle shutdown
73
+ const shutdown = async () => {
74
+ logger.info("Shutting down...");
75
+ await server.dispose();
76
+ process.exit(0);
77
+ };
78
+
79
+ process.on("SIGINT", shutdown);
80
+ process.on("SIGTERM", shutdown);
81
+ }
82
+
83
+ main().catch((err) => {
84
+ console.error("Failed to start server:", err);
85
+ process.exit(1);
86
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@agentxjs/server",
3
+ "version": "1.9.1-dev",
4
+ "description": "AgentX Server - WebSocket server with Provider support",
5
+ "type": "module",
6
+ "bin": {
7
+ "agentx-server": "./bin/server.ts"
8
+ },
9
+ "exports": {
10
+ ".": "./src/index.ts"
11
+ },
12
+ "scripts": {
13
+ "dev": "bun run bin/server.ts",
14
+ "typecheck": "tsc --noEmit",
15
+ "test": "bun test"
16
+ },
17
+ "dependencies": {
18
+ "@agentxjs/core": "workspace:*",
19
+ "@agentxjs/node-provider": "workspace:*",
20
+ "@agentxjs/claude-driver": "workspace:*",
21
+ "commonxjs": "^0.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.3.3"
25
+ }
26
+ }
@@ -0,0 +1,423 @@
1
+ /**
2
+ * CommandHandler - Handles JSON-RPC requests directly
3
+ *
4
+ * No longer uses EventBus for request/response. Instead:
5
+ * - Receives RPC requests directly
6
+ * - Returns RPC responses directly
7
+ * - EventBus is only used for stream events (notifications)
8
+ */
9
+
10
+ import type { AgentXRuntime } from "@agentxjs/core/runtime";
11
+ import type { UserContentPart } from "@agentxjs/core/agent";
12
+ import type { RpcMethod } from "@agentxjs/core/network";
13
+ import { createLogger } from "commonxjs/logger";
14
+
15
+ const logger = createLogger("server/CommandHandler");
16
+
17
+ /**
18
+ * RPC Result type
19
+ */
20
+ export interface RpcResult<T = unknown> {
21
+ success: true;
22
+ data: T;
23
+ }
24
+
25
+ export interface RpcError {
26
+ success: false;
27
+ code: number;
28
+ message: string;
29
+ }
30
+
31
+ export type RpcResponse<T = unknown> = RpcResult<T> | RpcError;
32
+
33
+ /**
34
+ * Helper to create success result
35
+ */
36
+ function ok<T>(data: T): RpcResult<T> {
37
+ return { success: true, data };
38
+ }
39
+
40
+ /**
41
+ * Helper to create error result
42
+ */
43
+ function err(code: number, message: string): RpcError {
44
+ return { success: false, code, message };
45
+ }
46
+
47
+ /**
48
+ * CommandHandler - Processes RPC requests directly
49
+ */
50
+ export class CommandHandler {
51
+ private readonly runtime: AgentXRuntime;
52
+
53
+ constructor(runtime: AgentXRuntime) {
54
+ this.runtime = runtime;
55
+ logger.debug("CommandHandler created");
56
+ }
57
+
58
+ /**
59
+ * Handle an RPC request and return response
60
+ */
61
+ async handle(method: RpcMethod, params: unknown): Promise<RpcResponse> {
62
+ logger.debug("Handling RPC request", { method });
63
+
64
+ try {
65
+ switch (method) {
66
+ // Container
67
+ case "container.create":
68
+ return await this.handleContainerCreate(params);
69
+ case "container.get":
70
+ return await this.handleContainerGet(params);
71
+ case "container.list":
72
+ return await this.handleContainerList(params);
73
+
74
+ // Image
75
+ case "image.create":
76
+ return await this.handleImageCreate(params);
77
+ case "image.get":
78
+ return await this.handleImageGet(params);
79
+ case "image.list":
80
+ return await this.handleImageList(params);
81
+ case "image.delete":
82
+ return await this.handleImageDelete(params);
83
+ case "image.run":
84
+ return await this.handleImageRun(params);
85
+ case "image.stop":
86
+ return await this.handleImageStop(params);
87
+ case "image.update":
88
+ return await this.handleImageUpdate(params);
89
+ case "image.messages":
90
+ return await this.handleImageMessages(params);
91
+
92
+ // Agent
93
+ case "agent.get":
94
+ return await this.handleAgentGet(params);
95
+ case "agent.list":
96
+ return await this.handleAgentList(params);
97
+ case "agent.destroy":
98
+ return await this.handleAgentDestroy(params);
99
+ case "agent.destroyAll":
100
+ return await this.handleAgentDestroyAll(params);
101
+ case "agent.interrupt":
102
+ return await this.handleAgentInterrupt(params);
103
+
104
+ // Message
105
+ case "message.send":
106
+ return await this.handleMessageSend(params);
107
+
108
+ default:
109
+ return err(-32601, `Method not found: ${method}`);
110
+ }
111
+ } catch (error) {
112
+ logger.error("RPC handler error", { method, error });
113
+ return err(-32000, error instanceof Error ? error.message : String(error));
114
+ }
115
+ }
116
+
117
+ // ==================== Container Commands ====================
118
+
119
+ private async handleContainerCreate(params: unknown): Promise<RpcResponse> {
120
+ const { containerId } = params as { containerId: string };
121
+ const { getOrCreateContainer } = await import("@agentxjs/core/container");
122
+ const { containerRepository, imageRepository, sessionRepository } = this.runtime.provider;
123
+
124
+ const container = await getOrCreateContainer(containerId, {
125
+ containerRepository,
126
+ imageRepository,
127
+ sessionRepository,
128
+ });
129
+
130
+ return ok({ containerId: container.containerId });
131
+ }
132
+
133
+ private async handleContainerGet(params: unknown): Promise<RpcResponse> {
134
+ const { containerId } = params as { containerId: string };
135
+ const exists = await this.runtime.provider.containerRepository.containerExists(containerId);
136
+ return ok({ containerId, exists });
137
+ }
138
+
139
+ private async handleContainerList(_params: unknown): Promise<RpcResponse> {
140
+ const containers = await this.runtime.provider.containerRepository.findAllContainers();
141
+ return ok({ containerIds: containers.map((c) => c.containerId) });
142
+ }
143
+
144
+ // ==================== Image Commands ====================
145
+
146
+ private async handleImageCreate(params: unknown): Promise<RpcResponse> {
147
+ const { containerId, name, description, systemPrompt, mcpServers } = params as {
148
+ containerId: string;
149
+ name?: string;
150
+ description?: string;
151
+ systemPrompt?: string;
152
+ mcpServers?: Record<string, unknown>;
153
+ };
154
+
155
+ const { imageRepository, sessionRepository } = this.runtime.provider;
156
+ const { createImage } = await import("@agentxjs/core/image");
157
+
158
+ const image = await createImage(
159
+ { containerId, name, description, systemPrompt, mcpServers: mcpServers as any },
160
+ { imageRepository, sessionRepository }
161
+ );
162
+
163
+ return ok({
164
+ record: image.toRecord(),
165
+ __subscriptions: [image.sessionId],
166
+ });
167
+ }
168
+
169
+ private async handleImageGet(params: unknown): Promise<RpcResponse> {
170
+ const { imageId } = params as { imageId: string };
171
+ const record = await this.runtime.provider.imageRepository.findImageById(imageId);
172
+ return ok({
173
+ record,
174
+ __subscriptions: record?.sessionId ? [record.sessionId] : undefined,
175
+ });
176
+ }
177
+
178
+ private async handleImageList(params: unknown): Promise<RpcResponse> {
179
+ const { containerId } = params as { containerId?: string };
180
+ const records = containerId
181
+ ? await this.runtime.provider.imageRepository.findImagesByContainerId(containerId)
182
+ : await this.runtime.provider.imageRepository.findAllImages();
183
+
184
+ return ok({
185
+ records,
186
+ __subscriptions: records.map((r) => r.sessionId),
187
+ });
188
+ }
189
+
190
+ private async handleImageDelete(params: unknown): Promise<RpcResponse> {
191
+ const { imageId } = params as { imageId: string };
192
+ const { loadImage } = await import("@agentxjs/core/image");
193
+ const { imageRepository, sessionRepository } = this.runtime.provider;
194
+
195
+ const image = await loadImage(imageId, { imageRepository, sessionRepository });
196
+ if (image) {
197
+ await image.delete();
198
+ }
199
+
200
+ return ok({ imageId });
201
+ }
202
+
203
+ private async handleImageRun(params: unknown): Promise<RpcResponse> {
204
+ const { imageId, agentId: requestedAgentId } = params as {
205
+ imageId: string;
206
+ agentId?: string;
207
+ };
208
+
209
+ // Check if already have a running agent for this image
210
+ const existingAgent = this.runtime
211
+ .getAgents()
212
+ .find((a) => a.imageId === imageId && a.lifecycle === "running");
213
+
214
+ if (existingAgent) {
215
+ logger.debug("Reusing existing agent for image", {
216
+ imageId,
217
+ agentId: existingAgent.agentId,
218
+ });
219
+ return ok({
220
+ imageId,
221
+ agentId: existingAgent.agentId,
222
+ sessionId: existingAgent.sessionId,
223
+ containerId: existingAgent.containerId,
224
+ reused: true,
225
+ });
226
+ }
227
+
228
+ // Create new agent (with optional custom agentId)
229
+ const agent = await this.runtime.createAgent({
230
+ imageId,
231
+ agentId: requestedAgentId,
232
+ });
233
+ logger.info("Created new agent for image", {
234
+ imageId,
235
+ agentId: agent.agentId,
236
+ });
237
+
238
+ return ok({
239
+ imageId,
240
+ agentId: agent.agentId,
241
+ sessionId: agent.sessionId,
242
+ containerId: agent.containerId,
243
+ reused: false,
244
+ });
245
+ }
246
+
247
+ private async handleImageStop(params: unknown): Promise<RpcResponse> {
248
+ const { imageId } = params as { imageId: string };
249
+
250
+ // Find running agent for this image
251
+ const agent = this.runtime
252
+ .getAgents()
253
+ .find((a) => a.imageId === imageId && a.lifecycle === "running");
254
+
255
+ if (agent) {
256
+ await this.runtime.stopAgent(agent.agentId);
257
+ logger.info("Stopped agent for image", { imageId, agentId: agent.agentId });
258
+ } else {
259
+ logger.debug("No running agent found for image", { imageId });
260
+ }
261
+
262
+ return ok({ imageId });
263
+ }
264
+
265
+ private async handleImageUpdate(params: unknown): Promise<RpcResponse> {
266
+ const { imageId, updates } = params as {
267
+ imageId: string;
268
+ updates: { name?: string; description?: string };
269
+ };
270
+
271
+ // Get existing image
272
+ const imageRecord = await this.runtime.provider.imageRepository.findImageById(imageId);
273
+ if (!imageRecord) {
274
+ return err(404, `Image not found: ${imageId}`);
275
+ }
276
+
277
+ // Update image record
278
+ const updatedRecord = {
279
+ ...imageRecord,
280
+ ...updates,
281
+ updatedAt: Date.now(),
282
+ };
283
+
284
+ await this.runtime.provider.imageRepository.saveImage(updatedRecord);
285
+
286
+ logger.info("Updated image", { imageId, updates });
287
+
288
+ return ok({ record: updatedRecord });
289
+ }
290
+
291
+ private async handleImageMessages(params: unknown): Promise<RpcResponse> {
292
+ const { imageId } = params as { imageId: string };
293
+
294
+ // Get image record to find sessionId
295
+ const imageRecord = await this.runtime.provider.imageRepository.findImageById(imageId);
296
+ if (!imageRecord) {
297
+ return err(404, `Image not found: ${imageId}`);
298
+ }
299
+
300
+ // Get messages from session
301
+ const messages = await this.runtime.provider.sessionRepository.getMessages(
302
+ imageRecord.sessionId
303
+ );
304
+
305
+ logger.debug("Got messages for image", { imageId, count: messages.length });
306
+
307
+ return ok({ imageId, messages });
308
+ }
309
+
310
+ // ==================== Agent Commands ====================
311
+
312
+ private async handleAgentGet(params: unknown): Promise<RpcResponse> {
313
+ const { agentId } = params as { agentId: string };
314
+ const agent = this.runtime.getAgent(agentId);
315
+
316
+ return ok({
317
+ agent: agent
318
+ ? {
319
+ agentId: agent.agentId,
320
+ imageId: agent.imageId,
321
+ containerId: agent.containerId,
322
+ sessionId: agent.sessionId,
323
+ lifecycle: agent.lifecycle,
324
+ }
325
+ : null,
326
+ exists: !!agent,
327
+ });
328
+ }
329
+
330
+ private async handleAgentList(params: unknown): Promise<RpcResponse> {
331
+ const { containerId } = params as { containerId?: string };
332
+ const agents = containerId
333
+ ? this.runtime.getAgentsByContainer(containerId)
334
+ : this.runtime.getAgents();
335
+
336
+ return ok({
337
+ agents: agents.map((a) => ({
338
+ agentId: a.agentId,
339
+ imageId: a.imageId,
340
+ containerId: a.containerId,
341
+ sessionId: a.sessionId,
342
+ lifecycle: a.lifecycle,
343
+ })),
344
+ });
345
+ }
346
+
347
+ private async handleAgentDestroy(params: unknown): Promise<RpcResponse> {
348
+ const { agentId } = params as { agentId: string };
349
+
350
+ // Check if agent exists first
351
+ const agent = this.runtime.getAgent(agentId);
352
+ if (!agent) {
353
+ return ok({ agentId, success: false });
354
+ }
355
+
356
+ await this.runtime.destroyAgent(agentId);
357
+ return ok({ agentId, success: true });
358
+ }
359
+
360
+ private async handleAgentDestroyAll(params: unknown): Promise<RpcResponse> {
361
+ const { containerId } = params as { containerId: string };
362
+ const agents = this.runtime.getAgentsByContainer(containerId);
363
+ for (const agent of agents) {
364
+ await this.runtime.destroyAgent(agent.agentId);
365
+ }
366
+ return ok({ containerId });
367
+ }
368
+
369
+ private async handleAgentInterrupt(params: unknown): Promise<RpcResponse> {
370
+ const { agentId } = params as { agentId: string };
371
+ this.runtime.interrupt(agentId);
372
+ return ok({ agentId });
373
+ }
374
+
375
+ // ==================== Message Commands ====================
376
+
377
+ private async handleMessageSend(params: unknown): Promise<RpcResponse> {
378
+ const { agentId, imageId, content } = params as {
379
+ agentId?: string;
380
+ imageId?: string;
381
+ content: string | UserContentPart[];
382
+ };
383
+
384
+ let targetAgentId: string;
385
+
386
+ if (agentId) {
387
+ // Direct agent reference
388
+ targetAgentId = agentId;
389
+ } else if (imageId) {
390
+ // Auto-activate image: find or create agent
391
+ const existingAgent = this.runtime
392
+ .getAgents()
393
+ .find((a) => a.imageId === imageId && a.lifecycle === "running");
394
+
395
+ if (existingAgent) {
396
+ targetAgentId = existingAgent.agentId;
397
+ logger.debug("Using existing agent for message", {
398
+ imageId,
399
+ agentId: targetAgentId,
400
+ });
401
+ } else {
402
+ // Create new agent for this image
403
+ const agent = await this.runtime.createAgent({ imageId });
404
+ targetAgentId = agent.agentId;
405
+ logger.info("Auto-created agent for message", {
406
+ imageId,
407
+ agentId: targetAgentId,
408
+ });
409
+ }
410
+ } else {
411
+ return err(-32602, "Either agentId or imageId is required");
412
+ }
413
+
414
+ await this.runtime.receive(targetAgentId, content);
415
+ return ok({ agentId: targetAgentId, imageId });
416
+ }
417
+
418
+ // ==================== Lifecycle ====================
419
+
420
+ dispose(): void {
421
+ logger.debug("CommandHandler disposed");
422
+ }
423
+ }
package/src/Server.ts ADDED
@@ -0,0 +1,350 @@
1
+ /**
2
+ * AgentX Server Implementation (JSON-RPC 2.0)
3
+ *
4
+ * Creates a WebSocket server that:
5
+ * 1. Accepts client connections
6
+ * 2. Handles JSON-RPC requests directly via CommandHandler
7
+ * 3. Broadcasts stream events as JSON-RPC notifications
8
+ *
9
+ * Message Types:
10
+ * - RPC Request (has id): Client → Server → Client (direct response)
11
+ * - RPC Notification (no id): Server → Client (stream events)
12
+ */
13
+
14
+ import type { AgentXProvider } from "@agentxjs/core/runtime";
15
+ import type { ChannelConnection } from "@agentxjs/core/network";
16
+ import type { BusEvent, SystemEvent } from "@agentxjs/core/event";
17
+ import { createAgentXRuntime } from "@agentxjs/core/runtime";
18
+ import {
19
+ parseMessage,
20
+ isRequest,
21
+ isNotification,
22
+ createSuccessResponse,
23
+ createErrorResponse,
24
+ createStreamEvent,
25
+ RpcErrorCodes,
26
+ type RpcMethod,
27
+ } from "@agentxjs/core/network";
28
+ import {
29
+ WebSocketServer,
30
+ isDeferredProvider,
31
+ type DeferredProviderConfig,
32
+ } from "@agentxjs/node-provider";
33
+ import { createLogger } from "commonxjs/logger";
34
+ import { CommandHandler } from "./CommandHandler";
35
+ import type { AgentXServer } from "./types";
36
+
37
+ const logger = createLogger("server/Server");
38
+
39
+ /**
40
+ * Connection state
41
+ */
42
+ interface ConnectionState {
43
+ connection: ChannelConnection;
44
+ subscribedTopics: Set<string>;
45
+ }
46
+
47
+ /**
48
+ * Server configuration (supports both immediate and deferred providers)
49
+ */
50
+ export interface ServerConfig {
51
+ /**
52
+ * AgentX Provider (can be AgentXProvider or DeferredProviderConfig)
53
+ */
54
+ provider: AgentXProvider | DeferredProviderConfig;
55
+
56
+ /**
57
+ * Port to listen on (standalone mode)
58
+ */
59
+ port?: number;
60
+
61
+ /**
62
+ * Host to bind to (default: "0.0.0.0")
63
+ */
64
+ host?: string;
65
+
66
+ /**
67
+ * Existing HTTP server to attach to (attached mode)
68
+ */
69
+ server?: import("@agentxjs/core/network").MinimalHTTPServer;
70
+
71
+ /**
72
+ * WebSocket path when attached (default: "/ws")
73
+ */
74
+ wsPath?: string;
75
+
76
+ /**
77
+ * Enable debug logging
78
+ */
79
+ debug?: boolean;
80
+ }
81
+
82
+ /**
83
+ * Create an AgentX server
84
+ */
85
+ export async function createServer(config: ServerConfig): Promise<AgentXServer> {
86
+ const { wsPath = "/ws" } = config;
87
+
88
+ // Resolve deferred provider if needed
89
+ const provider: AgentXProvider = isDeferredProvider(config.provider)
90
+ ? await config.provider.resolve()
91
+ : config.provider;
92
+
93
+ // Create runtime from provider
94
+ const runtime = createAgentXRuntime(provider);
95
+
96
+ // Create WebSocket server
97
+ const wsServer = new WebSocketServer({
98
+ heartbeat: true,
99
+ heartbeatInterval: 30000,
100
+ debug: config.debug,
101
+ });
102
+
103
+ // Create command handler (no longer needs eventBus)
104
+ const commandHandler = new CommandHandler(runtime);
105
+
106
+ // Track connections
107
+ const connections = new Map<string, ConnectionState>();
108
+
109
+ /**
110
+ * Subscribe connection to a topic
111
+ */
112
+ function subscribeToTopic(connectionId: string, topic: string): void {
113
+ const state = connections.get(connectionId);
114
+ if (!state || state.subscribedTopics.has(topic)) return;
115
+
116
+ state.subscribedTopics.add(topic);
117
+ logger.debug("Connection subscribed to topic", { connectionId, topic });
118
+ }
119
+
120
+ /**
121
+ * Check if event should be sent to connection based on subscriptions
122
+ */
123
+ function shouldSendToConnection(state: ConnectionState, event: BusEvent): boolean {
124
+ // Skip internal driver events
125
+ if (event.source === "driver" && event.intent !== "notification") {
126
+ return false;
127
+ }
128
+
129
+ // Skip command events (they are handled via RPC, not broadcast)
130
+ if (event.source === "command") {
131
+ return false;
132
+ }
133
+
134
+ // Check if subscribed to event's session
135
+ const eventWithContext = event as BusEvent & { context?: { sessionId?: string } };
136
+ const sessionId = eventWithContext.context?.sessionId;
137
+ if (sessionId && state.subscribedTopics.has(sessionId)) {
138
+ return true;
139
+ }
140
+
141
+ // Send to global subscribers
142
+ return state.subscribedTopics.has("global");
143
+ }
144
+
145
+ /**
146
+ * Send JSON-RPC response to a specific connection
147
+ */
148
+ function sendResponse(connection: ChannelConnection, id: string | number, result: unknown): void {
149
+ const response = createSuccessResponse(id, result);
150
+ connection.send(JSON.stringify(response));
151
+ }
152
+
153
+ /**
154
+ * Send JSON-RPC error to a specific connection
155
+ */
156
+ function sendError(
157
+ connection: ChannelConnection,
158
+ id: string | number | null,
159
+ code: number,
160
+ message: string
161
+ ): void {
162
+ const response = createErrorResponse(id, code, message);
163
+ connection.send(JSON.stringify(response));
164
+ }
165
+
166
+ // Handle new connections
167
+ wsServer.onConnection((connection) => {
168
+ const state: ConnectionState = {
169
+ connection,
170
+ subscribedTopics: new Set(["global"]),
171
+ };
172
+ connections.set(connection.id, state);
173
+
174
+ logger.info("Client connected", {
175
+ connectionId: connection.id,
176
+ totalConnections: connections.size,
177
+ });
178
+
179
+ // Handle messages from client
180
+ connection.onMessage(async (message) => {
181
+ try {
182
+ const parsed = parseMessage(message);
183
+
184
+ // Handle single message (not batch)
185
+ if (!Array.isArray(parsed)) {
186
+ await handleParsedMessage(connection, state, parsed);
187
+ } else {
188
+ // Handle batch (not common, but supported by JSON-RPC 2.0)
189
+ for (const item of parsed) {
190
+ await handleParsedMessage(connection, state, item);
191
+ }
192
+ }
193
+ } catch (err) {
194
+ logger.error("Failed to parse message", { error: (err as Error).message });
195
+ sendError(connection, null, RpcErrorCodes.PARSE_ERROR, "Parse error");
196
+ }
197
+ });
198
+
199
+ // Cleanup on disconnect
200
+ connection.onClose(() => {
201
+ connections.delete(connection.id);
202
+ logger.info("Client disconnected", {
203
+ connectionId: connection.id,
204
+ totalConnections: connections.size,
205
+ });
206
+ });
207
+ });
208
+
209
+ /**
210
+ * Handle a parsed JSON-RPC message
211
+ */
212
+ async function handleParsedMessage(
213
+ connection: ChannelConnection,
214
+ state: ConnectionState,
215
+ parsed: import("jsonrpc-lite").IParsedObject
216
+ ): Promise<void> {
217
+ if (isRequest(parsed)) {
218
+ // JSON-RPC Request - handle and respond directly
219
+ const payload = parsed.payload as {
220
+ id: string | number;
221
+ method: string;
222
+ params: unknown;
223
+ };
224
+ const { id, method, params } = payload;
225
+
226
+ logger.debug("Received RPC request", { id, method });
227
+
228
+ // Call command handler
229
+ const result = await commandHandler.handle(method as RpcMethod, params);
230
+
231
+ if (result.success) {
232
+ sendResponse(connection, id, result.data);
233
+ } else {
234
+ sendError(connection, id, result.code, result.message);
235
+ }
236
+ } else if (isNotification(parsed)) {
237
+ // JSON-RPC Notification - control messages
238
+ const payload = parsed.payload as {
239
+ method: string;
240
+ params: unknown;
241
+ };
242
+ const { method, params } = payload;
243
+
244
+ logger.debug("Received notification", { method });
245
+
246
+ if (method === "subscribe") {
247
+ const { topic } = params as { topic: string };
248
+ subscribeToTopic(connection.id, topic);
249
+ } else if (method === "unsubscribe") {
250
+ const { topic } = params as { topic: string };
251
+ state.subscribedTopics.delete(topic);
252
+ logger.debug("Connection unsubscribed from topic", { connectionId: connection.id, topic });
253
+ } else if (method === "control.ack") {
254
+ // ACK for reliable delivery - handled by network layer
255
+ logger.debug("Received ACK notification");
256
+ }
257
+ } else {
258
+ // Invalid message
259
+ logger.warn("Received invalid JSON-RPC message");
260
+ }
261
+ }
262
+
263
+ // Route internal events to connected clients as JSON-RPC notifications
264
+ provider.eventBus.onAny((event) => {
265
+ // Only broadcast broadcastable events
266
+ if (!shouldBroadcastEvent(event)) {
267
+ return;
268
+ }
269
+
270
+ // Get topic from event context
271
+ const eventWithContext = event as BusEvent & { context?: { sessionId?: string } };
272
+ const topic = eventWithContext.context?.sessionId || "global";
273
+
274
+ // Wrap as JSON-RPC notification
275
+ const notification = createStreamEvent(topic, event as SystemEvent);
276
+ const message = JSON.stringify(notification);
277
+
278
+ for (const [connectionId, state] of connections) {
279
+ if (shouldSendToConnection(state, event)) {
280
+ state.connection.sendReliable(message, {
281
+ timeout: 10000,
282
+ onTimeout: () => {
283
+ logger.warn("Event ACK timeout", {
284
+ connectionId,
285
+ eventType: event.type,
286
+ });
287
+ },
288
+ });
289
+ }
290
+ }
291
+ });
292
+
293
+ /**
294
+ * Check if event should be broadcast
295
+ */
296
+ function shouldBroadcastEvent(event: BusEvent): boolean {
297
+ // Skip internal driver events
298
+ if (event.source === "driver" && event.intent !== "notification") {
299
+ return false;
300
+ }
301
+
302
+ // Skip command events (handled via RPC)
303
+ if (event.source === "command") {
304
+ return false;
305
+ }
306
+
307
+ // Check broadcastable flag
308
+ const systemEvent = event as SystemEvent;
309
+ if (systemEvent.broadcastable === false) {
310
+ return false;
311
+ }
312
+
313
+ return true;
314
+ }
315
+
316
+ // Attach to existing server if provided
317
+ if (config.server) {
318
+ wsServer.attach(config.server, wsPath);
319
+ logger.info("WebSocket attached to existing server", { path: wsPath });
320
+ }
321
+
322
+ return {
323
+ async listen(port?: number, host?: string) {
324
+ if (config.server) {
325
+ throw new Error(
326
+ "Cannot listen when attached to existing server. The server should call listen() instead."
327
+ );
328
+ }
329
+
330
+ const listenPort = port ?? config.port ?? 5200;
331
+ const listenHost = host ?? config.host ?? "0.0.0.0";
332
+
333
+ await wsServer.listen(listenPort, listenHost);
334
+ logger.info("Server listening", { port: listenPort, host: listenHost });
335
+ },
336
+
337
+ async close() {
338
+ await wsServer.close();
339
+ logger.info("Server closed");
340
+ },
341
+
342
+ async dispose() {
343
+ // Cleanup in order
344
+ await wsServer.dispose();
345
+ commandHandler.dispose();
346
+ await runtime.shutdown();
347
+ logger.info("Server disposed");
348
+ },
349
+ };
350
+ }
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @agentxjs/server
3
+ *
4
+ * AgentX Server - WebSocket server with Provider support.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { createServer } from "@agentxjs/server";
9
+ * import { nodeProvider } from "@agentxjs/node-provider";
10
+ * import { claudeDriver } from "@agentxjs/claude-driver";
11
+ *
12
+ * const server = await createServer({
13
+ * provider: nodeProvider({
14
+ * dataPath: "./data",
15
+ * driver: claudeDriver({ apiKey: process.env.ANTHROPIC_API_KEY }),
16
+ * }),
17
+ * port: 5200,
18
+ * });
19
+ *
20
+ * await server.listen();
21
+ * console.log("Server listening on ws://localhost:5200");
22
+ *
23
+ * // Attach to existing HTTP server
24
+ * import { createServer as createHttpServer } from "node:http";
25
+ *
26
+ * const httpServer = createHttpServer();
27
+ * const agentxServer = await createServer({
28
+ * provider: nodeProvider({ ... }),
29
+ * server: httpServer,
30
+ * wsPath: "/ws",
31
+ * });
32
+ *
33
+ * httpServer.listen(3000);
34
+ * ```
35
+ */
36
+
37
+ export { createServer, type ServerConfig } from "./Server";
38
+ export type { AgentXServer } from "./types";
39
+ export { CommandHandler } from "./CommandHandler";
package/src/types.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Server Types
3
+ */
4
+
5
+ /**
6
+ * AgentX Server instance
7
+ */
8
+ export interface AgentXServer {
9
+ /**
10
+ * Start listening (standalone mode only)
11
+ */
12
+ listen(port?: number, host?: string): Promise<void>;
13
+
14
+ /**
15
+ * Close server and cleanup
16
+ */
17
+ close(): Promise<void>;
18
+
19
+ /**
20
+ * Dispose all resources
21
+ */
22
+ dispose(): Promise<void>;
23
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "outDir": "./dist",
6
+ "rootDir": "./src"
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["node_modules", "dist"]
10
+ }