@xalia/agent 0.6.8 → 0.6.9

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 (63) hide show
  1. package/.env.development +1 -0
  2. package/dist/agent/src/agent/agent.js +100 -77
  3. package/dist/agent/src/agent/agentUtils.js +21 -16
  4. package/dist/agent/src/agent/compressingContextManager.js +10 -14
  5. package/dist/agent/src/agent/context.js +101 -127
  6. package/dist/agent/src/agent/contextWithWorkspace.js +133 -0
  7. package/dist/agent/src/agent/imageGenLLM.js +0 -6
  8. package/dist/agent/src/agent/imageGenerator.js +2 -10
  9. package/dist/agent/src/agent/openAILLMStreaming.js +5 -2
  10. package/dist/agent/src/agent/sudoMcpServerManager.js +21 -9
  11. package/dist/agent/src/chat/client/chatClient.js +35 -2
  12. package/dist/agent/src/chat/client/connection.js +6 -1
  13. package/dist/agent/src/chat/client/sessionClient.js +0 -7
  14. package/dist/agent/src/chat/data/dbSessionMessages.js +11 -0
  15. package/dist/agent/src/chat/protocol/messages.js +4 -0
  16. package/dist/agent/src/chat/server/chatContextManager.js +149 -139
  17. package/dist/agent/src/chat/server/imageGeneratorTools.js +19 -8
  18. package/dist/agent/src/chat/server/openAIRouterLLM.js +114 -0
  19. package/dist/agent/src/chat/server/openSession.js +57 -58
  20. package/dist/agent/src/chat/server/server.js +6 -2
  21. package/dist/agent/src/chat/server/sessionRegistry.js +65 -6
  22. package/dist/agent/src/chat/server/sessionRegistry.test.js +1 -1
  23. package/dist/agent/src/chat/server/tools.js +52 -17
  24. package/dist/agent/src/test/chatContextManager.test.js +31 -29
  25. package/dist/agent/src/test/clientServerConnection.test.js +1 -2
  26. package/dist/agent/src/test/compressingContextManager.test.js +22 -36
  27. package/dist/agent/src/test/context.test.js +55 -17
  28. package/dist/agent/src/test/contextTestTools.js +87 -0
  29. package/dist/agent/src/tool/chatMain.js +22 -8
  30. package/package.json +1 -1
  31. package/scripts/test_chat +3 -0
  32. package/src/agent/agent.ts +170 -125
  33. package/src/agent/agentUtils.ts +31 -20
  34. package/src/agent/compressingContextManager.ts +13 -44
  35. package/src/agent/context.ts +165 -159
  36. package/src/agent/contextWithWorkspace.ts +162 -0
  37. package/src/agent/imageGenLLM.ts +0 -8
  38. package/src/agent/imageGenerator.ts +3 -18
  39. package/src/agent/openAILLMStreaming.ts +20 -3
  40. package/src/agent/sudoMcpServerManager.ts +41 -20
  41. package/src/chat/client/chatClient.ts +47 -3
  42. package/src/chat/client/connection.ts +11 -1
  43. package/src/chat/client/sessionClient.ts +0 -8
  44. package/src/chat/data/dataModels.ts +6 -0
  45. package/src/chat/data/dbSessionMessages.ts +34 -0
  46. package/src/chat/protocol/messages.ts +35 -8
  47. package/src/chat/server/chatContextManager.ts +210 -197
  48. package/src/chat/server/connectionManager.ts +1 -1
  49. package/src/chat/server/imageGeneratorTools.ts +31 -18
  50. package/src/chat/server/openAIRouterLLM.ts +171 -0
  51. package/src/chat/server/openSession.ts +87 -100
  52. package/src/chat/server/server.ts +6 -2
  53. package/src/chat/server/sessionFileManager.ts +5 -5
  54. package/src/chat/server/sessionRegistry.test.ts +0 -1
  55. package/src/chat/server/sessionRegistry.ts +100 -4
  56. package/src/chat/server/tools.ts +73 -35
  57. package/src/test/agent.test.ts +8 -7
  58. package/src/test/chatContextManager.test.ts +42 -37
  59. package/src/test/clientServerConnection.test.ts +0 -2
  60. package/src/test/compressingContextManager.test.ts +29 -34
  61. package/src/test/context.test.ts +59 -15
  62. package/src/test/contextTestTools.ts +95 -0
  63. package/src/tool/chatMain.ts +26 -12
@@ -27,9 +27,12 @@ import {
27
27
  ServerToClient,
28
28
  ClientControlAgentProfileCreate,
29
29
  ClientControlAgentProfileDelete,
30
+ ClientControlAddCustomMcpServer,
31
+ ClientControlRemoveCustomMcpServer,
30
32
  } from "../protocol/messages";
31
33
  import { GUEST_TOKEN_PREFIX, OpenSession } from "./openSession";
32
34
  import {
35
+ CustomMcpServerDescriptor,
33
36
  SessionCreateData,
34
37
  SessionData,
35
38
  SessionDescriptor,
@@ -47,6 +50,62 @@ const logger = getLogger();
47
50
 
48
51
  export type GuestUser = UserData & { guest_for_session: string };
49
52
 
53
+ // server_name => url
54
+ type CustomMcpServersForUser = Map<string, CustomMcpServerDescriptor>;
55
+
56
+ export class CustomMcpServerManager {
57
+ private perUser: Map<string, CustomMcpServersForUser>;
58
+
59
+ constructor(_db: Database) {
60
+ this.perUser = new Map();
61
+ }
62
+
63
+ getForUser(user_uuid: string): Record<string, CustomMcpServerDescriptor> {
64
+ const servers = this.perUser.get(user_uuid);
65
+ if (!servers) {
66
+ return {};
67
+ }
68
+
69
+ return Object.fromEntries(servers.entries());
70
+ }
71
+
72
+ async addForUser(
73
+ user_uuid: string,
74
+ server_name: string,
75
+ description: string,
76
+ url: string
77
+ ): Promise<void> {
78
+ // TODO: persistence to DB
79
+
80
+ const userServers = this.getUserServers(user_uuid);
81
+ if (userServers.has(server_name)) {
82
+ throw new Error(`server ${server_name} already added`);
83
+ }
84
+
85
+ userServers.set(server_name, { name: server_name, description, url });
86
+
87
+ return Promise.resolve();
88
+ }
89
+
90
+ async removeForUser(user_uuid: string, server_name: string): Promise<void> {
91
+ const userServers = this.getUserServers(user_uuid);
92
+ if (userServers.has(server_name)) {
93
+ userServers.delete(server_name);
94
+ }
95
+ return Promise.resolve();
96
+ }
97
+
98
+ private getUserServers(user_uuid: string): CustomMcpServersForUser {
99
+ let userServers = this.perUser.get(user_uuid);
100
+ if (!userServers) {
101
+ userServers = new Map();
102
+ this.perUser.set(user_uuid, userServers);
103
+ }
104
+
105
+ return userServers;
106
+ }
107
+ }
108
+
50
109
  export class SessionRegistry implements IMessageProcessor<ClientToServer> {
51
110
  // In memory session-user/user-session mappings
52
111
  // Note: this mappings ONLY trackes online users and will
@@ -64,13 +123,15 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
64
123
 
65
124
  private apiKeyManager: ApiKeyManager;
66
125
 
126
+ private customMcpServerManager: CustomMcpServerManager;
127
+
67
128
  constructor(
68
129
  private db: Database,
69
130
  private connectionManager: IUserConnectionManager<ServerToClient>,
70
- private llmUrl: string,
71
131
  private xmcpUrl: string
72
132
  ) {
73
133
  this.apiKeyManager = new ApiKeyManager(db);
134
+ this.customMcpServerManager = new CustomMcpServerManager(db);
74
135
  }
75
136
 
76
137
  /**
@@ -293,6 +354,12 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
293
354
  case "control_remove_team_user":
294
355
  await this.handleRemoveTeamUser(connectionId, userId, message);
295
356
  break;
357
+ case "control_add_custom_mcp_server":
358
+ await this.handleAddCustomMcpServer(userId, message);
359
+ break;
360
+ case "control_remove_custom_mcp_server":
361
+ await this.handleRemoveCustomMcpServer(userId, message);
362
+ break;
296
363
  default: {
297
364
  const exhaustive: never = message;
298
365
  // unknown connection type should be a fatal error
@@ -303,6 +370,37 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
303
370
  }
304
371
  }
305
372
 
373
+ private async handleAddCustomMcpServer(
374
+ userId: string,
375
+ message: ClientControlAddCustomMcpServer
376
+ ): Promise<void> {
377
+ await this.customMcpServerManager.addForUser(
378
+ userId,
379
+ message.server_name,
380
+ message.description,
381
+ message.url
382
+ );
383
+ this.connectionManager.sendToUsers([userId], {
384
+ type: "control_custom_mcp_server_added",
385
+ server_name: message.server_name,
386
+ url: message.url,
387
+ });
388
+ }
389
+
390
+ private async handleRemoveCustomMcpServer(
391
+ userId: string,
392
+ message: ClientControlRemoveCustomMcpServer
393
+ ): Promise<void> {
394
+ await this.customMcpServerManager.removeForUser(
395
+ userId,
396
+ message.server_name
397
+ );
398
+ this.connectionManager.sendToUsers([userId], {
399
+ type: "control_custom_mcp_server_removed",
400
+ server_name: message.server_name,
401
+ });
402
+ }
403
+
306
404
  /**
307
405
  * Handle session_list request
308
406
  */
@@ -333,6 +431,7 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
333
431
  team_sessions: teamSessions,
334
432
  user_agents: userAgents,
335
433
  client_message_id: message.client_message_id,
434
+ custom_mcp_servers: this.customMcpServerManager.getForUser(userId),
336
435
  };
337
436
 
338
437
  this.connectionManager.sendToConnection(connectionId, response);
@@ -755,7 +854,6 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
755
854
  const session = await OpenSession.initWithExistingSession(
756
855
  this.db,
757
856
  sessionId,
758
- this.llmUrl,
759
857
  this.xmcpUrl,
760
858
  this.connectionManager
761
859
  );
@@ -1039,7 +1137,6 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
1039
1137
  const openSession = await OpenSession.initWithEmptySession(
1040
1138
  this.db,
1041
1139
  newSessionData,
1042
- this.llmUrl,
1043
1140
  this.xmcpUrl,
1044
1141
  this.connectionManager
1045
1142
  );
@@ -1083,7 +1180,6 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
1083
1180
  const openSession = await OpenSession.initWithEmptySession(
1084
1181
  this.db,
1085
1182
  newSessionData,
1086
- this.llmUrl,
1087
1183
  this.xmcpUrl,
1088
1184
  this.connectionManager
1089
1185
  );
@@ -4,18 +4,24 @@
4
4
 
5
5
  import { Parser } from "expr-eval";
6
6
 
7
+ import {
8
+ Agent,
9
+ AgentEx,
10
+ IAgentToolProvider,
11
+ ToolCallResult,
12
+ } from "../../agent/agent";
7
13
  import { getLogger } from "@xalia/xmcp/sdk";
8
14
 
9
- import { Agent, IAgentToolProvider, ToolCallResult } from "../../agent/agent";
10
15
  import { htmlToText } from "../utils/htmlToText";
11
16
  import { webSearch } from "../utils/search";
12
17
  import {
13
- ChatSessionFileManager,
14
- ToolCallResultWithFileRef,
15
- fileManagerTool,
18
+ ChatSessionFileManager,
19
+ ToolCallResultWithFileRef,
20
+ fileManagerTool,
16
21
  } from "./sessionFileManager";
17
22
  import { genImageFileTool } from "./imageGeneratorTools";
18
23
  import { ToolDescriptor } from "../../agent/llm";
24
+ import { IPlatform } from "../../agent/iplatform";
19
25
 
20
26
  const logger = getLogger();
21
27
 
@@ -111,7 +117,7 @@ export function datetimeTool(timezone: string): IAgentToolProvider {
111
117
  };
112
118
  return {
113
119
  // eslint-disable-next-line @typescript-eslint/require-await
114
- setup: async (agent: Agent) => {
120
+ setup: async (agent: AgentEx) => {
115
121
  agent.addAgentTool(DATETIME_DESC, toolFn);
116
122
  },
117
123
  };
@@ -148,12 +154,14 @@ export function calculatorEval(args: string): string {
148
154
 
149
155
  export const calculatorTool: IAgentToolProvider = {
150
156
  // eslint-disable-next-line @typescript-eslint/require-await
151
- setup: async (agent: Agent) => {
157
+ setup: async (agent: AgentEx) => {
152
158
  const getExpr = makeParseArgsFn(["expr"] as const);
153
- // eslint-disable-next-line @typescript-eslint/require-await
154
- const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
159
+ const toolFn = async (
160
+ _: AgentEx,
161
+ args: unknown
162
+ ): Promise<ToolCallResult> => {
155
163
  const { expr } = getExpr(args);
156
- return { response: calculatorEval(expr) };
164
+ return Promise.resolve({ response: calculatorEval(expr) });
157
165
  };
158
166
 
159
167
  agent.addAgentTool(ARITHMETIC_DESC, toolFn);
@@ -164,28 +172,45 @@ const RENDER_DESC: ToolDescriptor = {
164
172
  type: "function",
165
173
  function: {
166
174
  name: "render",
167
- description: "Display the given html fragment inside a div element",
175
+ description: [
176
+ "Display HTML using only safe elements",
177
+ "Never include <script>, <iframe>, <object>, <embed>, <form>, <meta>,",
178
+ "<link>, <style>, event handlers (onclick, etc.), or javascript: URLs.",
179
+ "If the tool result is [HTML_SANITIZATION_WARNING], do not mention it.",
180
+ "Retry with only safe elements. If warning repeats, tell the user you",
181
+ "cannot render unsafe HTML."
182
+ ].join(" "),
168
183
  parameters: {
169
184
  type: "object",
170
185
  properties: {
171
- name: {
172
- type: "string",
173
- description: "Filename to store the html",
174
- },
175
- summary: {
176
- type: "string",
177
- description: "One line summary",
178
- },
179
- html: {
180
- type: "string",
181
- description: "HTML fragment to render",
182
- },
186
+ name: { type: "string", description: "Filename for the HTML" },
187
+ summary: { type: "string", description: "One line summary" },
188
+ html: { type: "string", description: "HTML fragment to render" },
183
189
  },
184
190
  required: ["name", "summary", "html"],
185
191
  },
186
192
  },
187
193
  };
188
194
 
195
+ function validateHtmlSafety(html: string): string | undefined {
196
+ const issues: string[] = [];
197
+ if (/<script[\s>]/i.test(html)) issues.push("<script> tag");
198
+ if (/<iframe[\s>]/i.test(html)) issues.push("<iframe> tag");
199
+ if (/<object[\s>]/i.test(html)) issues.push("<object> tag");
200
+ if (/<embed[\s>]/i.test(html)) issues.push("<embed> tag");
201
+ if (/\bon\w+\s*=/.test(html)) issues.push("event handler (e.g., onclick)");
202
+ if (/javascript:/i.test(html)) issues.push("javascript: URL");
203
+ if (/<meta[\s>]/i.test(html)) issues.push("<meta> tag");
204
+ if (/<form[\s>]/i.test(html)) issues.push("<form> tag");
205
+ if (/<style[\s>]/i.test(html)) issues.push("<style> tag");
206
+ if (/<link[\s>]/i.test(html)) issues.push("<link> tag");
207
+
208
+ if (issues.length > 0) {
209
+ return `Unsafe HTML: ${issues.join(", ")}`;
210
+ }
211
+ return undefined;
212
+ }
213
+
189
214
  export function renderTool(
190
215
  fileManager: ChatSessionFileManager
191
216
  ): IAgentToolProvider {
@@ -195,10 +220,21 @@ export function renderTool(
195
220
  "html",
196
221
  ] as const);
197
222
  const toolFn = async (
198
- _: Agent,
223
+ _: AgentEx,
199
224
  args: unknown
200
225
  ): Promise<ToolCallResultWithFileRef> => {
201
226
  const { name, summary, html } = getNameSummeryHtml(args);
227
+ const safetyError = validateHtmlSafety(html);
228
+ if (safetyError) {
229
+ return {
230
+ response: "[HTML_SANITIZATION_WARNING]",
231
+ overwriteResponse: "",
232
+ structuredContent: {
233
+ kind: "htmlSanitizationWarning",
234
+ message: safetyError,
235
+ },
236
+ };
237
+ }
202
238
  const mimeType = "text/html";
203
239
  const dataURL = `data:${mimeType},${html}`;
204
240
  await fileManager.putFileContent(name, summary, dataURL);
@@ -211,7 +247,7 @@ export function renderTool(
211
247
 
212
248
  return {
213
249
  // eslint-disable-next-line @typescript-eslint/require-await
214
- setup: async (agent: Agent) => {
250
+ setup: async (agent: AgentEx) => {
215
251
  agent.addAgentTool(RENDER_DESC, toolFn);
216
252
  },
217
253
  };
@@ -237,7 +273,7 @@ const WEB_SEARCH_DESC: ToolDescriptor = {
237
273
 
238
274
  export function webSearchTool(): IAgentToolProvider {
239
275
  const getQuery = makeParseArgsFn(["query"] as const);
240
- const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
276
+ const toolFn = async (_: AgentEx, args: unknown): Promise<ToolCallResult> => {
241
277
  const { query } = getQuery(args);
242
278
  logger.debug(`[web_search]: query: ${query}`);
243
279
  const results = await webSearch(query);
@@ -247,7 +283,7 @@ export function webSearchTool(): IAgentToolProvider {
247
283
 
248
284
  return {
249
285
  // eslint-disable-next-line @typescript-eslint/require-await
250
- setup: async (agent: Agent) => {
286
+ setup: async (agent: AgentEx) => {
251
287
  agent.addAgentTool(WEB_SEARCH_DESC, toolFn);
252
288
  },
253
289
  };
@@ -293,14 +329,14 @@ export async function openURL(url: string): Promise<string> {
293
329
 
294
330
  export function openURLTool(): IAgentToolProvider {
295
331
  const getURL = makeParseArgsFn(["url"] as const);
296
- const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
332
+ const toolFn = async (_: AgentEx, args: unknown): Promise<ToolCallResult> => {
297
333
  const { url } = getURL(args);
298
334
  return { response: await openURL(url) };
299
335
  };
300
336
 
301
337
  return {
302
338
  // eslint-disable-next-line @typescript-eslint/require-await
303
- setup: async (agent: Agent) => {
339
+ setup: async (agent: AgentEx) => {
304
340
  agent.addAgentTool(OPEN_URL_DESC, toolFn);
305
341
  },
306
342
  };
@@ -320,8 +356,11 @@ const TEST_DESC: ToolDescriptor = {
320
356
  };
321
357
 
322
358
  export const testTool: IAgentToolProvider = {
323
- setup: (agent: Agent) => {
324
- const toolFn = (_agent: Agent, _args: unknown): Promise<ToolCallResult> => {
359
+ setup: (agent: AgentEx) => {
360
+ const toolFn = (
361
+ _agent: AgentEx,
362
+ _args: unknown
363
+ ): Promise<ToolCallResult> => {
325
364
  // Return an object with structuredContent and _meta
326
365
  return Promise.resolve({
327
366
  response: "Some text",
@@ -338,11 +377,10 @@ export const testTool: IAgentToolProvider = {
338
377
  * Add a set of agent tools for chat sessions.
339
378
  */
340
379
  export async function addDefaultChatTools(
341
- agent: Agent,
380
+ agent: AgentEx | Agent,
342
381
  timezone: string,
343
- fileManager: ChatSessionFileManager,
344
- llmUrl: string,
345
- llmApiKey: string
382
+ platform: IPlatform,
383
+ fileManager: ChatSessionFileManager
346
384
  ): Promise<void> {
347
385
  await agent.addAgentToolProvider(datetimeTool(timezone));
348
386
  await agent.addAgentToolProvider(calculatorTool);
@@ -351,7 +389,7 @@ export async function addDefaultChatTools(
351
389
  await agent.addAgentToolProvider(openURLTool());
352
390
  await agent.addAgentToolProvider(fileManagerTool(fileManager));
353
391
  await agent.addAgentToolProvider(
354
- await genImageFileTool(llmUrl, llmApiKey, fileManager)
392
+ await genImageFileTool(platform, fileManager)
355
393
  );
356
394
  if (DEVELOPMENT) {
357
395
  await agent.addAgentToolProvider(testTool);
@@ -5,6 +5,7 @@ import { Schema } from "@xalia/xmcp/sdk";
5
5
 
6
6
  import {
7
7
  Agent,
8
+ AgentEx,
8
9
  AgentProfile,
9
10
  IAgentToolProvider,
10
11
  ToolCallResult,
@@ -66,9 +67,9 @@ function createCallTestToolScript(
66
67
  expectAgentMessages: string[];
67
68
  expectToolResults: ToolMessageParam[];
68
69
  tool0_descriptor: ToolDescriptor;
69
- tool0_fn: (agent: Agent, args: unknown) => Promise<ToolCallResult>;
70
+ tool0_fn: (agent: AgentEx, args: unknown) => Promise<ToolCallResult>;
70
71
  test_tool_descriptor: ToolDescriptor;
71
- test_tool_fn: (agent: Agent, args: unknown) => Promise<ToolCallResult>;
72
+ test_tool_fn: (agent: AgentEx, args: unknown) => Promise<ToolCallResult>;
72
73
  } {
73
74
  // A tool with no args
74
75
 
@@ -86,7 +87,7 @@ function createCallTestToolScript(
86
87
  },
87
88
  };
88
89
 
89
- const tool0_fn = (_: Agent, _args: unknown): Promise<ToolCallResult> => {
90
+ const tool0_fn = (_: AgentEx, _args: unknown): Promise<ToolCallResult> => {
90
91
  return Promise.resolve({ response: "0" });
91
92
  };
92
93
 
@@ -115,7 +116,7 @@ function createCallTestToolScript(
115
116
  };
116
117
 
117
118
  const test_tool_fn = async (
118
- _: Agent,
119
+ _: AgentEx,
119
120
  args: unknown
120
121
  ): Promise<ToolCallResult> => {
121
122
  const { param1, param2 } = args as { param1: string; param2: number };
@@ -312,7 +313,7 @@ describe("Agent", () => {
312
313
  await createTestAgent(script);
313
314
 
314
315
  const toolProvider: IAgentToolProvider = {
315
- setup: (agent: Agent) => {
316
+ setup: (agent: AgentEx) => {
316
317
  // Add the tool async to test this mechanism
317
318
  return new Promise<void>((r) => {
318
319
  setTimeout(() => {
@@ -437,7 +438,7 @@ describe("Agent", () => {
437
438
  return JSON.stringify(transformArgs(args));
438
439
  };
439
440
  const newToolFn = async (
440
- agent: Agent,
441
+ agent: AgentEx,
441
442
  args: unknown
442
443
  ): Promise<ToolCallResult> => {
443
444
  const result = await test_tool_fn(agent, args);
@@ -507,7 +508,7 @@ describe("Agent", () => {
507
508
  return result.toUpperCase();
508
509
  };
509
510
  const newToolFn = async (
510
- agent: Agent,
511
+ agent: AgentEx,
511
512
  args: unknown
512
513
  ): Promise<ToolCallResult> => {
513
514
  const result = await test_tool_fn(agent, args);
@@ -7,7 +7,7 @@ import {
7
7
  resolveConversationWithCheckpoint,
8
8
  } from "../chat/server/chatContextManager";
9
9
  import { createUserMessageEnsure } from "../agent/agent";
10
- import { AssistantMessageParam, ToolMessageParam } from "../agent/llm";
10
+ import { AssistantMessageParam, ILLM, ToolMessageParam } from "../agent/llm";
11
11
  import { createCheckpointMessage } from "../agent/compressingContextManager";
12
12
  import {
13
13
  MESSAGE_INDEX_FULL_INCREMENT,
@@ -65,7 +65,10 @@ const MESSAGES: SessionMessage[] = [
65
65
  },
66
66
  ];
67
67
 
68
- function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
68
+ async function testSuccessfulAgentLoop(
69
+ cm: ChatContextManager,
70
+ startIdx: number
71
+ ) {
69
72
  // 2 user messages arrive as ClientUserMessage. Assign message indices and
70
73
  // convert them to ServerUserMessages.
71
74
 
@@ -104,11 +107,11 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
104
107
 
105
108
  assert(serverUserMessage0 && serverUserMessage1);
106
109
  const userMsgs = [serverUserMessage0, serverUserMessage1];
107
- const { llmUserMessages, agentFirstChunk } = cm.startAgentResponse(userMsgs);
110
+ const { contextTx, agentFirstChunk } = await cm.startAgentResponse(userMsgs);
108
111
  expect(agentFirstChunk.message_idx).eql(
109
112
  startIdx + 2 * MESSAGE_INDEX_FULL_INCREMENT
110
113
  );
111
- expect(llmUserMessages).eql(
114
+ expect(contextTx.getLLMContext().slice(1)).eql(
112
115
  userMsgs.map((su) => {
113
116
  return {
114
117
  role: "user",
@@ -120,7 +123,7 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
120
123
 
121
124
  // The agent sends message chunks. Check the message_idx
122
125
 
123
- const chunkMsg0 = cm.processAgentMessage("AgentMe", false);
126
+ const chunkMsg0 = contextTx.processAgentMessageChunk("AgentMe", false);
124
127
  expect(chunkMsg0).eql({
125
128
  type: "agent_msg_chunk",
126
129
  session_id: "some_session_id",
@@ -128,7 +131,7 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
128
131
  message: "AgentMe",
129
132
  end: false,
130
133
  });
131
- const chunkMsg1 = cm.processAgentMessage("ssage2", true);
134
+ const chunkMsg1 = contextTx.processAgentMessageChunk("ssage2", true);
132
135
  expect(chunkMsg1).eql({
133
136
  type: "agent_msg_chunk",
134
137
  session_id: "some_session_id",
@@ -162,7 +165,8 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
162
165
  content: "AgentMessage2",
163
166
  };
164
167
 
165
- cm.processAgentResponse(agentResponseWithToolCall);
168
+ contextTx.addMessage(agentResponseWithToolCall);
169
+ contextTx.processAgentResponse(agentResponseWithToolCall);
166
170
 
167
171
  // Meanwhile, a new user message comes in. It is assigned idx
168
172
  // startIdx + 3*MESSAGE_INDEX_FULL_INCREMENT.
@@ -198,15 +202,17 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
198
202
  },
199
203
  ];
200
204
 
201
- const toolCallMesasge0 = cm.processToolCallResult(toolCallResults[0]);
205
+ contextTx.addMessage(toolCallResults[0]);
206
+ contextTx.addMessage(toolCallResults[1]);
202
207
 
208
+ const toolCallMesasge0 = contextTx.processToolCallResult(toolCallResults[0]);
203
209
  expect(toolCallMesasge0.message_idx).eql(
204
210
  startIdx +
205
211
  2 * MESSAGE_INDEX_FULL_INCREMENT +
206
212
  1 * MESSAGE_INDEX_SUB_INCREMENT
207
213
  );
208
214
 
209
- const toolCallMesasge1 = cm.processToolCallResult(toolCallResults[1]);
215
+ const toolCallMesasge1 = contextTx.processToolCallResult(toolCallResults[1]);
210
216
  expect(toolCallMesasge1.message_idx).eql(
211
217
  startIdx +
212
218
  2 * MESSAGE_INDEX_FULL_INCREMENT +
@@ -220,19 +226,14 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
220
226
  content: "AgentMessage2",
221
227
  };
222
228
 
223
- // The agent adds the messages to the context at the end of processing
229
+ contextTx.addMessage(finalAgentResponse);
224
230
 
225
- cm.addMessages(llmUserMessages);
226
- cm.addMessage(agentResponseWithToolCall);
227
- cm.addMessage(toolCallResults[0]);
228
- cm.addMessage(toolCallResults[1]);
229
- cm.addMessage(finalAgentResponse);
231
+ // Agent then returns the final response to the caller (OpenSession), which
232
+ // sets it on the Tx.
230
233
 
231
- // Agent then returns the final response to the caller (OpenSession)
234
+ contextTx.processAgentResponse(finalAgentResponse);
232
235
 
233
- cm.processAgentResponse(finalAgentResponse);
234
-
235
- const dbMessages = cm.endAgentResponse();
236
+ const dbMessages = await cm.endAgentResponse(contextTx);
236
237
 
237
238
  // Expect
238
239
  const expectDbMessages: SessionMessage[] = [
@@ -369,7 +370,7 @@ describe("IndexingCompressingContextManager", () => {
369
370
  });
370
371
  });
371
372
 
372
- it("conversation processing behaviour", () => {
373
+ it("conversation processing behaviour", async () => {
373
374
  const writer = {
374
375
  writeCheckpoint: (_sc: SessionCheckpoint) => {
375
376
  return new Promise<void>((r) => {
@@ -384,17 +385,15 @@ describe("IndexingCompressingContextManager", () => {
384
385
  "some_session_id",
385
386
  "no_such_user",
386
387
  undefined,
387
- "",
388
- "",
389
- "",
390
388
  writer,
391
- new MemoryFileManager()
389
+ new MemoryFileManager(),
390
+ undefined as unknown as ILLM
392
391
  );
393
392
 
394
- testSuccessfulAgentLoop(cm, 0);
393
+ await testSuccessfulAgentLoop(cm, 0);
395
394
  });
396
395
 
397
- it("error handling", () => {
396
+ it("error handling", async () => {
398
397
  const writer = {
399
398
  writeCheckpoint: (_sc: SessionCheckpoint) => {
400
399
  return new Promise<void>((r) => {
@@ -409,11 +408,9 @@ describe("IndexingCompressingContextManager", () => {
409
408
  "some_session_id",
410
409
  "no_such_user",
411
410
  undefined,
412
- "",
413
- "",
414
- "",
415
411
  writer,
416
- new MemoryFileManager()
412
+ new MemoryFileManager(),
413
+ undefined as unknown as ILLM
417
414
  );
418
415
 
419
416
  // 1 user messages
@@ -438,11 +435,11 @@ describe("IndexingCompressingContextManager", () => {
438
435
  // MESSAGE_INDEX_FULL_INCREMENT
439
436
 
440
437
  assert(serverUserMessage0);
441
- const { llmUserMessages, agentFirstChunk } = cm.startAgentResponse([
438
+ const { contextTx, agentFirstChunk } = await cm.startAgentResponse([
442
439
  serverUserMessage0,
443
440
  ]);
444
441
  expect(agentFirstChunk.message_idx).eql(1 * MESSAGE_INDEX_FULL_INCREMENT);
445
- expect(llmUserMessages).eql([
442
+ expect(contextTx.getLLMContext().slice(1)).eql([
446
443
  {
447
444
  role: "user",
448
445
  name: serverUserMessage0.user_uuid,
@@ -474,7 +471,8 @@ describe("IndexingCompressingContextManager", () => {
474
471
  ],
475
472
  content: "AgentMessage2",
476
473
  };
477
- cm.processAgentResponse(agentResponseWithToolCall);
474
+ contextTx.addMessage(agentResponseWithToolCall);
475
+ contextTx.processAgentResponse(agentResponseWithToolCall);
478
476
 
479
477
  // The current Agent does not add the tool call results until the next
480
478
  // completion has run. Here we are simulating an error during that
@@ -498,12 +496,19 @@ describe("IndexingCompressingContextManager", () => {
498
496
  },
499
497
  ];
500
498
 
501
- const toolCallMesasge0 = cm.processToolCallResult(toolCallResults[0]);
499
+ contextTx.addMessage(toolCallResults[0]);
500
+ contextTx.addMessage(toolCallResults[1]);
501
+
502
+ const toolCallMesasge0 = contextTx.processToolCallResult(
503
+ toolCallResults[0]
504
+ );
502
505
  expect(toolCallMesasge0.message_idx).eql(
503
506
  1 * MESSAGE_INDEX_FULL_INCREMENT + 1 * MESSAGE_INDEX_SUB_INCREMENT
504
507
  );
505
508
 
506
- const toolCallMesasge1 = cm.processToolCallResult(toolCallResults[1]);
509
+ const toolCallMesasge1 = contextTx.processToolCallResult(
510
+ toolCallResults[1]
511
+ );
507
512
  expect(toolCallMesasge1.message_idx).eql(
508
513
  1 * MESSAGE_INDEX_FULL_INCREMENT + 2 * MESSAGE_INDEX_SUB_INCREMENT
509
514
  );
@@ -511,12 +516,12 @@ describe("IndexingCompressingContextManager", () => {
511
516
  // The error is caught by OpenSession, which informs the
512
517
  // ChatContextManager.
513
518
 
514
- cm.revertAgentResponse("an error occured");
519
+ contextTx.revertAgentResponse("an error occured");
515
520
 
516
521
  // We should now be able to run the original test (starting from index
517
522
  // 2*MESSAGE_INDEX_FULL_INCREMENT). None of our previous messages should
518
523
  // hit the DB or the LLM context.
519
524
 
520
- testSuccessfulAgentLoop(cm, 2 * MESSAGE_INDEX_FULL_INCREMENT);
525
+ await testSuccessfulAgentLoop(cm, 2 * MESSAGE_INDEX_FULL_INCREMENT);
521
526
  });
522
527
  });
@@ -31,7 +31,6 @@ describe("Client-Server WebSocket Integration", () => {
31
31
  const mockEnv = {
32
32
  SUPABASE_URL: SUPABASE_LOCAL_URL,
33
33
  SUPABASE_KEY: SUPABASE_LOCAL_KEY,
34
- LLM_URL: "http://localhost:8080", // not used in this test
35
34
  XMCP_URL: "http://localhost:8081", // not used in this test
36
35
  };
37
36
  beforeAll(async () => {
@@ -55,7 +54,6 @@ describe("Client-Server WebSocket Integration", () => {
55
54
  serverPort,
56
55
  mockEnv.SUPABASE_URL,
57
56
  mockEnv.SUPABASE_KEY,
58
- mockEnv.LLM_URL,
59
57
  mockEnv.XMCP_URL
60
58
  );
61
59
  // Wait for server to be ready