@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
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LLMTitleGenerator = void 0;
4
+ exports.createTitleGenerator = createTitleGenerator;
5
+ const openAIRouterLLM_1 = require("./openAIRouterLLM");
6
+ const sdk_1 = require("@xalia/xmcp/sdk");
7
+ const logger = (0, sdk_1.getLogger)();
8
+ const TITLE_MODEL = "google/gemini-2.5-flash";
9
+ const TITLE_MAX_TOKENS = 20;
10
+ const TITLE_TEMPERATURE = 0.2;
11
+ const TITLE_TIMEOUT_MS = 5000;
12
+ const MAX_TITLE_LENGTH = 120;
13
+ const TITLE_SYSTEM_PROMPT = `You create ultra-short, descriptive titles for user prompts.
14
+ Rules:
15
+ - Maximum 8 words
16
+ - No punctuation except spaces and hyphens
17
+ - No quotes or special characters
18
+ - Use Title Case
19
+ - Output ONLY the title text
20
+ - Be descriptive and capture the main intent`;
21
+ class LLMTitleGenerator {
22
+ constructor(model = TITLE_MODEL) {
23
+ this.model = model;
24
+ }
25
+ async generateTitle(firstUserMessage) {
26
+ if (!firstUserMessage || firstUserMessage.trim().length === 0) {
27
+ return "New Chat";
28
+ }
29
+ try {
30
+ const title = await this.generateTitleWithTimeout(firstUserMessage);
31
+ return this.sanitizeTitle(title);
32
+ }
33
+ catch (error) {
34
+ const errorMsg = error instanceof Error ? error.message : String(error);
35
+ logger.warn(`[TitleGenerator] LLM title generation failed: ${errorMsg}, ` +
36
+ `using fallback`);
37
+ return this.fallbackTitle(firstUserMessage);
38
+ }
39
+ }
40
+ async generateTitleWithTimeout(message) {
41
+ const timeoutPromise = new Promise((_, reject) => {
42
+ setTimeout(() => {
43
+ reject(new Error("Title generation timeout"));
44
+ }, TITLE_TIMEOUT_MS);
45
+ });
46
+ const titlePromise = this.callLLM(message);
47
+ return Promise.race([titlePromise, timeoutPromise]);
48
+ }
49
+ async callLLM(message) {
50
+ const client = (0, openAIRouterLLM_1.getOpenAIClient)(this.model);
51
+ const truncatedMessage = message.slice(0, 500);
52
+ const response = await client.chat.completions.create({
53
+ model: this.model,
54
+ messages: [
55
+ {
56
+ role: "system",
57
+ content: TITLE_SYSTEM_PROMPT,
58
+ },
59
+ {
60
+ role: "user",
61
+ content: `User prompt: ${truncatedMessage}`,
62
+ },
63
+ ],
64
+ max_tokens: TITLE_MAX_TOKENS,
65
+ temperature: TITLE_TEMPERATURE,
66
+ });
67
+ const title = response.choices[0]?.message?.content?.trim();
68
+ if (!title) {
69
+ throw new Error("Empty response from LLM");
70
+ }
71
+ return title;
72
+ }
73
+ sanitizeTitle(title) {
74
+ let sanitized = title
75
+ .replace(/["'`]/g, "")
76
+ .replace(/\n/g, " ")
77
+ .replace(/\s+/g, " ")
78
+ .trim();
79
+ if (sanitized.length > MAX_TITLE_LENGTH) {
80
+ sanitized = sanitized.slice(0, MAX_TITLE_LENGTH).trim();
81
+ }
82
+ return sanitized || "New Chat";
83
+ }
84
+ fallbackTitle(message) {
85
+ const cleaned = message.trim();
86
+ if (cleaned.length === 0) {
87
+ return "New Chat";
88
+ }
89
+ const firstSentence = cleaned.split(/[.!?]\s/)[0];
90
+ const titleCandidate = firstSentence.length <= MAX_TITLE_LENGTH
91
+ ? firstSentence
92
+ : cleaned.slice(0, MAX_TITLE_LENGTH);
93
+ return titleCandidate.trim() || "New Chat";
94
+ }
95
+ }
96
+ exports.LLMTitleGenerator = LLMTitleGenerator;
97
+ class FallbackTitleGenerator {
98
+ // eslint-disable-next-line @typescript-eslint/require-await
99
+ async generateTitle(firstUserMessage) {
100
+ const cleaned = firstUserMessage.trim();
101
+ if (cleaned.length === 0) {
102
+ return "New Chat";
103
+ }
104
+ return cleaned.slice(0, 120) || "New Chat";
105
+ }
106
+ }
107
+ function createTitleGenerator(model) {
108
+ if (process.env.DISABLE_LLM_TITLES === "true") {
109
+ return new FallbackTitleGenerator();
110
+ }
111
+ return new LLMTitleGenerator(model);
112
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const titleGenerator_1 = require("./titleGenerator");
38
+ const openAIRouterLLM = __importStar(require("./openAIRouterLLM"));
39
+ vitest_1.vi.mock("./openAIRouterLLM", async (importOriginal) => {
40
+ const actual = await importOriginal();
41
+ return {
42
+ ...actual,
43
+ getOpenAIClient: vitest_1.vi.fn(),
44
+ };
45
+ });
46
+ vitest_1.vi.mock("@xalia/xmcp/sdk", () => ({
47
+ getLogger: () => ({
48
+ info: vitest_1.vi.fn(),
49
+ warn: vitest_1.vi.fn(),
50
+ error: vitest_1.vi.fn(),
51
+ debug: vitest_1.vi.fn(),
52
+ }),
53
+ }));
54
+ (0, vitest_1.describe)("LLMTitleGenerator", () => {
55
+ let generator;
56
+ let mockCreate;
57
+ (0, vitest_1.beforeEach)(() => {
58
+ vitest_1.vi.resetAllMocks();
59
+ mockCreate = vitest_1.vi.fn();
60
+ vitest_1.vi.mocked(openAIRouterLLM.getOpenAIClient).mockReturnValue({
61
+ chat: {
62
+ completions: {
63
+ create: mockCreate,
64
+ },
65
+ },
66
+ });
67
+ generator = new titleGenerator_1.LLMTitleGenerator();
68
+ });
69
+ (0, vitest_1.afterEach)(() => {
70
+ vitest_1.vi.restoreAllMocks();
71
+ });
72
+ (0, vitest_1.it)("should generate and sanitize title from LLM", async () => {
73
+ mockCreate.mockResolvedValue({
74
+ choices: [{ message: { content: '"Title\nWith Issues"' } }],
75
+ });
76
+ const title = await generator.generateTitle("Test message");
77
+ (0, vitest_1.expect)(title).toBe("Title With Issues");
78
+ (0, vitest_1.expect)(mockCreate).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
79
+ max_tokens: 20,
80
+ temperature: 0.2,
81
+ }));
82
+ });
83
+ (0, vitest_1.it)("should fallback on LLM failure", async () => {
84
+ mockCreate.mockRejectedValue(new Error("API error"));
85
+ const title = await generator.generateTitle("First sentence. Second sentence.");
86
+ (0, vitest_1.expect)(title).toBe("First sentence");
87
+ });
88
+ (0, vitest_1.it)("should handle empty messages", async () => {
89
+ (0, vitest_1.expect)(await generator.generateTitle("")).toBe("New Chat");
90
+ (0, vitest_1.expect)(await generator.generateTitle(" ")).toBe("New Chat");
91
+ (0, vitest_1.expect)(mockCreate).not.toHaveBeenCalled();
92
+ });
93
+ (0, vitest_1.it)("should enforce 120 char limit on both LLM and fallback", async () => {
94
+ const longText = "A".repeat(150);
95
+ mockCreate.mockResolvedValue({
96
+ choices: [{ message: { content: longText } }],
97
+ });
98
+ const llmTitle = await generator.generateTitle("Test");
99
+ (0, vitest_1.expect)(llmTitle.length).toBe(120);
100
+ mockCreate.mockRejectedValue(new Error("fail"));
101
+ const fallbackTitle = await generator.generateTitle(longText);
102
+ (0, vitest_1.expect)(fallbackTitle.length).toBe(120);
103
+ });
104
+ (0, vitest_1.it)("should use fallback when DISABLE_LLM_TITLES is true", async () => {
105
+ const originalEnv = process.env.DISABLE_LLM_TITLES;
106
+ process.env.DISABLE_LLM_TITLES = "true";
107
+ const fallbackGenerator = (0, titleGenerator_1.createTitleGenerator)();
108
+ const title = await fallbackGenerator.generateTitle("Hello from User 1");
109
+ (0, vitest_1.expect)(title).toBe("Hello from User 1");
110
+ (0, vitest_1.expect)(mockCreate).not.toHaveBeenCalled();
111
+ process.env.DISABLE_LLM_TITLES = originalEnv;
112
+ });
113
+ });
@@ -1,271 +1,78 @@
1
1
  "use strict";
2
2
  /**
3
- * Collection of simple Agent tools.
3
+ * Chat-specific agent tools.
4
+ *
5
+ * This file contains chat-specific tools like image generation and test tools.
4
6
  */
5
7
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.testTool = exports.calculatorTool = void 0;
7
- exports.makeParseArgsFn = makeParseArgsFn;
8
- exports.isoWithTimezone = isoWithTimezone;
9
- exports.datetimeTool = datetimeTool;
10
- exports.calculatorEval = calculatorEval;
11
- exports.renderTool = renderTool;
12
- exports.webSearchTool = webSearchTool;
13
- exports.openURL = openURL;
14
- exports.openURLTool = openURLTool;
8
+ exports.testTool = void 0;
9
+ exports.editTextTool = editTextTool;
15
10
  exports.addDefaultChatTools = addDefaultChatTools;
16
- const expr_eval_1 = require("expr-eval");
17
- const sdk_1 = require("@xalia/xmcp/sdk");
18
- const htmlToText_1 = require("../utils/htmlToText");
19
- const search_1 = require("../utils/search");
20
- const sessionFileManager_1 = require("./sessionFileManager");
11
+ const tools_1 = require("../../agent/tools");
21
12
  const imageGeneratorTools_1 = require("./imageGeneratorTools");
22
- const logger = (0, sdk_1.getLogger)();
23
13
  const DEVELOPMENT = process.env.DEVELOPMENT === "1";
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
- function makeParseArgsFn(names, optNames) {
47
- return (args) => {
48
- if (!args || typeof args !== "object") {
49
- throw new Error(`invalid args: ${typeof args}`);
50
- }
51
- const argsObj = args;
52
- for (const name of names) {
53
- const val = argsObj[name];
54
- if (typeof val !== "string") {
55
- throw new Error(`invalid expr args.${name}: ${typeof val}`);
56
- }
57
- }
58
- if (optNames) {
59
- for (const name of optNames) {
60
- const val = argsObj[name];
61
- if (typeof val !== "undefined" && typeof val !== "string") {
62
- throw new Error(`invalid expr args.${name}: ${typeof val}`);
63
- }
64
- }
65
- }
66
- return argsObj;
67
- };
68
- }
69
- const DATETIME_DESC = {
70
- type: "function",
71
- function: {
72
- name: "time_now",
73
- description: "Current time",
74
- },
75
- };
76
- function isoWithTimezone(timeZone) {
77
- return (new Intl.DateTimeFormat("sv-SE", {
78
- timeZone,
79
- year: "numeric",
80
- month: "2-digit",
81
- day: "2-digit",
82
- hour: "2-digit",
83
- minute: "2-digit",
84
- second: "2-digit",
85
- hour12: false,
86
- timeZoneName: "short",
87
- })
88
- .format(new Date())
89
- .replace(" ", "T") + ` (${timeZone})`);
90
- }
91
- function datetimeTool(timezone) {
92
- // eslint-disable-next-line @typescript-eslint/require-await
93
- const toolFn = async () => {
94
- return { response: isoWithTimezone(timezone) };
95
- };
96
- return {
97
- // eslint-disable-next-line @typescript-eslint/require-await
98
- setup: async (agent) => {
99
- agent.addAgentTool(DATETIME_DESC, toolFn);
100
- },
101
- };
102
- }
103
- const ARITHMETIC_DESC = {
14
+ const TEST = process.env.TEST === "1";
15
+ const EDIT_TEXT_DESC = {
104
16
  type: "function",
105
17
  function: {
106
- name: "arithmetic",
107
- description: "Evaluate arithmetic expression",
18
+ name: "edit_text",
19
+ description: "Modify text in the user's editor. Use this to write, update, " +
20
+ "or add content to the editor when the user asks you to create or " +
21
+ "edit text content. If the user quoted text from the editor, use " +
22
+ "'replace_selection' to modify only that quoted text.",
108
23
  parameters: {
109
24
  type: "object",
110
25
  properties: {
111
- expr: {
112
- type: "string",
113
- description: "Expression containing +-*/()",
114
- },
115
- },
116
- required: ["expr"],
117
- },
118
- },
119
- };
120
- function calculatorEval(args) {
121
- try {
122
- return String(expr_eval_1.Parser.evaluate(args));
123
- }
124
- catch (e) {
125
- if (typeof e.message === "string") {
126
- return e.message;
127
- }
128
- return String(e);
129
- }
130
- }
131
- exports.calculatorTool = {
132
- // eslint-disable-next-line @typescript-eslint/require-await
133
- setup: async (agent) => {
134
- const getExpr = makeParseArgsFn(["expr"]);
135
- // eslint-disable-next-line @typescript-eslint/require-await
136
- const toolFn = async (_, args) => {
137
- const { expr } = getExpr(args);
138
- return { response: calculatorEval(expr) };
139
- };
140
- agent.addAgentTool(ARITHMETIC_DESC, toolFn);
141
- },
142
- };
143
- const RENDER_DESC = {
144
- type: "function",
145
- function: {
146
- name: "render",
147
- description: "Display the given html fragment inside a div element",
148
- parameters: {
149
- type: "object",
150
- properties: {
151
- name: {
152
- type: "string",
153
- description: "Filename to store the html",
154
- },
155
- summary: {
26
+ operation: {
156
27
  type: "string",
157
- description: "One line summary",
28
+ enum: ["replace", "append", "prepend", "replace_selection"],
29
+ description: "How to modify the editor: 'replace' to overwrite all " +
30
+ "content, 'append' to add at the end, 'prepend' to add at " +
31
+ "the beginning, 'replace_selection' to replace the " +
32
+ "quoted/selected text",
158
33
  },
159
- html: {
34
+ content: {
160
35
  type: "string",
161
- description: "HTML fragment to render",
36
+ description: "The markdown content to set or add to the editor",
162
37
  },
163
38
  },
164
- required: ["name", "summary", "html"],
39
+ required: ["operation", "content"],
165
40
  },
166
41
  },
167
42
  };
168
- function renderTool(fileManager) {
169
- const getNameSummeryHtml = makeParseArgsFn([
170
- "name",
171
- "summary",
172
- "html",
173
- ]);
174
- const toolFn = async (_, args) => {
175
- const { name, summary, html } = getNameSummeryHtml(args);
176
- const mimeType = "text/html";
177
- const dataURL = `data:${mimeType},${html}`;
178
- await fileManager.putFileContent(name, summary, dataURL);
179
- const uri = fileManager.getSessionFileRelativeUrl(name);
180
- return {
181
- response: "",
182
- _meta: { "xalia/fileUri": uri, "xalia/fileMimeType": mimeType },
183
- };
184
- };
185
- return {
186
- // eslint-disable-next-line @typescript-eslint/require-await
187
- setup: async (agent) => {
188
- agent.addAgentTool(RENDER_DESC, toolFn);
189
- },
190
- };
191
- }
192
- const WEB_SEARCH_DESC = {
193
- type: "function",
194
- function: {
195
- name: "web_search",
196
- description: "Web search",
197
- parameters: {
198
- type: "object",
199
- properties: {
200
- query: {
201
- type: "string",
202
- description: "Search query text",
203
- },
204
- },
205
- required: ["query"],
206
- },
207
- },
208
- };
209
- function webSearchTool() {
210
- const getQuery = makeParseArgsFn(["query"]);
211
- const toolFn = async (_, args) => {
212
- const { query } = getQuery(args);
213
- logger.debug(`[web_search]: query: ${query}`);
214
- const results = await (0, search_1.webSearch)(query);
215
- logger.debug(`[web_search]: results: ${results}`);
216
- return { response: JSON.stringify(results) };
217
- };
218
- return {
219
- // eslint-disable-next-line @typescript-eslint/require-await
220
- setup: async (agent) => {
221
- agent.addAgentTool(WEB_SEARCH_DESC, toolFn);
222
- },
223
- };
224
- }
225
- // open_url
226
- /**
227
- * For now, this matches the duckduckgo-mcp-server length. Could extend
228
- * depending on the application / model etc.
229
- */
230
- const _OPEN_URL_MAX_LENGTH_STR = process.env["OPEN_URL_MAX_LENGTH"] || "8000";
231
- const OPEN_URL_MAX_LENGTH = parseInt(_OPEN_URL_MAX_LENGTH_STR, 10);
232
- const OPEN_URL_DESC = {
233
- type: "function",
234
- function: {
235
- name: "open_url",
236
- description: "Download content from a URL",
237
- parameters: {
238
- type: "object",
239
- properties: {
240
- url: {
241
- type: "string",
242
- description: "URL to download",
243
- },
244
- },
245
- required: ["url"],
246
- },
247
- },
248
- };
249
- async function openURL(url) {
250
- const response = await fetch(url);
251
- if (!response.ok) {
252
- const status = String(response.status);
253
- const code = response.statusText;
254
- throw new Error(`Failed to fetch ${url}: ${status} ${code}`);
255
- }
256
- const html = await response.text();
257
- return (0, htmlToText_1.htmlToText)(html, OPEN_URL_MAX_LENGTH);
258
- }
259
- function openURLTool() {
260
- const getURL = makeParseArgsFn(["url"]);
261
- const toolFn = async (_, args) => {
262
- const { url } = getURL(args);
263
- return { response: await openURL(url) };
43
+ const VALID_OPERATIONS = [
44
+ "replace",
45
+ "append",
46
+ "prepend",
47
+ "replace_selection",
48
+ ];
49
+ function editTextTool(messageSender) {
50
+ const parseArgs = (0, tools_1.makeParseArgsFn)(["operation", "content"]);
51
+ const toolFn = (_, args) => {
52
+ const { operation, content } = parseArgs(args);
53
+ // Validate operation
54
+ if (!VALID_OPERATIONS.includes(operation)) {
55
+ return Promise.resolve({
56
+ response: `Invalid operation: ${operation}`,
57
+ });
58
+ }
59
+ // Broadcast editor update to all session participants
60
+ messageSender.broadcast({
61
+ type: "editor_update",
62
+ operation: operation,
63
+ content,
64
+ format: "markdown",
65
+ session_id: "", // Filled by the message sender
66
+ });
67
+ return Promise.resolve({
68
+ response: `Editor updated with ${operation} operation`,
69
+ overwriteResponse: "", // Don't clutter context with this response
70
+ });
264
71
  };
265
72
  return {
266
73
  // eslint-disable-next-line @typescript-eslint/require-await
267
74
  setup: async (agent) => {
268
- agent.addAgentTool(OPEN_URL_DESC, toolFn);
75
+ agent.addAgentTool(EDIT_TEXT_DESC, toolFn);
269
76
  },
270
77
  };
271
78
  }
@@ -297,16 +104,20 @@ exports.testTool = {
297
104
  };
298
105
  /**
299
106
  * Add a set of agent tools for chat sessions.
107
+ *
108
+ * This includes all general-purpose tools plus chat-specific tools
109
+ * like image generation.
300
110
  */
301
- async function addDefaultChatTools(agent, timezone, fileManager, llmUrl, llmApiKey) {
302
- await agent.addAgentToolProvider(datetimeTool(timezone));
303
- await agent.addAgentToolProvider(exports.calculatorTool);
304
- await agent.addAgentToolProvider(renderTool(fileManager));
305
- await agent.addAgentToolProvider(webSearchTool());
306
- await agent.addAgentToolProvider(openURLTool());
307
- await agent.addAgentToolProvider((0, sessionFileManager_1.fileManagerTool)(fileManager));
308
- await agent.addAgentToolProvider(await (0, imageGeneratorTools_1.genImageFileTool)(llmUrl, llmApiKey, fileManager));
309
- if (DEVELOPMENT) {
111
+ async function addDefaultChatTools(agent, timezone, platform, fileManager, messageSender) {
112
+ await agent.addAgentToolProvider((0, tools_1.datetimeTool)(timezone));
113
+ await agent.addAgentToolProvider(tools_1.calculatorTool);
114
+ await agent.addAgentToolProvider((0, tools_1.renderTool)(fileManager));
115
+ await agent.addAgentToolProvider((0, tools_1.webSearchTool)());
116
+ await agent.addAgentToolProvider((0, tools_1.openURLTool)());
117
+ await agent.addAgentToolProvider((0, tools_1.fileManagerTool)(fileManager));
118
+ await agent.addAgentToolProvider(await (0, imageGeneratorTools_1.genImageFileTool)(platform, fileManager));
119
+ await agent.addAgentToolProvider(editTextTool(messageSender));
120
+ if (DEVELOPMENT || TEST) {
310
121
  await agent.addAgentToolProvider(exports.testTool);
311
122
  }
312
123
  }
@@ -33,7 +33,7 @@ class ToolApprovalManager {
33
33
  *
34
34
  * The returned `requested` value indicates whether approval was requested.
35
35
  */
36
- async getApproval(serverName, tool, toolCall) {
36
+ async getApproval(serverName, tool, toolCall, alt) {
37
37
  const autoApproved = (0, sdk_1.prefsGetAutoApprove)(this.agentProfilePreferences, serverName, tool);
38
38
  if (autoApproved) {
39
39
  return { approved: true, requested: false };
@@ -47,6 +47,7 @@ class ToolApprovalManager {
47
47
  id,
48
48
  tool_call: toolCall,
49
49
  session_id: this.sessionUUID,
50
+ ...(alt ? { alt } : {}),
50
51
  });
51
52
  logger.debug(`[ApprovalManager.getApproval] awaiting approval ${id}`);
52
53
  const approval = await approvalP;
@@ -54,7 +55,7 @@ class ToolApprovalManager {
54
55
  // Handle any auto-approve update, informing other clients.
55
56
  if (approval.auto_approve) {
56
57
  logger.debug("[ApprovalManager.getApproval] updated preferences");
57
- const autoApprovalMsg = await this.setAutoApprove(serverName, tool, true);
58
+ const autoApprovalMsg = await this.setAutoApprove(serverName, tool, true, alt);
58
59
  if (autoApprovalMsg) {
59
60
  this.sender.broadcast(autoApprovalMsg);
60
61
  }
@@ -65,6 +66,7 @@ class ToolApprovalManager {
65
66
  id: approval.id,
66
67
  result: approval.result,
67
68
  session_id: this.sessionUUID,
69
+ ...(alt ? { alt } : {}),
68
70
  });
69
71
  return { approved: approval.result, requested: true };
70
72
  }
@@ -78,7 +80,7 @@ class ToolApprovalManager {
78
80
  * Handle a request to set auto-approval for a given tool. If there was a
79
81
  * change, return the message to be broadcast.
80
82
  */
81
- async setAutoApprove(serverName, tool, autoApprove) {
83
+ async setAutoApprove(serverName, tool, autoApprove, alt) {
82
84
  if ((0, sdk_1.prefsSetAutoApprove)(this.agentProfilePreferences, serverName, tool, autoApprove)) {
83
85
  await this.writer.updatePreferences(this.agentProfileUUID, this.agentProfilePreferences);
84
86
  return {
@@ -87,6 +89,7 @@ class ToolApprovalManager {
87
89
  tool,
88
90
  auto_approve: autoApprove,
89
91
  session_id: this.sessionUUID,
92
+ ...(alt ? { alt } : {}),
90
93
  };
91
94
  }
92
95
  }
@@ -15,6 +15,9 @@ class MultiAsyncQueue {
15
15
  getMaxBacklog() {
16
16
  return this.maxBacklog;
17
17
  }
18
+ getEntries() {
19
+ return this.queue.slice();
20
+ }
18
21
  tryEnqueue(queueEntry) {
19
22
  if (this.maxBacklog > 0 && this.queue.length >= this.maxBacklog) {
20
23
  return false;