@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,1332 @@
1
+ import { strict as assert } from "assert";
2
+ import { Tool } from "@modelcontextprotocol/sdk/types.js";
3
+
4
+ import {
5
+ AgentPreferences,
6
+ Configuration,
7
+ getLogger,
8
+ McpServerSettings,
9
+ prefsGetAutoApprove,
10
+ prefsSetAutoApprove,
11
+ SavedAgentProfile,
12
+ } from "@xalia/xmcp/sdk";
13
+
14
+ import {
15
+ Agent,
16
+ ChatCompletionAssistantMessageParam,
17
+ ChatCompletionMessageToolCall,
18
+ ChatCompletionToolMessageParam,
19
+ createUserMessage,
20
+ } from "../../agent/agent";
21
+ import { SkillManager } from "../../agent/sudoMcpServerManager";
22
+ import { McpServerInfo } from "../../agent/mcpServerManager";
23
+ import { IAgentEventHandler } from "../../agent/iAgentEventHandler";
24
+ import {
25
+ createAgentWithoutSkills,
26
+ DEFAULT_LLM_MODEL,
27
+ } from "../../agent/agentUtils";
28
+ import { IPlatform } from "../../agent/iplatform";
29
+
30
+ import type {
31
+ ServerToClient,
32
+ ServerMcpServerAdded,
33
+ ServerMcpServerRemoved,
34
+ ServerMcpServerToolEnabled,
35
+ ServerMcpServerToolDisabled,
36
+ ServerSystemPromptUpdated,
37
+ ServerModelUpdated,
38
+ ServerUserMessage,
39
+ ClientUserMessage,
40
+ ServerToolAutoApprovalSet,
41
+ ClientSessionMessage,
42
+ ServerSessionError,
43
+ ServerUserAdded,
44
+ ServerUserRemoved,
45
+ ServerSessionInfo,
46
+ ServerSessionUpdate,
47
+ ClientSetWorkspace,
48
+ ClientSetMcpServerConfig,
49
+ } from "../protocol/messages";
50
+ import { AsyncQueue } from "../utils/asyncQueue";
51
+ import { MultiAsyncQueue } from "../utils/multiAsyncQueue";
52
+ import {
53
+ SessionCheckpoint,
54
+ SessionMessage,
55
+ SessionParticipantMap,
56
+ SessionData,
57
+ TeamParticipant,
58
+ SessionCreateData,
59
+ UserMessageData,
60
+ } from "../data/dataModels";
61
+ import {
62
+ Database,
63
+ createSessionParticipantMap,
64
+ UserData,
65
+ } from "../data/database";
66
+ import { ApprovalManager } from "../utils/approvalManager";
67
+ import { ChatErrorMessage, ChatFatalError } from "../protocol/errors";
68
+ import { addDefaultChatTools } from "./tools";
69
+ import { ChatContextManager, ICheckpointWriter } from "./chatContextManager";
70
+ import {
71
+ llmUserMessageToUserMessageData,
72
+ MESSAGE_INDEX_START_VALUE,
73
+ } from "./conversation";
74
+ import { ChatSessionFileManager } from "./sessionFileManager";
75
+ import { IUserConnectionManager } from "./connectionManager";
76
+ import { getErrorString } from "./errorUtils";
77
+ import { NODE_PLATFORM } from "../../tool/nodePlatform";
78
+ import { DbMcpServerConfigs } from "../data/dbMcpServerConfigs";
79
+
80
+ /**
81
+ * Num messages to load at conversation startup.
82
+ */
83
+ export const DEFAULT_NUM_MESSGAES = 500;
84
+
85
+ const logger = getLogger();
86
+
87
+ type QueuedClientMessage<
88
+ T extends ClientSessionMessage = ClientSessionMessage,
89
+ > = {
90
+ msg: T;
91
+ from: string;
92
+ };
93
+
94
+ /**
95
+ * Implementation of ICheckpointWriter that writes into the DB.
96
+ */
97
+ class DBCheckpointWriter implements ICheckpointWriter {
98
+ constructor(
99
+ private db: Database,
100
+ private sessionId: string
101
+ ) {}
102
+
103
+ writeCheckpoint(checkpoint: SessionCheckpoint): Promise<void> {
104
+ logger.info(
105
+ `[DBCheckpointWriter] writing (session ${this.sessionId}):\n` +
106
+ JSON.stringify(checkpoint)
107
+ );
108
+ return this.db.sessionCheckpointSet(this.sessionId, checkpoint);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Describes a Session (conversation) with connected participants.
114
+ *
115
+ * We maintain 2 queues - one for all incoming messages from clients, and
116
+ * another for user messages (to be gathered and sent to the Agent). This
117
+ * allows processing of user messages to depend on messages from clients
118
+ * (using a single queue would not allow this, since later messages could not
119
+ * be seen until user messages had been fully processed, which could block
120
+ * tool approvals and other interactions).
121
+ */
122
+ export class OpenSession implements IAgentEventHandler, IPlatform {
123
+ private readonly db: Database;
124
+ private /* readonly */ agent: Agent;
125
+ private readonly sessionUUID: string;
126
+ private readonly teamUUID: string | undefined;
127
+ private readonly userUUID: string;
128
+ private readonly agentProfileUUID: string;
129
+ private readonly sessionParticipants: SessionParticipantMap;
130
+ private readonly agentProfilePreferences: AgentPreferences;
131
+ private /* readonly */ skillManager: SkillManager;
132
+ private readonly connectionManager: IUserConnectionManager<ServerToClient>;
133
+ private readonly messageQueue: AsyncQueue<QueuedClientMessage>;
134
+ private readonly userMessageQueue: MultiAsyncQueue<ServerUserMessage>;
135
+ private readonly contextManager: ChatContextManager;
136
+ private readonly approvalManager: ApprovalManager;
137
+ private readonly savedAgentProfile: SavedAgentProfile;
138
+ private isPersisted: boolean;
139
+ private sessionTitle: string;
140
+ private sessionUpdatedAt: string;
141
+
142
+ private constructor(
143
+ db: Database,
144
+ agent: Agent,
145
+ sessionData: SessionData,
146
+ savedAgentProfile: SavedAgentProfile,
147
+ isPersisted: boolean,
148
+ sessionParticipants: SessionParticipantMap,
149
+ agentProfilePreferences: AgentPreferences,
150
+ skillManager: SkillManager,
151
+ contextManager: ChatContextManager,
152
+ connectionManager: IUserConnectionManager<ServerToClient>,
153
+ approvalManager: ApprovalManager = new ApprovalManager()
154
+ ) {
155
+ this.db = db;
156
+ this.agent = agent;
157
+ this.sessionUUID = sessionData.session_uuid;
158
+ this.teamUUID = sessionData.team_uuid;
159
+ this.userUUID = sessionData.user_uuid;
160
+ this.agentProfileUUID = sessionData.agent_profile_uuid;
161
+ this.sessionParticipants = sessionParticipants;
162
+ this.agentProfilePreferences = agentProfilePreferences;
163
+ this.skillManager = skillManager;
164
+ this.connectionManager = connectionManager;
165
+ this.messageQueue = new AsyncQueue<QueuedClientMessage>((m) =>
166
+ this.processMessage(m)
167
+ );
168
+
169
+ this.userMessageQueue = new MultiAsyncQueue<ServerUserMessage>((m) =>
170
+ this.processUserMessages(m)
171
+ );
172
+ this.contextManager = contextManager;
173
+ this.approvalManager = approvalManager;
174
+ this.savedAgentProfile = savedAgentProfile;
175
+ this.isPersisted = isPersisted;
176
+ this.sessionTitle = sessionData.title;
177
+ this.sessionUpdatedAt = sessionData.updated_at;
178
+ }
179
+
180
+ private static async init(
181
+ db: Database,
182
+ sessionData: SessionData,
183
+ savedAgentProfile: SavedAgentProfile,
184
+ sessionMessages: SessionMessage[],
185
+ sessionParticipants: SessionParticipantMap,
186
+ ownerData: UserData,
187
+ ownerApiKey: string,
188
+ sessionCheckpoint: SessionCheckpoint | undefined,
189
+ llmUrl: string,
190
+ xmcpUrl: string,
191
+ connectionManager: IUserConnectionManager<ServerToClient>
192
+ ): Promise<OpenSession> {
193
+ const sessionId = sessionData.session_uuid;
194
+ const fileManager = new ChatSessionFileManager(sessionId, db);
195
+ const contextManager = new ChatContextManager(
196
+ savedAgentProfile.profile.system_prompt,
197
+ sessionMessages,
198
+ sessionId,
199
+ ownerData.uuid,
200
+ sessionCheckpoint,
201
+ llmUrl,
202
+ savedAgentProfile.profile.model || DEFAULT_LLM_MODEL,
203
+ ownerApiKey,
204
+ new DBCheckpointWriter(db, sessionId),
205
+ fileManager
206
+ );
207
+ if (sessionData.workspace) {
208
+ const ws = sessionData.workspace;
209
+ contextManager.setWorkspace(createUserMessage(ws.message, ws.imageB64));
210
+ }
211
+
212
+ const openSession = new OpenSession(
213
+ db,
214
+ {} as Agent, // Placeholder - will be replaced after agent creation
215
+ sessionData,
216
+ savedAgentProfile,
217
+ false /* isPersisted */,
218
+ sessionParticipants,
219
+ savedAgentProfile.preferences,
220
+ {} as SkillManager, // Placeholder - will be replaced after agent creation
221
+ contextManager,
222
+ connectionManager
223
+ );
224
+
225
+ // Initialize an empty agent (to ensure there are no callbacks before
226
+ // the OpenSession and client are fully set up).
227
+
228
+ const xmcpConfig = Configuration.new(ownerApiKey, xmcpUrl, false);
229
+ const [agent, skillManager] = await createAgentWithoutSkills(
230
+ llmUrl,
231
+ savedAgentProfile.profile,
232
+ openSession,
233
+ openSession,
234
+ contextManager,
235
+ ownerApiKey,
236
+ xmcpConfig,
237
+ undefined,
238
+ true
239
+ );
240
+ await addDefaultChatTools(agent, ownerData.timezone, openSession);
241
+
242
+ // Update OpenSession with real agent and skillManager
243
+ openSession.agent = agent;
244
+ openSession.skillManager = skillManager;
245
+
246
+ await openSession.restoreMcpSettings(
247
+ savedAgentProfile.profile.mcp_settings
248
+ );
249
+ return openSession;
250
+ }
251
+
252
+ static async initWithEmptySession(
253
+ db: Database,
254
+ sessionData: SessionData,
255
+ llmUrl: string,
256
+ xmcpUrl: string,
257
+ connectionManager: IUserConnectionManager<ServerToClient>
258
+ ): Promise<OpenSession> {
259
+ const sessionMessages: SessionMessage[] = [];
260
+ const sessionCheckpoint = undefined;
261
+
262
+ const { savedAgentProfile, ownerData, ownerApiKey } =
263
+ await loadDataForEmptySession(
264
+ db,
265
+ sessionData.agent_profile_uuid,
266
+ sessionData.user_uuid
267
+ );
268
+ if (!ownerApiKey) {
269
+ throw new ChatErrorMessage("failed finding owners default api key");
270
+ }
271
+
272
+ const sessionParticipants = new Map<string, TeamParticipant>();
273
+ sessionParticipants.set(sessionData.user_uuid, {
274
+ user_uuid: sessionData.user_uuid,
275
+ nickname: ownerData.nickname || "",
276
+ email: ownerData.email,
277
+ role: "owner",
278
+ });
279
+
280
+ return OpenSession.init(
281
+ db,
282
+ sessionData,
283
+ savedAgentProfile,
284
+ sessionMessages,
285
+ sessionParticipants,
286
+ ownerData,
287
+ ownerApiKey,
288
+ sessionCheckpoint,
289
+ llmUrl,
290
+ xmcpUrl,
291
+ connectionManager
292
+ );
293
+ }
294
+
295
+ static async initWithExistingSession(
296
+ db: Database,
297
+ sessionId: string,
298
+ llmUrl: string,
299
+ xmcpUrl: string,
300
+ connectionManager: IUserConnectionManager<ServerToClient>
301
+ ): Promise<OpenSession> {
302
+ // Load session data from database
303
+ const {
304
+ sessionData,
305
+ sessionMessages,
306
+ sessionCheckpoint,
307
+ savedAgentProfile,
308
+ sessionParticipants,
309
+ ownerData,
310
+ ownerApiKey,
311
+ } = await loadSessionData(db, sessionId);
312
+ return OpenSession.init(
313
+ db,
314
+ sessionData,
315
+ savedAgentProfile,
316
+ sessionMessages,
317
+ sessionParticipants,
318
+ ownerData,
319
+ ownerApiKey,
320
+ sessionCheckpoint,
321
+ llmUrl,
322
+ xmcpUrl,
323
+ connectionManager
324
+ );
325
+ }
326
+
327
+ onEmpty(): void {
328
+ logger.info(`[OpenSession.onEmpty] session ${this.sessionUUID}`);
329
+ }
330
+
331
+ getParticipants(): SessionParticipantMap {
332
+ return this.sessionParticipants;
333
+ }
334
+
335
+ sendSessionData(connectionId: string, clientMessageId: string): void {
336
+ logger.info(
337
+ `[SessionRegistry] sending session data for session ${this.sessionUUID}`
338
+ );
339
+
340
+ const sessionInfo = this.serverSessionInfo(clientMessageId);
341
+ this.connectionManager.sendToConnection(connectionId, sessionInfo);
342
+
343
+ // send conversation history
344
+ const conversationMessages = this.contextManager.getConversationMessages();
345
+ conversationMessages.forEach((message) => {
346
+ this.connectionManager.sendToConnection(connectionId, message);
347
+ });
348
+
349
+ // add MCP settings
350
+ this.sendMcpSettings(connectionId);
351
+
352
+ // add system prompt and model
353
+ const agentProfile = this.agent.getAgentProfile();
354
+ this.connectionManager.sendToConnection(connectionId, {
355
+ type: "system_prompt_updated",
356
+ system_prompt: agentProfile.system_prompt,
357
+ session_id: this.sessionUUID,
358
+ });
359
+ this.connectionManager.sendToConnection(connectionId, {
360
+ type: "model_updated",
361
+ model: agentProfile.model || "",
362
+ session_id: this.sessionUUID,
363
+ });
364
+ }
365
+
366
+ /**
367
+ * Restore MCP settings from the database. We only broadcast when
368
+ * error occurs. The sending of MCP settings is handled by the
369
+ * the callers of `getMcpSettings` method.
370
+ * @param mcpServerSettings - MCP server settings from the database.
371
+ */
372
+ async restoreMcpSettings(
373
+ mcpServerSettings: McpServerSettings
374
+ ): Promise<void> {
375
+ // TODO: SkillManager.restoreSessings could support a callback to handle
376
+ // the errors, then we don't need to reimplement the logic.
377
+
378
+ // Add each server. We handle any errors so that conversations may
379
+ // continue even if not all MCP servers can be enabled (e.g. if the owner
380
+ // is not present to refresh an oauth token).
381
+ logger.info(
382
+ `[OpenSession] restoring MCP settings: ` +
383
+ JSON.stringify(mcpServerSettings)
384
+ );
385
+ const skillManager = this.skillManager;
386
+ for (const server_name in mcpServerSettings) {
387
+ try {
388
+ // TODO: the "[] => all-tools" logic is repeated here, in
389
+ // `sendSessionData` and in McpServerSettings. SkillManager could
390
+ // expose a method `expandMcpServerSettings` which removes missing
391
+ // elements and expands
392
+
393
+ // Handle the case where the mcp server no longer exists
394
+ if (!skillManager.hasServerBrief(server_name)) {
395
+ throw new Error(`unknown mcp server: ${server_name}`);
396
+ }
397
+
398
+ // Handle `[]` meaning "all tools"
399
+ let enabled_tools = mcpServerSettings[server_name];
400
+ if (enabled_tools.length === 0) {
401
+ const allTools = await skillManager.getServerTools(server_name);
402
+ enabled_tools = allTools.map((t) => t.name);
403
+ }
404
+
405
+ await skillManager.addMcpServer(server_name, false);
406
+ for (const enabled_tool of enabled_tools) {
407
+ skillManager.enableTool(server_name, enabled_tool);
408
+ }
409
+ } catch (e) {
410
+ this.broadcast({
411
+ type: "session_error",
412
+ message: `Error adding MCP server ${server_name}: ${String(e)}`,
413
+ session_id: this.sessionUUID,
414
+ });
415
+ logger.error(`Error adding MCP server ${server_name}: ${String(e)}`);
416
+ }
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Intended to be called within specific `catch` blocks, defining a
422
+ * consistent error handling approach. If the exception can be handled it
423
+ * is and `true` is returned. Otherwise false is returned (and the caller
424
+ * is expected to re-throw if they are unable to handle it).
425
+ */
426
+ private handleError(err: unknown, from?: string): boolean {
427
+ const sendError = (msg: ServerSessionError) => {
428
+ if (from) {
429
+ this.sendTo(from, msg);
430
+ } else {
431
+ this.broadcast(msg);
432
+ }
433
+ };
434
+
435
+ if (err instanceof ChatFatalError) {
436
+ // ChatFatalError in session context should send error to specific user
437
+ // or broadcast if no specific user context
438
+ sendError({
439
+ type: "session_error",
440
+ message: err.message,
441
+ session_id: this.sessionUUID,
442
+ });
443
+ } else if (err instanceof ChatErrorMessage) {
444
+ sendError({
445
+ type: "session_error",
446
+ message: err.message,
447
+ session_id: this.sessionUUID,
448
+ });
449
+ } else {
450
+ const errString = getErrorString(err);
451
+ sendError({
452
+ type: "session_error",
453
+ message: errString,
454
+ session_id: this.sessionUUID,
455
+ });
456
+ }
457
+
458
+ return true;
459
+ }
460
+
461
+ private broadcast(msg: ServerToClient): void {
462
+ const users: Set<string> = new Set(this.sessionParticipants.keys());
463
+ this.connectionManager.sendToUsers(users, msg);
464
+ }
465
+
466
+ sendTo(user_uuid: string, msg: ServerToClient): void {
467
+ this.connectionManager.sendToUsers(new Set([user_uuid]), msg);
468
+ }
469
+
470
+ // IPlatform.openUrl
471
+ openUrl(url: string, authResultP: Promise<boolean>, display_name: string) {
472
+ // These requests are always passed to the original owner, since it is
473
+ // their settings that will be used for all MCP servers.
474
+ this.broadcast({
475
+ type: "authentication_started",
476
+ session_id: this.sessionUUID,
477
+ url,
478
+ });
479
+ this.sendTo(this.userUUID, {
480
+ type: "authenticate",
481
+ session_id: this.sessionUUID,
482
+ url,
483
+ display_name,
484
+ });
485
+
486
+ // TODO: auth timeout
487
+ // Don't stall this function waiting for authentication
488
+ void authResultP.then((result) => {
489
+ this.sendTo(this.userUUID, {
490
+ type: "authentication_finished",
491
+ session_id: this.sessionUUID,
492
+ url,
493
+ result,
494
+ });
495
+ });
496
+ }
497
+
498
+ // IPlatform.load
499
+ load(filename: string): Promise<string> {
500
+ if (process.env.DEVELOPMENT === "1") {
501
+ return NODE_PLATFORM.load(filename);
502
+ }
503
+ throw new ChatErrorMessage("Platform.load not implemented");
504
+ }
505
+
506
+ // IPlatform.renderHTML
507
+ renderHTML(html: string): Promise<void> {
508
+ return new Promise<void>((r) => {
509
+ this.broadcast({
510
+ type: "render_html",
511
+ html,
512
+ session_id: this.sessionUUID,
513
+ });
514
+ r();
515
+ });
516
+ }
517
+
518
+ // IAgentEventHandler.onCompletion
519
+ onCompletion(result: ChatCompletionAssistantMessageParam): void {
520
+ logger.debug(`[OpenSession.onCompletion] : ${JSON.stringify(result)}`);
521
+ // Nothing to broadcast. Caller will receive this via onAgentMessage.
522
+ this.contextManager.processAgentResponse(result);
523
+ }
524
+
525
+ // IAgentEventHandler.onToolCallResult
526
+ onToolCallResult(result: ChatCompletionToolMessageParam): void {
527
+ logger.debug(`[onToolCallResult] : ${JSON.stringify(result)}`);
528
+ const toolCallMessage = this.contextManager.processToolCallResult(result);
529
+ this.broadcast(toolCallMessage);
530
+ }
531
+
532
+ // IAgentEventHandler.onToolCall
533
+ async onToolCall(
534
+ toolCall: ChatCompletionMessageToolCall,
535
+ agentTool: boolean
536
+ ): Promise<boolean> {
537
+ if (agentTool) {
538
+ // "Agent" tools are considered internal to the agent, and are always
539
+ // permitted. Inform all clients and immediately approve.
540
+ this.broadcast({
541
+ type: "tool_call",
542
+ tool_call: toolCall,
543
+ session_id: this.sessionUUID,
544
+ });
545
+ return true;
546
+ }
547
+
548
+ // TODO: Need a proper mapping to/from MCP calls to tool names
549
+
550
+ const [serverName, tool] = toolCall.function.name.split("__");
551
+ const autoApproved = prefsGetAutoApprove(
552
+ this.agentProfilePreferences,
553
+ serverName,
554
+ tool
555
+ );
556
+ if (!autoApproved) {
557
+ const { id, resultP } = this.approvalManager.startApproval(
558
+ toolCall.function.name
559
+ );
560
+ this.broadcast({
561
+ type: "approve_tool_call",
562
+ id,
563
+ tool_call: toolCall,
564
+ session_id: this.sessionUUID,
565
+ });
566
+
567
+ try {
568
+ logger.debug(`[OpenSession.onToolCall] awaiting approval ${id}`);
569
+ const { approved, auto_approve } = await resultP;
570
+ logger.debug(
571
+ `[OpenSession.onToolCall] approval ${id}: ${String(approved)}`
572
+ );
573
+ if (auto_approve) {
574
+ logger.debug(
575
+ "[OpenSession.onToolCall] auto_approve set. updated preferences"
576
+ );
577
+ const autoApprovalMsg = await this.onSetAutoApproval(
578
+ serverName,
579
+ tool,
580
+ true
581
+ );
582
+ if (autoApprovalMsg) {
583
+ this.broadcast(autoApprovalMsg);
584
+ }
585
+ }
586
+
587
+ return approved;
588
+ } catch (e) {
589
+ logger.debug(
590
+ `[OpenSession.onToolCall] error waiting for approval ${id}: ` +
591
+ String(e)
592
+ );
593
+ return false;
594
+ }
595
+ } else {
596
+ this.broadcast({
597
+ type: "tool_call",
598
+ tool_call: toolCall,
599
+ session_id: this.sessionUUID,
600
+ });
601
+ return true;
602
+ }
603
+ }
604
+
605
+ // IAgentEventHandler.onAgentMessage
606
+ // eslint-disable-next-line @typescript-eslint/require-await
607
+ async onAgentMessage(msg: string, end: boolean): Promise<void> {
608
+ logger.debug(
609
+ `[OpenSession.onAgentMessage] msg: ${msg}, end: ${String(end)}`
610
+ );
611
+
612
+ // Inform the contextManager and broadcast the ServerAgentMessageChunk
613
+ const agentMsgChunk = this.contextManager.processAgentMessage(msg, end);
614
+ this.broadcast(agentMsgChunk);
615
+ }
616
+
617
+ /**
618
+ * Handle incoming session-related message from client.
619
+ * These messages include tool calls, MCP changes, etc.
620
+ * These messages are processed by `messageQueue`.
621
+ */
622
+ onClientSessionMessage(from: string, message: ClientSessionMessage): void {
623
+ logger.info(
624
+ `[OpenSession] Message from ${from} in session ` +
625
+ `${this.sessionUUID}: ${JSON.stringify(message)}`
626
+ );
627
+
628
+ // Validate user is in session
629
+ if (!this.sessionParticipants.get(from)) {
630
+ logger.warn(
631
+ `User ${from} not in session ${this.sessionUUID} - ignoring message`
632
+ );
633
+ this.sendTo(from, {
634
+ type: "session_error",
635
+ message: "You are not a participant in this session",
636
+ session_id: this.sessionUUID,
637
+ client_message_id: message.client_message_id,
638
+ });
639
+ return;
640
+ }
641
+
642
+ // Enqueue message for processing
643
+ if (!this.messageQueue.tryEnqueue({ msg: message, from })) {
644
+ this.sendTo(
645
+ from,
646
+ this.addSessionContext({
647
+ type: "session_error",
648
+ message: "message queue full. try again later",
649
+ client_message_id: message.client_message_id,
650
+ })
651
+ );
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Get MCP settings in the form of `ServerToClient` messages.
657
+ * This is used to restore MCP settings when a user joins a session.
658
+ */
659
+ private sendMcpSettings(connectionId: string): void {
660
+ const servers = this.skillManager.getMcpServerNames();
661
+ servers.forEach((server_name) => {
662
+ const mcpServer = this.skillManager.getMcpServer(server_name);
663
+ const tools = mcpServer.getTools();
664
+ const enabled_tools = Array.from(mcpServer.getEnabledTools().keys());
665
+ this.connectionManager.sendToConnection(connectionId, {
666
+ type: "mcp_server_added",
667
+ server_name,
668
+ tools,
669
+ enabled_tools,
670
+ session_id: this.sessionUUID,
671
+ });
672
+ });
673
+ }
674
+
675
+ /**
676
+ * Called once for each message.
677
+ */
678
+ private async processMessage(
679
+ queuedMessage: QueuedClientMessage
680
+ ): Promise<void> {
681
+ logger.debug(
682
+ `[onMessage]: processing (${queuedMessage.from}) ` +
683
+ JSON.stringify(queuedMessage.msg)
684
+ );
685
+
686
+ // In general, handlers return a message to be broadcast. Errors are
687
+ // handled by returning an error to just the sender. Handlers can
688
+ // also broadcast and send directly.
689
+
690
+ try {
691
+ const msg = queuedMessage.msg;
692
+ let broadcastMsg: ServerToClient[] | ServerToClient | undefined =
693
+ undefined;
694
+ switch (msg.type) {
695
+ case "msg":
696
+ broadcastMsg = this.onUserMessage(msg, queuedMessage.from);
697
+ break;
698
+ case "add_mcp_server":
699
+ broadcastMsg = await this.onAddMcpServer(
700
+ msg.server_name,
701
+ msg.enable_all
702
+ );
703
+ break;
704
+ case "remove_mcp_server":
705
+ broadcastMsg = await this.onRemoveMcpServer(msg.server_name);
706
+ break;
707
+ case "enable_mcp_server_tool":
708
+ broadcastMsg = await this.onEnableMcpServerTool(
709
+ msg.server_name,
710
+ msg.tool
711
+ );
712
+ break;
713
+ case "disable_mcp_server_tool":
714
+ broadcastMsg = await this.onDisableMcpServerTool(
715
+ msg.server_name,
716
+ msg.tool
717
+ );
718
+ break;
719
+ case "enable_all_mcp_server_tools":
720
+ broadcastMsg = await this.onEnableAllMcpServerTools(msg.server_name);
721
+ break;
722
+ case "disable_all_mcp_server_tools":
723
+ broadcastMsg = await this.onDisableAllMcpServerTools(msg.server_name);
724
+ break;
725
+ case "tool_call_approval_result":
726
+ if (
727
+ this.approvalManager.approvalResult(
728
+ msg.id,
729
+ msg.result,
730
+ msg.auto_approve
731
+ )
732
+ ) {
733
+ broadcastMsg = {
734
+ type: "tool_call_approval_result",
735
+ id: msg.id,
736
+ result: msg.result,
737
+ session_id: this.sessionUUID,
738
+ };
739
+ }
740
+ break;
741
+ case "set_auto_approval":
742
+ broadcastMsg = await this.onSetAutoApproval(
743
+ msg.server_name,
744
+ msg.tool,
745
+ msg.auto_approve
746
+ );
747
+ break;
748
+ case "set_system_prompt":
749
+ broadcastMsg = await this.onSetSystemPrompt(msg.system_prompt);
750
+ break;
751
+ case "set_model":
752
+ broadcastMsg = await this.onSetModel(msg.model);
753
+ break;
754
+ case "set_workspace":
755
+ await this.onSetWorkspace(msg, queuedMessage.from);
756
+ break;
757
+ case "set_mcp_server_config":
758
+ await this.handleSetMcpServerConfig(msg, queuedMessage.from);
759
+ break;
760
+ default: {
761
+ const exhaustive: never = msg; // Error => non-exhaustive switch-case.
762
+ return exhaustive;
763
+ }
764
+ }
765
+
766
+ if (broadcastMsg) {
767
+ if (broadcastMsg instanceof Array) {
768
+ broadcastMsg.map((msg) => {
769
+ this.broadcast(msg);
770
+ });
771
+ } else {
772
+ this.broadcast(broadcastMsg);
773
+ }
774
+ }
775
+ } catch (err: unknown) {
776
+ if (!this.handleError(err, queuedMessage.from)) {
777
+ throw err;
778
+ }
779
+ }
780
+ }
781
+
782
+ private async onSetWorkspace(
783
+ msg: ClientSetWorkspace,
784
+ sender: string
785
+ ): Promise<void> {
786
+ this.contextManager.setWorkspace(
787
+ createUserMessage(msg.message, msg.imageB64, sender)
788
+ );
789
+ const m = this.workspaceUserMessageData();
790
+
791
+ // It's possible that the session has not been written to the DB yet (if
792
+ // the workspace is updated before any messages are sent).
793
+
794
+ if (!this.isPersisted) {
795
+ await this.createSessionInDB();
796
+ } else {
797
+ await this.db.sessionUpdateWorkspace(this.sessionUUID, m);
798
+ }
799
+ assert(this.isPersisted);
800
+ }
801
+
802
+ private async processUserMessages(msgs: ServerUserMessage[]): Promise<void> {
803
+ if (msgs.length === 0) {
804
+ logger.debug("ignoring empty messages");
805
+ return;
806
+ }
807
+
808
+ // Begin an agent response using the accumulated user messages.
809
+
810
+ const { llmUserMessages, agentFirstChunk } =
811
+ this.contextManager.startAgentResponse(msgs);
812
+ this.broadcast(agentFirstChunk);
813
+
814
+ try {
815
+ await this.agent.userMessagesRaw(llmUserMessages);
816
+ } catch (e) {
817
+ logger.warn(
818
+ `[OpenSession.processUserMessages] agent error: ${String(e)}`
819
+ );
820
+
821
+ // Errors during agent replies must be turned into messages.
822
+
823
+ const errMsg = `error from LLM: ${String(e)}`;
824
+ await this.onAgentMessage(errMsg, true);
825
+ const err = this.contextManager.revertAgentResponse(errMsg);
826
+ this.broadcast(err);
827
+ return;
828
+ }
829
+
830
+ const newSessionMessages = this.contextManager.endAgentResponse();
831
+
832
+ logger.debug(
833
+ "[processUserMessages] newSessionMessages: " +
834
+ JSON.stringify(newSessionMessages)
835
+ );
836
+
837
+ try {
838
+ // Append to in-memory conversation and write to the DB
839
+ await this.db.sessionMessagesAppend(this.sessionUUID, newSessionMessages);
840
+ } catch (e) {
841
+ if (!this.handleError(e)) {
842
+ throw e;
843
+ }
844
+ }
845
+ }
846
+
847
+ private onUserMessage(
848
+ msg: ClientUserMessage,
849
+ from: string
850
+ ): ServerUserMessage | undefined {
851
+ // Return a ServerUserMessage for broadcast. The actual message is places
852
+ // on a queue to be dealt with in another loop. This allows Agent
853
+ // processing of user messages to depend on other messages.
854
+
855
+ assert(msg);
856
+ assert(from);
857
+
858
+ // Assign the user message_idx and attempt to enqueue.
859
+
860
+ const userMessage = this.contextManager.processUserMessage(msg, from);
861
+
862
+ // Special case for the first message of the session
863
+
864
+ if (userMessage.message_idx === MESSAGE_INDEX_START_VALUE) {
865
+ // No need to wait for this to complete before broadcasting.
866
+ void this.onFirstMessage(userMessage);
867
+ }
868
+
869
+ if (!this.userMessageQueue.tryEnqueue(userMessage)) {
870
+ // We failed to enqueue - revert the `getNextMessageIdx`
871
+
872
+ // NOTE: Nothing should await between `getNextMessageIdx` and
873
+ // `freeMessageIdx` here.
874
+
875
+ this.contextManager.unprocessUserMessage(userMessage);
876
+
877
+ this.sendTo(from, {
878
+ type: "session_error",
879
+ message: "failed to queue message. try again later.",
880
+ session_id: this.sessionUUID,
881
+ });
882
+ return undefined;
883
+ }
884
+
885
+ // Message is enqueued. Broadcast to all participants.
886
+ return userMessage;
887
+ }
888
+
889
+ private async createSessionInDB(): Promise<void> {
890
+ assert(!this.isPersisted);
891
+ const sessionCreateData: SessionCreateData = {
892
+ session_uuid: this.sessionUUID,
893
+ title: this.sessionTitle,
894
+ team_uuid: this.teamUUID,
895
+ agent_profile_uuid: this.agentProfileUUID,
896
+ workspace: this.workspaceUserMessageData(),
897
+ user_uuid: this.userUUID,
898
+ };
899
+ logger.info(
900
+ `[OpenSession.onFirstMessage] writing session ${this.sessionUUID}`
901
+ );
902
+
903
+ await this.db.sessionCreate(sessionCreateData);
904
+ this.isPersisted = true;
905
+ }
906
+
907
+ private async onFirstMessage(userMsg: ServerUserMessage): Promise<void> {
908
+ // Update title on the class before writing to the DB
909
+
910
+ this.sessionTitle = userMsg.message?.slice(0, 128) || "New Chat";
911
+
912
+ // The session may already have been saved (e.g. if the workspace is
913
+ // updated before any messages are sent).
914
+
915
+ if (!this.isPersisted) {
916
+ await this.createSessionInDB();
917
+ } else {
918
+ await this.db.sessionUpdateTitle(this.sessionUUID, this.sessionTitle);
919
+ }
920
+ assert(this.isPersisted);
921
+
922
+ // Broadcast the SessionUpdated message
923
+
924
+ const msg: ServerSessionUpdate = {
925
+ type: "session_update",
926
+ session_id: this.sessionUUID,
927
+ title: this.sessionTitle,
928
+ };
929
+ this.broadcast(msg);
930
+ }
931
+
932
+ private async onAddMcpServer(
933
+ serverName: string,
934
+ enableAll: boolean
935
+ ): Promise<ServerMcpServerAdded> {
936
+ logger.info(
937
+ `[onAddMcpServer]: Adding server ${serverName} ` +
938
+ `(enable_all: ${String(enableAll)})`
939
+ );
940
+
941
+ if (this.skillManager.hasMcpServer(serverName)) {
942
+ throw new ChatErrorMessage(`${serverName} already added`);
943
+ }
944
+
945
+ if (!this.skillManager.hasServerBrief(serverName)) {
946
+ throw new ChatErrorMessage(`no such server: ${serverName}`);
947
+ }
948
+
949
+ await this.skillManager.addMcpServer(serverName, enableAll);
950
+ this.skillManager.enableAllTools(serverName);
951
+
952
+ // Save changes to the AgentProfile
953
+
954
+ await this.updateAgentProfile();
955
+
956
+ // Broadcast the message to all participants.
957
+
958
+ const server = this.skillManager.getMcpServer(serverName);
959
+ const tools = server.getTools();
960
+ const enabled_tools = Array.from(server.getEnabledTools().keys());
961
+ return {
962
+ type: "mcp_server_added",
963
+ server_name: serverName,
964
+ tools,
965
+ enabled_tools,
966
+ session_id: this.sessionUUID,
967
+ };
968
+ }
969
+
970
+ private async onRemoveMcpServer(
971
+ server_name: string
972
+ ): Promise<ServerMcpServerRemoved> {
973
+ logger.info(`[onRemoveMcpServer]: Removing server ${server_name}`);
974
+
975
+ if (!this.skillManager.hasMcpServer(server_name)) {
976
+ throw new ChatErrorMessage(`${server_name} not enabled`);
977
+ }
978
+
979
+ await this.skillManager.removeMcpServer(server_name);
980
+ await this.updateAgentProfile();
981
+
982
+ return {
983
+ type: "mcp_server_removed",
984
+ server_name,
985
+ session_id: this.sessionUUID,
986
+ };
987
+ }
988
+
989
+ private async onEnableMcpServerTool(
990
+ server_name: string,
991
+ tool: string
992
+ ): Promise<ServerMcpServerToolEnabled> {
993
+ this.ensureMcpServer(server_name);
994
+ this.skillManager.enableTool(server_name, tool);
995
+
996
+ await this.updateAgentProfile();
997
+
998
+ return {
999
+ type: "mcp_server_tool_enabled",
1000
+ server_name,
1001
+ tool,
1002
+ session_id: this.sessionUUID,
1003
+ };
1004
+ }
1005
+
1006
+ private async onDisableMcpServerTool(
1007
+ server_name: string,
1008
+ tool: string
1009
+ ): Promise<ServerMcpServerToolDisabled> {
1010
+ this.ensureMcpServerAndTool(server_name, tool);
1011
+ this.skillManager.disableTool(server_name, tool);
1012
+
1013
+ await this.updateAgentProfile();
1014
+
1015
+ return {
1016
+ type: "mcp_server_tool_disabled",
1017
+ server_name,
1018
+ tool,
1019
+ session_id: this.sessionUUID,
1020
+ };
1021
+ }
1022
+
1023
+ private async onEnableAllMcpServerTools(
1024
+ server_name: string
1025
+ ): Promise<ServerMcpServerToolEnabled[]> {
1026
+ // We reimplement the logic to enable any disabled tools so we can
1027
+ // construct messages along the way.
1028
+
1029
+ const server = this.ensureMcpServer(server_name);
1030
+ const enabledTools = server.getEnabledTools();
1031
+ const msgs: ServerMcpServerToolEnabled[] = [];
1032
+ for (const tool of server.getTools()) {
1033
+ if (!enabledTools.get(tool.name)) {
1034
+ this.skillManager.enableTool(server_name, tool.name);
1035
+ msgs.push({
1036
+ type: "mcp_server_tool_enabled",
1037
+ server_name,
1038
+ tool: tool.name,
1039
+ session_id: this.sessionUUID,
1040
+ });
1041
+ }
1042
+ }
1043
+
1044
+ await this.updateAgentProfile();
1045
+
1046
+ return msgs;
1047
+ }
1048
+
1049
+ private async onDisableAllMcpServerTools(
1050
+ server_name: string
1051
+ ): Promise<ServerMcpServerToolDisabled[]> {
1052
+ // We reimplement the logic to disable all enabled tools so we can
1053
+ // construct messages along the way.
1054
+
1055
+ const server = this.ensureMcpServer(server_name);
1056
+ const enabledTools = server.getEnabledTools();
1057
+ const msgs: ServerMcpServerToolDisabled[] = [];
1058
+ for (const tool of enabledTools.keys()) {
1059
+ this.skillManager.disableTool(server_name, tool);
1060
+ msgs.push({
1061
+ type: "mcp_server_tool_disabled",
1062
+ server_name,
1063
+ tool,
1064
+ session_id: this.sessionUUID,
1065
+ });
1066
+ }
1067
+
1068
+ await this.updateAgentProfile();
1069
+
1070
+ return msgs;
1071
+ }
1072
+
1073
+ private async onSetSystemPrompt(
1074
+ system_prompt: string
1075
+ ): Promise<ServerSystemPromptUpdated> {
1076
+ this.agent.setSystemPrompt(system_prompt);
1077
+ await this.updateAgentProfile();
1078
+ return {
1079
+ type: "system_prompt_updated",
1080
+ system_prompt,
1081
+ session_id: this.sessionUUID,
1082
+ };
1083
+ }
1084
+
1085
+ private async onSetModel(model: string): Promise<ServerModelUpdated> {
1086
+ this.agent.setModel(model);
1087
+ await this.updateAgentProfile();
1088
+ return { type: "model_updated", model, session_id: this.sessionUUID };
1089
+ }
1090
+
1091
+ private async handleSetMcpServerConfig(
1092
+ msg: ClientSetMcpServerConfig,
1093
+ from: string
1094
+ ): Promise<void> {
1095
+ if (!this.skillManager.hasServerBrief(msg.server_name)) {
1096
+ throw new Error(`unknown mcp server: ${msg.server_name}`);
1097
+ }
1098
+
1099
+ const msc = this.db.createTypedClient(DbMcpServerConfigs);
1100
+ if (msg.config) {
1101
+ await msc.setConfigForUser(from, msg.server_name, msg.config);
1102
+ } else {
1103
+ await msc.deleteConfigForUser(from, msg.server_name);
1104
+ }
1105
+
1106
+ // If the mcp server was enabled, disable and re-enable to ensure the new
1107
+ // settings are reflected. Note, we must track the set of enabled tools.
1108
+
1109
+ if (this.skillManager.hasMcpServer(msg.server_name)) {
1110
+ const enabledTools = this.skillManager
1111
+ .getMcpServer(msg.server_name)
1112
+ .getEnabledTools();
1113
+ await this.skillManager.removeMcpServer(msg.server_name);
1114
+ await this.skillManager.addMcpServer(msg.server_name, false);
1115
+ for (const tool of enabledTools.keys()) {
1116
+ this.skillManager.enableTool(msg.server_name, tool);
1117
+ }
1118
+ }
1119
+
1120
+ // TODO: Do we want to braodcast an "mcp_server_config_updated" message?
1121
+ }
1122
+
1123
+ private async onSetAutoApproval(
1124
+ serverName: string,
1125
+ tool: string,
1126
+ autoApprove: boolean
1127
+ ): Promise<ServerToolAutoApprovalSet | undefined> {
1128
+ if (
1129
+ prefsSetAutoApprove(
1130
+ this.agentProfilePreferences,
1131
+ serverName,
1132
+ tool,
1133
+ autoApprove
1134
+ )
1135
+ ) {
1136
+ await this.db.updateAgentProfilePreferences(
1137
+ this.agentProfileUUID,
1138
+ this.agentProfilePreferences
1139
+ );
1140
+
1141
+ return {
1142
+ type: "tool_auto_approval_set",
1143
+ server_name: serverName,
1144
+ tool,
1145
+ auto_approve: autoApprove,
1146
+ session_id: this.sessionUUID,
1147
+ };
1148
+ }
1149
+ }
1150
+
1151
+ private ensureMcpServer(serverName: string): McpServerInfo {
1152
+ return this.skillManager.getMcpServer(serverName);
1153
+ }
1154
+
1155
+ private ensureMcpServerAndTool(serverName: string, toolName: string): Tool {
1156
+ const server = this.ensureMcpServer(serverName);
1157
+ const tool = server.getTool(toolName);
1158
+ if (!tool) {
1159
+ throw new ChatErrorMessage(`Tool ${toolName} on ${serverName} not found`);
1160
+ }
1161
+ return tool;
1162
+ }
1163
+
1164
+ private async updateAgentProfile(): Promise<void> {
1165
+ const profile = this.agent.getAgentProfile();
1166
+ logger.debug(
1167
+ `[updateAgentProfile]: uuid: ${this.agentProfileUUID} profile: ` +
1168
+ JSON.stringify(profile)
1169
+ );
1170
+ return this.db.updateAgentProfile(this.agentProfileUUID, profile);
1171
+ }
1172
+
1173
+ /**
1174
+ * Update participant in session (called by SessionRegistry).
1175
+ * This only updates the local participant map - actual membership
1176
+ * tracking is handled by SessionRegistry.
1177
+ */
1178
+ addParticipant(userId: string, role: TeamParticipant): void {
1179
+ this.sessionParticipants.set(userId, role);
1180
+ // Broadcast result to all session participants
1181
+ const broadcastMessage: ServerUserAdded = {
1182
+ type: "user_added",
1183
+ user_uuid: userId,
1184
+ role: "participant",
1185
+ nickname: role.nickname,
1186
+ email: role.email,
1187
+ session_id: this.sessionUUID,
1188
+ };
1189
+
1190
+ this.broadcast(broadcastMessage);
1191
+ }
1192
+
1193
+ /**
1194
+ * Remove participant from session (called by SessionRegistry).
1195
+ * This only updates the local participant map - actual membership
1196
+ * tracking is handled by SessionRegistry.
1197
+ */
1198
+ removeParticipant(userId: string): void {
1199
+ this.sessionParticipants.delete(userId);
1200
+ // Broadcast result to all session participants
1201
+ const broadcastMessage: ServerUserRemoved = {
1202
+ type: "user_removed",
1203
+ user_uuid: userId,
1204
+ session_id: this.sessionUUID,
1205
+ };
1206
+
1207
+ this.broadcast(broadcastMessage);
1208
+ }
1209
+
1210
+ private getSessionParticipants(): TeamParticipant[] {
1211
+ return Array.from(this.sessionParticipants.entries()).map(
1212
+ ([userId, user]) => ({
1213
+ nickname: user.nickname,
1214
+ email: user.email,
1215
+ user_uuid: userId,
1216
+ role: user.role,
1217
+ })
1218
+ );
1219
+ }
1220
+
1221
+ private workspaceUserMessageData(): UserMessageData | undefined {
1222
+ const workspaceMsg = this.contextManager.getWorkspace();
1223
+ return workspaceMsg
1224
+ ? llmUserMessageToUserMessageData(workspaceMsg)
1225
+ : undefined;
1226
+ }
1227
+
1228
+ serverSessionInfo(clientMessageId: string): ServerSessionInfo {
1229
+ return {
1230
+ type: "session_info",
1231
+ owner_uuid: this.userUUID,
1232
+ title: this.sessionTitle,
1233
+ saved_agent_profile: this.savedAgentProfile,
1234
+ workspace: this.workspaceUserMessageData(),
1235
+ updated_at: this.sessionUpdatedAt,
1236
+ participants: this.getSessionParticipants(),
1237
+ mcp_server_briefs: this.skillManager.getServerBriefs(),
1238
+ agent_preferences: this.agentProfilePreferences,
1239
+ client_message_id: clientMessageId,
1240
+ session_id: this.sessionUUID,
1241
+ team_uuid: this.teamUUID,
1242
+ };
1243
+ }
1244
+
1245
+ // Helper method to add session context to messages
1246
+ private addSessionContext<T extends object>(
1247
+ msg: T
1248
+ ): T & { session_id: string } {
1249
+ return {
1250
+ ...msg,
1251
+ session_id: this.sessionUUID,
1252
+ };
1253
+ }
1254
+ }
1255
+
1256
+ async function loadDataForEmptySession(
1257
+ db: Database,
1258
+ agent_profile_uuid: string,
1259
+ owner_uuid: string
1260
+ ): Promise<{
1261
+ savedAgentProfile: SavedAgentProfile;
1262
+ ownerData: UserData;
1263
+ ownerApiKey: string;
1264
+ }> {
1265
+ const [savedAgentProfile, ownerData, ownerApiKey] = await Promise.all([
1266
+ db.getSavedAgentProfileById(agent_profile_uuid),
1267
+ db.getUserFromUuid(owner_uuid),
1268
+ db.getUserApiKey(owner_uuid),
1269
+ ]);
1270
+ if (!savedAgentProfile) {
1271
+ throw new ChatFatalError(`No such agent profile: ${agent_profile_uuid}`);
1272
+ }
1273
+ if (!ownerData) {
1274
+ throw new ChatFatalError(`No owner ${owner_uuid}`);
1275
+ }
1276
+ if (!ownerData.nickname) {
1277
+ throw new ChatFatalError(
1278
+ `User ${owner_uuid} has no username - cannot create session`
1279
+ );
1280
+ }
1281
+ if (!ownerApiKey) {
1282
+ throw new ChatFatalError(
1283
+ `User ${owner_uuid} has no api keys - cannot create session`
1284
+ );
1285
+ }
1286
+
1287
+ return { savedAgentProfile, ownerData, ownerApiKey };
1288
+ }
1289
+
1290
+ /**
1291
+ * Loads session data, agent profile, and owner information from the database.
1292
+ */
1293
+ async function loadSessionData(
1294
+ db: Database,
1295
+ sessionId: string
1296
+ ): Promise<{
1297
+ sessionData: SessionData;
1298
+ sessionMessages: SessionMessage[];
1299
+ sessionCheckpoint: SessionCheckpoint | undefined;
1300
+ savedAgentProfile: SavedAgentProfile;
1301
+ ownerData: UserData;
1302
+ ownerApiKey: string;
1303
+ sessionParticipants: SessionParticipantMap;
1304
+ }> {
1305
+ const [sessionData, sessionMessages, sessionParticipants, sessionCheckpoint] =
1306
+ await Promise.all([
1307
+ db.sessionGetById(sessionId),
1308
+ db.sessionMessagesGetConversation(sessionId, DEFAULT_NUM_MESSGAES, 0),
1309
+ db.sessionGetParticipants(sessionId),
1310
+ db.sessionCheckpointGet(sessionId),
1311
+ ]);
1312
+ if (!sessionData) {
1313
+ throw new ChatFatalError(`No such session: ${sessionId}`);
1314
+ }
1315
+
1316
+ const { savedAgentProfile, ownerData, ownerApiKey } =
1317
+ await loadDataForEmptySession(
1318
+ db,
1319
+ sessionData.agent_profile_uuid,
1320
+ sessionData.user_uuid
1321
+ );
1322
+
1323
+ return {
1324
+ sessionData,
1325
+ sessionMessages,
1326
+ sessionCheckpoint,
1327
+ savedAgentProfile,
1328
+ ownerData,
1329
+ ownerApiKey,
1330
+ sessionParticipants: createSessionParticipantMap(sessionParticipants),
1331
+ };
1332
+ }