@xalia/agent 0.5.7 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +23 -8
  2. package/dist/agent/src/agent/agent.js +176 -96
  3. package/dist/agent/src/agent/agentUtils.js +82 -59
  4. package/dist/agent/src/agent/compressingContextManager.js +102 -0
  5. package/dist/agent/src/agent/context.js +189 -0
  6. package/dist/agent/src/agent/dummyLLM.js +46 -5
  7. package/dist/agent/src/agent/mcpServerManager.js +23 -24
  8. package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
  9. package/dist/agent/src/agent/nullPlatform.js +14 -0
  10. package/dist/agent/src/agent/openAILLMStreaming.js +26 -14
  11. package/dist/agent/src/agent/promptProvider.js +63 -0
  12. package/dist/agent/src/agent/repeatLLM.js +5 -5
  13. package/dist/agent/src/agent/sudoMcpServerManager.js +23 -21
  14. package/dist/agent/src/agent/tokenAuth.js +7 -7
  15. package/dist/agent/src/agent/tools.js +1 -1
  16. package/dist/agent/src/chat/client/chatClient.js +733 -0
  17. package/dist/agent/src/chat/client/connection.js +209 -0
  18. package/dist/agent/src/chat/client/connection.test.js +188 -0
  19. package/dist/agent/src/chat/client/constants.js +5 -0
  20. package/dist/agent/src/chat/client/index.js +15 -0
  21. package/dist/agent/src/chat/client/interfaces.js +2 -0
  22. package/dist/agent/src/chat/client/responseHandler.js +105 -0
  23. package/dist/agent/src/chat/client/sessionClient.js +331 -0
  24. package/dist/agent/src/chat/client/teamManager.js +2 -0
  25. package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
  26. package/dist/agent/src/chat/data/dataModels.js +2 -0
  27. package/dist/agent/src/chat/data/database.js +749 -0
  28. package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
  29. package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
  30. package/dist/agent/src/chat/protocol/constants.js +50 -0
  31. package/dist/agent/src/chat/protocol/errors.js +22 -0
  32. package/dist/agent/src/chat/protocol/messages.js +110 -0
  33. package/dist/agent/src/chat/server/chatContextManager.js +405 -0
  34. package/dist/agent/src/chat/server/connectionManager.js +352 -0
  35. package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
  36. package/dist/agent/src/chat/server/conversation.js +198 -0
  37. package/dist/agent/src/chat/server/errorUtils.js +23 -0
  38. package/dist/agent/src/chat/server/openSession.js +869 -0
  39. package/dist/agent/src/chat/server/server.js +177 -0
  40. package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
  41. package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
  42. package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
  43. package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
  44. package/dist/agent/src/chat/server/tools.js +243 -0
  45. package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
  46. package/dist/agent/src/chat/utils/approvalManager.js +85 -0
  47. package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
  48. package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
  49. package/dist/agent/src/chat/utils/htmlToText.js +84 -0
  50. package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
  51. package/dist/agent/src/chat/utils/search.js +145 -0
  52. package/dist/agent/src/chat/utils/userResolver.js +46 -0
  53. package/dist/agent/src/chat/utils/websocket.js +16 -0
  54. package/dist/agent/src/test/agent.test.js +332 -0
  55. package/dist/agent/src/test/approvalManager.test.js +58 -0
  56. package/dist/agent/src/test/chatContextManager.test.js +392 -0
  57. package/dist/agent/src/test/clientServerConnection.test.js +158 -0
  58. package/dist/agent/src/test/compressingContextManager.test.js +65 -0
  59. package/dist/agent/src/test/context.test.js +83 -0
  60. package/dist/agent/src/test/conversation.test.js +89 -0
  61. package/dist/agent/src/test/db.test.js +271 -83
  62. package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
  63. package/dist/agent/src/test/dbTestTools.js +99 -0
  64. package/dist/agent/src/test/imageLoad.test.js +8 -7
  65. package/dist/agent/src/test/mcpServerManager.test.js +23 -20
  66. package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
  67. package/dist/agent/src/test/openaiStreaming.test.js +64 -35
  68. package/dist/agent/src/test/prompt.test.js +5 -4
  69. package/dist/agent/src/test/promptProvider.test.js +28 -0
  70. package/dist/agent/src/test/responseHandler.test.js +61 -0
  71. package/dist/agent/src/test/sudoMcpServerManager.test.js +24 -25
  72. package/dist/agent/src/test/testTools.js +109 -0
  73. package/dist/agent/src/test/tools.test.js +31 -0
  74. package/dist/agent/src/tool/agentChat.js +21 -10
  75. package/dist/agent/src/tool/agentMain.js +1 -1
  76. package/dist/agent/src/tool/chatMain.js +241 -58
  77. package/dist/agent/src/tool/commandPrompt.js +22 -17
  78. package/dist/agent/src/tool/files.js +20 -16
  79. package/dist/agent/src/tool/nodePlatform.js +47 -3
  80. package/dist/agent/src/tool/options.js +4 -4
  81. package/dist/agent/src/tool/prompt.js +19 -13
  82. package/eslint.config.mjs +14 -1
  83. package/package.json +14 -6
  84. package/scripts/chat_server +8 -0
  85. package/scripts/setup_chat +7 -2
  86. package/scripts/shutdown_chat_server +3 -0
  87. package/scripts/test_chat +135 -17
  88. package/src/agent/agent.ts +283 -138
  89. package/src/agent/agentUtils.ts +143 -108
  90. package/src/agent/compressingContextManager.ts +164 -0
  91. package/src/agent/context.ts +268 -0
  92. package/src/agent/dummyLLM.ts +76 -8
  93. package/src/agent/iAgentEventHandler.ts +54 -0
  94. package/src/agent/iplatform.ts +1 -0
  95. package/src/agent/mcpServerManager.ts +35 -31
  96. package/src/agent/nullAgentEventHandler.ts +20 -0
  97. package/src/agent/nullPlatform.ts +13 -0
  98. package/src/agent/openAILLMStreaming.ts +26 -13
  99. package/src/agent/promptProvider.ts +87 -0
  100. package/src/agent/repeatLLM.ts +5 -5
  101. package/src/agent/sudoMcpServerManager.ts +30 -29
  102. package/src/agent/tokenAuth.ts +7 -7
  103. package/src/agent/tools.ts +3 -1
  104. package/src/chat/client/chatClient.ts +900 -0
  105. package/src/chat/client/connection.test.ts +241 -0
  106. package/src/chat/client/connection.ts +276 -0
  107. package/src/chat/client/constants.ts +3 -0
  108. package/src/chat/client/index.ts +18 -0
  109. package/src/chat/client/interfaces.ts +34 -0
  110. package/src/chat/client/responseHandler.ts +131 -0
  111. package/src/chat/client/sessionClient.ts +443 -0
  112. package/src/chat/client/teamManager.ts +29 -0
  113. package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
  114. package/src/chat/data/dataModels.ts +85 -0
  115. package/src/chat/data/database.ts +982 -0
  116. package/src/chat/data/dbMcpServerConfigs.ts +59 -0
  117. package/src/chat/protocol/connectionMessages.ts +49 -0
  118. package/src/chat/protocol/constants.ts +55 -0
  119. package/src/chat/protocol/errors.ts +16 -0
  120. package/src/chat/protocol/messages.ts +682 -0
  121. package/src/chat/server/README.md +127 -0
  122. package/src/chat/server/chatContextManager.ts +612 -0
  123. package/src/chat/server/connectionManager.test.ts +266 -0
  124. package/src/chat/server/connectionManager.ts +541 -0
  125. package/src/chat/server/conversation.ts +269 -0
  126. package/src/chat/server/errorUtils.ts +28 -0
  127. package/src/chat/server/openSession.ts +1332 -0
  128. package/src/chat/server/server.ts +177 -0
  129. package/src/chat/server/sessionFileManager.ts +239 -0
  130. package/src/chat/server/sessionRegistry.test.ts +138 -0
  131. package/src/chat/server/sessionRegistry.ts +1064 -0
  132. package/src/chat/server/test-utils/mockFactories.ts +422 -0
  133. package/src/chat/server/tools.ts +265 -0
  134. package/src/chat/utils/agentSessionMap.ts +76 -0
  135. package/src/chat/utils/approvalManager.ts +111 -0
  136. package/src/{utils → chat/utils}/asyncLock.ts +3 -3
  137. package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
  138. package/src/chat/utils/htmlToText.ts +61 -0
  139. package/src/chat/utils/multiAsyncQueue.ts +52 -0
  140. package/src/chat/utils/search.ts +139 -0
  141. package/src/chat/utils/userResolver.ts +48 -0
  142. package/src/chat/utils/websocket.ts +16 -0
  143. package/src/test/agent.test.ts +487 -0
  144. package/src/test/approvalManager.test.ts +73 -0
  145. package/src/test/chatContextManager.test.ts +521 -0
  146. package/src/test/clientServerConnection.test.ts +207 -0
  147. package/src/test/compressingContextManager.test.ts +82 -0
  148. package/src/test/context.test.ts +105 -0
  149. package/src/test/conversation.test.ts +109 -0
  150. package/src/test/db.test.ts +358 -89
  151. package/src/test/dbMcpServerConfigs.test.ts +112 -0
  152. package/src/test/dbTestTools.ts +153 -0
  153. package/src/test/imageLoad.test.ts +7 -6
  154. package/src/test/mcpServerManager.test.ts +21 -16
  155. package/src/test/multiAsyncQueue.test.ts +125 -0
  156. package/src/test/openaiStreaming.test.ts +71 -36
  157. package/src/test/prompt.test.ts +4 -3
  158. package/src/test/promptProvider.test.ts +33 -0
  159. package/src/test/responseHandler.test.ts +78 -0
  160. package/src/test/sudoMcpServerManager.test.ts +32 -30
  161. package/src/test/testTools.ts +146 -0
  162. package/src/test/tools.test.ts +39 -0
  163. package/src/tool/agentChat.ts +26 -12
  164. package/src/tool/agentMain.ts +1 -1
  165. package/src/tool/chatMain.ts +292 -100
  166. package/src/tool/commandPrompt.ts +28 -19
  167. package/src/tool/files.ts +25 -19
  168. package/src/tool/nodePlatform.ts +52 -3
  169. package/src/tool/options.ts +4 -2
  170. package/src/tool/prompt.ts +22 -15
  171. package/test_data/dummyllm_script_crash.json +32 -0
  172. package/test_data/frog.png.b64 +1 -0
  173. package/vitest.config.ts +39 -0
  174. package/dist/agent/src/chat/client.js +0 -349
  175. package/dist/agent/src/chat/conversationManager.js +0 -392
  176. package/dist/agent/src/chat/db.js +0 -209
  177. package/dist/agent/src/chat/frontendClient.js +0 -74
  178. package/dist/agent/src/chat/server.js +0 -158
  179. package/src/chat/client.ts +0 -455
  180. package/src/chat/conversationManager.ts +0 -595
  181. package/src/chat/db.ts +0 -290
  182. package/src/chat/frontendClient.ts +0 -123
  183. package/src/chat/messages.ts +0 -235
  184. package/src/chat/server.ts +0 -177
  185. /package/dist/agent/src/{chat/messages.js → agent/iAgentEventHandler.js} +0 -0
  186. /package/{frog.png → test_data/frog.png} +0 -0
@@ -0,0 +1,153 @@
1
+ import assert from "assert";
2
+ import { AgentProfile } from "../agent/agent";
3
+ import {
4
+ Database,
5
+ SUPABASE_LOCAL_KEY,
6
+ SUPABASE_LOCAL_URL,
7
+ } from "../chat/data/database";
8
+
9
+ export function getLocalDB(): Database {
10
+ return new Database(SUPABASE_LOCAL_URL, SUPABASE_LOCAL_KEY);
11
+ }
12
+
13
+ export const AGENT_PROFILE: AgentProfile = {
14
+ model: undefined,
15
+ system_prompt: "You are an unhelpful agent",
16
+ mcp_settings: {},
17
+ };
18
+
19
+ export const AGENT_PROFILE_2: AgentProfile = {
20
+ model: undefined,
21
+ system_prompt: "You are a very helpful agent.",
22
+ mcp_settings: { simplecalc: [] },
23
+ };
24
+
25
+ /// User description
26
+ export type TestUserSpec = {
27
+ type: "spec";
28
+ uuid: string;
29
+ email: string;
30
+ name: string;
31
+ api_key: string;
32
+ };
33
+
34
+ /// User created in the DB
35
+ export type TestUser = {
36
+ uuid: string;
37
+ email: string;
38
+ name: string;
39
+ api_key: string;
40
+ };
41
+
42
+ let counter = 1;
43
+
44
+ export function randomString(length: number = 16): string {
45
+ const CHARS =
46
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
47
+ let result = "";
48
+ const array = new Uint8Array(length);
49
+ crypto.getRandomValues(array);
50
+ for (let i = 0; i < length; i++) {
51
+ result += CHARS[array[i] % CHARS.length];
52
+ }
53
+ return result;
54
+ }
55
+
56
+ export function testUserSpec(): TestUserSpec {
57
+ const uuid = randomString() + String(counter++);
58
+ const name = `user_${uuid}`;
59
+ const email = `${name}@host.com`;
60
+ const api_key = `apikey_${uuid}`;
61
+ return { type: "spec", uuid, name, email, api_key };
62
+ }
63
+
64
+ // Test user data that gets reset for each test
65
+ export async function createTestUser(
66
+ db: Database,
67
+ user?: TestUserSpec
68
+ ): Promise<TestUser> {
69
+ user = user || testUserSpec();
70
+ await db.createUser(user.uuid, user.email, user.name);
71
+ await db.addApiKey(user.uuid, user.api_key, "default", [], true);
72
+ return user;
73
+ }
74
+
75
+ export async function cleanupTestUser(
76
+ db: Database,
77
+ user: TestUser
78
+ // api_key: string
79
+ ): Promise<void> {
80
+ try {
81
+ await db.getClientForTesting().from("users").delete().eq("uuid", user.uuid);
82
+ } catch (_error) {
83
+ // Ignore cleanup errors
84
+ }
85
+ }
86
+
87
+ export async function createTestSession(
88
+ db: Database,
89
+ user1: TestUserSpec
90
+ ): Promise<{
91
+ agentProfileId: string;
92
+ sessionId: string;
93
+ }> {
94
+ await createTestUser(db, user1);
95
+
96
+ const agentProfile = await db.createAgentProfile(
97
+ user1.uuid,
98
+ undefined,
99
+ "test_profile",
100
+ AGENT_PROFILE
101
+ );
102
+ assert(agentProfile);
103
+
104
+ const sessionId = Database.sessionNewUUID();
105
+ await db.sessionCreate({
106
+ session_uuid: sessionId,
107
+ user_uuid: user1.uuid,
108
+ title: "test_session",
109
+ agent_profile_uuid: agentProfile.uuid,
110
+ });
111
+ assert(sessionId);
112
+
113
+ return { agentProfileId: agentProfile.uuid, sessionId };
114
+ }
115
+
116
+ export async function createTestTeamSession(
117
+ db: Database,
118
+ teamUuid: string,
119
+ testUsers: TestUserSpec[]
120
+ ): Promise<{
121
+ agentProfileId: string;
122
+ sessionId: string;
123
+ teamId: string;
124
+ }> {
125
+ const [user1, _] = await Promise.all([
126
+ createTestUser(db, testUsers[0]),
127
+ createTestUser(db, testUsers[1]),
128
+ ]);
129
+
130
+ const agentProfile = await db.createAgentProfile(
131
+ user1.uuid,
132
+ undefined,
133
+ // teamUuid,
134
+ "test_profile",
135
+ AGENT_PROFILE
136
+ );
137
+ assert(agentProfile);
138
+
139
+ const teamId = await db.createTeam(teamUuid, user1.uuid);
140
+ assert(teamId);
141
+
142
+ const sessionId = Database.sessionNewUUID();
143
+ await db.sessionCreate({
144
+ session_uuid: sessionId,
145
+ team_uuid: teamId,
146
+ user_uuid: user1.uuid,
147
+ title: "test_session",
148
+ agent_profile_uuid: agentProfile.uuid,
149
+ });
150
+ assert(sessionId);
151
+
152
+ return { agentProfileId: agentProfile.uuid, sessionId, teamId };
153
+ }
@@ -1,14 +1,15 @@
1
- import { expect } from "chai";
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ import { describe, it, expect } from "vitest";
2
3
  import { loadImageB64OrUndefined } from "../tool/files";
3
4
  import { strict as assert } from "assert";
4
5
 
5
6
  describe("Image loading", () => {
6
- it("convert to data url", async function () {
7
- const imageB64 = loadImageB64OrUndefined("frog.png");
8
- // console.log(`imageB64: ${imageB64}`);
7
+ it("convert to data url", async () => {
8
+ const imageB64 = loadImageB64OrUndefined("test_data/frog.png");
9
+ console.log(`imageB64: ${imageB64 || ""}`);
9
10
 
10
- expect(imageB64).to.be.a("string");
11
+ expect(imageB64).toBeTypeOf("string");
11
12
  assert(typeof imageB64 === "string");
12
- expect(imageB64.startsWith("data:image/png;base64")).eql(true);
13
+ expect(imageB64.startsWith("data:image/png;base64")).toBe(true);
13
14
  });
14
15
  });
@@ -1,4 +1,5 @@
1
- import { expect } from "chai";
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ import { describe, it, expect, afterAll } from "vitest";
2
3
  import {
3
4
  computeQualifiedName,
4
5
  McpServerManager,
@@ -12,7 +13,7 @@ let idx = 0;
12
13
 
13
14
  function getMcpServerManager(): McpServerManager {
14
15
  const tm = new McpServerManager();
15
- const name = "mgr" + ++idx;
16
+ const name = "mgr" + String(++idx);
16
17
  managers[name] = tm;
17
18
  return tm;
18
19
  }
@@ -25,10 +26,12 @@ async function shutdownAll() {
25
26
  managers = {};
26
27
  }
27
28
 
28
- describe("McpServerManager", async () => {
29
+ describe("McpServerManager", () => {
30
+ afterAll(shutdownAll);
31
+
29
32
  it("should initialize with correct descriptions", async (): Promise<void> => {
30
33
  const tm = getMcpServerManager();
31
- await tm.addMcpServer(
34
+ await tm.addMcpServerWithSSEUrl(
32
35
  "simplecalc",
33
36
  "http://localhost:5001/mcpservers/sudomcp/simplecalc/session",
34
37
  "dummy_key"
@@ -36,17 +39,17 @@ describe("McpServerManager", async () => {
36
39
 
37
40
  const server = tm.getMcpServer("simplecalc");
38
41
  const availableTools = server.getTools();
39
- expect(availableTools.length).greaterThan(0);
42
+ expect(availableTools.length).toBeGreaterThan(0);
40
43
 
41
44
  // Initially, no tools are enabled.
42
- expect(tm.getOpenAITools().length).equal(0);
45
+ expect(tm.getOpenAITools().length).toBe(0);
43
46
 
44
47
  // Enable a tool and check we get a description for it.
45
48
  const toolName = availableTools[0].name;
46
49
  {
47
50
  tm.enableTool("simplecalc", toolName);
48
51
  const tools = tm.getOpenAITools();
49
- expect(tools.length).greaterThan(0);
52
+ expect(tools.length).toBeGreaterThan(0);
50
53
  console.log(`tools: ${JSON.stringify(tools, undefined, 2)}`);
51
54
  }
52
55
 
@@ -61,9 +64,9 @@ describe("McpServerManager", async () => {
61
64
  {
62
65
  tm.disableTool("simplecalc", toolName);
63
66
  const tools = tm.getOpenAITools();
64
- expect(tools.length).equal(0);
67
+ expect(tools.length).toBe(0);
65
68
  }
66
- }).timeout(10000);
69
+ }, 10000);
67
70
 
68
71
  it("qualified names", async () => {
69
72
  const serverName = "server";
@@ -72,27 +75,29 @@ describe("McpServerManager", async () => {
72
75
 
73
76
  const [_serverName, _toolName] = splitQualifiedName(qualifiedName);
74
77
 
75
- expect(_serverName).eql(serverName);
76
- expect(_toolName).eql(toolName);
78
+ expect(_serverName).toBe(serverName);
79
+ expect(_toolName).toBe(toolName);
77
80
  });
78
81
 
79
82
  it("add / remove servers", async () => {
80
83
  const tm = getMcpServerManager();
81
84
 
82
- await tm.addMcpServer(
85
+ await tm.addMcpServerWithSSEUrl(
83
86
  "simplecalc",
84
87
  "http://localhost:5001/mcpservers/sudomcp/simplecalc/session",
85
88
  "dummy_key"
86
89
  );
87
90
  tm.enableAllTools("simplecalc");
88
- expect(tm.getOpenAITools().length).greaterThan(0);
91
+ expect(tm.getOpenAITools().length).toBeGreaterThan(0);
89
92
 
90
93
  // Remove server and check there is no server, and no openai entries.
91
94
 
92
95
  await tm.removeMcpServer("simplecalc");
93
- expect(tm.getOpenAITools().length).equal(0);
96
+ expect(tm.getOpenAITools().length).toBe(0);
94
97
  expect(() => {
95
98
  tm.getMcpServer("simplecalc");
96
- }).throws();
99
+ }).toThrow();
97
100
  });
98
- }).afterAll(shutdownAll);
101
+ });
102
+
103
+ afterAll(shutdownAll);
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { MultiAsyncQueue } from "../chat/utils/multiAsyncQueue";
3
+
4
+ describe("MultiAsyncQueue", () => {
5
+ it("should process all waiting elements in one go", async () => {
6
+ // Place a 1 on the queue. Inside the process fn, place a 2 and 3 on the
7
+ // queue. Next invocation of process should see BOTH 2 AND 3.
8
+
9
+ const context = { q: undefined as MultiAsyncQueue<number> | undefined };
10
+ const seen: number[] = [];
11
+ const process = (elements: number[]): Promise<void> => {
12
+ console.log(`elements: ${JSON.stringify(elements)}`);
13
+ expect(context.q).toBeDefined();
14
+
15
+ for (const e of elements) {
16
+ seen.push(e);
17
+ }
18
+ if (seen.length === 1) {
19
+ expect(seen[0]).toBe(1);
20
+ expect(context.q?.tryEnqueue(2)).toBe(true);
21
+ expect(context.q?.tryEnqueue(3)).toBe(true);
22
+ } else if (seen.length === 3) {
23
+ expect(seen[1]).toBe(2);
24
+ expect(seen[2]).toBe(3);
25
+ } else {
26
+ throw new Error("unexpected elements");
27
+ }
28
+
29
+ return Promise.resolve();
30
+ };
31
+
32
+ context.q = new MultiAsyncQueue<number>(process);
33
+ expect(context.q).toBeDefined();
34
+ expect(context.q.tryEnqueue(1)).toBe(true);
35
+
36
+ await new Promise<void>((r) => {
37
+ setTimeout(() => {
38
+ expect(seen).toEqual([1, 2, 3]);
39
+ r();
40
+ }, 100);
41
+ });
42
+ });
43
+
44
+ it("should reject items when queue is full", async () => {
45
+ let processCount = 0;
46
+ let resolveProcess: (() => void) | undefined;
47
+
48
+ // Create a process function that blocks until we resolve it
49
+ const process = async (_elements: number[]): Promise<void> => {
50
+ processCount++;
51
+ return new Promise((resolve) => {
52
+ resolveProcess = resolve;
53
+ });
54
+ };
55
+
56
+ // Create queue with maxBacklog of 2
57
+ const queue = new MultiAsyncQueue<number>(process, 2);
58
+
59
+ // First item triggers (async) processing
60
+ expect(queue.tryEnqueue(1)).toBe(true);
61
+ expect(queue.getLength()).toBe(1); // Item still there until we yield
62
+ await new Promise((r) => setTimeout(r, 1));
63
+ expect(queue.getLength()).toBe(0); // Item moved to processing
64
+
65
+ // Add items up to backlog limit while first batch is processing
66
+ expect(queue.tryEnqueue(2)).toBe(true);
67
+ expect(queue.getLength()).toBe(1);
68
+ expect(queue.tryEnqueue(3)).toBe(true);
69
+ expect(queue.getLength()).toBe(2);
70
+
71
+ // Should reject when at max capacity
72
+ expect(queue.tryEnqueue(4)).toBe(false);
73
+ expect(queue.getLength()).toBe(2);
74
+
75
+ // Resolve the first batch processing
76
+ expect(resolveProcess).toBeDefined();
77
+ resolveProcess?.();
78
+
79
+ // Wait for next batch to start
80
+ await new Promise((r) => setTimeout(r, 10));
81
+
82
+ // Now queue should have processed the first batch and be working on [2, 3]
83
+ expect(processCount).toBe(2);
84
+
85
+ // Should be able to add more items now
86
+ expect(queue.tryEnqueue(5)).toBe(true);
87
+
88
+ // Clean up by resolving the second batch
89
+ expect(resolveProcess).toBeDefined();
90
+ resolveProcess?.();
91
+ });
92
+
93
+ it("should handle empty queue correctly", async () => {
94
+ const processedBatches: number[][] = [];
95
+
96
+ const process = (elements: number[]): Promise<void> => {
97
+ processedBatches.push([...elements]);
98
+ return Promise.resolve();
99
+ };
100
+
101
+ const queue = new MultiAsyncQueue<number>(process);
102
+
103
+ // Queue starts empty
104
+ expect(queue.getLength()).toBe(0);
105
+
106
+ // Enqueue and process one item
107
+ expect(queue.tryEnqueue(1)).toBe(true);
108
+
109
+ await new Promise((r) => setTimeout(r, 10));
110
+
111
+ expect(processedBatches).toHaveLength(1);
112
+ expect(processedBatches[0]).toEqual([1]);
113
+
114
+ // Queue should be empty again
115
+ expect(queue.getLength()).toBe(0);
116
+
117
+ // Should be able to enqueue again
118
+ expect(queue.tryEnqueue(2)).toBe(true);
119
+
120
+ await new Promise((r) => setTimeout(r, 10));
121
+
122
+ expect(processedBatches).toHaveLength(2);
123
+ expect(processedBatches[1]).toEqual([2]);
124
+ });
125
+ });
@@ -1,4 +1,5 @@
1
- import { expect } from "chai";
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ import { describe, it, expect } from "vitest";
2
3
 
3
4
  import { OpenAI } from "openai";
4
5
  import {
@@ -6,7 +7,9 @@ import {
6
7
  updateCompletion,
7
8
  } from "../agent/openAILLMStreaming";
8
9
 
9
- const TEST_STANDARD: OpenAI.Chat.Completions.ChatCompletionChunk[] = [
10
+ type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
11
+
12
+ const TEST_STANDARD: ChatCompletionChunk[] = [
10
13
  {
11
14
  id: "chatcmpl-BzBqzVs5w0kU5we3KIUN1Q4FAZXjg",
12
15
  choices: [
@@ -34,7 +37,7 @@ const TEST_STANDARD: OpenAI.Chat.Completions.ChatCompletionChunk[] = [
34
37
  },
35
38
  ];
36
39
 
37
- const TEST_TRAILING_USAGE: OpenAI.Chat.Completions.ChatCompletionChunk[] = [
40
+ const TEST_TRAILING_USAGE: ChatCompletionChunk[] = [
38
41
  {
39
42
  id: "gen-1753923406-nsIKHyFRoJqkUntBnQTw",
40
43
  choices: [
@@ -73,14 +76,63 @@ const TEST_TRAILING_USAGE: OpenAI.Chat.Completions.ChatCompletionChunk[] = [
73
76
  },
74
77
  ];
75
78
 
79
+ const AGGREGATED_MESSAGE: OpenAI.Chat.Completions.ChatCompletion = {
80
+ id: "gen-1753923406-nsIKHyFRoJqkUntBnQTw",
81
+ choices: [
82
+ {
83
+ message: {
84
+ content: "test",
85
+ role: "assistant",
86
+ refusal: null,
87
+ tool_calls: undefined,
88
+ },
89
+ finish_reason: "stop",
90
+ index: 0,
91
+ logprobs: null,
92
+ },
93
+ ],
94
+ created: 1753923406,
95
+ model: "openai/gpt-4o",
96
+ object: "chat.completion",
97
+ service_tier: undefined,
98
+ system_fingerprint: "fp_a288987b44",
99
+ usage: {
100
+ completion_tokens: 50,
101
+ prompt_tokens: 271,
102
+ total_tokens: 321,
103
+ completion_tokens_details: { reasoning_tokens: 0 },
104
+ prompt_tokens_details: { cached_tokens: 0 },
105
+ },
106
+ };
107
+
108
+ const TEST_TRAILING_USAGE_WITH_3_CHUNKS: ChatCompletionChunk[] = [
109
+ {
110
+ ...TEST_TRAILING_USAGE[0],
111
+ choices: [
112
+ {
113
+ delta: { content: "test", role: "assistant" },
114
+ finish_reason: null,
115
+ index: 0,
116
+ logprobs: null,
117
+ },
118
+ ],
119
+ },
120
+ {
121
+ ...TEST_TRAILING_USAGE[1],
122
+ choices: [{ ...TEST_TRAILING_USAGE[1].choices[0], finish_reason: "stop" }],
123
+ usage: undefined,
124
+ },
125
+ { ...TEST_TRAILING_USAGE[1], choices: [] },
126
+ ];
127
+
76
128
  describe("OpenAI Streaming", () => {
77
- it("should support standard termination", async function () {
129
+ it("should support standard termination", async () => {
78
130
  const chunks = TEST_STANDARD;
79
- expect(chunks.length).eql(2);
131
+ expect(chunks.length).toBe(2);
80
132
  const { initMessage } = initializeCompletion(chunks[0]);
81
133
  updateCompletion(initMessage, chunks[1]);
82
134
 
83
- expect(initMessage).eql({
135
+ expect(initMessage).toEqual({
84
136
  id: "chatcmpl-BzBqzVs5w0kU5we3KIUN1Q4FAZXjg",
85
137
  choices: [
86
138
  {
@@ -104,39 +156,22 @@ describe("OpenAI Streaming", () => {
104
156
  });
105
157
  });
106
158
 
107
- it("should support trailing usage", async function () {
159
+ it("should support trailing usage", async () => {
108
160
  const chunks = TEST_TRAILING_USAGE;
109
- expect(chunks.length).eql(2);
161
+ expect(chunks.length).toBe(2);
110
162
  const { initMessage } = initializeCompletion(chunks[0]);
111
163
  updateCompletion(initMessage, chunks[1]);
112
164
 
113
- expect(initMessage).eql({
114
- id: "gen-1753923406-nsIKHyFRoJqkUntBnQTw",
115
- choices: [
116
- {
117
- message: {
118
- content: "test",
119
- role: "assistant",
120
- refusal: null,
121
- tool_calls: undefined,
122
- },
123
- finish_reason: "stop",
124
- index: 0,
125
- logprobs: null,
126
- },
127
- ],
128
- created: 1753923406,
129
- model: "openai/gpt-4o",
130
- object: "chat.completion",
131
- service_tier: undefined,
132
- system_fingerprint: "fp_a288987b44",
133
- usage: {
134
- completion_tokens: 50,
135
- prompt_tokens: 271,
136
- total_tokens: 321,
137
- completion_tokens_details: { reasoning_tokens: 0 },
138
- prompt_tokens_details: { cached_tokens: 0 },
139
- },
140
- });
165
+ expect(initMessage).toEqual(AGGREGATED_MESSAGE);
166
+ });
167
+
168
+ it("should support trailing usage with 3 chunks", async () => {
169
+ const chunks = TEST_TRAILING_USAGE_WITH_3_CHUNKS;
170
+ expect(chunks.length).toBe(3);
171
+ const { initMessage } = initializeCompletion(chunks[0]);
172
+ updateCompletion(initMessage, chunks[1]);
173
+ updateCompletion(initMessage, chunks[2]);
174
+
175
+ expect(initMessage).toEqual(AGGREGATED_MESSAGE);
141
176
  });
142
177
  });
@@ -1,8 +1,9 @@
1
- import { expect } from "chai";
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ import { describe, it, expect } from "vitest";
2
3
  import { parsePrompt } from "../tool/commandPrompt";
3
4
 
4
5
  describe("Prompt", () => {
5
- it("should recognise commands", async function () {
6
+ it("should recognise commands", async () => {
6
7
  const expectedResults = {
7
8
  "some text ": { msg: "some text", cmds: undefined },
8
9
  ":i image.png some text": { msg: "some text", cmds: ["i", "image.png"] },
@@ -20,7 +21,7 @@ describe("Prompt", () => {
20
21
 
21
22
  for (const [prompt, expected] of Object.entries(expectedResults)) {
22
23
  const res = parsePrompt(prompt);
23
- expect(res).eql(expected);
24
+ expect(res).toEqual(expected);
24
25
  }
25
26
  });
26
27
  });
@@ -0,0 +1,33 @@
1
+ import { expect } from "vitest";
2
+ import { SystemPromptProvider } from "../agent/promptProvider";
3
+
4
+ describe("SystemPromptProvider", () => {
5
+ it("correctly includes all components", function () {
6
+ SystemPromptProvider.setGlobalPrompt("global");
7
+ const pp = new SystemPromptProvider("agent");
8
+ pp.setFragment("frag1", "FRAG1");
9
+
10
+ expect(pp.getSystemPrompt()).eql("global\nagent\nFRAG1");
11
+ });
12
+
13
+ it("handles update/remove of components", function () {
14
+ SystemPromptProvider.setGlobalPrompt("global");
15
+ const pp = new SystemPromptProvider("agent");
16
+ pp.setFragment("frag1", "FRAG1");
17
+
18
+ // Update system and frag1. Add frag2, frag3. Remove frag3.
19
+
20
+ pp.setAgentPrompt("agent2");
21
+ pp.setFragment("frag1", "FRAG11");
22
+ pp.setFragment("frag2", "FRAG2");
23
+ pp.setFragment("frag3", "FRAG3");
24
+ pp.removeFragment("frag2");
25
+
26
+ // Result should contain system, frag1, frag3. NOT frag2.
27
+
28
+ expect(pp.getSystemPrompt().startsWith("global\nagent2\n")).eql(true);
29
+ expect(pp.getSystemPrompt().includes("FRAG11")).eql(true);
30
+ expect(pp.getSystemPrompt().includes("FRAG3")).eql(true);
31
+ expect(pp.getSystemPrompt().includes("FRAG2")).eql(false);
32
+ });
33
+ });
@@ -0,0 +1,78 @@
1
+ import { expect } from "vitest";
2
+ import { ResponseHandler } from "../chat/client/responseHandler";
3
+
4
+ type ClientA = { type: "a"; client_message_id: string; value: number };
5
+ type ClientB = { type: "b"; client_message_id: string; data: string };
6
+ type ClientMsg = ClientA | ClientB;
7
+
8
+ type ServerA = { type: "a"; client_message_id: string; value: number };
9
+ type ServerB = { type: "b"; data: string };
10
+ type ServerErr = { type: "err"; client_message_id: string; message: string };
11
+ type ServerMsg = ServerA | ServerB | ServerErr;
12
+
13
+ describe("ResponseHandler", () => {
14
+ const req: ClientMsg = {
15
+ type: "a",
16
+ client_message_id: "id_a",
17
+ value: 3,
18
+ };
19
+ const response: ServerMsg = {
20
+ type: "a",
21
+ client_message_id: "id_a",
22
+ value: 4,
23
+ };
24
+ const otherResponse: ServerMsg = {
25
+ type: "a",
26
+ client_message_id: "id_b",
27
+ value: 4,
28
+ };
29
+ const error: ServerMsg = {
30
+ type: "err",
31
+ client_message_id: "id_a",
32
+ message: "error",
33
+ };
34
+
35
+ it("should resolve", async function () {
36
+ const rh = new ResponseHandler<ClientMsg, ServerMsg>("err", 100);
37
+ const responseP = rh.waitForResponse(req);
38
+ rh.onMessage(otherResponse);
39
+ rh.onMessage(response);
40
+ rh.onMessage(error);
41
+
42
+ expect(await responseP).eql(response);
43
+
44
+ // This (late) messages should be ignored.
45
+ rh.onMessage(otherResponse);
46
+ rh.onMessage(error);
47
+ rh.onMessage(response);
48
+ });
49
+
50
+ it("should handle errors", async function () {
51
+ const rh = new ResponseHandler<ClientMsg, ServerMsg>("err", 100);
52
+ const responseP = rh.waitForResponse(req);
53
+ rh.onMessage(otherResponse);
54
+ rh.onMessage(error);
55
+ rh.onMessage(response);
56
+
57
+ await expect(responseP).rejects.toThrow(error.message);
58
+
59
+ // This (late) messages should be ignored.
60
+ rh.onMessage(otherResponse);
61
+ rh.onMessage(error);
62
+ rh.onMessage(response);
63
+ });
64
+
65
+ it("should handle timeouts", async function () {
66
+ const rh = new ResponseHandler<ClientMsg, ServerMsg>("err", 100);
67
+ const responseP = rh.waitForResponse(req);
68
+ rh.onMessage(otherResponse);
69
+ rh.onMessage({ type: "a", client_message_id: "id_b", value: 4 });
70
+
71
+ await expect(responseP).rejects.toThrow();
72
+
73
+ // This (late) messages should be ignored.
74
+ rh.onMessage(otherResponse);
75
+ rh.onMessage(error);
76
+ rh.onMessage(response);
77
+ });
78
+ });