@xalia/agent 0.6.9 → 0.6.10

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/.env.development +6 -1
  2. package/.env.test +7 -0
  3. package/README.md +11 -0
  4. package/context_system.md +498 -0
  5. package/dist/agent/src/agent/agent.js +77 -18
  6. package/dist/agent/src/agent/agentUtils.js +3 -2
  7. package/dist/agent/src/agent/documentSummarizer.js +126 -0
  8. package/dist/agent/src/agent/dummyLLM.js +25 -22
  9. package/dist/agent/src/agent/imageGenLLM.js +22 -19
  10. package/dist/agent/src/agent/llm.js +1 -1
  11. package/dist/agent/src/agent/openAILLM.js +15 -12
  12. package/dist/agent/src/agent/openAILLMStreaming.js +68 -37
  13. package/dist/agent/src/agent/repeatLLM.js +16 -7
  14. package/dist/agent/src/agent/tokenCounter.js +390 -0
  15. package/dist/agent/src/agent/tokenCounter.test.js +206 -0
  16. package/dist/agent/src/agent/toolSettings.js +17 -0
  17. package/dist/agent/src/agent/tools/calculatorTool.js +45 -0
  18. package/dist/agent/src/agent/tools/contentExtractors/pdfToText.js +55 -0
  19. package/dist/agent/src/agent/tools/datetimeTool.js +38 -0
  20. package/dist/agent/src/agent/tools/fileManager/fileManagerTool.js +156 -0
  21. package/dist/agent/src/agent/tools/fileManager/index.js +31 -0
  22. package/dist/agent/src/agent/tools/fileManager/memoryFileManager.js +102 -0
  23. package/dist/agent/src/{chat/data → agent/tools/fileManager}/mimeTypes.js +3 -1
  24. package/dist/agent/src/agent/tools/fileManager/prompt.js +33 -0
  25. package/dist/agent/src/{chat/data/dbSessionFileModels.js → agent/tools/fileManager/types.js} +7 -0
  26. package/dist/agent/src/agent/tools/index.js +64 -0
  27. package/dist/agent/src/agent/tools/openUrlTool.js +57 -0
  28. package/dist/agent/src/agent/tools/renderTool.js +89 -0
  29. package/dist/agent/src/agent/tools/utils.js +61 -0
  30. package/dist/agent/src/{chat/utils/search.js → agent/tools/webSearch.js} +1 -2
  31. package/dist/agent/src/agent/tools/webSearchTool.js +40 -0
  32. package/dist/agent/src/chat/client/chatClient.js +28 -0
  33. package/dist/agent/src/chat/client/index.js +4 -1
  34. package/dist/agent/src/chat/client/sessionClient.js +28 -2
  35. package/dist/agent/src/chat/constants.js +8 -0
  36. package/dist/agent/src/chat/data/dbSessionFiles.js +11 -6
  37. package/dist/agent/src/chat/protocol/messages.js +5 -0
  38. package/dist/agent/src/chat/server/chatContextManager.js +45 -25
  39. package/dist/agent/src/chat/server/conversation.js +3 -0
  40. package/dist/agent/src/chat/server/imageGeneratorTools.js +20 -8
  41. package/dist/agent/src/chat/server/openAIRouterLLM.js +0 -3
  42. package/dist/agent/src/chat/server/openSession.js +218 -55
  43. package/dist/agent/src/chat/server/promptRefiner.js +86 -0
  44. package/dist/agent/src/chat/server/server.js +5 -1
  45. package/dist/agent/src/chat/server/sessionFileManager.js +22 -221
  46. package/dist/agent/src/chat/server/sessionRegistry.js +87 -0
  47. package/dist/agent/src/chat/server/titleGenerator.js +112 -0
  48. package/dist/agent/src/chat/server/titleGenerator.test.js +113 -0
  49. package/dist/agent/src/chat/server/tools.js +63 -287
  50. package/dist/agent/src/chat/utils/approvalManager.js +6 -3
  51. package/dist/agent/src/chat/utils/multiAsyncQueue.js +3 -0
  52. package/dist/agent/src/test/agent.test.js +16 -17
  53. package/dist/agent/src/test/chatContextManager.test.js +15 -3
  54. package/dist/agent/src/test/dbMcpServerConfigs.test.js +4 -4
  55. package/dist/agent/src/test/dbSessionFiles.test.js +17 -17
  56. package/dist/agent/src/test/testTools.js +6 -1
  57. package/dist/agent/src/test/tools.test.js +27 -9
  58. package/dist/agent/src/tool/agentChat.js +5 -2
  59. package/dist/agent/src/tool/chatMain.js +34 -7
  60. package/dist/agent/src/tool/commandPrompt.js +2 -2
  61. package/dist/agent/src/tool/files.js +7 -8
  62. package/package.json +4 -1
  63. package/scripts/test_chat +195 -176
  64. package/src/agent/agent.ts +98 -23
  65. package/src/agent/agentUtils.ts +3 -2
  66. package/src/agent/documentSummarizer.ts +157 -0
  67. package/src/agent/dummyLLM.ts +27 -23
  68. package/src/agent/imageGenLLM.ts +28 -24
  69. package/src/agent/llm.ts +2 -2
  70. package/src/agent/openAILLM.ts +17 -13
  71. package/src/agent/openAILLMStreaming.ts +80 -41
  72. package/src/agent/repeatLLM.ts +19 -7
  73. package/src/agent/test_data/harrypotter.txt +6065 -0
  74. package/src/agent/tokenCounter.test.ts +243 -0
  75. package/src/agent/tokenCounter.ts +483 -0
  76. package/src/agent/toolSettings.ts +24 -0
  77. package/src/agent/tools/calculatorTool.ts +50 -0
  78. package/src/agent/tools/contentExtractors/pdfToText.ts +60 -0
  79. package/src/agent/tools/datetimeTool.ts +41 -0
  80. package/src/agent/tools/fileManager/fileManagerTool.ts +199 -0
  81. package/src/agent/tools/fileManager/index.ts +50 -0
  82. package/src/agent/tools/fileManager/memoryFileManager.ts +120 -0
  83. package/src/{chat/data → agent/tools/fileManager}/mimeTypes.ts +3 -1
  84. package/src/agent/tools/fileManager/prompt.ts +38 -0
  85. package/src/{chat/data/dbSessionFileModels.ts → agent/tools/fileManager/types.ts} +76 -0
  86. package/src/agent/tools/index.ts +49 -0
  87. package/src/agent/tools/openUrlTool.ts +62 -0
  88. package/src/agent/tools/renderTool.ts +92 -0
  89. package/src/agent/tools/utils.ts +74 -0
  90. package/src/{chat/utils/search.ts → agent/tools/webSearch.ts} +0 -1
  91. package/src/agent/tools/webSearchTool.ts +44 -0
  92. package/src/chat/client/chatClient.ts +45 -0
  93. package/src/chat/client/index.ts +3 -0
  94. package/src/chat/client/sessionClient.ts +40 -3
  95. package/src/chat/client/sessionFiles.ts +1 -1
  96. package/src/chat/constants.ts +6 -0
  97. package/src/chat/data/dataModels.ts +6 -0
  98. package/src/chat/data/dbSessionFiles.ts +12 -4
  99. package/src/chat/protocol/messages.ts +60 -7
  100. package/src/chat/server/chatContextManager.ts +58 -37
  101. package/src/chat/server/conversation.ts +3 -0
  102. package/src/chat/server/imageGeneratorTools.ts +31 -12
  103. package/src/chat/server/openAIRouterLLM.ts +1 -4
  104. package/src/chat/server/openSession.ts +323 -67
  105. package/src/chat/server/promptRefiner.ts +106 -0
  106. package/src/chat/server/server.ts +4 -1
  107. package/src/chat/server/sessionFileManager.ts +35 -306
  108. package/src/chat/server/sessionRegistry.ts +128 -0
  109. package/src/chat/server/titleGenerator.test.ts +103 -0
  110. package/src/chat/server/titleGenerator.ts +143 -0
  111. package/src/chat/server/tools.ts +77 -304
  112. package/src/chat/utils/approvalManager.ts +9 -3
  113. package/src/chat/utils/multiAsyncQueue.ts +4 -0
  114. package/src/test/agent.test.ts +17 -23
  115. package/src/test/chatContextManager.test.ts +29 -4
  116. package/src/test/dbMcpServerConfigs.test.ts +4 -4
  117. package/src/test/dbSessionFiles.test.ts +16 -16
  118. package/src/test/testTools.ts +8 -3
  119. package/src/test/tools.test.ts +30 -5
  120. package/src/tool/agentChat.ts +12 -3
  121. package/src/tool/chatMain.ts +33 -6
  122. package/src/tool/commandPrompt.ts +2 -2
  123. package/src/tool/files.ts +1 -3
  124. package/dist/agent/src/agent/tools.js +0 -44
  125. package/src/agent/tools.ts +0 -57
  126. /package/dist/agent/src/{chat/utils → agent/tools/contentExtractors}/htmlToText.js +0 -0
  127. /package/src/{chat/utils → agent/tools/contentExtractors}/htmlToText.ts +0 -0
@@ -1,6 +1,7 @@
1
1
  import OpenAI from "openai";
2
2
  import { strict as assert } from "assert";
3
3
  import { Tool } from "@modelcontextprotocol/sdk/types.js";
4
+ import { v4 as uuidv4 } from "uuid";
4
5
 
5
6
  import {
6
7
  AgentPreferences,
@@ -23,6 +24,7 @@ import { McpServerInfo } from "../../agent/mcpServerManager";
23
24
  import { IAgentEventHandler } from "../../agent/iAgentEventHandler";
24
25
  import { createSpecializedLLM } from "../../agent/agentUtils";
25
26
  import { IPlatform } from "../../agent/iplatform";
27
+ import { ITitleGenerator, createTitleGenerator } from "./titleGenerator";
26
28
 
27
29
  import type {
28
30
  ServerToClient,
@@ -47,6 +49,8 @@ import type {
47
49
  ClientSessionMessageBase,
48
50
  ServerAgentPaused,
49
51
  ClientGetMcpResource,
52
+ ClientRaceModeResult,
53
+ ServerRaceModeGetResult,
50
54
  } from "../protocol/messages";
51
55
  import { AsyncQueue } from "../utils/asyncQueue";
52
56
  import { MultiAsyncQueue } from "../utils/multiAsyncQueue";
@@ -80,20 +84,25 @@ import {
80
84
  llmUserMessageToUserMessageData,
81
85
  MESSAGE_INDEX_START_VALUE,
82
86
  } from "./conversation";
87
+ import { ChatSessionFileManager } from "./sessionFileManager";
83
88
  import {
84
- ChatSessionFileManager,
85
89
  ISessionFileManager,
86
90
  ISessionFileManagerEventHandler,
87
- } from "./sessionFileManager";
91
+ SessionFileEntry,
92
+ ParsedContent,
93
+ getMimeTypeFromDataUrl,
94
+ } from "../../agent/tools/fileManager";
95
+ import { pdfToText } from "../../agent/tools/contentExtractors/pdfToText";
96
+ import { summarizeDocument } from "../../agent/documentSummarizer";
88
97
  import { IUserConnectionManager } from "./connectionManager";
89
98
  import { getErrorString } from "./errorUtils";
90
99
  import { NODE_PLATFORM } from "../../tool/nodePlatform";
91
100
  import { DbMcpServerConfigs } from "../data/dbMcpServerConfigs";
92
- import { SessionFileEntry } from "../data/dbSessionFileModels";
93
101
  import { ApiKeyManager } from "../data/apiKeyManager";
94
102
  import { DbSessionMessages } from "../data/dbSessionMessages";
95
103
  import { ISessionMessageSender } from "./openSessionMessageSender";
96
104
  import { OpenAIRouterLLM } from "./openAIRouterLLM";
105
+ import { ResponseAwaiter } from "../utils/responseAwaiter";
97
106
 
98
107
  /**
99
108
  * The model to use when the AgentProfile does not specify one.
@@ -108,6 +117,8 @@ export const DEFAULT_NUM_MESSGAES = 500;
108
117
 
109
118
  export const GUEST_TOKEN_PREFIX = "guest";
110
119
 
120
+ export const RACE_MODE_TIMEOUT_MS = 120_000;
121
+
111
122
  const logger = getLogger();
112
123
 
113
124
  type QueuedClientMessage<
@@ -214,22 +225,29 @@ export class ChatSessionAgentEventHandler implements IAgentEventHandler {
214
225
  private readonly sessionUUID: string,
215
226
  private readonly sender: ISessionMessageSender<ServerToClient>,
216
227
  private readonly approvalManager: ToolApprovalManager,
217
- private readonly contextTx: ChatContextTransaction
228
+ private readonly contextTx: ChatContextTransaction,
229
+ private readonly alt?: string
218
230
  ) {}
219
231
 
220
232
  onCompletion(result: AssistantMessageParam): void {
221
- logger.debug(`[OpenSession.onCompletion] : ${JSON.stringify(result)}`);
233
+ logger.debug(
234
+ `[OpenSession.onCompletion] ${this.alt || ""} : ${JSON.stringify(result)}`
235
+ );
222
236
  // Nothing to broadcast. Caller will receive this via onAgentMessage.
223
237
  this.contextTx.processAgentResponse(result);
224
238
  }
225
239
 
226
240
  onImage(image: OpenAI.Chat.Completions.ChatCompletionContentPartImage): void {
227
- logger.debug(`[OpenSession.onImage] : ${image.image_url.url}`);
241
+ logger.debug(
242
+ `[OpenSession.onImage] ${this.alt || ""} : ${image.image_url.url}`
243
+ );
228
244
  throw new Error("[OpenSession.onImage] unimplemented");
229
245
  }
230
246
 
231
247
  onToolCallResult(result: ToolMessageParam): void {
232
- logger.debug(`[onToolCallResult] : ${JSON.stringify(result)}`);
248
+ logger.debug(
249
+ `[onToolCallResult] ${this.alt || ""} : ${JSON.stringify(result)}`
250
+ );
233
251
  const toolCallMessage = this.contextTx.processToolCallResult(result);
234
252
  this.sender.broadcast(toolCallMessage);
235
253
  }
@@ -245,6 +263,7 @@ export class ChatSessionAgentEventHandler implements IAgentEventHandler {
245
263
  type: "tool_call",
246
264
  tool_call: toolCall,
247
265
  session_id: this.sessionUUID,
266
+ ...(this.alt ? { alt: this.alt } : {}),
248
267
  });
249
268
  return true;
250
269
  }
@@ -255,7 +274,8 @@ export class ChatSessionAgentEventHandler implements IAgentEventHandler {
255
274
  const { approved, requested } = await this.approvalManager.getApproval(
256
275
  serverName,
257
276
  tool,
258
- toolCall
277
+ toolCall,
278
+ this.alt
259
279
  );
260
280
 
261
281
  // For now, the frontend uses the tool_call data in the
@@ -267,6 +287,7 @@ export class ChatSessionAgentEventHandler implements IAgentEventHandler {
267
287
  type: "tool_call",
268
288
  tool_call: toolCall,
269
289
  session_id: this.sessionUUID,
290
+ ...(this.alt ? { alt: this.alt } : {}),
270
291
  });
271
292
  }
272
293
 
@@ -275,23 +296,28 @@ export class ChatSessionAgentEventHandler implements IAgentEventHandler {
275
296
 
276
297
  onAgentMessage(msg: string, end: boolean): Promise<void> {
277
298
  logger.debug(
278
- `[OpenSession.onAgentMessage] msg: ${msg}, end: ${String(end)}`
299
+ `[OpenSession.onAgentMessage] ${this.alt || ""} msg: ${msg}, ` +
300
+ `end: ${String(end)}`
279
301
  );
280
302
 
281
303
  // Inform the contextManager and broadcast the ServerAgentMessageChunk
282
304
  const agentMsgChunk = this.contextTx.processAgentMessageChunk(msg, end);
305
+ if (this.alt) {
306
+ agentMsgChunk.alt = this.alt;
307
+ }
283
308
  this.sender.broadcast(agentMsgChunk);
284
309
  return Promise.resolve();
285
310
  }
286
311
 
287
312
  onReasoning(reasoning: string): Promise<void> {
288
313
  return new Promise<void>((r) => {
289
- logger.debug(`[OpenSession.onReasoning]${reasoning}`);
314
+ logger.debug(`[OpenSession.onReasoning] ${this.alt || ""} ${reasoning}`);
290
315
  if (reasoning.length > 0) {
291
316
  this.sender.broadcast({
292
317
  type: "agent_reasoning_chunk",
293
318
  reasoning,
294
319
  session_id: this.sessionUUID,
320
+ ...(this.alt ? { alt: this.alt } : {}),
295
321
  });
296
322
  }
297
323
  r();
@@ -326,11 +352,14 @@ export class OpenSession implements ISessionFileManagerEventHandler {
326
352
  private readonly approvalManager: ToolApprovalManager;
327
353
  private readonly savedAgentProfile: SavedAgentProfile;
328
354
  private readonly sessionFileManager: ISessionFileManager;
355
+ private readonly platform: IPlatform;
356
+ private readonly raceModeAwaiter: ResponseAwaiter<ClientRaceModeResult>;
329
357
  private isPersisted: boolean;
330
358
  private accessToken: string | undefined;
331
359
  private sessionTitle: string;
332
360
  private sessionUpdatedAt: string;
333
361
  private agentPaused: boolean;
362
+ private readonly titleGenerator: ITitleGenerator;
334
363
 
335
364
  private constructor(
336
365
  db: Database,
@@ -344,7 +373,8 @@ export class OpenSession implements ISessionFileManagerEventHandler {
344
373
  contextManager: ChatContextManager,
345
374
  sender: ChatSessionMessageSender,
346
375
  approvalManager: ToolApprovalManager,
347
- fileManager: ISessionFileManager
376
+ fileManager: ISessionFileManager,
377
+ platform: IPlatform
348
378
  ) {
349
379
  this.db = db;
350
380
  this.agent = agent;
@@ -368,10 +398,17 @@ export class OpenSession implements ISessionFileManagerEventHandler {
368
398
  this.approvalManager = approvalManager;
369
399
  this.savedAgentProfile = savedAgentProfile;
370
400
  this.sessionFileManager = fileManager;
401
+ this.platform = platform;
402
+ this.raceModeAwaiter = ResponseAwaiter.init(
403
+ undefined,
404
+ (m: ClientRaceModeResult) => m.message_id,
405
+ RACE_MODE_TIMEOUT_MS
406
+ );
371
407
  this.isPersisted = isPersisted;
372
408
  this.sessionTitle = sessionData.title;
373
409
  this.sessionUpdatedAt = sessionData.updated_at;
374
410
  this.agentPaused = sessionData.agent_paused;
411
+ this.titleGenerator = createTitleGenerator();
375
412
 
376
413
  fileManager.addEventHandler(this);
377
414
  this.updateParticipantsPrompt();
@@ -419,7 +456,8 @@ export class OpenSession implements ISessionFileManagerEventHandler {
419
456
  xmcpUrl,
420
457
  fileManager,
421
458
  platform,
422
- checkpointWriter
459
+ checkpointWriter,
460
+ sender
423
461
  );
424
462
 
425
463
  const openSession = new OpenSession(
@@ -434,7 +472,8 @@ export class OpenSession implements ISessionFileManagerEventHandler {
434
472
  contextManager,
435
473
  sender,
436
474
  toolApprovalManager,
437
- fileManager
475
+ fileManager,
476
+ platform
438
477
  );
439
478
 
440
479
  // Note, MCP servers have not been enabled yet
@@ -582,7 +621,9 @@ export class OpenSession implements ISessionFileManagerEventHandler {
582
621
  });
583
622
 
584
623
  // send conversation history
585
- const conversationMessages = this.contextManager.getConversationMessages();
624
+ const conversationMessages = this.contextManager
625
+ .getConversationMessages()
626
+ .concat(this.userMessageQueue.getEntries());
586
627
  conversationMessages.forEach((message) => {
587
628
  connMgr.sendToConnection(connectionId, message);
588
629
  });
@@ -680,29 +721,28 @@ export class OpenSession implements ISessionFileManagerEventHandler {
680
721
  }
681
722
  };
682
723
 
724
+ let errorMessage: string;
683
725
  if (err instanceof ChatFatalError) {
684
726
  // ChatFatalError in session context should send error to specific user
685
727
  // or broadcast if no specific user context
686
- sendError({
687
- type: "session_error",
688
- message: err.message,
689
- session_id: this.sessionUUID,
690
- });
728
+ errorMessage = err.message;
691
729
  } else if (err instanceof ChatErrorMessage) {
692
- sendError({
693
- type: "session_error",
694
- message: err.message,
695
- session_id: this.sessionUUID,
696
- });
730
+ errorMessage = err.message;
697
731
  } else {
698
- const errString = getErrorString(err);
699
- sendError({
700
- type: "session_error",
701
- message: errString,
702
- session_id: this.sessionUUID,
703
- });
732
+ errorMessage = getErrorString(err);
704
733
  }
705
734
 
735
+ logger.error(
736
+ `[OpenSession.handleError] sessionUUID=${this.sessionUUID} ` +
737
+ `from=${from ?? "broadcast"}: ${errorMessage}`
738
+ );
739
+
740
+ sendError({
741
+ type: "session_error",
742
+ message: errorMessage,
743
+ session_id: this.sessionUUID,
744
+ });
745
+
706
746
  return true;
707
747
  }
708
748
 
@@ -715,6 +755,16 @@ export class OpenSession implements ISessionFileManagerEventHandler {
715
755
  });
716
756
  }
717
757
 
758
+ private broadcastContextUsage(): void {
759
+ const { used, max } = this.contextManager.getContextUsage();
760
+ this.sender.broadcast({
761
+ type: "context_usage",
762
+ session_id: this.sessionUUID,
763
+ used_tokens: used,
764
+ max_tokens: max,
765
+ });
766
+ }
767
+
718
768
  // ISessionFileManagerEventHandler.onFileChanged
719
769
  onFileChanged(entry: SessionFileEntry, new_file: boolean): void {
720
770
  this.sender.broadcast({
@@ -812,6 +862,9 @@ export class OpenSession implements ISessionFileManagerEventHandler {
812
862
  case "msg":
813
863
  broadcastMsg = await this.handleUserMessage(msg, queuedMessage.from);
814
864
  break;
865
+ case "stop":
866
+ this.agent.stop();
867
+ break;
815
868
  case "add_mcp_server":
816
869
  broadcastMsg = await this.handleAddMcpServer(
817
870
  msg.server_name,
@@ -883,6 +936,9 @@ export class OpenSession implements ISessionFileManagerEventHandler {
883
936
  case "get_mcp_resource":
884
937
  void this.handleGetMcpResource(msg, queuedMessage.from);
885
938
  break;
939
+ case "race_mode_result":
940
+ this.raceModeAwaiter.onMessage(msg);
941
+ break;
886
942
  default: {
887
943
  const exhaustive: never = msg; // Error => non-exhaustive switch-case.
888
944
  return exhaustive;
@@ -896,6 +952,10 @@ export class OpenSession implements ISessionFileManagerEventHandler {
896
952
  });
897
953
  } else {
898
954
  this.sender.broadcast(broadcastMsg);
955
+ // Broadcast context usage after user message
956
+ if (broadcastMsg.type === "user_msg") {
957
+ this.broadcastContextUsage();
958
+ }
899
959
  }
900
960
  }
901
961
  } catch (err: unknown) {
@@ -978,16 +1038,14 @@ export class OpenSession implements ISessionFileManagerEventHandler {
978
1038
  // they are available to the LLM once it is restarted.
979
1039
 
980
1040
  const { contextTx } = await this.contextManager.startAgentResponse(msgs);
981
- return this.contextManager.endAgentResponse(contextTx);
1041
+ const result = this.contextManager.endAgentResponse(contextTx);
1042
+ this.broadcastContextUsage();
1043
+ return result;
982
1044
  }
983
1045
 
984
1046
  private async processUserMessagesActive(
985
1047
  msgs: ServerUserMessage[]
986
1048
  ): Promise<SessionMessage[]> {
987
- // TODO: create the contextTx and store all new messages on this. Event
988
- // handler should accept the contextTx and forward messages to it, as well
989
- // as sending the updates.
990
-
991
1049
  // All accumulated messages (DB, Protocol and LLM) should be on the
992
1050
  // specialized contextTx.
993
1051
 
@@ -1017,14 +1075,105 @@ export class OpenSession implements ISessionFileManagerEventHandler {
1017
1075
  // DB. Should we keep them?
1018
1076
  throw new Error(errMsg);
1019
1077
  }
1020
- return this.contextManager.endAgentResponse(contextTx);
1078
+ const result = this.contextManager.endAgentResponse(contextTx);
1079
+ this.broadcastContextUsage();
1080
+ return result;
1081
+ }
1082
+
1083
+ /**
1084
+ * `processUserMessage` logic for race-mode.
1085
+ */
1086
+ private async processUserMessagesRaceMode(
1087
+ msgs: ServerUserMessage[]
1088
+ ): Promise<SessionMessage[]> {
1089
+ // Create a second agent, same skillManager
1090
+
1091
+ const modelB = msgs[0].race_mode;
1092
+ assert(typeof modelB === "string");
1093
+
1094
+ const agentB = await (async () => {
1095
+ try {
1096
+ const llmB = await createLLM(modelB, this.platform);
1097
+ return new AgentEx(this.skillManager, llmB);
1098
+ } catch (e) {
1099
+ logger.warn(
1100
+ "[OpenSession.processUserMessages] error creating race agent: " +
1101
+ String(e)
1102
+ );
1103
+ throw new Error(`error creating race: ${String(e)}`);
1104
+ }
1105
+ })();
1106
+
1107
+ const run = async (
1108
+ alt: string,
1109
+ msgs: ServerUserMessage[]
1110
+ ): Promise<ChatContextTransaction> => {
1111
+ const { contextTx: contextTx, agentFirstChunk: agentFirstChunk } =
1112
+ await this.contextManager.startAgentResponse(msgs.slice());
1113
+ this.sender.broadcast(agentFirstChunk);
1114
+
1115
+ const eventHandler = new ChatSessionAgentEventHandler(
1116
+ this.sessionUUID,
1117
+ this.sender,
1118
+ this.approvalManager,
1119
+ contextTx,
1120
+ alt
1121
+ );
1122
+
1123
+ try {
1124
+ const agent = alt === "B" ? agentB : this.agent;
1125
+ await agent.userMessagesRaw(contextTx, eventHandler);
1126
+ } catch (e) {
1127
+ logger.warn(
1128
+ `[OpenSession.processUserMessages] agent ${alt} error: ${String(e)}`
1129
+ );
1130
+ const errMsg = `error from LLM: ${String(e)}`;
1131
+ contextTx.revertAgentResponse(errMsg);
1132
+ throw new Error(errMsg);
1133
+ }
1134
+
1135
+ return contextTx;
1136
+ };
1137
+
1138
+ const [txA, txB] = await Promise.all([
1139
+ run("A", msgs.slice()),
1140
+ run("B", msgs),
1141
+ ]);
1142
+
1143
+ // Ask which fork to commit
1144
+
1145
+ const answer = await (async () => {
1146
+ const message_id = uuidv4();
1147
+ const answerP = this.raceModeAwaiter.waitForResponse(message_id);
1148
+ const getResultMsg: ServerRaceModeGetResult = {
1149
+ type: "race_mode_get_result",
1150
+ message_id,
1151
+ alts: ["A", "B"],
1152
+ session_id: this.sessionUUID,
1153
+ };
1154
+ this.sender.broadcast(getResultMsg);
1155
+
1156
+ return answerP;
1157
+ })();
1158
+
1159
+ if (answer.result === "A") {
1160
+ return this.contextManager.endAgentResponse(txA);
1161
+ } else {
1162
+ return this.contextManager.endAgentResponse(txB);
1163
+ }
1164
+
1165
+ // UI is responsible for switching model if the user wants to.
1021
1166
  }
1022
1167
 
1023
1168
  private async processUserMessages(msgs: ServerUserMessage[]): Promise<void> {
1169
+ logger.debug(`[processUserMessages] msgs: ${JSON.stringify(msgs)}`);
1170
+
1024
1171
  try {
1025
1172
  const newSessionMessages = this.agentPaused
1026
1173
  ? await this.processUserMessagePaused(msgs)
1027
- : await this.processUserMessagesActive(msgs);
1174
+ : await (msgs[0].race_mode
1175
+ ? this.processUserMessagesRaceMode(msgs)
1176
+ : this.processUserMessagesActive(msgs));
1028
1177
 
1029
1178
  logger.debug(
1030
1179
  "[processUserMessages] newSessionMessages: " +
@@ -1035,6 +1184,10 @@ export class OpenSession implements ISessionFileManagerEventHandler {
1035
1184
  const dbsm = this.db.createTypedClient(DbSessionMessages);
1036
1185
  await dbsm.append(this.sessionUUID, newSessionMessages);
1037
1186
  } catch (e) {
1187
+ logger.error(
1188
+ `[processUserMessages] ERROR session=${this.sessionUUID}: ` +
1189
+ String(e)
1190
+ );
1038
1191
  if (!this.handleError(e)) {
1039
1192
  throw e;
1040
1193
  }
@@ -1049,11 +1202,22 @@ export class OpenSession implements ISessionFileManagerEventHandler {
1049
1202
  // on a queue to be dealt with in another loop. This allows Agent
1050
1203
  // processing of user messages to depend on other messages.
1051
1204
 
1052
- assert(msg);
1053
- assert(from);
1205
+ assert(msg, "undefined user message");
1206
+ assert(from, "undefined user message sender");
1054
1207
 
1055
- // Assign the user message_idx and attempt to enqueue.
1208
+ logger.info(
1209
+ `[handleUserMessage] msg.attachedFiles: ${JSON.stringify(
1210
+ msg.attachedFiles
1211
+ ? msg.attachedFiles.map((f) => ({
1212
+ name: f.name,
1213
+ data_url_length: f.data_url.length,
1214
+ }))
1215
+ : "none"
1216
+ )}`
1217
+ );
1056
1218
 
1219
+ // Assign the user message_idx first so we can check if this is
1220
+ // the first message
1057
1221
  const user = this.sessionParticipants.get(from);
1058
1222
  if (!user) {
1059
1223
  throw new Error(`unrecognized user ${from}`);
@@ -1066,8 +1230,87 @@ export class OpenSession implements ISessionFileManagerEventHandler {
1066
1230
  if (!userMessage) {
1067
1231
  return;
1068
1232
  }
1069
- // Special case for the first message of the session
1070
1233
 
1234
+ // Handle attached files by adding them to session files
1235
+ if (msg.attachedFiles && msg.attachedFiles.length > 0) {
1236
+ const fileCount = String(msg.attachedFiles.length);
1237
+ logger.info(`[handleUserMessage] Processing ${fileCount} attached files`);
1238
+
1239
+ // If this is the first message, set the session title before
1240
+ // creating the session
1241
+ if (
1242
+ userMessage.message_idx === MESSAGE_INDEX_START_VALUE &&
1243
+ !this.isPersisted
1244
+ ) {
1245
+ this.sessionTitle = userMessage.message?.slice(0, 128) || "New Chat";
1246
+ }
1247
+
1248
+ // If the session hasn't been persisted, it must be written to
1249
+ // the DB first
1250
+ if (!this.isPersisted) {
1251
+ await this.createSessionInDB();
1252
+ }
1253
+
1254
+ // Add each attached file to session files
1255
+ for (const file of msg.attachedFiles) {
1256
+ logger.info(
1257
+ `[handleUserMessage] Adding file ${file.name} to session files`
1258
+ );
1259
+
1260
+ // Extract content and generate summary for PDFs
1261
+ let parsed_content: ParsedContent | undefined;
1262
+ let summary: string | undefined;
1263
+
1264
+ const mimeType = getMimeTypeFromDataUrl(file.data_url);
1265
+ if (mimeType === "application/pdf") {
1266
+ try {
1267
+ // Extract base64 data and convert to ArrayBuffer
1268
+ const base64Data = file.data_url.split(",")[1];
1269
+ const binaryString = atob(base64Data);
1270
+ const bytes = new Uint8Array(binaryString.length);
1271
+ for (let i = 0; i < binaryString.length; i++) {
1272
+ bytes[i] = binaryString.charCodeAt(i);
1273
+ }
1274
+ const arrayBuffer = bytes.buffer;
1275
+
1276
+ // Extract text from PDF
1277
+ const extractedText = await pdfToText(arrayBuffer);
1278
+ parsed_content = {
1279
+ version: 1,
1280
+ text: extractedText,
1281
+ };
1282
+
1283
+ // Generate summary from extracted text
1284
+ if (extractedText.trim().length > 0) {
1285
+ summary = await summarizeDocument(extractedText);
1286
+ }
1287
+
1288
+ logger.info(
1289
+ `[handleUserMessage] Extracted ${String(extractedText.length)}` +
1290
+ ` chars from PDF ${file.name}`
1291
+ );
1292
+ } catch (err) {
1293
+ logger.warn(
1294
+ `[handleUserMessage] Failed to extract PDF content: ` +
1295
+ String(err)
1296
+ );
1297
+ }
1298
+ }
1299
+
1300
+ await this.sessionFileManager.putFileContent(
1301
+ file.name,
1302
+ summary,
1303
+ file.data_url,
1304
+ parsed_content
1305
+ );
1306
+ }
1307
+
1308
+ // Remove attachedFiles from the message so they don't get
1309
+ // included in the LLM context
1310
+ msg = { ...msg, attachedFiles: undefined };
1311
+ }
1312
+
1313
+ // Special case for the first message of the session
1071
1314
  if (userMessage.message_idx === MESSAGE_INDEX_START_VALUE) {
1072
1315
  // No need to wait for this to complete before broadcasting.
1073
1316
  await this.onFirstMessage(userMessage);
@@ -1118,9 +1361,11 @@ export class OpenSession implements ISessionFileManagerEventHandler {
1118
1361
  }
1119
1362
 
1120
1363
  private async onFirstMessage(userMsg: ServerUserMessage): Promise<void> {
1121
- // Update title on the class before writing to the DB
1364
+ // Generate title using LLM with automatic fallback
1122
1365
 
1123
- this.sessionTitle = userMsg.message?.slice(0, 128) || "New Chat";
1366
+ this.sessionTitle = await this.titleGenerator.generateTitle(
1367
+ userMsg.message || ""
1368
+ );
1124
1369
 
1125
1370
  // The session may already have been saved (e.g. if the workspace is
1126
1371
  // updated before any messages are sent).
@@ -1626,6 +1871,15 @@ async function loadSessionData(
1626
1871
  };
1627
1872
  }
1628
1873
 
1874
+ async function createLLM(model: string, platform: IPlatform): Promise<ILLM> {
1875
+ let llm = await createSpecializedLLM(model, platform);
1876
+ if (!llm) {
1877
+ llm = new OpenAIRouterLLM(model);
1878
+ }
1879
+ assert(llm);
1880
+ return llm;
1881
+ }
1882
+
1629
1883
  async function createContextAndAgent(
1630
1884
  sessionUUID: string,
1631
1885
  systemPrompt: string,
@@ -1638,22 +1892,38 @@ async function createContextAndAgent(
1638
1892
  xmcpUrl: string,
1639
1893
  fileManager: ChatSessionFileManager,
1640
1894
  platform: IPlatform,
1641
- checkpointWriter: ICheckpointWriter
1895
+ checkpointWriter: ICheckpointWriter,
1896
+ messageSender: ISessionMessageSender<ServerToClient>
1642
1897
  ): Promise<{
1643
1898
  agent: AgentEx;
1644
1899
  skillManager: SkillManager;
1645
1900
  contextManager: ChatContextManager;
1646
1901
  }> {
1902
+ // Create SkillManager
1903
+
1904
+ const xmcpConfig = Configuration.new(ownerApiKey, xmcpUrl, false);
1905
+ const skillManager = await SkillManager.initialize(
1906
+ (url: string, authResultP: Promise<boolean>, displayName: string) => {
1907
+ platform.openUrl(url, authResultP, displayName);
1908
+ },
1909
+ xmcpConfig.backend_url,
1910
+ xmcpConfig.api_key,
1911
+ undefined /* authorizedUrl */
1912
+ );
1913
+
1647
1914
  // Fn to create the llm. One invocation for the compression context, one
1648
1915
  // for the Agent.
1649
- const createLLM = async (): Promise<ILLM> => {
1650
- let llm = await createSpecializedLLM(model, platform);
1651
- if (!llm) {
1652
- llm = new OpenAIRouterLLM(model);
1653
- }
1654
- assert(llm);
1655
- return llm;
1656
- };
1916
+
1917
+ const llm = await createLLM(model, platform);
1918
+ const agent = new AgentEx(skillManager, llm);
1919
+
1920
+ await addDefaultChatTools(
1921
+ agent,
1922
+ ownerData.timezone,
1923
+ platform,
1924
+ fileManager,
1925
+ messageSender
1926
+ );
1657
1927
 
1658
1928
  const contextManager = new ChatContextManager(
1659
1929
  systemPrompt,
@@ -1663,7 +1933,7 @@ async function createContextAndAgent(
1663
1933
  sessionCheckpoint,
1664
1934
  checkpointWriter,
1665
1935
  fileManager,
1666
- await createLLM()
1936
+ await createLLM(model, platform)
1667
1937
  );
1668
1938
  if (workspace) {
1669
1939
  contextManager.setWorkspace(
@@ -1671,19 +1941,5 @@ async function createContextAndAgent(
1671
1941
  );
1672
1942
  }
1673
1943
 
1674
- const xmcpConfig = Configuration.new(ownerApiKey, xmcpUrl, false);
1675
- const skillManager = await SkillManager.initialize(
1676
- (url: string, authResultP: Promise<boolean>, displayName: string) => {
1677
- platform.openUrl(url, authResultP, displayName);
1678
- },
1679
- xmcpConfig.backend_url,
1680
- xmcpConfig.api_key,
1681
- undefined /* authorizedUrl */
1682
- );
1683
- const llm = await createLLM();
1684
- const agent = new AgentEx(skillManager, llm);
1685
-
1686
- await addDefaultChatTools(agent, ownerData.timezone, platform, fileManager);
1687
-
1688
1944
  return { agent, skillManager, contextManager };
1689
1945
  }