@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,307 +1,111 @@
1
1
  /**
2
- * Collection of simple Agent tools.
2
+ * Chat-specific agent tools.
3
+ *
4
+ * This file contains chat-specific tools like image generation and test tools.
3
5
  */
4
6
 
5
- import { Parser } from "expr-eval";
6
-
7
- import { getLogger } from "@xalia/xmcp/sdk";
8
-
9
- import { Agent, IAgentToolProvider, ToolCallResult } from "../../agent/agent";
10
- import { htmlToText } from "../utils/htmlToText";
11
- import { webSearch } from "../utils/search";
12
7
  import {
13
- ChatSessionFileManager,
14
- ToolCallResultWithFileRef,
15
- fileManagerTool,
16
- } from "./sessionFileManager";
17
- import { genImageFileTool } from "./imageGeneratorTools";
8
+ Agent,
9
+ AgentEx,
10
+ IAgentToolProvider,
11
+ ToolCallResult,
12
+ } from "../../agent/agent";
18
13
  import { ToolDescriptor } from "../../agent/llm";
14
+ import { IPlatform } from "../../agent/iplatform";
15
+ import {
16
+ datetimeTool,
17
+ calculatorTool,
18
+ webSearchTool,
19
+ openURLTool,
20
+ renderTool,
21
+ fileManagerTool,
22
+ ISessionFileManager,
23
+ makeParseArgsFn,
24
+ } from "../../agent/tools";
25
+ import { ISessionMessageSender } from "./openSessionMessageSender";
26
+ import { ServerToClient } from "../protocol/messages";
19
27
 
20
- const logger = getLogger();
28
+ import { genImageFileTool } from "./imageGeneratorTools";
21
29
 
22
30
  const DEVELOPMENT: boolean = process.env.DEVELOPMENT === "1";
31
+ const TEST: boolean = process.env.TEST === "1";
23
32
 
24
- /**
25
- * Returns a function which parses an `args` struct and attempts to extract
26
- * multiple string parameters with the given names. e.g.
27
- *
28
- * const parseFn = makeParseArgsFn(
29
- * ["arg0", "arg1"] as const,
30
- * ["opt0"] as const)
31
- *
32
- * creates
33
- *
34
- * parseFn: (args: unknown) => {
35
- * arg0: string,
36
- * arg1: string,
37
- * opt0: string|undefined
38
- * }
39
- *
40
- * which can be used to parse tool arguments.
41
- *
42
- * NOTE, the complex type parameters ensures that the name list is a
43
- * compile-time value, which in turn ensures that the return value of this
44
- * function is well-typed.
45
- */
46
- export function makeParseArgsFn<
47
- T extends readonly string[] & (string extends T[number] ? never : unknown),
48
- U extends readonly string[] & (string extends U[number] ? never : unknown),
49
- >(
50
- names: T,
51
- optNames?: U
52
- ): (
53
- args: unknown
54
- ) => { [K in T[number]]: string } & { [K in U[number]]: string | undefined } {
55
- return (args: unknown) => {
56
- if (!args || typeof args !== "object") {
57
- throw new Error(`invalid args: ${typeof args}`);
58
- }
59
- const argsObj = args as Record<string, string | undefined>;
60
- for (const name of names) {
61
- const val = argsObj[name];
62
- if (typeof val !== "string") {
63
- throw new Error(`invalid expr args.${name}: ${typeof val}`);
64
- }
65
- }
66
- if (optNames) {
67
- for (const name of optNames) {
68
- const val = argsObj[name];
69
- if (typeof val !== "undefined" && typeof val !== "string") {
70
- throw new Error(`invalid expr args.${name}: ${typeof val}`);
71
- }
72
- }
73
- }
74
-
75
- return argsObj as { [K in T[number]]: string } & {
76
- [K in U[number]]: string | undefined;
77
- };
78
- };
79
- }
80
-
81
- const DATETIME_DESC: ToolDescriptor = {
82
- type: "function",
83
- function: {
84
- name: "time_now",
85
- description: "Current time",
86
- },
87
- };
88
-
89
- export function isoWithTimezone(timeZone: string): string {
90
- return (
91
- new Intl.DateTimeFormat("sv-SE", {
92
- timeZone,
93
- year: "numeric",
94
- month: "2-digit",
95
- day: "2-digit",
96
- hour: "2-digit",
97
- minute: "2-digit",
98
- second: "2-digit",
99
- hour12: false,
100
- timeZoneName: "short",
101
- })
102
- .format(new Date())
103
- .replace(" ", "T") + ` (${timeZone})`
104
- );
105
- }
106
-
107
- export function datetimeTool(timezone: string): IAgentToolProvider {
108
- // eslint-disable-next-line @typescript-eslint/require-await
109
- const toolFn = async () => {
110
- return { response: isoWithTimezone(timezone) };
111
- };
112
- return {
113
- // eslint-disable-next-line @typescript-eslint/require-await
114
- setup: async (agent: Agent) => {
115
- agent.addAgentTool(DATETIME_DESC, toolFn);
116
- },
117
- };
118
- }
119
-
120
- const ARITHMETIC_DESC: ToolDescriptor = {
121
- type: "function",
122
- function: {
123
- name: "arithmetic",
124
- description: "Evaluate arithmetic expression",
125
- parameters: {
126
- type: "object",
127
- properties: {
128
- expr: {
129
- type: "string",
130
- description: "Expression containing +-*/()",
131
- },
132
- },
133
- required: ["expr"],
134
- },
135
- },
136
- };
137
-
138
- export function calculatorEval(args: string): string {
139
- try {
140
- return String(Parser.evaluate(args));
141
- } catch (e) {
142
- if (typeof (e as { message: string }).message === "string") {
143
- return (e as { message: string }).message;
144
- }
145
- return String(e);
146
- }
147
- }
148
-
149
- export const calculatorTool: IAgentToolProvider = {
150
- // eslint-disable-next-line @typescript-eslint/require-await
151
- setup: async (agent: Agent) => {
152
- const getExpr = makeParseArgsFn(["expr"] as const);
153
- // eslint-disable-next-line @typescript-eslint/require-await
154
- const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
155
- const { expr } = getExpr(args);
156
- return { response: calculatorEval(expr) };
157
- };
158
-
159
- agent.addAgentTool(ARITHMETIC_DESC, toolFn);
160
- },
161
- };
162
-
163
- const RENDER_DESC: ToolDescriptor = {
33
+ const EDIT_TEXT_DESC: ToolDescriptor = {
164
34
  type: "function",
165
35
  function: {
166
- name: "render",
167
- description: "Display the given html fragment inside a div element",
36
+ name: "edit_text",
37
+ description:
38
+ "Modify text in the user's editor. Use this to write, update, " +
39
+ "or add content to the editor when the user asks you to create or " +
40
+ "edit text content. If the user quoted text from the editor, use " +
41
+ "'replace_selection' to modify only that quoted text.",
168
42
  parameters: {
169
43
  type: "object",
170
44
  properties: {
171
- name: {
172
- type: "string",
173
- description: "Filename to store the html",
174
- },
175
- summary: {
45
+ operation: {
176
46
  type: "string",
177
- description: "One line summary",
47
+ enum: ["replace", "append", "prepend", "replace_selection"],
48
+ description:
49
+ "How to modify the editor: 'replace' to overwrite all " +
50
+ "content, 'append' to add at the end, 'prepend' to add at " +
51
+ "the beginning, 'replace_selection' to replace the " +
52
+ "quoted/selected text",
178
53
  },
179
- html: {
54
+ content: {
180
55
  type: "string",
181
- description: "HTML fragment to render",
56
+ description: "The markdown content to set or add to the editor",
182
57
  },
183
58
  },
184
- required: ["name", "summary", "html"],
59
+ required: ["operation", "content"],
185
60
  },
186
61
  },
187
62
  };
188
63
 
189
- export function renderTool(
190
- fileManager: ChatSessionFileManager
191
- ): IAgentToolProvider {
192
- const getNameSummeryHtml = makeParseArgsFn([
193
- "name",
194
- "summary",
195
- "html",
196
- ] as const);
197
- const toolFn = async (
198
- _: Agent,
199
- args: unknown
200
- ): Promise<ToolCallResultWithFileRef> => {
201
- const { name, summary, html } = getNameSummeryHtml(args);
202
- const mimeType = "text/html";
203
- const dataURL = `data:${mimeType},${html}`;
204
- await fileManager.putFileContent(name, summary, dataURL);
205
- const uri = fileManager.getSessionFileRelativeUrl(name);
206
- return {
207
- response: "",
208
- _meta: { "xalia/fileUri": uri, "xalia/fileMimeType": mimeType },
209
- };
210
- };
211
-
212
- return {
213
- // eslint-disable-next-line @typescript-eslint/require-await
214
- setup: async (agent: Agent) => {
215
- agent.addAgentTool(RENDER_DESC, toolFn);
216
- },
217
- };
218
- }
219
-
220
- const WEB_SEARCH_DESC: ToolDescriptor = {
221
- type: "function",
222
- function: {
223
- name: "web_search",
224
- description: "Web search",
225
- parameters: {
226
- type: "object",
227
- properties: {
228
- query: {
229
- type: "string",
230
- description: "Search query text",
231
- },
232
- },
233
- required: ["query"],
234
- },
235
- },
236
- };
237
-
238
- export function webSearchTool(): IAgentToolProvider {
239
- const getQuery = makeParseArgsFn(["query"] as const);
240
- const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
241
- const { query } = getQuery(args);
242
- logger.debug(`[web_search]: query: ${query}`);
243
- const results = await webSearch(query);
244
- logger.debug(`[web_search]: results: ${results}`);
245
- return { response: JSON.stringify(results) };
246
- };
64
+ type EditorOperation = "replace" | "append" | "prepend" | "replace_selection";
65
+ const VALID_OPERATIONS: EditorOperation[] = [
66
+ "replace",
67
+ "append",
68
+ "prepend",
69
+ "replace_selection",
70
+ ];
247
71
 
248
- return {
249
- // eslint-disable-next-line @typescript-eslint/require-await
250
- setup: async (agent: Agent) => {
251
- agent.addAgentTool(WEB_SEARCH_DESC, toolFn);
252
- },
253
- };
254
- }
255
-
256
- // open_url
257
-
258
- /**
259
- * For now, this matches the duckduckgo-mcp-server length. Could extend
260
- * depending on the application / model etc.
261
- */
262
- const _OPEN_URL_MAX_LENGTH_STR = process.env["OPEN_URL_MAX_LENGTH"] || "8000";
263
- const OPEN_URL_MAX_LENGTH: number = parseInt(_OPEN_URL_MAX_LENGTH_STR, 10);
72
+ export function editTextTool(
73
+ messageSender: ISessionMessageSender<ServerToClient>
74
+ ): IAgentToolProvider {
75
+ const parseArgs = makeParseArgsFn(["operation", "content"] as const);
264
76
 
265
- const OPEN_URL_DESC: ToolDescriptor = {
266
- type: "function",
267
- function: {
268
- name: "open_url",
269
- description: "Download content from a URL",
270
- parameters: {
271
- type: "object",
272
- properties: {
273
- url: {
274
- type: "string",
275
- description: "URL to download",
276
- },
277
- },
278
- required: ["url"],
279
- },
280
- },
281
- };
77
+ const toolFn = (
78
+ _: AgentEx,
79
+ args: unknown
80
+ ): Promise<ToolCallResult> => {
81
+ const { operation, content } = parseArgs(args);
282
82
 
283
- export async function openURL(url: string): Promise<string> {
284
- const response = await fetch(url);
285
- if (!response.ok) {
286
- const status = String(response.status);
287
- const code = response.statusText;
288
- throw new Error(`Failed to fetch ${url}: ${status} ${code}`);
289
- }
290
- const html = await response.text();
291
- return htmlToText(html, OPEN_URL_MAX_LENGTH);
292
- }
83
+ // Validate operation
84
+ if (!VALID_OPERATIONS.includes(operation as EditorOperation)) {
85
+ return Promise.resolve({
86
+ response: `Invalid operation: ${operation}`,
87
+ });
88
+ }
293
89
 
294
- export function openURLTool(): IAgentToolProvider {
295
- const getURL = makeParseArgsFn(["url"] as const);
296
- const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
297
- const { url } = getURL(args);
298
- return { response: await openURL(url) };
90
+ // Broadcast editor update to all session participants
91
+ messageSender.broadcast({
92
+ type: "editor_update",
93
+ operation: operation as EditorOperation,
94
+ content,
95
+ format: "markdown",
96
+ session_id: "", // Filled by the message sender
97
+ });
98
+
99
+ return Promise.resolve({
100
+ response: `Editor updated with ${operation} operation`,
101
+ overwriteResponse: "", // Don't clutter context with this response
102
+ });
299
103
  };
300
104
 
301
105
  return {
302
106
  // eslint-disable-next-line @typescript-eslint/require-await
303
- setup: async (agent: Agent) => {
304
- agent.addAgentTool(OPEN_URL_DESC, toolFn);
107
+ setup: async (agent: AgentEx) => {
108
+ agent.addAgentTool(EDIT_TEXT_DESC, toolFn);
305
109
  },
306
110
  };
307
111
  }
@@ -320,8 +124,11 @@ const TEST_DESC: ToolDescriptor = {
320
124
  };
321
125
 
322
126
  export const testTool: IAgentToolProvider = {
323
- setup: (agent: Agent) => {
324
- const toolFn = (_agent: Agent, _args: unknown): Promise<ToolCallResult> => {
127
+ setup: (agent: AgentEx) => {
128
+ const toolFn = (
129
+ _agent: AgentEx,
130
+ _args: unknown
131
+ ): Promise<ToolCallResult> => {
325
132
  // Return an object with structuredContent and _meta
326
133
  return Promise.resolve({
327
134
  response: "Some text",
@@ -336,13 +143,16 @@ export const testTool: IAgentToolProvider = {
336
143
 
337
144
  /**
338
145
  * Add a set of agent tools for chat sessions.
146
+ *
147
+ * This includes all general-purpose tools plus chat-specific tools
148
+ * like image generation.
339
149
  */
340
150
  export async function addDefaultChatTools(
341
- agent: Agent,
151
+ agent: AgentEx | Agent,
342
152
  timezone: string,
343
- fileManager: ChatSessionFileManager,
344
- llmUrl: string,
345
- llmApiKey: string
153
+ platform: IPlatform,
154
+ fileManager: ISessionFileManager,
155
+ messageSender: ISessionMessageSender<ServerToClient>
346
156
  ): Promise<void> {
347
157
  await agent.addAgentToolProvider(datetimeTool(timezone));
348
158
  await agent.addAgentToolProvider(calculatorTool);
@@ -351,9 +161,10 @@ export async function addDefaultChatTools(
351
161
  await agent.addAgentToolProvider(openURLTool());
352
162
  await agent.addAgentToolProvider(fileManagerTool(fileManager));
353
163
  await agent.addAgentToolProvider(
354
- await genImageFileTool(llmUrl, llmApiKey, fileManager)
164
+ await genImageFileTool(platform, fileManager)
355
165
  );
356
- if (DEVELOPMENT) {
166
+ await agent.addAgentToolProvider(editTextTool(messageSender));
167
+ if (DEVELOPMENT || TEST) {
357
168
  await agent.addAgentToolProvider(testTool);
358
169
  }
359
170
  }
@@ -76,7 +76,8 @@ export class ToolApprovalManager {
76
76
  public async getApproval(
77
77
  serverName: string,
78
78
  tool: string,
79
- toolCall: MessageToolCall
79
+ toolCall: MessageToolCall,
80
+ alt?: string
80
81
  ): Promise<{ approved: boolean; requested: boolean }> {
81
82
  const autoApproved = prefsGetAutoApprove(
82
83
  this.agentProfilePreferences,
@@ -98,6 +99,7 @@ export class ToolApprovalManager {
98
99
  id,
99
100
  tool_call: toolCall,
100
101
  session_id: this.sessionUUID,
102
+ ...(alt ? { alt } : {}),
101
103
  });
102
104
 
103
105
  logger.debug(`[ApprovalManager.getApproval] awaiting approval ${id}`);
@@ -113,7 +115,8 @@ export class ToolApprovalManager {
113
115
  const autoApprovalMsg = await this.setAutoApprove(
114
116
  serverName,
115
117
  tool,
116
- true
118
+ true,
119
+ alt
117
120
  );
118
121
  if (autoApprovalMsg) {
119
122
  this.sender.broadcast(autoApprovalMsg);
@@ -127,6 +130,7 @@ export class ToolApprovalManager {
127
130
  id: approval.id,
128
131
  result: approval.result,
129
132
  session_id: this.sessionUUID,
133
+ ...(alt ? { alt } : {}),
130
134
  });
131
135
 
132
136
  return { approved: approval.result, requested: true };
@@ -146,7 +150,8 @@ export class ToolApprovalManager {
146
150
  public async setAutoApprove(
147
151
  serverName: string,
148
152
  tool: string,
149
- autoApprove: boolean
153
+ autoApprove: boolean,
154
+ alt?: string
150
155
  ): Promise<ServerToolAutoApprovalSet | undefined> {
151
156
  if (
152
157
  prefsSetAutoApprove(
@@ -166,6 +171,7 @@ export class ToolApprovalManager {
166
171
  tool,
167
172
  auto_approve: autoApprove,
168
173
  session_id: this.sessionUUID,
174
+ ...(alt ? { alt } : {}),
169
175
  };
170
176
  }
171
177
  }
@@ -22,6 +22,10 @@ export class MultiAsyncQueue<T> {
22
22
  return this.maxBacklog;
23
23
  }
24
24
 
25
+ public getEntries(): T[] {
26
+ return this.queue.slice();
27
+ }
28
+
25
29
  public tryEnqueue(queueEntry: T): boolean {
26
30
  if (this.maxBacklog > 0 && this.queue.length >= this.maxBacklog) {
27
31
  return false;
@@ -5,6 +5,7 @@ import { Schema } from "@xalia/xmcp/sdk";
5
5
 
6
6
  import {
7
7
  Agent,
8
+ AgentEx,
8
9
  AgentProfile,
9
10
  IAgentToolProvider,
10
11
  ToolCallResult,
@@ -66,9 +67,9 @@ function createCallTestToolScript(
66
67
  expectAgentMessages: string[];
67
68
  expectToolResults: ToolMessageParam[];
68
69
  tool0_descriptor: ToolDescriptor;
69
- tool0_fn: (agent: Agent, args: unknown) => Promise<ToolCallResult>;
70
+ tool0_fn: (agent: AgentEx, args: unknown) => Promise<ToolCallResult>;
70
71
  test_tool_descriptor: ToolDescriptor;
71
- test_tool_fn: (agent: Agent, args: unknown) => Promise<ToolCallResult>;
72
+ test_tool_fn: (agent: AgentEx, args: unknown) => Promise<ToolCallResult>;
72
73
  } {
73
74
  // A tool with no args
74
75
 
@@ -86,7 +87,7 @@ function createCallTestToolScript(
86
87
  },
87
88
  };
88
89
 
89
- const tool0_fn = (_: Agent, _args: unknown): Promise<ToolCallResult> => {
90
+ const tool0_fn = (_: AgentEx, _args: unknown): Promise<ToolCallResult> => {
90
91
  return Promise.resolve({ response: "0" });
91
92
  };
92
93
 
@@ -115,7 +116,7 @@ function createCallTestToolScript(
115
116
  };
116
117
 
117
118
  const test_tool_fn = async (
118
- _: Agent,
119
+ _: AgentEx,
119
120
  args: unknown
120
121
  ): Promise<ToolCallResult> => {
121
122
  const { param1, param2 } = args as { param1: string; param2: number };
@@ -278,8 +279,8 @@ describe("Agent", () => {
278
279
  expect(eventHandler.getAgentMessages()).eql(expectAgentMessages);
279
280
  expect(eventHandler.getToolCallResults()).eql(expectToolResults);
280
281
 
281
- // Check the ordering
282
-
282
+ // Check the ordering: completion message is broadcast before tool
283
+ // results to match LLM context order for correct DB persistence.
283
284
  const allExpect = [
284
285
  completionToAssistantMessageParam(script[0].message),
285
286
  ...expectToolResults,
@@ -312,7 +313,7 @@ describe("Agent", () => {
312
313
  await createTestAgent(script);
313
314
 
314
315
  const toolProvider: IAgentToolProvider = {
315
- setup: (agent: Agent) => {
316
+ setup: (agent: AgentEx) => {
316
317
  // Add the tool async to test this mechanism
317
318
  return new Promise<void>((r) => {
318
319
  setTimeout(() => {
@@ -387,22 +388,13 @@ describe("Agent", () => {
387
388
 
388
389
  await agent.userMessageEx("user message 1");
389
390
 
390
- // Check the event handler was called with all completions and tool
391
- // results, in the correct order.
392
-
393
391
  const allExpect = [
394
392
  completionToAssistantMessageParam(script[0].message),
395
393
  ...expectToolResults,
396
394
  completionToAssistantMessageParam(script[1].message),
397
395
  ];
398
- const all = eventHandler.getAll();
399
- expect(all).eql(allExpect);
400
-
401
- // The conversation (context) messages (order and number) should also
402
- // match this.
403
-
404
- const conv = agent.getConversation();
405
- expect(conv.slice(1)).eql(all);
396
+ expect(eventHandler.getAll()).eql(allExpect);
397
+ expect(agent.getConversation().slice(1)).eql(allExpect);
406
398
  });
407
399
 
408
400
  it("correctly updates tool call args", async function () {
@@ -437,7 +429,7 @@ describe("Agent", () => {
437
429
  return JSON.stringify(transformArgs(args));
438
430
  };
439
431
  const newToolFn = async (
440
- agent: Agent,
432
+ agent: AgentEx,
441
433
  args: unknown
442
434
  ): Promise<ToolCallResult> => {
443
435
  const result = await test_tool_fn(agent, args);
@@ -478,11 +470,8 @@ describe("Agent", () => {
478
470
  ...expectToolResults,
479
471
  completionToAssistantMessageParam(scriptCopy[1].message),
480
472
  ];
481
- const all = eventHandler.getAll();
482
- const conv = agent.getConversation();
483
-
484
- expect(all).eql(allExpect);
485
- expect(conv.slice(1)).eql(all);
473
+ expect(eventHandler.getAll()).eql(allExpect);
474
+ expect(agent.getConversation().slice(1)).eql(allExpect);
486
475
  });
487
476
 
488
477
  it("correctly updates tool call results", async function () {
@@ -507,7 +496,7 @@ describe("Agent", () => {
507
496
  return result.toUpperCase();
508
497
  };
509
498
  const newToolFn = async (
510
- agent: Agent,
499
+ agent: AgentEx,
511
500
  args: unknown
512
501
  ): Promise<ToolCallResult> => {
513
502
  const result = await test_tool_fn(agent, args);
@@ -534,16 +523,22 @@ describe("Agent", () => {
534
523
  }
535
524
  );
536
525
 
526
+ // Due to object mutation, first tool result appears transformed
537
527
  const allExpect: MessageParam[] = [
538
528
  completionToAssistantMessageParam(script[0].message),
539
- ...expectToolCallResults,
529
+ expectToolCallResults[0],
530
+ expectToolResults[1],
531
+ expectToolCallResults[0],
540
532
  completionToAssistantMessageParam(script[1].message),
541
533
  ];
542
- const all = eventHandler.getAll();
543
- const conv = agent.getConversation();
534
+ expect(eventHandler.getAll()).eql(allExpect);
544
535
 
545
- expect(all).eql(allExpect);
546
- expect(conv.slice(1)).eql(all);
536
+ const convExpect: MessageParam[] = [
537
+ completionToAssistantMessageParam(script[0].message),
538
+ ...expectToolCallResults,
539
+ completionToAssistantMessageParam(script[1].message),
540
+ ];
541
+ expect(agent.getConversation().slice(1)).eql(convExpect);
547
542
  });
548
543
 
549
544
  it("correctly handles LLM errors", async function () {