@xalia/agent 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/agent/src/agent/agent.js +8 -5
  2. package/dist/agent/src/agent/agentUtils.js +9 -12
  3. package/dist/agent/src/chat/client/chatClient.js +88 -240
  4. package/dist/agent/src/chat/client/constants.js +1 -2
  5. package/dist/agent/src/chat/client/sessionClient.js +4 -13
  6. package/dist/agent/src/chat/client/sessionFiles.js +3 -3
  7. package/dist/agent/src/chat/protocol/messages.js +0 -1
  8. package/dist/agent/src/chat/server/chatContextManager.js +5 -9
  9. package/dist/agent/src/chat/server/connectionManager.test.js +1 -0
  10. package/dist/agent/src/chat/server/conversation.js +9 -4
  11. package/dist/agent/src/chat/server/openSession.js +241 -238
  12. package/dist/agent/src/chat/server/openSessionMessageSender.js +2 -0
  13. package/dist/agent/src/chat/server/sessionRegistry.js +17 -12
  14. package/dist/agent/src/chat/utils/approvalManager.js +82 -64
  15. package/dist/agent/src/chat/{client/responseHandler.js → utils/responseAwaiter.js} +41 -18
  16. package/dist/agent/src/test/agent.test.js +90 -53
  17. package/dist/agent/src/test/approvalManager.test.js +79 -35
  18. package/dist/agent/src/test/chatContextManager.test.js +12 -17
  19. package/dist/agent/src/test/responseAwaiter.test.js +74 -0
  20. package/dist/agent/src/tool/agentChat.js +1 -1
  21. package/dist/agent/src/tool/chatMain.js +2 -2
  22. package/package.json +1 -1
  23. package/scripts/setup_chat +2 -2
  24. package/scripts/test_chat +61 -60
  25. package/src/agent/agent.ts +9 -5
  26. package/src/agent/agentUtils.ts +14 -27
  27. package/src/chat/client/chatClient.ts +167 -296
  28. package/src/chat/client/constants.ts +0 -2
  29. package/src/chat/client/sessionClient.ts +15 -19
  30. package/src/chat/client/sessionFiles.ts +9 -12
  31. package/src/chat/data/dataModels.ts +1 -0
  32. package/src/chat/protocol/messages.ts +9 -12
  33. package/src/chat/server/chatContextManager.ts +7 -12
  34. package/src/chat/server/connectionManager.test.ts +1 -0
  35. package/src/chat/server/conversation.ts +19 -11
  36. package/src/chat/server/openSession.ts +383 -340
  37. package/src/chat/server/openSessionMessageSender.ts +4 -0
  38. package/src/chat/server/sessionRegistry.ts +33 -12
  39. package/src/chat/utils/approvalManager.ts +153 -81
  40. package/src/chat/{client/responseHandler.ts → utils/responseAwaiter.ts} +73 -23
  41. package/src/test/agent.test.ts +130 -62
  42. package/src/test/approvalManager.test.ts +108 -40
  43. package/src/test/chatContextManager.test.ts +19 -20
  44. package/src/test/responseAwaiter.test.ts +103 -0
  45. package/src/tool/agentChat.ts +2 -2
  46. package/src/tool/chatMain.ts +2 -2
  47. package/dist/agent/src/test/responseHandler.test.js +0 -61
  48. package/src/test/responseHandler.test.ts +0 -78
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OpenSession = exports.GUEST_TOKEN_PREFIX = exports.DEFAULT_NUM_MESSGAES = void 0;
3
+ exports.OpenSession = exports.ChatSessionAgentEventHandler = exports.ChatSessionMessageSender = exports.GUEST_TOKEN_PREFIX = exports.DEFAULT_NUM_MESSGAES = void 0;
4
4
  const assert_1 = require("assert");
5
5
  const sdk_1 = require("@xalia/xmcp/sdk");
6
6
  const agent_1 = require("../../agent/agent");
@@ -43,6 +43,140 @@ class DBCheckpointWriter {
43
43
  return this.db.sessionCheckpointSet(this.sessionId, checkpoint);
44
44
  }
45
45
  }
46
+ class ChatSessionMessageSender {
47
+ constructor(connectionManager, sessionParticipants) {
48
+ this.connectionManager = connectionManager;
49
+ this.sessionParticipants = sessionParticipants;
50
+ }
51
+ broadcast(msg) {
52
+ const users = new Set(this.sessionParticipants.keys());
53
+ this.connectionManager.sendToUsers(users, msg);
54
+ }
55
+ sendTo(userUUID, msg) {
56
+ this.connectionManager.sendToUsers(new Set([userUUID]), msg);
57
+ }
58
+ }
59
+ exports.ChatSessionMessageSender = ChatSessionMessageSender;
60
+ class ChatSessionPlatform {
61
+ constructor(sender, sessionUUID, ownerUUID) {
62
+ this.sender = sender;
63
+ this.sessionUUID = sessionUUID;
64
+ this.ownerUUID = ownerUUID;
65
+ }
66
+ // IPlatform.openUrl
67
+ openUrl(url, authResultP, display_name) {
68
+ // These requests are always passed to the original owner, since it is
69
+ // their settings that will be used for all MCP servers.
70
+ this.sender.broadcast({
71
+ type: "authentication_started",
72
+ session_id: this.sessionUUID,
73
+ url,
74
+ });
75
+ this.sender.sendTo(this.ownerUUID, {
76
+ type: "authenticate",
77
+ session_id: this.sessionUUID,
78
+ url,
79
+ display_name,
80
+ });
81
+ // TODO: auth timeout
82
+ // Don't stall this function waiting for authentication
83
+ void authResultP.then((result) => {
84
+ this.sender.broadcast({
85
+ type: "authentication_finished",
86
+ session_id: this.sessionUUID,
87
+ url,
88
+ result,
89
+ });
90
+ });
91
+ }
92
+ // IPlatform.load
93
+ load(filename) {
94
+ if (process.env.DEVELOPMENT === "1") {
95
+ return nodePlatform_1.NODE_PLATFORM.load(filename);
96
+ }
97
+ throw new errors_1.ChatErrorMessage("Platform.load not implemented");
98
+ }
99
+ // IPlatform.renderHTML
100
+ renderHTML(html) {
101
+ return new Promise((r) => {
102
+ this.sender.broadcast({
103
+ type: "render_html",
104
+ html,
105
+ session_id: this.sessionUUID,
106
+ });
107
+ r();
108
+ });
109
+ }
110
+ }
111
+ class ChatSessionAgentEventHandler {
112
+ constructor(sessionUUID, sender, approvalManager, contextManager) {
113
+ this.sessionUUID = sessionUUID;
114
+ this.sender = sender;
115
+ this.approvalManager = approvalManager;
116
+ this.contextManager = contextManager;
117
+ }
118
+ onCompletion(result) {
119
+ logger.debug(`[OpenSession.onCompletion] : ${JSON.stringify(result)}`);
120
+ // Nothing to broadcast. Caller will receive this via onAgentMessage.
121
+ this.contextManager.processAgentResponse(result);
122
+ }
123
+ onImage(image) {
124
+ logger.debug(`[OpenSession.onImage] : ${image.image_url.url}`);
125
+ throw new Error("[OpenSession.onImage] unimplemented");
126
+ }
127
+ onToolCallResult(result) {
128
+ logger.debug(`[onToolCallResult] : ${JSON.stringify(result)}`);
129
+ const toolCallMessage = this.contextManager.processToolCallResult(result);
130
+ this.sender.broadcast(toolCallMessage);
131
+ }
132
+ async onToolCall(toolCall, agentTool) {
133
+ if (agentTool) {
134
+ // "Agent" tools are considered internal to the agent, and are always
135
+ // permitted. Inform all clients and immediately approve.
136
+ this.sender.broadcast({
137
+ type: "tool_call",
138
+ tool_call: toolCall,
139
+ session_id: this.sessionUUID,
140
+ });
141
+ return true;
142
+ }
143
+ // TODO: Need a proper mapping to/from MCP calls to tool names
144
+ const [serverName, tool] = toolCall.function.name.split("__");
145
+ const { approved, requested } = await this.approvalManager.getApproval(serverName, tool, toolCall);
146
+ // For now, the frontend uses the tool_call data in the
147
+ // "approve_tool_call" request to display the tool call data. If approval
148
+ // was requested in this way, don't send the "tool_call" message as well.
149
+ if (approved && !requested) {
150
+ this.sender.broadcast({
151
+ type: "tool_call",
152
+ tool_call: toolCall,
153
+ session_id: this.sessionUUID,
154
+ });
155
+ }
156
+ return approved;
157
+ }
158
+ onAgentMessage(msg, end) {
159
+ logger.debug(`[OpenSession.onAgentMessage] msg: ${msg}, end: ${String(end)}`);
160
+ // Inform the contextManager and broadcast the ServerAgentMessageChunk
161
+ const agentMsgChunk = this.contextManager.processAgentMessage(msg, end);
162
+ this.sender.broadcast(agentMsgChunk);
163
+ return Promise.resolve();
164
+ }
165
+ onReasoning(reasoning) {
166
+ return new Promise((r) => {
167
+ logger.debug(`[OpenSession.onReasoning]${reasoning}`);
168
+ if (reasoning.length > 0) {
169
+ this.sender.broadcast({
170
+ type: "agent_reasoning_chunk",
171
+ reasoning,
172
+ session_id: this.sessionUUID,
173
+ });
174
+ }
175
+ r();
176
+ });
177
+ }
178
+ }
179
+ exports.ChatSessionAgentEventHandler = ChatSessionAgentEventHandler;
46
180
  /**
47
181
  * Describes a Session (conversation) with connected participants.
48
182
  *
@@ -54,7 +188,7 @@ class DBCheckpointWriter {
54
188
  * tool approvals and other interactions).
55
189
  */
56
190
  class OpenSession {
57
- constructor(db, agent, sessionData, savedAgentProfile, isPersisted, sessionParticipants, agentProfilePreferences, skillManager, contextManager, connectionManager, approvalManager, fileManager) {
191
+ constructor(db, agent, sessionData, savedAgentProfile, isPersisted, sessionParticipants, agentProfilePreferences, skillManager, contextManager, sender, approvalManager, fileManager) {
58
192
  this.db = db;
59
193
  this.agent = agent;
60
194
  this.sessionUUID = sessionData.session_uuid;
@@ -65,7 +199,7 @@ class OpenSession {
65
199
  this.sessionParticipants = sessionParticipants;
66
200
  this.agentProfilePreferences = agentProfilePreferences;
67
201
  this.skillManager = skillManager;
68
- this.connectionManager = connectionManager;
202
+ this.sender = sender;
69
203
  this.messageQueue = new asyncQueue_1.AsyncQueue((m) => this.processMessage(m));
70
204
  this.userMessageQueue = new multiAsyncQueue_1.MultiAsyncQueue((m) => this.processUserMessages(m));
71
205
  this.contextManager = contextManager;
@@ -81,23 +215,12 @@ class OpenSession {
81
215
  static async init(db, isPersisted, sessionData, savedAgentProfile, sessionMessages, sessionParticipants, ownerData, ownerApiKey, sessionCheckpoint, llmUrl, xmcpUrl, connectionManager) {
82
216
  const sessionId = sessionData.session_uuid;
83
217
  const fileManager = await sessionFileManager_1.ChatSessionFileManager.init(db, sessionId);
84
- const contextManager = new chatContextManager_1.ChatContextManager(savedAgentProfile.profile.system_prompt, sessionMessages, sessionId, ownerData.uuid, sessionCheckpoint, llmUrl, savedAgentProfile.profile.model || DEFAULT_CHAT_LLM_MODEL, ownerApiKey, new DBCheckpointWriter(db, sessionId), fileManager);
85
- if (sessionData.workspace) {
86
- const ws = sessionData.workspace;
87
- contextManager.setWorkspace((0, agent_1.createUserMessage)(ws.message, ws.imageB64));
88
- }
89
- const openSession = new OpenSession(db, {}, // Placeholder - will be replaced after agent creation
90
- sessionData, savedAgentProfile, isPersisted, sessionParticipants, savedAgentProfile.preferences, {}, // Placeholder - will be replaced after agent creation
91
- contextManager, connectionManager, new approvalManager_1.ApprovalManager(), fileManager);
92
- // Initialize an empty agent (to ensure there are no callbacks before
93
- // the OpenSession and client are fully set up).
94
- const xmcpConfig = sdk_1.Configuration.new(ownerApiKey, xmcpUrl, false);
95
- const [agent, skillManager] = await (0, agentUtils_1.createAgentWithoutSkills)(llmUrl, savedAgentProfile.profile, DEFAULT_CHAT_LLM_MODEL, openSession, openSession, contextManager, ownerApiKey, xmcpConfig, undefined, true);
96
- await (0, tools_1.addDefaultChatTools)(agent, ownerData.timezone, openSession, fileManager, llmUrl, ownerApiKey);
97
- // Update OpenSession with real agent and skillManager
98
- openSession.agent = agent;
99
- openSession.skillManager = skillManager;
100
- await openSession.restoreMcpSettings(savedAgentProfile.profile.mcp_settings);
218
+ const sender = new ChatSessionMessageSender(connectionManager, sessionParticipants);
219
+ const platform = new ChatSessionPlatform(sender, sessionId, ownerData.uuid);
220
+ const toolApprovalManager = new approvalManager_1.ToolApprovalManager(sessionData.session_uuid, savedAgentProfile.uuid, savedAgentProfile.preferences, sender, new approvalManager_1.DbAgentPreferencesWriter(db));
221
+ const { agent, skillManager, contextManager } = await createContextAndAgent(sessionId, savedAgentProfile.profile.system_prompt, savedAgentProfile.profile.model || DEFAULT_CHAT_LLM_MODEL, sessionMessages, sessionData.workspace, sessionCheckpoint, ownerData, ownerApiKey, llmUrl, xmcpUrl, fileManager, sender, platform, toolApprovalManager);
222
+ const openSession = new OpenSession(db, agent, sessionData, savedAgentProfile, isPersisted, sessionParticipants, savedAgentProfile.preferences, skillManager, contextManager, sender, toolApprovalManager, fileManager);
223
+ // Note, MCP servers have not been enabled yet
101
224
  return openSession;
102
225
  }
103
226
  static async initWithEmptySession(db, sessionData, llmUrl, xmcpUrl, connectionManager) {
@@ -108,12 +231,19 @@ class OpenSession {
108
231
  throw new errors_1.ChatErrorMessage("failed finding owners default api key");
109
232
  }
110
233
  const sessionParticipants = new Map();
111
- sessionParticipants.set(sessionData.user_uuid, {
112
- user_uuid: sessionData.user_uuid,
113
- nickname: ownerData.nickname || "",
114
- email: ownerData.email,
115
- role: "owner",
116
- });
234
+ if (sessionData.participants && sessionData.participants.length > 0) {
235
+ sessionData.participants.forEach((p) => {
236
+ sessionParticipants.set(p.user_uuid, p);
237
+ });
238
+ }
239
+ else {
240
+ sessionParticipants.set(sessionData.user_uuid, {
241
+ user_uuid: sessionData.user_uuid,
242
+ nickname: ownerData.nickname || "",
243
+ email: ownerData.email,
244
+ role: "owner",
245
+ });
246
+ }
117
247
  return OpenSession.init(db, false /* isPersisted */, sessionData, savedAgentProfile, sessionMessages, sessionParticipants, ownerData, ownerApiKey, sessionCheckpoint, llmUrl, xmcpUrl, connectionManager);
118
248
  }
119
249
  static async initWithExistingSession(db, sessionId, llmUrl, xmcpUrl, connectionManager) {
@@ -141,14 +271,22 @@ class OpenSession {
141
271
  getParticipants() {
142
272
  return this.sessionParticipants;
143
273
  }
144
- sendSessionData(connectionId, clientMessageId) {
274
+ async sendSessionData(connectionId, clientMessageId, restoreMcpState) {
145
275
  logger.info(`[SessionRegistry] sending session data for session ${this.sessionUUID}`);
146
276
  const sessionInfo = this.serverSessionInfo(clientMessageId);
147
- this.connectionManager.sendToConnection(connectionId, sessionInfo);
277
+ const connMgr = this.sender.connectionManager;
278
+ connMgr.sendToConnection(connectionId, sessionInfo);
279
+ // This could be cleaner. If the session has just been created, we must
280
+ // restore the mcp servers. However, we cannot do that until the client
281
+ // has initialized itself (in case we need auth messages), hence it must
282
+ // happen at this stage, BEFORE we call sendMcpSettings below.
283
+ if (restoreMcpState) {
284
+ await this.restoreMcpSettings(this.savedAgentProfile.profile.mcp_settings);
285
+ }
148
286
  // send session file info
149
287
  const fileDescriptors = this.sessionFileManager.listFiles();
150
288
  fileDescriptors.forEach((descriptor) => {
151
- this.connectionManager.sendToConnection(connectionId, {
289
+ connMgr.sendToConnection(connectionId, {
152
290
  type: "session_file_changed",
153
291
  session_id: this.sessionUUID,
154
292
  descriptor,
@@ -158,18 +296,18 @@ class OpenSession {
158
296
  // send conversation history
159
297
  const conversationMessages = this.contextManager.getConversationMessages();
160
298
  conversationMessages.forEach((message) => {
161
- this.connectionManager.sendToConnection(connectionId, message);
299
+ connMgr.sendToConnection(connectionId, message);
162
300
  });
163
301
  // add MCP settings
164
302
  this.sendMcpSettings(connectionId);
165
303
  // add system prompt and model
166
304
  const agentProfile = this.agent.getAgentProfile();
167
- this.connectionManager.sendToConnection(connectionId, {
305
+ connMgr.sendToConnection(connectionId, {
168
306
  type: "system_prompt_updated",
169
307
  system_prompt: agentProfile.system_prompt,
170
308
  session_id: this.sessionUUID,
171
309
  });
172
- this.connectionManager.sendToConnection(connectionId, {
310
+ connMgr.sendToConnection(connectionId, {
173
311
  type: "model_updated",
174
312
  model: agentProfile.model || "",
175
313
  session_id: this.sessionUUID,
@@ -212,7 +350,7 @@ class OpenSession {
212
350
  }
213
351
  }
214
352
  catch (e) {
215
- this.broadcast({
353
+ this.sender.broadcast({
216
354
  type: "session_error",
217
355
  message: `Error adding MCP server ${server_name}: ${String(e)}`,
218
356
  session_id: this.sessionUUID,
@@ -230,10 +368,10 @@ class OpenSession {
230
368
  handleError(err, from) {
231
369
  const sendError = (msg) => {
232
370
  if (from) {
233
- this.sendTo(from, msg);
371
+ this.sender.sendTo(from, msg);
234
372
  }
235
373
  else {
236
- this.broadcast(msg);
374
+ this.sender.broadcast(msg);
237
375
  }
238
376
  };
239
377
  if (err instanceof errors_1.ChatFatalError) {
@@ -262,150 +400,9 @@ class OpenSession {
262
400
  }
263
401
  return true;
264
402
  }
265
- broadcast(msg) {
266
- const users = new Set(this.sessionParticipants.keys());
267
- this.connectionManager.sendToUsers(users, msg);
268
- }
269
- sendTo(user_uuid, msg) {
270
- this.connectionManager.sendToUsers(new Set([user_uuid]), msg);
271
- }
272
- // IPlatform.openUrl
273
- openUrl(url, authResultP, display_name) {
274
- // These requests are always passed to the original owner, since it is
275
- // their settings that will be used for all MCP servers.
276
- this.broadcast({
277
- type: "authentication_started",
278
- session_id: this.sessionUUID,
279
- url,
280
- });
281
- this.sendTo(this.userUUID, {
282
- type: "authenticate",
283
- session_id: this.sessionUUID,
284
- url,
285
- display_name,
286
- });
287
- // TODO: auth timeout
288
- // Don't stall this function waiting for authentication
289
- void authResultP.then((result) => {
290
- this.sendTo(this.userUUID, {
291
- type: "authentication_finished",
292
- session_id: this.sessionUUID,
293
- url,
294
- result,
295
- });
296
- });
297
- }
298
- // IPlatform.load
299
- load(filename) {
300
- if (process.env.DEVELOPMENT === "1") {
301
- return nodePlatform_1.NODE_PLATFORM.load(filename);
302
- }
303
- throw new errors_1.ChatErrorMessage("Platform.load not implemented");
304
- }
305
- // IPlatform.renderHTML
306
- renderHTML(html) {
307
- return new Promise((r) => {
308
- this.broadcast({
309
- type: "render_html",
310
- html,
311
- session_id: this.sessionUUID,
312
- });
313
- r();
314
- });
315
- }
316
- // IAgentEventHandler.onCompletion
317
- onCompletion(result) {
318
- logger.debug(`[OpenSession.onCompletion] : ${JSON.stringify(result)}`);
319
- // Nothing to broadcast. Caller will receive this via onAgentMessage.
320
- this.contextManager.processAgentResponse(result);
321
- }
322
- // IAgentEventHandler.onImage
323
- onImage(image) {
324
- logger.debug(`[OpenSession.onImage] : ${image.image_url.url}`);
325
- throw new Error("[OpenSession.onImage] unimplemented");
326
- }
327
- // IAgentEventHandler.onToolCallResult
328
- onToolCallResult(result) {
329
- logger.debug(`[onToolCallResult] : ${JSON.stringify(result)}`);
330
- const toolCallMessage = this.contextManager.processToolCallResult(result);
331
- this.broadcast(toolCallMessage);
332
- }
333
- // IAgentEventHandler.onToolCall
334
- async onToolCall(toolCall, agentTool) {
335
- if (agentTool) {
336
- // "Agent" tools are considered internal to the agent, and are always
337
- // permitted. Inform all clients and immediately approve.
338
- this.broadcast({
339
- type: "tool_call",
340
- tool_call: toolCall,
341
- session_id: this.sessionUUID,
342
- });
343
- return true;
344
- }
345
- // TODO: Need a proper mapping to/from MCP calls to tool names
346
- const [serverName, tool] = toolCall.function.name.split("__");
347
- const autoApproved = (0, sdk_1.prefsGetAutoApprove)(this.agentProfilePreferences, serverName, tool);
348
- if (!autoApproved) {
349
- const { id, resultP } = this.approvalManager.startApproval(toolCall.function.name);
350
- this.broadcast({
351
- type: "approve_tool_call",
352
- id,
353
- tool_call: toolCall,
354
- session_id: this.sessionUUID,
355
- });
356
- try {
357
- logger.debug(`[OpenSession.onToolCall] awaiting approval ${id}`);
358
- const { approved, auto_approve } = await resultP;
359
- logger.debug(`[OpenSession.onToolCall] approval ${id}: ${String(approved)}`);
360
- if (auto_approve) {
361
- logger.debug("[OpenSession.onToolCall] auto_approve set. updated preferences");
362
- const autoApprovalMsg = await this.onSetAutoApproval(serverName, tool, true);
363
- if (autoApprovalMsg) {
364
- this.broadcast(autoApprovalMsg);
365
- }
366
- }
367
- return approved;
368
- }
369
- catch (e) {
370
- logger.debug(`[OpenSession.onToolCall] error waiting for approval ${id}: ` +
371
- String(e));
372
- return false;
373
- }
374
- }
375
- else {
376
- this.broadcast({
377
- type: "tool_call",
378
- tool_call: toolCall,
379
- session_id: this.sessionUUID,
380
- });
381
- return true;
382
- }
383
- }
384
- // IAgentEventHandler.onAgentMessage
385
- // eslint-disable-next-line @typescript-eslint/require-await
386
- async onAgentMessage(msg, end) {
387
- logger.debug(`[OpenSession.onAgentMessage] msg: ${msg}, end: ${String(end)}`);
388
- // Inform the contextManager and broadcast the ServerAgentMessageChunk
389
- const agentMsgChunk = this.contextManager.processAgentMessage(msg, end);
390
- this.broadcast(agentMsgChunk);
391
- }
392
- // IAgentEventHandler.onReasoning
393
- onReasoning(reasoning) {
394
- return new Promise((r) => {
395
- logger.debug(`[OpenSession.onReasoning]${reasoning}`);
396
- if (reasoning.length > 0) {
397
- this.broadcast({
398
- type: "agent_reasoning_chunk",
399
- reasoning,
400
- session_id: this.sessionUUID,
401
- });
402
- }
403
- r();
404
- });
405
- }
406
403
  // ISessionFileManagerEventHandler.onFileDeleted
407
404
  onFileDeleted(name) {
408
- this.broadcast({
405
+ this.sender.broadcast({
409
406
  type: "session_file_deleted",
410
407
  session_id: this.sessionUUID,
411
408
  name,
@@ -413,7 +410,7 @@ class OpenSession {
413
410
  }
414
411
  // ISessionFileManagerEventHandler.onFileChanged
415
412
  onFileChanged(entry, new_file) {
416
- this.broadcast({
413
+ this.sender.broadcast({
417
414
  type: "session_file_changed",
418
415
  session_id: this.sessionUUID,
419
416
  descriptor: {
@@ -435,7 +432,7 @@ class OpenSession {
435
432
  // Validate user is in session
436
433
  if (!this.sessionParticipants.get(from)) {
437
434
  logger.warn(`User ${from} not in session ${this.sessionUUID} - ignoring message`);
438
- this.sendTo(from, {
435
+ this.sender.sendTo(from, {
439
436
  type: "session_error",
440
437
  message: "You are not a participant in this session",
441
438
  session_id: this.sessionUUID,
@@ -445,7 +442,7 @@ class OpenSession {
445
442
  }
446
443
  // Enqueue message for processing
447
444
  if (!this.messageQueue.tryEnqueue({ msg: message, from })) {
448
- this.sendTo(from, this.addSessionContext({
445
+ this.sender.sendTo(from, this.addSessionContext({
449
446
  type: "session_error",
450
447
  message: "message queue full. try again later",
451
448
  client_message_id: message.client_message_id,
@@ -462,7 +459,7 @@ class OpenSession {
462
459
  const mcpServer = this.skillManager.getMcpServer(server_name);
463
460
  const tools = mcpServer.getTools();
464
461
  const enabled_tools = Array.from(mcpServer.getEnabledTools().keys());
465
- this.connectionManager.sendToConnection(connectionId, {
462
+ this.sender.connectionManager.sendToConnection(connectionId, {
466
463
  type: "mcp_server_added",
467
464
  server_name,
468
465
  tools,
@@ -485,7 +482,7 @@ class OpenSession {
485
482
  let broadcastMsg = undefined;
486
483
  switch (msg.type) {
487
484
  case "msg":
488
- broadcastMsg = this.handleUserMessage(msg, queuedMessage.from);
485
+ broadcastMsg = await this.handleUserMessage(msg, queuedMessage.from);
489
486
  break;
490
487
  case "add_mcp_server":
491
488
  broadcastMsg = await this.handleAddMcpServer(msg.server_name, msg.enable_all);
@@ -506,14 +503,7 @@ class OpenSession {
506
503
  broadcastMsg = await this.handleDisableAllMcpServerTools(msg.server_name);
507
504
  break;
508
505
  case "tool_call_approval_result":
509
- if (this.approvalManager.approvalResult(msg.id, msg.result, msg.auto_approve)) {
510
- broadcastMsg = {
511
- type: "tool_call_approval_result",
512
- id: msg.id,
513
- result: msg.result,
514
- session_id: this.sessionUUID,
515
- };
516
- }
506
+ this.approvalManager.onApprovalResult(msg);
517
507
  break;
518
508
  case "session_file_get_content":
519
509
  void this.handleSessionFileGetContent(msg, queuedMessage.from);
@@ -525,7 +515,7 @@ class OpenSession {
525
515
  await this.handleSessionFilePutContent(msg);
526
516
  break;
527
517
  case "set_auto_approval":
528
- broadcastMsg = await this.onSetAutoApproval(msg.server_name, msg.tool, msg.auto_approve);
518
+ broadcastMsg = await this.approvalManager.setAutoApprove(msg.server_name, msg.tool, msg.auto_approve);
529
519
  break;
530
520
  case "set_system_prompt":
531
521
  broadcastMsg = await this.handleSetSystemPrompt(msg.system_prompt);
@@ -553,11 +543,11 @@ class OpenSession {
553
543
  if (broadcastMsg) {
554
544
  if (broadcastMsg instanceof Array) {
555
545
  broadcastMsg.map((msg) => {
556
- this.broadcast(msg);
546
+ this.sender.broadcast(msg);
557
547
  });
558
548
  }
559
549
  else {
560
- this.broadcast(broadcastMsg);
550
+ this.sender.broadcast(broadcastMsg);
561
551
  }
562
552
  }
563
553
  }
@@ -573,7 +563,7 @@ class OpenSession {
573
563
  await this.db.sessionUpdateAccessToken(this.sessionUUID, accessToken);
574
564
  this.accessToken = accessToken;
575
565
  }
576
- this.sendTo(from, {
566
+ this.sender.sendTo(from, {
577
567
  type: "session_shared",
578
568
  access_token: this.accessToken,
579
569
  client_message_id: msg.client_message_id,
@@ -604,13 +594,9 @@ class OpenSession {
604
594
  this.contextManager.addMessages(llmUserMessages);
605
595
  return this.contextManager.endAgentResponse();
606
596
  }
607
- /**
608
- * `processUserMessage` logic when agent is active. Start the Agent loop,
609
- * adding all agent messages to the context. Extract the new DB messages.
610
- */
611
597
  async processUserMessagesActive(msgs) {
612
598
  const { llmUserMessages, agentFirstChunk } = this.contextManager.startAgentResponse(msgs);
613
- this.broadcast(agentFirstChunk);
599
+ this.sender.broadcast(agentFirstChunk);
614
600
  try {
615
601
  await this.agent.userMessagesRaw(llmUserMessages);
616
602
  }
@@ -618,20 +604,18 @@ class OpenSession {
618
604
  logger.warn(`[OpenSession.processUserMessages] agent error: ${String(e)}`);
619
605
  // Errors during agent replies must be turned into messages.
620
606
  const errMsg = `error from LLM: ${String(e)}`;
621
- await this.onAgentMessage(errMsg, true);
622
- const err = this.contextManager.revertAgentResponse(errMsg);
623
- this.broadcast(err);
624
- // return await this.processuserMessagesActive(msgs);
607
+ this.contextManager.revertAgentResponse(errMsg);
608
+ throw new Error(errMsg);
625
609
  }
626
610
  return this.contextManager.endAgentResponse();
627
611
  }
628
612
  async processUserMessages(msgs) {
629
- const newSessionMessages = this.agentPaused
630
- ? this.processUserMessagePaused(msgs)
631
- : await this.processUserMessagesActive(msgs);
632
- logger.debug("[processUserMessages] newSessionMessages: " +
633
- JSON.stringify(newSessionMessages));
634
613
  try {
614
+ const newSessionMessages = this.agentPaused
615
+ ? this.processUserMessagePaused(msgs)
616
+ : await this.processUserMessagesActive(msgs);
617
+ logger.debug("[processUserMessages] newSessionMessages: " +
618
+ JSON.stringify(newSessionMessages));
635
619
  // Append to in-memory conversation and write to the DB
636
620
  const dbsm = this.db.createTypedClient(dbSessionMessages_1.DbSessionMessages);
637
621
  await dbsm.append(this.sessionUUID, newSessionMessages);
@@ -642,28 +626,32 @@ class OpenSession {
642
626
  }
643
627
  }
644
628
  }
645
- handleUserMessage(msg, from) {
629
+ async handleUserMessage(msg, from) {
646
630
  // Return a ServerUserMessage for broadcast. The actual message is places
647
631
  // on a queue to be dealt with in another loop. This allows Agent
648
632
  // processing of user messages to depend on other messages.
649
633
  (0, assert_1.strict)(msg);
650
634
  (0, assert_1.strict)(from);
651
635
  // Assign the user message_idx and attempt to enqueue.
652
- const userMessage = this.contextManager.processUserMessage(msg, from);
636
+ const user = this.sessionParticipants.get(from);
637
+ if (!user) {
638
+ throw new Error(`unrecognized user ${from}`);
639
+ }
640
+ const userMessage = this.contextManager.processUserMessage(msg, from, user.nickname);
653
641
  if (!userMessage) {
654
642
  return;
655
643
  }
656
644
  // Special case for the first message of the session
657
645
  if (userMessage.message_idx === conversation_1.MESSAGE_INDEX_START_VALUE) {
658
646
  // No need to wait for this to complete before broadcasting.
659
- void this.onFirstMessage(userMessage);
647
+ await this.onFirstMessage(userMessage);
660
648
  }
661
649
  if (!this.userMessageQueue.tryEnqueue(userMessage)) {
662
650
  // We failed to enqueue - revert the `getNextMessageIdx`
663
651
  // NOTE: Nothing should await between `getNextMessageIdx` and
664
652
  // `freeMessageIdx` here.
665
653
  this.contextManager.unprocessUserMessage(userMessage);
666
- this.sendTo(from, {
654
+ this.sender.sendTo(from, {
667
655
  type: "session_error",
668
656
  message: "failed to queue message. try again later.",
669
657
  session_id: this.sessionUUID,
@@ -703,13 +691,28 @@ class OpenSession {
703
691
  await this.db.sessionUpdateTitle(this.sessionUUID, this.sessionTitle);
704
692
  }
705
693
  (0, assert_1.strict)(this.isPersisted);
706
- // Broadcast the SessionUpdated message
707
- const msg = {
708
- type: "session_update",
709
- session_id: this.sessionUUID,
710
- title: this.sessionTitle,
711
- };
712
- this.broadcast(msg);
694
+ // Send session created notification
695
+ const sessionInfo = this.serverSessionInfo("");
696
+ if (this.teamUUID) {
697
+ // Team session: notify all members about the new session
698
+ try {
699
+ const teamMembers = await this.db.teamGetMembers(this.teamUUID);
700
+ const teamMemberIds = new Set(teamMembers.map((m) => m.user_uuid));
701
+ this.sender.connectionManager.sendToUsers(teamMemberIds, sessionInfo);
702
+ logger.info(`[OpenSession] notified ${String(teamMemberIds.size)} team members` +
703
+ ` about new session ${this.sessionUUID} in team ${this.teamUUID}`);
704
+ }
705
+ catch (error) {
706
+ logger.error("[OpenSession] Error notifying team members about session" +
707
+ `${this.sessionUUID}: ${String(error)}`);
708
+ }
709
+ }
710
+ else {
711
+ // If this is a user session, notify the session owner
712
+ this.sender.connectionManager.sendToUsers(new Set([this.userUUID]), sessionInfo);
713
+ logger.info(`[OpenSession] notified session owner ${this.userUUID} about ` +
714
+ `new session ${this.sessionUUID}`);
715
+ }
713
716
  }
714
717
  async handleAddMcpServer(serverName, enableAll) {
715
718
  logger.info(`[onAddMcpServer]: Adding server ${serverName} ` +
@@ -862,7 +865,7 @@ class OpenSession {
862
865
  name: msg.name,
863
866
  data_url,
864
867
  };
865
- this.sendTo(from, contentMsg);
868
+ this.sender.sendTo(from, contentMsg);
866
869
  }
867
870
  async handleSessionFileDelete(msg) {
868
871
  await this.sessionFileManager.deleteFile(msg.name);
@@ -882,18 +885,6 @@ class OpenSession {
882
885
  // `ISessionFileManagerEventHandler.onFileChanged` if the deletion
883
886
  // succeeds. We broadcast in that callback.
884
887
  }
885
- async onSetAutoApproval(serverName, tool, autoApprove) {
886
- if ((0, sdk_1.prefsSetAutoApprove)(this.agentProfilePreferences, serverName, tool, autoApprove)) {
887
- await this.db.updateAgentProfilePreferences(this.agentProfileUUID, this.agentProfilePreferences);
888
- return {
889
- type: "tool_auto_approval_set",
890
- server_name: serverName,
891
- tool,
892
- auto_approve: autoApprove,
893
- session_id: this.sessionUUID,
894
- };
895
- }
896
- }
897
888
  ensureMcpServer(serverName) {
898
889
  return this.skillManager.getMcpServer(serverName);
899
890
  }
@@ -916,18 +907,18 @@ class OpenSession {
916
907
  * This only updates the local participant map - actual membership
917
908
  * tracking is handled by SessionRegistry.
918
909
  */
919
- addParticipant(userId, role) {
920
- this.sessionParticipants.set(userId, role);
910
+ addParticipant(userId, participant) {
911
+ this.sessionParticipants.set(userId, participant);
921
912
  // Broadcast result to all session participants
922
913
  const broadcastMessage = {
923
914
  type: "user_added",
924
915
  user_uuid: userId,
925
916
  role: "participant",
926
- nickname: role.nickname,
927
- email: role.email,
917
+ nickname: participant.nickname,
918
+ email: participant.email,
928
919
  session_id: this.sessionUUID,
929
920
  };
930
- this.broadcast(broadcastMessage);
921
+ this.sender.broadcast(broadcastMessage);
931
922
  }
932
923
  /**
933
924
  * Remove participant from session (called by SessionRegistry).
@@ -942,7 +933,7 @@ class OpenSession {
942
933
  user_uuid: userId,
943
934
  session_id: this.sessionUUID,
944
935
  };
945
- this.broadcast(broadcastMessage);
936
+ this.sender.broadcast(broadcastMessage);
946
937
  }
947
938
  getSessionParticipants() {
948
939
  return Array.from(this.sessionParticipants.entries()).map(([userId, user]) => ({
@@ -1029,3 +1020,15 @@ async function loadSessionData(db, sessionId) {
1029
1020
  sessionParticipants: (0, database_1.createSessionParticipantMap)(sessionParticipants),
1030
1021
  };
1031
1022
  }
1023
+ async function createContextAndAgent(sessionUUID, systemPrompt, model, sessionMessages, workspace, sessionCheckpoint, ownerData, ownerApiKey, llmUrl, xmcpUrl, fileManager, sender, platform, approvalManager) {
1024
+ const contextManager = new chatContextManager_1.ChatContextManager(systemPrompt, sessionMessages, sessionUUID, ownerData.uuid, sessionCheckpoint, llmUrl, model, ownerApiKey, undefined, // TODO
1025
+ fileManager);
1026
+ if (workspace) {
1027
+ contextManager.setWorkspace((0, agent_1.createUserMessage)(workspace.message, workspace.imageB64));
1028
+ }
1029
+ const eventHandler = new ChatSessionAgentEventHandler(sessionUUID, sender, approvalManager, contextManager);
1030
+ const xmcpConfig = sdk_1.Configuration.new(ownerApiKey, xmcpUrl, false);
1031
+ const [agent, skillManager] = await (0, agentUtils_1.createAgentWithoutSkills)(llmUrl, model, eventHandler, platform, contextManager, ownerApiKey, xmcpConfig, undefined, true);
1032
+ await (0, tools_1.addDefaultChatTools)(agent, ownerData.timezone, platform, fileManager, llmUrl, ownerApiKey);
1033
+ return { agent, skillManager, contextManager };
1034
+ }