@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
@@ -0,0 +1,145 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import { strict as assert } from "assert";
3
+
4
+ import { getLogger } from "@xalia/xmcp/sdk";
5
+ import { SessionFileDescriptor } from "../data/dbSessionFileModels";
6
+ import {
7
+ ClientToServer,
8
+ ServerSessionFileContent,
9
+ ServerSessionFileMessage,
10
+ } from "../protocol/messages";
11
+ import { ResponseHandler } from "./responseHandler";
12
+ import { IMessageSender } from "./connection";
13
+
14
+ const logger = getLogger();
15
+
16
+ type ClientRequestContent = Extract<
17
+ ClientToServer,
18
+ { type: "session_file_get_content" }
19
+ >;
20
+
21
+ /// Object for the UI to use to interact with the FileManager. If the UI
22
+ /// receives ServerSessionFileChanged or ServerSessionFileDeleted then it
23
+ /// should call `listFiles()` to get the new list of files, and optionally
24
+ /// request the latest content via `getFileContent`.
25
+ export class SessionFiles {
26
+ readonly sessionUUID: string;
27
+ readonly descriptors: Map<string, SessionFileDescriptor>;
28
+ readonly responseHandler: ResponseHandler<
29
+ ClientRequestContent,
30
+ ServerSessionFileContent
31
+ >;
32
+ readonly sender: IMessageSender<ClientToServer>;
33
+
34
+ constructor(
35
+ sessionUUID: string,
36
+ initialFileList: SessionFileDescriptor[],
37
+ sender: IMessageSender<ClientToServer>
38
+ ) {
39
+ this.sessionUUID = sessionUUID;
40
+ this.descriptors = new Map(initialFileList.map((d) => [d.name, d]));
41
+ this.responseHandler = new ResponseHandler(undefined);
42
+ this.sender = sender;
43
+ }
44
+
45
+ listFiles(): SessionFileDescriptor[] {
46
+ return Array.from(this.descriptors.values());
47
+ }
48
+
49
+ /**
50
+ * Retrieve file contents.
51
+ */
52
+ async getFileContent(name: string): Promise<string> {
53
+ const msg: ClientToServer = {
54
+ type: "session_file_get_content",
55
+ session_id: this.sessionUUID,
56
+ name,
57
+ client_message_id: uuidv4(),
58
+ };
59
+ this.sender.send(msg);
60
+ const response = await this.responseHandler.waitForResponse(msg);
61
+ if (response.name !== name) {
62
+ throw new Error(
63
+ `invalid name for file ${name}: ${JSON.stringify(response)}`
64
+ );
65
+ }
66
+ return response.data_url;
67
+ }
68
+
69
+ deleteFile(name: string): void {
70
+ const msg: ClientToServer = {
71
+ type: "session_file_delete",
72
+ session_id: this.sessionUUID,
73
+ name,
74
+ client_message_id: uuidv4(),
75
+ };
76
+ this.sender.send(msg);
77
+ }
78
+
79
+ putFileContent(
80
+ name: string | undefined,
81
+ summary: string | undefined,
82
+ data_url: string
83
+ ): void {
84
+ // TODO: eventually, we could wait for a response which includes an AI
85
+ // assigned name.
86
+ assert(name, "for now, uploaded content must have a name");
87
+
88
+ const msg: ClientToServer = {
89
+ type: "session_file_put_content",
90
+ session_id: this.sessionUUID,
91
+ name,
92
+ summary,
93
+ data_url,
94
+ client_message_id: uuidv4(),
95
+ };
96
+ this.sender.send(msg);
97
+ }
98
+
99
+ onMessage(msg: ServerSessionFileMessage) {
100
+ logger.debug(`[SessionFiles.onMessage]: msg: ${JSON.stringify(msg)}`);
101
+ switch (msg.type) {
102
+ case "session_file_changed":
103
+ this.descriptors.set(msg.descriptor.name, msg.descriptor);
104
+ break;
105
+
106
+ case "session_file_deleted":
107
+ this.descriptors.delete(msg.name);
108
+ break;
109
+
110
+ case "session_file_content":
111
+ this.responseHandler.onMessage(msg);
112
+ break;
113
+
114
+ default: {
115
+ const _: never = msg;
116
+ const msgStr = JSON.stringify(msg);
117
+ throw new Error(`[SessionFiles.onMessage] invalid message: ${msgStr}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ decodeSessionFileUrl(url: string): {
123
+ session_uuid: string;
124
+ name: string;
125
+ } {
126
+ const u = new URL(url);
127
+ if (u.protocol !== "file+session:") {
128
+ throw new Error(`unexpected protocol ${u.protocol} (file+session)`);
129
+ }
130
+ if (u.port || u.search || u.hash) {
131
+ throw new Error("badly formed session file url: ${url}");
132
+ }
133
+ return {
134
+ session_uuid: u.host || this.sessionUUID,
135
+ name: removeLeadingSlashes(u.pathname),
136
+ };
137
+ }
138
+ }
139
+
140
+ function removeLeadingSlashes(name: string): string {
141
+ while (name.startsWith("/")) {
142
+ name = name.slice(1);
143
+ }
144
+ return name;
145
+ }
@@ -6,23 +6,71 @@ import { getLogger } from "@xalia/xmcp/sdk";
6
6
 
7
7
  const logger = getLogger();
8
8
 
9
+ const API_KEY_ALPHABET =
10
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
11
+
9
12
  export class ApiKeyManager {
10
13
  constructor(private db: Database) {}
11
14
 
15
+ public static PREFIX = "xmcp";
16
+
12
17
  public async verifyApiKey(apiKey: string): Promise<UserData | undefined> {
13
18
  // TODO: Cache this
14
19
  logger.info(`[ApiKeyManager] Verifying API key: ${apiKey}`);
15
20
  const userInfo = await this.db.getUserDataFromApiKey(apiKey);
16
21
  logger.info(`[ApiKeyManager] User info: ${JSON.stringify(userInfo)}`);
17
22
  return userInfo;
23
+ }
24
+
25
+ /**
26
+ * Creates a standard api key of the form:
27
+ *
28
+ * <prefix>_<[A-Z][a-z][0-9]+....>
29
+ *
30
+ * matching the format used in the mcppro backend
31
+ */
32
+ public static createApiKey(
33
+ prefix: string = ApiKeyManager.PREFIX,
34
+ length: number = 32
35
+ ): string {
36
+ // See mcppro/app/server/api_key.py:
37
+ //
38
+ // def create_api_key(prefix: str, length: int = 32) -> str:
39
+ // ...
40
+
41
+ const chars = Array.from({ length }, () => {
42
+ const i = Math.floor(Math.random() * API_KEY_ALPHABET.length);
43
+ return API_KEY_ALPHABET[i];
44
+ }).join("");
45
+ return `${prefix}_${chars}`;
46
+ }
47
+
48
+ /**
49
+ * Creates an api key of the form:
50
+ *
51
+ * <prefix>_<[A-Z][a-z][0-9]+....>/<payload>
52
+ *
53
+ * where <payload> is used to convey extra data.
54
+ */
55
+ public static createApiKeyWithPayload(
56
+ prefix: string,
57
+ payload: string,
58
+ length: number = 32
59
+ ): string {
60
+ return `${ApiKeyManager.createApiKey(prefix, length)}_${payload}`;
61
+ }
18
62
 
19
- // if (apiKey.startsWith("dummy_key")) {
20
- // return {
21
- // user_uuid: apiKey,
22
- // nickname: apiKey,
23
- // };
24
- // }
63
+ /**
64
+ * Parse token (containing an api and optional payload)
65
+ */
66
+ public static parseToken(token: string): {
67
+ prefix: string;
68
+ apiKey: string;
69
+ payload: string | undefined;
70
+ } {
71
+ const [prefix, apiKeyVal, payload] = token.split("_");
72
+ const apiKey = `${prefix}_${apiKeyVal}`;
25
73
 
26
- // return undefined;
74
+ return { prefix, apiKey, payload };
27
75
  }
28
76
  }
@@ -1,5 +1,5 @@
1
1
  import { SavedAgentProfile } from "@xalia/xmcp/sdk";
2
- import { ChatCompletionMessageParam } from "../../agent/agent";
2
+ import { ChatCompletionMessageParam } from "../../agent/llm";
3
3
 
4
4
  export type UserMessageData = {
5
5
  message?: string | undefined;
@@ -50,20 +50,29 @@ export type SessionCreateData = {
50
50
  title: string;
51
51
  agent_profile_uuid: string;
52
52
  user_uuid: string;
53
+
53
54
  team_uuid?: string | undefined;
54
- workspace?: UserMessageData | undefined;
55
+ access_token?: string;
56
+ agent_paused: boolean;
55
57
  };
56
58
 
57
- /**
58
- * Data for existing session.
59
+ /*
60
+ * Description of (unjoined) sessions.
59
61
  */
60
- export type SessionData = SessionCreateData & {
62
+ export type SessionDescriptor = SessionCreateData & {
61
63
  updated_at: string;
62
64
  };
63
65
 
66
+ /**
67
+ * Data for existing session, including any workspace data
68
+ */
69
+ export type SessionData = SessionDescriptor & {
70
+ workspace?: UserMessageData | undefined;
71
+ };
72
+
64
73
  export type AgentSessionData = {
65
74
  agent_profile: SavedAgentProfile;
66
- sessions: SessionData[];
75
+ sessions: SessionDescriptor[];
67
76
  updated_at: number;
68
77
  };
69
78
 
@@ -75,7 +84,7 @@ export type TeamInfo = {
75
84
  team_name: string;
76
85
  owner_uuid: string;
77
86
  participants: Array<TeamParticipant>;
78
- sessions: Array<SessionData>;
87
+ sessions: Array<SessionDescriptor>;
79
88
  agents: Array<SavedAgentProfile>;
80
89
  };
81
90
 
@@ -6,12 +6,12 @@ import {
6
6
  TeamParticipant,
7
7
  TeamRole,
8
8
  SessionParticipantMap,
9
- SessionData,
10
9
  TeamInfo,
11
- SessionMessage,
12
10
  SessionCheckpoint,
13
11
  UserMessageData,
14
12
  SessionCreateData,
13
+ SessionDescriptor,
14
+ SessionData,
15
15
  } from "./dataModels";
16
16
  import { createClient, SupabaseClient } from "@supabase/supabase-js";
17
17
  import type * as supabase from "../../../../supabase/database.types";
@@ -165,6 +165,16 @@ export class Database {
165
165
  }
166
166
  }
167
167
 
168
+ async deleteUser(user_uuid: string): Promise<void> {
169
+ const { error } = await this.client
170
+ .from("users")
171
+ .delete()
172
+ .eq("uuid", user_uuid);
173
+ if (error) {
174
+ throw error;
175
+ }
176
+ }
177
+
168
178
  async addApiKey(
169
179
  user_uuid: string,
170
180
  api_key: string,
@@ -337,6 +347,16 @@ export class Database {
337
347
  await this.client.from("agent_profiles").delete().neq("uuid", "");
338
348
  }
339
349
 
350
+ async deleteAgentProfile(agentProfileUuid: string): Promise<void> {
351
+ const { error } = await this.client
352
+ .from("agent_profiles")
353
+ .delete()
354
+ .eq("uuid", agentProfileUuid);
355
+ if (error) {
356
+ throw error;
357
+ }
358
+ }
359
+
340
360
  //
341
361
  // sessions
342
362
  //
@@ -384,13 +404,44 @@ export class Database {
384
404
  };
385
405
  }
386
406
 
387
- async sessionGetByName(
407
+ async sessionGetDescriptorById(
408
+ session_uuid: string
409
+ ): Promise<SessionDescriptor | undefined> {
410
+ const { data, error } = await this.client
411
+ .from("sessions")
412
+ .select(
413
+ // eslint-disable-next-line max-len
414
+ "uuid,title,agent_profile_uuid,user_uuid,team_uuid,access_token,updated_at,agent_paused"
415
+ )
416
+ .eq("uuid", session_uuid)
417
+ .maybeSingle();
418
+
419
+ if (error) {
420
+ throw error;
421
+ }
422
+
423
+ if (!data) {
424
+ return undefined;
425
+ }
426
+
427
+ return {
428
+ ...data,
429
+ access_token: data.access_token || undefined,
430
+ session_uuid: data.uuid,
431
+ updated_at: data.updated_at || new Date().toISOString(),
432
+ };
433
+ }
434
+
435
+ async sessionGetDescriptorByName(
388
436
  user_uuid: string,
389
437
  session_name: string
390
- ): Promise<SessionData | undefined> {
438
+ ): Promise<SessionDescriptor | undefined> {
391
439
  const { data, error } = await this.client
392
440
  .from("sessions")
393
- .select("*")
441
+ .select(
442
+ // eslint-disable-next-line max-len
443
+ "uuid,title,agent_profile_uuid,user_uuid,team_uuid,access_token,updated_at,agent_paused"
444
+ )
394
445
  .eq("user_uuid", user_uuid)
395
446
  .eq("title", session_name)
396
447
  .maybeSingle();
@@ -399,11 +450,15 @@ export class Database {
399
450
  logger.error(`[getSessionByName] error: ${JSON.stringify(error)}`);
400
451
  throw error;
401
452
  }
453
+
454
+ if (!data) {
455
+ return undefined;
456
+ }
457
+
402
458
  return {
403
459
  ...data,
404
- workspace: data.workspace || undefined,
405
- owner_uuid: data.user_uuid,
406
460
  session_uuid: data.uuid,
461
+ access_token: data.access_token || undefined,
407
462
  updated_at: data.updated_at || new Date().toISOString(),
408
463
  };
409
464
  }
@@ -415,7 +470,7 @@ export class Database {
415
470
  agent_profile_uuid: session_data.agent_profile_uuid,
416
471
  user_uuid: session_data.user_uuid,
417
472
  team_uuid: session_data.team_uuid,
418
- workspace: session_data.workspace,
473
+ access_token: session_data.access_token,
419
474
  };
420
475
  const { error } = await this.client.from("sessions").insert(payload);
421
476
  if (error) {
@@ -424,9 +479,7 @@ export class Database {
424
479
  }
425
480
 
426
481
  async sessionUpdateTitle(session_uuid: string, title: string): Promise<void> {
427
- const payload: supabase.TablesUpdate<"sessions"> = {
428
- title,
429
- };
482
+ const payload: supabase.TablesUpdate<"sessions"> = { title };
430
483
  const { error } = await this.client
431
484
  .from("sessions")
432
485
  .update(payload)
@@ -452,6 +505,36 @@ export class Database {
452
505
  }
453
506
  }
454
507
 
508
+ async sessionUpdateAccessToken(
509
+ session_uuid: string,
510
+ access_token: string | undefined
511
+ ): Promise<void> {
512
+ const payload: supabase.TablesUpdate<"sessions"> = {
513
+ access_token: access_token || null,
514
+ };
515
+ const { error } = await this.client
516
+ .from("sessions")
517
+ .update(payload)
518
+ .eq("uuid", session_uuid);
519
+ if (error) {
520
+ throw error;
521
+ }
522
+ }
523
+
524
+ async sessionSetAgentPaused(
525
+ session_uuid: string,
526
+ paused: boolean
527
+ ): Promise<void> {
528
+ const payload: supabase.TablesUpdate<"sessions"> = { agent_paused: paused };
529
+ const { error } = await this.client
530
+ .from("sessions")
531
+ .update(payload)
532
+ .eq("uuid", session_uuid);
533
+ if (error) {
534
+ throw error;
535
+ }
536
+ }
537
+
455
538
  async sessionDeleteById(session_uuid: string): Promise<void> {
456
539
  await this.client.from("sessions").delete().eq("uuid", session_uuid);
457
540
  }
@@ -465,11 +548,12 @@ export class Database {
465
548
  * @param user_uuid
466
549
  * @returns SessionData[]
467
550
  */
468
- async getUserSessions(user_uuid: string): Promise<SessionData[]> {
551
+ async getUserSessions(user_uuid: string): Promise<SessionDescriptor[]> {
469
552
  const { data: userSessions, error: userSessionsError } = await this.client
470
553
  .from("sessions")
471
554
  .select(
472
- "uuid, title, agent_profile_uuid, workspace, updated_at, user_uuid"
555
+ // eslint-disable-next-line max-len
556
+ "uuid,title,agent_profile_uuid,updated_at,user_uuid,access_token,agent_paused"
473
557
  )
474
558
  .eq("user_uuid", user_uuid)
475
559
  .is("team_uuid", null);
@@ -483,9 +567,10 @@ export class Database {
483
567
  title: s.title,
484
568
  team_uuid: undefined,
485
569
  agent_profile_uuid: s.agent_profile_uuid,
486
- workspace: s.workspace || undefined,
570
+ access_token: s.access_token || undefined,
487
571
  updated_at: s.updated_at,
488
572
  user_uuid: s.user_uuid,
573
+ agent_paused: s.agent_paused,
489
574
  }));
490
575
  }
491
576
 
@@ -522,90 +607,22 @@ export class Database {
522
607
  }
523
608
 
524
609
  async sessionGetTeamUuid(session_uuid: string): Promise<string | undefined> {
525
- const data = await this.sessionGetById(session_uuid);
610
+ const data = await this.sessionGetDescriptorById(session_uuid);
526
611
  if (data) {
527
612
  return data.team_uuid;
528
613
  }
529
614
  return undefined;
530
615
  }
531
616
 
532
- //
533
- // session_messages
534
- //
535
-
536
- async sessionMessagesClearConversation(session_uuid: string): Promise<void> {
537
- const { error } = await this.client
538
- .from("session_messages")
539
- .delete()
540
- .eq("session_uuid", session_uuid);
541
- if (error) {
542
- throw error;
543
- }
544
- }
545
-
546
- async sessionMessagesGetConversation(
547
- session_uuid: string,
548
- numEntries: number,
549
- beforeIndex?: number
550
- ): Promise<SessionMessage[]> {
551
- // Query all message for the given session, ordered high-to-low by
552
- // message_idx, limited to `numEntries`. If `beforeIndex` is given, it
553
- // means we get messages with `message_idx < beforeIndex`
554
-
555
- let query = this.client
556
- .from("session_messages")
557
- .select("message_idx,sender_uuid,is_for_llm,content")
558
- .eq("session_uuid", session_uuid);
559
- if (beforeIndex) {
560
- query = query.lt("message_idx", beforeIndex);
561
- }
562
- query = query.order("message_idx", { ascending: false }).limit(numEntries);
563
-
564
- const { data, error } = await query;
565
- if (error) {
566
- throw error;
567
- }
568
-
569
- // To get the newest N messages, we've orded by index largest to smallest
570
- // (newest first), but caller wants the message first to last, hence
571
- // reverse the array.
572
-
573
- return data
574
- .map(({ sender_uuid, ...rest }) => {
575
- return sender_uuid
576
- ? {
577
- sender_uuid,
578
- ...rest,
579
- }
580
- : { ...rest };
581
- })
582
- .reverse();
583
- }
584
-
585
- async sessionMessagesAppend(
586
- session_uuid: string,
587
- messages: SessionMessage[]
588
- ): Promise<void> {
589
- const payload = messages.map((m) => {
590
- return { ...m, session_uuid };
591
- });
592
- const { error } = await this.client
593
- .from("session_messages")
594
- .insert(payload);
595
- if (error) {
596
- throw error;
597
- }
598
- }
599
-
600
617
  //
601
618
  // session_checkpoints
602
619
  //
603
620
 
604
- async sessionCheckpointsClear(): Promise<void> {
621
+ async sessionCheckpointDelete(session_uuid: string): Promise<void> {
605
622
  const { error } = await this.client
606
623
  .from("session_checkpoints")
607
624
  .delete()
608
- .neq("session_uuid", "");
625
+ .neq("session_uuid", session_uuid);
609
626
  if (error) {
610
627
  throw error;
611
628
  }
@@ -681,7 +698,7 @@ export class Database {
681
698
  * @param teamUuid - UUID of the team
682
699
  * @returns Array of agent profiles
683
700
  */
684
- async AgentProfilesGetByTeam(teamUuid: string): Promise<SavedAgentProfile[]> {
701
+ async agentProfilesGetByTeam(teamUuid: string): Promise<SavedAgentProfile[]> {
685
702
  const { data, error } = await this.client
686
703
  .from("agent_profiles")
687
704
  .select("*")
@@ -859,7 +876,7 @@ export class Database {
859
876
  const [participants, sessions, agents] = await Promise.all([
860
877
  this.teamGetMembers(teamUuid),
861
878
  this.teamGetSessions(teamUuid),
862
- this.AgentProfilesGetByTeam(teamUuid),
879
+ this.agentProfilesGetByTeam(teamUuid),
863
880
  ]);
864
881
 
865
882
  return {
@@ -951,12 +968,10 @@ export class Database {
951
968
  * @param teamUuid - UUID of the team
952
969
  * @returns Array of session data
953
970
  */
954
- async teamGetSessions(teamUuid: string): Promise<SessionData[]> {
971
+ async teamGetSessions(teamUuid: string): Promise<SessionDescriptor[]> {
955
972
  const { data, error } = await this.client
956
973
  .from("sessions")
957
- .select(
958
- "uuid, title, agent_profile_uuid, workspace, updated_at, user_uuid"
959
- )
974
+ .select("uuid,title,agent_profile_uuid,updated_at,user_uuid,agent_paused")
960
975
  .eq("team_uuid", teamUuid);
961
976
 
962
977
  if (error) {
@@ -968,9 +983,9 @@ export class Database {
968
983
  title: session.title,
969
984
  team_uuid: teamUuid,
970
985
  agent_profile_uuid: session.agent_profile_uuid,
971
- workspace: session.workspace || undefined,
972
986
  updated_at: session.updated_at || new Date().toISOString(),
973
987
  user_uuid: session.user_uuid,
988
+ agent_paused: session.agent_paused,
974
989
  }));
975
990
  }
976
991
 
@@ -0,0 +1,91 @@
1
+ import {
2
+ IMAGE_MIME_TYPES,
3
+ EXTENSION_TO_IMAGE_MIME_TYPE,
4
+ getMimeTypeFromDataUrl,
5
+ createDataUrlFromText,
6
+ createDataUrlFromBuffer,
7
+ } from "./mimeTypes";
8
+
9
+ export const SESSION_FILE_MIME_TYPES = [
10
+ ...IMAGE_MIME_TYPES,
11
+ "text/plain",
12
+ "text/markdown",
13
+ "application/pdf",
14
+ "image/svg+xml",
15
+ ] as const;
16
+
17
+ export type SessionFileMimeType = (typeof SESSION_FILE_MIME_TYPES)[number];
18
+
19
+ export function isSessionFileMimeType(
20
+ mime_type: unknown
21
+ ): mime_type is SessionFileMimeType {
22
+ return (
23
+ typeof mime_type === "string" &&
24
+ (SESSION_FILE_MIME_TYPES as readonly string[]).includes(mime_type)
25
+ );
26
+ }
27
+
28
+ export const SESSION_FILE_TEXT_MIME_TYPES = [
29
+ "text/plain",
30
+ "text/markdown",
31
+ ] as const;
32
+
33
+ export type SessionFileTextMimeType =
34
+ (typeof SESSION_FILE_TEXT_MIME_TYPES)[number];
35
+
36
+ export function isSessionFileTextMimeType(
37
+ mime_type: unknown
38
+ ): mime_type is SessionFileTextMimeType {
39
+ return (
40
+ typeof mime_type === "string" &&
41
+ (SESSION_FILE_TEXT_MIME_TYPES as readonly string[]).includes(mime_type)
42
+ );
43
+ }
44
+
45
+ export const EXTENSION_TO_SESSION_FILE_MIME_TYPE: Record<
46
+ string,
47
+ SessionFileMimeType | undefined
48
+ > = {
49
+ ...EXTENSION_TO_IMAGE_MIME_TYPE,
50
+ svg: "image/svg+xml",
51
+ pdf: "application/pdf",
52
+ txt: "text/plain",
53
+ md: "text/markdown",
54
+ };
55
+
56
+ export type SessionFileDescriptor = {
57
+ name: string;
58
+ mime_type: SessionFileMimeType;
59
+ summary?: string;
60
+ };
61
+
62
+ export type SessionFileEntry = SessionFileDescriptor & { data_url: string };
63
+
64
+ export function getSessionFileMimeTypeFromDataUrl(
65
+ data_url: string
66
+ ): SessionFileMimeType {
67
+ const mimeType = getMimeTypeFromDataUrl(data_url);
68
+ if (!isSessionFileMimeType(mimeType)) {
69
+ throw new Error(`${mimeType} is not a supported mime type`);
70
+ }
71
+ return mimeType;
72
+ }
73
+
74
+ export function createSessionFileDataUrl(
75
+ data: string,
76
+ mime_type: SessionFileTextMimeType
77
+ ): string;
78
+ export function createSessionFileDataUrl(
79
+ data: Buffer,
80
+ mime_type: SessionFileMimeType
81
+ ): string;
82
+ export function createSessionFileDataUrl(
83
+ data: string | Buffer,
84
+ mime_type: SessionFileMimeType | SessionFileTextMimeType
85
+ ): string {
86
+ if (typeof data === "string") {
87
+ return createDataUrlFromText(data, mime_type);
88
+ } else {
89
+ return createDataUrlFromBuffer(data, mime_type);
90
+ }
91
+ }