@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.
Files changed (112) hide show
  1. package/dist/agent/src/agent/agent.js +103 -54
  2. package/dist/agent/src/agent/agentUtils.js +22 -21
  3. package/dist/agent/src/agent/compressingContextManager.js +3 -2
  4. package/dist/agent/src/agent/dummyLLM.js +1 -3
  5. package/dist/agent/src/agent/imageGenLLM.js +67 -0
  6. package/dist/agent/src/agent/imageGenerator.js +43 -0
  7. package/dist/agent/src/agent/llm.js +27 -0
  8. package/dist/agent/src/agent/mcpServerManager.js +18 -6
  9. package/dist/agent/src/agent/nullAgentEventHandler.js +6 -0
  10. package/dist/agent/src/agent/openAILLM.js +3 -3
  11. package/dist/agent/src/agent/openAILLMStreaming.js +41 -6
  12. package/dist/agent/src/chat/client/chatClient.js +84 -13
  13. package/dist/agent/src/chat/client/sessionClient.js +47 -6
  14. package/dist/agent/src/chat/client/sessionFiles.js +102 -0
  15. package/dist/agent/src/chat/data/apiKeyManager.js +38 -7
  16. package/dist/agent/src/chat/data/database.js +83 -70
  17. package/dist/agent/src/chat/data/dbSessionFileModels.js +49 -0
  18. package/dist/agent/src/chat/data/dbSessionFiles.js +76 -0
  19. package/dist/agent/src/chat/data/dbSessionMessages.js +57 -0
  20. package/dist/agent/src/chat/data/mimeTypes.js +44 -0
  21. package/dist/agent/src/chat/protocol/messages.js +21 -0
  22. package/dist/agent/src/chat/server/chatContextManager.js +14 -7
  23. package/dist/agent/src/chat/server/connectionManager.js +14 -36
  24. package/dist/agent/src/chat/server/connectionManager.test.js +2 -16
  25. package/dist/agent/src/chat/server/conversation.js +69 -45
  26. package/dist/agent/src/chat/server/imageGeneratorTools.js +111 -0
  27. package/dist/agent/src/chat/server/openSession.js +205 -43
  28. package/dist/agent/src/chat/server/server.js +5 -8
  29. package/dist/agent/src/chat/server/sessionFileManager.js +171 -38
  30. package/dist/agent/src/chat/server/sessionRegistry.js +199 -32
  31. package/dist/agent/src/chat/server/test-utils/mockFactories.js +12 -11
  32. package/dist/agent/src/chat/server/tools.js +27 -6
  33. package/dist/agent/src/chat/utils/multiAsyncQueue.js +9 -1
  34. package/dist/agent/src/test/agent.test.js +15 -11
  35. package/dist/agent/src/test/chatContextManager.test.js +4 -0
  36. package/dist/agent/src/test/clientServerConnection.test.js +2 -2
  37. package/dist/agent/src/test/db.test.js +33 -70
  38. package/dist/agent/src/test/dbSessionFiles.test.js +179 -0
  39. package/dist/agent/src/test/dbSessionMessages.test.js +67 -0
  40. package/dist/agent/src/test/dbTestTools.js +6 -5
  41. package/dist/agent/src/test/imageLoad.test.js +1 -1
  42. package/dist/agent/src/test/mcpServerManager.test.js +1 -1
  43. package/dist/agent/src/test/multiAsyncQueue.test.js +50 -0
  44. package/dist/agent/src/test/testTools.js +12 -0
  45. package/dist/agent/src/tool/agentChat.js +25 -6
  46. package/dist/agent/src/tool/agentMain.js +1 -1
  47. package/dist/agent/src/tool/chatMain.js +113 -4
  48. package/dist/agent/src/tool/commandPrompt.js +7 -3
  49. package/dist/agent/src/tool/files.js +23 -15
  50. package/dist/agent/src/tool/options.js +2 -2
  51. package/package.json +1 -1
  52. package/scripts/test_chat +124 -66
  53. package/src/agent/agent.ts +145 -38
  54. package/src/agent/agentUtils.ts +27 -21
  55. package/src/agent/compressingContextManager.ts +5 -4
  56. package/src/agent/context.ts +1 -1
  57. package/src/agent/dummyLLM.ts +1 -3
  58. package/src/agent/iAgentEventHandler.ts +15 -2
  59. package/src/agent/imageGenLLM.ts +99 -0
  60. package/src/agent/imageGenerator.ts +60 -0
  61. package/src/agent/llm.ts +128 -4
  62. package/src/agent/mcpServerManager.ts +26 -7
  63. package/src/agent/nullAgentEventHandler.ts +6 -0
  64. package/src/agent/openAILLM.ts +3 -8
  65. package/src/agent/openAILLMStreaming.ts +60 -14
  66. package/src/chat/client/chatClient.ts +119 -14
  67. package/src/chat/client/sessionClient.ts +75 -9
  68. package/src/chat/client/sessionFiles.ts +145 -0
  69. package/src/chat/data/apiKeyManager.ts +55 -7
  70. package/src/chat/data/dataModels.ts +16 -7
  71. package/src/chat/data/database.ts +107 -92
  72. package/src/chat/data/dbSessionFileModels.ts +91 -0
  73. package/src/chat/data/dbSessionFiles.ts +99 -0
  74. package/src/chat/data/dbSessionMessages.ts +68 -0
  75. package/src/chat/data/mimeTypes.ts +58 -0
  76. package/src/chat/protocol/messages.ts +127 -13
  77. package/src/chat/server/chatContextManager.ts +36 -13
  78. package/src/chat/server/connectionManager.test.ts +1 -22
  79. package/src/chat/server/connectionManager.ts +18 -53
  80. package/src/chat/server/conversation.ts +96 -57
  81. package/src/chat/server/imageGeneratorTools.ts +138 -0
  82. package/src/chat/server/openSession.ts +287 -49
  83. package/src/chat/server/server.ts +5 -11
  84. package/src/chat/server/sessionFileManager.ts +223 -63
  85. package/src/chat/server/sessionRegistry.ts +285 -41
  86. package/src/chat/server/test-utils/mockFactories.ts +13 -13
  87. package/src/chat/server/tools.ts +43 -8
  88. package/src/chat/utils/agentSessionMap.ts +2 -2
  89. package/src/chat/utils/multiAsyncQueue.ts +11 -1
  90. package/src/test/agent.test.ts +23 -14
  91. package/src/test/chatContextManager.test.ts +7 -2
  92. package/src/test/clientServerConnection.test.ts +3 -3
  93. package/src/test/compressingContextManager.test.ts +1 -1
  94. package/src/test/context.test.ts +2 -1
  95. package/src/test/conversation.test.ts +1 -1
  96. package/src/test/db.test.ts +41 -83
  97. package/src/test/dbSessionFiles.test.ts +258 -0
  98. package/src/test/dbSessionMessages.test.ts +85 -0
  99. package/src/test/dbTestTools.ts +9 -5
  100. package/src/test/imageLoad.test.ts +2 -2
  101. package/src/test/mcpServerManager.test.ts +3 -1
  102. package/src/test/multiAsyncQueue.test.ts +58 -0
  103. package/src/test/testTools.ts +15 -1
  104. package/src/tool/agentChat.ts +35 -7
  105. package/src/tool/agentMain.ts +7 -7
  106. package/src/tool/chatMain.ts +126 -5
  107. package/src/tool/commandPrompt.ts +10 -5
  108. package/src/tool/files.ts +30 -13
  109. package/src/tool/options.ts +1 -1
  110. package/test_data/dummyllm_script_image_gen.json +19 -0
  111. package/test_data/dummyllm_script_invoke_image_gen_tool.json +30 -0
  112. package/test_data/image_gen_test_profile.json +5 -0
@@ -3,10 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OpenAILLMStreaming = void 0;
4
4
  exports.initializeCompletion = initializeCompletion;
5
5
  exports.updateCompletion = updateCompletion;
6
- const sdk_1 = require("@xalia/xmcp/sdk");
7
6
  const openai_1 = require("openai");
8
7
  const assert_1 = require("assert");
9
- const agentUtils_1 = require("./agentUtils");
8
+ const sdk_1 = require("@xalia/xmcp/sdk");
9
+ const llm_1 = require("./llm");
10
10
  const logger = (0, sdk_1.getLogger)();
11
11
  function initialToolCallFunction(deltaFn) {
12
12
  // export interface ChatCompletionChunk.Choice.Delta.ToolCall.Function {
@@ -152,6 +152,14 @@ function initializeCompletionMessage(delta) {
152
152
  tool_calls: toolCalls,
153
153
  };
154
154
  }
155
+ function updateReasoning(message, reasoning) {
156
+ if (!message.reasoning) {
157
+ message.reasoning = reasoning;
158
+ }
159
+ else {
160
+ message.reasoning += reasoning;
161
+ }
162
+ }
155
163
  function updateCompletionMessage(message, delta) {
156
164
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
157
165
  (0, assert_1.strict)(message.role === "assistant");
@@ -160,12 +168,20 @@ function updateCompletionMessage(message, delta) {
160
168
  (0, assert_1.strict)(!message.audio);
161
169
  (0, assert_1.strict)(message.tool_calls instanceof Array ||
162
170
  typeof message.tool_calls === "undefined");
163
- // export interface ChatCompletionChunk.Choice.Delta {
171
+ // export interface ChatCompletionChunkChoiceDeltaWithReasoning {
164
172
  // content?: string | null;
165
173
  // function_call?: Delta.FunctionCall;
166
174
  // refusal?: string | null;
167
175
  // role?: 'developer' | 'system' | 'user' | 'assistant' | 'tool';
168
176
  // tool_calls?: Array<Delta.ToolCall>;
177
+ //
178
+ // reasoning?: string;
179
+ // reasoning_details?: {
180
+ // type: "reasoning.text",
181
+ // text?: string,
182
+ // format?: string,
183
+ // index?:0
184
+ // }[]
169
185
  // }
170
186
  //
171
187
  // ->
@@ -178,6 +194,8 @@ function updateCompletionMessage(message, delta) {
178
194
  // audio?: ChatCompletionAudio | null;
179
195
  // function_call?: ChatCompletionMessage.FunctionCall | null;
180
196
  // tool_calls?: Array<ChatCompletionMessageToolCall>;
197
+ //
198
+ // reasoning?: string;
181
199
  // }
182
200
  if (delta.content) {
183
201
  if (message.content) {
@@ -203,6 +221,10 @@ function updateCompletionMessage(message, delta) {
203
221
  message.tool_calls = updateToolCalls(message.tool_calls, t);
204
222
  }
205
223
  }
224
+ const reasoning = (0, llm_1.choiceDeltaExtractReasoning)(delta);
225
+ if (reasoning) {
226
+ updateReasoning(message, reasoning);
227
+ }
206
228
  }
207
229
  function initializeCompletionChoice(chunkChoice) {
208
230
  // export interface ChatCompletionChunk.Choice {
@@ -377,9 +399,9 @@ class OpenAILLMStreaming {
377
399
  apiKey,
378
400
  baseURL: apiUrl,
379
401
  dangerouslyAllowBrowser: true,
380
- defaultHeaders: agentUtils_1.XALIA_APP_HEADER,
402
+ defaultHeaders: llm_1.XALIA_APP_HEADER,
381
403
  });
382
- this.model = model || agentUtils_1.DEFAULT_LLM_MODEL;
404
+ this.model = model;
383
405
  }
384
406
  setModel(model) {
385
407
  this.model = model;
@@ -390,7 +412,11 @@ class OpenAILLMStreaming {
390
412
  getUrl() {
391
413
  return this.openai.baseURL;
392
414
  }
393
- async getConversationResponse(messages, tools, onMessage) {
415
+ async getConversationResponse(messages, tools, onMessage, onReasoning) {
416
+ const reasoning = {
417
+ effort: "medium",
418
+ enabled: true,
419
+ };
394
420
  const chunks = await this.openai.chat.completions.create({
395
421
  model: this.model,
396
422
  messages,
@@ -399,6 +425,7 @@ class OpenAILLMStreaming {
399
425
  stream_options: {
400
426
  include_usage: true,
401
427
  },
428
+ extra_body: { reasoning },
402
429
  });
403
430
  // Check the type casting above
404
431
  if (!chunks.iterator) {
@@ -430,6 +457,14 @@ class OpenAILLMStreaming {
430
457
  await onMessage(delta.content, false);
431
458
  }
432
459
  }
460
+ if (onReasoning) {
461
+ const delta = chunk.choices[0]
462
+ ?.delta;
463
+ const reasoning = (0, llm_1.choiceDeltaExtractReasoning)(delta);
464
+ if (reasoning) {
465
+ await onReasoning(reasoning);
466
+ }
467
+ }
433
468
  }
434
469
  if (onMessage) {
435
470
  await onMessage("", true);
@@ -15,7 +15,9 @@ class ChatClient {
15
15
  // agent_uuid -> agent session data
16
16
  userAgentSessionMap = new Map(),
17
17
  // team_uuid -> team info
18
- teams = new Map(), closed = false, currentSessionId = undefined,
18
+ teams = new Map(),
19
+ // Whether this client is in guest mode
20
+ isGuestMode = false, closed = false, currentSessionId = undefined,
19
21
  // note: currentTeamId will not be reset when swtiching to a user session.
20
22
  currentTeamId = undefined,
21
23
  // session_uuid -> session client
@@ -28,6 +30,7 @@ class ChatClient {
28
30
  this.eventHandler = eventHandler;
29
31
  this.userAgentSessionMap = userAgentSessionMap;
30
32
  this.teams = teams;
33
+ this.isGuestMode = isGuestMode;
31
34
  this.closed = closed;
32
35
  this.currentSessionId = currentSessionId;
33
36
  this.currentTeamId = currentTeamId;
@@ -39,6 +42,8 @@ class ChatClient {
39
42
  * Connect to server and create ChatClient instance
40
43
  */
41
44
  static async init(url, token, eventHandler) {
45
+ // Determine if this is a guest token (format: guest_<hash>_<session-id>)
46
+ const isGuestMode = token.startsWith('guest_');
42
47
  const connection = new connection_1.Connection({
43
48
  url,
44
49
  token,
@@ -69,7 +74,7 @@ class ChatClient {
69
74
  });
70
75
  if (!client) {
71
76
  logger.info("Creating ChatClient");
72
- client = new ChatClient(connection, eventHandler, userAgentSessionMap, teams);
77
+ client = new ChatClient(connection, eventHandler, userAgentSessionMap, teams, isGuestMode);
73
78
  resolveClient(client);
74
79
  }
75
80
  else {
@@ -364,6 +369,22 @@ class ChatClient {
364
369
  });
365
370
  logger.debug(`[ChatClient] Sent session_delete_request for` + ` session ${sessionId}`);
366
371
  }
372
+ /**
373
+ * Delete an agent profile by sending control_agent_profile_delete message
374
+ * @param agentProfileUuid - The UUID of the agent profile to delete
375
+ * @returns void
376
+ */
377
+ deleteAgentProfile(agentProfileUuid) {
378
+ if (this.closed) {
379
+ throw new Error("ChatClient is closed");
380
+ }
381
+ this.connection.send({
382
+ type: "control_agent_profile_delete",
383
+ agent_profile_uuid: agentProfileUuid,
384
+ });
385
+ logger.debug(`[ChatClient] Sent control_agent_profile_delete for profile ` +
386
+ agentProfileUuid);
387
+ }
367
388
  /**
368
389
  * Create a new team with initial members
369
390
  * @param teamName
@@ -513,6 +534,9 @@ class ChatClient {
513
534
  case "control_team_created":
514
535
  this.handleTeamCreatedMessage(msg);
515
536
  break;
537
+ case "control_team_members_updated":
538
+ this.handleTeamMembersUpdated(msg);
539
+ break;
516
540
  default: {
517
541
  const _exhaustive = msg;
518
542
  throw new Error(`unexpected control msg: ${JSON.stringify(msg)}`);
@@ -541,8 +565,29 @@ class ChatClient {
541
565
  }
542
566
  void this.eventHandler.onMessage(msg, this);
543
567
  }
544
- handleAgentProfileDeleted(_msg) {
545
- throw new Error("handleAgentProfileDeleted not implemented");
568
+ handleAgentProfileDeleted(msg) {
569
+ logger.debug(`[ChatClient.handleAgentProfileDeleted] msg: ${JSON.stringify(msg)}`);
570
+ const profileUuid = msg.profile_uuid;
571
+ let found = false;
572
+ // Try to remove from user agent session map
573
+ if (this.userAgentSessionMap.has(profileUuid)) {
574
+ this.userAgentSessionMap.delete(profileUuid);
575
+ found = true;
576
+ }
577
+ else {
578
+ // Try to remove from team agent session maps
579
+ for (const [_teamId, team] of this.teams.entries()) {
580
+ if (team.agentSessionMap.has(profileUuid)) {
581
+ team.agentSessionMap.delete(profileUuid);
582
+ found = true;
583
+ break;
584
+ }
585
+ }
586
+ }
587
+ if (!found) {
588
+ logger.warn(`[ChatClient] Agent profile ${profileUuid} not found in any session map`);
589
+ }
590
+ void this.eventHandler.onMessage(msg, this);
546
591
  }
547
592
  handleSessionMessage(msg) {
548
593
  const sessionId = msg.session_id;
@@ -587,6 +632,19 @@ class ChatClient {
587
632
  void this.eventHandler.onMessage(msg, this);
588
633
  return;
589
634
  }
635
+ handleTeamMembersUpdated(msg) {
636
+ const team = this.teams.get(msg.team_uuid);
637
+ if (!team) {
638
+ logger.warn(`[ChatClient] Received team_members_updated for unknown team ` +
639
+ msg.team_uuid);
640
+ return;
641
+ }
642
+ // Update the participants map
643
+ team.participants = new Map(msg.members.map((participant) => [participant.user_uuid, participant]));
644
+ logger.info(`[ChatClient] Updated team members for team ${msg.team_uuid}, ` +
645
+ `now has ${String(msg.members.length)} members`);
646
+ void this.eventHandler.onMessage(msg, this);
647
+ }
590
648
  /**
591
649
  * Handle session_info message which can be for:
592
650
  * 1. A pending session join/create request
@@ -602,7 +660,7 @@ class ChatClient {
602
660
  try {
603
661
  logger.info(`[ChatClient] Creating SessionClient for session ${sessionId}`);
604
662
  logger.info(`[ChatClient] msg: ${JSON.stringify(msg)}`);
605
- const sessionClient = new sessionClient_1.SessionClient(sessionId, msg.saved_agent_profile, this.connection, msg.mcp_server_briefs, (0, database_1.createSessionParticipantMap)(msg.participants));
663
+ const sessionClient = new sessionClient_1.SessionClient(sessionId, msg.saved_agent_profile, this.connection, msg.mcp_server_briefs, (0, database_1.createSessionParticipantMap)(msg.participants), msg.agent_paused);
606
664
  // we need to pass the session id if this is a new session
607
665
  if (this.sessionJoinOrCreateRes.sessionId === "") {
608
666
  this.sessionJoinOrCreateRes.sessionId = sessionId;
@@ -639,12 +697,18 @@ class ChatClient {
639
697
  */
640
698
  updateAgentSessionMap(sessionInfo) {
641
699
  const sessionId = sessionInfo.session_id;
700
+ // Skip agent session map updates for guest sessions since they don't
701
+ // need session organization
702
+ if (this.isGuestMode) {
703
+ logger.info(`[ChatClient] Skipping agent session map update for ` +
704
+ `guest session ${sessionId}`);
705
+ return;
706
+ }
642
707
  if (sessionInfo.team_uuid) {
643
708
  const teamInfo = this.teams.get(sessionInfo.team_uuid);
644
709
  if (!teamInfo) {
645
710
  throw new Error(`Team ${sessionInfo.team_uuid} not found in team list`);
646
711
  }
647
- //teamInfo.sessions.set(sessionId, ChatClient.toSessionData(sessionInfo));
648
712
  const agentSessionMap = teamInfo.agentSessionMap;
649
713
  if (!doUpdateAgentSessionMap(agentSessionMap, sessionInfo)) {
650
714
  throw new Error(`[updateAgentSessionMap] team session ${sessionId}` +
@@ -660,9 +724,9 @@ class ChatClient {
660
724
  }
661
725
  }
662
726
  /**
663
- * Update the session info for an existing session. This passes the session
664
- * info to the session client. An error is thrown if the session client is not
665
- * found.
727
+ * Update the SessionDescriptor info for an existing session, given a full
728
+ * ServerSessionInfo. This also passes the session info to the session
729
+ * client. An error is thrown if the session client is not found.
666
730
  */
667
731
  updateSessionInfo(msg) {
668
732
  const sessionId = msg.session_id;
@@ -676,6 +740,13 @@ class ChatClient {
676
740
  }
677
741
  }
678
742
  addSessionToAgentSessionMap(msg) {
743
+ // Skip agent session map updates for guest sessions since they don't
744
+ // need session organization
745
+ if (this.isGuestMode) {
746
+ logger.info(`[ChatClient] Skipping agent session map add for ` +
747
+ `guest session ${msg.session_id}`);
748
+ return;
749
+ }
679
750
  // get the correct agent session map
680
751
  const agentSessionMap = msg.team_uuid
681
752
  ? this.teams.get(msg.team_uuid)?.agentSessionMap
@@ -690,20 +761,20 @@ class ChatClient {
690
761
  throw new Error(`[addSessionToAgentSessionMap] Agent ${agentUuid}` +
691
762
  ` not found in agent session map`);
692
763
  }
693
- agentSession.sessions.push(sessionInfoToSessionData(msg));
764
+ agentSession.sessions.push(sessionInfoToSessionDescriptor(msg));
694
765
  agentSession.updated_at = new Date(msg.updated_at).getTime();
695
766
  }
696
767
  }
697
768
  exports.ChatClient = ChatClient;
698
- function sessionInfoToSessionData(msg) {
769
+ function sessionInfoToSessionDescriptor(msg) {
699
770
  return {
700
771
  session_uuid: msg.session_id,
701
772
  title: msg.title,
702
773
  team_uuid: msg.team_uuid,
703
774
  agent_profile_uuid: msg.saved_agent_profile.uuid,
704
- workspace: msg.workspace,
705
775
  updated_at: msg.updated_at,
706
776
  user_uuid: msg.owner_uuid,
777
+ agent_paused: msg.agent_paused,
707
778
  };
708
779
  }
709
780
  /**
@@ -723,7 +794,7 @@ function doUpdateAgentSessionMap(agentSessionMap, sessionInfo) {
723
794
  agent.sessions.map((session) => {
724
795
  if (session.session_uuid === sessionId) {
725
796
  updated = true;
726
- return sessionInfoToSessionData(sessionInfo);
797
+ return sessionInfoToSessionDescriptor(sessionInfo);
727
798
  }
728
799
  else {
729
800
  return session;
@@ -5,7 +5,10 @@ const assert_1 = require("assert");
5
5
  const uuid_1 = require("uuid");
6
6
  const sdk_1 = require("@xalia/xmcp/sdk");
7
7
  const mcpServerManager_1 = require("../../agent/mcpServerManager");
8
+ const messages_1 = require("../protocol/messages");
8
9
  const database_1 = require("../data/database");
10
+ const sessionFiles_1 = require("./sessionFiles");
11
+ const responseHandler_1 = require("./responseHandler");
9
12
  const logger = (0, sdk_1.getLogger)();
10
13
  class RemoteSudoMcpServerManager {
11
14
  constructor(sender, briefs) {
@@ -143,20 +146,27 @@ class RemoteSudoMcpServerManager {
143
146
  async shutdown() { }
144
147
  }
145
148
  class SessionClient {
146
- constructor(sessionUUID, savedAgentProfile, sender, serverBriefs, participants) {
149
+ constructor(sessionUUID, savedAgentProfile, sender, serverBriefs, participants, agentPaused) {
150
+ const fileList = []; // Pass in?
147
151
  this.sessionUUID = sessionUUID;
148
152
  this.savedAgentProfile = savedAgentProfile;
149
153
  this.sender = sender;
150
- this.participants = participants;
151
- this.sender = sender;
152
154
  this.smsm = new RemoteSudoMcpServerManager(this, serverBriefs);
155
+ this.sessionFiles = new sessionFiles_1.SessionFiles(sessionUUID, fileList, sender);
156
+ this.responseHandler = new responseHandler_1.ResponseHandler("session_error");
157
+ this.participants = participants;
153
158
  this.systemPrompt = "";
154
159
  this.model = "";
155
- this.participants = participants;
160
+ this.agentPaused = agentPaused;
156
161
  }
157
162
  getSessionUUID() {
158
163
  return this.sessionUUID;
159
164
  }
165
+ /// This object can be queried to upload or download files, or get the
166
+ /// latest list. See `SessionFiles` for full usage.
167
+ getSessionFiles() {
168
+ return this.sessionFiles;
169
+ }
160
170
  getSudoMcpServerManager() {
161
171
  return this.smsm;
162
172
  }
@@ -187,6 +197,15 @@ class SessionClient {
187
197
  // Don't set model here. Wait until we get confirmation from the server.
188
198
  this.sendSessionMessage({ type: "set_model", model });
189
199
  }
200
+ getAgentPaused() {
201
+ return this.agentPaused;
202
+ }
203
+ setAgentPaused(paused) {
204
+ this.sendSessionMessage({
205
+ type: "set_agent_paused",
206
+ paused,
207
+ });
208
+ }
190
209
  userMessage(msg, imageB64) {
191
210
  (0, assert_1.strict)(msg || imageB64, "Either message or image must be provided");
192
211
  this.sendSessionMessage({ type: "msg", message: msg, imageB64 });
@@ -249,8 +268,7 @@ class SessionClient {
249
268
  this.sender.send(enrichedMessage);
250
269
  }
251
270
  updateSessionInfo(sessionInfo) {
252
- // TODO: Implement this
253
- // throw new Error("[SessionClient.updateSessionInfo] not implemented");
271
+ // TODO: Determine the correct approach. Cache the workspace?
254
272
  const infoStr = JSON.stringify(sessionInfo.workspace);
255
273
  logger.debug(`[SessionClient] ignoring session info: ${infoStr}`);
256
274
  }
@@ -278,7 +296,24 @@ class SessionClient {
278
296
  server_name,
279
297
  });
280
298
  }
299
+ async shareSession() {
300
+ const msg = {
301
+ type: "share_session",
302
+ client_message_id: (0, uuid_1.v4)(),
303
+ session_id: this.sessionUUID,
304
+ };
305
+ this.sender.send(msg);
306
+ const response = await this.responseHandler.waitForResponse(msg);
307
+ if (response.type === "session_shared") {
308
+ return response.access_token;
309
+ }
310
+ throw new Error(`unexpected response to "share"session": ${response.type}`);
311
+ }
281
312
  handleMessage(message) {
313
+ if ((0, messages_1.isServerSessionFileMessage)(message)) {
314
+ this.sessionFiles.onMessage(message);
315
+ return;
316
+ }
282
317
  switch (message.type) {
283
318
  //
284
319
  // State updates
@@ -318,6 +353,12 @@ class SessionClient {
318
353
  case "user_removed":
319
354
  this.participants.delete(message.user_uuid);
320
355
  break;
356
+ case "session_shared":
357
+ this.responseHandler.onMessage(message);
358
+ break;
359
+ case "agent_paused":
360
+ this.agentPaused = message.paused;
361
+ break;
321
362
  //
322
363
  // Ignore other messages - the owner (the UI layer) can handle them at
323
364
  // its discretion.
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionFiles = void 0;
4
+ const uuid_1 = require("uuid");
5
+ const assert_1 = require("assert");
6
+ const sdk_1 = require("@xalia/xmcp/sdk");
7
+ const responseHandler_1 = require("./responseHandler");
8
+ const logger = (0, sdk_1.getLogger)();
9
+ /// Object for the UI to use to interact with the FileManager. If the UI
10
+ /// receives ServerSessionFileChanged or ServerSessionFileDeleted then it
11
+ /// should call `listFiles()` to get the new list of files, and optionally
12
+ /// request the latest content via `getFileContent`.
13
+ class SessionFiles {
14
+ constructor(sessionUUID, initialFileList, sender) {
15
+ this.sessionUUID = sessionUUID;
16
+ this.descriptors = new Map(initialFileList.map((d) => [d.name, d]));
17
+ this.responseHandler = new responseHandler_1.ResponseHandler(undefined);
18
+ this.sender = sender;
19
+ }
20
+ listFiles() {
21
+ return Array.from(this.descriptors.values());
22
+ }
23
+ /**
24
+ * Retrieve file contents.
25
+ */
26
+ async getFileContent(name) {
27
+ const msg = {
28
+ type: "session_file_get_content",
29
+ session_id: this.sessionUUID,
30
+ name,
31
+ client_message_id: (0, uuid_1.v4)(),
32
+ };
33
+ this.sender.send(msg);
34
+ const response = await this.responseHandler.waitForResponse(msg);
35
+ if (response.name !== name) {
36
+ throw new Error(`invalid name for file ${name}: ${JSON.stringify(response)}`);
37
+ }
38
+ return response.data_url;
39
+ }
40
+ deleteFile(name) {
41
+ const msg = {
42
+ type: "session_file_delete",
43
+ session_id: this.sessionUUID,
44
+ name,
45
+ client_message_id: (0, uuid_1.v4)(),
46
+ };
47
+ this.sender.send(msg);
48
+ }
49
+ putFileContent(name, summary, data_url) {
50
+ // TODO: eventually, we could wait for a response which includes an AI
51
+ // assigned name.
52
+ (0, assert_1.strict)(name, "for now, uploaded content must have a name");
53
+ const msg = {
54
+ type: "session_file_put_content",
55
+ session_id: this.sessionUUID,
56
+ name,
57
+ summary,
58
+ data_url,
59
+ client_message_id: (0, uuid_1.v4)(),
60
+ };
61
+ this.sender.send(msg);
62
+ }
63
+ onMessage(msg) {
64
+ logger.debug(`[SessionFiles.onMessage]: msg: ${JSON.stringify(msg)}`);
65
+ switch (msg.type) {
66
+ case "session_file_changed":
67
+ this.descriptors.set(msg.descriptor.name, msg.descriptor);
68
+ break;
69
+ case "session_file_deleted":
70
+ this.descriptors.delete(msg.name);
71
+ break;
72
+ case "session_file_content":
73
+ this.responseHandler.onMessage(msg);
74
+ break;
75
+ default: {
76
+ const _ = msg;
77
+ const msgStr = JSON.stringify(msg);
78
+ throw new Error(`[SessionFiles.onMessage] invalid message: ${msgStr}`);
79
+ }
80
+ }
81
+ }
82
+ decodeSessionFileUrl(url) {
83
+ const u = new URL(url);
84
+ if (u.protocol !== "file+session:") {
85
+ throw new Error(`unexpected protocol ${u.protocol} (file+session)`);
86
+ }
87
+ if (u.port || u.search || u.hash) {
88
+ throw new Error("badly formed session file url: ${url}");
89
+ }
90
+ return {
91
+ session_uuid: u.host || this.sessionUUID,
92
+ name: removeLeadingSlashes(u.pathname),
93
+ };
94
+ }
95
+ }
96
+ exports.SessionFiles = SessionFiles;
97
+ function removeLeadingSlashes(name) {
98
+ while (name.startsWith("/")) {
99
+ name = name.slice(1);
100
+ }
101
+ return name;
102
+ }
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.ApiKeyManager = void 0;
6
6
  const sdk_1 = require("@xalia/xmcp/sdk");
7
7
  const logger = (0, sdk_1.getLogger)();
8
+ const API_KEY_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
8
9
  class ApiKeyManager {
9
10
  constructor(db) {
10
11
  this.db = db;
@@ -15,13 +16,43 @@ class ApiKeyManager {
15
16
  const userInfo = await this.db.getUserDataFromApiKey(apiKey);
16
17
  logger.info(`[ApiKeyManager] User info: ${JSON.stringify(userInfo)}`);
17
18
  return userInfo;
18
- // if (apiKey.startsWith("dummy_key")) {
19
- // return {
20
- // user_uuid: apiKey,
21
- // nickname: apiKey,
22
- // };
23
- // }
24
- // return undefined;
19
+ }
20
+ /**
21
+ * Creates a standard api key of the form:
22
+ *
23
+ * <prefix>_<[A-Z][a-z][0-9]+....>
24
+ *
25
+ * matching the format used in the mcppro backend
26
+ */
27
+ static createApiKey(prefix = ApiKeyManager.PREFIX, length = 32) {
28
+ // See mcppro/app/server/api_key.py:
29
+ //
30
+ // def create_api_key(prefix: str, length: int = 32) -> str:
31
+ // ...
32
+ const chars = Array.from({ length }, () => {
33
+ const i = Math.floor(Math.random() * API_KEY_ALPHABET.length);
34
+ return API_KEY_ALPHABET[i];
35
+ }).join("");
36
+ return `${prefix}_${chars}`;
37
+ }
38
+ /**
39
+ * Creates an api key of the form:
40
+ *
41
+ * <prefix>_<[A-Z][a-z][0-9]+....>/<payload>
42
+ *
43
+ * where <payload> is used to convey extra data.
44
+ */
45
+ static createApiKeyWithPayload(prefix, payload, length = 32) {
46
+ return `${ApiKeyManager.createApiKey(prefix, length)}_${payload}`;
47
+ }
48
+ /**
49
+ * Parse token (containing an api and optional payload)
50
+ */
51
+ static parseToken(token) {
52
+ const [prefix, apiKeyVal, payload] = token.split("_");
53
+ const apiKey = `${prefix}_${apiKeyVal}`;
54
+ return { prefix, apiKey, payload };
25
55
  }
26
56
  }
27
57
  exports.ApiKeyManager = ApiKeyManager;
58
+ ApiKeyManager.PREFIX = "xmcp";