@xalia/agent 0.6.1 → 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 (127) hide show
  1. package/dist/agent/src/agent/agent.js +109 -57
  2. package/dist/agent/src/agent/agentUtils.js +24 -26
  3. package/dist/agent/src/agent/compressingContextManager.js +3 -2
  4. package/dist/agent/src/agent/dummyLLM.js +1 -3
  5. package/dist/agent/src/agent/imageGenLLM.js +67 -0
  6. package/dist/agent/src/agent/imageGenerator.js +43 -0
  7. package/dist/agent/src/agent/llm.js +27 -0
  8. package/dist/agent/src/agent/mcpServerManager.js +18 -6
  9. package/dist/agent/src/agent/nullAgentEventHandler.js +6 -0
  10. package/dist/agent/src/agent/openAILLM.js +3 -3
  11. package/dist/agent/src/agent/openAILLMStreaming.js +41 -6
  12. package/dist/agent/src/chat/client/chatClient.js +154 -235
  13. package/dist/agent/src/chat/client/constants.js +1 -2
  14. package/dist/agent/src/chat/client/sessionClient.js +47 -15
  15. package/dist/agent/src/chat/client/sessionFiles.js +102 -0
  16. package/dist/agent/src/chat/data/apiKeyManager.js +38 -7
  17. package/dist/agent/src/chat/data/database.js +83 -70
  18. package/dist/agent/src/chat/data/dbSessionFileModels.js +49 -0
  19. package/dist/agent/src/chat/data/dbSessionFiles.js +76 -0
  20. package/dist/agent/src/chat/data/dbSessionMessages.js +57 -0
  21. package/dist/agent/src/chat/data/mimeTypes.js +44 -0
  22. package/dist/agent/src/chat/protocol/messages.js +21 -1
  23. package/dist/agent/src/chat/server/chatContextManager.js +19 -16
  24. package/dist/agent/src/chat/server/connectionManager.js +14 -36
  25. package/dist/agent/src/chat/server/connectionManager.test.js +3 -16
  26. package/dist/agent/src/chat/server/conversation.js +73 -44
  27. package/dist/agent/src/chat/server/imageGeneratorTools.js +111 -0
  28. package/dist/agent/src/chat/server/openSession.js +398 -233
  29. package/dist/agent/src/chat/server/openSessionMessageSender.js +2 -0
  30. package/dist/agent/src/chat/server/server.js +5 -8
  31. package/dist/agent/src/chat/server/sessionFileManager.js +171 -38
  32. package/dist/agent/src/chat/server/sessionRegistry.js +214 -42
  33. package/dist/agent/src/chat/server/test-utils/mockFactories.js +12 -11
  34. package/dist/agent/src/chat/server/tools.js +27 -6
  35. package/dist/agent/src/chat/utils/approvalManager.js +82 -64
  36. package/dist/agent/src/chat/utils/multiAsyncQueue.js +9 -1
  37. package/dist/agent/src/chat/{client/responseHandler.js → utils/responseAwaiter.js} +41 -18
  38. package/dist/agent/src/test/agent.test.js +104 -63
  39. package/dist/agent/src/test/approvalManager.test.js +79 -35
  40. package/dist/agent/src/test/chatContextManager.test.js +16 -17
  41. package/dist/agent/src/test/clientServerConnection.test.js +2 -2
  42. package/dist/agent/src/test/db.test.js +33 -70
  43. package/dist/agent/src/test/dbSessionFiles.test.js +179 -0
  44. package/dist/agent/src/test/dbSessionMessages.test.js +67 -0
  45. package/dist/agent/src/test/dbTestTools.js +6 -5
  46. package/dist/agent/src/test/imageLoad.test.js +1 -1
  47. package/dist/agent/src/test/mcpServerManager.test.js +1 -1
  48. package/dist/agent/src/test/multiAsyncQueue.test.js +50 -0
  49. package/dist/agent/src/test/responseAwaiter.test.js +74 -0
  50. package/dist/agent/src/test/testTools.js +12 -0
  51. package/dist/agent/src/tool/agentChat.js +25 -6
  52. package/dist/agent/src/tool/agentMain.js +1 -1
  53. package/dist/agent/src/tool/chatMain.js +115 -6
  54. package/dist/agent/src/tool/commandPrompt.js +7 -3
  55. package/dist/agent/src/tool/files.js +23 -15
  56. package/dist/agent/src/tool/options.js +2 -2
  57. package/package.json +1 -1
  58. package/scripts/setup_chat +2 -2
  59. package/scripts/test_chat +95 -36
  60. package/src/agent/agent.ts +152 -41
  61. package/src/agent/agentUtils.ts +34 -41
  62. package/src/agent/compressingContextManager.ts +5 -4
  63. package/src/agent/context.ts +1 -1
  64. package/src/agent/dummyLLM.ts +1 -3
  65. package/src/agent/iAgentEventHandler.ts +15 -2
  66. package/src/agent/imageGenLLM.ts +99 -0
  67. package/src/agent/imageGenerator.ts +60 -0
  68. package/src/agent/llm.ts +128 -4
  69. package/src/agent/mcpServerManager.ts +26 -7
  70. package/src/agent/nullAgentEventHandler.ts +6 -0
  71. package/src/agent/openAILLM.ts +3 -8
  72. package/src/agent/openAILLMStreaming.ts +60 -14
  73. package/src/chat/client/chatClient.ts +262 -286
  74. package/src/chat/client/constants.ts +0 -2
  75. package/src/chat/client/sessionClient.ts +82 -20
  76. package/src/chat/client/sessionFiles.ts +142 -0
  77. package/src/chat/data/apiKeyManager.ts +55 -7
  78. package/src/chat/data/dataModels.ts +17 -7
  79. package/src/chat/data/database.ts +107 -92
  80. package/src/chat/data/dbSessionFileModels.ts +91 -0
  81. package/src/chat/data/dbSessionFiles.ts +99 -0
  82. package/src/chat/data/dbSessionMessages.ts +68 -0
  83. package/src/chat/data/mimeTypes.ts +58 -0
  84. package/src/chat/protocol/messages.ts +136 -25
  85. package/src/chat/server/chatContextManager.ts +42 -24
  86. package/src/chat/server/connectionManager.test.ts +2 -22
  87. package/src/chat/server/connectionManager.ts +18 -53
  88. package/src/chat/server/conversation.ts +106 -59
  89. package/src/chat/server/imageGeneratorTools.ts +138 -0
  90. package/src/chat/server/openSession.ts +606 -325
  91. package/src/chat/server/openSessionMessageSender.ts +4 -0
  92. package/src/chat/server/server.ts +5 -11
  93. package/src/chat/server/sessionFileManager.ts +223 -63
  94. package/src/chat/server/sessionRegistry.ts +317 -52
  95. package/src/chat/server/test-utils/mockFactories.ts +13 -13
  96. package/src/chat/server/tools.ts +43 -8
  97. package/src/chat/utils/agentSessionMap.ts +2 -2
  98. package/src/chat/utils/approvalManager.ts +153 -81
  99. package/src/chat/utils/multiAsyncQueue.ts +11 -1
  100. package/src/chat/{client/responseHandler.ts → utils/responseAwaiter.ts} +73 -23
  101. package/src/test/agent.test.ts +152 -75
  102. package/src/test/approvalManager.test.ts +108 -40
  103. package/src/test/chatContextManager.test.ts +26 -22
  104. package/src/test/clientServerConnection.test.ts +3 -3
  105. package/src/test/compressingContextManager.test.ts +1 -1
  106. package/src/test/context.test.ts +2 -1
  107. package/src/test/conversation.test.ts +1 -1
  108. package/src/test/db.test.ts +41 -83
  109. package/src/test/dbSessionFiles.test.ts +258 -0
  110. package/src/test/dbSessionMessages.test.ts +85 -0
  111. package/src/test/dbTestTools.ts +9 -5
  112. package/src/test/imageLoad.test.ts +2 -2
  113. package/src/test/mcpServerManager.test.ts +3 -1
  114. package/src/test/multiAsyncQueue.test.ts +58 -0
  115. package/src/test/responseAwaiter.test.ts +103 -0
  116. package/src/test/testTools.ts +15 -1
  117. package/src/tool/agentChat.ts +36 -8
  118. package/src/tool/agentMain.ts +7 -7
  119. package/src/tool/chatMain.ts +128 -7
  120. package/src/tool/commandPrompt.ts +10 -5
  121. package/src/tool/files.ts +30 -13
  122. package/src/tool/options.ts +1 -1
  123. package/test_data/dummyllm_script_image_gen.json +19 -0
  124. package/test_data/dummyllm_script_invoke_image_gen_tool.json +30 -0
  125. package/test_data/image_gen_test_profile.json +5 -0
  126. package/dist/agent/src/test/responseHandler.test.js +0 -61
  127. package/src/test/responseHandler.test.ts +0 -78
@@ -1,3 +1,4 @@
1
+ import OpenAI from "openai";
1
2
  import { strict as assert } from "assert";
2
3
  import { Tool } from "@modelcontextprotocol/sdk/types.js";
3
4
 
@@ -6,25 +7,19 @@ import {
6
7
  Configuration,
7
8
  getLogger,
8
9
  McpServerSettings,
9
- prefsGetAutoApprove,
10
- prefsSetAutoApprove,
11
10
  SavedAgentProfile,
12
11
  } from "@xalia/xmcp/sdk";
13
12
 
13
+ import { Agent, createUserMessage } from "../../agent/agent";
14
14
  import {
15
- Agent,
16
15
  ChatCompletionAssistantMessageParam,
17
16
  ChatCompletionMessageToolCall,
18
17
  ChatCompletionToolMessageParam,
19
- createUserMessage,
20
- } from "../../agent/agent";
18
+ } from "../../agent/llm";
21
19
  import { SkillManager } from "../../agent/sudoMcpServerManager";
22
20
  import { McpServerInfo } from "../../agent/mcpServerManager";
23
21
  import { IAgentEventHandler } from "../../agent/iAgentEventHandler";
24
- import {
25
- createAgentWithoutSkills,
26
- DEFAULT_LLM_MODEL,
27
- } from "../../agent/agentUtils";
22
+ import { createAgentWithoutSkills } from "../../agent/agentUtils";
28
23
  import { IPlatform } from "../../agent/iplatform";
29
24
 
30
25
  import type {
@@ -37,15 +32,18 @@ import type {
37
32
  ServerModelUpdated,
38
33
  ServerUserMessage,
39
34
  ClientUserMessage,
40
- ServerToolAutoApprovalSet,
41
35
  ClientSessionMessage,
42
36
  ServerSessionError,
43
37
  ServerUserAdded,
44
38
  ServerUserRemoved,
45
39
  ServerSessionInfo,
46
- ServerSessionUpdate,
47
40
  ClientSetWorkspace,
41
+ ClientToServer,
42
+ ServerSessionFileContent,
48
43
  ClientSetMcpServerConfig,
44
+ ClientShareSession,
45
+ ClientSessionMessageBase,
46
+ ServerAgentPaused,
49
47
  } from "../protocol/messages";
50
48
  import { AsyncQueue } from "../utils/asyncQueue";
51
49
  import { MultiAsyncQueue } from "../utils/multiAsyncQueue";
@@ -57,13 +55,17 @@ import {
57
55
  TeamParticipant,
58
56
  SessionCreateData,
59
57
  UserMessageData,
58
+ SessionDescriptor,
60
59
  } from "../data/dataModels";
61
60
  import {
62
61
  Database,
63
62
  createSessionParticipantMap,
64
63
  UserData,
65
64
  } from "../data/database";
66
- import { ApprovalManager } from "../utils/approvalManager";
65
+ import {
66
+ DbAgentPreferencesWriter,
67
+ ToolApprovalManager,
68
+ } from "../utils/approvalManager";
67
69
  import { ChatErrorMessage, ChatFatalError } from "../protocol/errors";
68
70
  import { addDefaultChatTools } from "./tools";
69
71
  import { ChatContextManager, ICheckpointWriter } from "./chatContextManager";
@@ -71,17 +73,33 @@ import {
71
73
  llmUserMessageToUserMessageData,
72
74
  MESSAGE_INDEX_START_VALUE,
73
75
  } from "./conversation";
74
- import { ChatSessionFileManager } from "./sessionFileManager";
76
+ import {
77
+ ChatSessionFileManager,
78
+ ISessionFileManager,
79
+ ISessionFileManagerEventHandler,
80
+ } from "./sessionFileManager";
75
81
  import { IUserConnectionManager } from "./connectionManager";
76
82
  import { getErrorString } from "./errorUtils";
77
83
  import { NODE_PLATFORM } from "../../tool/nodePlatform";
78
84
  import { DbMcpServerConfigs } from "../data/dbMcpServerConfigs";
85
+ import { SessionFileEntry } from "../data/dbSessionFileModels";
86
+ import { ApiKeyManager } from "../data/apiKeyManager";
87
+ import { DbSessionMessages } from "../data/dbSessionMessages";
88
+ import { ISessionMessageSender } from "./openSessionMessageSender";
89
+
90
+ /**
91
+ * The model to use when the AgentProfile does not specify one.
92
+ */
93
+ const DEFAULT_CHAT_LLM_MODEL =
94
+ process.env["DEFAULT_LLM_MODEL"] || "anthropic/claude-sonnet-4.5";
79
95
 
80
96
  /**
81
97
  * Num messages to load at conversation startup.
82
98
  */
83
99
  export const DEFAULT_NUM_MESSGAES = 500;
84
100
 
101
+ export const GUEST_TOKEN_PREFIX = "guest";
102
+
85
103
  const logger = getLogger();
86
104
 
87
105
  type QueuedClientMessage<
@@ -109,6 +127,170 @@ class DBCheckpointWriter implements ICheckpointWriter {
109
127
  }
110
128
  }
111
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
+
112
294
  /**
113
295
  * Describes a Session (conversation) with connected participants.
114
296
  *
@@ -119,7 +301,7 @@ class DBCheckpointWriter implements ICheckpointWriter {
119
301
  * be seen until user messages had been fully processed, which could block
120
302
  * tool approvals and other interactions).
121
303
  */
122
- export class OpenSession implements IAgentEventHandler, IPlatform {
304
+ export class OpenSession implements ISessionFileManagerEventHandler {
123
305
  private readonly db: Database;
124
306
  private /* readonly */ agent: Agent;
125
307
  private readonly sessionUUID: string;
@@ -129,15 +311,18 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
129
311
  private readonly sessionParticipants: SessionParticipantMap;
130
312
  private readonly agentProfilePreferences: AgentPreferences;
131
313
  private /* readonly */ skillManager: SkillManager;
132
- private readonly connectionManager: IUserConnectionManager<ServerToClient>;
314
+ private readonly sender: ChatSessionMessageSender;
133
315
  private readonly messageQueue: AsyncQueue<QueuedClientMessage>;
134
316
  private readonly userMessageQueue: MultiAsyncQueue<ServerUserMessage>;
135
317
  private readonly contextManager: ChatContextManager;
136
- private readonly approvalManager: ApprovalManager;
318
+ private readonly approvalManager: ToolApprovalManager;
137
319
  private readonly savedAgentProfile: SavedAgentProfile;
320
+ private readonly sessionFileManager: ISessionFileManager;
138
321
  private isPersisted: boolean;
322
+ private accessToken: string | undefined;
139
323
  private sessionTitle: string;
140
324
  private sessionUpdatedAt: string;
325
+ private agentPaused: boolean;
141
326
 
142
327
  private constructor(
143
328
  db: Database,
@@ -149,8 +334,9 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
149
334
  agentProfilePreferences: AgentPreferences,
150
335
  skillManager: SkillManager,
151
336
  contextManager: ChatContextManager,
152
- connectionManager: IUserConnectionManager<ServerToClient>,
153
- approvalManager: ApprovalManager = new ApprovalManager()
337
+ sender: ChatSessionMessageSender,
338
+ approvalManager: ToolApprovalManager,
339
+ fileManager: ISessionFileManager
154
340
  ) {
155
341
  this.db = db;
156
342
  this.agent = agent;
@@ -158,10 +344,11 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
158
344
  this.teamUUID = sessionData.team_uuid;
159
345
  this.userUUID = sessionData.user_uuid;
160
346
  this.agentProfileUUID = sessionData.agent_profile_uuid;
347
+ this.accessToken = sessionData.access_token;
161
348
  this.sessionParticipants = sessionParticipants;
162
349
  this.agentProfilePreferences = agentProfilePreferences;
163
350
  this.skillManager = skillManager;
164
- this.connectionManager = connectionManager;
351
+ this.sender = sender;
165
352
  this.messageQueue = new AsyncQueue<QueuedClientMessage>((m) =>
166
353
  this.processMessage(m)
167
354
  );
@@ -172,13 +359,18 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
172
359
  this.contextManager = contextManager;
173
360
  this.approvalManager = approvalManager;
174
361
  this.savedAgentProfile = savedAgentProfile;
362
+ this.sessionFileManager = fileManager;
175
363
  this.isPersisted = isPersisted;
176
364
  this.sessionTitle = sessionData.title;
177
365
  this.sessionUpdatedAt = sessionData.updated_at;
366
+ this.agentPaused = sessionData.agent_paused;
367
+
368
+ fileManager.addEventHandler(this);
178
369
  }
179
370
 
180
371
  private static async init(
181
372
  db: Database,
373
+ isPersisted: boolean,
182
374
  sessionData: SessionData,
183
375
  savedAgentProfile: SavedAgentProfile,
184
376
  sessionMessages: SessionMessage[],
@@ -191,61 +383,54 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
191
383
  connectionManager: IUserConnectionManager<ServerToClient>
192
384
  ): Promise<OpenSession> {
193
385
  const sessionId = sessionData.session_uuid;
194
- const fileManager = new ChatSessionFileManager(sessionId, db);
195
- const contextManager = new ChatContextManager(
386
+
387
+ const fileManager = await ChatSessionFileManager.init(db, sessionId);
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,
196
402
  savedAgentProfile.profile.system_prompt,
403
+ savedAgentProfile.profile.model || DEFAULT_CHAT_LLM_MODEL,
197
404
  sessionMessages,
198
- sessionId,
199
- ownerData.uuid,
405
+ sessionData.workspace,
200
406
  sessionCheckpoint,
201
- llmUrl,
202
- savedAgentProfile.profile.model || DEFAULT_LLM_MODEL,
407
+ ownerData,
203
408
  ownerApiKey,
204
- new DBCheckpointWriter(db, sessionId),
205
- fileManager
409
+ llmUrl,
410
+ xmcpUrl,
411
+ fileManager,
412
+ sender,
413
+ platform,
414
+ toolApprovalManager
206
415
  );
207
- if (sessionData.workspace) {
208
- const ws = sessionData.workspace;
209
- contextManager.setWorkspace(createUserMessage(ws.message, ws.imageB64));
210
- }
211
416
 
212
417
  const openSession = new OpenSession(
213
418
  db,
214
- {} as Agent, // Placeholder - will be replaced after agent creation
419
+ agent,
215
420
  sessionData,
216
421
  savedAgentProfile,
217
- false /* isPersisted */,
422
+ isPersisted,
218
423
  sessionParticipants,
219
424
  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,
425
+ skillManager,
234
426
  contextManager,
235
- ownerApiKey,
236
- xmcpConfig,
237
- undefined,
238
- true
427
+ sender,
428
+ toolApprovalManager,
429
+ fileManager
239
430
  );
240
- await addDefaultChatTools(agent, ownerData.timezone, openSession);
241
431
 
242
- // Update OpenSession with real agent and skillManager
243
- openSession.agent = agent;
244
- openSession.skillManager = skillManager;
432
+ // Note, MCP servers have not been enabled yet
245
433
 
246
- await openSession.restoreMcpSettings(
247
- savedAgentProfile.profile.mcp_settings
248
- );
249
434
  return openSession;
250
435
  }
251
436
 
@@ -270,15 +455,22 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
270
455
  }
271
456
 
272
457
  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
- });
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
+ }
279
470
 
280
471
  return OpenSession.init(
281
472
  db,
473
+ false /* isPersisted */,
282
474
  sessionData,
283
475
  savedAgentProfile,
284
476
  sessionMessages,
@@ -311,6 +503,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
311
503
  } = await loadSessionData(db, sessionId);
312
504
  return OpenSession.init(
313
505
  db,
506
+ true /* isPersisted */,
314
507
  sessionData,
315
508
  savedAgentProfile,
316
509
  sessionMessages,
@@ -328,22 +521,66 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
328
521
  logger.info(`[OpenSession.onEmpty] session ${this.sessionUUID}`);
329
522
  }
330
523
 
524
+ getDescriptor(): SessionDescriptor {
525
+ assert(
526
+ typeof this.accessToken === "string" ||
527
+ typeof this.accessToken === "undefined"
528
+ );
529
+ return {
530
+ session_uuid: this.sessionUUID,
531
+ title: this.sessionTitle,
532
+ agent_profile_uuid: this.agentProfileUUID,
533
+ user_uuid: this.userUUID,
534
+ team_uuid: this.teamUUID,
535
+ access_token: this.accessToken,
536
+ updated_at: this.sessionUpdatedAt,
537
+ agent_paused: this.agentPaused,
538
+ };
539
+ }
540
+
331
541
  getParticipants(): SessionParticipantMap {
332
542
  return this.sessionParticipants;
333
543
  }
334
544
 
335
- sendSessionData(connectionId: string, clientMessageId: string): void {
545
+ async sendSessionData(
546
+ connectionId: string,
547
+ clientMessageId: string,
548
+ restoreMcpState: boolean
549
+ ): Promise<void> {
336
550
  logger.info(
337
551
  `[SessionRegistry] sending session data for session ${this.sessionUUID}`
338
552
  );
339
553
 
340
554
  const sessionInfo = this.serverSessionInfo(clientMessageId);
341
- 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
+ }
568
+
569
+ // send session file info
570
+ const fileDescriptors = this.sessionFileManager.listFiles();
571
+ fileDescriptors.forEach((descriptor) => {
572
+ connMgr.sendToConnection(connectionId, {
573
+ type: "session_file_changed",
574
+ session_id: this.sessionUUID,
575
+ descriptor,
576
+ new_file: false,
577
+ });
578
+ });
342
579
 
343
580
  // send conversation history
344
581
  const conversationMessages = this.contextManager.getConversationMessages();
345
582
  conversationMessages.forEach((message) => {
346
- this.connectionManager.sendToConnection(connectionId, message);
583
+ connMgr.sendToConnection(connectionId, message);
347
584
  });
348
585
 
349
586
  // add MCP settings
@@ -351,12 +588,12 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
351
588
 
352
589
  // add system prompt and model
353
590
  const agentProfile = this.agent.getAgentProfile();
354
- this.connectionManager.sendToConnection(connectionId, {
591
+ connMgr.sendToConnection(connectionId, {
355
592
  type: "system_prompt_updated",
356
593
  system_prompt: agentProfile.system_prompt,
357
594
  session_id: this.sessionUUID,
358
595
  });
359
- this.connectionManager.sendToConnection(connectionId, {
596
+ connMgr.sendToConnection(connectionId, {
360
597
  type: "model_updated",
361
598
  model: agentProfile.model || "",
362
599
  session_id: this.sessionUUID,
@@ -407,7 +644,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
407
644
  skillManager.enableTool(server_name, enabled_tool);
408
645
  }
409
646
  } catch (e) {
410
- this.broadcast({
647
+ this.sender.broadcast({
411
648
  type: "session_error",
412
649
  message: `Error adding MCP server ${server_name}: ${String(e)}`,
413
650
  session_id: this.sessionUUID,
@@ -426,9 +663,9 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
426
663
  private handleError(err: unknown, from?: string): boolean {
427
664
  const sendError = (msg: ServerSessionError) => {
428
665
  if (from) {
429
- this.sendTo(from, msg);
666
+ this.sender.sendTo(from, msg);
430
667
  } else {
431
- this.broadcast(msg);
668
+ this.sender.broadcast(msg);
432
669
  }
433
670
  };
434
671
 
@@ -458,162 +695,29 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
458
695
  return true;
459
696
  }
460
697
 
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",
698
+ // ISessionFileManagerEventHandler.onFileDeleted
699
+ onFileDeleted(name: string): void {
700
+ this.sender.broadcast({
701
+ type: "session_file_deleted",
481
702
  session_id: this.sessionUUID,
482
- url,
483
- display_name,
703
+ name,
484
704
  });
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
705
  }
505
706
 
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();
707
+ // ISessionFileManagerEventHandler.onFileChanged
708
+ onFileChanged(entry: SessionFileEntry, new_file: boolean): void {
709
+ this.sender.broadcast({
710
+ type: "session_file_changed",
711
+ session_id: this.sessionUUID,
712
+ descriptor: {
713
+ name: entry.name,
714
+ mime_type: entry.mime_type,
715
+ summary: entry.summary,
716
+ },
717
+ new_file,
515
718
  });
516
719
  }
517
720
 
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
721
  /**
618
722
  * Handle incoming session-related message from client.
619
723
  * These messages include tool calls, MCP changes, etc.
@@ -630,7 +734,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
630
734
  logger.warn(
631
735
  `User ${from} not in session ${this.sessionUUID} - ignoring message`
632
736
  );
633
- this.sendTo(from, {
737
+ this.sender.sendTo(from, {
634
738
  type: "session_error",
635
739
  message: "You are not a participant in this session",
636
740
  session_id: this.sessionUUID,
@@ -641,7 +745,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
641
745
 
642
746
  // Enqueue message for processing
643
747
  if (!this.messageQueue.tryEnqueue({ msg: message, from })) {
644
- this.sendTo(
748
+ this.sender.sendTo(
645
749
  from,
646
750
  this.addSessionContext({
647
751
  type: "session_error",
@@ -662,7 +766,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
662
766
  const mcpServer = this.skillManager.getMcpServer(server_name);
663
767
  const tools = mcpServer.getTools();
664
768
  const enabled_tools = Array.from(mcpServer.getEnabledTools().keys());
665
- this.connectionManager.sendToConnection(connectionId, {
769
+ this.sender.connectionManager.sendToConnection(connectionId, {
666
770
  type: "mcp_server_added",
667
771
  server_name,
668
772
  tools,
@@ -693,70 +797,76 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
693
797
  undefined;
694
798
  switch (msg.type) {
695
799
  case "msg":
696
- broadcastMsg = this.onUserMessage(msg, queuedMessage.from);
800
+ broadcastMsg = await this.handleUserMessage(msg, queuedMessage.from);
697
801
  break;
698
802
  case "add_mcp_server":
699
- broadcastMsg = await this.onAddMcpServer(
803
+ broadcastMsg = await this.handleAddMcpServer(
700
804
  msg.server_name,
701
805
  msg.enable_all
702
806
  );
703
807
  break;
704
808
  case "remove_mcp_server":
705
- broadcastMsg = await this.onRemoveMcpServer(msg.server_name);
809
+ broadcastMsg = await this.handleRemoveMcpServer(msg.server_name);
706
810
  break;
707
811
  case "enable_mcp_server_tool":
708
- broadcastMsg = await this.onEnableMcpServerTool(
812
+ broadcastMsg = await this.handleEnableMcpServerTool(
709
813
  msg.server_name,
710
814
  msg.tool
711
815
  );
712
816
  break;
713
817
  case "disable_mcp_server_tool":
714
- broadcastMsg = await this.onDisableMcpServerTool(
818
+ broadcastMsg = await this.handleDisableMcpServerTool(
715
819
  msg.server_name,
716
820
  msg.tool
717
821
  );
718
822
  break;
719
823
  case "enable_all_mcp_server_tools":
720
- broadcastMsg = await this.onEnableAllMcpServerTools(msg.server_name);
824
+ broadcastMsg = await this.handleEnableAllMcpServerTools(
825
+ msg.server_name
826
+ );
721
827
  break;
722
828
  case "disable_all_mcp_server_tools":
723
- broadcastMsg = await this.onDisableAllMcpServerTools(msg.server_name);
829
+ broadcastMsg = await this.handleDisableAllMcpServerTools(
830
+ msg.server_name
831
+ );
724
832
  break;
725
833
  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
- }
834
+ this.approvalManager.onApprovalResult(msg);
835
+ break;
836
+ case "session_file_get_content":
837
+ void this.handleSessionFileGetContent(msg, queuedMessage.from);
838
+ break;
839
+ case "session_file_delete":
840
+ await this.handleSessionFileDelete(msg);
841
+ break;
842
+ case "session_file_put_content":
843
+ await this.handleSessionFilePutContent(msg);
740
844
  break;
741
845
  case "set_auto_approval":
742
- broadcastMsg = await this.onSetAutoApproval(
846
+ broadcastMsg = await this.approvalManager.setAutoApprove(
743
847
  msg.server_name,
744
848
  msg.tool,
745
849
  msg.auto_approve
746
850
  );
747
851
  break;
748
852
  case "set_system_prompt":
749
- broadcastMsg = await this.onSetSystemPrompt(msg.system_prompt);
853
+ broadcastMsg = await this.handleSetSystemPrompt(msg.system_prompt);
750
854
  break;
751
855
  case "set_model":
752
- broadcastMsg = await this.onSetModel(msg.model);
856
+ broadcastMsg = await this.handleSetModel(msg.model);
857
+ break;
858
+ case "set_agent_paused":
859
+ broadcastMsg = await this.handleSetAgentPaused(msg.paused);
753
860
  break;
754
861
  case "set_workspace":
755
- await this.onSetWorkspace(msg, queuedMessage.from);
862
+ await this.handleSetWorkspace(msg, queuedMessage.from);
756
863
  break;
757
864
  case "set_mcp_server_config":
758
865
  await this.handleSetMcpServerConfig(msg, queuedMessage.from);
759
866
  break;
867
+ case "share_session":
868
+ await this.handleShareSession(msg, queuedMessage.from);
869
+ break;
760
870
  default: {
761
871
  const exhaustive: never = msg; // Error => non-exhaustive switch-case.
762
872
  return exhaustive;
@@ -766,10 +876,10 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
766
876
  if (broadcastMsg) {
767
877
  if (broadcastMsg instanceof Array) {
768
878
  broadcastMsg.map((msg) => {
769
- this.broadcast(msg);
879
+ this.sender.broadcast(msg);
770
880
  });
771
881
  } else {
772
- this.broadcast(broadcastMsg);
882
+ this.sender.broadcast(broadcastMsg);
773
883
  }
774
884
  }
775
885
  } catch (err: unknown) {
@@ -779,7 +889,28 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
779
889
  }
780
890
  }
781
891
 
782
- private async onSetWorkspace(
892
+ private async handleShareSession(
893
+ msg: ClientShareSession & ClientSessionMessageBase,
894
+ from: string
895
+ ) {
896
+ if (!this.accessToken) {
897
+ const accessToken = ApiKeyManager.createApiKeyWithPayload(
898
+ GUEST_TOKEN_PREFIX,
899
+ this.sessionUUID
900
+ );
901
+ await this.db.sessionUpdateAccessToken(this.sessionUUID, accessToken);
902
+ this.accessToken = accessToken;
903
+ }
904
+
905
+ this.sender.sendTo(from, {
906
+ type: "session_shared",
907
+ access_token: this.accessToken,
908
+ client_message_id: msg.client_message_id,
909
+ session_id: this.sessionUUID,
910
+ });
911
+ }
912
+
913
+ private async handleSetWorkspace(
783
914
  msg: ClientSetWorkspace,
784
915
  sender: string
785
916
  ): Promise<void> {
@@ -799,18 +930,28 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
799
930
  assert(this.isPersisted);
800
931
  }
801
932
 
802
- private async processUserMessages(msgs: ServerUserMessage[]): Promise<void> {
803
- if (msgs.length === 0) {
804
- logger.debug("ignoring empty messages");
805
- return;
806
- }
933
+ /**
934
+ * `processUserMessage` logic when agent is paused. Trigger the context,
935
+ * add the user messages and then extract the new DB messages.
936
+ */
937
+ private processUserMessagePaused(
938
+ msgs: ServerUserMessage[]
939
+ ): SessionMessage[] {
940
+ const { llmUserMessages } = this.contextManager.startAgentResponse(msgs);
941
+
942
+ // Just send the user LLM messages direct to the ContextManager, so they
943
+ // are available to the LLM once it is restarted.
807
944
 
808
- // Begin an agent response using the accumulated user messages.
945
+ this.contextManager.addMessages(llmUserMessages);
946
+ return this.contextManager.endAgentResponse();
947
+ }
809
948
 
949
+ private async processUserMessagesActive(
950
+ msgs: ServerUserMessage[]
951
+ ): Promise<SessionMessage[]> {
810
952
  const { llmUserMessages, agentFirstChunk } =
811
953
  this.contextManager.startAgentResponse(msgs);
812
- this.broadcast(agentFirstChunk);
813
-
954
+ this.sender.broadcast(agentFirstChunk);
814
955
  try {
815
956
  await this.agent.userMessagesRaw(llmUserMessages);
816
957
  } catch (e) {
@@ -821,22 +962,26 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
821
962
  // Errors during agent replies must be turned into messages.
822
963
 
823
964
  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;
965
+ this.contextManager.revertAgentResponse(errMsg);
966
+ throw new Error(errMsg);
828
967
  }
968
+ return this.contextManager.endAgentResponse();
969
+ }
829
970
 
830
- const newSessionMessages = this.contextManager.endAgentResponse();
971
+ private async processUserMessages(msgs: ServerUserMessage[]): Promise<void> {
972
+ try {
973
+ const newSessionMessages = this.agentPaused
974
+ ? this.processUserMessagePaused(msgs)
975
+ : await this.processUserMessagesActive(msgs);
831
976
 
832
- logger.debug(
833
- "[processUserMessages] newSessionMessages: " +
834
- JSON.stringify(newSessionMessages)
835
- );
977
+ logger.debug(
978
+ "[processUserMessages] newSessionMessages: " +
979
+ JSON.stringify(newSessionMessages)
980
+ );
836
981
 
837
- try {
838
982
  // Append to in-memory conversation and write to the DB
839
- await this.db.sessionMessagesAppend(this.sessionUUID, newSessionMessages);
983
+ const dbsm = this.db.createTypedClient(DbSessionMessages);
984
+ await dbsm.append(this.sessionUUID, newSessionMessages);
840
985
  } catch (e) {
841
986
  if (!this.handleError(e)) {
842
987
  throw e;
@@ -844,10 +989,10 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
844
989
  }
845
990
  }
846
991
 
847
- private onUserMessage(
992
+ private async handleUserMessage(
848
993
  msg: ClientUserMessage,
849
994
  from: string
850
- ): ServerUserMessage | undefined {
995
+ ): Promise<ServerUserMessage | undefined> {
851
996
  // Return a ServerUserMessage for broadcast. The actual message is places
852
997
  // on a queue to be dealt with in another loop. This allows Agent
853
998
  // processing of user messages to depend on other messages.
@@ -857,13 +1002,23 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
857
1002
 
858
1003
  // Assign the user message_idx and attempt to enqueue.
859
1004
 
860
- const userMessage = this.contextManager.processUserMessage(msg, from);
861
-
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
+ );
1014
+ if (!userMessage) {
1015
+ return;
1016
+ }
862
1017
  // Special case for the first message of the session
863
1018
 
864
1019
  if (userMessage.message_idx === MESSAGE_INDEX_START_VALUE) {
865
1020
  // No need to wait for this to complete before broadcasting.
866
- void this.onFirstMessage(userMessage);
1021
+ await this.onFirstMessage(userMessage);
867
1022
  }
868
1023
 
869
1024
  if (!this.userMessageQueue.tryEnqueue(userMessage)) {
@@ -874,7 +1029,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
874
1029
 
875
1030
  this.contextManager.unprocessUserMessage(userMessage);
876
1031
 
877
- this.sendTo(from, {
1032
+ this.sender.sendTo(from, {
878
1033
  type: "session_error",
879
1034
  message: "failed to queue message. try again later.",
880
1035
  session_id: this.sessionUUID,
@@ -893,14 +1048,20 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
893
1048
  title: this.sessionTitle,
894
1049
  team_uuid: this.teamUUID,
895
1050
  agent_profile_uuid: this.agentProfileUUID,
896
- workspace: this.workspaceUserMessageData(),
897
1051
  user_uuid: this.userUUID,
1052
+ agent_paused: false,
898
1053
  };
899
1054
  logger.info(
900
1055
  `[OpenSession.onFirstMessage] writing session ${this.sessionUUID}`
901
1056
  );
902
1057
 
903
1058
  await this.db.sessionCreate(sessionCreateData);
1059
+
1060
+ const workspace = this.workspaceUserMessageData();
1061
+ if (workspace) {
1062
+ await this.db.sessionUpdateWorkspace(this.sessionUUID, workspace);
1063
+ }
1064
+
904
1065
  this.isPersisted = true;
905
1066
  }
906
1067
 
@@ -919,17 +1080,42 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
919
1080
  }
920
1081
  assert(this.isPersisted);
921
1082
 
922
- // 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));
923
1091
 
924
- const msg: ServerSessionUpdate = {
925
- type: "session_update",
926
- session_id: this.sessionUUID,
927
- title: this.sessionTitle,
928
- };
929
- 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
+ }
930
1116
  }
931
1117
 
932
- private async onAddMcpServer(
1118
+ private async handleAddMcpServer(
933
1119
  serverName: string,
934
1120
  enableAll: boolean
935
1121
  ): Promise<ServerMcpServerAdded> {
@@ -967,7 +1153,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
967
1153
  };
968
1154
  }
969
1155
 
970
- private async onRemoveMcpServer(
1156
+ private async handleRemoveMcpServer(
971
1157
  server_name: string
972
1158
  ): Promise<ServerMcpServerRemoved> {
973
1159
  logger.info(`[onRemoveMcpServer]: Removing server ${server_name}`);
@@ -986,7 +1172,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
986
1172
  };
987
1173
  }
988
1174
 
989
- private async onEnableMcpServerTool(
1175
+ private async handleEnableMcpServerTool(
990
1176
  server_name: string,
991
1177
  tool: string
992
1178
  ): Promise<ServerMcpServerToolEnabled> {
@@ -1003,7 +1189,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
1003
1189
  };
1004
1190
  }
1005
1191
 
1006
- private async onDisableMcpServerTool(
1192
+ private async handleDisableMcpServerTool(
1007
1193
  server_name: string,
1008
1194
  tool: string
1009
1195
  ): Promise<ServerMcpServerToolDisabled> {
@@ -1020,7 +1206,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
1020
1206
  };
1021
1207
  }
1022
1208
 
1023
- private async onEnableAllMcpServerTools(
1209
+ private async handleEnableAllMcpServerTools(
1024
1210
  server_name: string
1025
1211
  ): Promise<ServerMcpServerToolEnabled[]> {
1026
1212
  // We reimplement the logic to enable any disabled tools so we can
@@ -1046,7 +1232,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
1046
1232
  return msgs;
1047
1233
  }
1048
1234
 
1049
- private async onDisableAllMcpServerTools(
1235
+ private async handleDisableAllMcpServerTools(
1050
1236
  server_name: string
1051
1237
  ): Promise<ServerMcpServerToolDisabled[]> {
1052
1238
  // We reimplement the logic to disable all enabled tools so we can
@@ -1070,7 +1256,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
1070
1256
  return msgs;
1071
1257
  }
1072
1258
 
1073
- private async onSetSystemPrompt(
1259
+ private async handleSetSystemPrompt(
1074
1260
  system_prompt: string
1075
1261
  ): Promise<ServerSystemPromptUpdated> {
1076
1262
  this.agent.setSystemPrompt(system_prompt);
@@ -1082,12 +1268,20 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
1082
1268
  };
1083
1269
  }
1084
1270
 
1085
- private async onSetModel(model: string): Promise<ServerModelUpdated> {
1271
+ private async handleSetModel(model: string): Promise<ServerModelUpdated> {
1086
1272
  this.agent.setModel(model);
1087
1273
  await this.updateAgentProfile();
1088
1274
  return { type: "model_updated", model, session_id: this.sessionUUID };
1089
1275
  }
1090
1276
 
1277
+ private async handleSetAgentPaused(
1278
+ paused: boolean
1279
+ ): Promise<ServerAgentPaused> {
1280
+ this.agentPaused = paused;
1281
+ await this.db.sessionSetAgentPaused(this.sessionUUID, paused);
1282
+ return { type: "agent_paused", session_id: this.sessionUUID, paused };
1283
+ }
1284
+
1091
1285
  private async handleSetMcpServerConfig(
1092
1286
  msg: ClientSetMcpServerConfig,
1093
1287
  from: string
@@ -1120,32 +1314,48 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
1120
1314
  // TODO: Do we want to braodcast an "mcp_server_config_updated" message?
1121
1315
  }
1122
1316
 
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
- );
1317
+ private async handleSessionFileGetContent(
1318
+ msg: Extract<ClientToServer, { type: "session_file_get_content" }>,
1319
+ from: string
1320
+ ): Promise<void> {
1321
+ const data_url = await this.sessionFileManager.getFileContent(msg.name);
1322
+ const contentMsg: ServerSessionFileContent = {
1323
+ type: "session_file_content",
1324
+ session_id: this.sessionUUID,
1325
+ client_message_id: msg.client_message_id,
1326
+ name: msg.name,
1327
+ data_url,
1328
+ };
1329
+ this.sender.sendTo(from, contentMsg);
1330
+ }
1140
1331
 
1141
- return {
1142
- type: "tool_auto_approval_set",
1143
- server_name: serverName,
1144
- tool,
1145
- auto_approve: autoApprove,
1146
- session_id: this.sessionUUID,
1147
- };
1332
+ private async handleSessionFileDelete(
1333
+ msg: Extract<ClientToServer, { type: "session_file_delete" }>
1334
+ ): Promise<void> {
1335
+ await this.sessionFileManager.deleteFile(msg.name);
1336
+ // Note, the SessionFileManager will call us back via
1337
+ // `ISessionFileManagerEventHandler.onFileDeleted` if the deletion
1338
+ // succeeds. We broadcast in that callback.
1339
+ }
1340
+
1341
+ private async handleSessionFilePutContent(
1342
+ msg: Extract<ClientToServer, { type: "session_file_put_content" }>
1343
+ ): Promise<void> {
1344
+ // If the session hasn't been persisted, it must be written to the DB,
1345
+ // (otherwise the sessionId in the SessionFiles entry will not be
1346
+ // considered valid byt he DB).
1347
+
1348
+ if (!this.isPersisted) {
1349
+ await this.createSessionInDB();
1148
1350
  }
1351
+ await this.sessionFileManager.putFileContent(
1352
+ msg.name,
1353
+ msg.summary,
1354
+ msg.data_url
1355
+ );
1356
+ // Note, the SessionFileManager will call us back via
1357
+ // `ISessionFileManagerEventHandler.onFileChanged` if the deletion
1358
+ // succeeds. We broadcast in that callback.
1149
1359
  }
1150
1360
 
1151
1361
  private ensureMcpServer(serverName: string): McpServerInfo {
@@ -1175,19 +1385,19 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
1175
1385
  * This only updates the local participant map - actual membership
1176
1386
  * tracking is handled by SessionRegistry.
1177
1387
  */
1178
- addParticipant(userId: string, role: TeamParticipant): void {
1179
- this.sessionParticipants.set(userId, role);
1388
+ addParticipant(userId: string, participant: TeamParticipant): void {
1389
+ this.sessionParticipants.set(userId, participant);
1180
1390
  // Broadcast result to all session participants
1181
1391
  const broadcastMessage: ServerUserAdded = {
1182
1392
  type: "user_added",
1183
1393
  user_uuid: userId,
1184
1394
  role: "participant",
1185
- nickname: role.nickname,
1186
- email: role.email,
1395
+ nickname: participant.nickname,
1396
+ email: participant.email,
1187
1397
  session_id: this.sessionUUID,
1188
1398
  };
1189
1399
 
1190
- this.broadcast(broadcastMessage);
1400
+ this.sender.broadcast(broadcastMessage);
1191
1401
  }
1192
1402
 
1193
1403
  /**
@@ -1204,7 +1414,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
1204
1414
  session_id: this.sessionUUID,
1205
1415
  };
1206
1416
 
1207
- this.broadcast(broadcastMessage);
1417
+ this.sender.broadcast(broadcastMessage);
1208
1418
  }
1209
1419
 
1210
1420
  private getSessionParticipants(): TeamParticipant[] {
@@ -1239,6 +1449,7 @@ export class OpenSession implements IAgentEventHandler, IPlatform {
1239
1449
  client_message_id: clientMessageId,
1240
1450
  session_id: this.sessionUUID,
1241
1451
  team_uuid: this.teamUUID,
1452
+ agent_paused: this.agentPaused,
1242
1453
  };
1243
1454
  }
1244
1455
 
@@ -1302,10 +1513,11 @@ async function loadSessionData(
1302
1513
  ownerApiKey: string;
1303
1514
  sessionParticipants: SessionParticipantMap;
1304
1515
  }> {
1516
+ const dbsm = db.createTypedClient(DbSessionMessages);
1305
1517
  const [sessionData, sessionMessages, sessionParticipants, sessionCheckpoint] =
1306
1518
  await Promise.all([
1307
- db.sessionGetById(sessionId),
1308
- db.sessionMessagesGetConversation(sessionId, DEFAULT_NUM_MESSGAES, 0),
1519
+ db.sessionGetDescriptorById(sessionId),
1520
+ dbsm.getConversation(sessionId, DEFAULT_NUM_MESSGAES, 0),
1309
1521
  db.sessionGetParticipants(sessionId),
1310
1522
  db.sessionCheckpointGet(sessionId),
1311
1523
  ]);
@@ -1330,3 +1542,72 @@ async function loadSessionData(
1330
1542
  sessionParticipants: createSessionParticipantMap(sessionParticipants),
1331
1543
  };
1332
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
+ }