agentxjs 2.0.2 → 2.0.4

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/src/index.ts CHANGED
@@ -1,146 +1,158 @@
1
1
  /**
2
2
  * agentxjs - AgentX Client SDK
3
3
  *
4
- * Unified entry point supporting local and remote modes.
4
+ * Fluent API supporting local, remote, and server modes.
5
5
  *
6
- * @example Local mode (embedded runtime)
6
+ * @example Local mode
7
7
  * ```typescript
8
8
  * import { createAgentX } from "agentxjs";
9
+ * import { node } from "@agentxjs/node-platform";
9
10
  *
10
- * const agentx = await createAgentX({
11
- * apiKey: process.env.ANTHROPIC_API_KEY,
12
- * provider: "anthropic",
13
- * });
14
- *
15
- * await agentx.containers.create("my-app");
16
- * const { record: image } = await agentx.images.create({
17
- * containerId: "my-app",
18
- * systemPrompt: "You are helpful",
19
- * });
20
- * const { agentId } = await agentx.agents.create({ imageId: image.imageId });
21
- *
22
- * agentx.on("text_delta", (e) => process.stdout.write(e.data.text));
23
- * await agentx.sessions.send(agentId, "Hello!");
11
+ * const ax = createAgentX(node({ createDriver }));
12
+ * await ax.agent.create({ imageId: "..." });
24
13
  * ```
25
14
  *
26
- * @example Remote mode (WebSocket client)
15
+ * @example Remote mode
27
16
  * ```typescript
28
- * import { createAgentX } from "agentxjs";
17
+ * const ax = createAgentX();
18
+ * const client = await ax.connect("ws://localhost:5200");
19
+ * ```
29
20
  *
30
- * const agentx = await createAgentX({
31
- * serverUrl: "ws://localhost:5200",
32
- * });
21
+ * @example Server mode
22
+ * ```typescript
23
+ * const ax = createAgentX(node({ createDriver }));
24
+ * const server = await ax.serve({ port: 5200 });
33
25
  * ```
34
26
  */
35
27
 
28
+ import type { CreateDriver } from "@agentxjs/core/driver";
29
+ import type { AgentXPlatform } from "@agentxjs/core/runtime";
30
+ import { createAgentXRuntime } from "@agentxjs/core/runtime";
36
31
  import { LocalClient } from "./LocalClient";
37
32
  import { RemoteClient } from "./RemoteClient";
38
- import type { AgentX, AgentXConfig } from "./types";
33
+ import type { AgentX, AgentXBuilder, AgentXServer, ConnectOptions, ServeConfig } from "./types";
39
34
 
40
35
  /**
41
- * Create an AgentX client
42
- *
43
- * Mode detection:
44
- * - `serverUrl` present → **Remote mode** (WebSocket client)
45
- * - `apiKey` present → **Local mode** (embedded Runtime + MonoDriver)
46
- *
47
- * @param config - Client configuration
48
- * @returns Connected AgentX client
36
+ * Platform configuration for createAgentX
49
37
  */
50
- export async function createAgentX(config: AgentXConfig): Promise<AgentX> {
51
- if (config.serverUrl) {
52
- // Remote mode — resolve platform for WebSocket factory if needed
53
- const resolvedConfig = await resolvePlatformForRemote(config);
54
- const client = new RemoteClient(resolvedConfig);
55
- await client.connect();
56
- return client;
57
- }
58
-
59
- if (config.apiKey || config.createDriver || config.customPlatform) {
60
- // Local mode
61
- return createLocalClient(config);
62
- }
63
-
64
- throw new Error(
65
- "Invalid AgentX config: provide either 'serverUrl' (remote mode) or 'apiKey' (local mode)"
66
- );
38
+ export interface PlatformConfig {
39
+ platform: AgentXPlatform;
40
+ createDriver: CreateDriver;
67
41
  }
68
42
 
69
43
  /**
70
- * Resolve platform for remote mode
44
+ * Create an AgentX builder
71
45
  *
72
- * In Node.js: auto-import node-platform to get channelClient
73
- * In browser: no platform needed (native WebSocket is the default)
46
+ * @param config - Platform configuration (optional). Without it, only connect() is available.
47
+ * @returns AgentXBuilder local AgentX + connect() + serve()
74
48
  */
75
- async function resolvePlatformForRemote(config: AgentXConfig): Promise<AgentXConfig> {
76
- if (config.customPlatform?.channelClient) {
77
- return config;
78
- }
49
+ export function createAgentX(config?: PlatformConfig): AgentXBuilder {
50
+ let localClient: LocalClient | null = null;
79
51
 
80
- // In browser, native WebSocket works — no platform needed
81
- if (typeof globalThis !== "undefined" && (globalThis as any).window?.document !== undefined) {
82
- return config;
52
+ function getLocalClient(): LocalClient {
53
+ if (localClient) return localClient;
54
+ if (!config) {
55
+ throw new Error(
56
+ "Local mode requires a platform. Pass a PlatformConfig to createAgentX(), or use connect() for remote mode."
57
+ );
58
+ }
59
+ const runtime = createAgentXRuntime(config.platform, config.createDriver);
60
+ localClient = new LocalClient(runtime);
61
+ return localClient;
83
62
  }
84
63
 
85
- // Node.js — auto-resolve channelClient from node-platform
86
- try {
87
- const { createNodeWebSocket } = await import("@agentxjs/node-platform/network");
88
- return {
89
- ...config,
90
- customPlatform: {
91
- ...config.customPlatform,
92
- channelClient: createNodeWebSocket,
93
- } as any,
94
- };
95
- } catch {
96
- // node-platform not available, fall back to global WebSocket
97
- return config;
64
+ if (config) {
65
+ getLocalClient();
98
66
  }
99
- }
100
67
 
101
- /**
102
- * Create a local client with embedded runtime
103
- */
104
- async function createLocalClient(config: AgentXConfig): Promise<AgentX> {
105
- const { createAgentXRuntime } = await import("@agentxjs/core/runtime");
106
-
107
- // Resolve platform
108
- let platform;
109
- if (config.customPlatform) {
110
- platform = config.customPlatform;
111
- } else {
112
- const { createNodePlatform } = await import("@agentxjs/node-platform");
113
- platform = await createNodePlatform({
114
- dataPath: config.dataPath ?? ":memory:",
115
- logLevel: config.logLevel ?? (config.debug ? "debug" : undefined),
116
- });
117
- }
68
+ return {
69
+ get connected() {
70
+ return localClient?.connected ?? false;
71
+ },
72
+
73
+ get events() {
74
+ return getLocalClient().events;
75
+ },
76
+
77
+ get container() {
78
+ return getLocalClient().container;
79
+ },
80
+
81
+ get image() {
82
+ return getLocalClient().image;
83
+ },
84
+
85
+ get agent() {
86
+ return getLocalClient().agent;
87
+ },
88
+
89
+ get session() {
90
+ return getLocalClient().session;
91
+ },
92
+
93
+ get presentation() {
94
+ return getLocalClient().presentation;
95
+ },
118
96
 
119
- // Resolve createDriver
120
- let createDriver = config.createDriver;
121
- if (!createDriver) {
122
- const { createMonoDriver } = await import("@agentxjs/mono-driver");
123
- createDriver = (driverConfig: import("@agentxjs/core/driver").DriverConfig) => {
124
- const existingOptions = (driverConfig as any).options ?? {};
125
- return createMonoDriver({
126
- ...driverConfig,
127
- apiKey: config.apiKey ?? driverConfig.apiKey,
128
- baseUrl: config.baseUrl ?? driverConfig.baseUrl,
129
- model: config.model ?? driverConfig.model,
130
- options: {
131
- ...existingOptions,
132
- provider: config.provider ?? existingOptions.provider ?? "anthropic",
133
- },
97
+ on(type, handler) {
98
+ return getLocalClient().on(type, handler);
99
+ },
100
+
101
+ onAny(handler) {
102
+ return getLocalClient().onAny(handler);
103
+ },
104
+
105
+ subscribe(sessionId) {
106
+ getLocalClient().subscribe(sessionId);
107
+ },
108
+
109
+ async disconnect() {
110
+ await localClient?.disconnect();
111
+ },
112
+
113
+ async dispose() {
114
+ await localClient?.dispose();
115
+ localClient = null;
116
+ },
117
+
118
+ async connect(serverUrl: string, options?: ConnectOptions): Promise<AgentX> {
119
+ const remoteClient = new RemoteClient({
120
+ serverUrl,
121
+ headers: options?.headers as Record<string, string> | undefined,
122
+ context: options?.context,
123
+ timeout: options?.timeout,
124
+ autoReconnect: options?.autoReconnect,
125
+ customPlatform: config?.platform,
134
126
  });
135
- };
136
- }
127
+ await remoteClient.connect();
128
+ return remoteClient;
129
+ },
137
130
 
138
- // Create runtime
139
- const runtime = createAgentXRuntime(platform, createDriver);
131
+ async serve(serveConfig?: ServeConfig): Promise<AgentXServer> {
132
+ if (!config) {
133
+ throw new Error("serve() requires a platform. Pass a PlatformConfig to createAgentX().");
134
+ }
135
+ if (!config.platform.channelServer) {
136
+ throw new Error(
137
+ "serve() requires platform.channelServer. Ensure your platform supports server mode."
138
+ );
139
+ }
140
140
 
141
- return new LocalClient(runtime);
141
+ const { createServer } = await import("./server");
142
+ return createServer({
143
+ platform: config.platform,
144
+ createDriver: config.createDriver,
145
+ port: serveConfig?.port,
146
+ host: serveConfig?.host,
147
+ server: serveConfig?.server as any,
148
+ wsPath: serveConfig?.wsPath,
149
+ });
150
+ },
151
+ };
142
152
  }
143
153
 
154
+ // Re-export server
155
+ export { CommandHandler } from "./CommandHandler";
144
156
  // Re-export Presentation types and classes
145
157
  export type {
146
158
  AssistantConversation,
@@ -164,6 +176,7 @@ export {
164
176
  Presentation,
165
177
  presentationReducer,
166
178
  } from "./presentation";
179
+ export { createServer, type ServerConfig } from "./server";
167
180
  // Re-export types
168
181
  export type {
169
182
  AgentCreateResponse,
@@ -172,8 +185,10 @@ export type {
172
185
  AgentListResponse,
173
186
  AgentNamespace,
174
187
  AgentX,
175
- AgentXConfig,
188
+ AgentXBuilder,
189
+ AgentXServer,
176
190
  BaseResponse,
191
+ ConnectOptions,
177
192
  ContainerCreateResponse,
178
193
  ContainerGetResponse,
179
194
  ContainerInfo,
@@ -184,9 +199,9 @@ export type {
184
199
  ImageListResponse,
185
200
  ImageNamespace,
186
201
  ImageRecord,
187
- LLMProvider,
188
202
  MaybeAsync,
189
203
  MessageSendResponse,
190
204
  PresentationNamespace,
205
+ ServeConfig,
191
206
  SessionNamespace,
192
207
  } from "./types";
@@ -14,7 +14,7 @@ import type { AgentX, PresentationNamespace } from "../types";
14
14
  export function createPresentations(agentx: AgentX): PresentationNamespace {
15
15
  return {
16
16
  async create(agentId: string, options?: PresentationOptions): Promise<Presentation> {
17
- const messages = await agentx.sessions.getMessages(agentId);
17
+ const messages = await agentx.session.getMessages(agentId);
18
18
  const conversations = messagesToConversations(messages);
19
19
  return new Presentation(agentx, agentId, options, conversations);
20
20
  },
@@ -110,7 +110,7 @@ export class Presentation {
110
110
 
111
111
  try {
112
112
  // Send message via agentx
113
- await this.agentx.sessions.send(this.agentId, content);
113
+ await this.agentx.session.send(this.agentId, content);
114
114
  } catch (error) {
115
115
  this.notifyError(error instanceof Error ? error : new Error(String(error)));
116
116
  }
@@ -121,7 +121,7 @@ export class Presentation {
121
121
  */
122
122
  async interrupt(): Promise<void> {
123
123
  try {
124
- await this.agentx.sessions.interrupt(this.agentId);
124
+ await this.agentx.session.interrupt(this.agentId);
125
125
  } catch (error) {
126
126
  this.notifyError(error instanceof Error ? error : new Error(String(error)));
127
127
  }
package/src/server.ts ADDED
@@ -0,0 +1,346 @@
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 { CreateDriver } from "@agentxjs/core/driver";
15
+ import type { BusEvent, SystemEvent } from "@agentxjs/core/event";
16
+ import type { ChannelConnection } from "@agentxjs/core/network";
17
+ import {
18
+ createErrorResponse,
19
+ createStreamEvent,
20
+ createSuccessResponse,
21
+ isNotification,
22
+ isRequest,
23
+ parseMessage,
24
+ RpcErrorCodes,
25
+ type RpcMethod,
26
+ } from "@agentxjs/core/network";
27
+ import type { AgentXPlatform } from "@agentxjs/core/runtime";
28
+ import { createAgentXRuntime } from "@agentxjs/core/runtime";
29
+ import { createLogger } from "@deepracticex/logger";
30
+ import { CommandHandler } from "./CommandHandler";
31
+ import type { AgentXServer } from "./types";
32
+
33
+ const logger = createLogger("server/Server");
34
+
35
+ /**
36
+ * Connection state
37
+ */
38
+ interface ConnectionState {
39
+ connection: ChannelConnection;
40
+ subscribedTopics: Set<string>;
41
+ }
42
+
43
+ /**
44
+ * Server configuration (supports both immediate and deferred platforms)
45
+ */
46
+ export interface ServerConfig {
47
+ /**
48
+ * AgentX Platform — must provide `channelServer` for accepting WebSocket connections.
49
+ */
50
+ platform: AgentXPlatform;
51
+
52
+ /**
53
+ * LLM Driver factory function - creates Driver per Agent
54
+ */
55
+ createDriver: CreateDriver;
56
+
57
+ /**
58
+ * Port to listen on (standalone mode)
59
+ */
60
+ port?: number;
61
+
62
+ /**
63
+ * Host to bind to (default: "0.0.0.0")
64
+ */
65
+ host?: string;
66
+
67
+ /**
68
+ * Existing HTTP server to attach to (attached mode)
69
+ */
70
+ server?: import("@agentxjs/core/network").MinimalHTTPServer;
71
+
72
+ /**
73
+ * WebSocket path when attached (default: "/ws")
74
+ */
75
+ wsPath?: string;
76
+
77
+ /**
78
+ * Enable debug logging
79
+ */
80
+ debug?: boolean;
81
+ }
82
+
83
+ /**
84
+ * Create an AgentX server
85
+ */
86
+ export async function createServer(config: ServerConfig): Promise<AgentXServer> {
87
+ const { wsPath = "/ws" } = config;
88
+ const platform = config.platform;
89
+
90
+ // Create runtime from platform + driver
91
+ const runtime = createAgentXRuntime(platform, config.createDriver);
92
+
93
+ // Get channel server from platform
94
+ const wsServer = platform.channelServer;
95
+ if (!wsServer) {
96
+ throw new Error("Platform must provide channelServer for server mode");
97
+ }
98
+
99
+ // Create command handler (no longer needs eventBus)
100
+ const commandHandler = new CommandHandler(runtime);
101
+
102
+ // Track connections
103
+ const connections = new Map<string, ConnectionState>();
104
+
105
+ /**
106
+ * Subscribe connection to a topic
107
+ */
108
+ function subscribeToTopic(connectionId: string, topic: string): void {
109
+ const state = connections.get(connectionId);
110
+ if (!state || state.subscribedTopics.has(topic)) return;
111
+
112
+ state.subscribedTopics.add(topic);
113
+ logger.debug("Connection subscribed to topic", { connectionId, topic });
114
+ }
115
+
116
+ /**
117
+ * Check if event should be sent to connection based on subscriptions
118
+ */
119
+ function shouldSendToConnection(state: ConnectionState, event: BusEvent): boolean {
120
+ // Skip internal driver events
121
+ if (event.source === "driver" && event.intent !== "notification") {
122
+ return false;
123
+ }
124
+
125
+ // Skip command events (they are handled via RPC, not broadcast)
126
+ if (event.source === "command") {
127
+ return false;
128
+ }
129
+
130
+ // Check if subscribed to event's session
131
+ const eventWithContext = event as BusEvent & { context?: { sessionId?: string } };
132
+ const sessionId = eventWithContext.context?.sessionId;
133
+ if (sessionId && state.subscribedTopics.has(sessionId)) {
134
+ return true;
135
+ }
136
+
137
+ // Send to global subscribers
138
+ return state.subscribedTopics.has("global");
139
+ }
140
+
141
+ /**
142
+ * Send JSON-RPC response to a specific connection
143
+ */
144
+ function sendResponse(connection: ChannelConnection, id: string | number, result: unknown): void {
145
+ const response = createSuccessResponse(id, result);
146
+ connection.send(JSON.stringify(response));
147
+ }
148
+
149
+ /**
150
+ * Send JSON-RPC error to a specific connection
151
+ */
152
+ function sendError(
153
+ connection: ChannelConnection,
154
+ id: string | number | null,
155
+ code: number,
156
+ message: string
157
+ ): void {
158
+ const response = createErrorResponse(id, code, message);
159
+ connection.send(JSON.stringify(response));
160
+ }
161
+
162
+ // Handle new connections
163
+ wsServer.onConnection((connection) => {
164
+ const state: ConnectionState = {
165
+ connection,
166
+ subscribedTopics: new Set(["global"]),
167
+ };
168
+ connections.set(connection.id, state);
169
+
170
+ logger.info("Client connected", {
171
+ connectionId: connection.id,
172
+ totalConnections: connections.size,
173
+ });
174
+
175
+ // Handle messages from client
176
+ connection.onMessage(async (message) => {
177
+ try {
178
+ const parsed = parseMessage(message);
179
+
180
+ // Handle single message (not batch)
181
+ if (!Array.isArray(parsed)) {
182
+ await handleParsedMessage(connection, state, parsed);
183
+ } else {
184
+ // Handle batch (not common, but supported by JSON-RPC 2.0)
185
+ for (const item of parsed) {
186
+ await handleParsedMessage(connection, state, item);
187
+ }
188
+ }
189
+ } catch (err) {
190
+ logger.error("Failed to parse message", { error: (err as Error).message });
191
+ sendError(connection, null, RpcErrorCodes.PARSE_ERROR, "Parse error");
192
+ }
193
+ });
194
+
195
+ // Cleanup on disconnect
196
+ connection.onClose(() => {
197
+ connections.delete(connection.id);
198
+ logger.info("Client disconnected", {
199
+ connectionId: connection.id,
200
+ totalConnections: connections.size,
201
+ });
202
+ });
203
+ });
204
+
205
+ /**
206
+ * Handle a parsed JSON-RPC message
207
+ */
208
+ async function handleParsedMessage(
209
+ connection: ChannelConnection,
210
+ state: ConnectionState,
211
+ parsed: import("jsonrpc-lite").IParsedObject
212
+ ): Promise<void> {
213
+ if (isRequest(parsed)) {
214
+ // JSON-RPC Request - handle and respond directly
215
+ const payload = parsed.payload as {
216
+ id: string | number;
217
+ method: string;
218
+ params: unknown;
219
+ };
220
+ const { id, method, params } = payload;
221
+
222
+ logger.debug("Received RPC request", { id, method });
223
+
224
+ // Call command handler
225
+ const result = await commandHandler.handle(method as RpcMethod, params);
226
+
227
+ if (result.success) {
228
+ sendResponse(connection, id, result.data);
229
+ } else {
230
+ sendError(connection, id, result.code, result.message);
231
+ }
232
+ } else if (isNotification(parsed)) {
233
+ // JSON-RPC Notification - control messages
234
+ const payload = parsed.payload as {
235
+ method: string;
236
+ params: unknown;
237
+ };
238
+ const { method, params } = payload;
239
+
240
+ logger.debug("Received notification", { method });
241
+
242
+ if (method === "subscribe") {
243
+ const { topic } = params as { topic: string };
244
+ subscribeToTopic(connection.id, topic);
245
+ } else if (method === "unsubscribe") {
246
+ const { topic } = params as { topic: string };
247
+ state.subscribedTopics.delete(topic);
248
+ logger.debug("Connection unsubscribed from topic", { connectionId: connection.id, topic });
249
+ } else if (method === "control.ack") {
250
+ // ACK for reliable delivery - handled by network layer
251
+ logger.debug("Received ACK notification");
252
+ }
253
+ } else {
254
+ // Invalid message
255
+ logger.warn("Received invalid JSON-RPC message");
256
+ }
257
+ }
258
+
259
+ // Route internal events to connected clients as JSON-RPC notifications
260
+ platform.eventBus.onAny((event) => {
261
+ // Only broadcast broadcastable events
262
+ if (!shouldBroadcastEvent(event)) {
263
+ return;
264
+ }
265
+
266
+ // Get topic from event context
267
+ const eventWithContext = event as BusEvent & { context?: { sessionId?: string } };
268
+ const topic = eventWithContext.context?.sessionId || "global";
269
+
270
+ // Wrap as JSON-RPC notification
271
+ const notification = createStreamEvent(topic, event as SystemEvent);
272
+ const message = JSON.stringify(notification);
273
+
274
+ for (const [connectionId, state] of connections) {
275
+ if (shouldSendToConnection(state, event)) {
276
+ state.connection.sendReliable(message, {
277
+ timeout: 10000,
278
+ onTimeout: () => {
279
+ logger.warn("Event ACK timeout", {
280
+ connectionId,
281
+ eventType: event.type,
282
+ });
283
+ },
284
+ });
285
+ }
286
+ }
287
+ });
288
+
289
+ /**
290
+ * Check if event should be broadcast
291
+ */
292
+ function shouldBroadcastEvent(event: BusEvent): boolean {
293
+ // Skip internal driver events
294
+ if (event.source === "driver" && event.intent !== "notification") {
295
+ return false;
296
+ }
297
+
298
+ // Skip command events (handled via RPC)
299
+ if (event.source === "command") {
300
+ return false;
301
+ }
302
+
303
+ // Check broadcastable flag
304
+ const systemEvent = event as SystemEvent;
305
+ if (systemEvent.broadcastable === false) {
306
+ return false;
307
+ }
308
+
309
+ return true;
310
+ }
311
+
312
+ // Attach to existing server if provided
313
+ if (config.server) {
314
+ wsServer.attach(config.server, wsPath);
315
+ logger.info("WebSocket attached to existing server", { path: wsPath });
316
+ }
317
+
318
+ return {
319
+ async listen(port?: number, host?: string) {
320
+ if (config.server) {
321
+ throw new Error(
322
+ "Cannot listen when attached to existing server. The server should call listen() instead."
323
+ );
324
+ }
325
+
326
+ const listenPort = port ?? config.port ?? 5200;
327
+ const listenHost = host ?? config.host ?? "0.0.0.0";
328
+
329
+ await wsServer.listen(listenPort, listenHost);
330
+ logger.info("Server listening", { port: listenPort, host: listenHost });
331
+ },
332
+
333
+ async close() {
334
+ await wsServer.close();
335
+ logger.info("Server closed");
336
+ },
337
+
338
+ async dispose() {
339
+ // Cleanup in order
340
+ await wsServer.dispose();
341
+ commandHandler.dispose();
342
+ await runtime.shutdown();
343
+ logger.info("Server disposed");
344
+ },
345
+ };
346
+ }