@xalia/agent 0.5.7 → 0.6.0

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 (186) hide show
  1. package/README.md +23 -8
  2. package/dist/agent/src/agent/agent.js +176 -96
  3. package/dist/agent/src/agent/agentUtils.js +82 -59
  4. package/dist/agent/src/agent/compressingContextManager.js +102 -0
  5. package/dist/agent/src/agent/context.js +189 -0
  6. package/dist/agent/src/agent/dummyLLM.js +46 -5
  7. package/dist/agent/src/agent/mcpServerManager.js +23 -24
  8. package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
  9. package/dist/agent/src/agent/nullPlatform.js +14 -0
  10. package/dist/agent/src/agent/openAILLMStreaming.js +26 -14
  11. package/dist/agent/src/agent/promptProvider.js +63 -0
  12. package/dist/agent/src/agent/repeatLLM.js +5 -5
  13. package/dist/agent/src/agent/sudoMcpServerManager.js +23 -21
  14. package/dist/agent/src/agent/tokenAuth.js +7 -7
  15. package/dist/agent/src/agent/tools.js +1 -1
  16. package/dist/agent/src/chat/client/chatClient.js +733 -0
  17. package/dist/agent/src/chat/client/connection.js +209 -0
  18. package/dist/agent/src/chat/client/connection.test.js +188 -0
  19. package/dist/agent/src/chat/client/constants.js +5 -0
  20. package/dist/agent/src/chat/client/index.js +15 -0
  21. package/dist/agent/src/chat/client/interfaces.js +2 -0
  22. package/dist/agent/src/chat/client/responseHandler.js +105 -0
  23. package/dist/agent/src/chat/client/sessionClient.js +331 -0
  24. package/dist/agent/src/chat/client/teamManager.js +2 -0
  25. package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
  26. package/dist/agent/src/chat/data/dataModels.js +2 -0
  27. package/dist/agent/src/chat/data/database.js +749 -0
  28. package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
  29. package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
  30. package/dist/agent/src/chat/protocol/constants.js +50 -0
  31. package/dist/agent/src/chat/protocol/errors.js +22 -0
  32. package/dist/agent/src/chat/protocol/messages.js +110 -0
  33. package/dist/agent/src/chat/server/chatContextManager.js +405 -0
  34. package/dist/agent/src/chat/server/connectionManager.js +352 -0
  35. package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
  36. package/dist/agent/src/chat/server/conversation.js +198 -0
  37. package/dist/agent/src/chat/server/errorUtils.js +23 -0
  38. package/dist/agent/src/chat/server/openSession.js +869 -0
  39. package/dist/agent/src/chat/server/server.js +177 -0
  40. package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
  41. package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
  42. package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
  43. package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
  44. package/dist/agent/src/chat/server/tools.js +243 -0
  45. package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
  46. package/dist/agent/src/chat/utils/approvalManager.js +85 -0
  47. package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
  48. package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
  49. package/dist/agent/src/chat/utils/htmlToText.js +84 -0
  50. package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
  51. package/dist/agent/src/chat/utils/search.js +145 -0
  52. package/dist/agent/src/chat/utils/userResolver.js +46 -0
  53. package/dist/agent/src/chat/utils/websocket.js +16 -0
  54. package/dist/agent/src/test/agent.test.js +332 -0
  55. package/dist/agent/src/test/approvalManager.test.js +58 -0
  56. package/dist/agent/src/test/chatContextManager.test.js +392 -0
  57. package/dist/agent/src/test/clientServerConnection.test.js +158 -0
  58. package/dist/agent/src/test/compressingContextManager.test.js +65 -0
  59. package/dist/agent/src/test/context.test.js +83 -0
  60. package/dist/agent/src/test/conversation.test.js +89 -0
  61. package/dist/agent/src/test/db.test.js +271 -83
  62. package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
  63. package/dist/agent/src/test/dbTestTools.js +99 -0
  64. package/dist/agent/src/test/imageLoad.test.js +8 -7
  65. package/dist/agent/src/test/mcpServerManager.test.js +23 -20
  66. package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
  67. package/dist/agent/src/test/openaiStreaming.test.js +64 -35
  68. package/dist/agent/src/test/prompt.test.js +5 -4
  69. package/dist/agent/src/test/promptProvider.test.js +28 -0
  70. package/dist/agent/src/test/responseHandler.test.js +61 -0
  71. package/dist/agent/src/test/sudoMcpServerManager.test.js +24 -25
  72. package/dist/agent/src/test/testTools.js +109 -0
  73. package/dist/agent/src/test/tools.test.js +31 -0
  74. package/dist/agent/src/tool/agentChat.js +21 -10
  75. package/dist/agent/src/tool/agentMain.js +1 -1
  76. package/dist/agent/src/tool/chatMain.js +241 -58
  77. package/dist/agent/src/tool/commandPrompt.js +22 -17
  78. package/dist/agent/src/tool/files.js +20 -16
  79. package/dist/agent/src/tool/nodePlatform.js +47 -3
  80. package/dist/agent/src/tool/options.js +4 -4
  81. package/dist/agent/src/tool/prompt.js +19 -13
  82. package/eslint.config.mjs +14 -1
  83. package/package.json +14 -6
  84. package/scripts/chat_server +8 -0
  85. package/scripts/setup_chat +7 -2
  86. package/scripts/shutdown_chat_server +3 -0
  87. package/scripts/test_chat +135 -17
  88. package/src/agent/agent.ts +283 -138
  89. package/src/agent/agentUtils.ts +143 -108
  90. package/src/agent/compressingContextManager.ts +164 -0
  91. package/src/agent/context.ts +268 -0
  92. package/src/agent/dummyLLM.ts +76 -8
  93. package/src/agent/iAgentEventHandler.ts +54 -0
  94. package/src/agent/iplatform.ts +1 -0
  95. package/src/agent/mcpServerManager.ts +35 -31
  96. package/src/agent/nullAgentEventHandler.ts +20 -0
  97. package/src/agent/nullPlatform.ts +13 -0
  98. package/src/agent/openAILLMStreaming.ts +26 -13
  99. package/src/agent/promptProvider.ts +87 -0
  100. package/src/agent/repeatLLM.ts +5 -5
  101. package/src/agent/sudoMcpServerManager.ts +30 -29
  102. package/src/agent/tokenAuth.ts +7 -7
  103. package/src/agent/tools.ts +3 -1
  104. package/src/chat/client/chatClient.ts +900 -0
  105. package/src/chat/client/connection.test.ts +241 -0
  106. package/src/chat/client/connection.ts +276 -0
  107. package/src/chat/client/constants.ts +3 -0
  108. package/src/chat/client/index.ts +18 -0
  109. package/src/chat/client/interfaces.ts +34 -0
  110. package/src/chat/client/responseHandler.ts +131 -0
  111. package/src/chat/client/sessionClient.ts +443 -0
  112. package/src/chat/client/teamManager.ts +29 -0
  113. package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
  114. package/src/chat/data/dataModels.ts +85 -0
  115. package/src/chat/data/database.ts +982 -0
  116. package/src/chat/data/dbMcpServerConfigs.ts +59 -0
  117. package/src/chat/protocol/connectionMessages.ts +49 -0
  118. package/src/chat/protocol/constants.ts +55 -0
  119. package/src/chat/protocol/errors.ts +16 -0
  120. package/src/chat/protocol/messages.ts +682 -0
  121. package/src/chat/server/README.md +127 -0
  122. package/src/chat/server/chatContextManager.ts +612 -0
  123. package/src/chat/server/connectionManager.test.ts +266 -0
  124. package/src/chat/server/connectionManager.ts +541 -0
  125. package/src/chat/server/conversation.ts +269 -0
  126. package/src/chat/server/errorUtils.ts +28 -0
  127. package/src/chat/server/openSession.ts +1332 -0
  128. package/src/chat/server/server.ts +177 -0
  129. package/src/chat/server/sessionFileManager.ts +239 -0
  130. package/src/chat/server/sessionRegistry.test.ts +138 -0
  131. package/src/chat/server/sessionRegistry.ts +1064 -0
  132. package/src/chat/server/test-utils/mockFactories.ts +422 -0
  133. package/src/chat/server/tools.ts +265 -0
  134. package/src/chat/utils/agentSessionMap.ts +76 -0
  135. package/src/chat/utils/approvalManager.ts +111 -0
  136. package/src/{utils → chat/utils}/asyncLock.ts +3 -3
  137. package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
  138. package/src/chat/utils/htmlToText.ts +61 -0
  139. package/src/chat/utils/multiAsyncQueue.ts +52 -0
  140. package/src/chat/utils/search.ts +139 -0
  141. package/src/chat/utils/userResolver.ts +48 -0
  142. package/src/chat/utils/websocket.ts +16 -0
  143. package/src/test/agent.test.ts +487 -0
  144. package/src/test/approvalManager.test.ts +73 -0
  145. package/src/test/chatContextManager.test.ts +521 -0
  146. package/src/test/clientServerConnection.test.ts +207 -0
  147. package/src/test/compressingContextManager.test.ts +82 -0
  148. package/src/test/context.test.ts +105 -0
  149. package/src/test/conversation.test.ts +109 -0
  150. package/src/test/db.test.ts +358 -89
  151. package/src/test/dbMcpServerConfigs.test.ts +112 -0
  152. package/src/test/dbTestTools.ts +153 -0
  153. package/src/test/imageLoad.test.ts +7 -6
  154. package/src/test/mcpServerManager.test.ts +21 -16
  155. package/src/test/multiAsyncQueue.test.ts +125 -0
  156. package/src/test/openaiStreaming.test.ts +71 -36
  157. package/src/test/prompt.test.ts +4 -3
  158. package/src/test/promptProvider.test.ts +33 -0
  159. package/src/test/responseHandler.test.ts +78 -0
  160. package/src/test/sudoMcpServerManager.test.ts +32 -30
  161. package/src/test/testTools.ts +146 -0
  162. package/src/test/tools.test.ts +39 -0
  163. package/src/tool/agentChat.ts +26 -12
  164. package/src/tool/agentMain.ts +1 -1
  165. package/src/tool/chatMain.ts +292 -100
  166. package/src/tool/commandPrompt.ts +28 -19
  167. package/src/tool/files.ts +25 -19
  168. package/src/tool/nodePlatform.ts +52 -3
  169. package/src/tool/options.ts +4 -2
  170. package/src/tool/prompt.ts +22 -15
  171. package/test_data/dummyllm_script_crash.json +32 -0
  172. package/test_data/frog.png.b64 +1 -0
  173. package/vitest.config.ts +39 -0
  174. package/dist/agent/src/chat/client.js +0 -349
  175. package/dist/agent/src/chat/conversationManager.js +0 -392
  176. package/dist/agent/src/chat/db.js +0 -209
  177. package/dist/agent/src/chat/frontendClient.js +0 -74
  178. package/dist/agent/src/chat/server.js +0 -158
  179. package/src/chat/client.ts +0 -455
  180. package/src/chat/conversationManager.ts +0 -595
  181. package/src/chat/db.ts +0 -290
  182. package/src/chat/frontendClient.ts +0 -123
  183. package/src/chat/messages.ts +0 -235
  184. package/src/chat/server.ts +0 -177
  185. /package/dist/agent/src/{chat/messages.js → agent/iAgentEventHandler.js} +0 -0
  186. /package/{frog.png → test_data/frog.png} +0 -0
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Common mock factories for chat server tests
3
+ * Provides consistent mock objects across all test suites
4
+ */
5
+
6
+ import { vi } from "vitest";
7
+ import type { Database, UserData } from "../../data/database";
8
+ import type { SessionData } from "../../data/dataModels";
9
+ import type { ApiKeyManager } from "../../data/apiKeyManager";
10
+ import type { OpenSession } from "../openSession";
11
+ import type { WebSocket } from "ws";
12
+ import type { AgentProfile } from "@xalia/xmcp/sdk";
13
+ import type * as supabase from "../../../../../supabase/database.types";
14
+ import { ClientToServer, ServerToClient } from "../../protocol/messages";
15
+ import {
16
+ IMessageProcessor,
17
+ IUserConnectionManager,
18
+ } from "../connectionManager";
19
+
20
+ // =====================================
21
+ // Mock Data Constants
22
+ // =====================================
23
+
24
+ export const MOCK_USERS = {
25
+ owner: {
26
+ uuid: "user-123",
27
+ nickname: "test-owner",
28
+ } as UserData,
29
+ participant: {
30
+ uuid: "user-456",
31
+ nickname: "test-participant",
32
+ } as UserData,
33
+ nonExistent: {
34
+ uuid: "user-999",
35
+ nickname: "non-existent",
36
+ } as UserData,
37
+ };
38
+
39
+ export const MOCK_SESSIONS = {
40
+ active: {
41
+ uuid: "session-abc",
42
+ title: "Test Session",
43
+ user_uuid: MOCK_USERS.owner.uuid,
44
+ agent_profile_uuid: "agent-profile-1",
45
+ workspace: null,
46
+ team_uuid: null,
47
+ public: false,
48
+ updated_at: "2025-01-01T00:00:00Z",
49
+ } as supabase.Tables<"sessions">,
50
+ secondary: {
51
+ uuid: "session-xyz",
52
+ title: "Secondary Session",
53
+ user_uuid: MOCK_USERS.owner.uuid,
54
+ agent_profile_uuid: "agent-profile-2",
55
+ workspace: null,
56
+ team_uuid: null,
57
+ public: false,
58
+ updated_at: "2025-01-01T01:00:00Z",
59
+ } as supabase.Tables<"sessions">,
60
+ };
61
+
62
+ export const MOCK_AGENT_PROFILES = {
63
+ default: {
64
+ system_prompt: "You are a helpful assistant",
65
+ model: "gpt-4",
66
+ mcp_settings: {},
67
+ } as AgentProfile,
68
+ custom: {
69
+ system_prompt: "You are a specialized assistant",
70
+ model: "claude-3",
71
+ mcp_settings: {
72
+ filesystem: ["read_file", "write_file"],
73
+ },
74
+ } as AgentProfile,
75
+ };
76
+
77
+ // =====================================
78
+ // Mock Factory Functions
79
+ // =====================================
80
+
81
+ /**
82
+ * Creates a mock Database instance with common methods
83
+ */
84
+ export function createMockDatabase(): {
85
+ mock: Database;
86
+ spies: {
87
+ sessionCreate: ReturnType<typeof vi.fn>;
88
+ sessionGetById: ReturnType<typeof vi.fn>;
89
+ getUserFromUuid: ReturnType<typeof vi.fn>;
90
+ getAgentProfileById: ReturnType<typeof vi.fn>;
91
+ getSavedAgentProfileById: ReturnType<typeof vi.fn>;
92
+ getUserApiKey: ReturnType<typeof vi.fn>;
93
+ sessionParticipantAdd: ReturnType<typeof vi.fn>;
94
+ sessionParticipantRemove: ReturnType<typeof vi.fn>;
95
+ updateAgentProfile: ReturnType<typeof vi.fn>;
96
+ sessionConversationAppend: ReturnType<typeof vi.fn>;
97
+ sessionGetParticipants: ReturnType<typeof vi.fn>;
98
+ getUserSessions: ReturnType<typeof vi.fn>;
99
+ };
100
+ } {
101
+ const spies = {
102
+ sessionCreate: vi.fn(),
103
+ sessionGetById: vi.fn(),
104
+ getUserFromUuid: vi.fn(),
105
+ getAgentProfileById: vi.fn(),
106
+ getSavedAgentProfileById: vi.fn(),
107
+ getUserApiKey: vi.fn(),
108
+ sessionParticipantAdd: vi.fn(),
109
+ sessionParticipantRemove: vi.fn(),
110
+ updateAgentProfile: vi.fn(),
111
+ sessionConversationAppend: vi.fn(),
112
+ sessionGetParticipants: vi.fn(),
113
+ getUserSessions: vi.fn(),
114
+ };
115
+
116
+ const mock = {
117
+ ...spies,
118
+ // Add any other Database methods that might be called
119
+ } as unknown as Database;
120
+
121
+ return { mock, spies };
122
+ }
123
+
124
+ /**
125
+ * Creates a mock IUserConnectionManager instance
126
+ */
127
+ export function createMockUserConnectionManager(): {
128
+ mock: IUserConnectionManager<ServerToClient>;
129
+ spies: {
130
+ sendToUsers: ReturnType<typeof vi.fn>;
131
+ getLiveUserApiKey: ReturnType<typeof vi.fn>;
132
+ };
133
+ } {
134
+ const spies = {
135
+ sendToUsers: vi.fn(),
136
+ sendToConnection: vi.fn(),
137
+ sendServerError: vi.fn(),
138
+ getLiveUserApiKey: vi.fn(),
139
+ };
140
+
141
+ const mock = {
142
+ sendToUsers: spies.sendToUsers,
143
+ sendToConnection: spies.sendToConnection,
144
+ sendServerError: spies.sendServerError,
145
+ getLiveUserApiKey: spies.getLiveUserApiKey,
146
+ } as IUserConnectionManager<ServerToClient>;
147
+
148
+ return { mock, spies };
149
+ }
150
+
151
+ /**
152
+ * Creates a mock IMessageProcessor instance
153
+ */
154
+ export function createMockSessionRegistry(): {
155
+ mock: IMessageProcessor<ClientToServer>;
156
+ spies: {
157
+ processMessage: ReturnType<typeof vi.fn>;
158
+ handleUserDisconnect: ReturnType<typeof vi.fn>;
159
+ };
160
+ } {
161
+ const spies = {
162
+ processMessage: vi.fn(),
163
+ handleUserDisconnect: vi.fn(),
164
+ };
165
+
166
+ const mock = {
167
+ processMessage: spies.processMessage,
168
+ handleUserDisconnect: spies.handleUserDisconnect,
169
+ } as IMessageProcessor<ClientToServer>;
170
+
171
+ return { mock, spies };
172
+ }
173
+
174
+ /**
175
+ * Creates a mock ApiKeyManager instance
176
+ */
177
+ export function createMockApiKeyManager(): {
178
+ mock: ApiKeyManager;
179
+ spies: {
180
+ verifyApiKey: ReturnType<typeof vi.fn>;
181
+ };
182
+ } {
183
+ const spies = {
184
+ verifyApiKey: vi.fn(),
185
+ };
186
+
187
+ const mock = {
188
+ ...spies,
189
+ } as unknown as ApiKeyManager;
190
+
191
+ return { mock, spies };
192
+ }
193
+
194
+ /**
195
+ * Creates a mock WebSocket instance
196
+ */
197
+ export function createMockWebSocket(readyState: number = 1): {
198
+ mock: WebSocket;
199
+ spies: {
200
+ on: ReturnType<typeof vi.fn>;
201
+ once: ReturnType<typeof vi.fn>;
202
+ send: ReturnType<typeof vi.fn>;
203
+ close: ReturnType<typeof vi.fn>;
204
+ };
205
+ } {
206
+ const spies = {
207
+ on: vi.fn(),
208
+ once: vi.fn(),
209
+ send: vi.fn(),
210
+ close: vi.fn(),
211
+ };
212
+
213
+ const mock = {
214
+ ...spies,
215
+ readyState, // Allow setting initial readyState
216
+ } as unknown as WebSocket;
217
+
218
+ return { mock, spies };
219
+ }
220
+
221
+ /**
222
+ * Creates a mock OpenSession instance
223
+ */
224
+ export function createMockOpenSession(sessionId = MOCK_SESSIONS.active.uuid): {
225
+ mock: OpenSession;
226
+ spies: {
227
+ addParticipant: ReturnType<typeof vi.fn>;
228
+ leave: ReturnType<typeof vi.fn>;
229
+ onClientMessage: ReturnType<typeof vi.fn>;
230
+ getSessionState: ReturnType<typeof vi.fn>;
231
+ broadcast: ReturnType<typeof vi.fn>;
232
+ sendTo: ReturnType<typeof vi.fn>;
233
+ onEmpty: ReturnType<typeof vi.fn>;
234
+ };
235
+ } {
236
+ const spies = {
237
+ addParticipant: vi.fn(),
238
+ leave: vi.fn(),
239
+ onClientMessage: vi.fn(),
240
+ getSessionState: vi.fn(),
241
+ broadcast: vi.fn(),
242
+ sendTo: vi.fn(),
243
+ onEmpty: vi.fn(),
244
+ };
245
+
246
+ const mock = {
247
+ ...spies,
248
+ sessionUUID: sessionId,
249
+ users: new Set<string>(),
250
+ agentProfileUUID: MOCK_SESSIONS.active.agent_profile_uuid,
251
+ sessionOwnerUserUUID: MOCK_USERS.owner.uuid,
252
+ } as unknown as OpenSession;
253
+
254
+ return { mock, spies };
255
+ }
256
+
257
+ /**
258
+ * Creates a standard set of session list items for testing
259
+ */
260
+ export function createMockSessionList(): Array<SessionData> {
261
+ return [
262
+ {
263
+ session_uuid: MOCK_SESSIONS.active.uuid,
264
+ title: MOCK_SESSIONS.active.title,
265
+ team_uuid: undefined,
266
+ agent_profile_uuid: MOCK_SESSIONS.active.agent_profile_uuid,
267
+ workspace: undefined,
268
+ updated_at: MOCK_SESSIONS.active.updated_at || "",
269
+ user_uuid: MOCK_SESSIONS.active.user_uuid,
270
+ },
271
+ {
272
+ session_uuid: MOCK_SESSIONS.secondary.uuid,
273
+ title: MOCK_SESSIONS.secondary.title,
274
+ team_uuid: undefined,
275
+ agent_profile_uuid: MOCK_SESSIONS.secondary.agent_profile_uuid,
276
+ workspace: undefined,
277
+ updated_at: MOCK_SESSIONS.secondary.updated_at || "",
278
+ user_uuid: MOCK_SESSIONS.secondary.user_uuid,
279
+ },
280
+ ];
281
+ }
282
+
283
+ /**
284
+ * Helper to setup common mock behaviors
285
+ */
286
+ export function setupStandardMockBehaviors(mocks: {
287
+ database?: ReturnType<typeof createMockDatabase>;
288
+ apiKeyManager?: ReturnType<typeof createMockApiKeyManager>;
289
+ userConnectionManager?: ReturnType<typeof createMockUserConnectionManager>;
290
+ }) {
291
+ // Setup database mocks
292
+ if (mocks.database) {
293
+ mocks.database.spies.getUserFromUuid.mockImplementation(
294
+ (userId: string) => {
295
+ if (userId === MOCK_USERS.owner.uuid)
296
+ return Promise.resolve(MOCK_USERS.owner);
297
+ if (userId === MOCK_USERS.participant.uuid)
298
+ return Promise.resolve(MOCK_USERS.participant);
299
+ return Promise.resolve(null);
300
+ }
301
+ );
302
+
303
+ mocks.database.spies.sessionGetById.mockImplementation(
304
+ (sessionId: string) => {
305
+ if (sessionId === MOCK_SESSIONS.active.uuid)
306
+ return Promise.resolve(MOCK_SESSIONS.active);
307
+ if (sessionId === MOCK_SESSIONS.secondary.uuid)
308
+ return Promise.resolve(MOCK_SESSIONS.secondary);
309
+ if (sessionId === "new-session-uuid")
310
+ return Promise.resolve({
311
+ ...MOCK_SESSIONS.active,
312
+ uuid: "new-session-uuid",
313
+ });
314
+ return Promise.resolve(null);
315
+ }
316
+ );
317
+
318
+ mocks.database.spies.getAgentProfileById.mockImplementation(
319
+ (profileId: string) => {
320
+ if (profileId === "agent-profile-1")
321
+ return Promise.resolve(MOCK_AGENT_PROFILES.default);
322
+ if (profileId === "agent-profile-2")
323
+ return Promise.resolve(MOCK_AGENT_PROFILES.custom);
324
+ return Promise.resolve(null);
325
+ }
326
+ );
327
+
328
+ mocks.database.spies.getSavedAgentProfileById.mockImplementation(
329
+ (profileId: string) => {
330
+ if (profileId === "agent-profile-1") {
331
+ return Promise.resolve({
332
+ uuid: "agent-profile-1",
333
+ user_uuid: MOCK_USERS.owner.uuid,
334
+ profile_name: "Default Profile",
335
+ profile: MOCK_AGENT_PROFILES.default,
336
+ preferences: { auto_approve: {} },
337
+ });
338
+ }
339
+ if (profileId === "agent-profile-2") {
340
+ return Promise.resolve({
341
+ uuid: "agent-profile-2",
342
+ user_uuid: MOCK_USERS.owner.uuid,
343
+ profile_name: "Custom Profile",
344
+ profile: MOCK_AGENT_PROFILES.custom,
345
+ preferences: { auto_approve: {} },
346
+ });
347
+ }
348
+ return Promise.resolve(null);
349
+ }
350
+ );
351
+
352
+ mocks.database.spies.getUserApiKey.mockImplementation((userId: string) => {
353
+ if (userId === MOCK_USERS.owner.uuid) {
354
+ return Promise.resolve("valid-api-key");
355
+ }
356
+ if (userId === MOCK_USERS.participant.uuid) {
357
+ return Promise.resolve("participant-api-key");
358
+ }
359
+ return Promise.resolve(undefined);
360
+ });
361
+
362
+ mocks.database.spies.sessionCreate.mockImplementation(() =>
363
+ Promise.resolve()
364
+ );
365
+
366
+ mocks.database.spies.sessionParticipantAdd.mockImplementation(() =>
367
+ Promise.resolve()
368
+ );
369
+
370
+ mocks.database.spies.sessionParticipantRemove.mockImplementation(() =>
371
+ Promise.resolve()
372
+ );
373
+
374
+ // Setup sessionGetParticipants to return participants based on session
375
+ mocks.database.spies.sessionGetParticipants.mockImplementation(
376
+ (sessionId: string) => {
377
+ if (sessionId === MOCK_SESSIONS.active.uuid) {
378
+ return Promise.resolve([
379
+ { user_uuid: MOCK_USERS.owner.uuid, role: "owner" },
380
+ { user_uuid: MOCK_USERS.participant.uuid, role: "participant" },
381
+ ]);
382
+ }
383
+ if (sessionId === MOCK_SESSIONS.secondary.uuid) {
384
+ return Promise.resolve([
385
+ { user_uuid: MOCK_USERS.owner.uuid, role: "owner" },
386
+ ]);
387
+ }
388
+ return Promise.resolve([]);
389
+ }
390
+ );
391
+
392
+ // Setup getUserSessions to return empty array by default
393
+ mocks.database.spies.getUserSessions.mockImplementation(() =>
394
+ Promise.resolve([])
395
+ );
396
+ }
397
+
398
+ // Setup API key manager mocks
399
+ if (mocks.apiKeyManager) {
400
+ mocks.apiKeyManager.spies.verifyApiKey.mockImplementation(
401
+ (apiKey: string) => {
402
+ if (apiKey === "valid-api-key")
403
+ return Promise.resolve(MOCK_USERS.owner);
404
+ if (apiKey === "participant-api-key")
405
+ return Promise.resolve(MOCK_USERS.participant);
406
+ return Promise.resolve(null);
407
+ }
408
+ );
409
+ }
410
+
411
+ // Setup user connection manager mocks
412
+ if (mocks.userConnectionManager) {
413
+ mocks.userConnectionManager.spies.getLiveUserApiKey.mockImplementation(
414
+ (userId: string) => {
415
+ if (userId === MOCK_USERS.owner.uuid) return "valid-api-key";
416
+ if (userId === MOCK_USERS.participant.uuid)
417
+ return "participant-api-key";
418
+ return undefined;
419
+ }
420
+ );
421
+ }
422
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Collection of simple Agent tools.
3
+ */
4
+
5
+ import { OpenAI } from "openai";
6
+ import { Parser } from "expr-eval";
7
+
8
+ import { Agent, IAgentToolProvider, ToolCallResult } from "../../agent/agent";
9
+ import { IPlatform } from "../../agent/iplatform";
10
+ import { htmlToText } from "../utils/htmlToText";
11
+ import { getLogger } from "@xalia/xmcp/sdk";
12
+ import { webSearch } from "../utils/search";
13
+
14
+ const logger = getLogger();
15
+
16
+ /**
17
+ * Returns a function which parses an `args` struct and attempts to extract
18
+ * multiple string parameters with the given names. e.g.
19
+ *
20
+ * const parseFn = makeParseArgsFn(["arg0", "arg1"] as const)
21
+ *
22
+ * creates `parseFn: (args: unknown) => { arg0: string, arg1: string }` which
23
+ * can be used to parse tool arguments.
24
+ *
25
+ * NOTE, the complex type parameters ensures that the name list is a
26
+ * compile-time value, which in turn ensures that the return value of this
27
+ * function is well-typed.
28
+ */
29
+ export function makeParseArgsFn<
30
+ T extends readonly string[] & (string extends T[number] ? never : unknown),
31
+ >(names: T): (args: unknown) => { [K in T[number]]: string } {
32
+ return (args: unknown) => {
33
+ if (typeof args !== "object") {
34
+ throw new Error(`invalid args: ${typeof args}`);
35
+ }
36
+ const argsObj = args as Record<string, string>;
37
+ for (const name of names) {
38
+ const val = argsObj[name];
39
+ if (typeof val !== "string") {
40
+ throw new Error(`invalid expr args.${name}: ${typeof val}`);
41
+ }
42
+ }
43
+ return argsObj as { [K in T[number]]: string };
44
+ };
45
+ }
46
+
47
+ const DATETIME_DESC: OpenAI.ChatCompletionTool = {
48
+ type: "function",
49
+ function: {
50
+ name: "time_now",
51
+ description: "Current time",
52
+ },
53
+ };
54
+
55
+ export function isoWithTimezone(timeZone: string): string {
56
+ return (
57
+ new Intl.DateTimeFormat("sv-SE", {
58
+ timeZone,
59
+ year: "numeric",
60
+ month: "2-digit",
61
+ day: "2-digit",
62
+ hour: "2-digit",
63
+ minute: "2-digit",
64
+ second: "2-digit",
65
+ hour12: false,
66
+ timeZoneName: "short",
67
+ })
68
+ .format(new Date())
69
+ .replace(" ", "T") + ` (${timeZone})`
70
+ );
71
+ }
72
+
73
+ export function datetimeTool(timezone: string): IAgentToolProvider {
74
+ // eslint-disable-next-line @typescript-eslint/require-await
75
+ const toolFn = async () => {
76
+ return { response: isoWithTimezone(timezone) };
77
+ };
78
+ return {
79
+ // eslint-disable-next-line @typescript-eslint/require-await
80
+ setup: async (agent: Agent) => {
81
+ agent.addAgentTool(DATETIME_DESC, toolFn);
82
+ },
83
+ };
84
+ }
85
+
86
+ const ARITHMETIC_DESC: OpenAI.ChatCompletionTool = {
87
+ type: "function",
88
+ function: {
89
+ name: "arithmetic",
90
+ description: "Evaluate arithmetic expression",
91
+ parameters: {
92
+ type: "object",
93
+ properties: {
94
+ expr: {
95
+ type: "string",
96
+ description: "Expression containing +-*/()",
97
+ },
98
+ },
99
+ required: ["expr"],
100
+ },
101
+ },
102
+ };
103
+
104
+ export function calculatorEval(args: string): string {
105
+ try {
106
+ return String(Parser.evaluate(args));
107
+ } catch (e) {
108
+ if (typeof (e as { message: string }).message === "string") {
109
+ return (e as { message: string }).message;
110
+ }
111
+ return String(e);
112
+ }
113
+ }
114
+
115
+ export const calculatorTool: IAgentToolProvider = {
116
+ // eslint-disable-next-line @typescript-eslint/require-await
117
+ setup: async (agent: Agent) => {
118
+ const getExpr = makeParseArgsFn(["expr"] as const);
119
+ // eslint-disable-next-line @typescript-eslint/require-await
120
+ const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
121
+ const { expr } = getExpr(args);
122
+ return { response: calculatorEval(expr) };
123
+ };
124
+
125
+ agent.addAgentTool(ARITHMETIC_DESC, toolFn);
126
+ },
127
+ };
128
+
129
+ const RENDER_DESC: OpenAI.ChatCompletionTool = {
130
+ type: "function",
131
+ function: {
132
+ name: "render",
133
+ description: "Display the given html fragment inside a div element",
134
+ parameters: {
135
+ type: "object",
136
+ properties: {
137
+ html: {
138
+ type: "string",
139
+ description: "HTML fragment to render",
140
+ },
141
+ },
142
+ required: ["html"],
143
+ },
144
+ },
145
+ };
146
+
147
+ export function renderTool(platform: IPlatform): IAgentToolProvider {
148
+ const getHtml = makeParseArgsFn(["html"] as const);
149
+ const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
150
+ const { html } = getHtml(args);
151
+ await platform.renderHTML(html);
152
+ return { response: "" };
153
+ };
154
+
155
+ return {
156
+ // eslint-disable-next-line @typescript-eslint/require-await
157
+ setup: async (agent: Agent) => {
158
+ agent.addAgentTool(RENDER_DESC, toolFn);
159
+ },
160
+ };
161
+ }
162
+
163
+ const WEB_SEARCH_DESC: OpenAI.ChatCompletionTool = {
164
+ type: "function",
165
+ function: {
166
+ name: "web_search",
167
+ description: "Web search",
168
+ parameters: {
169
+ type: "object",
170
+ properties: {
171
+ query: {
172
+ type: "string",
173
+ description: "Search query text",
174
+ },
175
+ },
176
+ required: ["query"],
177
+ },
178
+ },
179
+ };
180
+
181
+ export function webSearchTool(): IAgentToolProvider {
182
+ const getQuery = makeParseArgsFn(["query"] as const);
183
+ const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
184
+ const { query } = getQuery(args);
185
+ logger.debug(`[web_search]: query: ${query}`);
186
+ const results = await webSearch(query);
187
+ logger.debug(`[web_search]: results: ${results}`);
188
+ return { response: JSON.stringify(results) };
189
+ };
190
+
191
+ return {
192
+ // eslint-disable-next-line @typescript-eslint/require-await
193
+ setup: async (agent: Agent) => {
194
+ agent.addAgentTool(WEB_SEARCH_DESC, toolFn);
195
+ },
196
+ };
197
+ }
198
+
199
+ // open_url
200
+
201
+ /**
202
+ * For now, this matches the duckduckgo-mcp-server length. Could extend
203
+ * depending on the application / model etc.
204
+ */
205
+ const _OPEN_URL_MAX_LENGTH_STR = process.env["OPEN_URL_MAX_LENGTH"] || "8000";
206
+ const OPEN_URL_MAX_LENGTH: number = parseInt(_OPEN_URL_MAX_LENGTH_STR, 10);
207
+
208
+ const OPEN_URL_DESC: OpenAI.ChatCompletionTool = {
209
+ type: "function",
210
+ function: {
211
+ name: "open_url",
212
+ description: "Download content from a URL",
213
+ parameters: {
214
+ type: "object",
215
+ properties: {
216
+ url: {
217
+ type: "string",
218
+ description: "URL to download",
219
+ },
220
+ },
221
+ required: ["url"],
222
+ },
223
+ },
224
+ };
225
+
226
+ export async function openURL(url: string): Promise<string> {
227
+ const response = await fetch(url);
228
+ if (!response.ok) {
229
+ const status = String(response.status);
230
+ const code = response.statusText;
231
+ throw new Error(`Failed to fetch ${url}: ${status} ${code}`);
232
+ }
233
+ const html = await response.text();
234
+ return htmlToText(html, OPEN_URL_MAX_LENGTH);
235
+ }
236
+
237
+ export function openURLTool(): IAgentToolProvider {
238
+ const getURL = makeParseArgsFn(["url"] as const);
239
+ const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
240
+ const { url } = getURL(args);
241
+ return { response: await openURL(url) };
242
+ };
243
+
244
+ return {
245
+ // eslint-disable-next-line @typescript-eslint/require-await
246
+ setup: async (agent: Agent) => {
247
+ agent.addAgentTool(OPEN_URL_DESC, toolFn);
248
+ },
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Add a set of agent tools for chat sessions.
254
+ */
255
+ export async function addDefaultChatTools(
256
+ agent: Agent,
257
+ timezone: string,
258
+ platform: IPlatform
259
+ ): Promise<void> {
260
+ await agent.addAgentToolProvider(datetimeTool(timezone));
261
+ await agent.addAgentToolProvider(calculatorTool);
262
+ await agent.addAgentToolProvider(renderTool(platform));
263
+ await agent.addAgentToolProvider(webSearchTool());
264
+ await agent.addAgentToolProvider(openURLTool());
265
+ }