@xalia/agent 0.6.0 → 0.6.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.
- package/dist/agent/src/agent/agent.js +103 -54
- package/dist/agent/src/agent/agentUtils.js +22 -21
- package/dist/agent/src/agent/compressingContextManager.js +3 -2
- package/dist/agent/src/agent/dummyLLM.js +1 -3
- package/dist/agent/src/agent/imageGenLLM.js +67 -0
- package/dist/agent/src/agent/imageGenerator.js +43 -0
- package/dist/agent/src/agent/llm.js +27 -0
- package/dist/agent/src/agent/mcpServerManager.js +18 -6
- package/dist/agent/src/agent/nullAgentEventHandler.js +6 -0
- package/dist/agent/src/agent/openAILLM.js +3 -3
- package/dist/agent/src/agent/openAILLMStreaming.js +41 -6
- package/dist/agent/src/chat/client/chatClient.js +84 -13
- package/dist/agent/src/chat/client/sessionClient.js +47 -6
- package/dist/agent/src/chat/client/sessionFiles.js +102 -0
- package/dist/agent/src/chat/data/apiKeyManager.js +38 -7
- package/dist/agent/src/chat/data/database.js +83 -70
- package/dist/agent/src/chat/data/dbSessionFileModels.js +49 -0
- package/dist/agent/src/chat/data/dbSessionFiles.js +76 -0
- package/dist/agent/src/chat/data/dbSessionMessages.js +57 -0
- package/dist/agent/src/chat/data/mimeTypes.js +44 -0
- package/dist/agent/src/chat/protocol/messages.js +21 -0
- package/dist/agent/src/chat/server/chatContextManager.js +14 -7
- package/dist/agent/src/chat/server/connectionManager.js +14 -36
- package/dist/agent/src/chat/server/connectionManager.test.js +2 -16
- package/dist/agent/src/chat/server/conversation.js +69 -45
- package/dist/agent/src/chat/server/imageGeneratorTools.js +111 -0
- package/dist/agent/src/chat/server/openSession.js +205 -43
- package/dist/agent/src/chat/server/server.js +5 -8
- package/dist/agent/src/chat/server/sessionFileManager.js +171 -38
- package/dist/agent/src/chat/server/sessionRegistry.js +199 -32
- package/dist/agent/src/chat/server/test-utils/mockFactories.js +12 -11
- package/dist/agent/src/chat/server/tools.js +27 -6
- package/dist/agent/src/chat/utils/multiAsyncQueue.js +9 -1
- package/dist/agent/src/test/agent.test.js +15 -11
- package/dist/agent/src/test/chatContextManager.test.js +4 -0
- package/dist/agent/src/test/clientServerConnection.test.js +2 -2
- package/dist/agent/src/test/db.test.js +33 -70
- package/dist/agent/src/test/dbSessionFiles.test.js +179 -0
- package/dist/agent/src/test/dbSessionMessages.test.js +67 -0
- package/dist/agent/src/test/dbTestTools.js +6 -5
- package/dist/agent/src/test/imageLoad.test.js +1 -1
- package/dist/agent/src/test/mcpServerManager.test.js +1 -1
- package/dist/agent/src/test/multiAsyncQueue.test.js +50 -0
- package/dist/agent/src/test/testTools.js +12 -0
- package/dist/agent/src/tool/agentChat.js +25 -6
- package/dist/agent/src/tool/agentMain.js +1 -1
- package/dist/agent/src/tool/chatMain.js +113 -4
- package/dist/agent/src/tool/commandPrompt.js +7 -3
- package/dist/agent/src/tool/files.js +23 -15
- package/dist/agent/src/tool/options.js +2 -2
- package/package.json +1 -1
- package/scripts/test_chat +124 -66
- package/src/agent/agent.ts +145 -38
- package/src/agent/agentUtils.ts +27 -21
- package/src/agent/compressingContextManager.ts +5 -4
- package/src/agent/context.ts +1 -1
- package/src/agent/dummyLLM.ts +1 -3
- package/src/agent/iAgentEventHandler.ts +15 -2
- package/src/agent/imageGenLLM.ts +99 -0
- package/src/agent/imageGenerator.ts +60 -0
- package/src/agent/llm.ts +128 -4
- package/src/agent/mcpServerManager.ts +26 -7
- package/src/agent/nullAgentEventHandler.ts +6 -0
- package/src/agent/openAILLM.ts +3 -8
- package/src/agent/openAILLMStreaming.ts +60 -14
- package/src/chat/client/chatClient.ts +119 -14
- package/src/chat/client/sessionClient.ts +75 -9
- package/src/chat/client/sessionFiles.ts +145 -0
- package/src/chat/data/apiKeyManager.ts +55 -7
- package/src/chat/data/dataModels.ts +16 -7
- package/src/chat/data/database.ts +107 -92
- package/src/chat/data/dbSessionFileModels.ts +91 -0
- package/src/chat/data/dbSessionFiles.ts +99 -0
- package/src/chat/data/dbSessionMessages.ts +68 -0
- package/src/chat/data/mimeTypes.ts +58 -0
- package/src/chat/protocol/messages.ts +127 -13
- package/src/chat/server/chatContextManager.ts +36 -13
- package/src/chat/server/connectionManager.test.ts +1 -22
- package/src/chat/server/connectionManager.ts +18 -53
- package/src/chat/server/conversation.ts +96 -57
- package/src/chat/server/imageGeneratorTools.ts +138 -0
- package/src/chat/server/openSession.ts +287 -49
- package/src/chat/server/server.ts +5 -11
- package/src/chat/server/sessionFileManager.ts +223 -63
- package/src/chat/server/sessionRegistry.ts +285 -41
- package/src/chat/server/test-utils/mockFactories.ts +13 -13
- package/src/chat/server/tools.ts +43 -8
- package/src/chat/utils/agentSessionMap.ts +2 -2
- package/src/chat/utils/multiAsyncQueue.ts +11 -1
- package/src/test/agent.test.ts +23 -14
- package/src/test/chatContextManager.test.ts +7 -2
- package/src/test/clientServerConnection.test.ts +3 -3
- package/src/test/compressingContextManager.test.ts +1 -1
- package/src/test/context.test.ts +2 -1
- package/src/test/conversation.test.ts +1 -1
- package/src/test/db.test.ts +41 -83
- package/src/test/dbSessionFiles.test.ts +258 -0
- package/src/test/dbSessionMessages.test.ts +85 -0
- package/src/test/dbTestTools.ts +9 -5
- package/src/test/imageLoad.test.ts +2 -2
- package/src/test/mcpServerManager.test.ts +3 -1
- package/src/test/multiAsyncQueue.test.ts +58 -0
- package/src/test/testTools.ts +15 -1
- package/src/tool/agentChat.ts +35 -7
- package/src/tool/agentMain.ts +7 -7
- package/src/tool/chatMain.ts +126 -5
- package/src/tool/commandPrompt.ts +10 -5
- package/src/tool/files.ts +30 -13
- package/src/tool/options.ts +1 -1
- package/test_data/dummyllm_script_image_gen.json +19 -0
- package/test_data/dummyllm_script_invoke_image_gen_tool.json +30 -0
- package/test_data/image_gen_test_profile.json +5 -0
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
SavedAgentProfile,
|
|
7
7
|
DEFAULT_AGENT_PROFILE_NAME,
|
|
8
8
|
DEFAULT_AGENT_PROFILE,
|
|
9
|
-
DEFAULT_AGENT_PROFILE_SYSTEM_PROMPT,
|
|
10
9
|
} from "@xalia/xmcp/sdk";
|
|
11
10
|
|
|
12
11
|
import {
|
|
@@ -27,24 +26,28 @@ import {
|
|
|
27
26
|
ClientControlRemoveTeamUser,
|
|
28
27
|
ServerToClient,
|
|
29
28
|
ClientControlAgentProfileCreate,
|
|
29
|
+
ClientControlAgentProfileDelete,
|
|
30
30
|
} from "../protocol/messages";
|
|
31
|
-
import { OpenSession } from "./openSession";
|
|
31
|
+
import { GUEST_TOKEN_PREFIX, OpenSession } from "./openSession";
|
|
32
32
|
import {
|
|
33
33
|
SessionCreateData,
|
|
34
34
|
SessionData,
|
|
35
|
+
SessionDescriptor,
|
|
35
36
|
TeamInfo,
|
|
36
37
|
TeamParticipant,
|
|
37
38
|
} from "../data/dataModels";
|
|
38
|
-
import { Database } from "../data/database";
|
|
39
|
+
import { Database, UserData } from "../data/database";
|
|
39
40
|
import { resolveUserIdentifier } from "../utils/userResolver";
|
|
40
41
|
import { ChatFatalError } from "../protocol/errors";
|
|
41
|
-
import { IUserConnectionManager } from "./connectionManager";
|
|
42
|
-
import { DEFAULT_LLM_MODEL } from "../../agent/agentUtils";
|
|
42
|
+
import { IMessageProcessor, IUserConnectionManager } from "./connectionManager";
|
|
43
43
|
import { getErrorString } from "./errorUtils";
|
|
44
|
+
import { ApiKeyManager } from "../data/apiKeyManager";
|
|
44
45
|
|
|
45
46
|
const logger = getLogger();
|
|
46
47
|
|
|
47
|
-
export
|
|
48
|
+
export type GuestUser = UserData & { guest_for_session: string };
|
|
49
|
+
|
|
50
|
+
export class SessionRegistry implements IMessageProcessor<ClientToServer> {
|
|
48
51
|
// In memory session-user/user-session mappings
|
|
49
52
|
// Note: this mappings ONLY trackes online users and will
|
|
50
53
|
// be cleaned up when the user disconnects.
|
|
@@ -57,12 +60,18 @@ export class SessionRegistry {
|
|
|
57
60
|
// sessionId -> OpenSession
|
|
58
61
|
private openSessions: Map<string, OpenSession> = new Map();
|
|
59
62
|
|
|
63
|
+
private guests: Map<string, GuestUser> = new Map();
|
|
64
|
+
|
|
65
|
+
private apiKeyManager: ApiKeyManager;
|
|
66
|
+
|
|
60
67
|
constructor(
|
|
61
68
|
private db: Database,
|
|
62
69
|
private connectionManager: IUserConnectionManager<ServerToClient>,
|
|
63
70
|
private llmUrl: string,
|
|
64
71
|
private xmcpUrl: string
|
|
65
|
-
) {
|
|
72
|
+
) {
|
|
73
|
+
this.apiKeyManager = new ApiKeyManager(db);
|
|
74
|
+
}
|
|
66
75
|
|
|
67
76
|
/**
|
|
68
77
|
* Add user to session membership.
|
|
@@ -168,6 +177,60 @@ export class SessionRegistry {
|
|
|
168
177
|
return this.userSessions.get(userId) || new Set();
|
|
169
178
|
}
|
|
170
179
|
|
|
180
|
+
// IMessageProcessor<ClientToServer>
|
|
181
|
+
async authenticate(token: string): Promise<string | undefined> {
|
|
182
|
+
// Parse the api key to determine the type of connection
|
|
183
|
+
const {
|
|
184
|
+
prefix,
|
|
185
|
+
apiKey,
|
|
186
|
+
payload: sessionUUID,
|
|
187
|
+
} = ApiKeyManager.parseToken(token);
|
|
188
|
+
|
|
189
|
+
if (prefix === GUEST_TOKEN_PREFIX) {
|
|
190
|
+
if (!sessionUUID) {
|
|
191
|
+
throw new Error(`invalid token ${token}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const descriptor = await this.getSessionDescriptor(sessionUUID);
|
|
195
|
+
if (!descriptor) {
|
|
196
|
+
throw new Error(`no such session ${sessionUUID}`);
|
|
197
|
+
}
|
|
198
|
+
if (descriptor.access_token !== token) {
|
|
199
|
+
throw new Error(`invalid guest key ${apiKey}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// For now, add a new user to the DB (so simplify the process of adding
|
|
203
|
+
// users).
|
|
204
|
+
|
|
205
|
+
const user_uuid = uuidv4();
|
|
206
|
+
const email = `${user_uuid}@`;
|
|
207
|
+
const nickname = "Guest";
|
|
208
|
+
const timezone = "UTC";
|
|
209
|
+
|
|
210
|
+
await this.db.createUser(user_uuid, email, nickname, timezone);
|
|
211
|
+
|
|
212
|
+
const user: GuestUser = {
|
|
213
|
+
uuid: user_uuid,
|
|
214
|
+
nickname: "Guest",
|
|
215
|
+
email: "guest",
|
|
216
|
+
timezone: "UTC",
|
|
217
|
+
guest_for_session: sessionUUID,
|
|
218
|
+
};
|
|
219
|
+
this.guests.set(user_uuid, user);
|
|
220
|
+
return user.uuid;
|
|
221
|
+
|
|
222
|
+
// throw new Error("unimpl");
|
|
223
|
+
// // return "";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const userData = await this.apiKeyManager.verifyApiKey(apiKey);
|
|
227
|
+
if (!userData) {
|
|
228
|
+
throw new ChatFatalError("invalid api key");
|
|
229
|
+
}
|
|
230
|
+
return userData.uuid;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// IMessageProcessor<ClientToServer>
|
|
171
234
|
async processMessage(
|
|
172
235
|
connectionId: string,
|
|
173
236
|
userId: string,
|
|
@@ -204,11 +267,11 @@ export class SessionRegistry {
|
|
|
204
267
|
): Promise<void> {
|
|
205
268
|
switch (message.type) {
|
|
206
269
|
case "control_agent_profile_create":
|
|
207
|
-
await this.handleAgentProfileCreate(
|
|
270
|
+
await this.handleAgentProfileCreate(userId, message);
|
|
208
271
|
break;
|
|
209
272
|
case "control_agent_profile_delete":
|
|
210
|
-
|
|
211
|
-
|
|
273
|
+
await this.handleAgentProfileDelete(userId, message);
|
|
274
|
+
break;
|
|
212
275
|
case "control_get_session_list":
|
|
213
276
|
await this.handleGetSessionList(connectionId, userId, message);
|
|
214
277
|
break;
|
|
@@ -251,6 +314,19 @@ export class SessionRegistry {
|
|
|
251
314
|
try {
|
|
252
315
|
const [userSessions, teamSessions, userAgents] =
|
|
253
316
|
await this.getAllAgentsAndSessionsByUser(userId);
|
|
317
|
+
|
|
318
|
+
// Mark any guest session as a user session
|
|
319
|
+
|
|
320
|
+
const guest = this.guests.get(userId);
|
|
321
|
+
if (guest) {
|
|
322
|
+
const guestSession = await this.db.sessionGetDescriptorById(
|
|
323
|
+
guest.guest_for_session
|
|
324
|
+
);
|
|
325
|
+
if (guestSession) {
|
|
326
|
+
userSessions.push(guestSession);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
254
330
|
const response: ServerControlSessionList = {
|
|
255
331
|
type: "control_session_list",
|
|
256
332
|
user_sessions: userSessions,
|
|
@@ -286,11 +362,11 @@ export class SessionRegistry {
|
|
|
286
362
|
await this.validateSessionAccess(sessionId, userId, "owner");
|
|
287
363
|
|
|
288
364
|
// get users/team-uuid before deletion
|
|
289
|
-
const [users,
|
|
365
|
+
const [users, sessionData] = await Promise.all([
|
|
290
366
|
this.getSessionUsers(sessionId),
|
|
291
|
-
this.db.
|
|
292
|
-
this.db.sessionGetById(sessionId),
|
|
367
|
+
this.db.sessionGetDescriptorById(sessionId),
|
|
293
368
|
]);
|
|
369
|
+
const teamUuid = sessionData?.team_uuid;
|
|
294
370
|
|
|
295
371
|
if (!sessionData) {
|
|
296
372
|
throw new ChatFatalError(`No such session: ${sessionId}`);
|
|
@@ -355,9 +431,10 @@ export class SessionRegistry {
|
|
|
355
431
|
const validMemberIds: string[] = [];
|
|
356
432
|
const participants: TeamParticipant[] = [];
|
|
357
433
|
|
|
358
|
-
// filter out the owner
|
|
434
|
+
// No need to filter out the owner here, that is done in
|
|
435
|
+
// `createTeamWithParticipants`
|
|
359
436
|
resolvedMemberIds.forEach((user, index) => {
|
|
360
|
-
if (user
|
|
437
|
+
if (user) {
|
|
361
438
|
validMemberIds.push(user.uuid);
|
|
362
439
|
participants.push({
|
|
363
440
|
user_uuid: user.uuid,
|
|
@@ -365,7 +442,7 @@ export class SessionRegistry {
|
|
|
365
442
|
email: user.email,
|
|
366
443
|
role: "participant",
|
|
367
444
|
});
|
|
368
|
-
} else
|
|
445
|
+
} else {
|
|
369
446
|
failedLookups.push(message.initial_members[index]);
|
|
370
447
|
}
|
|
371
448
|
});
|
|
@@ -377,17 +454,50 @@ export class SessionRegistry {
|
|
|
377
454
|
validMemberIds
|
|
378
455
|
);
|
|
379
456
|
|
|
457
|
+
// Create a default agent for the new team
|
|
458
|
+
const defaultAgentProfile = await this.db.createAgentProfile(
|
|
459
|
+
undefined,
|
|
460
|
+
teamUuid,
|
|
461
|
+
DEFAULT_AGENT_PROFILE_NAME,
|
|
462
|
+
DEFAULT_AGENT_PROFILE
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
if (!defaultAgentProfile) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Failed to create default agent profile for team ${teamUuid}`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Get owner's information and add to participants for the response
|
|
472
|
+
const ownerInfo = await this.db.getUserFromUuid(userId);
|
|
473
|
+
if (!ownerInfo) {
|
|
474
|
+
throw new Error(`Cannot find owner user data for user ${userId}`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const ownerParticipant: TeamParticipant = {
|
|
478
|
+
user_uuid: userId,
|
|
479
|
+
nickname: ownerInfo.nickname || "",
|
|
480
|
+
email: ownerInfo.email,
|
|
481
|
+
role: "owner",
|
|
482
|
+
};
|
|
483
|
+
|
|
380
484
|
const response: ServerControlTeamCreated = {
|
|
381
485
|
type: "control_team_created",
|
|
382
486
|
team_uuid: teamUuid,
|
|
383
487
|
team_owner_uuid: userId,
|
|
384
488
|
team_name: message.team_name,
|
|
385
|
-
members: participants,
|
|
489
|
+
members: [ownerParticipant, ...participants],
|
|
386
490
|
failed_lookups: failedLookups,
|
|
387
491
|
};
|
|
388
492
|
const members = new Set(validMemberIds);
|
|
389
493
|
members.add(userId);
|
|
390
494
|
this.connectionManager.sendToUsers(members, response);
|
|
495
|
+
|
|
496
|
+
// Broadcast the default agent profile to all team members
|
|
497
|
+
this.connectionManager.sendToUsers(members, {
|
|
498
|
+
type: "control_agent_profile_created",
|
|
499
|
+
profile: defaultAgentProfile,
|
|
500
|
+
});
|
|
391
501
|
} catch (error) {
|
|
392
502
|
logger.error(`[SessionRegistry] Error creating team: ${String(error)}`);
|
|
393
503
|
this.sendControlError(
|
|
@@ -500,6 +610,19 @@ export class SessionRegistry {
|
|
|
500
610
|
}
|
|
501
611
|
}
|
|
502
612
|
|
|
613
|
+
// Notify all team members about the updated member list
|
|
614
|
+
const updatedParticipants = await this.db.teamGetMembers(
|
|
615
|
+
message.target_team_id
|
|
616
|
+
);
|
|
617
|
+
this.connectionManager.sendToUsers(
|
|
618
|
+
new Set(updatedParticipants.map((p) => p.user_uuid)),
|
|
619
|
+
{
|
|
620
|
+
type: "control_team_members_updated",
|
|
621
|
+
team_uuid: message.target_team_id,
|
|
622
|
+
members: updatedParticipants,
|
|
623
|
+
}
|
|
624
|
+
);
|
|
625
|
+
|
|
503
626
|
logger.info(
|
|
504
627
|
`[SessionRegistry] User ${user.uuid}` +
|
|
505
628
|
`added to team ${message.target_team_id}`
|
|
@@ -592,6 +715,19 @@ export class SessionRegistry {
|
|
|
592
715
|
}
|
|
593
716
|
}
|
|
594
717
|
|
|
718
|
+
// Notify all remaining team members about the updated member list
|
|
719
|
+
const updatedParticipants = await this.db.teamGetMembers(
|
|
720
|
+
message.target_team_id
|
|
721
|
+
);
|
|
722
|
+
this.connectionManager.sendToUsers(
|
|
723
|
+
new Set(updatedParticipants.map((p) => p.user_uuid)),
|
|
724
|
+
{
|
|
725
|
+
type: "control_team_members_updated",
|
|
726
|
+
team_uuid: message.target_team_id,
|
|
727
|
+
members: updatedParticipants,
|
|
728
|
+
}
|
|
729
|
+
);
|
|
730
|
+
|
|
595
731
|
logger.info(
|
|
596
732
|
`[SessionRegistry] User ${user.uuid} ` +
|
|
597
733
|
`removed from team ${message.target_team_id}`
|
|
@@ -645,7 +781,7 @@ export class SessionRegistry {
|
|
|
645
781
|
const access = await this.validateSessionAccess(sessionId, userId);
|
|
646
782
|
if (!access) {
|
|
647
783
|
throw new ChatFatalError(
|
|
648
|
-
`User ${userId} is not authorized to
|
|
784
|
+
`User ${userId} is not authorized to join session ${sessionId}`
|
|
649
785
|
);
|
|
650
786
|
}
|
|
651
787
|
|
|
@@ -658,7 +794,15 @@ export class SessionRegistry {
|
|
|
658
794
|
`Server internal error: ` + `failed to load session ${sessionId}`
|
|
659
795
|
);
|
|
660
796
|
}
|
|
661
|
-
|
|
797
|
+
const guest = this.guests.get(userId);
|
|
798
|
+
if (guest) {
|
|
799
|
+
session.addParticipant(userId, {
|
|
800
|
+
user_uuid: guest.uuid,
|
|
801
|
+
nickname: guest.nickname || "Guest",
|
|
802
|
+
email: guest.email,
|
|
803
|
+
role: "participant",
|
|
804
|
+
});
|
|
805
|
+
}
|
|
662
806
|
// To this point, there should be no error thrown.
|
|
663
807
|
// Update in-memory session-user/user-session mappings
|
|
664
808
|
this.addUserToSessionMemory(userId, sessionId);
|
|
@@ -667,6 +811,7 @@ export class SessionRegistry {
|
|
|
667
811
|
if (!this.openSessions.has(sessionId)) {
|
|
668
812
|
this.openSessions.set(sessionId, session);
|
|
669
813
|
}
|
|
814
|
+
|
|
670
815
|
// pass the message to the session to handle the rest
|
|
671
816
|
session.sendSessionData(connectionId, message.client_message_id);
|
|
672
817
|
} catch (error) {
|
|
@@ -682,8 +827,8 @@ export class SessionRegistry {
|
|
|
682
827
|
}
|
|
683
828
|
|
|
684
829
|
async handleAgentProfileCreate(
|
|
685
|
-
|
|
686
|
-
|
|
830
|
+
userId: string,
|
|
831
|
+
message: ClientControlAgentProfileCreate
|
|
687
832
|
): Promise<string> {
|
|
688
833
|
// get agent profile from template
|
|
689
834
|
let agentProfileFromTemplate: AgentProfile | undefined = undefined;
|
|
@@ -697,11 +842,13 @@ export class SessionRegistry {
|
|
|
697
842
|
agentProfileFromTemplate = template.agent_profile;
|
|
698
843
|
}
|
|
699
844
|
|
|
700
|
-
const newAgentProfile: AgentProfile =
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
845
|
+
const newAgentProfile: AgentProfile =
|
|
846
|
+
agentProfileFromTemplate ||
|
|
847
|
+
new AgentProfile(
|
|
848
|
+
DEFAULT_AGENT_PROFILE.model,
|
|
849
|
+
DEFAULT_AGENT_PROFILE.system_prompt,
|
|
850
|
+
DEFAULT_AGENT_PROFILE.mcp_settings
|
|
851
|
+
);
|
|
705
852
|
const team_uuid = message.team_uuid || undefined;
|
|
706
853
|
|
|
707
854
|
if (team_uuid) {
|
|
@@ -732,7 +879,7 @@ export class SessionRegistry {
|
|
|
732
879
|
// User AgentProfile
|
|
733
880
|
|
|
734
881
|
const savedAgentProfile = await this.db.createAgentProfile(
|
|
735
|
-
|
|
882
|
+
userId,
|
|
736
883
|
undefined,
|
|
737
884
|
message.title,
|
|
738
885
|
newAgentProfile
|
|
@@ -742,7 +889,7 @@ export class SessionRegistry {
|
|
|
742
889
|
}
|
|
743
890
|
|
|
744
891
|
// Send the new AgentProfile to the user
|
|
745
|
-
this.connectionManager.sendToUsers(new Set([
|
|
892
|
+
this.connectionManager.sendToUsers(new Set([userId]), {
|
|
746
893
|
type: "control_agent_profile_created",
|
|
747
894
|
profile: savedAgentProfile,
|
|
748
895
|
});
|
|
@@ -750,6 +897,54 @@ export class SessionRegistry {
|
|
|
750
897
|
return savedAgentProfile.uuid;
|
|
751
898
|
}
|
|
752
899
|
|
|
900
|
+
async handleAgentProfileDelete(
|
|
901
|
+
userId: string,
|
|
902
|
+
message: ClientControlAgentProfileDelete
|
|
903
|
+
): Promise<void> {
|
|
904
|
+
const agentProfileUuid = message.agent_profile_uuid;
|
|
905
|
+
|
|
906
|
+
// Get the agent profile to determine if it's a team or user profile
|
|
907
|
+
const agentProfile =
|
|
908
|
+
await this.db.getSavedAgentProfileById(agentProfileUuid);
|
|
909
|
+
if (!agentProfile) {
|
|
910
|
+
throw new Error(`Agent profile ${agentProfileUuid} not found`);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Validate team access: user must own the profile or be in the team
|
|
914
|
+
// TODO: Only allow the owner to delete the profile?
|
|
915
|
+
if (agentProfile.team_uuid) {
|
|
916
|
+
const hasAccess = await this.validateTeamAccess(
|
|
917
|
+
agentProfile.team_uuid,
|
|
918
|
+
userId
|
|
919
|
+
);
|
|
920
|
+
if (!hasAccess) {
|
|
921
|
+
throw new Error(
|
|
922
|
+
`User ${userId} is not authorized to delete this agent profile`
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Delete the agent profile from database
|
|
928
|
+
await this.db.deleteAgentProfile(agentProfileUuid);
|
|
929
|
+
|
|
930
|
+
// Send confirmation to all relevant users
|
|
931
|
+
if (agentProfile.team_uuid) {
|
|
932
|
+
const participants = await this.db.teamGetMembers(agentProfile.team_uuid);
|
|
933
|
+
this.connectionManager.sendToUsers(
|
|
934
|
+
new Set(participants.map((p) => p.user_uuid)),
|
|
935
|
+
{
|
|
936
|
+
type: "control_agent_profile_deleted",
|
|
937
|
+
profile_uuid: agentProfileUuid,
|
|
938
|
+
}
|
|
939
|
+
);
|
|
940
|
+
} else {
|
|
941
|
+
this.connectionManager.sendToUsers(new Set([userId]), {
|
|
942
|
+
type: "control_agent_profile_deleted",
|
|
943
|
+
profile_uuid: agentProfileUuid,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
753
948
|
/**
|
|
754
949
|
* Create a new session for a user.
|
|
755
950
|
* - create session in database (via `newSession`)
|
|
@@ -769,13 +964,13 @@ export class SessionRegistry {
|
|
|
769
964
|
|
|
770
965
|
// Create AgentProfile and inform the client
|
|
771
966
|
message.agent_profile_id = await this.handleAgentProfileCreate(
|
|
967
|
+
fromUserId,
|
|
772
968
|
{
|
|
773
969
|
type: "control_agent_profile_create",
|
|
774
970
|
title: "New Agent " + uuidv4(),
|
|
775
971
|
user_uuid: fromUserId,
|
|
776
972
|
team_uuid: message.team_id,
|
|
777
|
-
}
|
|
778
|
-
fromUserId
|
|
973
|
+
}
|
|
779
974
|
);
|
|
780
975
|
}
|
|
781
976
|
|
|
@@ -825,8 +1020,8 @@ export class SessionRegistry {
|
|
|
825
1020
|
// validate the agent profile
|
|
826
1021
|
await this.validateSavedAgentProfile(agentProfileId);
|
|
827
1022
|
|
|
828
|
-
const newSessionData:
|
|
829
|
-
...
|
|
1023
|
+
const newSessionData: SessionDescriptor = {
|
|
1024
|
+
...userSessionCreateData(ownerId, title, agentProfileId),
|
|
830
1025
|
updated_at: new Date().toISOString(),
|
|
831
1026
|
};
|
|
832
1027
|
const openSession = await OpenSession.initWithEmptySession(
|
|
@@ -860,8 +1055,8 @@ export class SessionRegistry {
|
|
|
860
1055
|
);
|
|
861
1056
|
}
|
|
862
1057
|
|
|
863
|
-
const newSessionData:
|
|
864
|
-
...
|
|
1058
|
+
const newSessionData: SessionDescriptor = {
|
|
1059
|
+
...teamSessionCreateData(teamId, fromUserId, title, agentProfileId),
|
|
865
1060
|
updated_at: new Date().toISOString(),
|
|
866
1061
|
};
|
|
867
1062
|
|
|
@@ -917,9 +1112,27 @@ export class SessionRegistry {
|
|
|
917
1112
|
* Handle user disconnect - clean up from all sessions.
|
|
918
1113
|
* Called when a connection is closed to ensure proper cleanup.
|
|
919
1114
|
*/
|
|
920
|
-
handleUserDisconnect(userId: string): void {
|
|
1115
|
+
async handleUserDisconnect(userId: string): Promise<void> {
|
|
921
1116
|
logger.info(`[SessionRegistry] Handling disconnect for user ${userId}`);
|
|
922
1117
|
|
|
1118
|
+
// If the user is a guest, remove his DB entry.
|
|
1119
|
+
const guest = this.guests.get(userId);
|
|
1120
|
+
if (guest) {
|
|
1121
|
+
const session = this.openSessions.get(guest.guest_for_session);
|
|
1122
|
+
if (session) {
|
|
1123
|
+
session.removeParticipant(userId);
|
|
1124
|
+
}
|
|
1125
|
+
this.guests.delete(userId);
|
|
1126
|
+
try {
|
|
1127
|
+
logger.info(
|
|
1128
|
+
`[SessionRegistry.handleUserDisconnect] deleting user ${userId}`
|
|
1129
|
+
);
|
|
1130
|
+
await this.db.deleteUser(userId);
|
|
1131
|
+
} catch (e) {
|
|
1132
|
+
logger.warn(`error removing guest ${userId}: ${getErrorString(e)}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
923
1136
|
// Get all sessions the user is in (copy to avoid concurrent modification)
|
|
924
1137
|
const userSessionIds = this.getInMemoryUserSessions(userId);
|
|
925
1138
|
const sessionsToLeave = Array.from(userSessionIds);
|
|
@@ -943,11 +1156,13 @@ export class SessionRegistry {
|
|
|
943
1156
|
private async getAllAgentsAndSessionsByUser(
|
|
944
1157
|
userId: string
|
|
945
1158
|
): Promise<[SessionData[], TeamInfo[], SavedAgentProfile[]]> {
|
|
946
|
-
|
|
1159
|
+
const [userSessions, teamInfos, userAgents] = await Promise.all([
|
|
947
1160
|
this.db.getUserSessions(userId),
|
|
948
1161
|
this.db.getTeamInfosByUser(userId),
|
|
949
1162
|
this.agentProfilesGetByUserOrDefault(userId),
|
|
950
1163
|
]);
|
|
1164
|
+
|
|
1165
|
+
return [userSessions, teamInfos, userAgents];
|
|
951
1166
|
}
|
|
952
1167
|
|
|
953
1168
|
private async agentProfilesGetByUserOrDefault(
|
|
@@ -987,7 +1202,7 @@ export class SessionRegistry {
|
|
|
987
1202
|
/**
|
|
988
1203
|
* Validates that a user has permission to access a session.
|
|
989
1204
|
*/
|
|
990
|
-
|
|
1205
|
+
async validateSessionAccess(
|
|
991
1206
|
sessionId: string,
|
|
992
1207
|
userId: string,
|
|
993
1208
|
accessType?: "participant" | "owner"
|
|
@@ -998,6 +1213,13 @@ export class SessionRegistry {
|
|
|
998
1213
|
const participants = session.getParticipants();
|
|
999
1214
|
const role = participants.get(userId);
|
|
1000
1215
|
if (!role) {
|
|
1216
|
+
// Check for guest access
|
|
1217
|
+
const guest = this.guests.get(userId);
|
|
1218
|
+
if (guest && guest.guest_for_session === sessionId) {
|
|
1219
|
+
return !accessType || accessType === "participant";
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Not authorized to join the session
|
|
1001
1223
|
return false;
|
|
1002
1224
|
} else {
|
|
1003
1225
|
return !accessType || role.role === accessType;
|
|
@@ -1007,6 +1229,13 @@ export class SessionRegistry {
|
|
|
1007
1229
|
const participants = await this.db.sessionGetParticipants(sessionId);
|
|
1008
1230
|
const role = participants.find((p) => p.user_uuid === userId)?.role;
|
|
1009
1231
|
if (!role) {
|
|
1232
|
+
// Check for guest access
|
|
1233
|
+
const guest = this.guests.get(userId);
|
|
1234
|
+
logger.info(`[validateSessionAccess] guest: ${JSON.stringify(guest)}`);
|
|
1235
|
+
if (guest && guest.guest_for_session === sessionId) {
|
|
1236
|
+
return !accessType || accessType === "participant";
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1010
1239
|
return false;
|
|
1011
1240
|
} else {
|
|
1012
1241
|
return !accessType || role === accessType;
|
|
@@ -1030,9 +1259,24 @@ export class SessionRegistry {
|
|
|
1030
1259
|
return !accessType || role === accessType;
|
|
1031
1260
|
}
|
|
1032
1261
|
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Read from DB if the session is not active
|
|
1265
|
+
*/
|
|
1266
|
+
private async getSessionDescriptor(
|
|
1267
|
+
sessionId: string
|
|
1268
|
+
): Promise<SessionDescriptor | undefined> {
|
|
1269
|
+
const session = this.openSessions.get(sessionId);
|
|
1270
|
+
if (session) {
|
|
1271
|
+
return session.getDescriptor();
|
|
1272
|
+
} else {
|
|
1273
|
+
// fetch the session from database
|
|
1274
|
+
return this.db.sessionGetDescriptorById(sessionId);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1033
1277
|
}
|
|
1034
1278
|
|
|
1035
|
-
export function
|
|
1279
|
+
export function userSessionCreateData(
|
|
1036
1280
|
ownerId: string,
|
|
1037
1281
|
title: string,
|
|
1038
1282
|
agentProfileId: string
|
|
@@ -1042,12 +1286,12 @@ export function userSessionDataCreate(
|
|
|
1042
1286
|
title,
|
|
1043
1287
|
team_uuid: undefined,
|
|
1044
1288
|
agent_profile_uuid: agentProfileId,
|
|
1045
|
-
workspace: undefined,
|
|
1046
1289
|
user_uuid: ownerId,
|
|
1290
|
+
agent_paused: false,
|
|
1047
1291
|
};
|
|
1048
1292
|
}
|
|
1049
1293
|
|
|
1050
|
-
export function
|
|
1294
|
+
export function teamSessionCreateData(
|
|
1051
1295
|
teamId: string,
|
|
1052
1296
|
ownerId: string,
|
|
1053
1297
|
title: string,
|
|
@@ -1058,7 +1302,7 @@ export function teamSessionDataCreate(
|
|
|
1058
1302
|
title,
|
|
1059
1303
|
team_uuid: teamId,
|
|
1060
1304
|
agent_profile_uuid: agentProfileId,
|
|
1061
|
-
workspace: undefined,
|
|
1062
1305
|
user_uuid: ownerId,
|
|
1306
|
+
agent_paused: false,
|
|
1063
1307
|
};
|
|
1064
1308
|
}
|
|
@@ -128,21 +128,18 @@ export function createMockUserConnectionManager(): {
|
|
|
128
128
|
mock: IUserConnectionManager<ServerToClient>;
|
|
129
129
|
spies: {
|
|
130
130
|
sendToUsers: ReturnType<typeof vi.fn>;
|
|
131
|
-
getLiveUserApiKey: ReturnType<typeof vi.fn>;
|
|
132
131
|
};
|
|
133
132
|
} {
|
|
134
133
|
const spies = {
|
|
135
134
|
sendToUsers: vi.fn(),
|
|
136
135
|
sendToConnection: vi.fn(),
|
|
137
136
|
sendServerError: vi.fn(),
|
|
138
|
-
getLiveUserApiKey: vi.fn(),
|
|
139
137
|
};
|
|
140
138
|
|
|
141
139
|
const mock = {
|
|
142
140
|
sendToUsers: spies.sendToUsers,
|
|
143
141
|
sendToConnection: spies.sendToConnection,
|
|
144
142
|
sendServerError: spies.sendServerError,
|
|
145
|
-
getLiveUserApiKey: spies.getLiveUserApiKey,
|
|
146
143
|
} as IUserConnectionManager<ServerToClient>;
|
|
147
144
|
|
|
148
145
|
return { mock, spies };
|
|
@@ -154,16 +151,25 @@ export function createMockUserConnectionManager(): {
|
|
|
154
151
|
export function createMockSessionRegistry(): {
|
|
155
152
|
mock: IMessageProcessor<ClientToServer>;
|
|
156
153
|
spies: {
|
|
154
|
+
authenticate: ReturnType<typeof vi.fn>;
|
|
157
155
|
processMessage: ReturnType<typeof vi.fn>;
|
|
158
156
|
handleUserDisconnect: ReturnType<typeof vi.fn>;
|
|
159
157
|
};
|
|
160
158
|
} {
|
|
161
159
|
const spies = {
|
|
160
|
+
authenticate: vi.fn().mockImplementation((apiKey) => {
|
|
161
|
+
if (apiKey === "valid-api-key")
|
|
162
|
+
return Promise.resolve(MOCK_USERS.owner.uuid);
|
|
163
|
+
if (apiKey === "participant-api-key")
|
|
164
|
+
return Promise.resolve(MOCK_USERS.participant.uuid);
|
|
165
|
+
return Promise.resolve(null);
|
|
166
|
+
}),
|
|
162
167
|
processMessage: vi.fn(),
|
|
163
168
|
handleUserDisconnect: vi.fn(),
|
|
164
169
|
};
|
|
165
170
|
|
|
166
171
|
const mock = {
|
|
172
|
+
authenticate: spies.authenticate,
|
|
167
173
|
processMessage: spies.processMessage,
|
|
168
174
|
handleUserDisconnect: spies.handleUserDisconnect,
|
|
169
175
|
} as IMessageProcessor<ClientToServer>;
|
|
@@ -267,6 +273,7 @@ export function createMockSessionList(): Array<SessionData> {
|
|
|
267
273
|
workspace: undefined,
|
|
268
274
|
updated_at: MOCK_SESSIONS.active.updated_at || "",
|
|
269
275
|
user_uuid: MOCK_SESSIONS.active.user_uuid,
|
|
276
|
+
agent_paused: false,
|
|
270
277
|
},
|
|
271
278
|
{
|
|
272
279
|
session_uuid: MOCK_SESSIONS.secondary.uuid,
|
|
@@ -276,6 +283,7 @@ export function createMockSessionList(): Array<SessionData> {
|
|
|
276
283
|
workspace: undefined,
|
|
277
284
|
updated_at: MOCK_SESSIONS.secondary.updated_at || "",
|
|
278
285
|
user_uuid: MOCK_SESSIONS.secondary.user_uuid,
|
|
286
|
+
agent_paused: false,
|
|
279
287
|
},
|
|
280
288
|
];
|
|
281
289
|
}
|
|
@@ -409,14 +417,6 @@ export function setupStandardMockBehaviors(mocks: {
|
|
|
409
417
|
}
|
|
410
418
|
|
|
411
419
|
// Setup user connection manager mocks
|
|
412
|
-
if (mocks.userConnectionManager) {
|
|
413
|
-
|
|
414
|
-
(userId: string) => {
|
|
415
|
-
if (userId === MOCK_USERS.owner.uuid) return "valid-api-key";
|
|
416
|
-
if (userId === MOCK_USERS.participant.uuid)
|
|
417
|
-
return "participant-api-key";
|
|
418
|
-
return undefined;
|
|
419
|
-
}
|
|
420
|
-
);
|
|
421
|
-
}
|
|
420
|
+
// if (mocks.userConnectionManager) {
|
|
421
|
+
// }
|
|
422
422
|
}
|