@xalia/agent 0.5.3 → 0.5.5
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/dist/agent/src/agent/agent.js +16 -9
- package/dist/agent/src/agent/agentUtils.js +24 -4
- package/dist/agent/src/agent/mcpServerManager.js +19 -9
- package/dist/agent/src/agent/openAILLM.js +3 -1
- package/dist/agent/src/agent/openAILLMStreaming.js +24 -25
- package/dist/agent/src/agent/repeatLLM.js +43 -0
- package/dist/agent/src/agent/sudoMcpServerManager.js +12 -6
- package/dist/agent/src/chat/client.js +259 -36
- package/dist/agent/src/chat/conversationManager.js +243 -24
- package/dist/agent/src/chat/db.js +24 -1
- package/dist/agent/src/chat/frontendClient.js +74 -0
- package/dist/agent/src/chat/server.js +3 -3
- package/dist/agent/src/test/db.test.js +25 -2
- package/dist/agent/src/test/openaiStreaming.test.js +133 -0
- package/dist/agent/src/test/prompt.test.js +2 -2
- package/dist/agent/src/test/sudoMcpServerManager.test.js +1 -1
- package/dist/agent/src/tool/agentChat.js +7 -197
- package/dist/agent/src/tool/chatMain.js +18 -23
- package/dist/agent/src/tool/commandPrompt.js +248 -0
- package/dist/agent/src/tool/prompt.js +27 -31
- package/package.json +1 -1
- package/scripts/test_chat +17 -1
- package/src/agent/agent.ts +34 -11
- package/src/agent/agentUtils.ts +52 -3
- package/src/agent/mcpServerManager.ts +43 -13
- package/src/agent/openAILLM.ts +3 -1
- package/src/agent/openAILLMStreaming.ts +28 -27
- package/src/agent/repeatLLM.ts +51 -0
- package/src/agent/sudoMcpServerManager.ts +41 -12
- package/src/chat/client.ts +353 -40
- package/src/chat/conversationManager.ts +345 -33
- package/src/chat/db.ts +28 -2
- package/src/chat/frontendClient.ts +123 -0
- package/src/chat/messages.ts +146 -2
- package/src/chat/server.ts +3 -3
- package/src/test/db.test.ts +35 -2
- package/src/test/openaiStreaming.test.ts +142 -0
- package/src/test/prompt.test.ts +1 -1
- package/src/test/sudoMcpServerManager.test.ts +1 -1
- package/src/tool/agentChat.ts +13 -211
- package/src/tool/chatMain.ts +28 -43
- package/src/tool/commandPrompt.ts +252 -0
- package/src/tool/prompt.ts +33 -32
package/src/chat/client.ts
CHANGED
@@ -1,32 +1,230 @@
|
|
1
|
-
import
|
2
|
-
import { getLogger } from "@xalia/xmcp/sdk";
|
1
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
3
2
|
import { strict as assert } from "assert";
|
4
3
|
import * as websocket from "ws";
|
5
|
-
import { ServerToClient, ClientToServer } from "./messages";
|
6
4
|
|
7
|
-
|
5
|
+
import { AgentProfile, McpServerBrief, getLogger } from "@xalia/xmcp/sdk";
|
6
|
+
|
7
|
+
import { ISkillManager } from "../agent/sudoMcpServerManager";
|
8
|
+
import {
|
9
|
+
IMcpServerManager,
|
10
|
+
McpServerInfo,
|
11
|
+
McpServerInfoRW,
|
12
|
+
} from "../agent/mcpServerManager";
|
13
|
+
|
14
|
+
import { ServerToClient, ClientToServer } from "./messages";
|
15
|
+
import { ChatCompletionMessageParam, IConversation } from "../agent/agent";
|
16
|
+
import { IPlatform } from "../agent/iplatform";
|
8
17
|
|
9
18
|
const logger = getLogger();
|
10
19
|
|
11
20
|
type OnMessageCallback = (msg: ServerToClient) => void;
|
12
21
|
|
13
|
-
type OnConnectionClosedCallback = () => void
|
22
|
+
type OnConnectionClosedCallback = () => Promise<void>;
|
23
|
+
|
24
|
+
interface IMessageSender {
|
25
|
+
sendMessage(message: ClientToServer): void;
|
26
|
+
}
|
27
|
+
|
28
|
+
class RemoteMcpServerManager implements IMcpServerManager {
|
29
|
+
private mcpServers: { [serverName: string]: McpServerInfoRW } = {};
|
30
|
+
|
31
|
+
constructor(private sender: IMessageSender) {
|
32
|
+
this.mcpServers = {};
|
33
|
+
}
|
34
|
+
|
35
|
+
hasMcpServer(mcpServerName: string): boolean {
|
36
|
+
return !!this.mcpServers[mcpServerName];
|
37
|
+
}
|
38
|
+
|
39
|
+
getMcpServerNames(): string[] {
|
40
|
+
return Object.keys(this.mcpServers);
|
41
|
+
}
|
42
|
+
|
43
|
+
getMcpServer(mcpServerName: string): McpServerInfo {
|
44
|
+
const server = this.mcpServers[mcpServerName];
|
45
|
+
if (server) {
|
46
|
+
return server;
|
47
|
+
}
|
48
|
+
throw Error(`unknown server ${mcpServerName}`);
|
49
|
+
}
|
50
|
+
|
51
|
+
async removeMcpServer(mcpServerName: string): Promise<void> {
|
52
|
+
if (!this.mcpServers[mcpServerName]) {
|
53
|
+
throw Error(`no server ${mcpServerName} (removeMcpServer)`);
|
54
|
+
}
|
55
|
+
|
56
|
+
this.sender.sendMessage({
|
57
|
+
type: "remove_mcp_server",
|
58
|
+
server_name: mcpServerName,
|
59
|
+
});
|
60
|
+
}
|
61
|
+
|
62
|
+
enableAllTools(mcpServerName: string): void {
|
63
|
+
if (!this.mcpServers[mcpServerName]) {
|
64
|
+
throw Error(`no server ${mcpServerName} (enableAllTools)`);
|
65
|
+
}
|
66
|
+
|
67
|
+
this.sender.sendMessage({
|
68
|
+
type: "enable_all_mcp_server_tools",
|
69
|
+
server_name: mcpServerName,
|
70
|
+
});
|
71
|
+
}
|
72
|
+
|
73
|
+
disableAllTools(mcpServerName: string): void {
|
74
|
+
if (!this.mcpServers[mcpServerName]) {
|
75
|
+
throw Error(`no server ${mcpServerName} (disableAllTools)`);
|
76
|
+
}
|
77
|
+
|
78
|
+
this.sender.sendMessage({
|
79
|
+
type: "disable_all_mcp_server_tools",
|
80
|
+
server_name: mcpServerName,
|
81
|
+
});
|
82
|
+
}
|
83
|
+
|
84
|
+
enableTool(mcpServerName: string, toolName: string): void {
|
85
|
+
const server = this.mcpServers[mcpServerName];
|
86
|
+
if (!server) {
|
87
|
+
throw Error(`no server ${mcpServerName} (enableTool)`);
|
88
|
+
}
|
89
|
+
const tools = server.getTool(toolName);
|
90
|
+
if (!tools) {
|
91
|
+
throw Error(`no tool ${toolName} on server ${mcpServerName}`);
|
92
|
+
}
|
93
|
+
|
94
|
+
this.sender.sendMessage({
|
95
|
+
type: "enable_mcp_server_tool",
|
96
|
+
server_name: mcpServerName,
|
97
|
+
tool: toolName,
|
98
|
+
});
|
99
|
+
}
|
14
100
|
|
15
|
-
|
101
|
+
disableTool(mcpServerName: string, toolName: string): void {
|
102
|
+
const server = this.mcpServers[mcpServerName];
|
103
|
+
if (!server) {
|
104
|
+
throw Error(`no server ${mcpServerName} (disableTool)`);
|
105
|
+
}
|
106
|
+
const tools = server.getTool(toolName);
|
107
|
+
if (!tools) {
|
108
|
+
throw Error(`no tool ${toolName} on server ${mcpServerName}`);
|
109
|
+
}
|
110
|
+
|
111
|
+
this.sender.sendMessage({
|
112
|
+
type: "disable_mcp_server_tool",
|
113
|
+
server_name: mcpServerName,
|
114
|
+
tool: toolName,
|
115
|
+
});
|
116
|
+
}
|
117
|
+
|
118
|
+
onMcpServerAdded(
|
119
|
+
mcpServerName: string,
|
120
|
+
tools: Tool[],
|
121
|
+
enabled_tools: string[]
|
122
|
+
) {
|
123
|
+
logger.debug(
|
124
|
+
`[onMcpServerAdded]: ${mcpServerName}, tools: ${JSON.stringify(tools)}` +
|
125
|
+
`, enabled: ${JSON.stringify(enabled_tools)}`
|
126
|
+
);
|
127
|
+
|
128
|
+
const mcpServerInfo = new McpServerInfoRW(tools);
|
129
|
+
for (const tool of enabled_tools) {
|
130
|
+
mcpServerInfo.enableTool(tool);
|
131
|
+
}
|
132
|
+
this.mcpServers[mcpServerName] = mcpServerInfo;
|
133
|
+
}
|
134
|
+
|
135
|
+
onMcpServerRemoved(mcpServerName: string) {
|
136
|
+
delete this.mcpServers[mcpServerName];
|
137
|
+
}
|
138
|
+
|
139
|
+
onMcpServerToolEnabled(mcpServerName: string, toolName: string) {
|
140
|
+
const server = this.mcpServers[mcpServerName];
|
141
|
+
if (!server) {
|
142
|
+
throw Error(`no server ${mcpServerName} (onMcpServerToolEnabled)`);
|
143
|
+
}
|
144
|
+
const tools = server.getTool(toolName);
|
145
|
+
if (!tools) {
|
146
|
+
throw Error(`no tool ${toolName} on server ${mcpServerName}`);
|
147
|
+
}
|
148
|
+
|
149
|
+
server.enableTool(toolName);
|
150
|
+
}
|
151
|
+
|
152
|
+
onMcpServerToolDisabled(mcpServerName: string, toolName: string) {
|
153
|
+
const server = this.mcpServers[mcpServerName];
|
154
|
+
if (!server) {
|
155
|
+
throw Error(`no server ${mcpServerName} (onMcpServerToolDisabled)`);
|
156
|
+
}
|
157
|
+
const tools = server.getTool(toolName);
|
158
|
+
if (!tools) {
|
159
|
+
throw Error(`no tool ${toolName} on server ${mcpServerName}`);
|
160
|
+
}
|
161
|
+
|
162
|
+
server.disableTool(toolName);
|
163
|
+
}
|
164
|
+
}
|
165
|
+
|
166
|
+
class RemoteSudoMcpServerManager implements ISkillManager {
|
167
|
+
private briefsMap: Record<string, McpServerBrief>;
|
168
|
+
|
169
|
+
constructor(
|
170
|
+
private sender: IMessageSender,
|
171
|
+
private briefs: McpServerBrief[],
|
172
|
+
private msm: RemoteMcpServerManager
|
173
|
+
) {
|
174
|
+
this.briefsMap = {};
|
175
|
+
briefs.forEach((b) => {
|
176
|
+
this.briefsMap[b.name] = b;
|
177
|
+
});
|
178
|
+
}
|
179
|
+
|
180
|
+
getMcpServerManager(): IMcpServerManager {
|
181
|
+
return this.msm;
|
182
|
+
}
|
183
|
+
|
184
|
+
getServerBriefs(): McpServerBrief[] {
|
185
|
+
return this.briefs;
|
186
|
+
}
|
187
|
+
|
188
|
+
async addMcpServer(server_name: string, enable_all: boolean): Promise<void> {
|
189
|
+
if (!this.briefsMap[server_name]) {
|
190
|
+
throw Error(`no such server ${server_name} (addMcpServer)`);
|
191
|
+
}
|
192
|
+
|
193
|
+
this.sender.sendMessage({
|
194
|
+
type: "add_mcp_server",
|
195
|
+
server_name,
|
196
|
+
enable_all,
|
197
|
+
});
|
198
|
+
}
|
199
|
+
}
|
200
|
+
|
201
|
+
export class ChatClient implements IMessageSender, IConversation {
|
202
|
+
private platform: IPlatform;
|
16
203
|
private ws: websocket.WebSocket;
|
17
204
|
private onMessageCB: OnMessageCallback;
|
18
205
|
private onConnectionClosedCB: OnConnectionClosedCallback;
|
19
206
|
private closed: boolean;
|
207
|
+
private msm: RemoteMcpServerManager;
|
208
|
+
private smsm: RemoteSudoMcpServerManager;
|
209
|
+
private systemPrompt: string;
|
210
|
+
private model: string;
|
20
211
|
|
21
212
|
private constructor(
|
213
|
+
platform: IPlatform,
|
22
214
|
ws: websocket.WebSocket,
|
23
215
|
onMessageCB: OnMessageCallback,
|
24
|
-
onConnectionClosedCB: OnConnectionClosedCallback
|
216
|
+
onConnectionClosedCB: OnConnectionClosedCallback,
|
217
|
+
serverBriefs: McpServerBrief[]
|
25
218
|
) {
|
219
|
+
this.platform = platform;
|
26
220
|
this.ws = ws;
|
27
221
|
this.onMessageCB = onMessageCB;
|
28
222
|
this.onConnectionClosedCB = onConnectionClosedCB;
|
29
223
|
this.closed = false;
|
224
|
+
this.msm = new RemoteMcpServerManager(this);
|
225
|
+
this.smsm = new RemoteSudoMcpServerManager(this, serverBriefs, this.msm);
|
226
|
+
this.systemPrompt = "";
|
227
|
+
this.model = "";
|
30
228
|
}
|
31
229
|
|
32
230
|
static async initWithParams(
|
@@ -35,16 +233,40 @@ export class ChatClient {
|
|
35
233
|
token: string,
|
36
234
|
params: Record<string, string>,
|
37
235
|
onMessageCB: OnMessageCallback,
|
38
|
-
onConnectionClosedCB: OnConnectionClosedCallback
|
236
|
+
onConnectionClosedCB: OnConnectionClosedCallback,
|
237
|
+
platform: IPlatform
|
39
238
|
): Promise<ChatClient> {
|
40
|
-
return new Promise((
|
239
|
+
return new Promise((resolveClient, e) => {
|
41
240
|
const urlParams = new URLSearchParams(params);
|
42
241
|
const url = `ws://${host}:${port}?${urlParams}`;
|
43
242
|
|
44
243
|
const ws = new websocket.WebSocket(url, [token]);
|
45
244
|
logger.info("created ws");
|
46
245
|
|
47
|
-
|
246
|
+
let client: ChatClient | undefined = undefined;
|
247
|
+
const onMsg = (msg: ServerToClient) => {
|
248
|
+
if (msg.type === "mcp_server_briefs") {
|
249
|
+
// This should be received once, as the first message at startup.
|
250
|
+
assert(!client);
|
251
|
+
client = new ChatClient(
|
252
|
+
platform,
|
253
|
+
ws,
|
254
|
+
onMessageCB,
|
255
|
+
onConnectionClosedCB,
|
256
|
+
msg.server_briefs
|
257
|
+
);
|
258
|
+
resolveClient(client);
|
259
|
+
} else {
|
260
|
+
assert(client);
|
261
|
+
|
262
|
+
// Pass all messages to our internal handler (to update mcp state,
|
263
|
+
// etc) before sending to the client code.
|
264
|
+
|
265
|
+
// logger.debug(`[ChatClient.init(onMsg)]: ${JSON.stringify(msg)}`);
|
266
|
+
client.handleMessageInternal(msg);
|
267
|
+
client.onMessageCB(msg);
|
268
|
+
}
|
269
|
+
};
|
48
270
|
|
49
271
|
ws.onopen = async () => {
|
50
272
|
logger.info("opened");
|
@@ -56,59 +278,46 @@ export class ChatClient {
|
|
56
278
|
throw `expected "string" data, got ${typeof msgData}`;
|
57
279
|
}
|
58
280
|
logger.debug(`[client.onmessage]: ${msgData}`);
|
59
|
-
|
60
|
-
client.onMessageCB(msg);
|
281
|
+
onMsg(JSON.parse(msgData) as ServerToClient);
|
61
282
|
} catch (e) {
|
62
|
-
client
|
283
|
+
if (client) {
|
284
|
+
client.close();
|
285
|
+
}
|
63
286
|
throw e;
|
64
287
|
}
|
65
288
|
};
|
66
|
-
r(client);
|
67
289
|
};
|
68
290
|
|
69
|
-
ws.onclose = (err) => {
|
291
|
+
ws.onclose = async (err) => {
|
70
292
|
logger.info("closed");
|
71
293
|
logger.info(
|
72
294
|
`[client] WebSocket connection closed: ${JSON.stringify(err)}`
|
73
295
|
);
|
74
|
-
client
|
75
|
-
|
296
|
+
if (client) {
|
297
|
+
client.closed = true;
|
298
|
+
}
|
299
|
+
await onConnectionClosedCB();
|
76
300
|
};
|
77
301
|
|
78
|
-
ws.onerror = (error) => {
|
302
|
+
ws.onerror = async (error) => {
|
79
303
|
logger.error("[client] WebSocket error:", JSON.stringify(error));
|
80
304
|
e(error);
|
81
305
|
|
82
|
-
client
|
83
|
-
|
306
|
+
if (client) {
|
307
|
+
client.closed = true;
|
308
|
+
}
|
309
|
+
await onConnectionClosedCB();
|
84
310
|
};
|
85
311
|
});
|
86
312
|
}
|
87
313
|
|
88
|
-
// public static async initWithSession(
|
89
|
-
// host: string,
|
90
|
-
// port: number,
|
91
|
-
// token: string,
|
92
|
-
// sessionId: string,
|
93
|
-
// onMessageCB: OnMessageCallback,
|
94
|
-
// onConnectionClosedCB: OnConnectionClosedCallback
|
95
|
-
// ): Promise<ChatClient> {
|
96
|
-
// return ChatClient.initWithParams(
|
97
|
-
// host,
|
98
|
-
// port,
|
99
|
-
// token,
|
100
|
-
// { session_id: sessionId },
|
101
|
-
// onMessageCB,
|
102
|
-
// onConnectionClosedCB
|
103
|
-
// );
|
104
|
-
// }
|
105
|
-
|
106
314
|
public static async init(
|
107
315
|
host: string,
|
108
316
|
port: number,
|
109
317
|
token: string,
|
110
318
|
onMessageCB: OnMessageCallback,
|
111
319
|
onConnectionClosedCB: OnConnectionClosedCallback,
|
320
|
+
platform: IPlatform,
|
112
321
|
sessionId: string = "untitled",
|
113
322
|
agentProfileId: string | undefined = undefined
|
114
323
|
): Promise<ChatClient> {
|
@@ -122,13 +331,60 @@ export class ChatClient {
|
|
122
331
|
token,
|
123
332
|
params,
|
124
333
|
onMessageCB,
|
125
|
-
onConnectionClosedCB
|
334
|
+
onConnectionClosedCB,
|
335
|
+
platform
|
126
336
|
);
|
127
337
|
}
|
128
338
|
|
129
|
-
public
|
339
|
+
public getSudoMcpServerManager(): ISkillManager {
|
340
|
+
return this.smsm;
|
341
|
+
}
|
342
|
+
|
343
|
+
public getConversation(): ChatCompletionMessageParam[] {
|
344
|
+
throw "unimpl";
|
345
|
+
}
|
346
|
+
|
347
|
+
public getAgentProfile(): AgentProfile {
|
348
|
+
throw "unimpl";
|
349
|
+
}
|
350
|
+
|
351
|
+
getSystemPrompt(): string {
|
352
|
+
return this.systemPrompt;
|
353
|
+
}
|
354
|
+
|
355
|
+
setSystemPrompt(system_prompt: string): void {
|
356
|
+
// Don't set system prompt here. Wait until we get confirmation from the
|
357
|
+
// server.
|
358
|
+
this.sendMessage({ type: "set_system_prompt", system_prompt });
|
359
|
+
}
|
360
|
+
|
361
|
+
getModel(): string {
|
362
|
+
return this.model;
|
363
|
+
}
|
364
|
+
|
365
|
+
setModel(model: string): void {
|
366
|
+
// Don't set model here. Wait until we get confirmation from the server.
|
367
|
+
this.sendMessage({ type: "set_model", model });
|
368
|
+
}
|
369
|
+
|
370
|
+
userMessage(msg?: string, imageB64?: string): void {
|
371
|
+
assert(msg);
|
372
|
+
assert(!imageB64, "images not supported in Chat yet");
|
373
|
+
|
374
|
+
this.sendMessage({
|
375
|
+
type: "msg",
|
376
|
+
message: msg,
|
377
|
+
});
|
378
|
+
}
|
379
|
+
|
380
|
+
resetConversation(): void {
|
381
|
+
throw "resetConversation not implemented for ChatClient";
|
382
|
+
}
|
383
|
+
|
384
|
+
public sendMessage(message: ClientToServer): undefined {
|
130
385
|
assert(!this.closed);
|
131
386
|
const data = JSON.stringify(message);
|
387
|
+
logger.debug(`[ChatClient.sendMessage] ${data}`);
|
132
388
|
this.ws.send(data);
|
133
389
|
}
|
134
390
|
|
@@ -137,6 +393,63 @@ export class ChatClient {
|
|
137
393
|
this.onConnectionClosedCB();
|
138
394
|
this.ws.close();
|
139
395
|
}
|
396
|
+
|
397
|
+
handleMessageInternal(message: ServerToClient): void {
|
398
|
+
switch (message.type) {
|
399
|
+
//
|
400
|
+
// State updates
|
401
|
+
//
|
402
|
+
|
403
|
+
case "mcp_server_added":
|
404
|
+
this.msm.onMcpServerAdded(
|
405
|
+
message.server_name,
|
406
|
+
message.tools,
|
407
|
+
message.enabled_tools
|
408
|
+
);
|
409
|
+
break;
|
410
|
+
case "mcp_server_removed":
|
411
|
+
this.msm.onMcpServerRemoved(message.server_name);
|
412
|
+
break;
|
413
|
+
case "mcp_server_tool_enabled":
|
414
|
+
this.msm.onMcpServerToolEnabled(message.server_name, message.tool);
|
415
|
+
break;
|
416
|
+
case "mcp_server_tool_disabled":
|
417
|
+
this.msm.onMcpServerToolDisabled(message.server_name, message.tool);
|
418
|
+
break;
|
419
|
+
case "system_prompt_updated":
|
420
|
+
this.systemPrompt = message.system_prompt;
|
421
|
+
break;
|
422
|
+
case "model_updated":
|
423
|
+
this.model = message.model;
|
424
|
+
break;
|
425
|
+
|
426
|
+
//
|
427
|
+
// Actions
|
428
|
+
//
|
429
|
+
|
430
|
+
case "open_url":
|
431
|
+
this.platform.openUrl(
|
432
|
+
message.url,
|
433
|
+
new Promise(() => {}), // TODO: Why do we need this? Remove it.
|
434
|
+
message.display_name
|
435
|
+
);
|
436
|
+
break;
|
437
|
+
case "approve_tool_call":
|
438
|
+
throw "unimpl approve_tool_call";
|
439
|
+
// break;
|
440
|
+
|
441
|
+
//
|
442
|
+
// Ignore other messages - the owner (the UI layer) can handle them at
|
443
|
+
// its discretion.
|
444
|
+
//
|
445
|
+
|
446
|
+
default:
|
447
|
+
logger.debug(
|
448
|
+
`[handleMessageInternal]: ignoring message: ${message.type}`
|
449
|
+
);
|
450
|
+
break;
|
451
|
+
}
|
452
|
+
}
|
140
453
|
}
|
141
454
|
|
142
455
|
// TODO: remove this
|