@xalia/agent 0.5.8 → 0.6.0

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 (185) hide show
  1. package/README.md +23 -8
  2. package/dist/agent/src/agent/agent.js +173 -96
  3. package/dist/agent/src/agent/agentUtils.js +82 -53
  4. package/dist/agent/src/agent/compressingContextManager.js +102 -0
  5. package/dist/agent/src/agent/context.js +189 -0
  6. package/dist/agent/src/agent/dummyLLM.js +46 -5
  7. package/dist/agent/src/agent/iAgentEventHandler.js +2 -0
  8. package/dist/agent/src/agent/mcpServerManager.js +22 -23
  9. package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
  10. package/dist/agent/src/agent/nullPlatform.js +14 -0
  11. package/dist/agent/src/agent/openAILLMStreaming.js +12 -7
  12. package/dist/agent/src/agent/promptProvider.js +63 -0
  13. package/dist/agent/src/agent/repeatLLM.js +5 -5
  14. package/dist/agent/src/agent/sudoMcpServerManager.js +11 -9
  15. package/dist/agent/src/agent/tokenAuth.js +7 -7
  16. package/dist/agent/src/agent/tools.js +1 -1
  17. package/dist/agent/src/chat/client/chatClient.js +733 -0
  18. package/dist/agent/src/chat/client/connection.js +209 -0
  19. package/dist/agent/src/chat/client/connection.test.js +188 -0
  20. package/dist/agent/src/chat/client/constants.js +5 -0
  21. package/dist/agent/src/chat/client/index.js +15 -0
  22. package/dist/agent/src/chat/client/interfaces.js +2 -0
  23. package/dist/agent/src/chat/client/responseHandler.js +105 -0
  24. package/dist/agent/src/chat/client/sessionClient.js +331 -0
  25. package/dist/agent/src/chat/client/teamManager.js +2 -0
  26. package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
  27. package/dist/agent/src/chat/data/dataModels.js +2 -0
  28. package/dist/agent/src/chat/data/database.js +749 -0
  29. package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
  30. package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
  31. package/dist/agent/src/chat/protocol/constants.js +50 -0
  32. package/dist/agent/src/chat/protocol/errors.js +22 -0
  33. package/dist/agent/src/chat/protocol/messages.js +110 -0
  34. package/dist/agent/src/chat/server/chatContextManager.js +405 -0
  35. package/dist/agent/src/chat/server/connectionManager.js +352 -0
  36. package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
  37. package/dist/agent/src/chat/server/conversation.js +198 -0
  38. package/dist/agent/src/chat/server/errorUtils.js +23 -0
  39. package/dist/agent/src/chat/server/openSession.js +869 -0
  40. package/dist/agent/src/chat/server/server.js +177 -0
  41. package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
  42. package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
  43. package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
  44. package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
  45. package/dist/agent/src/chat/server/tools.js +243 -0
  46. package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
  47. package/dist/agent/src/chat/utils/approvalManager.js +85 -0
  48. package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
  49. package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
  50. package/dist/agent/src/chat/utils/htmlToText.js +84 -0
  51. package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
  52. package/dist/agent/src/chat/utils/search.js +145 -0
  53. package/dist/agent/src/chat/utils/userResolver.js +46 -0
  54. package/dist/agent/src/chat/{websocket.js → utils/websocket.js} +2 -0
  55. package/dist/agent/src/test/agent.test.js +332 -0
  56. package/dist/agent/src/test/approvalManager.test.js +58 -0
  57. package/dist/agent/src/test/chatContextManager.test.js +392 -0
  58. package/dist/agent/src/test/clientServerConnection.test.js +158 -0
  59. package/dist/agent/src/test/compressingContextManager.test.js +65 -0
  60. package/dist/agent/src/test/context.test.js +83 -0
  61. package/dist/agent/src/test/conversation.test.js +89 -0
  62. package/dist/agent/src/test/db.test.js +262 -90
  63. package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
  64. package/dist/agent/src/test/dbTestTools.js +99 -0
  65. package/dist/agent/src/test/imageLoad.test.js +8 -7
  66. package/dist/agent/src/test/mcpServerManager.test.js +21 -18
  67. package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
  68. package/dist/agent/src/test/openaiStreaming.test.js +12 -11
  69. package/dist/agent/src/test/prompt.test.js +5 -4
  70. package/dist/agent/src/test/promptProvider.test.js +28 -0
  71. package/dist/agent/src/test/responseHandler.test.js +61 -0
  72. package/dist/agent/src/test/sudoMcpServerManager.test.js +14 -12
  73. package/dist/agent/src/test/testTools.js +109 -0
  74. package/dist/agent/src/test/tools.test.js +31 -0
  75. package/dist/agent/src/tool/agentChat.js +21 -10
  76. package/dist/agent/src/tool/agentMain.js +1 -1
  77. package/dist/agent/src/tool/chatMain.js +235 -58
  78. package/dist/agent/src/tool/commandPrompt.js +15 -9
  79. package/dist/agent/src/tool/files.js +20 -16
  80. package/dist/agent/src/tool/nodePlatform.js +47 -3
  81. package/dist/agent/src/tool/options.js +4 -4
  82. package/dist/agent/src/tool/prompt.js +19 -13
  83. package/eslint.config.mjs +14 -1
  84. package/package.json +14 -6
  85. package/scripts/chat_server +8 -0
  86. package/scripts/setup_chat +7 -2
  87. package/scripts/shutdown_chat_server +3 -0
  88. package/scripts/test_chat +135 -17
  89. package/src/agent/agent.ts +270 -135
  90. package/src/agent/agentUtils.ts +136 -95
  91. package/src/agent/compressingContextManager.ts +164 -0
  92. package/src/agent/context.ts +268 -0
  93. package/src/agent/dummyLLM.ts +76 -8
  94. package/src/agent/iAgentEventHandler.ts +54 -0
  95. package/src/agent/iplatform.ts +1 -0
  96. package/src/agent/mcpServerManager.ts +32 -30
  97. package/src/agent/nullAgentEventHandler.ts +20 -0
  98. package/src/agent/nullPlatform.ts +13 -0
  99. package/src/agent/openAILLMStreaming.ts +12 -6
  100. package/src/agent/promptProvider.ts +87 -0
  101. package/src/agent/repeatLLM.ts +5 -5
  102. package/src/agent/sudoMcpServerManager.ts +13 -11
  103. package/src/agent/tokenAuth.ts +7 -7
  104. package/src/agent/tools.ts +3 -1
  105. package/src/chat/client/chatClient.ts +900 -0
  106. package/src/chat/client/connection.test.ts +241 -0
  107. package/src/chat/client/connection.ts +276 -0
  108. package/src/chat/client/constants.ts +3 -0
  109. package/src/chat/client/index.ts +18 -0
  110. package/src/chat/client/interfaces.ts +34 -0
  111. package/src/chat/client/responseHandler.ts +131 -0
  112. package/src/chat/client/sessionClient.ts +443 -0
  113. package/src/chat/client/teamManager.ts +29 -0
  114. package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
  115. package/src/chat/data/dataModels.ts +85 -0
  116. package/src/chat/data/database.ts +982 -0
  117. package/src/chat/data/dbMcpServerConfigs.ts +59 -0
  118. package/src/chat/protocol/connectionMessages.ts +49 -0
  119. package/src/chat/protocol/constants.ts +55 -0
  120. package/src/chat/protocol/errors.ts +16 -0
  121. package/src/chat/protocol/messages.ts +682 -0
  122. package/src/chat/server/README.md +127 -0
  123. package/src/chat/server/chatContextManager.ts +612 -0
  124. package/src/chat/server/connectionManager.test.ts +266 -0
  125. package/src/chat/server/connectionManager.ts +541 -0
  126. package/src/chat/server/conversation.ts +269 -0
  127. package/src/chat/server/errorUtils.ts +28 -0
  128. package/src/chat/server/openSession.ts +1332 -0
  129. package/src/chat/server/server.ts +177 -0
  130. package/src/chat/server/sessionFileManager.ts +239 -0
  131. package/src/chat/server/sessionRegistry.test.ts +138 -0
  132. package/src/chat/server/sessionRegistry.ts +1064 -0
  133. package/src/chat/server/test-utils/mockFactories.ts +422 -0
  134. package/src/chat/server/tools.ts +265 -0
  135. package/src/chat/utils/agentSessionMap.ts +76 -0
  136. package/src/chat/utils/approvalManager.ts +111 -0
  137. package/src/{utils → chat/utils}/asyncLock.ts +3 -3
  138. package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
  139. package/src/chat/utils/htmlToText.ts +61 -0
  140. package/src/chat/utils/multiAsyncQueue.ts +52 -0
  141. package/src/chat/utils/search.ts +139 -0
  142. package/src/chat/utils/userResolver.ts +48 -0
  143. package/src/chat/{websocket.ts → utils/websocket.ts} +2 -0
  144. package/src/test/agent.test.ts +487 -0
  145. package/src/test/approvalManager.test.ts +73 -0
  146. package/src/test/chatContextManager.test.ts +521 -0
  147. package/src/test/clientServerConnection.test.ts +207 -0
  148. package/src/test/compressingContextManager.test.ts +82 -0
  149. package/src/test/context.test.ts +105 -0
  150. package/src/test/conversation.test.ts +109 -0
  151. package/src/test/db.test.ts +351 -103
  152. package/src/test/dbMcpServerConfigs.test.ts +112 -0
  153. package/src/test/dbTestTools.ts +153 -0
  154. package/src/test/imageLoad.test.ts +7 -6
  155. package/src/test/mcpServerManager.test.ts +19 -14
  156. package/src/test/multiAsyncQueue.test.ts +125 -0
  157. package/src/test/openaiStreaming.test.ts +11 -10
  158. package/src/test/prompt.test.ts +4 -3
  159. package/src/test/promptProvider.test.ts +33 -0
  160. package/src/test/responseHandler.test.ts +78 -0
  161. package/src/test/sudoMcpServerManager.test.ts +22 -15
  162. package/src/test/testTools.ts +146 -0
  163. package/src/test/tools.test.ts +39 -0
  164. package/src/tool/agentChat.ts +26 -12
  165. package/src/tool/agentMain.ts +1 -1
  166. package/src/tool/chatMain.ts +283 -100
  167. package/src/tool/commandPrompt.ts +25 -9
  168. package/src/tool/files.ts +25 -19
  169. package/src/tool/nodePlatform.ts +52 -3
  170. package/src/tool/options.ts +4 -2
  171. package/src/tool/prompt.ts +22 -15
  172. package/test_data/dummyllm_script_crash.json +32 -0
  173. package/test_data/frog.png.b64 +1 -0
  174. package/vitest.config.ts +39 -0
  175. package/dist/agent/src/chat/client.js +0 -310
  176. package/dist/agent/src/chat/conversationManager.js +0 -502
  177. package/dist/agent/src/chat/db.js +0 -218
  178. package/dist/agent/src/chat/messages.js +0 -29
  179. package/dist/agent/src/chat/server.js +0 -158
  180. package/src/chat/client.ts +0 -445
  181. package/src/chat/conversationManager.ts +0 -730
  182. package/src/chat/db.ts +0 -304
  183. package/src/chat/messages.ts +0 -266
  184. package/src/chat/server.ts +0 -177
  185. /package/{frog.png → test_data/frog.png} +0 -0
@@ -0,0 +1,900 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+
3
+ import { getLogger, SavedAgentProfile } from "@xalia/xmcp/sdk";
4
+
5
+ import { Connection } from "./connection";
6
+ import { SessionClient } from "./sessionClient";
7
+ import { IChatClientEventHandler } from "./interfaces";
8
+ import {
9
+ SessionData,
10
+ TeamInfo,
11
+ TeamParticipant,
12
+ AgentSessionData,
13
+ } from "../data/dataModels";
14
+ import { createSessionParticipantMap } from "../data/database";
15
+ import {
16
+ ServerSessionScopedMessage,
17
+ ServerSessionInfo,
18
+ isServerSessionScopedMessage,
19
+ ServerControlError,
20
+ isServerControlMessage,
21
+ ServerControlMessage,
22
+ ServerControlSessionDeleted,
23
+ ServerControlTeamCreated,
24
+ ServerToClient,
25
+ ClientToServer,
26
+ ServerControlAgentProfileCreated,
27
+ ServerControlAgentProfileDeleted,
28
+ } from "../protocol/messages";
29
+ import { SESSION_CREATE_TIMEOUT, SESSION_JOIN_TIMEOUT } from "./constants";
30
+ import { ITeamManager } from "./teamManager";
31
+ import { buildAgentSessionMap } from "../utils/agentSessionMap";
32
+
33
+ const logger = getLogger();
34
+
35
+ // client team info, used for session tracking etc.
36
+ export type ClientTeamInfo = {
37
+ team_uuid: string;
38
+ team_name: string;
39
+ owner_uuid: string;
40
+ participants: Map<string, TeamParticipant>;
41
+ agentSessionMap: Map<string, AgentSessionData>;
42
+ };
43
+
44
+ type SessionJoinOrCreateResolver = {
45
+ resolve: (
46
+ sessionClient: SessionClient,
47
+ sessionInfo: ServerSessionInfo
48
+ ) => void;
49
+ reject: (error: Error) => void;
50
+ sessionId: string;
51
+ clientMessageId: string;
52
+ };
53
+
54
+ export class ChatClient implements ITeamManager {
55
+ private constructor(
56
+ private connection: Connection<ClientToServer, ServerToClient>,
57
+ private eventHandler: IChatClientEventHandler,
58
+ // agent_uuid -> agent session data
59
+ private userAgentSessionMap: Map<string, AgentSessionData> = new Map(),
60
+ // team_uuid -> team info
61
+ private teams: Map<string, ClientTeamInfo> = new Map(),
62
+ private closed: boolean = false,
63
+ private currentSessionId: string | undefined = undefined,
64
+ // note: currentTeamId will not be reset when swtiching to a user session.
65
+ private currentTeamId: string | undefined = undefined,
66
+ // session_uuid -> session client
67
+ private activeSessions: Map<string, SessionClient> = new Map(),
68
+ // session_uuid -> true if there is a new message.
69
+ private newMessage: Map<string, boolean> = new Map(),
70
+ // Track pending session join request, this should be a singleton
71
+ private sessionJoinOrCreateRes:
72
+ | SessionJoinOrCreateResolver
73
+ | undefined = undefined
74
+ ) {}
75
+
76
+ /**
77
+ * Connect to server and create ChatClient instance
78
+ */
79
+ static async init(
80
+ url: string,
81
+ token: string,
82
+ eventHandler: IChatClientEventHandler
83
+ ): Promise<ChatClient> {
84
+ const connection = new Connection<ClientToServer, ServerToClient>({
85
+ url,
86
+ token,
87
+ });
88
+ return new Promise((resolveClient, reject) => {
89
+ let client: ChatClient | undefined = undefined;
90
+ const clientMessageId = uuidv4();
91
+
92
+ // Register session_info handler for initialization
93
+ connection.on("control_session_list", (msg) => {
94
+ // get user sessions, user agents, and team sessions
95
+ const userSessions = new Map<string, SessionData>();
96
+ msg.user_sessions.forEach((session) => {
97
+ userSessions.set(session.session_uuid, session);
98
+ });
99
+
100
+ const userAgents = new Map<string, SavedAgentProfile>();
101
+ msg.user_agents.forEach((agent) => {
102
+ userAgents.set(agent.uuid, agent);
103
+ });
104
+
105
+ const userAgentSessionMap = buildAgentSessionMap(
106
+ userSessions,
107
+ userAgents
108
+ );
109
+ logger.info(
110
+ `[ChatClient.init] session list msg: ${JSON.stringify(msg)}`
111
+ );
112
+ for (const agentSession of userAgentSessionMap.values()) {
113
+ logger.info(
114
+ `[ChatClient.init] agent: ` +
115
+ agentSession.agent_profile.profile_name
116
+ );
117
+ }
118
+
119
+ const teams = new Map<string, ClientTeamInfo>();
120
+ msg.team_sessions.forEach((team) => {
121
+ teams.set(team.team_uuid, ChatClient.toClientTeamInfo(team));
122
+ });
123
+
124
+ if (!client) {
125
+ logger.info("Creating ChatClient");
126
+ client = new ChatClient(
127
+ connection,
128
+ eventHandler,
129
+ userAgentSessionMap,
130
+ teams
131
+ );
132
+ resolveClient(client);
133
+ } else {
134
+ client.userAgentSessionMap = userAgentSessionMap;
135
+ client.teams = teams;
136
+ void client.eventHandler.onMessage(msg, client);
137
+ }
138
+ });
139
+
140
+ // Register general message handler for established client
141
+ connection.on("*", (msg) => {
142
+ if (!client) {
143
+ if (msg.type === "control_error") {
144
+ logger.error(
145
+ `[ChatClient] client error (init): ${JSON.stringify(msg)}`
146
+ );
147
+ reject(new Error(msg.message));
148
+ } else {
149
+ logger.error(
150
+ `[ChatClient] invalid message (init): ${JSON.stringify(msg.type)}`
151
+ );
152
+ }
153
+ return;
154
+ }
155
+ if (isServerControlMessage(msg)) {
156
+ client.handleControlMessage(msg);
157
+ } else if (isServerSessionScopedMessage(msg)) {
158
+ client.handleSessionMessage(msg);
159
+ } else {
160
+ logger.error(`[ChatClient] unhandled msg: ${JSON.stringify(msg)}`);
161
+ }
162
+ });
163
+
164
+ // Register direct error callback for client-side errors.
165
+ connection.onError((errorMessage) => {
166
+ if (client) {
167
+ client.connection.close();
168
+ }
169
+ eventHandler.onError(`Connection error: ${errorMessage}`);
170
+ });
171
+
172
+ // Start connection
173
+ connection
174
+ .connect()
175
+ .then(() => {
176
+ connection.send({
177
+ type: "control_get_session_list",
178
+ client_message_id: clientMessageId,
179
+ });
180
+ })
181
+ .catch((error: unknown) => {
182
+ reject(error instanceof Error ? error : new Error(String(error)));
183
+ });
184
+ });
185
+ }
186
+
187
+ private static toClientTeamInfo(teamInfo: TeamInfo): ClientTeamInfo {
188
+ const sessions = new Map(
189
+ teamInfo.sessions.map((session) => [session.session_uuid, session])
190
+ );
191
+ const agents = new Map(teamInfo.agents.map((agent) => [agent.uuid, agent]));
192
+ const agentSessionMap = buildAgentSessionMap(sessions, agents);
193
+ return {
194
+ team_uuid: teamInfo.team_uuid,
195
+ team_name: teamInfo.team_name,
196
+ owner_uuid: teamInfo.owner_uuid,
197
+ participants: new Map(
198
+ teamInfo.participants.map((participant) => [
199
+ participant.user_uuid,
200
+ participant,
201
+ ])
202
+ ),
203
+ agentSessionMap,
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Get the current session.
209
+ * Will throw an error if the current session is not set.
210
+ * @param location - The location of the caller.
211
+ * @returns The current session client.
212
+ */
213
+ public getCurrentSession(location: string): SessionClient {
214
+ const session = this.getCurrentSessionMaybe();
215
+ if (!session) {
216
+ throw new Error(`[${location}]: No current session`);
217
+ }
218
+ return session;
219
+ }
220
+
221
+ public getCurrentSessionMaybe(): SessionClient | undefined {
222
+ if (!this.currentSessionId) {
223
+ return undefined;
224
+ }
225
+ return this.activeSessions.get(this.currentSessionId);
226
+ }
227
+
228
+ public getCurrentAgentUuid(): string | undefined {
229
+ const session = this.getCurrentSessionMaybe();
230
+ if (!session) {
231
+ return undefined;
232
+ }
233
+ return session.getAgentUuid();
234
+ }
235
+
236
+ getUserAgentSessionMap(): Map<string, AgentSessionData> {
237
+ return this.userAgentSessionMap;
238
+ }
239
+
240
+ getClientTeamInfo(teamUuid: string): ClientTeamInfo | undefined {
241
+ return this.teams.get(teamUuid);
242
+ }
243
+
244
+ // ITeamManager -> getCurrentTeamInfo
245
+ getCurrentTeamInfo(): ClientTeamInfo | undefined {
246
+ if (!this.currentTeamId) {
247
+ return undefined;
248
+ }
249
+ return this.teams.get(this.currentTeamId);
250
+ }
251
+
252
+ // ITeamManager -> getTeams
253
+ getTeams(): Map<string, ClientTeamInfo> {
254
+ return this.teams;
255
+ }
256
+
257
+ /**
258
+ * Get the currently active session ID
259
+ */
260
+ getCurrentSessionId(): string | undefined {
261
+ return this.currentSessionId;
262
+ }
263
+
264
+ /**
265
+ * Get the currently team ID
266
+ */
267
+ getCurrentTeamId(): string | undefined {
268
+ return this.currentTeamId;
269
+ }
270
+
271
+ // ITeamManager -> setCurrentTeamId
272
+ setCurrentTeamId(teamUuid: string): void {
273
+ if (!this.teams.has(teamUuid)) {
274
+ throw new Error(`Team ${teamUuid} not found`);
275
+ }
276
+ this.currentTeamId = teamUuid;
277
+ }
278
+
279
+ /**
280
+ * Check if client is connected and operational
281
+ */
282
+ isClosed(): boolean {
283
+ return this.closed;
284
+ }
285
+
286
+ shutdown(): void {
287
+ this.connection.close();
288
+ this.closed = true;
289
+ this.activeSessions.clear();
290
+ this.newMessage.clear();
291
+ this.teams.clear();
292
+ this.currentSessionId = undefined;
293
+ this.currentTeamId = undefined;
294
+ this.sessionJoinOrCreateRes = undefined;
295
+ }
296
+
297
+ public async createNewSession(
298
+ newTitle: string,
299
+ agentProfileId?: string,
300
+ teamId?: string
301
+ ): Promise<SessionClient> {
302
+ if (this.closed) {
303
+ throw new Error("ChatClient is closed");
304
+ }
305
+
306
+ if (this.sessionJoinOrCreateRes) {
307
+ logger.error(
308
+ `[ChatClient] creating new session with ` +
309
+ `profile id ${JSON.stringify(agentProfileId)}`
310
+ );
311
+ throw new Error("Session join/create is already in progress");
312
+ }
313
+
314
+ return new Promise((resolve, reject) => {
315
+ const clientMessageId = uuidv4();
316
+
317
+ // Set up timeout for the request
318
+ const timeoutId = setTimeout(() => {
319
+ this.sessionJoinOrCreateRes = undefined;
320
+ reject(new Error(`Session join timeout for creating new session`));
321
+ }, SESSION_CREATE_TIMEOUT);
322
+
323
+ // Track this pending request
324
+ this.sessionJoinOrCreateRes = {
325
+ resolve: (
326
+ sessionClient: SessionClient,
327
+ sessionInfo: ServerSessionInfo
328
+ ) => {
329
+ const sessionId = this.sessionJoinOrCreateRes?.sessionId;
330
+ if (!sessionId) {
331
+ throw new Error("Session id is not set in resolving.");
332
+ }
333
+ if (sessionId !== sessionInfo.session_id) {
334
+ throw new Error("SessionInfo id mismatch");
335
+ }
336
+ clearTimeout(timeoutId);
337
+ this.activeSessions.set(sessionId, sessionClient);
338
+ this.currentSessionId = sessionId;
339
+ if (sessionInfo.team_uuid) {
340
+ this.currentTeamId = sessionInfo.team_uuid;
341
+ }
342
+ this.sessionJoinOrCreateRes = undefined;
343
+ this.addSessionToAgentSessionMap(sessionInfo);
344
+ resolve(sessionClient);
345
+ },
346
+ reject: (error: Error) => {
347
+ clearTimeout(timeoutId);
348
+ this.sessionJoinOrCreateRes = undefined;
349
+ reject(error);
350
+ },
351
+ sessionId: "",
352
+ clientMessageId,
353
+ };
354
+
355
+ // Send session_create message
356
+ try {
357
+ this.connection.send({
358
+ type: "control_session_create",
359
+ client_message_id: clientMessageId,
360
+ title: newTitle,
361
+ agent_profile_id: agentProfileId,
362
+ team_id: teamId,
363
+ });
364
+ logger.info(
365
+ `[ChatClient] Sent session_create for session ${newTitle}` +
366
+ ` client message id: ${clientMessageId}`
367
+ );
368
+ } catch (error) {
369
+ this.sessionJoinOrCreateRes = undefined;
370
+ clearTimeout(timeoutId);
371
+ reject(error instanceof Error ? error : new Error(String(error)));
372
+ }
373
+ });
374
+ }
375
+
376
+ /**
377
+ * Connect to an existing session by sending session_join message
378
+ * and waiting for session_info response from server
379
+ */
380
+ async connectToSession(sessionId: string): Promise<SessionClient> {
381
+ if (this.closed) {
382
+ throw new Error("ChatClient is closed");
383
+ }
384
+
385
+ if (this.sessionJoinOrCreateRes) {
386
+ logger.error(`[ChatClient] connecting to session ${sessionId}`);
387
+ throw new Error("Session join/create is already in progress");
388
+ }
389
+
390
+ // TODO: we can directly return a session client if we cache conversation.
391
+ // For now, we go through the join process.
392
+ return new Promise((resolve, reject) => {
393
+ const clientMessageId = uuidv4();
394
+
395
+ // Set up timeout for the request
396
+ const timeoutId = setTimeout(() => {
397
+ this.sessionJoinOrCreateRes = undefined;
398
+ reject(new Error(`Session join timeout for session ${sessionId}`));
399
+ }, SESSION_JOIN_TIMEOUT);
400
+
401
+ // Track this pending request
402
+ this.sessionJoinOrCreateRes = {
403
+ resolve: (
404
+ sessionClient: SessionClient,
405
+ sessionInfo: ServerSessionInfo
406
+ ) => {
407
+ clearTimeout(timeoutId);
408
+ this.activeSessions.set(sessionId, sessionClient);
409
+ this.currentSessionId = sessionId;
410
+ if (sessionInfo.team_uuid) {
411
+ this.currentTeamId = sessionInfo.team_uuid;
412
+ }
413
+ this.sessionJoinOrCreateRes = undefined;
414
+ this.updateSessionInfo(sessionInfo);
415
+ logger.info(`[ChatClient] joined session ${sessionId}`);
416
+ resolve(sessionClient);
417
+ },
418
+ reject: (error: Error) => {
419
+ clearTimeout(timeoutId);
420
+ this.sessionJoinOrCreateRes = undefined;
421
+ logger.error(
422
+ `[ChatClient] failed to join session` +
423
+ ` ${sessionId}: ${error.message}`
424
+ );
425
+ reject(error);
426
+ },
427
+ sessionId,
428
+ clientMessageId,
429
+ };
430
+
431
+ // Send session_join message
432
+ try {
433
+ this.connection.send({
434
+ type: "control_session_join",
435
+ client_message_id: clientMessageId,
436
+ target_session_id: sessionId,
437
+ });
438
+ logger.debug(`[ChatClient] Sent session_join for session ${sessionId}`);
439
+ } catch (error) {
440
+ this.sessionJoinOrCreateRes = undefined;
441
+ clearTimeout(timeoutId);
442
+ reject(error instanceof Error ? error : new Error(String(error)));
443
+ }
444
+ });
445
+ }
446
+
447
+ /**
448
+ * Fetch the session list from the server
449
+ */
450
+ fetchSessionList(): void {
451
+ this.connection.send({
452
+ type: "control_get_session_list",
453
+ client_message_id: uuidv4(),
454
+ });
455
+ }
456
+
457
+ /**
458
+ * Delete a session by sending session_delete_request message
459
+ * and waiting for session_deleted response from server
460
+ * @param sessionId
461
+ * @returns void
462
+ */
463
+ deleteSession(sessionId: string): void {
464
+ if (this.closed) {
465
+ throw new Error("ChatClient is closed");
466
+ }
467
+
468
+ this.connection.send({
469
+ type: "control_session_delete",
470
+ client_message_id: uuidv4(),
471
+ target_session_id: sessionId,
472
+ });
473
+
474
+ logger.debug(
475
+ `[ChatClient] Sent session_delete_request for` + ` session ${sessionId}`
476
+ );
477
+ }
478
+
479
+ /**
480
+ * Create a new team with initial members
481
+ * @param teamName
482
+ * @param initialMembers could be UUIDs or emails
483
+ */
484
+ createNewTeam(teamName: string, initialMembers: string[]): void {
485
+ if (this.closed) {
486
+ throw new Error("ChatClient is closed");
487
+ }
488
+ // send team_create_request message
489
+ try {
490
+ this.connection.send({
491
+ type: "control_team_create",
492
+ client_message_id: uuidv4(),
493
+ team_name: teamName,
494
+ initial_members: initialMembers,
495
+ });
496
+ } catch (error) {
497
+ this.eventHandler.onError(String(error));
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Add a user to a team
503
+ * @param teamId - UUID of the team
504
+ * @param userId - UUID of the user to add
505
+ */
506
+ addTeamMember(teamId: string, userId: string): void {
507
+ if (this.closed) {
508
+ throw new Error("ChatClient is closed");
509
+ }
510
+ try {
511
+ this.connection.send({
512
+ type: "control_add_team_user",
513
+ client_message_id: uuidv4(),
514
+ target_team_id: teamId,
515
+ user_uuid_or_email: userId,
516
+ });
517
+ } catch (error) {
518
+ this.eventHandler.onError(String(error));
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Remove a user from a team
524
+ * @param teamId - UUID of the team
525
+ * @param userId - UUID of the user to remove
526
+ */
527
+ removeTeamMember(teamId: string, userId: string): void {
528
+ if (this.closed) {
529
+ throw new Error("ChatClient is closed");
530
+ }
531
+ try {
532
+ this.connection.send({
533
+ type: "control_remove_team_user",
534
+ client_message_id: uuidv4(),
535
+ target_team_id: teamId,
536
+ user_uuid_or_email: userId,
537
+ });
538
+ } catch (error) {
539
+ this.eventHandler.onError(String(error));
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Create a new agent
545
+ * @param agentName - the agent name
546
+ * @param templateName - the template name
547
+ * @param teamUuid - the team uuid
548
+ */
549
+ createNewAgent(
550
+ agentName: string,
551
+ templateName?: string,
552
+ teamUuid?: string
553
+ ): void {
554
+ if (this.closed) {
555
+ throw new Error("ChatClient is closed");
556
+ }
557
+ try {
558
+ this.connection.send({
559
+ type: "control_agent_profile_create",
560
+ title: agentName,
561
+ template_name: templateName,
562
+ team_uuid: teamUuid,
563
+ });
564
+ } catch (error) {
565
+ this.eventHandler.onError(String(error));
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Delete a session from the agent session map
571
+ * @param agentSessionMap - the agent session map
572
+ * @param agentUuid - the agent uuid
573
+ * @param sessionId - the session id
574
+ * @returns true if the session is deleted, false otherwise
575
+ */
576
+ private static deleteSessionFromAgentSessionMap(
577
+ agentSessionMap: Map<string, AgentSessionData>,
578
+ agentUuid: string,
579
+ sessionId: string
580
+ ): boolean {
581
+ const agentSession = agentSessionMap.get(agentUuid);
582
+ if (!agentSession) {
583
+ return false;
584
+ }
585
+ const size = agentSession.sessions.length;
586
+ agentSession.sessions = agentSession.sessions.filter(
587
+ (session) => session.session_uuid !== sessionId
588
+ );
589
+ return size > agentSession.sessions.length;
590
+ }
591
+
592
+ private handleSessionDeleted(msg: ServerControlSessionDeleted): void {
593
+ const sessionId = msg.session_id;
594
+ const agentUuid = msg.agent_profile_uuid;
595
+ logger.info(`[ChatClient] Session deleted: ${JSON.stringify(msg)}`);
596
+ let deleted = false;
597
+ if (msg.team_uuid) {
598
+ deleted = ChatClient.deleteSessionFromAgentSessionMap(
599
+ this.teams.get(msg.team_uuid)?.agentSessionMap ??
600
+ new Map<string, AgentSessionData>(),
601
+ agentUuid,
602
+ sessionId
603
+ );
604
+ } else {
605
+ deleted = ChatClient.deleteSessionFromAgentSessionMap(
606
+ this.userAgentSessionMap,
607
+ agentUuid,
608
+ sessionId
609
+ );
610
+ }
611
+ if (!deleted) {
612
+ // could be a race condition, we fetch the session list from the server
613
+ logger.warn(`[ChatClient] Session ${sessionId} is not in session map`);
614
+ this.connection.send({
615
+ type: "control_get_session_list",
616
+ client_message_id: uuidv4(),
617
+ });
618
+ }
619
+ this.activeSessions.delete(sessionId);
620
+ this.newMessage.delete(sessionId);
621
+ void this.eventHandler.onMessage(msg, this);
622
+ }
623
+
624
+ private handleControlMessage(msg: ServerControlMessage): void {
625
+ switch (msg.type) {
626
+ case "control_session_left":
627
+ this.activeSessions.delete(msg.session_id);
628
+ break;
629
+ case "control_agent_profile_created":
630
+ this.handleAgentProfileCreated(msg);
631
+ break;
632
+ case "control_agent_profile_deleted":
633
+ this.handleAgentProfileDeleted(msg);
634
+ break;
635
+ case "control_error":
636
+ this.handleControlError(msg);
637
+ break;
638
+ case "control_session_deleted":
639
+ this.handleSessionDeleted(msg);
640
+ break;
641
+ case "control_session_list":
642
+ logger.warn(`[ChatClient] Unexpected message in runtime: ${msg.type} `);
643
+ break;
644
+ case "control_team_created":
645
+ this.handleTeamCreatedMessage(msg);
646
+ break;
647
+ default: {
648
+ const _exhaustive: never = msg;
649
+ throw new Error(`unexpected control msg: ${JSON.stringify(msg)}`);
650
+ }
651
+ }
652
+ void this.eventHandler.onMessage(msg, this);
653
+ }
654
+
655
+ private handleAgentProfileCreated(msg: ServerControlAgentProfileCreated) {
656
+ logger.debug(
657
+ `[ChatClient.handleAgentProfileCreated] msg: ${JSON.stringify(msg)}`
658
+ );
659
+
660
+ const teamId = msg.profile.team_uuid;
661
+ const profileId = msg.profile.uuid;
662
+ const newAgentSession = {
663
+ agent_profile: msg.profile,
664
+ sessions: [],
665
+ updated_at: Date.now(),
666
+ };
667
+ if (teamId) {
668
+ const team = this.teams.get(teamId);
669
+ if (!team) {
670
+ throw new Error(`[handleAgentProfileCreated] no such team ${teamId}`);
671
+ }
672
+ team.agentSessionMap.set(profileId, newAgentSession);
673
+ } else {
674
+ this.userAgentSessionMap.set(profileId, newAgentSession);
675
+ }
676
+ void this.eventHandler.onMessage(msg, this);
677
+ }
678
+
679
+ private handleAgentProfileDeleted(_msg: ServerControlAgentProfileDeleted) {
680
+ throw new Error("handleAgentProfileDeleted not implemented");
681
+ }
682
+
683
+ private handleSessionMessage(msg: ServerSessionScopedMessage): void {
684
+ const sessionId = msg.session_id;
685
+
686
+ // TODO: we need to handle create new session case here, previously create
687
+ // new session's param are passed by URL, this is no longer an option
688
+ // since we are not creating a new connection for each session.
689
+ // Handle session_info message for pending session joins
690
+ if (msg.type === "session_info") {
691
+ this.handleSessionInfoMessage(msg);
692
+ // notify the event handler, i.e. UI that the session info update
693
+ void this.eventHandler.onMessage(msg, this);
694
+ return;
695
+ }
696
+
697
+ // Handle messages for existing active sessions
698
+ if (this.activeSessions.has(sessionId)) {
699
+ const sessionClient = this.activeSessions.get(sessionId);
700
+ if (sessionClient) {
701
+ sessionClient.handleMessage(msg);
702
+ // we are only calling the onMessage for session scoped message for now.
703
+ void this.eventHandler.onMessage(msg, this);
704
+ } else {
705
+ throw new Error(`Session client not found for session ${sessionId}`);
706
+ }
707
+ } else {
708
+ // Session not active yet, mark as having new messages
709
+ this.newMessage.set(sessionId, true);
710
+ }
711
+ }
712
+
713
+ private handleTeamCreatedMessage(msg: ServerControlTeamCreated): void {
714
+ const teamInfo: TeamInfo = {
715
+ team_uuid: msg.team_uuid,
716
+ team_name: msg.team_name,
717
+ owner_uuid: msg.team_owner_uuid,
718
+ participants: msg.members,
719
+ sessions: [],
720
+ agents: [],
721
+ };
722
+ this.teams.set(teamInfo.team_uuid, ChatClient.toClientTeamInfo(teamInfo));
723
+ this.currentTeamId = teamInfo.team_uuid;
724
+ void this.eventHandler.onMessage(msg, this);
725
+ return;
726
+ }
727
+
728
+ /**
729
+ * Handle session_info message which can be for:
730
+ * 1. A pending session join/create request
731
+ * 2. An update to an existing active session
732
+ * We update the session list here as well.
733
+ */
734
+ private handleSessionInfoMessage(msg: ServerSessionInfo): void {
735
+ const sessionId = msg.session_id;
736
+
737
+ // There is a pending request, check if this is the response
738
+ if (
739
+ this.sessionJoinOrCreateRes &&
740
+ msg.client_message_id === this.sessionJoinOrCreateRes.clientMessageId
741
+ ) {
742
+ const { resolve, reject } = this.sessionJoinOrCreateRes;
743
+
744
+ try {
745
+ logger.info(
746
+ `[ChatClient] Creating SessionClient for session ${sessionId}`
747
+ );
748
+ logger.info(`[ChatClient] msg: ${JSON.stringify(msg)}`);
749
+ const sessionClient = new SessionClient(
750
+ sessionId,
751
+ msg.saved_agent_profile,
752
+ this.connection,
753
+ msg.mcp_server_briefs,
754
+ createSessionParticipantMap(msg.participants)
755
+ );
756
+
757
+ // we need to pass the session id if this is a new session
758
+ if (this.sessionJoinOrCreateRes.sessionId === "") {
759
+ this.sessionJoinOrCreateRes.sessionId = sessionId;
760
+ } else if (this.sessionJoinOrCreateRes.sessionId !== sessionId) {
761
+ throw new Error(
762
+ `[ChatClient] session id mismatch: ` +
763
+ `${this.sessionJoinOrCreateRes.sessionId} !== ${sessionId}`
764
+ );
765
+ }
766
+
767
+ resolve(sessionClient, msg);
768
+ } catch (error) {
769
+ const errorMsg =
770
+ error instanceof Error ? error : new Error(String(error));
771
+ logger.error(
772
+ `[ChatClient] Failed to create SessionClient: ${errorMsg.message}`
773
+ );
774
+ reject(errorMsg);
775
+ }
776
+ } else {
777
+ this.updateSessionInfo(msg);
778
+ }
779
+ }
780
+
781
+ handleControlError(msg: ServerControlError): void {
782
+ // reject the pending session join/create request if message id matches
783
+ if (
784
+ this.sessionJoinOrCreateRes &&
785
+ msg.client_message_id === this.sessionJoinOrCreateRes.clientMessageId
786
+ ) {
787
+ this.sessionJoinOrCreateRes.reject(new Error(msg.message));
788
+ }
789
+ this.eventHandler.onError(`Server error: ${msg.message}`);
790
+ }
791
+
792
+ /**
793
+ * Update the session list. This is called when a session is updated.
794
+ * An error is thrown if the session id is not found.
795
+ * TODO: we might want to resync session map, for now, throw error to
796
+ * expose problems in the code.
797
+ */
798
+ private updateAgentSessionMap(sessionInfo: ServerSessionInfo): void {
799
+ const sessionId = sessionInfo.session_id;
800
+ if (sessionInfo.team_uuid) {
801
+ const teamInfo = this.teams.get(sessionInfo.team_uuid);
802
+ if (!teamInfo) {
803
+ throw new Error(`Team ${sessionInfo.team_uuid} not found in team list`);
804
+ }
805
+ //teamInfo.sessions.set(sessionId, ChatClient.toSessionData(sessionInfo));
806
+ const agentSessionMap = teamInfo.agentSessionMap;
807
+ if (!doUpdateAgentSessionMap(agentSessionMap, sessionInfo)) {
808
+ throw new Error(
809
+ `[updateAgentSessionMap] team session ${sessionId}` +
810
+ ` not found in agent session map`
811
+ );
812
+ }
813
+ } else {
814
+ const agentSessionMap = this.userAgentSessionMap;
815
+ if (!doUpdateAgentSessionMap(agentSessionMap, sessionInfo)) {
816
+ throw new Error(
817
+ `[updateAgentSessionMap] user session ${sessionId}` +
818
+ ` not found in agent session map`
819
+ );
820
+ }
821
+ }
822
+ }
823
+
824
+ /**
825
+ * Update the session info for an existing session. This passes the session
826
+ * info to the session client. An error is thrown if the session client is not
827
+ * found.
828
+ */
829
+ private updateSessionInfo(msg: ServerSessionInfo): void {
830
+ const sessionId = msg.session_id;
831
+ const sessionClient = this.activeSessions.get(sessionId);
832
+ this.updateAgentSessionMap(msg);
833
+ if (sessionClient) {
834
+ sessionClient.updateSessionInfo(msg);
835
+ } else {
836
+ throw new Error(`Session client not found for session ${sessionId}`);
837
+ }
838
+ }
839
+
840
+ private addSessionToAgentSessionMap(msg: ServerSessionInfo): void {
841
+ // get the correct agent session map
842
+ const agentSessionMap = msg.team_uuid
843
+ ? this.teams.get(msg.team_uuid)?.agentSessionMap
844
+ : this.userAgentSessionMap;
845
+ if (!agentSessionMap) {
846
+ throw new Error(`Invalid team uuid ${JSON.stringify(msg.team_uuid)}`);
847
+ }
848
+ // update the agent session map
849
+ const agentUuid = msg.saved_agent_profile.uuid;
850
+ const agentSession = agentSessionMap.get(agentUuid);
851
+ if (!agentSession) {
852
+ throw new Error(
853
+ `[addSessionToAgentSessionMap] Agent ${agentUuid}` +
854
+ ` not found in agent session map`
855
+ );
856
+ }
857
+ agentSession.sessions.push(sessionInfoToSessionData(msg));
858
+ agentSession.updated_at = new Date(msg.updated_at).getTime();
859
+ }
860
+ }
861
+
862
+ function sessionInfoToSessionData(msg: ServerSessionInfo): SessionData {
863
+ return {
864
+ session_uuid: msg.session_id,
865
+ title: msg.title,
866
+ team_uuid: msg.team_uuid,
867
+ agent_profile_uuid: msg.saved_agent_profile.uuid,
868
+ workspace: msg.workspace,
869
+ updated_at: msg.updated_at,
870
+ user_uuid: msg.owner_uuid,
871
+ };
872
+ }
873
+
874
+ /**
875
+ * Update the agent session map
876
+ * @param agentSessionMap - the agent session map
877
+ * @param sessionInfo - the session info
878
+ * @returns true if the session is updated, false otherwise
879
+ */
880
+ function doUpdateAgentSessionMap(
881
+ agentSessionMap: Map<string, AgentSessionData>,
882
+ sessionInfo: ServerSessionInfo
883
+ ): boolean {
884
+ const sessionId = sessionInfo.session_id;
885
+ const agentUuid = sessionInfo.saved_agent_profile.uuid;
886
+ const agent = agentSessionMap.get(agentUuid);
887
+ if (!agent) {
888
+ return false;
889
+ }
890
+ let updated = false;
891
+ agent.sessions.map((session) => {
892
+ if (session.session_uuid === sessionId) {
893
+ updated = true;
894
+ return sessionInfoToSessionData(sessionInfo);
895
+ } else {
896
+ return session;
897
+ }
898
+ });
899
+ return updated;
900
+ }