@xalia/agent 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -7,8 +7,6 @@ import {
7
7
  Configuration,
8
8
  getLogger,
9
9
  McpServerSettings,
10
- prefsGetAutoApprove,
11
- prefsSetAutoApprove,
12
10
  SavedAgentProfile,
13
11
  } from "@xalia/xmcp/sdk";
14
12
 
@@ -34,13 +32,11 @@ import type {
34
32
  ServerModelUpdated,
35
33
  ServerUserMessage,
36
34
  ClientUserMessage,
37
- ServerToolAutoApprovalSet,
38
35
  ClientSessionMessage,
39
36
  ServerSessionError,
40
37
  ServerUserAdded,
41
38
  ServerUserRemoved,
42
39
  ServerSessionInfo,
43
- ServerSessionUpdate,
44
40
  ClientSetWorkspace,
45
41
  ClientToServer,
46
42
  ServerSessionFileContent,
@@ -66,7 +62,10 @@ import {
66
62
  createSessionParticipantMap,
67
63
  UserData,
68
64
  } from "../data/database";
69
- import { ApprovalManager } from "../utils/approvalManager";
65
+ import {
66
+ DbAgentPreferencesWriter,
67
+ ToolApprovalManager,
68
+ } from "../utils/approvalManager";
70
69
  import { ChatErrorMessage, ChatFatalError } from "../protocol/errors";
71
70
  import { addDefaultChatTools } from "./tools";
72
71
  import { ChatContextManager, ICheckpointWriter } from "./chatContextManager";
@@ -86,6 +85,7 @@ import { DbMcpServerConfigs } from "../data/dbMcpServerConfigs";
86
85
  import { SessionFileEntry } from "../data/dbSessionFileModels";
87
86
  import { ApiKeyManager } from "../data/apiKeyManager";
88
87
  import { DbSessionMessages } from "../data/dbSessionMessages";
88
+ import { ISessionMessageSender } from "./openSessionMessageSender";
89
89
 
90
90
  /**
91
91
  * The model to use when the AgentProfile does not specify one.
@@ -127,6 +127,170 @@ class DBCheckpointWriter implements ICheckpointWriter {
127
127
  }
128
128
  }
129
129
 
130
+ export class ChatSessionMessageSender
131
+ implements ISessionMessageSender<ServerToClient>
132
+ {
133
+ constructor(
134
+ public readonly connectionManager: IUserConnectionManager<ServerToClient>,
135
+ public readonly sessionParticipants: SessionParticipantMap
136
+ ) {}
137
+
138
+ broadcast(msg: ServerToClient): void {
139
+ const users: Set<string> = new Set(this.sessionParticipants.keys());
140
+ this.connectionManager.sendToUsers(users, msg);
141
+ }
142
+
143
+ sendTo(userUUID: string, msg: ServerToClient): void {
144
+ this.connectionManager.sendToUsers(new Set([userUUID]), msg);
145
+ }
146
+ }
147
+
148
+ class ChatSessionPlatform {
149
+ constructor(
150
+ private sender: ISessionMessageSender<ServerToClient>,
151
+ private sessionUUID: string,
152
+ private ownerUUID: string
153
+ ) {}
154
+
155
+ // IPlatform.openUrl
156
+ openUrl(url: string, authResultP: Promise<boolean>, display_name: string) {
157
+ // These requests are always passed to the original owner, since it is
158
+ // their settings that will be used for all MCP servers.
159
+ this.sender.broadcast({
160
+ type: "authentication_started",
161
+ session_id: this.sessionUUID,
162
+ url,
163
+ });
164
+ this.sender.sendTo(this.ownerUUID, {
165
+ type: "authenticate",
166
+ session_id: this.sessionUUID,
167
+ url,
168
+ display_name,
169
+ });
170
+
171
+ // TODO: auth timeout
172
+ // Don't stall this function waiting for authentication
173
+ void authResultP.then((result) => {
174
+ this.sender.broadcast({
175
+ type: "authentication_finished",
176
+ session_id: this.sessionUUID,
177
+ url,
178
+ result,
179
+ });
180
+ });
181
+ }
182
+
183
+ // IPlatform.load
184
+ load(filename: string): Promise<string> {
185
+ if (process.env.DEVELOPMENT === "1") {
186
+ return NODE_PLATFORM.load(filename);
187
+ }
188
+ throw new ChatErrorMessage("Platform.load not implemented");
189
+ }
190
+
191
+ // IPlatform.renderHTML
192
+ renderHTML(html: string): Promise<void> {
193
+ return new Promise<void>((r) => {
194
+ this.sender.broadcast({
195
+ type: "render_html",
196
+ html,
197
+ session_id: this.sessionUUID,
198
+ });
199
+ r();
200
+ });
201
+ }
202
+ }
203
+
204
+ export class ChatSessionAgentEventHandler implements IAgentEventHandler {
205
+ constructor(
206
+ private readonly sessionUUID: string,
207
+ private readonly sender: ISessionMessageSender<ServerToClient>,
208
+ private readonly approvalManager: ToolApprovalManager,
209
+ private readonly contextManager: ChatContextManager
210
+ ) {}
211
+
212
+ onCompletion(result: ChatCompletionAssistantMessageParam): void {
213
+ logger.debug(`[OpenSession.onCompletion] : ${JSON.stringify(result)}`);
214
+ // Nothing to broadcast. Caller will receive this via onAgentMessage.
215
+ this.contextManager.processAgentResponse(result);
216
+ }
217
+
218
+ onImage(image: OpenAI.Chat.Completions.ChatCompletionContentPartImage): void {
219
+ logger.debug(`[OpenSession.onImage] : ${image.image_url.url}`);
220
+ throw new Error("[OpenSession.onImage] unimplemented");
221
+ }
222
+
223
+ onToolCallResult(result: ChatCompletionToolMessageParam): void {
224
+ logger.debug(`[onToolCallResult] : ${JSON.stringify(result)}`);
225
+ const toolCallMessage = this.contextManager.processToolCallResult(result);
226
+ this.sender.broadcast(toolCallMessage);
227
+ }
228
+
229
+ async onToolCall(
230
+ toolCall: ChatCompletionMessageToolCall,
231
+ agentTool: boolean
232
+ ): Promise<boolean> {
233
+ if (agentTool) {
234
+ // "Agent" tools are considered internal to the agent, and are always
235
+ // permitted. Inform all clients and immediately approve.
236
+ this.sender.broadcast({
237
+ type: "tool_call",
238
+ tool_call: toolCall,
239
+ session_id: this.sessionUUID,
240
+ });
241
+ return true;
242
+ }
243
+
244
+ // TODO: Need a proper mapping to/from MCP calls to tool names
245
+
246
+ const [serverName, tool] = toolCall.function.name.split("__");
247
+ const { approved, requested } = await this.approvalManager.getApproval(
248
+ serverName,
249
+ tool,
250
+ toolCall
251
+ );
252
+
253
+ // For now, the frontend uses the tool_call data in the
254
+ // "approve_tool_call" request to display the tool call data. If approval
255
+ // was requested in this way, don't send the "tool_call" message as well.
256
+
257
+ if (approved && !requested) {
258
+ this.sender.broadcast({
259
+ type: "tool_call",
260
+ tool_call: toolCall,
261
+ session_id: this.sessionUUID,
262
+ });
263
+ }
264
+
265
+ return approved;
266
+ }
267
+
268
+ onAgentMessage(msg: string, end: boolean): Promise<void> {
269
+ logger.debug(
270
+ `[OpenSession.onAgentMessage] msg: ${msg}, end: ${String(end)}`
271
+ );
272
+
273
+ // Inform the contextManager and broadcast the ServerAgentMessageChunk
274
+ const agentMsgChunk = this.contextManager.processAgentMessage(msg, end);
275
+ this.sender.broadcast(agentMsgChunk);
276
+ return Promise.resolve();
277
+ }
278
+
279
+ onReasoning(reasoning: string): Promise<void> {
280
+ return new Promise<void>((r) => {
281
+ logger.debug(`[OpenSession.onReasoning]${reasoning}`);
282
+ if (reasoning.length > 0) {
283
+ this.sender.broadcast({
284
+ type: "agent_reasoning_chunk",
285
+ reasoning,
286
+ session_id: this.sessionUUID,
287
+ });
288
+ }
289
+ r();
290
+ });
291
+ }
292
+ }
293
+
130
294
  /**
131
295
  * Describes a Session (conversation) with connected participants.
132
296
  *
@@ -137,9 +301,7 @@ class DBCheckpointWriter implements ICheckpointWriter {
137
301
  * be seen until user messages had been fully processed, which could block
138
302
  * tool approvals and other interactions).
139
303
  */
140
- export class OpenSession
141
- implements IAgentEventHandler, ISessionFileManagerEventHandler, IPlatform
142
- {
304
+ export class OpenSession implements ISessionFileManagerEventHandler {
143
305
  private readonly db: Database;
144
306
  private /* readonly */ agent: Agent;
145
307
  private readonly sessionUUID: string;
@@ -149,11 +311,11 @@ export class OpenSession
149
311
  private readonly sessionParticipants: SessionParticipantMap;
150
312
  private readonly agentProfilePreferences: AgentPreferences;
151
313
  private /* readonly */ skillManager: SkillManager;
152
- private readonly connectionManager: IUserConnectionManager<ServerToClient>;
314
+ private readonly sender: ChatSessionMessageSender;
153
315
  private readonly messageQueue: AsyncQueue<QueuedClientMessage>;
154
316
  private readonly userMessageQueue: MultiAsyncQueue<ServerUserMessage>;
155
317
  private readonly contextManager: ChatContextManager;
156
- private readonly approvalManager: ApprovalManager;
318
+ private readonly approvalManager: ToolApprovalManager;
157
319
  private readonly savedAgentProfile: SavedAgentProfile;
158
320
  private readonly sessionFileManager: ISessionFileManager;
159
321
  private isPersisted: boolean;
@@ -172,8 +334,8 @@ export class OpenSession
172
334
  agentProfilePreferences: AgentPreferences,
173
335
  skillManager: SkillManager,
174
336
  contextManager: ChatContextManager,
175
- connectionManager: IUserConnectionManager<ServerToClient>,
176
- approvalManager: ApprovalManager,
337
+ sender: ChatSessionMessageSender,
338
+ approvalManager: ToolApprovalManager,
177
339
  fileManager: ISessionFileManager
178
340
  ) {
179
341
  this.db = db;
@@ -186,7 +348,7 @@ export class OpenSession
186
348
  this.sessionParticipants = sessionParticipants;
187
349
  this.agentProfilePreferences = agentProfilePreferences;
188
350
  this.skillManager = skillManager;
189
- this.connectionManager = connectionManager;
351
+ this.sender = sender;
190
352
  this.messageQueue = new AsyncQueue<QueuedClientMessage>((m) =>
191
353
  this.processMessage(m)
192
354
  );
@@ -223,70 +385,52 @@ export class OpenSession
223
385
  const sessionId = sessionData.session_uuid;
224
386
 
225
387
  const fileManager = await ChatSessionFileManager.init(db, sessionId);
226
- const contextManager = new ChatContextManager(
388
+ const sender = new ChatSessionMessageSender(
389
+ connectionManager,
390
+ sessionParticipants
391
+ );
392
+ const platform = new ChatSessionPlatform(sender, sessionId, ownerData.uuid);
393
+ const toolApprovalManager = new ToolApprovalManager(
394
+ sessionData.session_uuid,
395
+ savedAgentProfile.uuid,
396
+ savedAgentProfile.preferences,
397
+ sender,
398
+ new DbAgentPreferencesWriter(db)
399
+ );
400
+ const { agent, skillManager, contextManager } = await createContextAndAgent(
401
+ sessionId,
227
402
  savedAgentProfile.profile.system_prompt,
403
+ savedAgentProfile.profile.model || DEFAULT_CHAT_LLM_MODEL,
228
404
  sessionMessages,
229
- sessionId,
230
- ownerData.uuid,
405
+ sessionData.workspace,
231
406
  sessionCheckpoint,
232
- llmUrl,
233
- savedAgentProfile.profile.model || DEFAULT_CHAT_LLM_MODEL,
407
+ ownerData,
234
408
  ownerApiKey,
235
- new DBCheckpointWriter(db, sessionId),
236
- fileManager
409
+ llmUrl,
410
+ xmcpUrl,
411
+ fileManager,
412
+ sender,
413
+ platform,
414
+ toolApprovalManager
237
415
  );
238
- if (sessionData.workspace) {
239
- const ws = sessionData.workspace;
240
- contextManager.setWorkspace(createUserMessage(ws.message, ws.imageB64));
241
- }
242
416
 
243
417
  const openSession = new OpenSession(
244
418
  db,
245
- {} as Agent, // Placeholder - will be replaced after agent creation
419
+ agent,
246
420
  sessionData,
247
421
  savedAgentProfile,
248
422
  isPersisted,
249
423
  sessionParticipants,
250
424
  savedAgentProfile.preferences,
251
- {} as SkillManager, // Placeholder - will be replaced after agent creation
425
+ skillManager,
252
426
  contextManager,
253
- connectionManager,
254
- new ApprovalManager(),
427
+ sender,
428
+ toolApprovalManager,
255
429
  fileManager
256
430
  );
257
431
 
258
- // Initialize an empty agent (to ensure there are no callbacks before
259
- // the OpenSession and client are fully set up).
260
-
261
- const xmcpConfig = Configuration.new(ownerApiKey, xmcpUrl, false);
262
- const [agent, skillManager] = await createAgentWithoutSkills(
263
- llmUrl,
264
- savedAgentProfile.profile,
265
- DEFAULT_CHAT_LLM_MODEL,
266
- openSession,
267
- openSession,
268
- contextManager,
269
- ownerApiKey,
270
- xmcpConfig,
271
- undefined,
272
- true
273
- );
274
- await addDefaultChatTools(
275
- agent,
276
- ownerData.timezone,
277
- openSession,
278
- fileManager,
279
- llmUrl,
280
- ownerApiKey
281
- );
282
-
283
- // Update OpenSession with real agent and skillManager
284
- openSession.agent = agent;
285
- openSession.skillManager = skillManager;
432
+ // Note, MCP servers have not been enabled yet
286
433
 
287
- await openSession.restoreMcpSettings(
288
- savedAgentProfile.profile.mcp_settings
289
- );
290
434
  return openSession;
291
435
  }
292
436
 
@@ -311,12 +455,18 @@ export class OpenSession
311
455
  }
312
456
 
313
457
  const sessionParticipants = new Map<string, TeamParticipant>();
314
- sessionParticipants.set(sessionData.user_uuid, {
315
- user_uuid: sessionData.user_uuid,
316
- nickname: ownerData.nickname || "",
317
- email: ownerData.email,
318
- role: "owner",
319
- });
458
+ if (sessionData.participants && sessionData.participants.length > 0) {
459
+ sessionData.participants.forEach((p) => {
460
+ sessionParticipants.set(p.user_uuid, p);
461
+ });
462
+ } else {
463
+ sessionParticipants.set(sessionData.user_uuid, {
464
+ user_uuid: sessionData.user_uuid,
465
+ nickname: ownerData.nickname || "",
466
+ email: ownerData.email,
467
+ role: "owner",
468
+ });
469
+ }
320
470
 
321
471
  return OpenSession.init(
322
472
  db,
@@ -392,18 +542,34 @@ export class OpenSession
392
542
  return this.sessionParticipants;
393
543
  }
394
544
 
395
- sendSessionData(connectionId: string, clientMessageId: string): void {
545
+ async sendSessionData(
546
+ connectionId: string,
547
+ clientMessageId: string,
548
+ restoreMcpState: boolean
549
+ ): Promise<void> {
396
550
  logger.info(
397
551
  `[SessionRegistry] sending session data for session ${this.sessionUUID}`
398
552
  );
399
553
 
400
554
  const sessionInfo = this.serverSessionInfo(clientMessageId);
401
- this.connectionManager.sendToConnection(connectionId, sessionInfo);
555
+ const connMgr = this.sender.connectionManager;
556
+ connMgr.sendToConnection(connectionId, sessionInfo);
557
+
558
+ // This could be cleaner. If the session has just been created, we must
559
+ // restore the mcp servers. However, we cannot do that until the client
560
+ // has initialized itself (in case we need auth messages), hence it must
561
+ // happen at this stage, BEFORE we call sendMcpSettings below.
562
+
563
+ if (restoreMcpState) {
564
+ await this.restoreMcpSettings(
565
+ this.savedAgentProfile.profile.mcp_settings
566
+ );
567
+ }
402
568
 
403
569
  // send session file info
404
570
  const fileDescriptors = this.sessionFileManager.listFiles();
405
571
  fileDescriptors.forEach((descriptor) => {
406
- this.connectionManager.sendToConnection(connectionId, {
572
+ connMgr.sendToConnection(connectionId, {
407
573
  type: "session_file_changed",
408
574
  session_id: this.sessionUUID,
409
575
  descriptor,
@@ -414,7 +580,7 @@ export class OpenSession
414
580
  // send conversation history
415
581
  const conversationMessages = this.contextManager.getConversationMessages();
416
582
  conversationMessages.forEach((message) => {
417
- this.connectionManager.sendToConnection(connectionId, message);
583
+ connMgr.sendToConnection(connectionId, message);
418
584
  });
419
585
 
420
586
  // add MCP settings
@@ -422,12 +588,12 @@ export class OpenSession
422
588
 
423
589
  // add system prompt and model
424
590
  const agentProfile = this.agent.getAgentProfile();
425
- this.connectionManager.sendToConnection(connectionId, {
591
+ connMgr.sendToConnection(connectionId, {
426
592
  type: "system_prompt_updated",
427
593
  system_prompt: agentProfile.system_prompt,
428
594
  session_id: this.sessionUUID,
429
595
  });
430
- this.connectionManager.sendToConnection(connectionId, {
596
+ connMgr.sendToConnection(connectionId, {
431
597
  type: "model_updated",
432
598
  model: agentProfile.model || "",
433
599
  session_id: this.sessionUUID,
@@ -478,7 +644,7 @@ export class OpenSession
478
644
  skillManager.enableTool(server_name, enabled_tool);
479
645
  }
480
646
  } catch (e) {
481
- this.broadcast({
647
+ this.sender.broadcast({
482
648
  type: "session_error",
483
649
  message: `Error adding MCP server ${server_name}: ${String(e)}`,
484
650
  session_id: this.sessionUUID,
@@ -497,9 +663,9 @@ export class OpenSession
497
663
  private handleError(err: unknown, from?: string): boolean {
498
664
  const sendError = (msg: ServerSessionError) => {
499
665
  if (from) {
500
- this.sendTo(from, msg);
666
+ this.sender.sendTo(from, msg);
501
667
  } else {
502
- this.broadcast(msg);
668
+ this.sender.broadcast(msg);
503
669
  }
504
670
  };
505
671
 
@@ -529,186 +695,9 @@ export class OpenSession
529
695
  return true;
530
696
  }
531
697
 
532
- private broadcast(msg: ServerToClient): void {
533
- const users: Set<string> = new Set(this.sessionParticipants.keys());
534
- this.connectionManager.sendToUsers(users, msg);
535
- }
536
-
537
- sendTo(user_uuid: string, msg: ServerToClient): void {
538
- this.connectionManager.sendToUsers(new Set([user_uuid]), msg);
539
- }
540
-
541
- // IPlatform.openUrl
542
- openUrl(url: string, authResultP: Promise<boolean>, display_name: string) {
543
- // These requests are always passed to the original owner, since it is
544
- // their settings that will be used for all MCP servers.
545
- this.broadcast({
546
- type: "authentication_started",
547
- session_id: this.sessionUUID,
548
- url,
549
- });
550
- this.sendTo(this.userUUID, {
551
- type: "authenticate",
552
- session_id: this.sessionUUID,
553
- url,
554
- display_name,
555
- });
556
-
557
- // TODO: auth timeout
558
- // Don't stall this function waiting for authentication
559
- void authResultP.then((result) => {
560
- this.sendTo(this.userUUID, {
561
- type: "authentication_finished",
562
- session_id: this.sessionUUID,
563
- url,
564
- result,
565
- });
566
- });
567
- }
568
-
569
- // IPlatform.load
570
- load(filename: string): Promise<string> {
571
- if (process.env.DEVELOPMENT === "1") {
572
- return NODE_PLATFORM.load(filename);
573
- }
574
- throw new ChatErrorMessage("Platform.load not implemented");
575
- }
576
-
577
- // IPlatform.renderHTML
578
- renderHTML(html: string): Promise<void> {
579
- return new Promise<void>((r) => {
580
- this.broadcast({
581
- type: "render_html",
582
- html,
583
- session_id: this.sessionUUID,
584
- });
585
- r();
586
- });
587
- }
588
-
589
- // IAgentEventHandler.onCompletion
590
- onCompletion(result: ChatCompletionAssistantMessageParam): void {
591
- logger.debug(`[OpenSession.onCompletion] : ${JSON.stringify(result)}`);
592
- // Nothing to broadcast. Caller will receive this via onAgentMessage.
593
- this.contextManager.processAgentResponse(result);
594
- }
595
-
596
- // IAgentEventHandler.onImage
597
- onImage(image: OpenAI.Chat.Completions.ChatCompletionContentPartImage): void {
598
- logger.debug(`[OpenSession.onImage] : ${image.image_url.url}`);
599
- throw new Error("[OpenSession.onImage] unimplemented");
600
- }
601
-
602
- // IAgentEventHandler.onToolCallResult
603
- onToolCallResult(result: ChatCompletionToolMessageParam): void {
604
- logger.debug(`[onToolCallResult] : ${JSON.stringify(result)}`);
605
- const toolCallMessage = this.contextManager.processToolCallResult(result);
606
- this.broadcast(toolCallMessage);
607
- }
608
-
609
- // IAgentEventHandler.onToolCall
610
- async onToolCall(
611
- toolCall: ChatCompletionMessageToolCall,
612
- agentTool: boolean
613
- ): Promise<boolean> {
614
- if (agentTool) {
615
- // "Agent" tools are considered internal to the agent, and are always
616
- // permitted. Inform all clients and immediately approve.
617
- this.broadcast({
618
- type: "tool_call",
619
- tool_call: toolCall,
620
- session_id: this.sessionUUID,
621
- });
622
- return true;
623
- }
624
-
625
- // TODO: Need a proper mapping to/from MCP calls to tool names
626
-
627
- const [serverName, tool] = toolCall.function.name.split("__");
628
- const autoApproved = prefsGetAutoApprove(
629
- this.agentProfilePreferences,
630
- serverName,
631
- tool
632
- );
633
- if (!autoApproved) {
634
- const { id, resultP } = this.approvalManager.startApproval(
635
- toolCall.function.name
636
- );
637
- this.broadcast({
638
- type: "approve_tool_call",
639
- id,
640
- tool_call: toolCall,
641
- session_id: this.sessionUUID,
642
- });
643
-
644
- try {
645
- logger.debug(`[OpenSession.onToolCall] awaiting approval ${id}`);
646
- const { approved, auto_approve } = await resultP;
647
- logger.debug(
648
- `[OpenSession.onToolCall] approval ${id}: ${String(approved)}`
649
- );
650
- if (auto_approve) {
651
- logger.debug(
652
- "[OpenSession.onToolCall] auto_approve set. updated preferences"
653
- );
654
- const autoApprovalMsg = await this.onSetAutoApproval(
655
- serverName,
656
- tool,
657
- true
658
- );
659
- if (autoApprovalMsg) {
660
- this.broadcast(autoApprovalMsg);
661
- }
662
- }
663
-
664
- return approved;
665
- } catch (e) {
666
- logger.debug(
667
- `[OpenSession.onToolCall] error waiting for approval ${id}: ` +
668
- String(e)
669
- );
670
- return false;
671
- }
672
- } else {
673
- this.broadcast({
674
- type: "tool_call",
675
- tool_call: toolCall,
676
- session_id: this.sessionUUID,
677
- });
678
- return true;
679
- }
680
- }
681
-
682
- // IAgentEventHandler.onAgentMessage
683
- // eslint-disable-next-line @typescript-eslint/require-await
684
- async onAgentMessage(msg: string, end: boolean): Promise<void> {
685
- logger.debug(
686
- `[OpenSession.onAgentMessage] msg: ${msg}, end: ${String(end)}`
687
- );
688
-
689
- // Inform the contextManager and broadcast the ServerAgentMessageChunk
690
- const agentMsgChunk = this.contextManager.processAgentMessage(msg, end);
691
- this.broadcast(agentMsgChunk);
692
- }
693
-
694
- // IAgentEventHandler.onReasoning
695
- onReasoning(reasoning: string): Promise<void> {
696
- return new Promise<void>((r) => {
697
- logger.debug(`[OpenSession.onReasoning]${reasoning}`);
698
- if (reasoning.length > 0) {
699
- this.broadcast({
700
- type: "agent_reasoning_chunk",
701
- reasoning,
702
- session_id: this.sessionUUID,
703
- });
704
- }
705
- r();
706
- });
707
- }
708
-
709
698
  // ISessionFileManagerEventHandler.onFileDeleted
710
699
  onFileDeleted(name: string): void {
711
- this.broadcast({
700
+ this.sender.broadcast({
712
701
  type: "session_file_deleted",
713
702
  session_id: this.sessionUUID,
714
703
  name,
@@ -717,7 +706,7 @@ export class OpenSession
717
706
 
718
707
  // ISessionFileManagerEventHandler.onFileChanged
719
708
  onFileChanged(entry: SessionFileEntry, new_file: boolean): void {
720
- this.broadcast({
709
+ this.sender.broadcast({
721
710
  type: "session_file_changed",
722
711
  session_id: this.sessionUUID,
723
712
  descriptor: {
@@ -745,7 +734,7 @@ export class OpenSession
745
734
  logger.warn(
746
735
  `User ${from} not in session ${this.sessionUUID} - ignoring message`
747
736
  );
748
- this.sendTo(from, {
737
+ this.sender.sendTo(from, {
749
738
  type: "session_error",
750
739
  message: "You are not a participant in this session",
751
740
  session_id: this.sessionUUID,
@@ -756,7 +745,7 @@ export class OpenSession
756
745
 
757
746
  // Enqueue message for processing
758
747
  if (!this.messageQueue.tryEnqueue({ msg: message, from })) {
759
- this.sendTo(
748
+ this.sender.sendTo(
760
749
  from,
761
750
  this.addSessionContext({
762
751
  type: "session_error",
@@ -777,7 +766,7 @@ export class OpenSession
777
766
  const mcpServer = this.skillManager.getMcpServer(server_name);
778
767
  const tools = mcpServer.getTools();
779
768
  const enabled_tools = Array.from(mcpServer.getEnabledTools().keys());
780
- this.connectionManager.sendToConnection(connectionId, {
769
+ this.sender.connectionManager.sendToConnection(connectionId, {
781
770
  type: "mcp_server_added",
782
771
  server_name,
783
772
  tools,
@@ -808,7 +797,7 @@ export class OpenSession
808
797
  undefined;
809
798
  switch (msg.type) {
810
799
  case "msg":
811
- broadcastMsg = this.handleUserMessage(msg, queuedMessage.from);
800
+ broadcastMsg = await this.handleUserMessage(msg, queuedMessage.from);
812
801
  break;
813
802
  case "add_mcp_server":
814
803
  broadcastMsg = await this.handleAddMcpServer(
@@ -842,20 +831,7 @@ export class OpenSession
842
831
  );
843
832
  break;
844
833
  case "tool_call_approval_result":
845
- if (
846
- this.approvalManager.approvalResult(
847
- msg.id,
848
- msg.result,
849
- msg.auto_approve
850
- )
851
- ) {
852
- broadcastMsg = {
853
- type: "tool_call_approval_result",
854
- id: msg.id,
855
- result: msg.result,
856
- session_id: this.sessionUUID,
857
- };
858
- }
834
+ this.approvalManager.onApprovalResult(msg);
859
835
  break;
860
836
  case "session_file_get_content":
861
837
  void this.handleSessionFileGetContent(msg, queuedMessage.from);
@@ -867,7 +843,7 @@ export class OpenSession
867
843
  await this.handleSessionFilePutContent(msg);
868
844
  break;
869
845
  case "set_auto_approval":
870
- broadcastMsg = await this.onSetAutoApproval(
846
+ broadcastMsg = await this.approvalManager.setAutoApprove(
871
847
  msg.server_name,
872
848
  msg.tool,
873
849
  msg.auto_approve
@@ -900,10 +876,10 @@ export class OpenSession
900
876
  if (broadcastMsg) {
901
877
  if (broadcastMsg instanceof Array) {
902
878
  broadcastMsg.map((msg) => {
903
- this.broadcast(msg);
879
+ this.sender.broadcast(msg);
904
880
  });
905
881
  } else {
906
- this.broadcast(broadcastMsg);
882
+ this.sender.broadcast(broadcastMsg);
907
883
  }
908
884
  }
909
885
  } catch (err: unknown) {
@@ -926,7 +902,7 @@ export class OpenSession
926
902
  this.accessToken = accessToken;
927
903
  }
928
904
 
929
- this.sendTo(from, {
905
+ this.sender.sendTo(from, {
930
906
  type: "session_shared",
931
907
  access_token: this.accessToken,
932
908
  client_message_id: msg.client_message_id,
@@ -970,16 +946,12 @@ export class OpenSession
970
946
  return this.contextManager.endAgentResponse();
971
947
  }
972
948
 
973
- /**
974
- * `processUserMessage` logic when agent is active. Start the Agent loop,
975
- * adding all agent messages to the context. Extract the new DB messages.
976
- */
977
949
  private async processUserMessagesActive(
978
950
  msgs: ServerUserMessage[]
979
951
  ): Promise<SessionMessage[]> {
980
952
  const { llmUserMessages, agentFirstChunk } =
981
953
  this.contextManager.startAgentResponse(msgs);
982
- this.broadcast(agentFirstChunk);
954
+ this.sender.broadcast(agentFirstChunk);
983
955
  try {
984
956
  await this.agent.userMessagesRaw(llmUserMessages);
985
957
  } catch (e) {
@@ -990,26 +962,23 @@ export class OpenSession
990
962
  // Errors during agent replies must be turned into messages.
991
963
 
992
964
  const errMsg = `error from LLM: ${String(e)}`;
993
- await this.onAgentMessage(errMsg, true);
994
- const err = this.contextManager.revertAgentResponse(errMsg);
995
- this.broadcast(err);
996
-
997
- // return await this.processuserMessagesActive(msgs);
965
+ this.contextManager.revertAgentResponse(errMsg);
966
+ throw new Error(errMsg);
998
967
  }
999
968
  return this.contextManager.endAgentResponse();
1000
969
  }
1001
970
 
1002
971
  private async processUserMessages(msgs: ServerUserMessage[]): Promise<void> {
1003
- const newSessionMessages = this.agentPaused
1004
- ? this.processUserMessagePaused(msgs)
1005
- : await this.processUserMessagesActive(msgs);
972
+ try {
973
+ const newSessionMessages = this.agentPaused
974
+ ? this.processUserMessagePaused(msgs)
975
+ : await this.processUserMessagesActive(msgs);
1006
976
 
1007
- logger.debug(
1008
- "[processUserMessages] newSessionMessages: " +
1009
- JSON.stringify(newSessionMessages)
1010
- );
977
+ logger.debug(
978
+ "[processUserMessages] newSessionMessages: " +
979
+ JSON.stringify(newSessionMessages)
980
+ );
1011
981
 
1012
- try {
1013
982
  // Append to in-memory conversation and write to the DB
1014
983
  const dbsm = this.db.createTypedClient(DbSessionMessages);
1015
984
  await dbsm.append(this.sessionUUID, newSessionMessages);
@@ -1020,10 +989,10 @@ export class OpenSession
1020
989
  }
1021
990
  }
1022
991
 
1023
- private handleUserMessage(
992
+ private async handleUserMessage(
1024
993
  msg: ClientUserMessage,
1025
994
  from: string
1026
- ): ServerUserMessage | undefined {
995
+ ): Promise<ServerUserMessage | undefined> {
1027
996
  // Return a ServerUserMessage for broadcast. The actual message is places
1028
997
  // on a queue to be dealt with in another loop. This allows Agent
1029
998
  // processing of user messages to depend on other messages.
@@ -1033,7 +1002,15 @@ export class OpenSession
1033
1002
 
1034
1003
  // Assign the user message_idx and attempt to enqueue.
1035
1004
 
1036
- const userMessage = this.contextManager.processUserMessage(msg, from);
1005
+ const user = this.sessionParticipants.get(from);
1006
+ if (!user) {
1007
+ throw new Error(`unrecognized user ${from}`);
1008
+ }
1009
+ const userMessage = this.contextManager.processUserMessage(
1010
+ msg,
1011
+ from,
1012
+ user.nickname
1013
+ );
1037
1014
  if (!userMessage) {
1038
1015
  return;
1039
1016
  }
@@ -1041,7 +1018,7 @@ export class OpenSession
1041
1018
 
1042
1019
  if (userMessage.message_idx === MESSAGE_INDEX_START_VALUE) {
1043
1020
  // No need to wait for this to complete before broadcasting.
1044
- void this.onFirstMessage(userMessage);
1021
+ await this.onFirstMessage(userMessage);
1045
1022
  }
1046
1023
 
1047
1024
  if (!this.userMessageQueue.tryEnqueue(userMessage)) {
@@ -1052,7 +1029,7 @@ export class OpenSession
1052
1029
 
1053
1030
  this.contextManager.unprocessUserMessage(userMessage);
1054
1031
 
1055
- this.sendTo(from, {
1032
+ this.sender.sendTo(from, {
1056
1033
  type: "session_error",
1057
1034
  message: "failed to queue message. try again later.",
1058
1035
  session_id: this.sessionUUID,
@@ -1103,14 +1080,39 @@ export class OpenSession
1103
1080
  }
1104
1081
  assert(this.isPersisted);
1105
1082
 
1106
- // Broadcast the SessionUpdated message
1083
+ // Send session created notification
1084
+ const sessionInfo = this.serverSessionInfo("");
1085
+
1086
+ if (this.teamUUID) {
1087
+ // Team session: notify all members about the new session
1088
+ try {
1089
+ const teamMembers = await this.db.teamGetMembers(this.teamUUID);
1090
+ const teamMemberIds = new Set(teamMembers.map((m) => m.user_uuid));
1107
1091
 
1108
- const msg: ServerSessionUpdate = {
1109
- type: "session_update",
1110
- session_id: this.sessionUUID,
1111
- title: this.sessionTitle,
1112
- };
1113
- this.broadcast(msg);
1092
+ this.sender.connectionManager.sendToUsers(teamMemberIds, sessionInfo);
1093
+
1094
+ logger.info(
1095
+ `[OpenSession] notified ${String(teamMemberIds.size)} team members` +
1096
+ ` about new session ${this.sessionUUID} in team ${this.teamUUID}`
1097
+ );
1098
+ } catch (error) {
1099
+ logger.error(
1100
+ "[OpenSession] Error notifying team members about session" +
1101
+ `${this.sessionUUID}: ${String(error)}`
1102
+ );
1103
+ }
1104
+ } else {
1105
+ // If this is a user session, notify the session owner
1106
+ this.sender.connectionManager.sendToUsers(
1107
+ new Set([this.userUUID]),
1108
+ sessionInfo
1109
+ );
1110
+
1111
+ logger.info(
1112
+ `[OpenSession] notified session owner ${this.userUUID} about ` +
1113
+ `new session ${this.sessionUUID}`
1114
+ );
1115
+ }
1114
1116
  }
1115
1117
 
1116
1118
  private async handleAddMcpServer(
@@ -1324,7 +1326,7 @@ export class OpenSession
1324
1326
  name: msg.name,
1325
1327
  data_url,
1326
1328
  };
1327
- this.sendTo(from, contentMsg);
1329
+ this.sender.sendTo(from, contentMsg);
1328
1330
  }
1329
1331
 
1330
1332
  private async handleSessionFileDelete(
@@ -1356,34 +1358,6 @@ export class OpenSession
1356
1358
  // succeeds. We broadcast in that callback.
1357
1359
  }
1358
1360
 
1359
- private async onSetAutoApproval(
1360
- serverName: string,
1361
- tool: string,
1362
- autoApprove: boolean
1363
- ): Promise<ServerToolAutoApprovalSet | undefined> {
1364
- if (
1365
- prefsSetAutoApprove(
1366
- this.agentProfilePreferences,
1367
- serverName,
1368
- tool,
1369
- autoApprove
1370
- )
1371
- ) {
1372
- await this.db.updateAgentProfilePreferences(
1373
- this.agentProfileUUID,
1374
- this.agentProfilePreferences
1375
- );
1376
-
1377
- return {
1378
- type: "tool_auto_approval_set",
1379
- server_name: serverName,
1380
- tool,
1381
- auto_approve: autoApprove,
1382
- session_id: this.sessionUUID,
1383
- };
1384
- }
1385
- }
1386
-
1387
1361
  private ensureMcpServer(serverName: string): McpServerInfo {
1388
1362
  return this.skillManager.getMcpServer(serverName);
1389
1363
  }
@@ -1411,19 +1385,19 @@ export class OpenSession
1411
1385
  * This only updates the local participant map - actual membership
1412
1386
  * tracking is handled by SessionRegistry.
1413
1387
  */
1414
- addParticipant(userId: string, role: TeamParticipant): void {
1415
- this.sessionParticipants.set(userId, role);
1388
+ addParticipant(userId: string, participant: TeamParticipant): void {
1389
+ this.sessionParticipants.set(userId, participant);
1416
1390
  // Broadcast result to all session participants
1417
1391
  const broadcastMessage: ServerUserAdded = {
1418
1392
  type: "user_added",
1419
1393
  user_uuid: userId,
1420
1394
  role: "participant",
1421
- nickname: role.nickname,
1422
- email: role.email,
1395
+ nickname: participant.nickname,
1396
+ email: participant.email,
1423
1397
  session_id: this.sessionUUID,
1424
1398
  };
1425
1399
 
1426
- this.broadcast(broadcastMessage);
1400
+ this.sender.broadcast(broadcastMessage);
1427
1401
  }
1428
1402
 
1429
1403
  /**
@@ -1440,7 +1414,7 @@ export class OpenSession
1440
1414
  session_id: this.sessionUUID,
1441
1415
  };
1442
1416
 
1443
- this.broadcast(broadcastMessage);
1417
+ this.sender.broadcast(broadcastMessage);
1444
1418
  }
1445
1419
 
1446
1420
  private getSessionParticipants(): TeamParticipant[] {
@@ -1568,3 +1542,72 @@ async function loadSessionData(
1568
1542
  sessionParticipants: createSessionParticipantMap(sessionParticipants),
1569
1543
  };
1570
1544
  }
1545
+
1546
+ async function createContextAndAgent(
1547
+ sessionUUID: string,
1548
+ systemPrompt: string,
1549
+ model: string,
1550
+ sessionMessages: SessionMessage[],
1551
+ workspace: UserMessageData | undefined,
1552
+ sessionCheckpoint: SessionCheckpoint | undefined,
1553
+ ownerData: UserData,
1554
+ ownerApiKey: string,
1555
+ llmUrl: string,
1556
+ xmcpUrl: string,
1557
+ fileManager: ChatSessionFileManager,
1558
+ sender: ISessionMessageSender<ServerToClient>,
1559
+ platform: IPlatform,
1560
+ approvalManager: ToolApprovalManager
1561
+ ): Promise<{
1562
+ agent: Agent;
1563
+ skillManager: SkillManager;
1564
+ contextManager: ChatContextManager;
1565
+ }> {
1566
+ const contextManager = new ChatContextManager(
1567
+ systemPrompt,
1568
+ sessionMessages,
1569
+ sessionUUID,
1570
+ ownerData.uuid,
1571
+ sessionCheckpoint,
1572
+ llmUrl,
1573
+ model,
1574
+ ownerApiKey,
1575
+ undefined as unknown as DBCheckpointWriter, // TODO
1576
+ fileManager
1577
+ );
1578
+ if (workspace) {
1579
+ contextManager.setWorkspace(
1580
+ createUserMessage(workspace.message, workspace.imageB64)
1581
+ );
1582
+ }
1583
+
1584
+ const eventHandler = new ChatSessionAgentEventHandler(
1585
+ sessionUUID,
1586
+ sender,
1587
+ approvalManager,
1588
+ contextManager
1589
+ );
1590
+
1591
+ const xmcpConfig = Configuration.new(ownerApiKey, xmcpUrl, false);
1592
+ const [agent, skillManager] = await createAgentWithoutSkills(
1593
+ llmUrl,
1594
+ model,
1595
+ eventHandler,
1596
+ platform,
1597
+ contextManager,
1598
+ ownerApiKey,
1599
+ xmcpConfig,
1600
+ undefined,
1601
+ true
1602
+ );
1603
+ await addDefaultChatTools(
1604
+ agent,
1605
+ ownerData.timezone,
1606
+ platform,
1607
+ fileManager,
1608
+ llmUrl,
1609
+ ownerApiKey
1610
+ );
1611
+
1612
+ return { agent, skillManager, contextManager };
1613
+ }