@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
@@ -9,16 +9,20 @@ import {
9
9
  } from "cmd-ts";
10
10
  import { stdout } from "process";
11
11
  import * as fs from "fs";
12
+ import { strict as assert } from "assert";
12
13
 
13
14
  import { configuration } from "@xalia/xmcp/tool";
14
15
  import { getLogger } from "@xalia/xmcp/sdk";
15
16
 
17
+ import { IConversation } from "../agent/agent";
16
18
  import { ChatClient } from "../chat/client";
17
19
  import { runServer } from "../chat/server";
18
20
  import { ClientUserMessage, ServerToClient } from "../chat/messages";
19
21
 
20
- import { Prompt } from "./prompt";
22
+ import { IPrompt, Prompt, ScriptPrompt } from "./prompt";
21
23
  import * as options from "./options";
24
+ import { CommandPrompt } from "./commandPrompt";
25
+ import { NODE_PLATFORM } from "./nodePlatform";
22
26
 
23
27
  const logger = getLogger();
24
28
 
@@ -114,19 +118,23 @@ const client = command({
114
118
  return;
115
119
  }
116
120
 
117
- if (script) {
118
- await runScript(host, port, apiKey, session, agentProfile, script);
119
- return;
120
- }
121
-
122
121
  const onMessage = getCLIOnMessage();
123
- const repl = new Prompt();
122
+ const repl: IPrompt = (() => {
123
+ if (script) {
124
+ const scriptLines = fs.readFileSync(script, "utf8").split("\n");
125
+ return new ScriptPrompt(scriptLines);
126
+ }
127
+ return new Prompt();
128
+ })();
129
+ const cmdPrompt = new CommandPrompt(repl);
130
+
124
131
  const client = await ChatClient.init(
125
132
  host,
126
133
  port,
127
134
  apiKey,
128
135
  onMessage,
129
- () => repl.shutdown(),
136
+ async () => await cmdPrompt.shutdown(),
137
+ NODE_PLATFORM,
130
138
  session,
131
139
  agentProfile
132
140
  );
@@ -134,13 +142,20 @@ const client = command({
134
142
  logger.debug("client created");
135
143
 
136
144
  while (true) {
137
- const msgText = await repl.run("ME: ");
138
- logger.debug(`prompt got '${msgText}'`);
145
+ const [msgText, imageFile] = await cmdPrompt.getNextPrompt(
146
+ undefined as unknown as IConversation, // TODO: client
147
+ client.getSudoMcpServerManager()
148
+ );
149
+ assert(imageFile === undefined);
150
+
139
151
  if (msgText === undefined) {
140
152
  logger.debug("exiting...");
141
153
  client.close();
142
154
  return;
143
155
  }
156
+
157
+ // TODO: other prompts
158
+
144
159
  if (msgText.length > 0) {
145
160
  const msg: ClientUserMessage = {
146
161
  type: "msg",
@@ -176,43 +191,12 @@ function getCLIOnMessage(): (msg: ServerToClient) => void {
176
191
  }
177
192
  break;
178
193
  default:
179
- stdout.write(`(${JSON.stringify(msg)})\n`);
194
+ stdout.write(`(unrecognised) ${JSON.stringify(msg)}\n`);
180
195
  break;
181
196
  }
182
197
  };
183
198
  }
184
199
 
185
- async function runScript(
186
- host: string,
187
- port: number,
188
- apiKey: string,
189
- session: string,
190
- agentProfile: string | undefined,
191
- script: string
192
- ): Promise<void> {
193
- const client = await ChatClient.init(
194
- host,
195
- port,
196
- apiKey,
197
- getCLIOnMessage(),
198
- () => {},
199
- session,
200
- agentProfile
201
- );
202
- const scriptData = fs.readFileSync(script, "utf8");
203
- const lines = scriptData.split("\n");
204
- for (const line of lines) {
205
- await new Promise((r) => setTimeout(r, 1000));
206
- client.sendMessage({ type: "msg", message: line });
207
- }
208
-
209
- // 2 second wait period
210
- await new Promise((r) => setTimeout(r, 2000));
211
-
212
- client.close();
213
- return;
214
- }
215
-
216
200
  async function runClientTest(
217
201
  host: string,
218
202
  port: number,
@@ -224,7 +208,8 @@ async function runClientTest(
224
208
  port,
225
209
  apiKey,
226
210
  getCLIOnMessage(),
227
- () => {},
211
+ async () => {},
212
+ NODE_PLATFORM,
228
213
  session
229
214
  );
230
215
 
@@ -0,0 +1,252 @@
1
+ import * as fs from "fs";
2
+ import chalk from "chalk";
3
+ import OpenAI from "openai";
4
+
5
+ import { InvalidConfiguration } from "@xalia/xmcp/sdk";
6
+
7
+ import { IConversation } from "../agent/agent";
8
+ import { displayToolCall } from "../agent/tools";
9
+ import { ISkillManager } from "../agent/sudoMcpServerManager";
10
+ import { IMcpServerManager } from "../agent/mcpServerManager";
11
+
12
+ import { IPrompt } from "./prompt";
13
+
14
+ /**
15
+ * A prompt parser which can accept commands or messages from an IPrompt.
16
+ * Commands are sent to an IMcpServerManager, ISkillManager and IConversation.
17
+ */
18
+ export class CommandPrompt {
19
+ prompt: IPrompt;
20
+
21
+ constructor(prompt: IPrompt) {
22
+ this.prompt = prompt;
23
+ }
24
+
25
+ shutdown(): Promise<void> {
26
+ return this.prompt.shutdown();
27
+ }
28
+
29
+ async getNextPrompt(
30
+ agent: IConversation,
31
+ sudoMcpServerManager: ISkillManager
32
+ ): Promise<[string | undefined, string | undefined]> {
33
+ while (true) {
34
+ // Get a line, detecting the EOF signal.
35
+ const line = await this.prompt.run();
36
+ if (typeof line === "undefined") {
37
+ console.log("closing ...");
38
+ return [undefined, undefined];
39
+ }
40
+ if (line.length === 0) {
41
+ continue;
42
+ }
43
+
44
+ // Extract prompt or commands
45
+ const { msg, cmds } = parsePrompt(line);
46
+
47
+ // If there are no commands, this must be a prompt only
48
+ if (!cmds) {
49
+ return [msg, undefined];
50
+ }
51
+
52
+ // There are commands. If it's image, return [prompt, image]. If it's
53
+ // quit, return [undefined, undefined], otherwise it must be a command.
54
+ // Execute it and prompt again.
55
+ switch (cmds[0]) {
56
+ case "i": // image
57
+ return [msg, cmds[1]];
58
+ case "q":
59
+ case "quit":
60
+ case "exit":
61
+ return [undefined, undefined];
62
+ default:
63
+ break;
64
+ }
65
+
66
+ try {
67
+ await this.runCommand(
68
+ agent,
69
+ sudoMcpServerManager.getMcpServerManager(),
70
+ sudoMcpServerManager,
71
+ cmds
72
+ );
73
+ } catch (e) {
74
+ console.log(`ERROR: ${e}`);
75
+ }
76
+ }
77
+ }
78
+
79
+ async runCommand(
80
+ agent: IConversation,
81
+ mcpServerManager: IMcpServerManager,
82
+ sudoMcpServerManager: ISkillManager,
83
+ cmds: string[]
84
+ ) {
85
+ switch (cmds[0]) {
86
+ case "lt":
87
+ this.listTools(mcpServerManager);
88
+ break;
89
+ case "ls":
90
+ this.listServers(sudoMcpServerManager);
91
+ break;
92
+ case "as":
93
+ await this.addServer(sudoMcpServerManager, cmds[1]);
94
+ break;
95
+ case "rs":
96
+ await mcpServerManager.removeMcpServer(cmds[1]);
97
+ break;
98
+ case "e":
99
+ mcpServerManager.enableTool(cmds[1], cmds[2]);
100
+ console.log(`Enabled tool ${cmds[2]} for server ${cmds[1]}`);
101
+ break;
102
+ case "d":
103
+ mcpServerManager.disableTool(cmds[1], cmds[2]);
104
+ console.log(`Disabled tool ${cmds[2]} for server ${cmds[1]}`);
105
+ break;
106
+ case "ea":
107
+ mcpServerManager.enableAllTools(cmds[1]);
108
+ console.log(`Enabled all tools for server ${cmds[1]}`);
109
+ break;
110
+ case "da":
111
+ mcpServerManager.disableAllTools(cmds[1]);
112
+ console.log(`Disabled all tools for server ${cmds[1]}`);
113
+ break;
114
+ case "wc":
115
+ this.writeConversation(agent, cmds[1]);
116
+ break;
117
+ case "wa":
118
+ this.writeAgentProfile(agent, cmds[1]);
119
+ break;
120
+ case "h":
121
+ case "help":
122
+ case "?":
123
+ this.helpMenu();
124
+ break;
125
+ default:
126
+ console.log(`error: Unknown command ${cmds[0]}`);
127
+ }
128
+ }
129
+
130
+ helpMenu() {
131
+ const write = console.log;
132
+ write(`Tool management commands:`);
133
+ write(` ${chalk.yellow("/lt")} List tools: `);
134
+ write(` ${chalk.yellow("/ls")} List servers`);
135
+ write(` ${chalk.yellow("/as <server>")} Add server`);
136
+ write(` ${chalk.yellow("/rs <server>")} Remove server`);
137
+ write(` ${chalk.yellow("/e <server> <tool>")} Enable tool`);
138
+ write(` ${chalk.yellow("/d <server> <tool>")} Disable tool`);
139
+ write(` ${chalk.yellow("/ea <server>")} Enable all tools`);
140
+ write(` ${chalk.yellow("/da <server>")} Disable all tools`);
141
+ write(` ${chalk.yellow("/wc <file-name>")} Write conversation file`);
142
+ write(` ${chalk.yellow("/wa <file-name>")} Write agent profile file`);
143
+ write(` ${chalk.yellow("/q")} Quit`);
144
+ }
145
+
146
+ listTools(mcpServerManager: IMcpServerManager) {
147
+ console.log("Mcp servers and tools (* - enabled):");
148
+
149
+ const serverNames = mcpServerManager.getMcpServerNames();
150
+ for (const serverName of serverNames) {
151
+ const server = mcpServerManager.getMcpServer(serverName);
152
+ console.log(` ${chalk.green(serverName)}`);
153
+ const tools = server.getTools();
154
+ const enabled = server.getEnabledTools();
155
+ for (const tool of tools) {
156
+ const isEnabled = enabled[tool.name] ? "*" : " ";
157
+ console.log(` [${isEnabled}] ${tool.name}`);
158
+ }
159
+ }
160
+ }
161
+
162
+ listServers(sudoMcpServerManager: ISkillManager) {
163
+ console.log(`Available MCP Servers:`);
164
+
165
+ const serverBriefs = sudoMcpServerManager.getServerBriefs();
166
+ serverBriefs.sort((a, b) => a.name.localeCompare(b.name));
167
+ for (const brief of serverBriefs) {
168
+ console.log(`- ${chalk.green(brief.name)}: ${brief.description}`);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Adds server and enables all tools.
174
+ */
175
+ async addServer(sudoMcpServerManager: ISkillManager, serverName: string) {
176
+ try {
177
+ await sudoMcpServerManager.addMcpServer(serverName, true);
178
+ } catch (e) {
179
+ if (e instanceof InvalidConfiguration) {
180
+ throw `${e}. \n
181
+ Have you installed server ${serverName} using the SudoMCP CLI tool?`;
182
+ } else {
183
+ throw e;
184
+ }
185
+ }
186
+ const msm = sudoMcpServerManager.getMcpServerManager();
187
+ console.log(`Added server: ${serverName} (enabled all tools).`);
188
+ console.log(`Current tool list:`);
189
+ this.listTools(msm);
190
+ }
191
+
192
+ writeConversation(agent: IConversation, fileName: string) {
193
+ const conversation = agent.getConversation();
194
+ fs.writeFileSync(fileName, JSON.stringify(conversation), {
195
+ encoding: "utf8",
196
+ });
197
+ console.log(`Conversation written to ${fileName}`);
198
+ }
199
+
200
+ writeAgentProfile(agent: IConversation, fileName: string) {
201
+ const profile = agent.getAgentProfile();
202
+ fs.writeFileSync(fileName, JSON.stringify(profile));
203
+ console.log(`AgentProfile written to ${fileName}`);
204
+ }
205
+
206
+ async promptToolCall(
207
+ toolCall: OpenAI.ChatCompletionMessageToolCall
208
+ ): Promise<boolean> {
209
+ displayToolCall(toolCall);
210
+ const response = await this.prompt.run("Approve tool call? (Y/n) ");
211
+ return response === "y" || response === "yes" || response == "";
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Support prompts of the form:
217
+ * - some text (msg: some text, cmds: undefined)
218
+ * - :i image.png some text (msg: some text, cmds: ["i", "image.png"])
219
+ * - :i image.png (msg: undefined, cmds: ["i", "image.png"])
220
+ * - :l (msg: undefined, cmds: ["l"])
221
+ * - :e toolName .. (msg: undefined, cmds: ["e", "toolName", ...])
222
+ * - :ea toolName .. (msg: undefined, cmds: ["ea", "toolName", ...])
223
+ */
224
+ export function parsePrompt(prompt: string): {
225
+ msg?: string;
226
+ cmds?: string[];
227
+ } {
228
+ prompt = prompt.trim();
229
+
230
+ let msg: string | undefined = undefined;
231
+ let cmds: string[] | undefined = undefined;
232
+
233
+ if (prompt.startsWith(":") || prompt.startsWith("/")) {
234
+ cmds = prompt.split(" ");
235
+ cmds[0] = cmds[0].slice(1);
236
+
237
+ if (cmds[0] == "i") {
238
+ // :i is special as it may have a trailing message
239
+ const fileDelim = prompt.indexOf(" ", 3);
240
+ if (fileDelim < 0) {
241
+ cmds = [cmds[0], prompt.slice(3)];
242
+ } else {
243
+ msg = prompt.slice(fileDelim + 1);
244
+ cmds = [cmds[0], prompt.slice(3, fileDelim)];
245
+ }
246
+ }
247
+ } else {
248
+ msg = prompt;
249
+ }
250
+
251
+ return { msg, cmds };
252
+ }
@@ -1,8 +1,16 @@
1
+ import { getLogger } from "@xalia/xmcp/sdk";
1
2
  import readline from "readline";
2
3
 
4
+ const logger = getLogger();
5
+
3
6
  const DEFAULT_PROMPT: string = "USER: ";
4
7
 
5
- export class Prompt {
8
+ export interface IPrompt {
9
+ run(prompt?: string): Promise<string | undefined>;
10
+ shutdown(): Promise<void>;
11
+ }
12
+
13
+ export class Prompt implements IPrompt {
6
14
  private line: string | undefined;
7
15
  private online: { (line: string | undefined): void } | undefined;
8
16
  // private onerror: { (reason?: any): void } | undefined;
@@ -41,7 +49,7 @@ export class Prompt {
41
49
  });
42
50
  }
43
51
 
44
- shutdown(): void {
52
+ async shutdown(): Promise<void> {
45
53
  this.prompt.close();
46
54
  }
47
55
 
@@ -54,40 +62,33 @@ export class Prompt {
54
62
  }
55
63
 
56
64
  /**
57
- * Support prompts:
58
- * - some text (msg: some text, cmds: undefined)
59
- * - :i image.png some text (msg: some text, cmds: ["i", "image.png"])
60
- * - :i image.png (msg: undefined, cmds: ["i", "image.png"])
61
- * - :l (msg: undefined, cmds: ["l"])
62
- * - :e toolName .. (msg: undefined, cmds: ["e", "toolName", ...])
63
- * - :ea toolName .. (msg: undefined, cmds: ["ea", "toolName", ...])
65
+ * A prompt which just reads from a script
64
66
  */
65
- export function parsePrompt(prompt: string): {
66
- msg?: string;
67
- cmds?: string[];
68
- } {
69
- prompt = prompt.trim();
67
+ export class ScriptPrompt implements IPrompt {
68
+ lines: string[];
70
69
 
71
- let msg: string | undefined = undefined;
72
- let cmds: string[] | undefined = undefined;
70
+ constructor(lines: string[]) {
71
+ this.lines = lines;
72
+ }
73
73
 
74
- if (prompt.startsWith(":") || prompt.startsWith("/")) {
75
- cmds = prompt.split(" ");
76
- cmds[0] = cmds[0].slice(1);
74
+ run(_prompt?: string): Promise<string | undefined> {
75
+ return new Promise((r) => {
76
+ setTimeout(() => {
77
+ logger.debug(`[ScriptPrompt.run]: ${this.lines.length} remaining`);
77
78
 
78
- if (cmds[0] == "i") {
79
- // :i is special as it may have a trailing message
80
- const fileDelim = prompt.indexOf(" ", 3);
81
- if (fileDelim < 0) {
82
- cmds = [cmds[0], prompt.slice(3)];
83
- } else {
84
- msg = prompt.slice(fileDelim + 1);
85
- cmds = [cmds[0], prompt.slice(3, fileDelim)];
86
- }
87
- }
88
- } else {
89
- msg = prompt;
79
+ if (this.lines.length === 0) {
80
+ logger.debug("[ScriptPrompt.run]: returning undefined");
81
+ r(undefined);
82
+ } else {
83
+ const line = this.lines.shift();
84
+ logger.debug(`[ScriptPrompt.run]: returning: ${line}`);
85
+ r(line);
86
+ }
87
+ }, 1000);
88
+ });
90
89
  }
91
90
 
92
- return { msg, cmds };
91
+ shutdown(): Promise<void> {
92
+ return new Promise((r) => setTimeout(r, 100));
93
+ }
93
94
  }