@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
@@ -1,18 +1,123 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ChatContextManager = void 0;
3
+ exports.ChatContextManager = exports.ChatContextTransaction = void 0;
4
4
  exports.resolveConversationWithCheckpoint = resolveConversationWithCheckpoint;
5
5
  const assert_1 = require("assert");
6
6
  const sdk_1 = require("@xalia/xmcp/sdk");
7
- const agent_1 = require("../../agent/agent");
8
7
  const compressingContextManager_1 = require("../../agent/compressingContextManager");
9
8
  const conversation_1 = require("./conversation");
10
- const sessionFileManager_1 = require("./sessionFileManager");
9
+ const fileManager_1 = require("../../agent/tools/fileManager");
10
+ const errorUtils_1 = require("./errorUtils");
11
+ const agent_1 = require("../../agent/agent");
12
+ const tokenCounter_1 = require("../../agent/tokenCounter");
11
13
  const logger = (0, sdk_1.getLogger)();
12
14
  /**
13
15
  * TODO: Until token-tracking is in place.
14
16
  */
15
17
  const COMPRESSION_TRIGGER_NUM_MESSAGES = parseInt(process.env["COMPRESSION_TRIGGER_NUM_MESSAGES"] || "80", 10);
18
+ class ChatContextTransaction {
19
+ constructor(baseTx, sessionUUID, baseMsgIdx, pendingUserMessages, curAgentMsgIdx) {
20
+ (0, assert_1.strict)(typeof curAgentMsgIdx !== "undefined");
21
+ this.sessionUUID = sessionUUID;
22
+ this.baseTx = baseTx;
23
+ this.baseMsgIdx = baseMsgIdx;
24
+ this.startingLLMContextLength = baseTx.getLLMContextLength();
25
+ this.pendingMessages = pendingUserMessages;
26
+ this.curAgentMsgIdx = curAgentMsgIdx;
27
+ }
28
+ // IContextTransaction.addMessages
29
+ addMessages(messages) {
30
+ return this.baseTx.addMessages(messages);
31
+ }
32
+ // IContextTransaction.addMessage
33
+ addMessage(message) {
34
+ return this.baseTx.addMessage(message);
35
+ }
36
+ // IContextTransaction.getMessage
37
+ getMessage(handle) {
38
+ return this.baseTx.getMessage(handle);
39
+ }
40
+ // IContextTransaction.getLLMContext
41
+ getLLMContext() {
42
+ return this.baseTx.getLLMContext();
43
+ }
44
+ // IContextTransaction.getLLMContextLength
45
+ getLLMContextLength() {
46
+ return this.baseTx.getLLMContextLength();
47
+ }
48
+ getPending() {
49
+ return this.pendingMessages;
50
+ }
51
+ baseMessageIdx() {
52
+ return this.baseMsgIdx;
53
+ }
54
+ getBaseTx() {
55
+ return this.baseTx;
56
+ }
57
+ /**
58
+ * Process a FULL Agent message (not chunks from stream). No message is
59
+ * required for broadcast as the calling code is expected to broadcast this
60
+ * as chunks.
61
+ */
62
+ processAgentResponse(result) {
63
+ // Insert this (full) agent response into the list of agent messages
64
+ const msg = {
65
+ type: "agent_msg",
66
+ session_id: this.sessionUUID,
67
+ message_idx: this.getNextMessageSubIdx(),
68
+ message: result,
69
+ };
70
+ this.pendingMessages.push(msg);
71
+ }
72
+ processAgentMessageChunk(msg, end) {
73
+ const message = {
74
+ type: "agent_msg_chunk",
75
+ session_id: this.sessionUUID,
76
+ message_idx: this.getCurrentAgentMessageIdx(),
77
+ message: msg,
78
+ end,
79
+ };
80
+ return message;
81
+ }
82
+ processToolCallResult(result) {
83
+ // Allocate the sub-index for this tool call result. It should not
84
+ // have been used already.
85
+ const message_idx = this.getNextMessageSubIdx();
86
+ const numPending = this.pendingMessages.length;
87
+ (0, assert_1.strict)(numPending > 0);
88
+ (0, assert_1.strict)(this.pendingMessages[numPending - 1].message_idx < message_idx);
89
+ const msg = {
90
+ type: "tool_call_result",
91
+ session_id: this.sessionUUID,
92
+ message_idx,
93
+ result,
94
+ };
95
+ this.pendingMessages.push(msg);
96
+ return msg;
97
+ }
98
+ revertAgentResponse(errMsg) {
99
+ logger.warn(`[ChatContextManager.revertAgentResponse] error: ${errMsg}`);
100
+ // Remove all messages since the user messages were placed on.
101
+ while (this.baseTx.getLLMContextLength() > this.startingLLMContextLength) {
102
+ const last = this.baseTx.popMessage();
103
+ (0, assert_1.strict)(last);
104
+ }
105
+ }
106
+ newMessages() {
107
+ return this.baseTx.newMessages();
108
+ }
109
+ getNextMessageSubIdx() {
110
+ const idx = this.curAgentMsgIdx;
111
+ this.curAgentMsgIdx += conversation_1.MESSAGE_INDEX_SUB_INCREMENT;
112
+ return idx;
113
+ }
114
+ /// Get the current index to use for streaming Agent chunks
115
+ getCurrentAgentMessageIdx() {
116
+ (0, assert_1.strict)(typeof this.curAgentMsgIdx !== "undefined");
117
+ return this.curAgentMsgIdx;
118
+ }
119
+ }
120
+ exports.ChatContextTransaction = ChatContextTransaction;
16
121
  /**
17
122
  * A context manager for Agents interacting with the (potentially multi-user)
18
123
  * chat conversations.
@@ -25,38 +130,27 @@ const COMPRESSION_TRIGGER_NUM_MESSAGES = parseInt(process.env["COMPRESSION_TRIGG
25
130
  * committed to the DB
26
131
  */
27
132
  class ChatContextManager {
28
- constructor(systemPrompt, sessionMessages, sessionUUID, defaultUserName, checkpoint = undefined, compressionAgentUrl, compressionAgentModel, compressionAgentApiKey, checkpointWriter, fileManager) {
133
+ constructor(systemPrompt, sessionMessages, sessionUUID, defaultUserName, checkpoint = undefined, checkpointWriter, fileManager, llm) {
29
134
  const nextMessageIdx = (0, conversation_1.sessionMessagesToNextIndex)(sessionMessages);
30
135
  const { messages: llmMessages } = resolveConversationWithCheckpoint(sessionMessages, checkpoint);
31
136
  logger.debug(`[ChatContextManager]: llm messages: ${JSON.stringify(llmMessages)}`);
32
- // Insert a system message placeholder into the context
137
+ const getLLM = () => Promise.resolve(llm);
33
138
  this.sessionUUID = sessionUUID;
34
139
  this.conversationMessages = (0, conversation_1.sessionMessagesToConversationMessages)(sessionMessages, defaultUserName, sessionUUID);
35
- this.pendingUserMessages = [];
36
- this.llmContext = new compressingContextManager_1.CompressingContextManager(systemPrompt, llmMessages, compressionAgentUrl, compressionAgentModel, compressionAgentApiKey);
140
+ this.llmContext = new compressingContextManager_1.CompressingContextManager(systemPrompt, llmMessages, getLLM);
37
141
  this.nextMessageIdx = nextMessageIdx;
38
- this.startingLLMContextLength = undefined;
39
- this.curAgentMsgIdx = undefined;
40
- this.pendingMessages = undefined;
41
142
  this.pendingCompression = false;
42
143
  this.checkpointWriter = checkpointWriter;
43
144
  this.fileManager = fileManager;
44
145
  fileManager.addEventHandler(this);
45
146
  this.fileManagerDescriptionsDirty = true;
46
- }
47
- // IContextManager.addMessages
48
- addMessages(messages) {
49
- this.llmContext.addMessages(messages);
50
- }
51
- // IContextManager.addMessage
52
- addMessage(message) {
53
- this.llmContext.addMessage(message);
147
+ this.llm = llm;
148
+ this.tokenCounter = new tokenCounter_1.TokenCounter(llm.getModel());
54
149
  }
55
150
  // IContextManager.getLLMContext
56
151
  getLLMContext() {
57
152
  if (this.fileManagerDescriptionsDirty) {
58
- const prompt = (0, sessionFileManager_1.createSessionFilesManagerPrompt)(this.fileManager);
59
- logger.debug(`[ChatContextManager] filemanager prompt:\n${prompt}`);
153
+ const prompt = (0, fileManager_1.createSessionFilesManagerPrompt)(this.fileManager);
60
154
  this.llmContext.setPromptFragment("file_manager", prompt);
61
155
  this.fileManagerDescriptionsDirty = false;
62
156
  }
@@ -97,13 +191,28 @@ class ChatContextManager {
97
191
  }
98
192
  // Get the conversation (to send to clients)
99
193
  getConversationMessages() {
100
- return this.conversationMessages.concat(this.pendingUserMessages);
194
+ return this.conversationMessages;
195
+ }
196
+ // Get current context usage (tokens)
197
+ getContextUsage() {
198
+ // Update tokenCounter if model changed
199
+ const currentModel = this.llm.getModel();
200
+ if (this.tokenCounter.getModel() !== currentModel) {
201
+ this.tokenCounter.free();
202
+ this.tokenCounter = new tokenCounter_1.TokenCounter(currentModel);
203
+ }
204
+ const messages = this.getLLMContext();
205
+ const used = this.tokenCounter.countMessagesTokens(messages);
206
+ const max = this.tokenCounter.getContextWindow();
207
+ return { used, max };
101
208
  }
102
209
  processUserMessage(msg, from_uuid, from_nickname) {
103
210
  // TODO: maintain a queue internally instead of relying on the caller to
104
211
  // pass in our generated messages back into `startAgentResponse`.
105
212
  // Filter out null messages immediately.
106
- if (!msg.imageB64 && !msg.message) {
213
+ if (!msg.imageB64 &&
214
+ !msg.message &&
215
+ (!msg.attachedFiles || msg.attachedFiles.length === 0)) {
107
216
  return undefined;
108
217
  }
109
218
  const message_idx = this.getNextMessageIdx();
@@ -118,7 +227,12 @@ class ChatContextManager {
118
227
  if (msg.imageB64) {
119
228
  userMessage.imageB64 = msg.imageB64;
120
229
  }
121
- this.pendingUserMessages.push(userMessage);
230
+ if (msg.attachedFiles) {
231
+ userMessage.attachedFiles = msg.attachedFiles;
232
+ }
233
+ if (msg.race_mode) {
234
+ userMessage.race_mode = msg.race_mode;
235
+ }
122
236
  return userMessage;
123
237
  }
124
238
  unprocessUserMessage(userMsg) {
@@ -128,58 +242,60 @@ class ChatContextManager {
128
242
  // TODO: Don't take the set of messages. Instead, have OpenSession or this
129
243
  // class manage the interaction with the agent and ensure this process only
130
244
  // happens one-at-a-time.
131
- startAgentResponse(msgs) {
132
- // Sanity check the state
133
- (0, assert_1.strict)(typeof this.startingLLMContextLength === "undefined", "already processing");
134
- (0, assert_1.strict)(typeof this.pendingMessages === "undefined", "already processing");
135
- (0, assert_1.strict)(typeof this.curAgentMsgIdx === "undefined", "already processing");
136
- // Sanity check the messages
137
- const numMessages = this.pendingUserMessages.length;
138
- (0, assert_1.strict)(numMessages > 0);
139
- (0, assert_1.strict)(msgs.length === this.pendingUserMessages.length);
140
- (0, assert_1.strict)(msgs[0].message_idx === this.pendingUserMessages[0].message_idx);
141
- (0, assert_1.strict)(msgs[numMessages - 1].message_idx ===
142
- this.pendingUserMessages[numMessages - 1].message_idx);
245
+ async startAgentResponse(msgs) {
246
+ // Sanity check the state - the incoming user messages should match the
247
+ // pending user messages.
248
+ const numMessages = msgs.length;
249
+ (0, assert_1.strict)(numMessages > 0, "no messages");
143
250
  // Collect the pending user messages and allocate a starting index for
144
251
  // agent messages and tool calls.
145
- const pendingUserMessages = this.pendingUserMessages;
146
- this.pendingUserMessages = [];
147
- this.startingLLMContextLength = this.llmContext.getCommittedLength();
148
- this.curAgentMsgIdx = this.getNextMessageIdx();
149
- this.pendingMessages = pendingUserMessages;
252
+ const baseMsgIdx = this.lastCommittedMessageIdx();
253
+ const curAgentMsgIdx = this.getNextMessageIdx();
150
254
  // Compute the new llm messages
151
255
  const llmUserMessages = [];
152
- for (const msg of pendingUserMessages) {
256
+ for (const msg of msgs) {
153
257
  const userMsg = (0, agent_1.createUserMessage)(msg.message, msg.imageB64, msg.user_uuid);
154
258
  if (userMsg) {
155
259
  llmUserMessages.push(userMsg);
156
260
  }
157
261
  }
158
- return {
159
- llmUserMessages,
160
- agentFirstChunk: {
161
- type: "agent_msg_chunk",
162
- session_id: this.sessionUUID,
163
- message_idx: this.curAgentMsgIdx,
164
- message: "",
165
- end: false,
166
- },
262
+ // Return the context tx and first ServerAgentMessageChunk
263
+ const agentFirstChunk = {
264
+ type: "agent_msg_chunk",
265
+ session_id: this.sessionUUID,
266
+ message_idx: curAgentMsgIdx,
267
+ message: "",
268
+ end: false,
167
269
  };
270
+ // Update file manager fragment BEFORE starting the transaction
271
+ if (this.fileManagerDescriptionsDirty) {
272
+ const prompt = (0, fileManager_1.createSessionFilesManagerPrompt)(this.fileManager);
273
+ this.llmContext.setPromptFragment("file_manager", prompt);
274
+ this.fileManagerDescriptionsDirty = false;
275
+ }
276
+ const baseTx = await this.llmContext.startTx(llmUserMessages);
277
+ const contextTx = new ChatContextTransaction(baseTx, this.sessionUUID, baseMsgIdx, msgs, curAgentMsgIdx);
278
+ return { agentFirstChunk, contextTx };
168
279
  }
169
- endAgentResponse() {
170
- (0, assert_1.strict)(typeof this.startingLLMContextLength !== "undefined", "agent response not started (startingLLMContextLength)");
171
- (0, assert_1.strict)(typeof this.pendingMessages !== "undefined", "agent response not started (pendingMessages)");
172
- (0, assert_1.strict)(typeof this.curAgentMsgIdx !== "undefined", "agent response not started (curAgentMsgIdx)");
173
- const numPending = this.pendingMessages.length;
280
+ async endAgentResponse(tx) {
281
+ (0, assert_1.strict)(tx instanceof ChatContextTransaction);
282
+ if (tx.baseMessageIdx() !== this.lastCommittedMessageIdx()) {
283
+ throw new Error(`Tx stale? tx.baseMessageIdx=${String(tx.baseMessageIdx())}, ` +
284
+ `this.conv: ${JSON.stringify(this.conversationMessages)}`);
285
+ }
286
+ const pending = tx.getPending();
287
+ const numPending = pending.length;
174
288
  (0, assert_1.strict)(numPending > 0, "no pending"); // at least 1 user message
175
289
  // Compute DB messages
176
- const newSessionMessages = (0, conversation_1.chatMessagesToSessionMessages)(this.pendingMessages);
177
- const newLLMMessages = this.llmContext.getPending();
290
+ const newSessionMessages = (0, conversation_1.chatMessagesToSessionMessages)(pending);
291
+ const newLLMMessages = tx.newMessages();
178
292
  const messageListError = (error) => {
179
- throw new Error(`${error}:` +
293
+ const fullError = `[endAgentResponse] Message list validation failed - ${error}:` +
180
294
  `\n newSessionMessages: ${JSON.stringify(newSessionMessages)}` +
181
- `\n this.pendingMessages: ${JSON.stringify(this.pendingMessages)}` +
182
- `\n newLLMMessages: ${JSON.stringify(newLLMMessages)}`);
295
+ `\n pending: ${JSON.stringify(pending)}` +
296
+ `\n newLLMMessages: ${JSON.stringify(newLLMMessages)}`;
297
+ logger.error(fullError);
298
+ throw new Error(fullError);
183
299
  };
184
300
  if (newSessionMessages.length !== numPending) {
185
301
  messageListError("newSessionMessages.length !== numPending");
@@ -193,7 +309,7 @@ class ChatContextManager {
193
309
  // this ensures all representations are aligned.
194
310
  for (let i = 0; i < numPending; ++i) {
195
311
  const sMsg = newSessionMessages[i];
196
- const pMsg = this.pendingMessages[i];
312
+ const pMsg = pending[i];
197
313
  const lMsg = newLLMMessages[i];
198
314
  if (sMsg.content.role !== lMsg.role) {
199
315
  messageListError(`newSessionMessages[${String(i)}].role !== ` +
@@ -210,91 +326,18 @@ class ChatContextManager {
210
326
  }
211
327
  // Update our internal state and return the SessionMessages to write to
212
328
  // the DB
213
- this.llmContext.commit();
214
- this.conversationMessages.push(...this.pendingMessages);
215
- this.startingLLMContextLength = undefined;
216
- this.pendingMessages = undefined;
217
- this.curAgentMsgIdx = undefined;
329
+ await this.llmContext.commit(tx.getBaseTx());
330
+ this.conversationMessages.push(...pending);
218
331
  // Kick off a compression?
219
332
  this.checkCompression();
220
333
  return newSessionMessages;
221
334
  }
222
- /**
223
- * End the Agent message session with an error. Caller should not call
224
- * `endAgentResponse` after calling this function.
225
- *
226
- * This function checks that nothing has been entered into the LLM context,
227
- * and drops any new user messages or responses before the error.
228
- */
229
- revertAgentResponse(errMsg) {
230
- logger.warn(`[ChatContextManager.revertAgentResponse] error: ${errMsg}`);
231
- (0, assert_1.strict)(typeof this.startingLLMContextLength !== "undefined");
232
- (0, assert_1.strict)(typeof this.pendingMessages !== "undefined");
233
- (0, assert_1.strict)(typeof this.curAgentMsgIdx !== "undefined");
234
- // Sanity check that no new messages were put into the context (The Agent
235
- // is expected to only call `addMessage(s)` at the end of the Agent loop.
236
- // (Note, we don't check for equality here, just in case the context was
237
- // compressed while the Agent was executing).
238
- const contextLength = this.llmContext.getCommittedLength();
239
- if (contextLength > this.startingLLMContextLength) {
240
- logger.error("[ChatContextManager.revertAgentResponse] llmContext has grown " +
241
- `despite Agent error (${String(contextLength)}, ` +
242
- `${String(this.startingLLMContextLength)})`);
335
+ lastCommittedMessageIdx() {
336
+ const numMsgs = this.conversationMessages.length;
337
+ if (numMsgs > 0) {
338
+ return this.conversationMessages[numMsgs - 1].message_idx;
243
339
  }
244
- // We simply reset the state, dropping any pending messages.
245
- this.startingLLMContextLength = undefined;
246
- this.pendingMessages = undefined;
247
- this.curAgentMsgIdx = undefined;
248
- }
249
- processAgentMessage(msg, end) {
250
- (0, assert_1.strict)(typeof this.startingLLMContextLength !== "undefined");
251
- (0, assert_1.strict)(typeof this.pendingMessages !== "undefined");
252
- (0, assert_1.strict)(typeof this.curAgentMsgIdx !== "undefined");
253
- const message = {
254
- type: "agent_msg_chunk",
255
- session_id: this.sessionUUID,
256
- message_idx: this.getCurrentAgentMessageIdx(),
257
- message: msg,
258
- end,
259
- };
260
- return message;
261
- }
262
- /**
263
- * Process a FULL Agent message (not chunks from stream). No message is
264
- * required for broadcast as the calling code is expected to broadcast this
265
- * as chunks.
266
- */
267
- processAgentResponse(result) {
268
- (0, assert_1.strict)(typeof this.startingLLMContextLength !== "undefined");
269
- (0, assert_1.strict)(typeof this.pendingMessages !== "undefined");
270
- (0, assert_1.strict)(typeof this.curAgentMsgIdx !== "undefined");
271
- // Insert this (full) agent response into the list of agent messages
272
- const msg = {
273
- type: "agent_msg",
274
- session_id: this.sessionUUID,
275
- message_idx: this.getNextMessageSubIdx(),
276
- message: result,
277
- };
278
- this.pendingMessages.push(msg);
279
- }
280
- processToolCallResult(result) {
281
- (0, assert_1.strict)(typeof this.startingLLMContextLength !== "undefined");
282
- (0, assert_1.strict)(typeof this.pendingMessages !== "undefined");
283
- (0, assert_1.strict)(typeof this.curAgentMsgIdx !== "undefined");
284
- // Allocate the sub-index for this tool call result. It should not
285
- // have been used already.
286
- const message_idx = this.getNextMessageSubIdx();
287
- const numPending = this.pendingMessages.length;
288
- (0, assert_1.strict)(numPending > 0);
289
- (0, assert_1.strict)(this.pendingMessages[numPending - 1].message_idx < message_idx);
290
- const msg = {
291
- type: "tool_call_result",
292
- session_id: this.sessionUUID,
293
- message_idx,
294
- result,
295
- };
296
- this.pendingMessages.push(msg);
297
- return msg;
340
+ return undefined;
298
341
  }
299
342
  getNextMessageIdx() {
300
343
  const idx = this.nextMessageIdx;
@@ -309,25 +352,12 @@ class ChatContextManager {
309
352
  (0, assert_1.strict)(messageIdx === this.nextMessageIdx - conversation_1.MESSAGE_INDEX_FULL_INCREMENT, "message idx cannot be free-ed");
310
353
  this.nextMessageIdx = messageIdx;
311
354
  }
312
- /// Get the current index to use for streaming Agent chunks
313
- getCurrentAgentMessageIdx() {
314
- (0, assert_1.strict)(typeof this.pendingMessages !== "undefined");
315
- (0, assert_1.strict)(typeof this.curAgentMsgIdx !== "undefined");
316
- return this.curAgentMsgIdx;
317
- }
318
- getNextMessageSubIdx() {
319
- (0, assert_1.strict)(typeof this.pendingMessages !== "undefined");
320
- (0, assert_1.strict)(typeof this.curAgentMsgIdx !== "undefined");
321
- const idx = this.curAgentMsgIdx;
322
- this.curAgentMsgIdx += conversation_1.MESSAGE_INDEX_SUB_INCREMENT;
323
- return idx;
324
- }
325
355
  checkCompression() {
326
356
  if (this.pendingCompression) {
327
357
  return;
328
358
  }
329
359
  // TODO: track tokens and use that to trigger compression
330
- const numCommitted = this.llmContext.getCommittedLength();
360
+ const numCommitted = this.llmContext.numMessages();
331
361
  if (numCommitted < COMPRESSION_TRIGGER_NUM_MESSAGES) {
332
362
  return;
333
363
  }
@@ -347,7 +377,7 @@ class ChatContextManager {
347
377
  await this.checkpointWriter.writeCheckpoint(checkpoint);
348
378
  }
349
379
  catch (err) {
350
- logger.warn(`[runCompression] error during compression: ${JSON.stringify(err)}`);
380
+ logger.warn(`[runCompression] error during compression: ${(0, errorUtils_1.getErrorString)(err)}`);
351
381
  }
352
382
  finally {
353
383
  this.pendingCompression = false;
@@ -92,6 +92,9 @@ function userMessageToConversationMessage(userMessage, user_uuid, message_idx, s
92
92
  if (userMsgData.imageB64) {
93
93
  msg.imageB64 = userMsgData.imageB64;
94
94
  }
95
+ if (userMsgData.attachedFiles) {
96
+ msg.attachedFiles = userMsgData.attachedFiles;
97
+ }
95
98
  return msg;
96
99
  }
97
100
  /**
@@ -4,19 +4,30 @@ exports.genImageTool = genImageTool;
4
4
  exports.genImageFileTool = genImageFileTool;
5
5
  const sdk_1 = require("@xalia/xmcp/sdk");
6
6
  const imageGenerator_1 = require("../../agent/imageGenerator");
7
- const tools_1 = require("./tools");
8
- const dbSessionFileModels_1 = require("../data/dbSessionFileModels");
7
+ const tools_1 = require("../../agent/tools");
8
+ const openAIRouterLLM_1 = require("./openAIRouterLLM");
9
+ const imageGenLLM_1 = require("../../agent/imageGenLLM");
10
+ const agentUtils_1 = require("../../agent/agentUtils");
11
+ const constants_1 = require("../constants");
9
12
  const logger = (0, sdk_1.getLogger)();
10
- function createImageGenerator(llmUrl, llmApiKey) {
11
- const imageGenModel = process.env["GEN_IMAGE_MODEL"];
12
- logger.debug(`[genImageFileTool] model: ${imageGenModel || "(default)"}`);
13
- return imageGenerator_1.ImageGenerator.init(llmUrl, llmApiKey, imageGenModel);
13
+ async function createImageGenerator(platform) {
14
+ const imageGenModel = process.env["GEN_IMAGE_MODEL"] || imageGenLLM_1.DEFAULT_IMAGE_GEN_MODEL;
15
+ // Allow the image generator to use a "specialized" or "debug" LLM if
16
+ // requested. Otherwise query the router maps for the url and api key and
17
+ // instantiate an ImageGenLLM.
18
+ let llm = await (0, agentUtils_1.createSpecializedLLM)(imageGenModel, platform);
19
+ if (!llm) {
20
+ const { apiKey, baseURL } = (0, openAIRouterLLM_1.getOpenAIClientParams)(imageGenModel);
21
+ logger.debug(`[genImageFileTool] model: ${imageGenModel}, url: ${baseURL}"}`);
22
+ llm = new imageGenLLM_1.ImageGenLLM(apiKey, baseURL, imageGenModel);
23
+ }
24
+ return imageGenerator_1.ImageGenerator.init(llm);
14
25
  }
15
26
  // gen_image
16
27
  //
17
28
  // Simple tool to generate image output as tool output. The user can then
18
29
  // decide how/whether to use the image.
19
- async function genImageTool(llmUrl, llmApiKey) {
30
+ async function genImageTool(platform) {
20
31
  const GEN_IMAGE_DESC = {
21
32
  type: "function",
22
33
  function: {
@@ -38,7 +49,7 @@ async function genImageTool(llmUrl, llmApiKey) {
38
49
  },
39
50
  },
40
51
  };
41
- const imageGenerator = await createImageGenerator(llmUrl, llmApiKey);
52
+ const imageGenerator = await createImageGenerator(platform);
42
53
  const getPromptInputImg = (0, tools_1.makeParseArgsFn)(["prompt"], ["input_img"]);
43
54
  const toolFn = async (_, args) => {
44
55
  const { prompt, input_img } = getPromptInputImg(args);
@@ -59,12 +70,15 @@ async function genImageTool(llmUrl, llmApiKey) {
59
70
  };
60
71
  }
61
72
  // gen_image_file
62
- async function genImageFileTool(llmUrl, llmApiKey, fileManager) {
73
+ async function genImageFileTool(platform, fileManager) {
63
74
  const GEN_IMAGE_FILE_DESC = {
64
75
  type: "function",
65
76
  function: {
66
77
  name: "gen_image_file",
67
- description: "Generate image (into session file manager)",
78
+ description: "Generate or edit an image. Saves result to file manager. " +
79
+ `Canvas workspace is '${constants_1.CANVAS_WORKSPACE_FILENAME}'. ` +
80
+ "When editing canvas images, set add_to_canvas=true to " +
81
+ "place the result back onto the canvas.",
68
82
  parameters: {
69
83
  type: "object",
70
84
  properties: {
@@ -82,27 +96,36 @@ async function genImageFileTool(llmUrl, llmApiKey, fileManager) {
82
96
  },
83
97
  input_name: {
84
98
  type: "string",
85
- description: "(Optional) input image filename",
99
+ description: "(Optional) input image filename. " +
100
+ `Use '${constants_1.CANVAS_WORKSPACE_FILENAME}' for canvas edits.`,
101
+ },
102
+ add_to_canvas: {
103
+ type: "boolean",
104
+ description: "Set true to place result on canvas (use when editing canvas)",
86
105
  },
87
106
  },
88
107
  required: ["prompt", "name", "summary"],
89
108
  },
90
109
  },
91
110
  };
92
- const imageGenerator = await createImageGenerator(llmUrl, llmApiKey);
93
- const getPromptNameSummary = (0, tools_1.makeParseArgsFn)(["prompt", "name", "summary"], ["input_name"]);
111
+ const imageGenerator = await createImageGenerator(platform);
112
+ const getArgs = (0, tools_1.makeParseArgsFn)(["prompt", "name", "summary"], ["input_name"], ["add_to_canvas"]);
94
113
  const toolFn = async (_, args) => {
95
- const { prompt, name, summary, input_name } = getPromptNameSummary(args);
114
+ const { prompt, name, summary, input_name, add_to_canvas } = getArgs(args);
115
+ const addToCanvas = add_to_canvas ?? false;
96
116
  const input_image = input_name
97
117
  ? await fileManager.getFileContent(input_name)
98
118
  : undefined;
99
119
  const image = await imageGenerator.generate(prompt, input_image);
100
- const mimeType = (0, dbSessionFileModels_1.getSessionFileMimeTypeFromDataUrl)(image);
101
120
  await fileManager.putFileContent(name, summary, image);
102
121
  const uri = fileManager.getSessionFileRelativeUrl(name);
103
122
  return {
104
123
  response: uri,
105
- _meta: { "xalia/fileUri": uri, "xalia/fileMimeType": mimeType },
124
+ _meta: {
125
+ "xalia/fileUri": uri,
126
+ "xalia/fileMimeType": (0, tools_1.getSessionFileMimeTypeFromDataUrl)(image),
127
+ ...(addToCanvas && { "xalia/addToCanvas": "true" }),
128
+ },
106
129
  };
107
130
  };
108
131
  return {