@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.
- package/README.md +23 -8
- package/dist/agent/src/agent/agent.js +176 -96
- package/dist/agent/src/agent/agentUtils.js +82 -59
- package/dist/agent/src/agent/compressingContextManager.js +102 -0
- package/dist/agent/src/agent/context.js +189 -0
- package/dist/agent/src/agent/dummyLLM.js +46 -5
- package/dist/agent/src/agent/mcpServerManager.js +23 -24
- package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
- package/dist/agent/src/agent/nullPlatform.js +14 -0
- package/dist/agent/src/agent/openAILLMStreaming.js +26 -14
- package/dist/agent/src/agent/promptProvider.js +63 -0
- package/dist/agent/src/agent/repeatLLM.js +5 -5
- package/dist/agent/src/agent/sudoMcpServerManager.js +23 -21
- package/dist/agent/src/agent/tokenAuth.js +7 -7
- package/dist/agent/src/agent/tools.js +1 -1
- package/dist/agent/src/chat/client/chatClient.js +733 -0
- package/dist/agent/src/chat/client/connection.js +209 -0
- package/dist/agent/src/chat/client/connection.test.js +188 -0
- package/dist/agent/src/chat/client/constants.js +5 -0
- package/dist/agent/src/chat/client/index.js +15 -0
- package/dist/agent/src/chat/client/interfaces.js +2 -0
- package/dist/agent/src/chat/client/responseHandler.js +105 -0
- package/dist/agent/src/chat/client/sessionClient.js +331 -0
- package/dist/agent/src/chat/client/teamManager.js +2 -0
- package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
- package/dist/agent/src/chat/data/dataModels.js +2 -0
- package/dist/agent/src/chat/data/database.js +749 -0
- package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
- package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
- package/dist/agent/src/chat/protocol/constants.js +50 -0
- package/dist/agent/src/chat/protocol/errors.js +22 -0
- package/dist/agent/src/chat/protocol/messages.js +110 -0
- package/dist/agent/src/chat/server/chatContextManager.js +405 -0
- package/dist/agent/src/chat/server/connectionManager.js +352 -0
- package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
- package/dist/agent/src/chat/server/conversation.js +198 -0
- package/dist/agent/src/chat/server/errorUtils.js +23 -0
- package/dist/agent/src/chat/server/openSession.js +869 -0
- package/dist/agent/src/chat/server/server.js +177 -0
- package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
- package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
- package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
- package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
- package/dist/agent/src/chat/server/tools.js +243 -0
- package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
- package/dist/agent/src/chat/utils/approvalManager.js +85 -0
- package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
- package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
- package/dist/agent/src/chat/utils/htmlToText.js +84 -0
- package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
- package/dist/agent/src/chat/utils/search.js +145 -0
- package/dist/agent/src/chat/utils/userResolver.js +46 -0
- package/dist/agent/src/chat/utils/websocket.js +16 -0
- package/dist/agent/src/test/agent.test.js +332 -0
- package/dist/agent/src/test/approvalManager.test.js +58 -0
- package/dist/agent/src/test/chatContextManager.test.js +392 -0
- package/dist/agent/src/test/clientServerConnection.test.js +158 -0
- package/dist/agent/src/test/compressingContextManager.test.js +65 -0
- package/dist/agent/src/test/context.test.js +83 -0
- package/dist/agent/src/test/conversation.test.js +89 -0
- package/dist/agent/src/test/db.test.js +271 -83
- package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
- package/dist/agent/src/test/dbTestTools.js +99 -0
- package/dist/agent/src/test/imageLoad.test.js +8 -7
- package/dist/agent/src/test/mcpServerManager.test.js +23 -20
- package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
- package/dist/agent/src/test/openaiStreaming.test.js +64 -35
- package/dist/agent/src/test/prompt.test.js +5 -4
- package/dist/agent/src/test/promptProvider.test.js +28 -0
- package/dist/agent/src/test/responseHandler.test.js +61 -0
- package/dist/agent/src/test/sudoMcpServerManager.test.js +24 -25
- package/dist/agent/src/test/testTools.js +109 -0
- package/dist/agent/src/test/tools.test.js +31 -0
- package/dist/agent/src/tool/agentChat.js +21 -10
- package/dist/agent/src/tool/agentMain.js +1 -1
- package/dist/agent/src/tool/chatMain.js +241 -58
- package/dist/agent/src/tool/commandPrompt.js +22 -17
- package/dist/agent/src/tool/files.js +20 -16
- package/dist/agent/src/tool/nodePlatform.js +47 -3
- package/dist/agent/src/tool/options.js +4 -4
- package/dist/agent/src/tool/prompt.js +19 -13
- package/eslint.config.mjs +14 -1
- package/package.json +14 -6
- package/scripts/chat_server +8 -0
- package/scripts/setup_chat +7 -2
- package/scripts/shutdown_chat_server +3 -0
- package/scripts/test_chat +135 -17
- package/src/agent/agent.ts +283 -138
- package/src/agent/agentUtils.ts +143 -108
- package/src/agent/compressingContextManager.ts +164 -0
- package/src/agent/context.ts +268 -0
- package/src/agent/dummyLLM.ts +76 -8
- package/src/agent/iAgentEventHandler.ts +54 -0
- package/src/agent/iplatform.ts +1 -0
- package/src/agent/mcpServerManager.ts +35 -31
- package/src/agent/nullAgentEventHandler.ts +20 -0
- package/src/agent/nullPlatform.ts +13 -0
- package/src/agent/openAILLMStreaming.ts +26 -13
- package/src/agent/promptProvider.ts +87 -0
- package/src/agent/repeatLLM.ts +5 -5
- package/src/agent/sudoMcpServerManager.ts +30 -29
- package/src/agent/tokenAuth.ts +7 -7
- package/src/agent/tools.ts +3 -1
- package/src/chat/client/chatClient.ts +900 -0
- package/src/chat/client/connection.test.ts +241 -0
- package/src/chat/client/connection.ts +276 -0
- package/src/chat/client/constants.ts +3 -0
- package/src/chat/client/index.ts +18 -0
- package/src/chat/client/interfaces.ts +34 -0
- package/src/chat/client/responseHandler.ts +131 -0
- package/src/chat/client/sessionClient.ts +443 -0
- package/src/chat/client/teamManager.ts +29 -0
- package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
- package/src/chat/data/dataModels.ts +85 -0
- package/src/chat/data/database.ts +982 -0
- package/src/chat/data/dbMcpServerConfigs.ts +59 -0
- package/src/chat/protocol/connectionMessages.ts +49 -0
- package/src/chat/protocol/constants.ts +55 -0
- package/src/chat/protocol/errors.ts +16 -0
- package/src/chat/protocol/messages.ts +682 -0
- package/src/chat/server/README.md +127 -0
- package/src/chat/server/chatContextManager.ts +612 -0
- package/src/chat/server/connectionManager.test.ts +266 -0
- package/src/chat/server/connectionManager.ts +541 -0
- package/src/chat/server/conversation.ts +269 -0
- package/src/chat/server/errorUtils.ts +28 -0
- package/src/chat/server/openSession.ts +1332 -0
- package/src/chat/server/server.ts +177 -0
- package/src/chat/server/sessionFileManager.ts +239 -0
- package/src/chat/server/sessionRegistry.test.ts +138 -0
- package/src/chat/server/sessionRegistry.ts +1064 -0
- package/src/chat/server/test-utils/mockFactories.ts +422 -0
- package/src/chat/server/tools.ts +265 -0
- package/src/chat/utils/agentSessionMap.ts +76 -0
- package/src/chat/utils/approvalManager.ts +111 -0
- package/src/{utils → chat/utils}/asyncLock.ts +3 -3
- package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
- package/src/chat/utils/htmlToText.ts +61 -0
- package/src/chat/utils/multiAsyncQueue.ts +52 -0
- package/src/chat/utils/search.ts +139 -0
- package/src/chat/utils/userResolver.ts +48 -0
- package/src/chat/utils/websocket.ts +16 -0
- package/src/test/agent.test.ts +487 -0
- package/src/test/approvalManager.test.ts +73 -0
- package/src/test/chatContextManager.test.ts +521 -0
- package/src/test/clientServerConnection.test.ts +207 -0
- package/src/test/compressingContextManager.test.ts +82 -0
- package/src/test/context.test.ts +105 -0
- package/src/test/conversation.test.ts +109 -0
- package/src/test/db.test.ts +358 -89
- package/src/test/dbMcpServerConfigs.test.ts +112 -0
- package/src/test/dbTestTools.ts +153 -0
- package/src/test/imageLoad.test.ts +7 -6
- package/src/test/mcpServerManager.test.ts +21 -16
- package/src/test/multiAsyncQueue.test.ts +125 -0
- package/src/test/openaiStreaming.test.ts +71 -36
- package/src/test/prompt.test.ts +4 -3
- package/src/test/promptProvider.test.ts +33 -0
- package/src/test/responseHandler.test.ts +78 -0
- package/src/test/sudoMcpServerManager.test.ts +32 -30
- package/src/test/testTools.ts +146 -0
- package/src/test/tools.test.ts +39 -0
- package/src/tool/agentChat.ts +26 -12
- package/src/tool/agentMain.ts +1 -1
- package/src/tool/chatMain.ts +292 -100
- package/src/tool/commandPrompt.ts +28 -19
- package/src/tool/files.ts +25 -19
- package/src/tool/nodePlatform.ts +52 -3
- package/src/tool/options.ts +4 -2
- package/src/tool/prompt.ts +22 -15
- package/test_data/dummyllm_script_crash.json +32 -0
- package/test_data/frog.png.b64 +1 -0
- package/vitest.config.ts +39 -0
- package/dist/agent/src/chat/client.js +0 -349
- package/dist/agent/src/chat/conversationManager.js +0 -392
- package/dist/agent/src/chat/db.js +0 -209
- package/dist/agent/src/chat/frontendClient.js +0 -74
- package/dist/agent/src/chat/server.js +0 -158
- package/src/chat/client.ts +0 -455
- package/src/chat/conversationManager.ts +0 -595
- package/src/chat/db.ts +0 -290
- package/src/chat/frontendClient.ts +0 -123
- package/src/chat/messages.ts +0 -235
- package/src/chat/server.ts +0 -177
- /package/dist/agent/src/{chat/messages.js → agent/iAgentEventHandler.js} +0 -0
- /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
|
+
});
|