@xalia/agent 0.5.0 → 0.5.2

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 (76) hide show
  1. package/README.md +46 -7
  2. package/dist/{agent.js → agent/src/agent/agent.js} +5 -4
  3. package/dist/{agentUtils.js → agent/src/agent/agentUtils.js} +10 -9
  4. package/dist/{mcpServerManager.js → agent/src/agent/mcpServerManager.js} +2 -1
  5. package/dist/{sudoMcpServerManager.js → agent/src/agent/sudoMcpServerManager.js} +4 -4
  6. package/dist/agent/src/chat/apiKeyManager.js +23 -0
  7. package/dist/agent/src/chat/asyncQueue.js +41 -0
  8. package/dist/agent/src/chat/client.js +126 -0
  9. package/dist/agent/src/chat/conversationManager.js +173 -0
  10. package/dist/agent/src/chat/db.js +186 -0
  11. package/dist/agent/src/chat/messages.js +2 -0
  12. package/dist/agent/src/chat/server.js +158 -0
  13. package/dist/agent/src/index.js +2 -0
  14. package/dist/agent/src/test/db.test.js +73 -0
  15. package/dist/{test → agent/src/test}/imageLoad.test.js +1 -1
  16. package/dist/{test → agent/src/test}/mcpServerManager.test.js +1 -1
  17. package/dist/{test → agent/src/test}/prompt.test.js +1 -1
  18. package/dist/{test → agent/src/test}/sudoMcpServerManager.test.js +3 -3
  19. package/dist/{chat.js → agent/src/tool/agentChat.js} +5 -5
  20. package/dist/{main.js → agent/src/tool/agentMain.js} +9 -15
  21. package/dist/agent/src/tool/chatMain.js +207 -0
  22. package/dist/agent/src/tool/main.js +54 -0
  23. package/dist/{options.js → agent/src/tool/options.js} +36 -2
  24. package/dist/agent/src/utils/asyncLock.js +45 -0
  25. package/dist/supabase/database.types.js +8 -0
  26. package/eslint.config.mjs +14 -14
  27. package/package.json +9 -15
  28. package/scripts/test_chat +84 -0
  29. package/src/{agent.ts → agent/agent.ts} +22 -11
  30. package/src/{agentUtils.ts → agent/agentUtils.ts} +13 -14
  31. package/src/{mcpServerManager.ts → agent/mcpServerManager.ts} +2 -1
  32. package/src/{sudoMcpServerManager.ts → agent/sudoMcpServerManager.ts} +3 -3
  33. package/src/chat/apiKeyManager.ts +24 -0
  34. package/src/chat/asyncQueue.ts +51 -0
  35. package/src/chat/client.ts +142 -0
  36. package/src/chat/conversationManager.ts +283 -0
  37. package/src/chat/db.ts +264 -0
  38. package/src/chat/messages.ts +91 -0
  39. package/src/chat/server.ts +177 -0
  40. package/src/test/db.test.ts +103 -0
  41. package/src/test/imageLoad.test.ts +1 -1
  42. package/src/test/mcpServerManager.test.ts +1 -1
  43. package/src/test/prompt.test.ts +1 -1
  44. package/src/test/sudoMcpServerManager.test.ts +6 -10
  45. package/src/{chat.ts → tool/agentChat.ts} +26 -24
  46. package/src/{main.ts → tool/agentMain.ts} +12 -19
  47. package/src/tool/chatMain.ts +250 -0
  48. package/src/{files.ts → tool/files.ts} +1 -1
  49. package/src/tool/main.ts +25 -0
  50. package/src/{nodePlatform.ts → tool/nodePlatform.ts} +1 -1
  51. package/src/{options.ts → tool/options.ts} +40 -1
  52. package/src/utils/asyncLock.ts +43 -0
  53. package/test_data/simplecalc_profile.json +1 -1
  54. package/test_data/sudomcp_import_profile.json +1 -1
  55. package/test_data/test_script_profile.json +1 -1
  56. package/tsconfig.json +1 -1
  57. package/scripts/test_script +0 -60
  58. /package/dist/{dummyLLM.js → agent/src/agent/dummyLLM.js} +0 -0
  59. /package/dist/{iplatform.js → agent/src/agent/iplatform.js} +0 -0
  60. /package/dist/{llm.js → agent/src/agent/llm.js} +0 -0
  61. /package/dist/{openAILLM.js → agent/src/agent/openAILLM.js} +0 -0
  62. /package/dist/{openAILLMStreaming.js → agent/src/agent/openAILLMStreaming.js} +0 -0
  63. /package/dist/{tokenAuth.js → agent/src/agent/tokenAuth.js} +0 -0
  64. /package/dist/{tools.js → agent/src/agent/tools.js} +0 -0
  65. /package/dist/{files.js → agent/src/tool/files.js} +0 -0
  66. /package/dist/{nodePlatform.js → agent/src/tool/nodePlatform.js} +0 -0
  67. /package/dist/{prompt.js → agent/src/tool/prompt.js} +0 -0
  68. /package/src/{dummyLLM.ts → agent/dummyLLM.ts} +0 -0
  69. /package/src/{iplatform.ts → agent/iplatform.ts} +0 -0
  70. /package/src/{llm.ts → agent/llm.ts} +0 -0
  71. /package/src/{openAILLM.ts → agent/openAILLM.ts} +0 -0
  72. /package/src/{openAILLMStreaming.ts → agent/openAILLMStreaming.ts} +0 -0
  73. /package/src/{tokenAuth.ts → agent/tokenAuth.ts} +0 -0
  74. /package/src/{tools.ts → agent/tools.ts} +0 -0
  75. /package/src/{test/prompt.test.src → index.ts} +0 -0
  76. /package/src/{prompt.ts → tool/prompt.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { getLogger } from "@xalia/xmcp/sdk";
2
2
  import { Agent, AgentProfile, OnMessageCB, OnToolCallCB } from "./agent";
3
3
  import { IPlatform } from "./iplatform";
4
- import { SudoMcpServerManager } from "./sudoMcpServerManager";
4
+ import { SkillManager } from "./sudoMcpServerManager";
5
5
  import OpenAI from "openai";
6
6
  import { Configuration as SudoMcpConfiguration } from "@xalia/xmcp/sdk";
7
7
  import { OpenAILLM } from "./openAILLM";
@@ -13,6 +13,7 @@ import { strict as assert } from "assert";
13
13
  const logger = getLogger();
14
14
 
15
15
  export const DEFAULT_LLM_URL = "http://localhost:5001/v1";
16
+ export const DEFAULT_LLM_MODEL = "gpt-4o";
16
17
 
17
18
  /**
18
19
  * Util function to create an Agent from some config information.
@@ -61,28 +62,28 @@ async function createAgent(
61
62
  /**
62
63
  * Util function to create and initialize an Agent given an AgentProfile.
63
64
  */
64
- export async function createAgentAndSudoMcpServerManager(
65
- url: string,
65
+ export async function createAgentWithSkills(
66
+ llmUrl: string,
66
67
  agentProfile: AgentProfile,
67
68
  onMessage: OnMessageCB,
68
69
  onToolCall: OnToolCallCB,
69
70
  platform: IPlatform,
70
- openaiApiKey: string | undefined,
71
+ llmApiKey: string | undefined,
71
72
  sudomcpConfig: SudoMcpConfiguration,
72
73
  authorizedUrl: string | undefined,
73
74
  conversation: OpenAI.ChatCompletionMessageParam[] | undefined,
74
75
  stream: boolean = false
75
- ): Promise<[Agent, SudoMcpServerManager]> {
76
+ ): Promise<[Agent, SkillManager]> {
76
77
  // Create agent
77
78
  logger.debug("[createAgentAndSudoMcpServerManager] creating agent ...");
78
79
  const agent = await createAgent(
79
- url,
80
+ llmUrl,
80
81
  agentProfile.model,
81
82
  agentProfile.system_prompt,
82
83
  onMessage,
83
84
  onToolCall,
84
85
  platform,
85
- openaiApiKey,
86
+ llmApiKey,
86
87
  stream
87
88
  );
88
89
  if (conversation) {
@@ -90,10 +91,8 @@ export async function createAgentAndSudoMcpServerManager(
90
91
  }
91
92
 
92
93
  // Init SudoMcpServerManager
93
- logger.debug(
94
- "[createAgentAndSudoMcpServerManager] creating SudoMcpServerManager."
95
- );
96
- const sudoMcpServerManager = await SudoMcpServerManager.initialize(
94
+ logger.debug("[createAgentWithSkills] creating SudoMcpServerManager.");
95
+ const sudoMcpServerManager = await SkillManager.initialize(
97
96
  agent.getMcpServerManager(),
98
97
  platform.openUrl,
99
98
  sudomcpConfig.backend_url,
@@ -101,12 +100,12 @@ export async function createAgentAndSudoMcpServerManager(
101
100
  authorizedUrl
102
101
  );
103
102
  logger.debug(
104
- "[createAgentAndSudoMcpServerManager] restore mcp settings:" +
103
+ "[createAgentWithSkills] restore mcp settings:" +
105
104
  JSON.stringify(agentProfile.mcp_settings)
106
105
  );
107
106
  await sudoMcpServerManager.restoreMcpSettings(agentProfile.mcp_settings);
108
107
 
109
- logger.debug("[createAgentAndSudoMcpServerManager] done");
108
+ logger.debug("[createAgentWithSkills] done");
110
109
  return [agent, sudoMcpServerManager];
111
110
  }
112
111
 
@@ -135,7 +134,7 @@ export async function createNonInteractiveAgent(
135
134
  return false;
136
135
  };
137
136
 
138
- const [agent, _] = await createAgentAndSudoMcpServerManager(
137
+ const [agent, _] = await createAgentWithSkills(
139
138
  url,
140
139
  agentProfile,
141
140
  onMessage,
@@ -177,7 +177,8 @@ export class McpServerManager {
177
177
  try {
178
178
  await client.connect(transport);
179
179
  } catch (e) {
180
- // TODO: is this catch necessary?
180
+ // Ensure the socket is closed so the process can exit if there is an
181
+ // error at connection time.
181
182
  await client.close();
182
183
  throw e;
183
184
  }
@@ -39,7 +39,7 @@ class SanitizedServerBrief extends McpServerBrief {
39
39
  * Manages access to the catalogue of servers hosted by sudomcp. Supports
40
40
  * adding these servers to McpServerManager.
41
41
  */
42
- export class SudoMcpServerManager {
42
+ export class SkillManager {
43
43
  private constructor(
44
44
  private mcpServerManager: McpServerManager,
45
45
  private apiClient: ApiClient,
@@ -69,7 +69,7 @@ export class SudoMcpServerManager {
69
69
  sudoMcpUrl?: string,
70
70
  sudoMcpApiKey?: string,
71
71
  authorized_url?: string
72
- ): Promise<SudoMcpServerManager> {
72
+ ): Promise<SkillManager> {
73
73
  // TODO: Keep it on here and pass to `McpServerManager.addMcpServer`
74
74
  const apiClient = new ApiClient(
75
75
  sudoMcpUrl ?? DEFAULT_SERVER_URL,
@@ -79,7 +79,7 @@ export class SudoMcpServerManager {
79
79
  const servers = await apiClient.listServers();
80
80
 
81
81
  const [mcpServers, mcpServersMap] = buildServersList(servers);
82
- return new SudoMcpServerManager(
82
+ return new SkillManager(
83
83
  mcpServerManager,
84
84
  apiClient,
85
85
  mcpServers,
@@ -0,0 +1,24 @@
1
+ // TODO:
2
+ // - lru-cache
3
+
4
+ import { Database, UserData } from "./db";
5
+
6
+ export class ApiKeyManager {
7
+ constructor(private db: Database) {}
8
+
9
+ public async verifyApiKey(apiKey: string): Promise<UserData | undefined> {
10
+ // TODO: Cache this
11
+
12
+ const userInfo = await this.db.getUserDataFromApiKey(apiKey);
13
+ return userInfo;
14
+
15
+ // if (apiKey.startsWith("dummy_key")) {
16
+ // return {
17
+ // user_uuid: apiKey,
18
+ // nickname: apiKey,
19
+ // };
20
+ // }
21
+
22
+ // return undefined;
23
+ }
24
+ }
@@ -0,0 +1,51 @@
1
+ export class AsyncQueue<T> {
2
+ private queue: T[];
3
+ private process: (queueEntry: T) => Promise<void>;
4
+ private maxBacklog: number;
5
+ private running: boolean = false;
6
+
7
+ constructor(
8
+ process: (queueEntry: T) => Promise<void>,
9
+ maxBacklog: number = 100
10
+ ) {
11
+ this.queue = [];
12
+ this.process = process;
13
+ this.maxBacklog = maxBacklog;
14
+ }
15
+
16
+ public getLength(): number {
17
+ return this.queue.length;
18
+ }
19
+
20
+ public getMaxBacklog(): number {
21
+ return this.maxBacklog;
22
+ }
23
+
24
+ public async enqueueAsync(queueEntry: T): Promise<void> {
25
+ while (this.maxBacklog > 0 && this.queue.length >= this.maxBacklog) {
26
+ await new Promise((r) => setTimeout(r, 1));
27
+ }
28
+ this.queue.push(queueEntry);
29
+ this.tryNext();
30
+ }
31
+
32
+ private shift(): T | undefined {
33
+ return this.queue.shift();
34
+ }
35
+
36
+ private async tryNext(): Promise<void> {
37
+ if (this.running) {
38
+ return;
39
+ }
40
+
41
+ const queueEntry = this.shift();
42
+ if (queueEntry) {
43
+ this.running = true;
44
+ await this.process(queueEntry);
45
+ this.running = false;
46
+
47
+ // Check for more tasks on the queue.
48
+ setTimeout(() => this.tryNext());
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,142 @@
1
+ import * as dotenv from "dotenv";
2
+ import { getLogger } from "@xalia/xmcp/sdk";
3
+ import { strict as assert } from "assert";
4
+ import * as websocket from "ws";
5
+ import { ServerToClient, ClientToServer } from "./messages";
6
+
7
+ dotenv.config();
8
+
9
+ const logger = getLogger();
10
+
11
+ type OnMessageCallback = (msg: ServerToClient) => void;
12
+
13
+ type OnConnectionClosedCallback = () => void;
14
+
15
+ export class ChatClient {
16
+ private ws: websocket.WebSocket;
17
+ private onMessageCB: OnMessageCallback;
18
+ private onConnectionClosedCB: OnConnectionClosedCallback;
19
+ private closed: boolean;
20
+
21
+ private constructor(
22
+ ws: websocket.WebSocket,
23
+ onMessageCB: OnMessageCallback,
24
+ onConnectionClosedCB: OnConnectionClosedCallback
25
+ ) {
26
+ this.ws = ws;
27
+ this.onMessageCB = onMessageCB;
28
+ this.onConnectionClosedCB = onConnectionClosedCB;
29
+ this.closed = false;
30
+ }
31
+
32
+ static async initWithParams(
33
+ host: string,
34
+ port: number,
35
+ token: string,
36
+ params: Record<string, string>,
37
+ onMessageCB: OnMessageCallback,
38
+ onConnectionClosedCB: OnConnectionClosedCallback
39
+ ): Promise<ChatClient> {
40
+ return new Promise((r, e) => {
41
+ const urlParams = new URLSearchParams(params);
42
+ const url = `ws://${host}:${port}?${urlParams}`;
43
+
44
+ const ws = new websocket.WebSocket(url, [token]);
45
+ logger.info("created ws");
46
+
47
+ const client = new ChatClient(ws, onMessageCB, onConnectionClosedCB);
48
+
49
+ ws.onopen = async () => {
50
+ logger.info("opened");
51
+
52
+ ws.onmessage = (ev: websocket.MessageEvent) => {
53
+ try {
54
+ const msgData = ev.data;
55
+ if (typeof msgData !== "string") {
56
+ throw `expected "string" data, got ${typeof msgData}`;
57
+ }
58
+ logger.debug(`[client.onmessage]: ${msgData}`);
59
+ const msg: ServerToClient = JSON.parse(msgData);
60
+ client.onMessageCB(msg);
61
+ } catch (e) {
62
+ client.close();
63
+ throw e;
64
+ }
65
+ };
66
+ r(client);
67
+ };
68
+
69
+ ws.onclose = (err) => {
70
+ logger.info("closed");
71
+ logger.info(
72
+ `[client] WebSocket connection closed: ${JSON.stringify(err)}`
73
+ );
74
+ client.closed = true;
75
+ onConnectionClosedCB();
76
+ };
77
+
78
+ ws.onerror = (error) => {
79
+ logger.error("[client] WebSocket error:", JSON.stringify(error));
80
+ e(error);
81
+
82
+ client.closed = true;
83
+ onConnectionClosedCB();
84
+ };
85
+ });
86
+ }
87
+
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
+ public static async init(
107
+ host: string,
108
+ port: number,
109
+ token: string,
110
+ onMessageCB: OnMessageCallback,
111
+ onConnectionClosedCB: OnConnectionClosedCallback,
112
+ sessionId: string = "untitled",
113
+ agentProfileId: string | undefined = undefined
114
+ ): Promise<ChatClient> {
115
+ const params: Record<string, string> = { session_id: sessionId };
116
+ if (agentProfileId) {
117
+ params["agent_profile_id"] = agentProfileId;
118
+ }
119
+ return ChatClient.initWithParams(
120
+ host,
121
+ port,
122
+ token,
123
+ params,
124
+ onMessageCB,
125
+ onConnectionClosedCB
126
+ );
127
+ }
128
+
129
+ public sendMessage(message: ClientToServer): void {
130
+ assert(!this.closed);
131
+ const data = JSON.stringify(message);
132
+ this.ws.send(data);
133
+ }
134
+
135
+ public close(): void {
136
+ this.closed = true;
137
+ this.onConnectionClosedCB();
138
+ this.ws.close();
139
+ }
140
+ }
141
+
142
+ // TODO: remove this
@@ -0,0 +1,283 @@
1
+ import * as ws from "ws";
2
+ import { v4 as uuidv4 } from "uuid";
3
+ import { strict as assert } from "assert";
4
+
5
+ import { Configuration, getLogger } from "@xalia/xmcp/sdk";
6
+
7
+ import {
8
+ Agent,
9
+ ChatCompletionMessageParam,
10
+ ChatCompletionMessageToolCall,
11
+ } from "../agent/agent";
12
+ import { SkillManager } from "../agent/sudoMcpServerManager";
13
+ import { createAgentWithSkills } from "../agent/agentUtils";
14
+
15
+ import type {
16
+ ClientToServer,
17
+ ServerToClient,
18
+ ClientUserMessage,
19
+ } from "./messages";
20
+ import { AsyncQueue } from "./asyncQueue";
21
+ import { Database, UserData } from "./db";
22
+ import { AsyncLock } from "../utils/asyncLock";
23
+
24
+ const logger = getLogger();
25
+
26
+ export class UserAlreadyConnected extends Error {
27
+ constructor() {
28
+ super("User already connected to the conversation");
29
+ }
30
+ }
31
+
32
+ type QueuedClientMessage<T extends ClientToServer = ClientToServer> = {
33
+ msg: T;
34
+ from: string;
35
+ };
36
+
37
+ /**
38
+ * Describes a Session (conversation) with connected participants.
39
+ */
40
+ export class OpenSession {
41
+ public onEmpty: () => void;
42
+ public connections: Record<string, ws.WebSocket>;
43
+ public agent: Agent;
44
+ public skillManager: SkillManager;
45
+ public messageQueue: AsyncQueue<QueuedClientMessage>;
46
+ public curAgentMsgId: string | undefined;
47
+
48
+ constructor(
49
+ agent: Agent,
50
+ sudoMcpServerManager: SkillManager,
51
+ onEmpty: () => void
52
+ ) {
53
+ // public agent: Agent,
54
+ this.agent = agent;
55
+ this.skillManager = sudoMcpServerManager;
56
+ this.onEmpty = onEmpty;
57
+ this.connections = {};
58
+ this.messageQueue = new AsyncQueue<QueuedClientMessage>((m) =>
59
+ this.onMessage(m)
60
+ );
61
+ this.curAgentMsgId = undefined;
62
+ }
63
+
64
+ join(userName: string, ws: ws.WebSocket): void {
65
+ if (this.connections[userName]) {
66
+ throw new UserAlreadyConnected();
67
+ }
68
+
69
+ // Inform any other participants, and add the WebSocket to the map.
70
+
71
+ this.broadcast({ type: "user_joined", user: userName });
72
+ this.connections[userName] = ws;
73
+
74
+ ws.on("message", async (message: ws.RawData) => {
75
+ logger.debug(`[convMgr]: got message: (from ${userName}): ${message}`);
76
+ const msgStr = message.toString();
77
+ const msg: ClientToServer = JSON.parse(msgStr);
78
+ await this.messageQueue.enqueueAsync({ msg, from: userName });
79
+ });
80
+
81
+ ws.on("close", () => {
82
+ logger.debug(`[convMgr]: ${userName} closed`);
83
+
84
+ // Remove our connection then inform any other participants
85
+
86
+ delete this.connections[userName];
87
+ this.broadcast({ type: "user_left", user: userName });
88
+
89
+ if (Object.keys(this.connections).length == 0) {
90
+ this.onEmpty();
91
+ }
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Called once for each message. Messages are queued to avoid overlapping
97
+ * calls to the LLM.
98
+ */
99
+ async onMessage(queuedMessage: QueuedClientMessage): Promise<void> {
100
+ logger.debug(
101
+ `[onMessage]: processing (${queuedMessage.from}) ` +
102
+ `${JSON.stringify(queuedMessage.msg)}`
103
+ );
104
+
105
+ const msg = queuedMessage.msg;
106
+ switch (msg.type) {
107
+ case "msg":
108
+ return this.onChatMessage(
109
+ queuedMessage as QueuedClientMessage<ClientUserMessage>
110
+ );
111
+ default:
112
+ throw `unknown message: ${JSON.stringify(queuedMessage)}`;
113
+ }
114
+ }
115
+
116
+ async onAgentMessage(msg: string, end: boolean): Promise<void> {
117
+ logger.debug(`[onAgentMessage] msg: ${msg}, end: ${end}`);
118
+ assert(this.curAgentMsgId);
119
+ this.broadcast({
120
+ type: "agent_msg_chunk",
121
+ message_id: this.curAgentMsgId,
122
+ message: msg,
123
+ end,
124
+ });
125
+ }
126
+
127
+ async onChatMessage(
128
+ queuedMessage: QueuedClientMessage<ClientUserMessage>
129
+ ): Promise<void> {
130
+ const msg = queuedMessage.msg;
131
+ const userToken = queuedMessage.from;
132
+ const msgId = uuidv4();
133
+ this.broadcast({
134
+ type: "user_msg",
135
+ message_id: msgId,
136
+ message: msg.message,
137
+ from: userToken,
138
+ });
139
+
140
+ // Messages will be handled by the Agent.onMessage callback. We await the
141
+ // response here before processing further messages.
142
+
143
+ this.curAgentMsgId = `${msgId}-resp`;
144
+ await this.agent.userMessage(msg.message, undefined, userToken);
145
+ }
146
+
147
+ broadcast(msg: ServerToClient) {
148
+ logger.info(`[broadcast]: broadcast msg: ${JSON.stringify(msg)}`);
149
+ const msgString = JSON.stringify(msg);
150
+ for (const ws of Object.values(this.connections)) {
151
+ ws.send(msgString);
152
+ }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Handles forwarding of messages between all participants of a session, as
158
+ * well as messages to/from and Agent.
159
+ */
160
+ export class ConversationManager {
161
+ public db: Database;
162
+ public llmUrl: string;
163
+ public xmcpUrl: string;
164
+ public openSessionsLock: AsyncLock;
165
+ public openSessions: Record<string, OpenSession>;
166
+
167
+ constructor(db: Database, llmUrl: string, xmcpUrl: string) {
168
+ this.db = db;
169
+ this.llmUrl = llmUrl;
170
+ this.xmcpUrl = xmcpUrl;
171
+ this.openSessionsLock = new AsyncLock();
172
+ this.openSessions = {};
173
+ }
174
+
175
+ async join(
176
+ sessionId: string,
177
+ llmApiKey: string,
178
+ xmcpApiKey: string,
179
+ userData: UserData,
180
+ ws: ws.WebSocket
181
+ ): Promise<void> {
182
+ await this.openSessionsLock.lockAndProcess(() => {
183
+ return this.getOrCreateAndSubscribe(
184
+ sessionId,
185
+ llmApiKey,
186
+ xmcpApiKey,
187
+ userData.nickname || userData.user_uuid,
188
+ ws
189
+ );
190
+ });
191
+ }
192
+ /**
193
+ * Must be called while holding the openSessionsLock
194
+ */
195
+ private async getOrCreateAndSubscribe(
196
+ sessionId: string,
197
+ llmApiKey: string,
198
+ xmcpApiKey: string,
199
+ userName: string,
200
+ ws: ws.WebSocket
201
+ ): Promise<OpenSession> {
202
+ let openSession = this.openSessions[sessionId];
203
+ if (openSession) {
204
+ openSession.join(userName, ws);
205
+ return openSession;
206
+ }
207
+
208
+ // Create a new session
209
+
210
+ // TODO: The owner of llmApiKey and xmcpApiKey may not be the owner of the
211
+ // session. Should we create the Agent and SudoMcpServerManager with the
212
+ // session-owners api key?
213
+
214
+ const onEmpty = () => {
215
+ logger.debug(`session ${sessionId} empty. removing`);
216
+ delete this.openSessions[sessionId];
217
+ };
218
+
219
+ const sessionData = await this.db.getSessionById(sessionId);
220
+ if (!sessionData) {
221
+ throw `no such session ${sessionId}`;
222
+ }
223
+ const conversation =
224
+ sessionData.conversation as unknown as ChatCompletionMessageParam[];
225
+
226
+ const agentProfile = await this.db.getAgentProfileById(
227
+ sessionData.agent_profile_uuid
228
+ );
229
+ if (!agentProfile) {
230
+ throw `no such agent profile ${sessionData.agent_profile_uuid}`;
231
+ }
232
+
233
+ const platform = {
234
+ openUrl: (
235
+ _url: string,
236
+ _authResultP: Promise<boolean>,
237
+ _displayName: string
238
+ ) => {
239
+ throw "unimpl";
240
+ },
241
+ load: (_filename: string): Promise<string> => {
242
+ throw "unimpl";
243
+ },
244
+ };
245
+
246
+ // Forward agent messages to the OpenSession (once it is iniailized
247
+
248
+ const context: { openSession?: OpenSession } = {};
249
+ const onMessage = async (msg: string, end: boolean) => {
250
+ logger.debug(`[onMessage] msg: ${msg}, end: ${end}`);
251
+ assert(context.openSession);
252
+ context.openSession.onAgentMessage(msg, end);
253
+ };
254
+
255
+ const onToolCall = async (
256
+ toolCall: ChatCompletionMessageToolCall
257
+ ): Promise<boolean> => {
258
+ logger.debug(`[onToolCall] : ${JSON.stringify(toolCall)}`);
259
+ return true;
260
+ };
261
+
262
+ const xmcpConfig = Configuration.new(xmcpApiKey, this.xmcpUrl, false);
263
+
264
+ const [agent, smsm] = await createAgentWithSkills(
265
+ this.llmUrl,
266
+ agentProfile,
267
+ onMessage,
268
+ onToolCall,
269
+ platform,
270
+ llmApiKey,
271
+ xmcpConfig,
272
+ undefined,
273
+ conversation,
274
+ true
275
+ );
276
+
277
+ openSession = new OpenSession(agent, smsm, onEmpty);
278
+ context.openSession = openSession;
279
+ this.openSessions[sessionId] = openSession;
280
+ openSession.join(userName, ws);
281
+ return openSession;
282
+ }
283
+ }