@xalia/agent 0.5.4 → 0.5.6

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.
Files changed (43) hide show
  1. package/dist/agent/src/agent/agent.js +16 -9
  2. package/dist/agent/src/agent/agentUtils.js +24 -4
  3. package/dist/agent/src/agent/mcpServerManager.js +19 -9
  4. package/dist/agent/src/agent/openAILLM.js +3 -1
  5. package/dist/agent/src/agent/openAILLMStreaming.js +24 -25
  6. package/dist/agent/src/agent/repeatLLM.js +43 -0
  7. package/dist/agent/src/agent/sudoMcpServerManager.js +12 -6
  8. package/dist/agent/src/chat/client.js +259 -36
  9. package/dist/agent/src/chat/conversationManager.js +243 -24
  10. package/dist/agent/src/chat/db.js +24 -1
  11. package/dist/agent/src/chat/frontendClient.js +74 -0
  12. package/dist/agent/src/chat/server.js +3 -3
  13. package/dist/agent/src/test/db.test.js +25 -2
  14. package/dist/agent/src/test/openaiStreaming.test.js +133 -0
  15. package/dist/agent/src/test/prompt.test.js +2 -2
  16. package/dist/agent/src/test/sudoMcpServerManager.test.js +1 -1
  17. package/dist/agent/src/tool/agentChat.js +7 -197
  18. package/dist/agent/src/tool/chatMain.js +18 -23
  19. package/dist/agent/src/tool/commandPrompt.js +248 -0
  20. package/dist/agent/src/tool/prompt.js +27 -31
  21. package/package.json +1 -1
  22. package/scripts/test_chat +17 -1
  23. package/src/agent/agent.ts +34 -11
  24. package/src/agent/agentUtils.ts +52 -3
  25. package/src/agent/mcpServerManager.ts +43 -13
  26. package/src/agent/openAILLM.ts +3 -1
  27. package/src/agent/openAILLMStreaming.ts +28 -27
  28. package/src/agent/repeatLLM.ts +51 -0
  29. package/src/agent/sudoMcpServerManager.ts +41 -12
  30. package/src/chat/client.ts +353 -40
  31. package/src/chat/conversationManager.ts +345 -33
  32. package/src/chat/db.ts +28 -2
  33. package/src/chat/frontendClient.ts +123 -0
  34. package/src/chat/messages.ts +146 -2
  35. package/src/chat/server.ts +3 -3
  36. package/src/test/db.test.ts +35 -2
  37. package/src/test/openaiStreaming.test.ts +142 -0
  38. package/src/test/prompt.test.ts +1 -1
  39. package/src/test/sudoMcpServerManager.test.ts +1 -1
  40. package/src/tool/agentChat.ts +13 -211
  41. package/src/tool/chatMain.ts +28 -43
  42. package/src/tool/commandPrompt.ts +252 -0
  43. package/src/tool/prompt.ts +33 -32
@@ -15,11 +15,17 @@ import { createAgentWithSkills } from "../agent/agentUtils";
15
15
  import type {
16
16
  ClientToServer,
17
17
  ServerToClient,
18
- ClientUserMessage,
18
+ ServerMcpServerAdded,
19
+ ServerMcpServerRemoved,
20
+ ServerMcpServerToolEnabled,
21
+ ServerMcpServerToolDisabled,
22
+ ServerSystemPromptUpdated,
23
+ ServerModelUpdated,
19
24
  } from "./messages";
20
25
  import { AsyncQueue } from "./asyncQueue";
21
26
  import { Database, UserData } from "./db";
22
27
  import { AsyncLock } from "../utils/asyncLock";
28
+ import { McpServerInfo, McpServerManager } from "../agent/mcpServerManager";
23
29
 
24
30
  const logger = getLogger();
25
31
 
@@ -38,20 +44,29 @@ type QueuedClientMessage<T extends ClientToServer = ClientToServer> = {
38
44
  * Describes a Session (conversation) with connected participants.
39
45
  */
40
46
  export class OpenSession {
47
+ public db: Database;
41
48
  public onEmpty: () => void;
49
+ /// Map of user identifier to connection
42
50
  public connections: Record<string, ws.WebSocket>;
43
51
  public agent: Agent;
52
+ public sessionUUID: string;
53
+ public agentProfileUUID: string;
44
54
  public skillManager: SkillManager;
45
55
  public messageQueue: AsyncQueue<QueuedClientMessage>;
46
56
  public curAgentMsgId: string | undefined;
47
57
 
48
58
  constructor(
59
+ db: Database,
49
60
  agent: Agent,
61
+ sessionUUID: string,
62
+ agentProfileUUID: string,
50
63
  sudoMcpServerManager: SkillManager,
51
64
  onEmpty: () => void
52
65
  ) {
53
- // public agent: Agent,
66
+ this.db = db;
54
67
  this.agent = agent;
68
+ this.sessionUUID = sessionUUID;
69
+ this.agentProfileUUID = agentProfileUUID;
55
70
  this.skillManager = sudoMcpServerManager;
56
71
  this.onEmpty = onEmpty;
57
72
  this.connections = {};
@@ -71,6 +86,25 @@ export class OpenSession {
71
86
  this.broadcast({ type: "user_joined", user: userName });
72
87
  this.connections[userName] = ws;
73
88
 
89
+ // TODO:
90
+ //
91
+ // - send conversation state (ServerHistory)
92
+
93
+ const briefs = this.skillManager.getServerBriefs();
94
+ this.sendTo(userName, { type: "mcp_server_briefs", server_briefs: briefs });
95
+
96
+ const msm = this.agent.getMcpServerManager();
97
+ const mcpServerNames = msm.getMcpServerNames();
98
+ for (const serverName of mcpServerNames) {
99
+ const info = msm.getMcpServer(serverName);
100
+ this.sendTo(userName, {
101
+ type: "mcp_server_added",
102
+ server_name: serverName,
103
+ tools: info.getTools(),
104
+ enabled_tools: Object.keys(info.getEnabledTools()),
105
+ });
106
+ }
107
+
74
108
  ws.on("message", async (message: ws.RawData) => {
75
109
  logger.debug(`[convMgr]: got message: (from ${userName}): ${message}`);
76
110
  const msgStr = message.toString();
@@ -102,14 +136,71 @@ export class OpenSession {
102
136
  `${JSON.stringify(queuedMessage.msg)}`
103
137
  );
104
138
 
105
- const msg = queuedMessage.msg;
106
- switch (msg.type) {
107
- case "msg":
108
- return this.onChatMessage(
109
- queuedMessage as QueuedClientMessage<ClientUserMessage>
110
- );
111
- default:
112
- throw `unknown message: ${JSON.stringify(queuedMessage)}`;
139
+ // In general, handlers return a message to be broadcast. Errors are
140
+ // handled by returning an error to just the sender. Handlers can
141
+ // also broadcast and send directly.
142
+
143
+ try {
144
+ const msg = queuedMessage.msg;
145
+ let broadcastMsg: ServerToClient[] | ServerToClient | undefined =
146
+ undefined;
147
+ switch (msg.type) {
148
+ case "msg":
149
+ broadcastMsg = await this.onChatMessage(
150
+ msg.message,
151
+ queuedMessage.from
152
+ );
153
+ break;
154
+ case "add_mcp_server":
155
+ broadcastMsg = await this.onAddMcpServer(
156
+ msg.server_name,
157
+ msg.enable_all
158
+ );
159
+ break;
160
+ case "remove_mcp_server":
161
+ broadcastMsg = await this.onRemoveMcpServer(msg.server_name);
162
+ break;
163
+ case "enable_mcp_server_tool":
164
+ broadcastMsg = await this.onEnableMcpServerTool(
165
+ msg.server_name,
166
+ msg.tool
167
+ );
168
+ break;
169
+ case "disable_mcp_server_tool":
170
+ broadcastMsg = await this.onDisableMcpServerTool(
171
+ msg.server_name,
172
+ msg.tool
173
+ );
174
+ break;
175
+ case "enable_all_mcp_server_tools":
176
+ broadcastMsg = await this.onEnableAllMcpServerTools(msg.server_name);
177
+ break;
178
+ case "disable_all_mcp_server_tools":
179
+ broadcastMsg = await this.onDisableAllMcpServerTools(msg.server_name);
180
+ break;
181
+ case "set_system_prompt":
182
+ broadcastMsg = await this.onSetSystemPrompt(msg.system_prompt);
183
+ break;
184
+ case "set_model":
185
+ broadcastMsg = await this.onSetModel(msg.model);
186
+ break;
187
+ default:
188
+ throw `unknown message: ${JSON.stringify(queuedMessage)}`;
189
+ }
190
+
191
+ if (broadcastMsg) {
192
+ if (broadcastMsg instanceof Array) {
193
+ broadcastMsg.map((msg) => this.broadcast(msg));
194
+ } else {
195
+ this.broadcast(broadcastMsg);
196
+ }
197
+ }
198
+ } catch (err: unknown) {
199
+ if (typeof err === "string") {
200
+ this.sendTo(queuedMessage.from, { type: "error", message: err });
201
+ } else {
202
+ throw err;
203
+ }
113
204
  }
114
205
  }
115
206
 
@@ -124,24 +215,163 @@ export class OpenSession {
124
215
  });
125
216
  }
126
217
 
127
- async onChatMessage(
128
- queuedMessage: QueuedClientMessage<ClientUserMessage>
129
- ): Promise<void> {
130
- const msg = queuedMessage.msg;
131
- const userToken = queuedMessage.from;
218
+ async onChatMessage(message: string, userToken: string): Promise<undefined> {
219
+ // We manually broadcast the user's message here and start the agent
220
+ // conversation, and then wait to get back all data from the agent before
221
+ // processing further messages from clients.
222
+
132
223
  const msgId = uuidv4();
133
224
  this.broadcast({
134
225
  type: "user_msg",
135
226
  message_id: msgId,
136
- message: msg.message,
227
+ message,
137
228
  from: userToken,
138
229
  });
139
230
 
140
- // Messages will be handled by the Agent.onMessage callback. We await the
141
- // response here before processing further messages.
142
-
143
231
  this.curAgentMsgId = `${msgId}-resp`;
144
- await this.agent.userMessage(msg.message, undefined, userToken);
232
+ await this.agent.userMessageEx(message, undefined, userToken);
233
+ }
234
+
235
+ async onAddMcpServer(
236
+ serverName: string,
237
+ enableAll: boolean
238
+ ): Promise<ServerMcpServerAdded> {
239
+ logger.info(
240
+ `[onAddMcpServer]: Adding server ${serverName} (enable_all: ${enableAll})`
241
+ );
242
+
243
+ const mcpServerManager = this.agent.getMcpServerManager();
244
+ if (mcpServerManager.hasMcpServer(serverName)) {
245
+ throw `${serverName} already added`;
246
+ }
247
+
248
+ if (!this.skillManager.hasServer(serverName)) {
249
+ throw `no such server: ${serverName}`;
250
+ }
251
+
252
+ await this.skillManager.addMcpServer(serverName, enableAll);
253
+ mcpServerManager.enableAllTools(serverName);
254
+
255
+ // Save changes to the AgentProfile
256
+
257
+ await this.updateAgentProfile();
258
+
259
+ // Broadcast the message to all participants.
260
+
261
+ const server = mcpServerManager.getMcpServer(serverName);
262
+ const tools = server.getTools();
263
+ const enabled_tools = Object.keys(server.getEnabledTools());
264
+ return {
265
+ type: "mcp_server_added",
266
+ server_name: serverName,
267
+ tools,
268
+ enabled_tools,
269
+ };
270
+ }
271
+
272
+ async onRemoveMcpServer(
273
+ server_name: string
274
+ ): Promise<ServerMcpServerRemoved> {
275
+ logger.info(`[onRemoveMcpServer]: Removing server ${server_name}`);
276
+
277
+ const mcpServerManager = this.agent.getMcpServerManager();
278
+ if (!mcpServerManager.hasMcpServer(server_name)) {
279
+ throw `${server_name} not enabled`;
280
+ }
281
+
282
+ await mcpServerManager.removeMcpServer(server_name);
283
+ await this.updateAgentProfile();
284
+
285
+ return {
286
+ type: "mcp_server_removed",
287
+ server_name,
288
+ };
289
+ }
290
+
291
+ async onEnableMcpServerTool(
292
+ server_name: string,
293
+ tool: string
294
+ ): Promise<ServerMcpServerToolEnabled> {
295
+ const msm = this.agent.getMcpServerManager();
296
+ this.ensureMcpServerAndTool(msm, server_name, tool);
297
+ msm.enableTool(server_name, tool);
298
+
299
+ await this.updateAgentProfile();
300
+
301
+ return { type: "mcp_server_tool_enabled", server_name, tool };
302
+ }
303
+
304
+ async onDisableMcpServerTool(
305
+ server_name: string,
306
+ tool: string
307
+ ): Promise<ServerMcpServerToolDisabled> {
308
+ const msm = this.agent.getMcpServerManager();
309
+ this.ensureMcpServerAndTool(msm, server_name, tool);
310
+ msm.disableTool(server_name, tool);
311
+
312
+ await this.updateAgentProfile();
313
+
314
+ return { type: "mcp_server_tool_disabled", server_name, tool };
315
+ }
316
+
317
+ async onEnableAllMcpServerTools(
318
+ server_name: string
319
+ ): Promise<ServerMcpServerToolEnabled[]> {
320
+ // We reimplement the logic to enable any disabled tools so we can
321
+ // construct messages along the way.
322
+
323
+ const msm = this.agent.getMcpServerManager();
324
+ const server = this.ensureMcpServer(msm, server_name);
325
+ const enabledTools = server.getEnabledTools();
326
+ const msgs: ServerMcpServerToolEnabled[] = [];
327
+ for (const tool of server.getTools()) {
328
+ if (!enabledTools[tool.name]) {
329
+ msm.enableTool(server_name, tool.name);
330
+ msgs.push({
331
+ type: "mcp_server_tool_enabled",
332
+ server_name,
333
+ tool: tool.name,
334
+ });
335
+ }
336
+ }
337
+
338
+ await this.updateAgentProfile();
339
+
340
+ return msgs;
341
+ }
342
+
343
+ async onDisableAllMcpServerTools(
344
+ server_name: string
345
+ ): Promise<ServerMcpServerToolDisabled[]> {
346
+ // We reimplement the logic to disable all enabled tools so we can
347
+ // construct messages along the way.
348
+
349
+ const msm = this.agent.getMcpServerManager();
350
+ const server = this.ensureMcpServer(msm, server_name);
351
+ const enabledTools = server.getEnabledTools();
352
+ const msgs: ServerMcpServerToolDisabled[] = [];
353
+ for (const tool in enabledTools) {
354
+ msm.disableTool(server_name, tool);
355
+ msgs.push({ type: "mcp_server_tool_disabled", server_name, tool });
356
+ }
357
+
358
+ await this.updateAgentProfile();
359
+
360
+ return msgs;
361
+ }
362
+
363
+ async onSetSystemPrompt(
364
+ system_prompt: string
365
+ ): Promise<ServerSystemPromptUpdated> {
366
+ this.agent.setSystemPrompt(system_prompt);
367
+ await this.updateAgentProfile();
368
+ return { type: "system_prompt_updated", system_prompt };
369
+ }
370
+
371
+ async onSetModel(model: string): Promise<ServerModelUpdated> {
372
+ this.agent.setModel(model);
373
+ await this.updateAgentProfile();
374
+ return { type: "model_updated", model };
145
375
  }
146
376
 
147
377
  broadcast(msg: ServerToClient) {
@@ -151,6 +381,47 @@ export class OpenSession {
151
381
  ws.send(msgString);
152
382
  }
153
383
  }
384
+
385
+ sendTo(userName: string, msg: ServerToClient) {
386
+ const ws = this.connections[userName];
387
+ const msgString = JSON.stringify(msg);
388
+ logger.info(`[sendTo]: (${userName}) msg: ${msgString}`);
389
+ assert(ws);
390
+ ws.send(msgString);
391
+ }
392
+
393
+ private ensureMcpServer(
394
+ msm: McpServerManager,
395
+ serverName: string
396
+ ): McpServerInfo {
397
+ const server = msm.getMcpServer(serverName);
398
+ if (!server) {
399
+ throw `${serverName} not added`;
400
+ }
401
+ return server;
402
+ }
403
+
404
+ private ensureMcpServerAndTool(
405
+ msm: McpServerManager,
406
+ serverName: string,
407
+ toolName: string
408
+ ) {
409
+ const server = this.ensureMcpServer(msm, serverName);
410
+ const tool = server.getTool(toolName);
411
+ if (!tool) {
412
+ throw `Tool ${toolName} on ${serverName} not found`;
413
+ }
414
+ return tool;
415
+ }
416
+
417
+ private async updateAgentProfile(): Promise<void> {
418
+ const profile = this.agent.getAgentProfile();
419
+ logger.debug(
420
+ `[updateAgentProfile]: uuid: ${this.agentProfileUUID} profile: ` +
421
+ JSON.stringify(profile)
422
+ );
423
+ return this.db.updateAgentProfile(this.agentProfileUUID, profile);
424
+ }
154
425
  }
155
426
 
156
427
  /**
@@ -184,11 +455,12 @@ export class ConversationManager {
184
455
  sessionId,
185
456
  llmApiKey,
186
457
  xmcpApiKey,
187
- userData.nickname || userData.user_uuid,
458
+ userData.nickname || userData.uuid,
188
459
  ws
189
460
  );
190
461
  });
191
462
  }
463
+
192
464
  /**
193
465
  * Must be called while holding the openSessionsLock
194
466
  */
@@ -223,29 +495,56 @@ export class ConversationManager {
223
495
  const conversation =
224
496
  sessionData.conversation as unknown as ChatCompletionMessageParam[];
225
497
 
226
- const agentProfile = await this.db.getAgentProfileById(
227
- sessionData.agent_profile_uuid
228
- );
498
+ const agentProfileUUID = sessionData.agent_profile_uuid;
499
+ const agentProfile = await this.db.getAgentProfileById(agentProfileUUID);
229
500
  if (!agentProfile) {
230
- throw `no such agent profile ${sessionData.agent_profile_uuid}`;
501
+ throw `no such agent profile ${agentProfileUUID}`;
231
502
  }
232
503
 
504
+ // TODO: store some owner data on the OpenSession object itself?
505
+
506
+ const owner = await this.db.getUserFromUuid(sessionData.user_uuid);
507
+ assert(owner, `no owner for session ${JSON.stringify(sessionData)}`);
508
+ const ownerUserName = owner.nickname;
509
+ if (!ownerUserName) {
510
+ throw (
511
+ `user ${sessionData.user_uuid} has no user name - ` +
512
+ "cannot create chat session"
513
+ );
514
+ }
515
+
516
+ // Access to the OpenSession (once it is iniailized
517
+ const context: { openSession?: OpenSession } = {};
518
+
233
519
  const platform = {
234
520
  openUrl: (
235
- _url: string,
521
+ url: string,
236
522
  _authResultP: Promise<boolean>,
237
- _displayName: string
523
+ display_name: string
238
524
  ) => {
239
- throw "unimpl";
525
+ // These requests are always passed to the original owner, since it is
526
+ // his settings that will be used for all mcp servers.
527
+
528
+ if (context.openSession) {
529
+ const conn = openSession.connections[ownerUserName];
530
+ if (conn) {
531
+ openSession.sendTo(ownerUserName, {
532
+ type: "open_url",
533
+ url,
534
+ display_name,
535
+ });
536
+ } else {
537
+ throw `user ${ownerUserName} must authenticate`;
538
+ }
539
+ } else {
540
+ throw `no open session ${sessionData.uuid}`;
541
+ }
240
542
  },
241
543
  load: (_filename: string): Promise<string> => {
242
- throw "unimpl";
544
+ throw "unimpl platform.load";
243
545
  },
244
546
  };
245
547
 
246
- // Forward agent messages to the OpenSession (once it is iniailized
247
-
248
- const context: { openSession?: OpenSession } = {};
249
548
  const onMessage = async (msg: string, end: boolean) => {
250
549
  logger.debug(`[onMessage] msg: ${msg}, end: ${end}`);
251
550
  assert(context.openSession);
@@ -256,6 +555,12 @@ export class ConversationManager {
256
555
  toolCall: ChatCompletionMessageToolCall
257
556
  ): Promise<boolean> => {
258
557
  logger.debug(`[onToolCall] : ${JSON.stringify(toolCall)}`);
558
+ assert(context.openSession);
559
+ context.openSession.broadcast({
560
+ type: "agent_tool_call",
561
+ message_id: toolCall.id,
562
+ message: toolCall,
563
+ });
259
564
  return true;
260
565
  };
261
566
 
@@ -274,7 +579,14 @@ export class ConversationManager {
274
579
  true
275
580
  );
276
581
 
277
- openSession = new OpenSession(agent, smsm, onEmpty);
582
+ openSession = new OpenSession(
583
+ this.db,
584
+ agent,
585
+ sessionId,
586
+ agentProfileUUID,
587
+ smsm,
588
+ onEmpty
589
+ );
278
590
  context.openSession = openSession;
279
591
  this.openSessions[sessionId] = openSession;
280
592
  openSession.join(userName, ws);
package/src/chat/db.ts CHANGED
@@ -31,7 +31,7 @@ export function resolveCompoundName(name: string): string | [string, string] {
31
31
  }
32
32
 
33
33
  export type UserData = {
34
- user_uuid: supabase.Tables<"api_keys">["user_uuid"];
34
+ uuid: supabase.Tables<"api_keys">["user_uuid"];
35
35
  nickname: supabase.Tables<"users">["nickname"];
36
36
  };
37
37
 
@@ -67,11 +67,24 @@ export class Database {
67
67
  }
68
68
 
69
69
  return {
70
- user_uuid: data.user_uuid,
70
+ uuid: data.user_uuid,
71
71
  nickname: data.users.nickname || `user ${data.user_uuid}`,
72
72
  };
73
73
  }
74
74
 
75
+ async getUserFromUuid(user_uuid: string): Promise<UserData | undefined> {
76
+ const { data, error } = await this.client
77
+ .from("users")
78
+ .select("*")
79
+ .eq("uuid", user_uuid)
80
+ .maybeSingle();
81
+ if (error) {
82
+ throw error;
83
+ }
84
+
85
+ return data;
86
+ }
87
+
75
88
  async createUser(
76
89
  user_uuid: string,
77
90
  email: string,
@@ -196,6 +209,19 @@ export class Database {
196
209
  return data[0].uuid;
197
210
  }
198
211
 
212
+ async updateAgentProfile(uuid: string, profile: AgentProfile): Promise<void> {
213
+ const payload: supabase.TablesUpdate<"agent_profiles"> = {
214
+ profile: profile as unknown as supabase.Json,
215
+ };
216
+ const { error } = await this.client
217
+ .from("agent_profiles")
218
+ .update(payload)
219
+ .eq("uuid", uuid);
220
+ if (error) {
221
+ throw error;
222
+ }
223
+ }
224
+
199
225
  async clearAgentProfiles(): Promise<void> {
200
226
  await this.client.from("agent_profiles").delete().neq("uuid", "");
201
227
  }
@@ -0,0 +1,123 @@
1
+ import { ServerToClient, ClientToServer } from "./messages";
2
+
3
+ type OnMessageCallback = (msg: ServerToClient) => void;
4
+ type OnConnectionClosedCallback = () => void;
5
+
6
+ export class FrontendChatClient {
7
+ private ws: WebSocket;
8
+ private onMessageCB: OnMessageCallback;
9
+ private onConnectionClosedCB: OnConnectionClosedCallback;
10
+ private closed: boolean;
11
+
12
+ private constructor(
13
+ ws: WebSocket,
14
+ onMessageCB: OnMessageCallback,
15
+ onConnectionClosedCB: OnConnectionClosedCallback
16
+ ) {
17
+ this.ws = ws;
18
+ this.onMessageCB = onMessageCB;
19
+ this.onConnectionClosedCB = onConnectionClosedCB;
20
+ this.closed = false;
21
+ }
22
+
23
+ static async initWithParams(
24
+ host: string,
25
+ port: number,
26
+ token: string,
27
+ params: Record<string, string>,
28
+ onMessageCB: OnMessageCallback,
29
+ onConnectionClosedCB: OnConnectionClosedCallback
30
+ ): Promise<FrontendChatClient> {
31
+ return new Promise((resolve, reject) => {
32
+ const urlParams = new URLSearchParams(params);
33
+ const url = `ws://${host}:${port}?${urlParams}`;
34
+
35
+ const ws = new WebSocket(url, [token]);
36
+ console.log("created ws");
37
+
38
+ const client = new FrontendChatClient(
39
+ ws,
40
+ onMessageCB,
41
+ onConnectionClosedCB
42
+ );
43
+
44
+ ws.onopen = async () => {
45
+ console.log("opened");
46
+
47
+ ws.onmessage = (ev: MessageEvent) => {
48
+ try {
49
+ const msgData = ev.data;
50
+ if (typeof msgData !== "string") {
51
+ throw `expected "string" data, got ${typeof msgData}`;
52
+ }
53
+ console.debug(`[client.onmessage]: ${msgData}`);
54
+ const msg: ServerToClient = JSON.parse(msgData);
55
+ client.onMessageCB(msg);
56
+ } catch (e) {
57
+ client.close();
58
+ throw e;
59
+ }
60
+ };
61
+ resolve(client);
62
+ };
63
+
64
+ ws.onclose = (event) => {
65
+ console.log("closed");
66
+ console.log(
67
+ `[client] WebSocket connection closed: ${JSON.stringify(event)}`
68
+ );
69
+ client.closed = true;
70
+ onConnectionClosedCB();
71
+ };
72
+
73
+ ws.onerror = (error) => {
74
+ console.error("[client] WebSocket error:", error);
75
+ reject(error);
76
+
77
+ client.closed = true;
78
+ onConnectionClosedCB();
79
+ };
80
+ });
81
+ }
82
+
83
+ public static async init(
84
+ host: string,
85
+ port: number,
86
+ token: string,
87
+ onMessageCB: OnMessageCallback,
88
+ onConnectionClosedCB: OnConnectionClosedCallback,
89
+ sessionId: string = "untitled",
90
+ agentProfileId: string | undefined = undefined
91
+ ): Promise<FrontendChatClient> {
92
+ const params: Record<string, string> = { session_id: sessionId };
93
+ if (agentProfileId) {
94
+ params["agent_profile_id"] = agentProfileId;
95
+ }
96
+ return FrontendChatClient.initWithParams(
97
+ host,
98
+ port,
99
+ token,
100
+ params,
101
+ onMessageCB,
102
+ onConnectionClosedCB
103
+ );
104
+ }
105
+
106
+ public sendMessage(message: ClientToServer): void {
107
+ if (this.closed) {
108
+ throw new Error("Cannot send message on closed connection");
109
+ }
110
+ const data = JSON.stringify(message);
111
+ this.ws.send(data);
112
+ }
113
+
114
+ public close(): void {
115
+ this.closed = true;
116
+ this.onConnectionClosedCB();
117
+ this.ws.close();
118
+ }
119
+
120
+ public isClosed(): boolean {
121
+ return this.closed;
122
+ }
123
+ }