@xalia/agent 0.5.4 → 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.
Files changed (43) hide show
  1. package/dist/agent/src/agent/agent.js +16 -9
  2. package/dist/agent/src/agent/agentUtils.js +24 -4
  3. package/dist/agent/src/agent/mcpServerManager.js +19 -9
  4. package/dist/agent/src/agent/openAILLM.js +3 -1
  5. package/dist/agent/src/agent/openAILLMStreaming.js +24 -25
  6. package/dist/agent/src/agent/repeatLLM.js +43 -0
  7. package/dist/agent/src/agent/sudoMcpServerManager.js +12 -6
  8. package/dist/agent/src/chat/client.js +259 -36
  9. package/dist/agent/src/chat/conversationManager.js +243 -24
  10. package/dist/agent/src/chat/db.js +24 -1
  11. package/dist/agent/src/chat/frontendClient.js +74 -0
  12. package/dist/agent/src/chat/server.js +3 -3
  13. package/dist/agent/src/test/db.test.js +25 -2
  14. package/dist/agent/src/test/openaiStreaming.test.js +133 -0
  15. package/dist/agent/src/test/prompt.test.js +2 -2
  16. package/dist/agent/src/test/sudoMcpServerManager.test.js +1 -1
  17. package/dist/agent/src/tool/agentChat.js +7 -197
  18. package/dist/agent/src/tool/chatMain.js +18 -23
  19. package/dist/agent/src/tool/commandPrompt.js +248 -0
  20. package/dist/agent/src/tool/prompt.js +27 -31
  21. package/package.json +1 -1
  22. package/scripts/test_chat +17 -1
  23. package/src/agent/agent.ts +34 -11
  24. package/src/agent/agentUtils.ts +52 -3
  25. package/src/agent/mcpServerManager.ts +43 -13
  26. package/src/agent/openAILLM.ts +3 -1
  27. package/src/agent/openAILLMStreaming.ts +28 -27
  28. package/src/agent/repeatLLM.ts +51 -0
  29. package/src/agent/sudoMcpServerManager.ts +41 -12
  30. package/src/chat/client.ts +353 -40
  31. package/src/chat/conversationManager.ts +345 -33
  32. package/src/chat/db.ts +28 -2
  33. package/src/chat/frontendClient.ts +123 -0
  34. package/src/chat/messages.ts +146 -2
  35. package/src/chat/server.ts +3 -3
  36. package/src/test/db.test.ts +35 -2
  37. package/src/test/openaiStreaming.test.ts +142 -0
  38. package/src/test/prompt.test.ts +1 -1
  39. package/src/test/sudoMcpServerManager.test.ts +1 -1
  40. package/src/tool/agentChat.ts +13 -211
  41. package/src/tool/chatMain.ts +28 -43
  42. package/src/tool/commandPrompt.ts +252 -0
  43. package/src/tool/prompt.ts +33 -32
@@ -1,32 +1,230 @@
1
- import * as dotenv from "dotenv";
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
- dotenv.config();
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
- export class ChatClient {
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((r, e) => {
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
- const client = new ChatClient(ws, onMessageCB, onConnectionClosedCB);
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
- const msg: ServerToClient = JSON.parse(msgData);
60
- client.onMessageCB(msg);
281
+ onMsg(JSON.parse(msgData) as ServerToClient);
61
282
  } catch (e) {
62
- client.close();
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.closed = true;
75
- onConnectionClosedCB();
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.closed = true;
83
- onConnectionClosedCB();
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 sendMessage(message: ClientToServer): void {
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