@xalia/agent 0.6.8 → 0.6.9
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.
- package/.env.development +1 -0
- package/dist/agent/src/agent/agent.js +100 -77
- package/dist/agent/src/agent/agentUtils.js +21 -16
- package/dist/agent/src/agent/compressingContextManager.js +10 -14
- package/dist/agent/src/agent/context.js +101 -127
- package/dist/agent/src/agent/contextWithWorkspace.js +133 -0
- package/dist/agent/src/agent/imageGenLLM.js +0 -6
- package/dist/agent/src/agent/imageGenerator.js +2 -10
- package/dist/agent/src/agent/openAILLMStreaming.js +5 -2
- package/dist/agent/src/agent/sudoMcpServerManager.js +21 -9
- package/dist/agent/src/chat/client/chatClient.js +35 -2
- package/dist/agent/src/chat/client/connection.js +6 -1
- package/dist/agent/src/chat/client/sessionClient.js +0 -7
- package/dist/agent/src/chat/data/dbSessionMessages.js +11 -0
- package/dist/agent/src/chat/protocol/messages.js +4 -0
- package/dist/agent/src/chat/server/chatContextManager.js +149 -139
- package/dist/agent/src/chat/server/imageGeneratorTools.js +19 -8
- package/dist/agent/src/chat/server/openAIRouterLLM.js +114 -0
- package/dist/agent/src/chat/server/openSession.js +57 -58
- package/dist/agent/src/chat/server/server.js +6 -2
- package/dist/agent/src/chat/server/sessionRegistry.js +65 -6
- package/dist/agent/src/chat/server/sessionRegistry.test.js +1 -1
- package/dist/agent/src/chat/server/tools.js +52 -17
- package/dist/agent/src/test/chatContextManager.test.js +31 -29
- package/dist/agent/src/test/clientServerConnection.test.js +1 -2
- package/dist/agent/src/test/compressingContextManager.test.js +22 -36
- package/dist/agent/src/test/context.test.js +55 -17
- package/dist/agent/src/test/contextTestTools.js +87 -0
- package/dist/agent/src/tool/chatMain.js +22 -8
- package/package.json +1 -1
- package/scripts/test_chat +3 -0
- package/src/agent/agent.ts +170 -125
- package/src/agent/agentUtils.ts +31 -20
- package/src/agent/compressingContextManager.ts +13 -44
- package/src/agent/context.ts +165 -159
- package/src/agent/contextWithWorkspace.ts +162 -0
- package/src/agent/imageGenLLM.ts +0 -8
- package/src/agent/imageGenerator.ts +3 -18
- package/src/agent/openAILLMStreaming.ts +20 -3
- package/src/agent/sudoMcpServerManager.ts +41 -20
- package/src/chat/client/chatClient.ts +47 -3
- package/src/chat/client/connection.ts +11 -1
- package/src/chat/client/sessionClient.ts +0 -8
- package/src/chat/data/dataModels.ts +6 -0
- package/src/chat/data/dbSessionMessages.ts +34 -0
- package/src/chat/protocol/messages.ts +35 -8
- package/src/chat/server/chatContextManager.ts +210 -197
- package/src/chat/server/connectionManager.ts +1 -1
- package/src/chat/server/imageGeneratorTools.ts +31 -18
- package/src/chat/server/openAIRouterLLM.ts +171 -0
- package/src/chat/server/openSession.ts +87 -100
- package/src/chat/server/server.ts +6 -2
- package/src/chat/server/sessionFileManager.ts +5 -5
- package/src/chat/server/sessionRegistry.test.ts +0 -1
- package/src/chat/server/sessionRegistry.ts +100 -4
- package/src/chat/server/tools.ts +73 -35
- package/src/test/agent.test.ts +8 -7
- package/src/test/chatContextManager.test.ts +42 -37
- package/src/test/clientServerConnection.test.ts +0 -2
- package/src/test/compressingContextManager.test.ts +29 -34
- package/src/test/context.test.ts +59 -15
- package/src/test/contextTestTools.ts +95 -0
- package/src/tool/chatMain.ts +26 -12
|
@@ -27,9 +27,12 @@ import {
|
|
|
27
27
|
ServerToClient,
|
|
28
28
|
ClientControlAgentProfileCreate,
|
|
29
29
|
ClientControlAgentProfileDelete,
|
|
30
|
+
ClientControlAddCustomMcpServer,
|
|
31
|
+
ClientControlRemoveCustomMcpServer,
|
|
30
32
|
} from "../protocol/messages";
|
|
31
33
|
import { GUEST_TOKEN_PREFIX, OpenSession } from "./openSession";
|
|
32
34
|
import {
|
|
35
|
+
CustomMcpServerDescriptor,
|
|
33
36
|
SessionCreateData,
|
|
34
37
|
SessionData,
|
|
35
38
|
SessionDescriptor,
|
|
@@ -47,6 +50,62 @@ const logger = getLogger();
|
|
|
47
50
|
|
|
48
51
|
export type GuestUser = UserData & { guest_for_session: string };
|
|
49
52
|
|
|
53
|
+
// server_name => url
|
|
54
|
+
type CustomMcpServersForUser = Map<string, CustomMcpServerDescriptor>;
|
|
55
|
+
|
|
56
|
+
export class CustomMcpServerManager {
|
|
57
|
+
private perUser: Map<string, CustomMcpServersForUser>;
|
|
58
|
+
|
|
59
|
+
constructor(_db: Database) {
|
|
60
|
+
this.perUser = new Map();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getForUser(user_uuid: string): Record<string, CustomMcpServerDescriptor> {
|
|
64
|
+
const servers = this.perUser.get(user_uuid);
|
|
65
|
+
if (!servers) {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Object.fromEntries(servers.entries());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async addForUser(
|
|
73
|
+
user_uuid: string,
|
|
74
|
+
server_name: string,
|
|
75
|
+
description: string,
|
|
76
|
+
url: string
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
// TODO: persistence to DB
|
|
79
|
+
|
|
80
|
+
const userServers = this.getUserServers(user_uuid);
|
|
81
|
+
if (userServers.has(server_name)) {
|
|
82
|
+
throw new Error(`server ${server_name} already added`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
userServers.set(server_name, { name: server_name, description, url });
|
|
86
|
+
|
|
87
|
+
return Promise.resolve();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async removeForUser(user_uuid: string, server_name: string): Promise<void> {
|
|
91
|
+
const userServers = this.getUserServers(user_uuid);
|
|
92
|
+
if (userServers.has(server_name)) {
|
|
93
|
+
userServers.delete(server_name);
|
|
94
|
+
}
|
|
95
|
+
return Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private getUserServers(user_uuid: string): CustomMcpServersForUser {
|
|
99
|
+
let userServers = this.perUser.get(user_uuid);
|
|
100
|
+
if (!userServers) {
|
|
101
|
+
userServers = new Map();
|
|
102
|
+
this.perUser.set(user_uuid, userServers);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return userServers;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
50
109
|
export class SessionRegistry implements IMessageProcessor<ClientToServer> {
|
|
51
110
|
// In memory session-user/user-session mappings
|
|
52
111
|
// Note: this mappings ONLY trackes online users and will
|
|
@@ -64,13 +123,15 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
|
|
|
64
123
|
|
|
65
124
|
private apiKeyManager: ApiKeyManager;
|
|
66
125
|
|
|
126
|
+
private customMcpServerManager: CustomMcpServerManager;
|
|
127
|
+
|
|
67
128
|
constructor(
|
|
68
129
|
private db: Database,
|
|
69
130
|
private connectionManager: IUserConnectionManager<ServerToClient>,
|
|
70
|
-
private llmUrl: string,
|
|
71
131
|
private xmcpUrl: string
|
|
72
132
|
) {
|
|
73
133
|
this.apiKeyManager = new ApiKeyManager(db);
|
|
134
|
+
this.customMcpServerManager = new CustomMcpServerManager(db);
|
|
74
135
|
}
|
|
75
136
|
|
|
76
137
|
/**
|
|
@@ -293,6 +354,12 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
|
|
|
293
354
|
case "control_remove_team_user":
|
|
294
355
|
await this.handleRemoveTeamUser(connectionId, userId, message);
|
|
295
356
|
break;
|
|
357
|
+
case "control_add_custom_mcp_server":
|
|
358
|
+
await this.handleAddCustomMcpServer(userId, message);
|
|
359
|
+
break;
|
|
360
|
+
case "control_remove_custom_mcp_server":
|
|
361
|
+
await this.handleRemoveCustomMcpServer(userId, message);
|
|
362
|
+
break;
|
|
296
363
|
default: {
|
|
297
364
|
const exhaustive: never = message;
|
|
298
365
|
// unknown connection type should be a fatal error
|
|
@@ -303,6 +370,37 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
|
|
|
303
370
|
}
|
|
304
371
|
}
|
|
305
372
|
|
|
373
|
+
private async handleAddCustomMcpServer(
|
|
374
|
+
userId: string,
|
|
375
|
+
message: ClientControlAddCustomMcpServer
|
|
376
|
+
): Promise<void> {
|
|
377
|
+
await this.customMcpServerManager.addForUser(
|
|
378
|
+
userId,
|
|
379
|
+
message.server_name,
|
|
380
|
+
message.description,
|
|
381
|
+
message.url
|
|
382
|
+
);
|
|
383
|
+
this.connectionManager.sendToUsers([userId], {
|
|
384
|
+
type: "control_custom_mcp_server_added",
|
|
385
|
+
server_name: message.server_name,
|
|
386
|
+
url: message.url,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async handleRemoveCustomMcpServer(
|
|
391
|
+
userId: string,
|
|
392
|
+
message: ClientControlRemoveCustomMcpServer
|
|
393
|
+
): Promise<void> {
|
|
394
|
+
await this.customMcpServerManager.removeForUser(
|
|
395
|
+
userId,
|
|
396
|
+
message.server_name
|
|
397
|
+
);
|
|
398
|
+
this.connectionManager.sendToUsers([userId], {
|
|
399
|
+
type: "control_custom_mcp_server_removed",
|
|
400
|
+
server_name: message.server_name,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
306
404
|
/**
|
|
307
405
|
* Handle session_list request
|
|
308
406
|
*/
|
|
@@ -333,6 +431,7 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
|
|
|
333
431
|
team_sessions: teamSessions,
|
|
334
432
|
user_agents: userAgents,
|
|
335
433
|
client_message_id: message.client_message_id,
|
|
434
|
+
custom_mcp_servers: this.customMcpServerManager.getForUser(userId),
|
|
336
435
|
};
|
|
337
436
|
|
|
338
437
|
this.connectionManager.sendToConnection(connectionId, response);
|
|
@@ -755,7 +854,6 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
|
|
|
755
854
|
const session = await OpenSession.initWithExistingSession(
|
|
756
855
|
this.db,
|
|
757
856
|
sessionId,
|
|
758
|
-
this.llmUrl,
|
|
759
857
|
this.xmcpUrl,
|
|
760
858
|
this.connectionManager
|
|
761
859
|
);
|
|
@@ -1039,7 +1137,6 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
|
|
|
1039
1137
|
const openSession = await OpenSession.initWithEmptySession(
|
|
1040
1138
|
this.db,
|
|
1041
1139
|
newSessionData,
|
|
1042
|
-
this.llmUrl,
|
|
1043
1140
|
this.xmcpUrl,
|
|
1044
1141
|
this.connectionManager
|
|
1045
1142
|
);
|
|
@@ -1083,7 +1180,6 @@ export class SessionRegistry implements IMessageProcessor<ClientToServer> {
|
|
|
1083
1180
|
const openSession = await OpenSession.initWithEmptySession(
|
|
1084
1181
|
this.db,
|
|
1085
1182
|
newSessionData,
|
|
1086
|
-
this.llmUrl,
|
|
1087
1183
|
this.xmcpUrl,
|
|
1088
1184
|
this.connectionManager
|
|
1089
1185
|
);
|
package/src/chat/server/tools.ts
CHANGED
|
@@ -4,18 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
import { Parser } from "expr-eval";
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
Agent,
|
|
9
|
+
AgentEx,
|
|
10
|
+
IAgentToolProvider,
|
|
11
|
+
ToolCallResult,
|
|
12
|
+
} from "../../agent/agent";
|
|
7
13
|
import { getLogger } from "@xalia/xmcp/sdk";
|
|
8
14
|
|
|
9
|
-
import { Agent, IAgentToolProvider, ToolCallResult } from "../../agent/agent";
|
|
10
15
|
import { htmlToText } from "../utils/htmlToText";
|
|
11
16
|
import { webSearch } from "../utils/search";
|
|
12
17
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
ChatSessionFileManager,
|
|
19
|
+
ToolCallResultWithFileRef,
|
|
20
|
+
fileManagerTool,
|
|
16
21
|
} from "./sessionFileManager";
|
|
17
22
|
import { genImageFileTool } from "./imageGeneratorTools";
|
|
18
23
|
import { ToolDescriptor } from "../../agent/llm";
|
|
24
|
+
import { IPlatform } from "../../agent/iplatform";
|
|
19
25
|
|
|
20
26
|
const logger = getLogger();
|
|
21
27
|
|
|
@@ -111,7 +117,7 @@ export function datetimeTool(timezone: string): IAgentToolProvider {
|
|
|
111
117
|
};
|
|
112
118
|
return {
|
|
113
119
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
114
|
-
setup: async (agent:
|
|
120
|
+
setup: async (agent: AgentEx) => {
|
|
115
121
|
agent.addAgentTool(DATETIME_DESC, toolFn);
|
|
116
122
|
},
|
|
117
123
|
};
|
|
@@ -148,12 +154,14 @@ export function calculatorEval(args: string): string {
|
|
|
148
154
|
|
|
149
155
|
export const calculatorTool: IAgentToolProvider = {
|
|
150
156
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
151
|
-
setup: async (agent:
|
|
157
|
+
setup: async (agent: AgentEx) => {
|
|
152
158
|
const getExpr = makeParseArgsFn(["expr"] as const);
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
const toolFn = async (
|
|
160
|
+
_: AgentEx,
|
|
161
|
+
args: unknown
|
|
162
|
+
): Promise<ToolCallResult> => {
|
|
155
163
|
const { expr } = getExpr(args);
|
|
156
|
-
return { response: calculatorEval(expr) };
|
|
164
|
+
return Promise.resolve({ response: calculatorEval(expr) });
|
|
157
165
|
};
|
|
158
166
|
|
|
159
167
|
agent.addAgentTool(ARITHMETIC_DESC, toolFn);
|
|
@@ -164,28 +172,45 @@ const RENDER_DESC: ToolDescriptor = {
|
|
|
164
172
|
type: "function",
|
|
165
173
|
function: {
|
|
166
174
|
name: "render",
|
|
167
|
-
description:
|
|
175
|
+
description: [
|
|
176
|
+
"Display HTML using only safe elements",
|
|
177
|
+
"Never include <script>, <iframe>, <object>, <embed>, <form>, <meta>,",
|
|
178
|
+
"<link>, <style>, event handlers (onclick, etc.), or javascript: URLs.",
|
|
179
|
+
"If the tool result is [HTML_SANITIZATION_WARNING], do not mention it.",
|
|
180
|
+
"Retry with only safe elements. If warning repeats, tell the user you",
|
|
181
|
+
"cannot render unsafe HTML."
|
|
182
|
+
].join(" "),
|
|
168
183
|
parameters: {
|
|
169
184
|
type: "object",
|
|
170
185
|
properties: {
|
|
171
|
-
name: {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
},
|
|
175
|
-
summary: {
|
|
176
|
-
type: "string",
|
|
177
|
-
description: "One line summary",
|
|
178
|
-
},
|
|
179
|
-
html: {
|
|
180
|
-
type: "string",
|
|
181
|
-
description: "HTML fragment to render",
|
|
182
|
-
},
|
|
186
|
+
name: { type: "string", description: "Filename for the HTML" },
|
|
187
|
+
summary: { type: "string", description: "One line summary" },
|
|
188
|
+
html: { type: "string", description: "HTML fragment to render" },
|
|
183
189
|
},
|
|
184
190
|
required: ["name", "summary", "html"],
|
|
185
191
|
},
|
|
186
192
|
},
|
|
187
193
|
};
|
|
188
194
|
|
|
195
|
+
function validateHtmlSafety(html: string): string | undefined {
|
|
196
|
+
const issues: string[] = [];
|
|
197
|
+
if (/<script[\s>]/i.test(html)) issues.push("<script> tag");
|
|
198
|
+
if (/<iframe[\s>]/i.test(html)) issues.push("<iframe> tag");
|
|
199
|
+
if (/<object[\s>]/i.test(html)) issues.push("<object> tag");
|
|
200
|
+
if (/<embed[\s>]/i.test(html)) issues.push("<embed> tag");
|
|
201
|
+
if (/\bon\w+\s*=/.test(html)) issues.push("event handler (e.g., onclick)");
|
|
202
|
+
if (/javascript:/i.test(html)) issues.push("javascript: URL");
|
|
203
|
+
if (/<meta[\s>]/i.test(html)) issues.push("<meta> tag");
|
|
204
|
+
if (/<form[\s>]/i.test(html)) issues.push("<form> tag");
|
|
205
|
+
if (/<style[\s>]/i.test(html)) issues.push("<style> tag");
|
|
206
|
+
if (/<link[\s>]/i.test(html)) issues.push("<link> tag");
|
|
207
|
+
|
|
208
|
+
if (issues.length > 0) {
|
|
209
|
+
return `Unsafe HTML: ${issues.join(", ")}`;
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
189
214
|
export function renderTool(
|
|
190
215
|
fileManager: ChatSessionFileManager
|
|
191
216
|
): IAgentToolProvider {
|
|
@@ -195,10 +220,21 @@ export function renderTool(
|
|
|
195
220
|
"html",
|
|
196
221
|
] as const);
|
|
197
222
|
const toolFn = async (
|
|
198
|
-
_:
|
|
223
|
+
_: AgentEx,
|
|
199
224
|
args: unknown
|
|
200
225
|
): Promise<ToolCallResultWithFileRef> => {
|
|
201
226
|
const { name, summary, html } = getNameSummeryHtml(args);
|
|
227
|
+
const safetyError = validateHtmlSafety(html);
|
|
228
|
+
if (safetyError) {
|
|
229
|
+
return {
|
|
230
|
+
response: "[HTML_SANITIZATION_WARNING]",
|
|
231
|
+
overwriteResponse: "",
|
|
232
|
+
structuredContent: {
|
|
233
|
+
kind: "htmlSanitizationWarning",
|
|
234
|
+
message: safetyError,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
202
238
|
const mimeType = "text/html";
|
|
203
239
|
const dataURL = `data:${mimeType},${html}`;
|
|
204
240
|
await fileManager.putFileContent(name, summary, dataURL);
|
|
@@ -211,7 +247,7 @@ export function renderTool(
|
|
|
211
247
|
|
|
212
248
|
return {
|
|
213
249
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
214
|
-
setup: async (agent:
|
|
250
|
+
setup: async (agent: AgentEx) => {
|
|
215
251
|
agent.addAgentTool(RENDER_DESC, toolFn);
|
|
216
252
|
},
|
|
217
253
|
};
|
|
@@ -237,7 +273,7 @@ const WEB_SEARCH_DESC: ToolDescriptor = {
|
|
|
237
273
|
|
|
238
274
|
export function webSearchTool(): IAgentToolProvider {
|
|
239
275
|
const getQuery = makeParseArgsFn(["query"] as const);
|
|
240
|
-
const toolFn = async (_:
|
|
276
|
+
const toolFn = async (_: AgentEx, args: unknown): Promise<ToolCallResult> => {
|
|
241
277
|
const { query } = getQuery(args);
|
|
242
278
|
logger.debug(`[web_search]: query: ${query}`);
|
|
243
279
|
const results = await webSearch(query);
|
|
@@ -247,7 +283,7 @@ export function webSearchTool(): IAgentToolProvider {
|
|
|
247
283
|
|
|
248
284
|
return {
|
|
249
285
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
250
|
-
setup: async (agent:
|
|
286
|
+
setup: async (agent: AgentEx) => {
|
|
251
287
|
agent.addAgentTool(WEB_SEARCH_DESC, toolFn);
|
|
252
288
|
},
|
|
253
289
|
};
|
|
@@ -293,14 +329,14 @@ export async function openURL(url: string): Promise<string> {
|
|
|
293
329
|
|
|
294
330
|
export function openURLTool(): IAgentToolProvider {
|
|
295
331
|
const getURL = makeParseArgsFn(["url"] as const);
|
|
296
|
-
const toolFn = async (_:
|
|
332
|
+
const toolFn = async (_: AgentEx, args: unknown): Promise<ToolCallResult> => {
|
|
297
333
|
const { url } = getURL(args);
|
|
298
334
|
return { response: await openURL(url) };
|
|
299
335
|
};
|
|
300
336
|
|
|
301
337
|
return {
|
|
302
338
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
303
|
-
setup: async (agent:
|
|
339
|
+
setup: async (agent: AgentEx) => {
|
|
304
340
|
agent.addAgentTool(OPEN_URL_DESC, toolFn);
|
|
305
341
|
},
|
|
306
342
|
};
|
|
@@ -320,8 +356,11 @@ const TEST_DESC: ToolDescriptor = {
|
|
|
320
356
|
};
|
|
321
357
|
|
|
322
358
|
export const testTool: IAgentToolProvider = {
|
|
323
|
-
setup: (agent:
|
|
324
|
-
const toolFn = (
|
|
359
|
+
setup: (agent: AgentEx) => {
|
|
360
|
+
const toolFn = (
|
|
361
|
+
_agent: AgentEx,
|
|
362
|
+
_args: unknown
|
|
363
|
+
): Promise<ToolCallResult> => {
|
|
325
364
|
// Return an object with structuredContent and _meta
|
|
326
365
|
return Promise.resolve({
|
|
327
366
|
response: "Some text",
|
|
@@ -338,11 +377,10 @@ export const testTool: IAgentToolProvider = {
|
|
|
338
377
|
* Add a set of agent tools for chat sessions.
|
|
339
378
|
*/
|
|
340
379
|
export async function addDefaultChatTools(
|
|
341
|
-
agent: Agent,
|
|
380
|
+
agent: AgentEx | Agent,
|
|
342
381
|
timezone: string,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
llmApiKey: string
|
|
382
|
+
platform: IPlatform,
|
|
383
|
+
fileManager: ChatSessionFileManager
|
|
346
384
|
): Promise<void> {
|
|
347
385
|
await agent.addAgentToolProvider(datetimeTool(timezone));
|
|
348
386
|
await agent.addAgentToolProvider(calculatorTool);
|
|
@@ -351,7 +389,7 @@ export async function addDefaultChatTools(
|
|
|
351
389
|
await agent.addAgentToolProvider(openURLTool());
|
|
352
390
|
await agent.addAgentToolProvider(fileManagerTool(fileManager));
|
|
353
391
|
await agent.addAgentToolProvider(
|
|
354
|
-
await genImageFileTool(
|
|
392
|
+
await genImageFileTool(platform, fileManager)
|
|
355
393
|
);
|
|
356
394
|
if (DEVELOPMENT) {
|
|
357
395
|
await agent.addAgentToolProvider(testTool);
|
package/src/test/agent.test.ts
CHANGED
|
@@ -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:
|
|
70
|
+
tool0_fn: (agent: AgentEx, args: unknown) => Promise<ToolCallResult>;
|
|
70
71
|
test_tool_descriptor: ToolDescriptor;
|
|
71
|
-
test_tool_fn: (agent:
|
|
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 = (_:
|
|
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
|
-
_:
|
|
119
|
+
_: AgentEx,
|
|
119
120
|
args: unknown
|
|
120
121
|
): Promise<ToolCallResult> => {
|
|
121
122
|
const { param1, param2 } = args as { param1: string; param2: number };
|
|
@@ -312,7 +313,7 @@ describe("Agent", () => {
|
|
|
312
313
|
await createTestAgent(script);
|
|
313
314
|
|
|
314
315
|
const toolProvider: IAgentToolProvider = {
|
|
315
|
-
setup: (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(() => {
|
|
@@ -437,7 +438,7 @@ describe("Agent", () => {
|
|
|
437
438
|
return JSON.stringify(transformArgs(args));
|
|
438
439
|
};
|
|
439
440
|
const newToolFn = async (
|
|
440
|
-
agent:
|
|
441
|
+
agent: AgentEx,
|
|
441
442
|
args: unknown
|
|
442
443
|
): Promise<ToolCallResult> => {
|
|
443
444
|
const result = await test_tool_fn(agent, args);
|
|
@@ -507,7 +508,7 @@ describe("Agent", () => {
|
|
|
507
508
|
return result.toUpperCase();
|
|
508
509
|
};
|
|
509
510
|
const newToolFn = async (
|
|
510
|
-
agent:
|
|
511
|
+
agent: AgentEx,
|
|
511
512
|
args: unknown
|
|
512
513
|
): Promise<ToolCallResult> => {
|
|
513
514
|
const result = await test_tool_fn(agent, args);
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolveConversationWithCheckpoint,
|
|
8
8
|
} from "../chat/server/chatContextManager";
|
|
9
9
|
import { createUserMessageEnsure } from "../agent/agent";
|
|
10
|
-
import { AssistantMessageParam, ToolMessageParam } from "../agent/llm";
|
|
10
|
+
import { AssistantMessageParam, ILLM, ToolMessageParam } from "../agent/llm";
|
|
11
11
|
import { createCheckpointMessage } from "../agent/compressingContextManager";
|
|
12
12
|
import {
|
|
13
13
|
MESSAGE_INDEX_FULL_INCREMENT,
|
|
@@ -65,7 +65,10 @@ const MESSAGES: SessionMessage[] = [
|
|
|
65
65
|
},
|
|
66
66
|
];
|
|
67
67
|
|
|
68
|
-
function testSuccessfulAgentLoop(
|
|
68
|
+
async function testSuccessfulAgentLoop(
|
|
69
|
+
cm: ChatContextManager,
|
|
70
|
+
startIdx: number
|
|
71
|
+
) {
|
|
69
72
|
// 2 user messages arrive as ClientUserMessage. Assign message indices and
|
|
70
73
|
// convert them to ServerUserMessages.
|
|
71
74
|
|
|
@@ -104,11 +107,11 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
|
|
|
104
107
|
|
|
105
108
|
assert(serverUserMessage0 && serverUserMessage1);
|
|
106
109
|
const userMsgs = [serverUserMessage0, serverUserMessage1];
|
|
107
|
-
const {
|
|
110
|
+
const { contextTx, agentFirstChunk } = await cm.startAgentResponse(userMsgs);
|
|
108
111
|
expect(agentFirstChunk.message_idx).eql(
|
|
109
112
|
startIdx + 2 * MESSAGE_INDEX_FULL_INCREMENT
|
|
110
113
|
);
|
|
111
|
-
expect(
|
|
114
|
+
expect(contextTx.getLLMContext().slice(1)).eql(
|
|
112
115
|
userMsgs.map((su) => {
|
|
113
116
|
return {
|
|
114
117
|
role: "user",
|
|
@@ -120,7 +123,7 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
|
|
|
120
123
|
|
|
121
124
|
// The agent sends message chunks. Check the message_idx
|
|
122
125
|
|
|
123
|
-
const chunkMsg0 =
|
|
126
|
+
const chunkMsg0 = contextTx.processAgentMessageChunk("AgentMe", false);
|
|
124
127
|
expect(chunkMsg0).eql({
|
|
125
128
|
type: "agent_msg_chunk",
|
|
126
129
|
session_id: "some_session_id",
|
|
@@ -128,7 +131,7 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
|
|
|
128
131
|
message: "AgentMe",
|
|
129
132
|
end: false,
|
|
130
133
|
});
|
|
131
|
-
const chunkMsg1 =
|
|
134
|
+
const chunkMsg1 = contextTx.processAgentMessageChunk("ssage2", true);
|
|
132
135
|
expect(chunkMsg1).eql({
|
|
133
136
|
type: "agent_msg_chunk",
|
|
134
137
|
session_id: "some_session_id",
|
|
@@ -162,7 +165,8 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
|
|
|
162
165
|
content: "AgentMessage2",
|
|
163
166
|
};
|
|
164
167
|
|
|
165
|
-
|
|
168
|
+
contextTx.addMessage(agentResponseWithToolCall);
|
|
169
|
+
contextTx.processAgentResponse(agentResponseWithToolCall);
|
|
166
170
|
|
|
167
171
|
// Meanwhile, a new user message comes in. It is assigned idx
|
|
168
172
|
// startIdx + 3*MESSAGE_INDEX_FULL_INCREMENT.
|
|
@@ -198,15 +202,17 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
|
|
|
198
202
|
},
|
|
199
203
|
];
|
|
200
204
|
|
|
201
|
-
|
|
205
|
+
contextTx.addMessage(toolCallResults[0]);
|
|
206
|
+
contextTx.addMessage(toolCallResults[1]);
|
|
202
207
|
|
|
208
|
+
const toolCallMesasge0 = contextTx.processToolCallResult(toolCallResults[0]);
|
|
203
209
|
expect(toolCallMesasge0.message_idx).eql(
|
|
204
210
|
startIdx +
|
|
205
211
|
2 * MESSAGE_INDEX_FULL_INCREMENT +
|
|
206
212
|
1 * MESSAGE_INDEX_SUB_INCREMENT
|
|
207
213
|
);
|
|
208
214
|
|
|
209
|
-
const toolCallMesasge1 =
|
|
215
|
+
const toolCallMesasge1 = contextTx.processToolCallResult(toolCallResults[1]);
|
|
210
216
|
expect(toolCallMesasge1.message_idx).eql(
|
|
211
217
|
startIdx +
|
|
212
218
|
2 * MESSAGE_INDEX_FULL_INCREMENT +
|
|
@@ -220,19 +226,14 @@ function testSuccessfulAgentLoop(cm: ChatContextManager, startIdx: number) {
|
|
|
220
226
|
content: "AgentMessage2",
|
|
221
227
|
};
|
|
222
228
|
|
|
223
|
-
|
|
229
|
+
contextTx.addMessage(finalAgentResponse);
|
|
224
230
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
cm.addMessage(toolCallResults[0]);
|
|
228
|
-
cm.addMessage(toolCallResults[1]);
|
|
229
|
-
cm.addMessage(finalAgentResponse);
|
|
231
|
+
// Agent then returns the final response to the caller (OpenSession), which
|
|
232
|
+
// sets it on the Tx.
|
|
230
233
|
|
|
231
|
-
|
|
234
|
+
contextTx.processAgentResponse(finalAgentResponse);
|
|
232
235
|
|
|
233
|
-
cm.
|
|
234
|
-
|
|
235
|
-
const dbMessages = cm.endAgentResponse();
|
|
236
|
+
const dbMessages = await cm.endAgentResponse(contextTx);
|
|
236
237
|
|
|
237
238
|
// Expect
|
|
238
239
|
const expectDbMessages: SessionMessage[] = [
|
|
@@ -369,7 +370,7 @@ describe("IndexingCompressingContextManager", () => {
|
|
|
369
370
|
});
|
|
370
371
|
});
|
|
371
372
|
|
|
372
|
-
it("conversation processing behaviour", () => {
|
|
373
|
+
it("conversation processing behaviour", async () => {
|
|
373
374
|
const writer = {
|
|
374
375
|
writeCheckpoint: (_sc: SessionCheckpoint) => {
|
|
375
376
|
return new Promise<void>((r) => {
|
|
@@ -384,17 +385,15 @@ describe("IndexingCompressingContextManager", () => {
|
|
|
384
385
|
"some_session_id",
|
|
385
386
|
"no_such_user",
|
|
386
387
|
undefined,
|
|
387
|
-
"",
|
|
388
|
-
"",
|
|
389
|
-
"",
|
|
390
388
|
writer,
|
|
391
|
-
new MemoryFileManager()
|
|
389
|
+
new MemoryFileManager(),
|
|
390
|
+
undefined as unknown as ILLM
|
|
392
391
|
);
|
|
393
392
|
|
|
394
|
-
testSuccessfulAgentLoop(cm, 0);
|
|
393
|
+
await testSuccessfulAgentLoop(cm, 0);
|
|
395
394
|
});
|
|
396
395
|
|
|
397
|
-
it("error handling", () => {
|
|
396
|
+
it("error handling", async () => {
|
|
398
397
|
const writer = {
|
|
399
398
|
writeCheckpoint: (_sc: SessionCheckpoint) => {
|
|
400
399
|
return new Promise<void>((r) => {
|
|
@@ -409,11 +408,9 @@ describe("IndexingCompressingContextManager", () => {
|
|
|
409
408
|
"some_session_id",
|
|
410
409
|
"no_such_user",
|
|
411
410
|
undefined,
|
|
412
|
-
"",
|
|
413
|
-
"",
|
|
414
|
-
"",
|
|
415
411
|
writer,
|
|
416
|
-
new MemoryFileManager()
|
|
412
|
+
new MemoryFileManager(),
|
|
413
|
+
undefined as unknown as ILLM
|
|
417
414
|
);
|
|
418
415
|
|
|
419
416
|
// 1 user messages
|
|
@@ -438,11 +435,11 @@ describe("IndexingCompressingContextManager", () => {
|
|
|
438
435
|
// MESSAGE_INDEX_FULL_INCREMENT
|
|
439
436
|
|
|
440
437
|
assert(serverUserMessage0);
|
|
441
|
-
const {
|
|
438
|
+
const { contextTx, agentFirstChunk } = await cm.startAgentResponse([
|
|
442
439
|
serverUserMessage0,
|
|
443
440
|
]);
|
|
444
441
|
expect(agentFirstChunk.message_idx).eql(1 * MESSAGE_INDEX_FULL_INCREMENT);
|
|
445
|
-
expect(
|
|
442
|
+
expect(contextTx.getLLMContext().slice(1)).eql([
|
|
446
443
|
{
|
|
447
444
|
role: "user",
|
|
448
445
|
name: serverUserMessage0.user_uuid,
|
|
@@ -474,7 +471,8 @@ describe("IndexingCompressingContextManager", () => {
|
|
|
474
471
|
],
|
|
475
472
|
content: "AgentMessage2",
|
|
476
473
|
};
|
|
477
|
-
|
|
474
|
+
contextTx.addMessage(agentResponseWithToolCall);
|
|
475
|
+
contextTx.processAgentResponse(agentResponseWithToolCall);
|
|
478
476
|
|
|
479
477
|
// The current Agent does not add the tool call results until the next
|
|
480
478
|
// completion has run. Here we are simulating an error during that
|
|
@@ -498,12 +496,19 @@ describe("IndexingCompressingContextManager", () => {
|
|
|
498
496
|
},
|
|
499
497
|
];
|
|
500
498
|
|
|
501
|
-
|
|
499
|
+
contextTx.addMessage(toolCallResults[0]);
|
|
500
|
+
contextTx.addMessage(toolCallResults[1]);
|
|
501
|
+
|
|
502
|
+
const toolCallMesasge0 = contextTx.processToolCallResult(
|
|
503
|
+
toolCallResults[0]
|
|
504
|
+
);
|
|
502
505
|
expect(toolCallMesasge0.message_idx).eql(
|
|
503
506
|
1 * MESSAGE_INDEX_FULL_INCREMENT + 1 * MESSAGE_INDEX_SUB_INCREMENT
|
|
504
507
|
);
|
|
505
508
|
|
|
506
|
-
const toolCallMesasge1 =
|
|
509
|
+
const toolCallMesasge1 = contextTx.processToolCallResult(
|
|
510
|
+
toolCallResults[1]
|
|
511
|
+
);
|
|
507
512
|
expect(toolCallMesasge1.message_idx).eql(
|
|
508
513
|
1 * MESSAGE_INDEX_FULL_INCREMENT + 2 * MESSAGE_INDEX_SUB_INCREMENT
|
|
509
514
|
);
|
|
@@ -511,12 +516,12 @@ describe("IndexingCompressingContextManager", () => {
|
|
|
511
516
|
// The error is caught by OpenSession, which informs the
|
|
512
517
|
// ChatContextManager.
|
|
513
518
|
|
|
514
|
-
|
|
519
|
+
contextTx.revertAgentResponse("an error occured");
|
|
515
520
|
|
|
516
521
|
// We should now be able to run the original test (starting from index
|
|
517
522
|
// 2*MESSAGE_INDEX_FULL_INCREMENT). None of our previous messages should
|
|
518
523
|
// hit the DB or the LLM context.
|
|
519
524
|
|
|
520
|
-
testSuccessfulAgentLoop(cm, 2 * MESSAGE_INDEX_FULL_INCREMENT);
|
|
525
|
+
await testSuccessfulAgentLoop(cm, 2 * MESSAGE_INDEX_FULL_INCREMENT);
|
|
521
526
|
});
|
|
522
527
|
});
|
|
@@ -31,7 +31,6 @@ describe("Client-Server WebSocket Integration", () => {
|
|
|
31
31
|
const mockEnv = {
|
|
32
32
|
SUPABASE_URL: SUPABASE_LOCAL_URL,
|
|
33
33
|
SUPABASE_KEY: SUPABASE_LOCAL_KEY,
|
|
34
|
-
LLM_URL: "http://localhost:8080", // not used in this test
|
|
35
34
|
XMCP_URL: "http://localhost:8081", // not used in this test
|
|
36
35
|
};
|
|
37
36
|
beforeAll(async () => {
|
|
@@ -55,7 +54,6 @@ describe("Client-Server WebSocket Integration", () => {
|
|
|
55
54
|
serverPort,
|
|
56
55
|
mockEnv.SUPABASE_URL,
|
|
57
56
|
mockEnv.SUPABASE_KEY,
|
|
58
|
-
mockEnv.LLM_URL,
|
|
59
57
|
mockEnv.XMCP_URL
|
|
60
58
|
);
|
|
61
59
|
// Wait for server to be ready
|