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/dist/chunk-ZFL27HK7.js +467 -0
- package/dist/chunk-ZFL27HK7.js.map +1 -0
- package/dist/index.d.ts +181 -140
- package/dist/index.js +115 -84
- package/dist/index.js.map +1 -1
- package/dist/server-IFVYHIJF.js +7 -0
- package/dist/server-IFVYHIJF.js.map +1 -0
- package/package.json +4 -6
- package/src/CommandHandler.ts +424 -0
- package/src/LocalClient.ts +10 -10
- package/src/RemoteClient.ts +14 -14
- package/src/index.ts +125 -110
- package/src/namespaces/presentations.ts +1 -1
- package/src/presentation/Presentation.ts +2 -2
- package/src/server.ts +346 -0
- package/src/types.ts +84 -137
package/src/index.ts
CHANGED
|
@@ -1,146 +1,158 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agentxjs - AgentX Client SDK
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Fluent API supporting local, remote, and server modes.
|
|
5
5
|
*
|
|
6
|
-
* @example Local mode
|
|
6
|
+
* @example Local mode
|
|
7
7
|
* ```typescript
|
|
8
8
|
* import { createAgentX } from "agentxjs";
|
|
9
|
+
* import { node } from "@agentxjs/node-platform";
|
|
9
10
|
*
|
|
10
|
-
* const
|
|
11
|
-
*
|
|
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
|
|
15
|
+
* @example Remote mode
|
|
27
16
|
* ```typescript
|
|
28
|
-
*
|
|
17
|
+
* const ax = createAgentX();
|
|
18
|
+
* const client = await ax.connect("ws://localhost:5200");
|
|
19
|
+
* ```
|
|
29
20
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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,
|
|
33
|
+
import type { AgentX, AgentXBuilder, AgentXServer, ConnectOptions, ServeConfig } from "./types";
|
|
39
34
|
|
|
40
35
|
/**
|
|
41
|
-
*
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
*
|
|
44
|
+
* Create an AgentX builder
|
|
71
45
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
46
|
+
* @param config - Platform configuration (optional). Without it, only connect() is available.
|
|
47
|
+
* @returns AgentXBuilder — local AgentX + connect() + serve()
|
|
74
48
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return config;
|
|
78
|
-
}
|
|
49
|
+
export function createAgentX(config?: PlatformConfig): AgentXBuilder {
|
|
50
|
+
let localClient: LocalClient | null = null;
|
|
79
51
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|