@gnsx/genesys.agent.client 0.3.3

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.
@@ -0,0 +1,318 @@
1
+ import { AgentEventType, ClientMessageType, ServerErrorCode, ServerMessageType } from '@gnsx/genesys.agent.shared/browser';
2
+
3
+ import { ClientEventType, ClientState } from './types.js';
4
+
5
+ import type { ClientEventMap , ClientOptions, SessionConfig } from './types.js';
6
+ import type { ClientMessage, ErrorMessage, ServerMessage} from '@gnsx/genesys.agent.shared/browser';
7
+
8
+ const LOG_PREFIX = '[GenesysAgentClient]';
9
+
10
+ export class GenesysAgentClient {
11
+ protected ws: WebSocket | null = null;
12
+ protected sessionId: string | null = null;
13
+ protected wsUrl: string | null = null;
14
+ protected state: ClientState = ClientState.DISCONNECTED;
15
+ protected reconnectAttempts = 0;
16
+ protected reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
17
+ protected intentionalClose = false;
18
+
19
+ protected eventListeners: Map<ClientEventType, Set<Function>> = new Map();
20
+
21
+ protected serverAddress: string | null = null;
22
+
23
+ protected config: SessionConfig | null = null;
24
+
25
+ constructor(readonly options: ClientOptions = {}) {
26
+ this.options.autoReconnect = this.options.autoReconnect ?? true;
27
+ this.options.maxReconnectAttempts = this.options.maxReconnectAttempts ?? 5;
28
+ this.options.reconnectInterval = this.options.reconnectInterval ?? 1000;
29
+ this.options.debug = this.options.debug ?? false;
30
+ }
31
+
32
+ public async connect(config: SessionConfig): Promise<void> {
33
+ if (this.state === ClientState.CONNECTED || this.state === ClientState.CONNECTING) {
34
+ throw new Error('Already connected or connecting');
35
+ }
36
+
37
+ if (!config.serverAddress) {
38
+ throw new Error('Server address is required');
39
+ }
40
+
41
+ // save the config in case we need to connect again
42
+ this.config = config;
43
+
44
+ // Remove trailing slash from server address
45
+ this.serverAddress = config.serverAddress.replace(/\/+$/, '');
46
+
47
+ this.setState(ClientState.CONNECTING);
48
+ this.intentionalClose = false;
49
+
50
+ try {
51
+ // Create session via HTTP
52
+ const response = await fetch(`${this.serverAddress}/sessions`, {
53
+ method: 'POST',
54
+ headers: { 'Content-Type': 'application/json' },
55
+ body: JSON.stringify({ specPath: config.specPath })
56
+ });
57
+
58
+ if (!response.ok) {
59
+ const errorData = await response.json().catch(() => ({ error: 'Failed to create session' }));
60
+ throw new Error(errorData.error ?? 'Failed to create session');
61
+ }
62
+
63
+ const sessionInfo: { sessionId: string; wsUrl: string } = await response.json();
64
+ this.sessionId = sessionInfo.sessionId;
65
+ this.wsUrl = sessionInfo.wsUrl;
66
+ if (!this.sessionId) {
67
+ throw new Error('Session ID is required');
68
+ }
69
+ if (!this.wsUrl) {
70
+ throw new Error('WebSocket URL is required');
71
+ }
72
+
73
+ // Connect WebSocket
74
+ await this.connectWebSocket(false);
75
+ } catch (error) {
76
+ this.setState(ClientState.DISCONNECTED);
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ protected async connectWebSocket(isReconnecting: boolean): Promise<void> {
82
+ if (!this.wsUrl) {
83
+ throw new Error('No WebSocket URL available');
84
+ }
85
+
86
+ return new Promise((resolve, reject) => {
87
+ this.ws = new WebSocket(this.wsUrl!);
88
+
89
+ this.ws.onopen = () => {
90
+ this.setState(ClientState.CONNECTED);
91
+ this.reconnectAttempts = 0;
92
+ resolve();
93
+ };
94
+
95
+ this.ws.onmessage = (event) => {
96
+ try {
97
+ const message: ServerMessage = JSON.parse(event.data);
98
+ this.handleServerMessage(message);
99
+ } catch (error) {
100
+ this.debug('Failed to parse message:', error);
101
+ }
102
+ };
103
+
104
+ this.ws.onclose = () => {
105
+ this.setState(ClientState.DISCONNECTED);
106
+ if (!isReconnecting) {
107
+ this.emit(ClientEventType.DISCONNECTED, this.intentionalClose ? 'User disconnected' : 'Connection lost');
108
+
109
+ if (!this.intentionalClose && this.options.autoReconnect && this.sessionId) {
110
+ this.attemptReconnect();
111
+ }
112
+ }
113
+ };
114
+
115
+ this.ws.onerror = (error) => {
116
+ this.debug('WebSocket error:', error);
117
+ reject(error);
118
+ };
119
+ });
120
+ }
121
+
122
+ protected handleServerMessage(message: ServerMessage): void {
123
+ if (message.type === ServerMessageType.AGENT_EVENT) {
124
+ switch (message.event.type) {
125
+ case AgentEventType.USER_MESSAGE:
126
+ this.debug('USER_MESSAGE:\n', message.event.data.message);
127
+ break;
128
+ case AgentEventType.AGENT_MESSAGE:
129
+ this.debug('AGENT_MESSAGE:\n', message.event.data.message);
130
+ break;
131
+ case AgentEventType.STREAM_CHUNK:
132
+ this.debug('STREAM_CHUNK:\n', message.event.data.chunk);
133
+ break;
134
+ case AgentEventType.TOOL_CALL:
135
+ this.debug('TOOL_CALL:\n', message.event.data.toolName, message.event.data.input);
136
+ break;
137
+ case AgentEventType.TOOL_RESULT:
138
+ this.debug('TOOL_RESULT:\n', message.event.data.toolName, message.event.data.result);
139
+ break;
140
+ case AgentEventType.TOOL_ERROR:
141
+ console.error(LOG_PREFIX, 'TOOL_ERROR:\n', message.event.data.toolName, message.event.data.error);
142
+ break;
143
+ case AgentEventType.REASONING:
144
+ this.debug('REASONING:\n', message.event.data.text);
145
+ break;
146
+ case AgentEventType.TOKEN_USAGE:
147
+ this.debug('TOKEN_USAGE:\n', message.event.data);
148
+ break;
149
+ case AgentEventType.TODO_LIST:
150
+ this.debug('TODO_LIST:\n', message.event.data.todos);
151
+ break;
152
+ case AgentEventType.SEND_DATA:
153
+ this.debug('SEND_DATA:\n', message.event.data.data);
154
+ break;
155
+ case AgentEventType.ERROR:
156
+ console.error(LOG_PREFIX, 'AGENT_ERROR:\n', message.event.data.error);
157
+ break;
158
+ case AgentEventType.MESSAGES_CLEARED:
159
+ this.debug('MESSAGES_CLEARED:\n');
160
+ break;
161
+ default:
162
+ console.error(LOG_PREFIX, 'Unknown agent event:\n', message.event);
163
+ break;
164
+ }
165
+ } else {
166
+ this.debug('handleServerMessage:', message);
167
+ }
168
+
169
+ if (message.type === ServerMessageType.ERROR) {
170
+ this.handleServerError(message);
171
+ }
172
+ this.emit(ClientEventType.SERVER_MESSAGE, message);
173
+ }
174
+
175
+ public sendInput(text: string): void {
176
+ if (this.state !== ClientState.CONNECTED) {
177
+ throw new Error('Not connected');
178
+ }
179
+
180
+ this.send({
181
+ type: ClientMessageType.INPUT,
182
+ text
183
+ });
184
+ }
185
+
186
+ public abort(): void {
187
+ if (this.state !== ClientState.CONNECTED) {
188
+ throw new Error('Not connected');
189
+ }
190
+
191
+ throw new Error('Not implemented');
192
+ }
193
+
194
+ public disconnect(): void {
195
+ this.intentionalClose = true;
196
+
197
+ if (this.reconnectTimeout) {
198
+ clearTimeout(this.reconnectTimeout);
199
+ this.reconnectTimeout = null;
200
+ }
201
+
202
+ if (this.ws) {
203
+ this.ws.close();
204
+ this.ws = null;
205
+ }
206
+
207
+ this.sessionId = null;
208
+ this.wsUrl = null;
209
+ this.setState(ClientState.DISCONNECTED);
210
+ }
211
+
212
+ protected attemptReconnect(): void {
213
+ if (this.reconnectAttempts >= this.options.maxReconnectAttempts!) {
214
+ this.emit(ClientEventType.CONTROL_MESSAGE, { message: `Failed to reconnect after ${this.options.maxReconnectAttempts} attempts`, isError: true });
215
+ this.sessionId = null;
216
+ this.wsUrl = null;
217
+ return;
218
+ }
219
+
220
+ this.reconnectAttempts++;
221
+ const delay = Math.min(5000, this.options.reconnectInterval! * Math.pow(1.5, this.reconnectAttempts - 1));
222
+
223
+ this.setState(ClientState.RECONNECTING);
224
+ this.emit(ClientEventType.CONTROL_MESSAGE, { message: `Reconnect attempt ${this.reconnectAttempts}/${this.options.maxReconnectAttempts}...`, isError: false });
225
+
226
+ this.reconnectTimeout = setTimeout(async () => {
227
+ try {
228
+ await this.connectWebSocket(true);
229
+ this.emit(ClientEventType.CONTROL_MESSAGE, { message: 'Reconnected to server', isError: false });
230
+ } catch {
231
+ this.attemptReconnect();
232
+ }
233
+ }, delay);
234
+ }
235
+
236
+ protected send(message: ClientMessage): void {
237
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
238
+ this.ws.send(JSON.stringify(message));
239
+ }
240
+ }
241
+
242
+ protected setState(state: ClientState): void {
243
+ if (this.state !== state) {
244
+ this.state = state;
245
+ this.emit(ClientEventType.STATE_CHANGE, state);
246
+ }
247
+ }
248
+
249
+ public getState(): ClientState {
250
+ return this.state;
251
+ }
252
+
253
+ public isConnected(): boolean {
254
+ return this.state === ClientState.CONNECTED;
255
+ }
256
+
257
+ public getSessionId(): string | null {
258
+ return this.sessionId;
259
+ }
260
+
261
+ // Event emitter methods
262
+ public on(event: ClientEventType, listener: ClientEventMap[ClientEventType]): ClientEventMap[ClientEventType] {
263
+ if (!this.eventListeners.has(event)) {
264
+ this.eventListeners.set(event, new Set());
265
+ }
266
+ this.eventListeners.get(event)!.add(listener);
267
+ return listener;
268
+ }
269
+
270
+ public off(event: ClientEventType, listener: ClientEventMap[ClientEventType]): void {
271
+ const listeners = this.eventListeners.get(event);
272
+ if (listeners) {
273
+ listeners.delete(listener);
274
+ }
275
+ }
276
+
277
+ public once(event: ClientEventType, listener: ClientEventMap[ClientEventType]): void {
278
+ const onceListener = ((...args: Parameters<ClientEventMap[ClientEventType]>) => {
279
+ this.off(event, onceListener);
280
+ (listener as Function)(...args);
281
+ }) as ClientEventMap[ClientEventType];
282
+
283
+ this.on(event, onceListener);
284
+ }
285
+
286
+ protected emit(event: ClientEventType, ...args: Parameters<ClientEventMap[ClientEventType]>): void {
287
+ const listeners = this.eventListeners.get(event);
288
+ if (listeners) {
289
+ listeners.forEach(listener => {
290
+ try {
291
+ (listener as Function)(...args);
292
+ } catch (error) {
293
+ this.debug(`Error in ${event} listener:`, error);
294
+ }
295
+ });
296
+ }
297
+ }
298
+
299
+ protected debug(...args: unknown[]): void {
300
+ if (this.options.debug) {
301
+ console.log(LOG_PREFIX, ...args);
302
+ }
303
+ }
304
+
305
+
306
+ protected async handleServerError(message: ErrorMessage): Promise<void> {
307
+ if (message.code === ServerErrorCode.SESSION_NOT_FOUND) {
308
+ // reconnect to get a new session
309
+ try {
310
+ console.warn('SESSION_NOT_FOUND, reconnecting to get a new session');
311
+ this.disconnect();
312
+ await this.connect(this.config!);
313
+ } catch (error) {
314
+ console.error(error);
315
+ }
316
+ }
317
+ }
318
+ }
package/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ export * from './GenesysAgentClient.js';
2
+ export * from './types.js';
3
+
4
+ // Re-export shared types explicitly for proper bundling with dts-bundle-generator
5
+ // (inlinedLibraries doesn't properly handle subpath exports like /browser)
6
+ export {
7
+ AgentEventType,
8
+ ServerMessageType,
9
+ ClientMessageType,
10
+ ServerErrorCode,
11
+ TodoStatus,
12
+ TodoPriority,
13
+ } from '@gnsx/genesys.agent.shared/browser';
14
+
15
+ export type {
16
+ AgentEvent,
17
+ ServerMessage,
18
+ ClientMessage,
19
+ SessionInfo,
20
+ AgentEventMessage,
21
+ ConnectionEstablishedMessage,
22
+ RequestInputMessage,
23
+ ErrorMessage,
24
+ PingMessage,
25
+ SessionInfoMessage,
26
+ SessionRestoredMessage,
27
+ InputMessage,
28
+ // Event types
29
+ UserMessageEvent,
30
+ AgentMessageEvent,
31
+ StreamChunkEvent,
32
+ ToolCallEvent,
33
+ ToolResultEvent,
34
+ ToolErrorEvent,
35
+ ReasoningEvent,
36
+ TokenUsageEvent,
37
+ TodoItem,
38
+ TodoListEvent,
39
+ SendDataEvent,
40
+ ErrorEvent,
41
+ MessagesClearedEvent,
42
+ ModelUpdatedEvent,
43
+ RoundFinishedEvent,
44
+ } from '@gnsx/genesys.agent.shared/browser';
45
+
46
+ // Re-export constants
47
+ export {
48
+ DEFAULT_PORT,
49
+ DEFAULT_SESSION_DIR,
50
+ DEFAULT_SESSION_TIMEOUT,
51
+ DEFAULT_MAX_SESSIONS,
52
+ MAX_FILE_SIZE,
53
+ WS_HEARTBEAT_INTERVAL,
54
+ SYSTEM_CONTEXT_PLACEHOLDER,
55
+ PROJECT_LAYOUT_PLACEHOLDER,
56
+ } from '@gnsx/genesys.agent.shared/browser';
package/src/types.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { ServerMessage } from '@gnsx/genesys.agent.shared/browser';
2
+
3
+ export interface ClientOptions {
4
+ autoReconnect?: boolean;
5
+ maxReconnectAttempts?: number;
6
+ reconnectInterval?: number;
7
+ debug?: boolean;
8
+ }
9
+
10
+ export interface SessionConfig {
11
+ serverAddress: string;
12
+ specPath: string;
13
+ }
14
+
15
+ export interface ControlMessage {
16
+ message: string;
17
+ isError: boolean;
18
+ }
19
+
20
+ export enum ClientState {
21
+ DISCONNECTED = 'disconnected',
22
+ CONNECTING = 'connecting',
23
+ CONNECTED = 'connected',
24
+ RECONNECTING = 'reconnecting'
25
+ }
26
+
27
+ export enum ClientEventType {
28
+ SERVER_MESSAGE = 'serverMessage',
29
+ DISCONNECTED = 'disconnected',
30
+ STATE_CHANGE = 'stateChange',
31
+ CONTROL_MESSAGE = 'controlMessage',
32
+ }
33
+
34
+ export interface ClientEventMap {
35
+ [ClientEventType.SERVER_MESSAGE]: (message: ServerMessage) => void;
36
+ [ClientEventType.DISCONNECTED]: (reason?: string) => void;
37
+ [ClientEventType.STATE_CHANGE]: (state: ClientState) => void;
38
+ [ClientEventType.CONTROL_MESSAGE]: (message: ControlMessage) => void;
39
+ }