@xalia/agent 0.6.8 → 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 (152) hide show
  1. package/.env.development +6 -0
  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 +169 -87
  6. package/dist/agent/src/agent/agentUtils.js +24 -18
  7. package/dist/agent/src/agent/compressingContextManager.js +10 -14
  8. package/dist/agent/src/agent/context.js +101 -127
  9. package/dist/agent/src/agent/contextWithWorkspace.js +133 -0
  10. package/dist/agent/src/agent/documentSummarizer.js +126 -0
  11. package/dist/agent/src/agent/dummyLLM.js +25 -22
  12. package/dist/agent/src/agent/imageGenLLM.js +22 -25
  13. package/dist/agent/src/agent/imageGenerator.js +2 -10
  14. package/dist/agent/src/agent/llm.js +1 -1
  15. package/dist/agent/src/agent/openAILLM.js +15 -12
  16. package/dist/agent/src/agent/openAILLMStreaming.js +73 -39
  17. package/dist/agent/src/agent/repeatLLM.js +16 -7
  18. package/dist/agent/src/agent/sudoMcpServerManager.js +21 -9
  19. package/dist/agent/src/agent/tokenCounter.js +390 -0
  20. package/dist/agent/src/agent/tokenCounter.test.js +206 -0
  21. package/dist/agent/src/agent/toolSettings.js +17 -0
  22. package/dist/agent/src/agent/tools/calculatorTool.js +45 -0
  23. package/dist/agent/src/agent/tools/contentExtractors/pdfToText.js +55 -0
  24. package/dist/agent/src/agent/tools/datetimeTool.js +38 -0
  25. package/dist/agent/src/agent/tools/fileManager/fileManagerTool.js +156 -0
  26. package/dist/agent/src/agent/tools/fileManager/index.js +31 -0
  27. package/dist/agent/src/agent/tools/fileManager/memoryFileManager.js +102 -0
  28. package/dist/agent/src/{chat/data → agent/tools/fileManager}/mimeTypes.js +3 -1
  29. package/dist/agent/src/agent/tools/fileManager/prompt.js +33 -0
  30. package/dist/agent/src/{chat/data/dbSessionFileModels.js → agent/tools/fileManager/types.js} +7 -0
  31. package/dist/agent/src/agent/tools/index.js +64 -0
  32. package/dist/agent/src/agent/tools/openUrlTool.js +57 -0
  33. package/dist/agent/src/agent/tools/renderTool.js +89 -0
  34. package/dist/agent/src/agent/tools/utils.js +61 -0
  35. package/dist/agent/src/{chat/utils/search.js → agent/tools/webSearch.js} +1 -2
  36. package/dist/agent/src/agent/tools/webSearchTool.js +40 -0
  37. package/dist/agent/src/chat/client/chatClient.js +63 -2
  38. package/dist/agent/src/chat/client/connection.js +6 -1
  39. package/dist/agent/src/chat/client/index.js +4 -1
  40. package/dist/agent/src/chat/client/sessionClient.js +28 -9
  41. package/dist/agent/src/chat/constants.js +8 -0
  42. package/dist/agent/src/chat/data/dbSessionFiles.js +11 -6
  43. package/dist/agent/src/chat/data/dbSessionMessages.js +11 -0
  44. package/dist/agent/src/chat/protocol/messages.js +9 -0
  45. package/dist/agent/src/chat/server/chatContextManager.js +186 -156
  46. package/dist/agent/src/chat/server/conversation.js +3 -0
  47. package/dist/agent/src/chat/server/imageGeneratorTools.js +39 -16
  48. package/dist/agent/src/chat/server/openAIRouterLLM.js +111 -0
  49. package/dist/agent/src/chat/server/openSession.js +253 -91
  50. package/dist/agent/src/chat/server/promptRefiner.js +86 -0
  51. package/dist/agent/src/chat/server/server.js +10 -2
  52. package/dist/agent/src/chat/server/sessionFileManager.js +22 -221
  53. package/dist/agent/src/chat/server/sessionRegistry.js +152 -6
  54. package/dist/agent/src/chat/server/sessionRegistry.test.js +1 -1
  55. package/dist/agent/src/chat/server/titleGenerator.js +112 -0
  56. package/dist/agent/src/chat/server/titleGenerator.test.js +113 -0
  57. package/dist/agent/src/chat/server/tools.js +64 -253
  58. package/dist/agent/src/chat/utils/approvalManager.js +6 -3
  59. package/dist/agent/src/chat/utils/multiAsyncQueue.js +3 -0
  60. package/dist/agent/src/test/agent.test.js +16 -17
  61. package/dist/agent/src/test/chatContextManager.test.js +44 -30
  62. package/dist/agent/src/test/clientServerConnection.test.js +1 -2
  63. package/dist/agent/src/test/compressingContextManager.test.js +22 -36
  64. package/dist/agent/src/test/context.test.js +55 -17
  65. package/dist/agent/src/test/contextTestTools.js +87 -0
  66. package/dist/agent/src/test/dbMcpServerConfigs.test.js +4 -4
  67. package/dist/agent/src/test/dbSessionFiles.test.js +17 -17
  68. package/dist/agent/src/test/testTools.js +6 -1
  69. package/dist/agent/src/test/tools.test.js +27 -9
  70. package/dist/agent/src/tool/agentChat.js +5 -2
  71. package/dist/agent/src/tool/chatMain.js +56 -15
  72. package/dist/agent/src/tool/commandPrompt.js +2 -2
  73. package/dist/agent/src/tool/files.js +7 -8
  74. package/package.json +4 -1
  75. package/scripts/test_chat +195 -173
  76. package/src/agent/agent.ts +257 -137
  77. package/src/agent/agentUtils.ts +32 -20
  78. package/src/agent/compressingContextManager.ts +13 -44
  79. package/src/agent/context.ts +165 -159
  80. package/src/agent/contextWithWorkspace.ts +162 -0
  81. package/src/agent/documentSummarizer.ts +157 -0
  82. package/src/agent/dummyLLM.ts +27 -23
  83. package/src/agent/imageGenLLM.ts +28 -32
  84. package/src/agent/imageGenerator.ts +3 -18
  85. package/src/agent/llm.ts +2 -2
  86. package/src/agent/openAILLM.ts +17 -13
  87. package/src/agent/openAILLMStreaming.ts +99 -43
  88. package/src/agent/repeatLLM.ts +19 -7
  89. package/src/agent/sudoMcpServerManager.ts +41 -20
  90. package/src/agent/test_data/harrypotter.txt +6065 -0
  91. package/src/agent/tokenCounter.test.ts +243 -0
  92. package/src/agent/tokenCounter.ts +483 -0
  93. package/src/agent/toolSettings.ts +24 -0
  94. package/src/agent/tools/calculatorTool.ts +50 -0
  95. package/src/agent/tools/contentExtractors/pdfToText.ts +60 -0
  96. package/src/agent/tools/datetimeTool.ts +41 -0
  97. package/src/agent/tools/fileManager/fileManagerTool.ts +199 -0
  98. package/src/agent/tools/fileManager/index.ts +50 -0
  99. package/src/agent/tools/fileManager/memoryFileManager.ts +120 -0
  100. package/src/{chat/data → agent/tools/fileManager}/mimeTypes.ts +3 -1
  101. package/src/agent/tools/fileManager/prompt.ts +38 -0
  102. package/src/{chat/data/dbSessionFileModels.ts → agent/tools/fileManager/types.ts} +76 -0
  103. package/src/agent/tools/index.ts +49 -0
  104. package/src/agent/tools/openUrlTool.ts +62 -0
  105. package/src/agent/tools/renderTool.ts +92 -0
  106. package/src/agent/tools/utils.ts +74 -0
  107. package/src/{chat/utils/search.ts → agent/tools/webSearch.ts} +0 -1
  108. package/src/agent/tools/webSearchTool.ts +44 -0
  109. package/src/chat/client/chatClient.ts +92 -3
  110. package/src/chat/client/connection.ts +11 -1
  111. package/src/chat/client/index.ts +3 -0
  112. package/src/chat/client/sessionClient.ts +40 -11
  113. package/src/chat/client/sessionFiles.ts +1 -1
  114. package/src/chat/constants.ts +6 -0
  115. package/src/chat/data/dataModels.ts +12 -0
  116. package/src/chat/data/dbSessionFiles.ts +12 -4
  117. package/src/chat/data/dbSessionMessages.ts +34 -0
  118. package/src/chat/protocol/messages.ts +94 -14
  119. package/src/chat/server/chatContextManager.ts +255 -221
  120. package/src/chat/server/connectionManager.ts +1 -1
  121. package/src/chat/server/conversation.ts +3 -0
  122. package/src/chat/server/imageGeneratorTools.ts +62 -30
  123. package/src/chat/server/openAIRouterLLM.ts +168 -0
  124. package/src/chat/server/openSession.ts +381 -138
  125. package/src/chat/server/promptRefiner.ts +106 -0
  126. package/src/chat/server/server.ts +9 -2
  127. package/src/chat/server/sessionFileManager.ts +35 -306
  128. package/src/chat/server/sessionRegistry.test.ts +0 -1
  129. package/src/chat/server/sessionRegistry.ts +228 -4
  130. package/src/chat/server/titleGenerator.test.ts +103 -0
  131. package/src/chat/server/titleGenerator.ts +143 -0
  132. package/src/chat/server/tools.ts +92 -281
  133. package/src/chat/utils/approvalManager.ts +9 -3
  134. package/src/chat/utils/multiAsyncQueue.ts +4 -0
  135. package/src/test/agent.test.ts +25 -30
  136. package/src/test/chatContextManager.test.ts +68 -38
  137. package/src/test/clientServerConnection.test.ts +0 -2
  138. package/src/test/compressingContextManager.test.ts +29 -34
  139. package/src/test/context.test.ts +59 -15
  140. package/src/test/contextTestTools.ts +95 -0
  141. package/src/test/dbMcpServerConfigs.test.ts +4 -4
  142. package/src/test/dbSessionFiles.test.ts +16 -16
  143. package/src/test/testTools.ts +8 -3
  144. package/src/test/tools.test.ts +30 -5
  145. package/src/tool/agentChat.ts +12 -3
  146. package/src/tool/chatMain.ts +59 -18
  147. package/src/tool/commandPrompt.ts +2 -2
  148. package/src/tool/files.ts +1 -3
  149. package/dist/agent/src/agent/tools.js +0 -44
  150. package/src/agent/tools.ts +0 -57
  151. /package/dist/agent/src/{chat/utils → agent/tools/contentExtractors}/htmlToText.js +0 -0
  152. /package/src/{chat/utils → agent/tools/contentExtractors}/htmlToText.ts +0 -0
@@ -2,14 +2,14 @@ import { strict as assert } from "assert";
2
2
 
3
3
  import { getLogger } from "@xalia/xmcp/sdk";
4
4
 
5
- import { createUserMessage } from "../../agent/agent";
6
5
  import {
7
6
  AssistantMessageParam,
7
+ ILLM,
8
8
  MessageParam,
9
9
  ToolMessageParam,
10
10
  UserMessageParam,
11
11
  } from "../../agent/llm";
12
- import { IContextManager } from "../../agent/context";
12
+ import { IContextTransaction } from "../../agent/context";
13
13
  import {
14
14
  CompressingContextManager,
15
15
  createCheckpointMessage,
@@ -34,11 +34,14 @@ import {
34
34
  ISessionFileManager,
35
35
  ISessionFileManagerEventHandler,
36
36
  createSessionFilesManagerPrompt,
37
- } from "./sessionFileManager";
38
- import {
39
37
  SessionFileDescriptor,
40
38
  SessionFileEntry,
41
- } from "../data/dbSessionFileModels";
39
+ } from "../../agent/tools/fileManager";
40
+ // eslint-disable-next-line max-len
41
+ import { ContextTransactionWithWorkspace } from "../../agent/contextWithWorkspace";
42
+ import { getErrorString } from "./errorUtils";
43
+ import { createUserMessage } from "../../agent/agent";
44
+ import { TokenCounter } from "../../agent/tokenCounter";
42
45
 
43
46
  const logger = getLogger();
44
47
 
@@ -57,6 +60,144 @@ export interface ICheckpointWriter {
57
60
  writeCheckpoint(checkpoint: SessionCheckpoint): Promise<void>;
58
61
  }
59
62
 
63
+ export class ChatContextTransaction implements IContextTransaction {
64
+ private readonly baseTx: ContextTransactionWithWorkspace;
65
+ private readonly sessionUUID: string;
66
+ /// Index of final message in the committed context. If this has changed
67
+ /// before we try to commit this tx, the commit will fail.
68
+ private readonly baseMsgIdx: number | undefined;
69
+ private readonly startingLLMContextLength: number;
70
+ private readonly pendingMessages: ConversationMessage[];
71
+ private curAgentMsgIdx: number;
72
+
73
+ constructor(
74
+ baseTx: ContextTransactionWithWorkspace,
75
+ sessionUUID: string,
76
+ baseMsgIdx: number | undefined,
77
+ pendingUserMessages: ServerUserMessage[],
78
+ curAgentMsgIdx: number
79
+ ) {
80
+ assert(typeof curAgentMsgIdx !== "undefined");
81
+
82
+ this.sessionUUID = sessionUUID;
83
+ this.baseTx = baseTx;
84
+ this.baseMsgIdx = baseMsgIdx;
85
+ this.startingLLMContextLength = baseTx.getLLMContextLength();
86
+ this.pendingMessages = pendingUserMessages;
87
+ this.curAgentMsgIdx = curAgentMsgIdx;
88
+ }
89
+
90
+ // IContextTransaction.addMessages
91
+ addMessages(messages: MessageParam[]): number {
92
+ return this.baseTx.addMessages(messages);
93
+ }
94
+
95
+ // IContextTransaction.addMessage
96
+ addMessage(message: MessageParam): number {
97
+ return this.baseTx.addMessage(message);
98
+ }
99
+
100
+ // IContextTransaction.getMessage
101
+ getMessage(handle: number): MessageParam {
102
+ return this.baseTx.getMessage(handle);
103
+ }
104
+
105
+ // IContextTransaction.getLLMContext
106
+ getLLMContext(): MessageParam[] {
107
+ return this.baseTx.getLLMContext();
108
+ }
109
+
110
+ // IContextTransaction.getLLMContextLength
111
+ getLLMContextLength(): number {
112
+ return this.baseTx.getLLMContextLength();
113
+ }
114
+
115
+ getPending(): ConversationMessage[] {
116
+ return this.pendingMessages;
117
+ }
118
+
119
+ baseMessageIdx(): number | undefined {
120
+ return this.baseMsgIdx;
121
+ }
122
+
123
+ getBaseTx(): ContextTransactionWithWorkspace {
124
+ return this.baseTx;
125
+ }
126
+
127
+ /**
128
+ * Process a FULL Agent message (not chunks from stream). No message is
129
+ * required for broadcast as the calling code is expected to broadcast this
130
+ * as chunks.
131
+ */
132
+ processAgentResponse(result: AssistantMessageParam) {
133
+ // Insert this (full) agent response into the list of agent messages
134
+ const msg: ServerAgentMessage = {
135
+ type: "agent_msg",
136
+ session_id: this.sessionUUID,
137
+ message_idx: this.getNextMessageSubIdx(),
138
+ message: result,
139
+ };
140
+ this.pendingMessages.push(msg);
141
+ }
142
+
143
+ processAgentMessageChunk(msg: string, end: boolean): ServerAgentMessageChunk {
144
+ const message: ServerAgentMessageChunk = {
145
+ type: "agent_msg_chunk",
146
+ session_id: this.sessionUUID,
147
+ message_idx: this.getCurrentAgentMessageIdx(),
148
+ message: msg,
149
+ end,
150
+ };
151
+ return message;
152
+ }
153
+
154
+ processToolCallResult(result: ToolMessageParam): ServerToolCallResult {
155
+ // Allocate the sub-index for this tool call result. It should not
156
+ // have been used already.
157
+
158
+ const message_idx = this.getNextMessageSubIdx();
159
+ const numPending = this.pendingMessages.length;
160
+ assert(numPending > 0);
161
+ assert(this.pendingMessages[numPending - 1].message_idx < message_idx);
162
+
163
+ const msg: ServerToolCallResult = {
164
+ type: "tool_call_result",
165
+ session_id: this.sessionUUID,
166
+ message_idx,
167
+ result,
168
+ };
169
+ this.pendingMessages.push(msg);
170
+ return msg;
171
+ }
172
+
173
+ revertAgentResponse(errMsg: string): void {
174
+ logger.warn(`[ChatContextManager.revertAgentResponse] error: ${errMsg}`);
175
+
176
+ // Remove all messages since the user messages were placed on.
177
+
178
+ while (this.baseTx.getLLMContextLength() > this.startingLLMContextLength) {
179
+ const last = this.baseTx.popMessage();
180
+ assert(last);
181
+ }
182
+ }
183
+
184
+ newMessages(): MessageParam[] {
185
+ return this.baseTx.newMessages();
186
+ }
187
+
188
+ private getNextMessageSubIdx(): number {
189
+ const idx = this.curAgentMsgIdx;
190
+ this.curAgentMsgIdx += MESSAGE_INDEX_SUB_INCREMENT;
191
+ return idx;
192
+ }
193
+
194
+ /// Get the current index to use for streaming Agent chunks
195
+ private getCurrentAgentMessageIdx(): number {
196
+ assert(typeof this.curAgentMsgIdx !== "undefined");
197
+ return this.curAgentMsgIdx;
198
+ }
199
+ }
200
+
60
201
  /**
61
202
  * A context manager for Agents interacting with the (potentially multi-user)
62
203
  * chat conversations.
@@ -68,40 +209,35 @@ export interface ICheckpointWriter {
68
209
  * - Maintain pending user messages, agent loop messages and messages to be
69
210
  * committed to the DB
70
211
  */
71
- export class ChatContextManager
72
- implements IContextManager, ISessionFileManagerEventHandler
73
- {
212
+ export class ChatContextManager implements ISessionFileManagerEventHandler {
74
213
  // Including any pending user messages
75
- private sessionUUID: string;
76
- private conversationMessages: ConversationMessage[];
77
- private pendingUserMessages: ServerUserMessage[];
78
- private llmContext: CompressingContextManager;
79
- private nextMessageIdx: number;
214
+ private readonly sessionUUID: string;
215
+ private readonly conversationMessages: ConversationMessage[];
216
+ private readonly llmContext: CompressingContextManager;
80
217
 
81
- // State while processing agent messages
82
- private startingLLMContextLength: number | undefined;
83
- private curAgentMsgIdx: number | undefined; // active agent message
84
- private pendingMessages: ConversationMessage[] | undefined;
218
+ private nextMessageIdx: number;
85
219
 
86
220
  // Compression state
87
- private checkpointWriter: ICheckpointWriter;
221
+ private readonly checkpointWriter: ICheckpointWriter;
88
222
  private pendingCompression: boolean;
89
223
 
90
224
  // FileManager
91
- private fileManager: ISessionFileManager;
225
+ private readonly fileManager: ISessionFileManager;
92
226
  private fileManagerDescriptionsDirty: boolean;
93
227
 
228
+ // LLM and token counting
229
+ private readonly llm: ILLM;
230
+ private tokenCounter: TokenCounter;
231
+
94
232
  constructor(
95
233
  systemPrompt: string,
96
234
  sessionMessages: SessionMessage[],
97
235
  sessionUUID: string,
98
236
  defaultUserName: string,
99
237
  checkpoint: SessionCheckpoint | undefined = undefined,
100
- compressionAgentUrl: string,
101
- compressionAgentModel: string,
102
- compressionAgentApiKey: string,
103
238
  checkpointWriter: ICheckpointWriter,
104
- fileManager: ISessionFileManager
239
+ fileManager: ISessionFileManager,
240
+ llm: ILLM
105
241
  ) {
106
242
  const nextMessageIdx = sessionMessagesToNextIndex(sessionMessages);
107
243
  const { messages: llmMessages } = resolveConversationWithCheckpoint(
@@ -113,7 +249,7 @@ export class ChatContextManager
113
249
  `[ChatContextManager]: llm messages: ${JSON.stringify(llmMessages)}`
114
250
  );
115
251
 
116
- // Insert a system message placeholder into the context
252
+ const getLLM = () => Promise.resolve(llm);
117
253
 
118
254
  this.sessionUUID = sessionUUID;
119
255
  this.conversationMessages = sessionMessagesToConversationMessages(
@@ -121,40 +257,25 @@ export class ChatContextManager
121
257
  defaultUserName,
122
258
  sessionUUID
123
259
  );
124
- this.pendingUserMessages = [];
125
260
  this.llmContext = new CompressingContextManager(
126
261
  systemPrompt,
127
262
  llmMessages,
128
- compressionAgentUrl,
129
- compressionAgentModel,
130
- compressionAgentApiKey
263
+ getLLM
131
264
  );
132
265
  this.nextMessageIdx = nextMessageIdx;
133
- this.startingLLMContextLength = undefined;
134
- this.curAgentMsgIdx = undefined;
135
- this.pendingMessages = undefined;
136
266
  this.pendingCompression = false;
137
267
  this.checkpointWriter = checkpointWriter;
138
268
  this.fileManager = fileManager;
139
269
  fileManager.addEventHandler(this);
140
270
  this.fileManagerDescriptionsDirty = true;
141
- }
142
-
143
- // IContextManager.addMessages
144
- addMessages(messages: MessageParam[]): void {
145
- this.llmContext.addMessages(messages);
146
- }
147
-
148
- // IContextManager.addMessage
149
- addMessage(message: MessageParam): void {
150
- this.llmContext.addMessage(message);
271
+ this.llm = llm;
272
+ this.tokenCounter = new TokenCounter(llm.getModel());
151
273
  }
152
274
 
153
275
  // IContextManager.getLLMContext
154
276
  getLLMContext(): MessageParam[] {
155
277
  if (this.fileManagerDescriptionsDirty) {
156
278
  const prompt = createSessionFilesManagerPrompt(this.fileManager);
157
- logger.debug(`[ChatContextManager] filemanager prompt:\n${prompt}`);
158
279
  this.llmContext.setPromptFragment("file_manager", prompt);
159
280
  this.fileManagerDescriptionsDirty = false;
160
281
  }
@@ -166,6 +287,7 @@ export class ChatContextManager
166
287
  getAgentPrompt(): string {
167
288
  return this.llmContext.getAgentPrompt();
168
289
  }
290
+
169
291
  // IContextManager.setAgentPrompt
170
292
  setAgentPrompt(prompt: string): void {
171
293
  this.llmContext.setAgentPrompt(prompt);
@@ -205,7 +327,22 @@ export class ChatContextManager
205
327
 
206
328
  // Get the conversation (to send to clients)
207
329
  getConversationMessages(): ConversationMessage[] {
208
- return this.conversationMessages.concat(this.pendingUserMessages);
330
+ return this.conversationMessages;
331
+ }
332
+
333
+ // Get current context usage (tokens)
334
+ getContextUsage(): { used: number; max: number } {
335
+ // Update tokenCounter if model changed
336
+ const currentModel = this.llm.getModel();
337
+ if (this.tokenCounter.getModel() !== currentModel) {
338
+ this.tokenCounter.free();
339
+ this.tokenCounter = new TokenCounter(currentModel);
340
+ }
341
+
342
+ const messages = this.getLLMContext();
343
+ const used = this.tokenCounter.countMessagesTokens(messages);
344
+ const max = this.tokenCounter.getContextWindow();
345
+ return { used, max };
209
346
  }
210
347
 
211
348
  processUserMessage(
@@ -217,7 +354,11 @@ export class ChatContextManager
217
354
  // pass in our generated messages back into `startAgentResponse`.
218
355
 
219
356
  // Filter out null messages immediately.
220
- if (!msg.imageB64 && !msg.message) {
357
+ if (
358
+ !msg.imageB64 &&
359
+ !msg.message &&
360
+ (!msg.attachedFiles || msg.attachedFiles.length === 0)
361
+ ) {
221
362
  return undefined;
222
363
  }
223
364
 
@@ -233,8 +374,13 @@ export class ChatContextManager
233
374
  if (msg.imageB64) {
234
375
  userMessage.imageB64 = msg.imageB64;
235
376
  }
377
+ if (msg.attachedFiles) {
378
+ userMessage.attachedFiles = msg.attachedFiles;
379
+ }
380
+ if (msg.race_mode) {
381
+ userMessage.race_mode = msg.race_mode;
382
+ }
236
383
 
237
- this.pendingUserMessages.push(userMessage);
238
384
  return userMessage;
239
385
  }
240
386
 
@@ -247,43 +393,25 @@ export class ChatContextManager
247
393
  // class manage the interaction with the agent and ensure this process only
248
394
  // happens one-at-a-time.
249
395
 
250
- startAgentResponse(msgs: ServerUserMessage[]): {
251
- llmUserMessages: UserMessageParam[];
396
+ async startAgentResponse(msgs: ServerUserMessage[]): Promise<{
397
+ contextTx: ChatContextTransaction;
252
398
  agentFirstChunk: ServerAgentMessageChunk;
253
- } {
254
- // Sanity check the state
255
-
256
- assert(
257
- typeof this.startingLLMContextLength === "undefined",
258
- "already processing"
259
- );
260
- assert(typeof this.pendingMessages === "undefined", "already processing");
261
- assert(typeof this.curAgentMsgIdx === "undefined", "already processing");
262
-
263
- // Sanity check the messages
264
-
265
- const numMessages = this.pendingUserMessages.length;
266
- assert(numMessages > 0);
267
- assert(msgs.length === this.pendingUserMessages.length);
268
- assert(msgs[0].message_idx === this.pendingUserMessages[0].message_idx);
269
- assert(
270
- msgs[numMessages - 1].message_idx ===
271
- this.pendingUserMessages[numMessages - 1].message_idx
272
- );
399
+ }> {
400
+ // Sanity check the state - the incoming user messages should match the
401
+ // pending user messages.
273
402
 
403
+ const numMessages = msgs.length;
404
+ assert(numMessages > 0, "no messages");
274
405
  // Collect the pending user messages and allocate a starting index for
275
406
  // agent messages and tool calls.
276
407
 
277
- const pendingUserMessages = this.pendingUserMessages;
278
- this.pendingUserMessages = [];
279
- this.startingLLMContextLength = this.llmContext.getCommittedLength();
280
- this.curAgentMsgIdx = this.getNextMessageIdx();
281
- this.pendingMessages = pendingUserMessages as ConversationMessage[];
408
+ const baseMsgIdx = this.lastCommittedMessageIdx();
409
+ const curAgentMsgIdx = this.getNextMessageIdx();
282
410
 
283
411
  // Compute the new llm messages
284
412
 
285
413
  const llmUserMessages: UserMessageParam[] = [];
286
- for (const msg of pendingUserMessages) {
414
+ for (const msg of msgs) {
287
415
  const userMsg = createUserMessage(
288
416
  msg.message,
289
417
  msg.imageB64,
@@ -294,49 +422,60 @@ export class ChatContextManager
294
422
  }
295
423
  }
296
424
 
297
- return {
298
- llmUserMessages,
299
- agentFirstChunk: {
300
- type: "agent_msg_chunk",
301
- session_id: this.sessionUUID,
302
- message_idx: this.curAgentMsgIdx,
303
- message: "",
304
- end: false,
305
- },
425
+ // Return the context tx and first ServerAgentMessageChunk
426
+
427
+ const agentFirstChunk: ServerAgentMessageChunk = {
428
+ type: "agent_msg_chunk",
429
+ session_id: this.sessionUUID,
430
+ message_idx: curAgentMsgIdx,
431
+ message: "",
432
+ end: false,
306
433
  };
307
- }
308
434
 
309
- endAgentResponse(): SessionMessage[] {
310
- assert(
311
- typeof this.startingLLMContextLength !== "undefined",
312
- "agent response not started (startingLLMContextLength)"
313
- );
314
- assert(
315
- typeof this.pendingMessages !== "undefined",
316
- "agent response not started (pendingMessages)"
317
- );
318
- assert(
319
- typeof this.curAgentMsgIdx !== "undefined",
320
- "agent response not started (curAgentMsgIdx)"
435
+ // Update file manager fragment BEFORE starting the transaction
436
+ if (this.fileManagerDescriptionsDirty) {
437
+ const prompt = createSessionFilesManagerPrompt(this.fileManager);
438
+ this.llmContext.setPromptFragment("file_manager", prompt);
439
+ this.fileManagerDescriptionsDirty = false;
440
+ }
441
+
442
+ const baseTx = await this.llmContext.startTx(llmUserMessages);
443
+ const contextTx = new ChatContextTransaction(
444
+ baseTx,
445
+ this.sessionUUID,
446
+ baseMsgIdx,
447
+ msgs,
448
+ curAgentMsgIdx
321
449
  );
450
+ return { agentFirstChunk, contextTx };
451
+ }
322
452
 
323
- const numPending = this.pendingMessages.length;
453
+ async endAgentResponse(tx: IContextTransaction): Promise<SessionMessage[]> {
454
+ assert(tx instanceof ChatContextTransaction);
455
+ if (tx.baseMessageIdx() !== this.lastCommittedMessageIdx()) {
456
+ throw new Error(
457
+ `Tx stale? tx.baseMessageIdx=${String(tx.baseMessageIdx())}, ` +
458
+ `this.conv: ${JSON.stringify(this.conversationMessages)}`
459
+ );
460
+ }
461
+
462
+ const pending = tx.getPending();
463
+ const numPending = pending.length;
324
464
  assert(numPending > 0, "no pending"); // at least 1 user message
325
465
 
326
466
  // Compute DB messages
327
467
 
328
- const newSessionMessages = chatMessagesToSessionMessages(
329
- this.pendingMessages
330
- );
331
- const newLLMMessages = this.llmContext.getPending();
468
+ const newSessionMessages = chatMessagesToSessionMessages(pending);
469
+ const newLLMMessages = tx.newMessages();
332
470
 
333
471
  const messageListError = (error: string) => {
334
- throw new Error(
335
- `${error}:` +
336
- `\n newSessionMessages: ${JSON.stringify(newSessionMessages)}` +
337
- `\n this.pendingMessages: ${JSON.stringify(this.pendingMessages)}` +
338
- `\n newLLMMessages: ${JSON.stringify(newLLMMessages)}`
339
- );
472
+ const fullError =
473
+ `[endAgentResponse] Message list validation failed - ${error}:` +
474
+ `\n newSessionMessages: ${JSON.stringify(newSessionMessages)}` +
475
+ `\n pending: ${JSON.stringify(pending)}` +
476
+ `\n newLLMMessages: ${JSON.stringify(newLLMMessages)}`;
477
+ logger.error(fullError);
478
+ throw new Error(fullError);
340
479
  };
341
480
 
342
481
  if (newSessionMessages.length !== numPending) {
@@ -353,7 +492,7 @@ export class ChatContextManager
353
492
 
354
493
  for (let i = 0; i < numPending; ++i) {
355
494
  const sMsg = newSessionMessages[i];
356
- const pMsg = this.pendingMessages[i];
495
+ const pMsg = pending[i];
357
496
  const lMsg = newLLMMessages[i];
358
497
 
359
498
  if (sMsg.content.role !== lMsg.role) {
@@ -379,11 +518,8 @@ export class ChatContextManager
379
518
  // Update our internal state and return the SessionMessages to write to
380
519
  // the DB
381
520
 
382
- this.llmContext.commit();
383
- this.conversationMessages.push(...this.pendingMessages);
384
- this.startingLLMContextLength = undefined;
385
- this.pendingMessages = undefined;
386
- this.curAgentMsgIdx = undefined;
521
+ await this.llmContext.commit(tx.getBaseTx());
522
+ this.conversationMessages.push(...pending);
387
523
 
388
524
  // Kick off a compression?
389
525
  this.checkCompression();
@@ -391,98 +527,12 @@ export class ChatContextManager
391
527
  return newSessionMessages;
392
528
  }
393
529
 
394
- /**
395
- * End the Agent message session with an error. Caller should not call
396
- * `endAgentResponse` after calling this function.
397
- *
398
- * This function checks that nothing has been entered into the LLM context,
399
- * and drops any new user messages or responses before the error.
400
- */
401
- revertAgentResponse(errMsg: string): void {
402
- logger.warn(`[ChatContextManager.revertAgentResponse] error: ${errMsg}`);
403
-
404
- assert(typeof this.startingLLMContextLength !== "undefined");
405
- assert(typeof this.pendingMessages !== "undefined");
406
- assert(typeof this.curAgentMsgIdx !== "undefined");
407
-
408
- // Sanity check that no new messages were put into the context (The Agent
409
- // is expected to only call `addMessage(s)` at the end of the Agent loop.
410
- // (Note, we don't check for equality here, just in case the context was
411
- // compressed while the Agent was executing).
412
-
413
- const contextLength = this.llmContext.getCommittedLength();
414
- if (contextLength > this.startingLLMContextLength) {
415
- logger.error(
416
- "[ChatContextManager.revertAgentResponse] llmContext has grown " +
417
- `despite Agent error (${String(contextLength)}, ` +
418
- `${String(this.startingLLMContextLength)})`
419
- );
530
+ private lastCommittedMessageIdx(): number | undefined {
531
+ const numMsgs = this.conversationMessages.length;
532
+ if (numMsgs > 0) {
533
+ return this.conversationMessages[numMsgs - 1].message_idx;
420
534
  }
421
-
422
- // We simply reset the state, dropping any pending messages.
423
-
424
- this.startingLLMContextLength = undefined;
425
- this.pendingMessages = undefined;
426
- this.curAgentMsgIdx = undefined;
427
- }
428
-
429
- processAgentMessage(msg: string, end: boolean): ServerAgentMessageChunk {
430
- assert(typeof this.startingLLMContextLength !== "undefined");
431
- assert(typeof this.pendingMessages !== "undefined");
432
- assert(typeof this.curAgentMsgIdx !== "undefined");
433
-
434
- const message: ServerAgentMessageChunk = {
435
- type: "agent_msg_chunk",
436
- session_id: this.sessionUUID,
437
- message_idx: this.getCurrentAgentMessageIdx(),
438
- message: msg,
439
- end,
440
- };
441
- return message;
442
- }
443
-
444
- /**
445
- * Process a FULL Agent message (not chunks from stream). No message is
446
- * required for broadcast as the calling code is expected to broadcast this
447
- * as chunks.
448
- */
449
- processAgentResponse(result: AssistantMessageParam) {
450
- assert(typeof this.startingLLMContextLength !== "undefined");
451
- assert(typeof this.pendingMessages !== "undefined");
452
- assert(typeof this.curAgentMsgIdx !== "undefined");
453
-
454
- // Insert this (full) agent response into the list of agent messages
455
-
456
- const msg: ServerAgentMessage = {
457
- type: "agent_msg",
458
- session_id: this.sessionUUID,
459
- message_idx: this.getNextMessageSubIdx(),
460
- message: result,
461
- };
462
- this.pendingMessages.push(msg);
463
- }
464
-
465
- processToolCallResult(result: ToolMessageParam): ServerToolCallResult {
466
- assert(typeof this.startingLLMContextLength !== "undefined");
467
- assert(typeof this.pendingMessages !== "undefined");
468
- assert(typeof this.curAgentMsgIdx !== "undefined");
469
-
470
- // Allocate the sub-index for this tool call result. It should not
471
- // have been used already.
472
-
473
- const message_idx = this.getNextMessageSubIdx();
474
- const numPending = this.pendingMessages.length;
475
- assert(numPending > 0);
476
- assert(this.pendingMessages[numPending - 1].message_idx < message_idx);
477
-
478
- const msg: ServerToolCallResult = {
479
- type: "tool_call_result",
480
- session_id: this.sessionUUID,
481
- message_idx,
482
- result,
483
- };
484
- this.pendingMessages.push(msg);
485
- return msg;
535
+ return undefined;
486
536
  }
487
537
 
488
538
  private getNextMessageIdx(): number {
@@ -503,22 +553,6 @@ export class ChatContextManager
503
553
  this.nextMessageIdx = messageIdx;
504
554
  }
505
555
 
506
- /// Get the current index to use for streaming Agent chunks
507
- private getCurrentAgentMessageIdx(): number {
508
- assert(typeof this.pendingMessages !== "undefined");
509
- assert(typeof this.curAgentMsgIdx !== "undefined");
510
- return this.curAgentMsgIdx;
511
- }
512
-
513
- private getNextMessageSubIdx(): number {
514
- assert(typeof this.pendingMessages !== "undefined");
515
- assert(typeof this.curAgentMsgIdx !== "undefined");
516
-
517
- const idx = this.curAgentMsgIdx;
518
- this.curAgentMsgIdx += MESSAGE_INDEX_SUB_INCREMENT;
519
- return idx;
520
- }
521
-
522
556
  private checkCompression(): void {
523
557
  if (this.pendingCompression) {
524
558
  return;
@@ -526,7 +560,7 @@ export class ChatContextManager
526
560
 
527
561
  // TODO: track tokens and use that to trigger compression
528
562
 
529
- const numCommitted = this.llmContext.getCommittedLength();
563
+ const numCommitted = this.llmContext.numMessages();
530
564
  if (numCommitted < COMPRESSION_TRIGGER_NUM_MESSAGES) {
531
565
  return;
532
566
  }
@@ -551,7 +585,7 @@ export class ChatContextManager
551
585
  await this.checkpointWriter.writeCheckpoint(checkpoint);
552
586
  } catch (err: unknown) {
553
587
  logger.warn(
554
- `[runCompression] error during compression: ${JSON.stringify(err)}`
588
+ `[runCompression] error during compression: ${getErrorString(err)}`
555
589
  );
556
590
  } finally {
557
591
  this.pendingCompression = false;
@@ -25,7 +25,7 @@ export interface IUserConnectionManager<ServerMsgT> {
25
25
  * Send message to all active connections of specific users.
26
26
  * Handles user-to-connection routing internally.
27
27
  */
28
- sendToUsers(userIds: Set<string>, message: ServerMsgT): void;
28
+ sendToUsers(userIds: Set<string> | string[], message: ServerMsgT): void;
29
29
 
30
30
  /**
31
31
  * Send message to a specific connection.
@@ -127,6 +127,9 @@ export function userMessageToConversationMessage(
127
127
  if (userMsgData.imageB64) {
128
128
  msg.imageB64 = userMsgData.imageB64;
129
129
  }
130
+ if (userMsgData.attachedFiles) {
131
+ msg.attachedFiles = userMsgData.attachedFiles;
132
+ }
130
133
  return msg;
131
134
  }
132
135