@xalia/agent 0.5.7 → 0.6.0

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 (186) hide show
  1. package/README.md +23 -8
  2. package/dist/agent/src/agent/agent.js +176 -96
  3. package/dist/agent/src/agent/agentUtils.js +82 -59
  4. package/dist/agent/src/agent/compressingContextManager.js +102 -0
  5. package/dist/agent/src/agent/context.js +189 -0
  6. package/dist/agent/src/agent/dummyLLM.js +46 -5
  7. package/dist/agent/src/agent/mcpServerManager.js +23 -24
  8. package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
  9. package/dist/agent/src/agent/nullPlatform.js +14 -0
  10. package/dist/agent/src/agent/openAILLMStreaming.js +26 -14
  11. package/dist/agent/src/agent/promptProvider.js +63 -0
  12. package/dist/agent/src/agent/repeatLLM.js +5 -5
  13. package/dist/agent/src/agent/sudoMcpServerManager.js +23 -21
  14. package/dist/agent/src/agent/tokenAuth.js +7 -7
  15. package/dist/agent/src/agent/tools.js +1 -1
  16. package/dist/agent/src/chat/client/chatClient.js +733 -0
  17. package/dist/agent/src/chat/client/connection.js +209 -0
  18. package/dist/agent/src/chat/client/connection.test.js +188 -0
  19. package/dist/agent/src/chat/client/constants.js +5 -0
  20. package/dist/agent/src/chat/client/index.js +15 -0
  21. package/dist/agent/src/chat/client/interfaces.js +2 -0
  22. package/dist/agent/src/chat/client/responseHandler.js +105 -0
  23. package/dist/agent/src/chat/client/sessionClient.js +331 -0
  24. package/dist/agent/src/chat/client/teamManager.js +2 -0
  25. package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
  26. package/dist/agent/src/chat/data/dataModels.js +2 -0
  27. package/dist/agent/src/chat/data/database.js +749 -0
  28. package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
  29. package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
  30. package/dist/agent/src/chat/protocol/constants.js +50 -0
  31. package/dist/agent/src/chat/protocol/errors.js +22 -0
  32. package/dist/agent/src/chat/protocol/messages.js +110 -0
  33. package/dist/agent/src/chat/server/chatContextManager.js +405 -0
  34. package/dist/agent/src/chat/server/connectionManager.js +352 -0
  35. package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
  36. package/dist/agent/src/chat/server/conversation.js +198 -0
  37. package/dist/agent/src/chat/server/errorUtils.js +23 -0
  38. package/dist/agent/src/chat/server/openSession.js +869 -0
  39. package/dist/agent/src/chat/server/server.js +177 -0
  40. package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
  41. package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
  42. package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
  43. package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
  44. package/dist/agent/src/chat/server/tools.js +243 -0
  45. package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
  46. package/dist/agent/src/chat/utils/approvalManager.js +85 -0
  47. package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
  48. package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
  49. package/dist/agent/src/chat/utils/htmlToText.js +84 -0
  50. package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
  51. package/dist/agent/src/chat/utils/search.js +145 -0
  52. package/dist/agent/src/chat/utils/userResolver.js +46 -0
  53. package/dist/agent/src/chat/utils/websocket.js +16 -0
  54. package/dist/agent/src/test/agent.test.js +332 -0
  55. package/dist/agent/src/test/approvalManager.test.js +58 -0
  56. package/dist/agent/src/test/chatContextManager.test.js +392 -0
  57. package/dist/agent/src/test/clientServerConnection.test.js +158 -0
  58. package/dist/agent/src/test/compressingContextManager.test.js +65 -0
  59. package/dist/agent/src/test/context.test.js +83 -0
  60. package/dist/agent/src/test/conversation.test.js +89 -0
  61. package/dist/agent/src/test/db.test.js +271 -83
  62. package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
  63. package/dist/agent/src/test/dbTestTools.js +99 -0
  64. package/dist/agent/src/test/imageLoad.test.js +8 -7
  65. package/dist/agent/src/test/mcpServerManager.test.js +23 -20
  66. package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
  67. package/dist/agent/src/test/openaiStreaming.test.js +64 -35
  68. package/dist/agent/src/test/prompt.test.js +5 -4
  69. package/dist/agent/src/test/promptProvider.test.js +28 -0
  70. package/dist/agent/src/test/responseHandler.test.js +61 -0
  71. package/dist/agent/src/test/sudoMcpServerManager.test.js +24 -25
  72. package/dist/agent/src/test/testTools.js +109 -0
  73. package/dist/agent/src/test/tools.test.js +31 -0
  74. package/dist/agent/src/tool/agentChat.js +21 -10
  75. package/dist/agent/src/tool/agentMain.js +1 -1
  76. package/dist/agent/src/tool/chatMain.js +241 -58
  77. package/dist/agent/src/tool/commandPrompt.js +22 -17
  78. package/dist/agent/src/tool/files.js +20 -16
  79. package/dist/agent/src/tool/nodePlatform.js +47 -3
  80. package/dist/agent/src/tool/options.js +4 -4
  81. package/dist/agent/src/tool/prompt.js +19 -13
  82. package/eslint.config.mjs +14 -1
  83. package/package.json +14 -6
  84. package/scripts/chat_server +8 -0
  85. package/scripts/setup_chat +7 -2
  86. package/scripts/shutdown_chat_server +3 -0
  87. package/scripts/test_chat +135 -17
  88. package/src/agent/agent.ts +283 -138
  89. package/src/agent/agentUtils.ts +143 -108
  90. package/src/agent/compressingContextManager.ts +164 -0
  91. package/src/agent/context.ts +268 -0
  92. package/src/agent/dummyLLM.ts +76 -8
  93. package/src/agent/iAgentEventHandler.ts +54 -0
  94. package/src/agent/iplatform.ts +1 -0
  95. package/src/agent/mcpServerManager.ts +35 -31
  96. package/src/agent/nullAgentEventHandler.ts +20 -0
  97. package/src/agent/nullPlatform.ts +13 -0
  98. package/src/agent/openAILLMStreaming.ts +26 -13
  99. package/src/agent/promptProvider.ts +87 -0
  100. package/src/agent/repeatLLM.ts +5 -5
  101. package/src/agent/sudoMcpServerManager.ts +30 -29
  102. package/src/agent/tokenAuth.ts +7 -7
  103. package/src/agent/tools.ts +3 -1
  104. package/src/chat/client/chatClient.ts +900 -0
  105. package/src/chat/client/connection.test.ts +241 -0
  106. package/src/chat/client/connection.ts +276 -0
  107. package/src/chat/client/constants.ts +3 -0
  108. package/src/chat/client/index.ts +18 -0
  109. package/src/chat/client/interfaces.ts +34 -0
  110. package/src/chat/client/responseHandler.ts +131 -0
  111. package/src/chat/client/sessionClient.ts +443 -0
  112. package/src/chat/client/teamManager.ts +29 -0
  113. package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
  114. package/src/chat/data/dataModels.ts +85 -0
  115. package/src/chat/data/database.ts +982 -0
  116. package/src/chat/data/dbMcpServerConfigs.ts +59 -0
  117. package/src/chat/protocol/connectionMessages.ts +49 -0
  118. package/src/chat/protocol/constants.ts +55 -0
  119. package/src/chat/protocol/errors.ts +16 -0
  120. package/src/chat/protocol/messages.ts +682 -0
  121. package/src/chat/server/README.md +127 -0
  122. package/src/chat/server/chatContextManager.ts +612 -0
  123. package/src/chat/server/connectionManager.test.ts +266 -0
  124. package/src/chat/server/connectionManager.ts +541 -0
  125. package/src/chat/server/conversation.ts +269 -0
  126. package/src/chat/server/errorUtils.ts +28 -0
  127. package/src/chat/server/openSession.ts +1332 -0
  128. package/src/chat/server/server.ts +177 -0
  129. package/src/chat/server/sessionFileManager.ts +239 -0
  130. package/src/chat/server/sessionRegistry.test.ts +138 -0
  131. package/src/chat/server/sessionRegistry.ts +1064 -0
  132. package/src/chat/server/test-utils/mockFactories.ts +422 -0
  133. package/src/chat/server/tools.ts +265 -0
  134. package/src/chat/utils/agentSessionMap.ts +76 -0
  135. package/src/chat/utils/approvalManager.ts +111 -0
  136. package/src/{utils → chat/utils}/asyncLock.ts +3 -3
  137. package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
  138. package/src/chat/utils/htmlToText.ts +61 -0
  139. package/src/chat/utils/multiAsyncQueue.ts +52 -0
  140. package/src/chat/utils/search.ts +139 -0
  141. package/src/chat/utils/userResolver.ts +48 -0
  142. package/src/chat/utils/websocket.ts +16 -0
  143. package/src/test/agent.test.ts +487 -0
  144. package/src/test/approvalManager.test.ts +73 -0
  145. package/src/test/chatContextManager.test.ts +521 -0
  146. package/src/test/clientServerConnection.test.ts +207 -0
  147. package/src/test/compressingContextManager.test.ts +82 -0
  148. package/src/test/context.test.ts +105 -0
  149. package/src/test/conversation.test.ts +109 -0
  150. package/src/test/db.test.ts +358 -89
  151. package/src/test/dbMcpServerConfigs.test.ts +112 -0
  152. package/src/test/dbTestTools.ts +153 -0
  153. package/src/test/imageLoad.test.ts +7 -6
  154. package/src/test/mcpServerManager.test.ts +21 -16
  155. package/src/test/multiAsyncQueue.test.ts +125 -0
  156. package/src/test/openaiStreaming.test.ts +71 -36
  157. package/src/test/prompt.test.ts +4 -3
  158. package/src/test/promptProvider.test.ts +33 -0
  159. package/src/test/responseHandler.test.ts +78 -0
  160. package/src/test/sudoMcpServerManager.test.ts +32 -30
  161. package/src/test/testTools.ts +146 -0
  162. package/src/test/tools.test.ts +39 -0
  163. package/src/tool/agentChat.ts +26 -12
  164. package/src/tool/agentMain.ts +1 -1
  165. package/src/tool/chatMain.ts +292 -100
  166. package/src/tool/commandPrompt.ts +28 -19
  167. package/src/tool/files.ts +25 -19
  168. package/src/tool/nodePlatform.ts +52 -3
  169. package/src/tool/options.ts +4 -2
  170. package/src/tool/prompt.ts +22 -15
  171. package/test_data/dummyllm_script_crash.json +32 -0
  172. package/test_data/frog.png.b64 +1 -0
  173. package/vitest.config.ts +39 -0
  174. package/dist/agent/src/chat/client.js +0 -349
  175. package/dist/agent/src/chat/conversationManager.js +0 -392
  176. package/dist/agent/src/chat/db.js +0 -209
  177. package/dist/agent/src/chat/frontendClient.js +0 -74
  178. package/dist/agent/src/chat/server.js +0 -158
  179. package/src/chat/client.ts +0 -455
  180. package/src/chat/conversationManager.ts +0 -595
  181. package/src/chat/db.ts +0 -290
  182. package/src/chat/frontendClient.ts +0 -123
  183. package/src/chat/messages.ts +0 -235
  184. package/src/chat/server.ts +0 -177
  185. /package/dist/agent/src/{chat/messages.js → agent/iAgentEventHandler.js} +0 -0
  186. /package/{frog.png → test_data/frog.png} +0 -0
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ // -*- typescript -*-
3
+
4
+ import * as dotenv from "dotenv";
5
+ import { getLogger } from "@xalia/xmcp/sdk";
6
+ import { WebSocketServer } from "ws";
7
+ import * as ws from "ws";
8
+ import { IncomingMessage } from "http";
9
+ import { ConnectionManager, IUserConnectionManager } from "./connectionManager";
10
+ import { Database, resolveCompoundName, UserData } from "../data/database";
11
+ import { SessionData } from "../data/dataModels";
12
+ import { ApiKeyManager } from "../data/apiKeyManager";
13
+ import { ChatFatalError } from "../protocol/errors";
14
+ import { ServerToClient } from "../protocol/messages";
15
+ import { SessionRegistry } from "./sessionRegistry";
16
+
17
+ dotenv.config();
18
+
19
+ const logger = getLogger();
20
+
21
+ /**
22
+ * Extract error message from unknown error type
23
+ */
24
+ function extractErrorMessage(e: unknown): string {
25
+ if (typeof e === "string") return e;
26
+ if (e instanceof Error) return e.message;
27
+ return "Unknown connection error";
28
+ }
29
+
30
+ /**
31
+ * Send error response and close WebSocket connection
32
+ */
33
+ function sendErrorAndClose(ws: ws.WebSocket, message: string): void {
34
+ try {
35
+ sendErrorResponse(ws, message);
36
+ } catch (closeError) {
37
+ logger.error(`[server] Error closing connection:`, closeError);
38
+ } finally {
39
+ ws.close(4000, message);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Send error response but keep connection open
45
+ */
46
+ function sendErrorResponse(ws: ws.WebSocket, message: string): void {
47
+ try {
48
+ ws.send(
49
+ JSON.stringify({
50
+ t: "error",
51
+ e: message,
52
+ })
53
+ );
54
+ } catch (sendError) {
55
+ logger.error(`[server] Error sending error response:`, sendError);
56
+ }
57
+ }
58
+
59
+ export async function runServer(
60
+ port: number,
61
+ supabaseUrl: string,
62
+ supabaseKey: string,
63
+ llmUrl: string,
64
+ xmcpUrl: string
65
+ ): Promise<ws.Server> {
66
+ return new Promise((r, _e) => {
67
+ const wss = new WebSocketServer({ port });
68
+ const db = new Database(supabaseUrl, supabaseKey);
69
+ const apiKeyManager = new ApiKeyManager(db);
70
+ const createSessionRegistry = (
71
+ connManager: IUserConnectionManager<ServerToClient>
72
+ ) => {
73
+ return new SessionRegistry(db, connManager, llmUrl, xmcpUrl);
74
+ };
75
+ const connectionManager = ConnectionManager.init(
76
+ apiKeyManager,
77
+ createSessionRegistry
78
+ );
79
+
80
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
81
+ wss.on("connection", async (ws: ws.WebSocket, req: IncomingMessage) => {
82
+ try {
83
+ logger.info(`[server] connection: ${req.url || "unknown"}`);
84
+ logger.info(`[server] headers: ${JSON.stringify(req.headers)}`);
85
+
86
+ // Extract API key and clientMessageId from WebSocket subprotocol header
87
+ // Format: "apiKey, clientMessageId" (comma-separated)
88
+ const subprotocols = req.headers["sec-websocket-protocol"];
89
+ if (!subprotocols) {
90
+ throw new ChatFatalError("empty api key");
91
+ }
92
+
93
+ // Parse the subprotocols - they come as comma-separated string
94
+ if (typeof subprotocols !== "string") {
95
+ throw new ChatFatalError("subprotocols was not a string");
96
+ }
97
+ const protocols = subprotocols.split(",").map((p) => p.trim());
98
+ const apiKey = protocols[0];
99
+ if (!apiKey) {
100
+ throw new ChatFatalError("empty api key");
101
+ }
102
+
103
+ // Handle via ConnectionManager (multi-session protocol only)
104
+ await connectionManager.handleConnection(ws, apiKey, req);
105
+ } catch (e) {
106
+ logger.error(`[server] Connection error:`, e);
107
+
108
+ // Extract error message consistently
109
+ const errorMessage = extractErrorMessage(e);
110
+
111
+ if (e instanceof ChatFatalError) {
112
+ // Fatal errors: send error response and close connection
113
+ sendErrorAndClose(ws, errorMessage);
114
+ } else {
115
+ // Non-fatal errors: send error response but keep connection open
116
+ sendErrorResponse(ws, errorMessage);
117
+ logger.error(`[server] Client error:`, errorMessage);
118
+ }
119
+ }
120
+ });
121
+
122
+ logger.info(`[server] started: ws://localhost:${String(port)}/`);
123
+
124
+ r(wss);
125
+ });
126
+ }
127
+
128
+ export async function resolveSessionIdFromIdentifier(
129
+ db: Database,
130
+ sessionIdentifier: string
131
+ ): Promise<string | undefined> {
132
+ logger.info(`[resolveSessionIdFromIdentifier] ${sessionIdentifier}`);
133
+ let session: SessionData | undefined = undefined;
134
+ const compound = resolveCompoundName(sessionIdentifier);
135
+ logger.info(
136
+ `[resolveSessionIdFromIdentifier] compound: ${JSON.stringify(compound)}`
137
+ );
138
+ if (typeof compound === "string") {
139
+ // Interpret as an id
140
+ session = await db.sessionGetById(compound);
141
+ } else {
142
+ session = await db.sessionGetByName(compound[0], compound[1]);
143
+ }
144
+ logger.info(`[resolveSessionIdFromIdentifier] ${JSON.stringify(session)}`);
145
+ return session?.session_uuid;
146
+ }
147
+
148
+ /**
149
+ * Try as id, then by user/name and return the AgentProfile uuid.
150
+ */
151
+ export async function resolveAgentProfileId(
152
+ db: Database,
153
+ userData: UserData,
154
+ agentProfileIdentifier: string
155
+ ): Promise<string | undefined> {
156
+ let ap = await db.getSavedAgentProfileById(agentProfileIdentifier);
157
+ logger.debug(`[resolveAgentProfileId]: by id: {JSON.stringify(ap)}`);
158
+ if (ap) {
159
+ return agentProfileIdentifier;
160
+ }
161
+
162
+ ap = await db.getSavedAgentProfileByName(
163
+ userData.uuid,
164
+ agentProfileIdentifier
165
+ );
166
+ logger.debug(`[resolveAgentProfileId]: by name: {JSON.stringify(ap)}`);
167
+ if (ap) {
168
+ return ap.uuid;
169
+ }
170
+
171
+ logger.debug("[resolveAgentProfileId]: agent profile not found");
172
+ return undefined;
173
+ }
174
+
175
+ // Export main classes for external use
176
+ export { ConnectionManager } from "./connectionManager";
177
+ export { OpenSession } from "./openSession";
@@ -0,0 +1,239 @@
1
+ import { OpenAI } from "openai";
2
+ import { v4 as uuidv4 } from "uuid";
3
+
4
+ import {
5
+ Agent,
6
+ IAgentToolProvider,
7
+ ToolCallResult,
8
+ ToolHandler,
9
+ } from "../../agent/agent";
10
+ import { makeParseArgsFn } from "./tools";
11
+ import { Database } from "../data/database";
12
+
13
+ export const SESSION_FILE_TYPES = ["text", "image", "pdf"] as const;
14
+
15
+ // SessionFileType = "text" |"image" | "pdf";
16
+ export type SessionFileType = (typeof SESSION_FILE_TYPES)[number];
17
+
18
+ export type SessionFileDescriptor = {
19
+ name: string;
20
+ file_type: SessionFileType;
21
+ summary?: string;
22
+ };
23
+
24
+ export type SessionFileEntry = SessionFileDescriptor & { content: string };
25
+
26
+ /**
27
+ * Calling code implements this to be informed of file changes.
28
+ */
29
+ export interface ISessionFileManagerEventHandler {
30
+ onFileDescriptorChange(desc: SessionFileDescriptor): void;
31
+ onFileChange(entry: SessionFileEntry): void;
32
+ }
33
+
34
+ /**
35
+ * Interface to a set of files in a session.
36
+ */
37
+ export interface ISessionFileManager {
38
+ /**
39
+ * List file names, types and summaries,
40
+ */
41
+ listFiles(): SessionFileDescriptor[];
42
+ /**
43
+ * Retrieve file contents.
44
+ */
45
+ getFileContent(name: string): Promise<string>;
46
+ /**
47
+ * Create or update a file with the given name. Returns the name (which is
48
+ * created if one is not passed in).
49
+ */
50
+ putFileContent(
51
+ name: string | undefined,
52
+ file_type: SessionFileType,
53
+ summary: string | undefined,
54
+ content: string
55
+ ): Promise<string>;
56
+
57
+ setEventHandler(eventHandler: ISessionFileManagerEventHandler): void;
58
+ }
59
+
60
+ /**
61
+ * In-memory implementation of ISessionFileManager
62
+ */
63
+ export class MemoryFileManager implements ISessionFileManager {
64
+ private readonly files: Map<string, SessionFileEntry>;
65
+ private eventHandler: ISessionFileManagerEventHandler | undefined;
66
+
67
+ constructor() {
68
+ this.files = new Map();
69
+ this.eventHandler = undefined;
70
+ }
71
+
72
+ // ISessionFileManager.listFiles
73
+ listFiles(): SessionFileDescriptor[] {
74
+ return Array.from(this.files.values());
75
+ }
76
+
77
+ // ISessionFileManager.getFileContent
78
+ getFileContent(name: string): Promise<string> {
79
+ return new Promise((r, e) => {
80
+ const entry = this.files.get(name);
81
+ if (entry) {
82
+ r(entry.content);
83
+ } else {
84
+ e(new Error(`no such file ${name}`));
85
+ }
86
+ });
87
+ }
88
+
89
+ // ISessionFileManager.addFile
90
+ putFileContent(
91
+ name: string | undefined,
92
+ file_type: SessionFileType,
93
+ summary: string | undefined,
94
+ content: string
95
+ ): Promise<string> {
96
+ return new Promise((r, e) => {
97
+ if (!name) {
98
+ name = uuidv4();
99
+ }
100
+ if (!summary) {
101
+ summary = "";
102
+ }
103
+ if (!isSingleLine(summary)) {
104
+ e(new Error("summary must no contain new-lines"));
105
+ } else {
106
+ const entry = { name, file_type, summary, content };
107
+ this.eventHandler?.onFileChange(entry);
108
+ this.files.set(name, entry);
109
+ r(name);
110
+ }
111
+ });
112
+ }
113
+
114
+ // ISessionFileManager.setEventHandler
115
+ setEventHandler(eventHandler: ISessionFileManagerEventHandler) {
116
+ this.eventHandler = eventHandler;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Implementation of ISessionFileManager which can store files in the DB.
122
+ */
123
+ export class ChatSessionFileManager extends MemoryFileManager {
124
+ // private readonly db: Database;
125
+ // private readonly sessionUUID: string;
126
+
127
+ constructor(_sessionUUID: string, _db: Database) {
128
+ super();
129
+ // this.db = db;
130
+ // this.sessionUUID = sessionUUID;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Return the file list in a form easily parsable by the LLM.
136
+ */
137
+ export function listFilesForLLM(fm: ISessionFileManager): string {
138
+ const files = fm.listFiles();
139
+ if (files.length === 0) {
140
+ return "";
141
+ }
142
+ let summary = "Available files:\nname,type,summary\n";
143
+ for (const f of files) {
144
+ summary += `${f.name},${f.file_type},${f.summary || ""}\n`;
145
+ }
146
+ return summary;
147
+ }
148
+
149
+ const GET_FILE_CONTENT_TOOL: OpenAI.ChatCompletionTool = {
150
+ type: "function",
151
+ function: {
152
+ name: "get_file_content",
153
+ description: "Obtain the contents of a file listed in the system prompt",
154
+ parameters: {
155
+ type: "object",
156
+ properties: {
157
+ name: {
158
+ type: "string",
159
+ description: "File name",
160
+ },
161
+ },
162
+ required: ["name"],
163
+ },
164
+ },
165
+ };
166
+
167
+ const PUT_FILE_CONTENT_TOOL: OpenAI.ChatCompletionTool = {
168
+ type: "function",
169
+ function: {
170
+ name: "put_file_content",
171
+ description: "Create or update file content",
172
+ parameters: {
173
+ type: "object",
174
+ properties: {
175
+ name: {
176
+ type: "string",
177
+ description: "File name",
178
+ },
179
+ file_type: {
180
+ type: "string",
181
+ enum: SESSION_FILE_TYPES,
182
+ },
183
+ summary: {
184
+ type: "string",
185
+ description: "Content summary",
186
+ },
187
+ content: {
188
+ type: "string",
189
+ description: "Content (text/dataurl)",
190
+ },
191
+ },
192
+ required: ["name", "type", "summary", "content"],
193
+ },
194
+ },
195
+ };
196
+
197
+ export function fileManagerTool(
198
+ fileManager: ChatSessionFileManager
199
+ ): IAgentToolProvider {
200
+ // get_file_content
201
+ const getName = makeParseArgsFn(["name"] as const);
202
+ const getFileContentFn: ToolHandler = async (
203
+ _agent: Agent,
204
+ args: unknown
205
+ ): Promise<ToolCallResult> => {
206
+ const { name } = getName(args);
207
+ const response = await fileManager.getFileContent(name);
208
+ return { response };
209
+ };
210
+
211
+ // set_file_content
212
+ const putArgs = ["name", "file_type", "summary", "content"] as const;
213
+ const getNameSummaryContent = makeParseArgsFn(putArgs);
214
+ const putFileContentFn: ToolHandler = async (_: Agent, args: unknown) => {
215
+ const { name, file_type, summary, content } = getNameSummaryContent(args);
216
+ const response = await fileManager.putFileContent(
217
+ name,
218
+ file_type as SessionFileType,
219
+ summary,
220
+ content
221
+ );
222
+ return { response };
223
+ };
224
+
225
+ const provider = {
226
+ setup: (agent: Agent) => {
227
+ agent.addAgentTool(GET_FILE_CONTENT_TOOL, getFileContentFn);
228
+ agent.addAgentTool(PUT_FILE_CONTENT_TOOL, putFileContentFn);
229
+ return new Promise<void>((r) => {
230
+ r();
231
+ });
232
+ },
233
+ };
234
+ return provider;
235
+ }
236
+
237
+ function isSingleLine(str: string): boolean {
238
+ return !/[\r\n]/.test(str);
239
+ }
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { SessionRegistry } from "./sessionRegistry";
3
+ import {
4
+ createMockDatabase,
5
+ createMockUserConnectionManager,
6
+ createMockOpenSession,
7
+ setupStandardMockBehaviors,
8
+ MOCK_USERS,
9
+ } from "./test-utils/mockFactories";
10
+
11
+ // Mock the complex agent creation to avoid network calls
12
+ const mockCreateAgentWithoutSkills = vi.hoisted(() => vi.fn());
13
+ vi.mock("../../agent/agentUtils", () => ({
14
+ createAgentWithoutSkills: mockCreateAgentWithoutSkills,
15
+ }));
16
+
17
+ describe("SessionRegistry", () => {
18
+ let sessionRegistry: SessionRegistry;
19
+ let mockDatabase: ReturnType<typeof createMockDatabase>;
20
+ let mockUserConnectionManager: ReturnType<
21
+ typeof createMockUserConnectionManager
22
+ >;
23
+
24
+ beforeEach(() => {
25
+ vi.resetAllMocks();
26
+
27
+ // Setup the mock to return array with agent and skill manager
28
+ mockCreateAgentWithoutSkills.mockResolvedValue([
29
+ {
30
+ /* mock agent */
31
+ },
32
+ {
33
+ getServerBriefs: vi.fn().mockReturnValue([]),
34
+ /* mock skill manager */
35
+ },
36
+ ]);
37
+
38
+ // Create mocks using factories
39
+ mockDatabase = createMockDatabase();
40
+ mockUserConnectionManager = createMockUserConnectionManager();
41
+
42
+ // Setup standard behaviors
43
+ setupStandardMockBehaviors({
44
+ database: mockDatabase,
45
+ userConnectionManager: mockUserConnectionManager,
46
+ });
47
+
48
+ // Create SessionRegistry instance
49
+ sessionRegistry = new SessionRegistry(
50
+ mockDatabase.mock,
51
+ mockUserConnectionManager.mock,
52
+ "http://llm-api.test",
53
+ "http://xmcp-api.test"
54
+ );
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.restoreAllMocks();
59
+ });
60
+
61
+ describe("Session Membership Tracking", () => {
62
+ it("should correctly track user-session mapping", () => {
63
+ // Arrange - Setup session state using public API
64
+ const sessionId1 = "session-1";
65
+ const sessionId2 = "session-2";
66
+ const userId1 = MOCK_USERS.owner.uuid;
67
+ const userId2 = MOCK_USERS.participant.uuid;
68
+
69
+ // Mock database for session validation
70
+ mockDatabase.spies.sessionGetById.mockImplementation(
71
+ (sessionId: string) => {
72
+ if (sessionId === sessionId1 || sessionId === sessionId2) {
73
+ return Promise.resolve({
74
+ session_uuid: sessionId,
75
+ title: `Test Session`,
76
+ agent_profile_uuid: "agent-profile-1",
77
+ owner_uuid: userId1,
78
+ updated_at: "2025-01-01T00:00:00Z",
79
+ });
80
+ }
81
+ return Promise.resolve(null);
82
+ }
83
+ );
84
+
85
+ // Create mock sessions in openSessions map
86
+ const mockSession1 = createMockOpenSession(sessionId1);
87
+ const mockSession2 = createMockOpenSession(sessionId2);
88
+ mockSession1.spies.getSessionState.mockReturnValue([]);
89
+ mockSession2.spies.getSessionState.mockReturnValue([]);
90
+ // Use bracket notation to access private property for testing
91
+ const registryWithSessions = sessionRegistry as unknown as {
92
+ openSessions: Map<string, unknown>;
93
+ };
94
+ registryWithSessions.openSessions.set(sessionId1, mockSession1.mock);
95
+ registryWithSessions.openSessions.set(sessionId2, mockSession2.mock);
96
+
97
+ // Act - Use private method via bracket notation for testing
98
+ type SessionRegistryWithPrivate = {
99
+ addUserToSessionMemory: (userId: string, sessionId: string) => void;
100
+ };
101
+ const registryWithPrivate =
102
+ sessionRegistry as unknown as SessionRegistryWithPrivate;
103
+ registryWithPrivate.addUserToSessionMemory(userId1, sessionId1);
104
+ registryWithPrivate.addUserToSessionMemory(userId2, sessionId1);
105
+ registryWithPrivate.addUserToSessionMemory(userId1, sessionId2);
106
+
107
+ // Assert - Test getUserSessions for both users
108
+ const user1Sessions = sessionRegistry.getUserSessions(userId1);
109
+ expect(user1Sessions).toEqual(new Set([sessionId1, sessionId2]));
110
+
111
+ const user2Sessions = sessionRegistry.getUserSessions(userId2);
112
+ expect(user2Sessions).toEqual(new Set([sessionId1]));
113
+
114
+ // Assert - Test getInMemorySessionUsers for both sessions
115
+ const session1Users = sessionRegistry.getInMemorySessionUsers(sessionId1);
116
+ expect(session1Users).toEqual(new Set([userId1, userId2]));
117
+ expect(session1Users.size).toBe(2);
118
+
119
+ const session2Users = sessionRegistry.getInMemorySessionUsers(sessionId2);
120
+ expect(session2Users).toEqual(new Set([userId1]));
121
+ expect(session2Users.size).toBe(1);
122
+ });
123
+
124
+ it("should return empty sets for non-existent sessions or users", () => {
125
+ // Act & Assert - Test methods with non-existent data
126
+ const nonExistentSessionUsers = sessionRegistry.getInMemorySessionUsers(
127
+ "non-existent-session"
128
+ );
129
+ expect(nonExistentSessionUsers).toEqual(new Set());
130
+ expect(nonExistentSessionUsers.size).toBe(0);
131
+
132
+ const nonExistentUserSessions =
133
+ sessionRegistry.getUserSessions("non-existent-user");
134
+ expect(nonExistentUserSessions).toEqual(new Set());
135
+ expect(nonExistentUserSessions.size).toBe(0);
136
+ });
137
+ });
138
+ });