@xalia/agent 0.6.1 → 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,258 @@
1
+ import { expect } from "chai";
2
+
3
+ import { Database } from "../chat/data/database";
4
+ import { DbSessionFiles } from "../chat/data/dbSessionFiles";
5
+ import {
6
+ SessionFileDescriptor,
7
+ SessionFileEntry,
8
+ } from "../chat/data/dbSessionFileModels";
9
+ import {
10
+ TestUserSpec,
11
+ cleanupTestUser,
12
+ createTestSession,
13
+ getLocalDB,
14
+ testUserSpec,
15
+ } from "./dbTestTools";
16
+ import { getMimeTypeFromDataUrl } from "../chat/data/mimeTypes";
17
+
18
+ const TEST_FILES_A: SessionFileEntry[] = [
19
+ {
20
+ name: "file_1",
21
+ summary: "Some PDF",
22
+ mime_type: "application/pdf",
23
+ data_url: "data:application/pdf;base64,AAAA",
24
+ },
25
+ {
26
+ name: "file_2",
27
+ summary: "Some PNG",
28
+ mime_type: "image/png",
29
+ data_url: "data:image/png;base64,BBBB",
30
+ },
31
+ ];
32
+
33
+ const TEST_FILE_A_1_ALT: SessionFileEntry = {
34
+ ...TEST_FILES_A[0],
35
+ summary: "Some other PDF",
36
+ data_url: "data:application/pdf;base64,CCCC",
37
+ };
38
+
39
+ const TEST_FILE_A_3_NEW: SessionFileEntry = {
40
+ name: "file_3",
41
+ summary: "Some markdown",
42
+ mime_type: "text/markdown",
43
+ data_url: "data:text/markdown,# TITLE\n## SUBSECTION\nA parapgraph\n",
44
+ };
45
+
46
+ const TEST_FILES_B: SessionFileEntry[] = [
47
+ {
48
+ name: "file_1",
49
+ summary: "Some other PDF",
50
+ mime_type: "application/pdf",
51
+ data_url: "data:application/pdf;base64,DDD",
52
+ },
53
+ ];
54
+
55
+ function entryToDescriptor(entry: SessionFileEntry): SessionFileDescriptor {
56
+ return {
57
+ name: entry.name,
58
+ summary: entry.summary,
59
+ mime_type: entry.mime_type,
60
+ };
61
+ }
62
+
63
+ // async function cleanup(
64
+ // db: Database,
65
+ // user1: TestUserSpec
66
+ // // user2: TestUser
67
+ // ): Promise<void> {
68
+ // await cleanupTestUser(db, user1);
69
+ // // await cleanupTestUser(db, user2.uuid, user2.api_key);
70
+ // }
71
+
72
+ async function setup(
73
+ db: Database,
74
+ user1: TestUserSpec
75
+ ): Promise<{
76
+ sf: DbSessionFiles;
77
+ session_id_A: string;
78
+ session_id_B: string;
79
+ }> {
80
+ const { agentProfileId, sessionId: session_id_A } = await createTestSession(
81
+ db,
82
+ user1
83
+ );
84
+
85
+ const session_id_B = Database.sessionNewUUID();
86
+ await db.sessionCreate({
87
+ session_uuid: session_id_B,
88
+ user_uuid: user1.uuid,
89
+ title: "test_session_2",
90
+ agent_profile_uuid: agentProfileId,
91
+ agent_paused: false,
92
+ });
93
+
94
+ const sf = db.createTypedClient(DbSessionFiles);
95
+
96
+ await Promise.all(
97
+ TEST_FILES_A.map((tf) =>
98
+ sf.setFileContent(session_id_A, tf.name, tf.summary, tf.data_url)
99
+ )
100
+ );
101
+ await Promise.all(
102
+ TEST_FILES_B.map((tf) =>
103
+ sf.setFileContent(session_id_B, tf.name, tf.summary, tf.data_url)
104
+ )
105
+ );
106
+
107
+ return { sf, session_id_A, session_id_B };
108
+ }
109
+
110
+ function expectArraysEqual<T extends { name: string }>(a: T[], b: T[]): void {
111
+ const sortByName = (a: { name: string }, b: { name: string }) =>
112
+ a.name.localeCompare(b.name);
113
+ expect(a.sort(sortByName)).eql(b.sort(sortByName));
114
+ }
115
+
116
+ describe("DB Session Files", () => {
117
+ let db: Database;
118
+ let user: TestUserSpec;
119
+
120
+ beforeEach(() => {
121
+ db = getLocalDB();
122
+ user = testUserSpec();
123
+ });
124
+
125
+ afterEach(async () => {
126
+ await cleanupTestUser(db, user);
127
+ });
128
+
129
+ it("extract mime type from data url", function () {
130
+ const mimeTypeA = getMimeTypeFromDataUrl("data:image/png;base64,AAAAA");
131
+ const mimeTypeB = getMimeTypeFromDataUrl("data:text/markdown,# TITLE ...");
132
+
133
+ expect(mimeTypeA).eql("image/png");
134
+ expect(mimeTypeB).eql("text/markdown");
135
+ });
136
+
137
+ it("listing entries and getting data", async function () {
138
+ const { sf, session_id_A, session_id_B } = await setup(db, user);
139
+
140
+ const [list_A, list_B] = await Promise.all([
141
+ sf.getFilesForSession(session_id_A),
142
+ sf.getFilesForSession(session_id_B),
143
+ ]);
144
+ const [content_A_1, content_A_2, content_B_1] = await Promise.all([
145
+ sf.getFileContent(session_id_A, TEST_FILES_A[0].name),
146
+ sf.getFileContent(session_id_A, TEST_FILES_A[1].name),
147
+ sf.getFileContent(session_id_B, TEST_FILES_B[0].name),
148
+ ]);
149
+
150
+ expectArraysEqual(list_A, TEST_FILES_A.map(entryToDescriptor));
151
+ expectArraysEqual(list_B, TEST_FILES_B.map(entryToDescriptor));
152
+ expect(content_A_1).eql(TEST_FILES_A[0].data_url);
153
+ expect(content_A_2).eql(TEST_FILES_A[1].data_url);
154
+ expect(content_B_1).eql(TEST_FILES_B[0].data_url);
155
+ });
156
+
157
+ it("deleting files", async function () {
158
+ const { sf, session_id_A, session_id_B } = await setup(db, user);
159
+
160
+ const [list_A_2, list_B_2] = await Promise.all([
161
+ sf.getFilesForSession(session_id_A),
162
+ sf.getFilesForSession(session_id_B),
163
+ ]);
164
+
165
+ // Should do nothing
166
+ await sf.deleteFile(session_id_B, TEST_FILES_A[1].name);
167
+
168
+ const [list_A_0, list_B_0] = await Promise.all([
169
+ sf.getFilesForSession(session_id_A),
170
+ sf.getFilesForSession(session_id_B),
171
+ ]);
172
+
173
+ // Should delete a file
174
+ await sf.deleteFile(session_id_A, TEST_FILES_A[0].name);
175
+
176
+ const [list_A_1, list_B_1] = await Promise.all([
177
+ sf.getFilesForSession(session_id_A),
178
+ sf.getFilesForSession(session_id_B),
179
+ ]);
180
+
181
+ expectArraysEqual(list_A_2, TEST_FILES_A.map(entryToDescriptor));
182
+ expectArraysEqual(list_B_2, TEST_FILES_B.map(entryToDescriptor));
183
+ expectArraysEqual(list_A_0, TEST_FILES_A.map(entryToDescriptor));
184
+ expectArraysEqual(list_B_0, TEST_FILES_B.map(entryToDescriptor));
185
+ expectArraysEqual(list_A_1, TEST_FILES_A.slice(1).map(entryToDescriptor));
186
+ expectArraysEqual(list_B_1, TEST_FILES_B.map(entryToDescriptor));
187
+ });
188
+
189
+ it("deleting all", async function () {
190
+ const { sf, session_id_A, session_id_B } = await setup(db, user);
191
+
192
+ await sf.clearFiles(session_id_A);
193
+
194
+ const [list_A, list_B] = await Promise.all([
195
+ sf.getFilesForSession(session_id_A),
196
+ sf.getFilesForSession(session_id_B),
197
+ ]);
198
+ expectArraysEqual(list_A, []);
199
+ expectArraysEqual(list_B, TEST_FILES_B.map(entryToDescriptor));
200
+ });
201
+
202
+ it("file updates", async function () {
203
+ const { sf, session_id_A, session_id_B } = await setup(db, user);
204
+
205
+ await sf.setFileContent(
206
+ session_id_A,
207
+ TEST_FILE_A_1_ALT.name,
208
+ TEST_FILE_A_1_ALT.summary,
209
+ TEST_FILE_A_1_ALT.data_url
210
+ );
211
+
212
+ const expectFiles_A = [TEST_FILE_A_1_ALT, TEST_FILES_A[1]];
213
+
214
+ const list_A = await sf.getFilesForSession(session_id_A);
215
+ const list_B = await sf.getFilesForSession(session_id_B);
216
+ const [content_A_1, content_A_2, content_B_1] = await Promise.all([
217
+ sf.getFileContent(session_id_A, TEST_FILES_A[0].name),
218
+ sf.getFileContent(session_id_A, TEST_FILES_A[1].name),
219
+ sf.getFileContent(session_id_B, TEST_FILES_B[0].name),
220
+ ]);
221
+
222
+ expectArraysEqual(list_A, expectFiles_A.map(entryToDescriptor));
223
+ expectArraysEqual(list_B, TEST_FILES_B.map(entryToDescriptor));
224
+ expect(content_A_1).eql(TEST_FILE_A_1_ALT.data_url);
225
+ expect(content_A_2).eql(TEST_FILES_A[1].data_url);
226
+ expect(content_B_1).eql(TEST_FILES_B[0].data_url);
227
+ });
228
+
229
+ it("adding new files", async function () {
230
+ const { sf, session_id_A, session_id_B } = await setup(db, user);
231
+
232
+ await sf.setFileContent(
233
+ session_id_A,
234
+ TEST_FILE_A_3_NEW.name,
235
+ TEST_FILE_A_3_NEW.summary,
236
+ TEST_FILE_A_3_NEW.data_url
237
+ );
238
+
239
+ const list_A = await sf.getFilesForSession(session_id_A);
240
+ const list_B = await sf.getFilesForSession(session_id_B);
241
+ const [content_A_1, content_A_2, content_A_3, content_B_1] =
242
+ await Promise.all([
243
+ sf.getFileContent(session_id_A, TEST_FILES_A[0].name),
244
+ sf.getFileContent(session_id_A, TEST_FILES_A[1].name),
245
+ sf.getFileContent(session_id_A, TEST_FILE_A_3_NEW.name),
246
+ sf.getFileContent(session_id_B, TEST_FILES_B[0].name),
247
+ ]);
248
+
249
+ const expectFiles_A = [...TEST_FILES_A, TEST_FILE_A_3_NEW];
250
+
251
+ expectArraysEqual(list_A, expectFiles_A.map(entryToDescriptor));
252
+ expectArraysEqual(list_B, TEST_FILES_B.map(entryToDescriptor));
253
+ expect(content_A_1).eql(TEST_FILES_A[0].data_url);
254
+ expect(content_A_2).eql(TEST_FILES_A[1].data_url);
255
+ expect(content_A_3).eql(TEST_FILE_A_3_NEW.data_url);
256
+ expect(content_B_1).eql(TEST_FILES_B[0].data_url);
257
+ });
258
+ });
@@ -0,0 +1,85 @@
1
+ import { expect, beforeEach, afterEach, describe, it } from "vitest";
2
+ import { Database } from "../chat/data/database";
3
+ import { SessionMessage } from "../chat/data/dataModels";
4
+ import { ChatCompletionMessageParam } from "../agent/llm";
5
+ import {
6
+ TestUserSpec,
7
+ cleanupTestUser,
8
+ createTestSession,
9
+ getLocalDB,
10
+ testUserSpec,
11
+ } from "./dbTestTools";
12
+ import { DbSessionMessages } from "../chat/data/dbSessionMessages";
13
+
14
+ describe("DB Session Messages", () => {
15
+ let db: Database;
16
+ let dbsm: DbSessionMessages;
17
+ let testUsers: TestUserSpec[];
18
+
19
+ beforeEach(() => {
20
+ // Generate unique test data for each test
21
+ db = getLocalDB();
22
+ dbsm = db.createTypedClient(DbSessionMessages);
23
+ testUsers = [testUserSpec()];
24
+ });
25
+
26
+ afterEach(async () => {
27
+ await Promise.all(testUsers.map((tu) => cleanupTestUser(db, tu)));
28
+ testUsers = [];
29
+ });
30
+
31
+ it("should update conversations", async () => {
32
+ const { sessionId } = await createTestSession(db, testUsers[0]);
33
+
34
+ const CONV_0: ChatCompletionMessageParam[] = [
35
+ { role: "user", content: "message 0" },
36
+ { role: "assistant", content: "message 1" },
37
+ ];
38
+ const CONV_1: ChatCompletionMessageParam[] = [
39
+ { role: "user", content: "message 2" },
40
+ { role: "assistant", content: "message 3" },
41
+ ];
42
+
43
+ const MESSGES_0: SessionMessage[] = [
44
+ {
45
+ message_idx: 0,
46
+ sender_uuid: testUsers[0].uuid,
47
+ is_for_llm: true,
48
+ content: CONV_0[0],
49
+ },
50
+ { message_idx: 1, is_for_llm: true, content: CONV_0[1] },
51
+ ];
52
+ const MESSGES_1: SessionMessage[] = [
53
+ {
54
+ message_idx: 2,
55
+ sender_uuid: testUsers[0].uuid,
56
+ is_for_llm: true,
57
+ content: CONV_1[0],
58
+ },
59
+ { message_idx: 3, is_for_llm: true, content: CONV_1[1] },
60
+ ];
61
+
62
+ // Append messages to empty conversation
63
+ await dbsm.append(sessionId, MESSGES_0);
64
+ const msgs0 = await dbsm.getConversation(sessionId, 10);
65
+ expect(msgs0).eql(MESSGES_0);
66
+
67
+ // Append further messages
68
+ await dbsm.append(sessionId, MESSGES_1);
69
+ const msgs1 = await dbsm.getConversation(sessionId, 10);
70
+ expect(msgs1).eql(MESSGES_0.concat(MESSGES_1));
71
+
72
+ // Partial conversation (most recent 2)
73
+ const msgs2 = await dbsm.getConversation(sessionId, 2);
74
+ expect(msgs2).eql(MESSGES_1);
75
+
76
+ // Partial conversation (next 2, start at 2)
77
+ const msgs3 = await dbsm.getConversation(sessionId, 2, 2);
78
+ expect(msgs3).eql(MESSGES_0);
79
+
80
+ // Delete messages and check conv is empty
81
+ await dbsm.clearConversation(sessionId);
82
+ const msgs4 = await dbsm.getConversation(sessionId, 10);
83
+ expect(msgs4).eql([]);
84
+ });
85
+ });
@@ -78,7 +78,7 @@ export async function cleanupTestUser(
78
78
  // api_key: string
79
79
  ): Promise<void> {
80
80
  try {
81
- await db.getClientForTesting().from("users").delete().eq("uuid", user.uuid);
81
+ await db.deleteUser(user.uuid);
82
82
  } catch (_error) {
83
83
  // Ignore cleanup errors
84
84
  }
@@ -107,6 +107,7 @@ export async function createTestSession(
107
107
  user_uuid: user1.uuid,
108
108
  title: "test_session",
109
109
  agent_profile_uuid: agentProfile.uuid,
110
+ agent_paused: false,
110
111
  });
111
112
  assert(sessionId);
112
113
 
@@ -122,11 +123,13 @@ export async function createTestTeamSession(
122
123
  sessionId: string;
123
124
  teamId: string;
124
125
  }> {
125
- const [user1, _] = await Promise.all([
126
- createTestUser(db, testUsers[0]),
127
- createTestUser(db, testUsers[1]),
128
- ]);
126
+ assert(testUsers.length > 0);
129
127
 
128
+ const [user1, _] = await Promise.all(
129
+ testUsers.map((tu) => createTestUser(db, tu))
130
+ );
131
+
132
+ // Profile for first user
130
133
  const agentProfile = await db.createAgentProfile(
131
134
  user1.uuid,
132
135
  undefined,
@@ -146,6 +149,7 @@ export async function createTestTeamSession(
146
149
  user_uuid: user1.uuid,
147
150
  title: "test_session",
148
151
  agent_profile_uuid: agentProfile.uuid,
152
+ agent_paused: false,
149
153
  });
150
154
  assert(sessionId);
151
155
 
@@ -1,11 +1,11 @@
1
1
  /* eslint-disable @typescript-eslint/require-await */
2
2
  import { describe, it, expect } from "vitest";
3
- import { loadImageB64OrUndefined } from "../tool/files";
3
+ import { loadImageAsDataUrlOrUndefined } from "../tool/files";
4
4
  import { strict as assert } from "assert";
5
5
 
6
6
  describe("Image loading", () => {
7
7
  it("convert to data url", async () => {
8
- const imageB64 = loadImageB64OrUndefined("test_data/frog.png");
8
+ const imageB64 = loadImageAsDataUrlOrUndefined("test_data/frog.png");
9
9
  console.log(`imageB64: ${imageB64 || ""}`);
10
10
 
11
11
  expect(imageB64).toBeTypeOf("string");
@@ -56,7 +56,9 @@ describe("McpServerManager", () => {
56
56
  // Invoke
57
57
  {
58
58
  const qualifiedName = tm.getOpenAITools()[0].function.name;
59
- const r = await tm.invoke(qualifiedName, { a: 1000, b: 10 });
59
+ const r = await tm.invoke(
60
+ tm.verifyToolCall(qualifiedName, { a: 1000, b: 10 })
61
+ );
60
62
  console.log(`response: ${r}`);
61
63
  }
62
64
 
@@ -122,4 +122,62 @@ describe("MultiAsyncQueue", () => {
122
122
  expect(processedBatches).toHaveLength(2);
123
123
  expect(processedBatches[1]).toEqual([2]);
124
124
  });
125
+
126
+ it("should not process elements while paused", async () => {
127
+ // Place a 1 on the queue. Inside the process fn, place a 2 and 3 on the
128
+ // queue. Next invocation of process should see BOTH 2 AND 3.
129
+
130
+ const context = { q: undefined as MultiAsyncQueue<number> | undefined };
131
+ const seen: number[] = [];
132
+ let paused: number = 0;
133
+ const process = (elements: number[]): Promise<void> => {
134
+ console.log(`elements: ${JSON.stringify(elements)}`);
135
+ expect(context.q).toBeDefined();
136
+
137
+ for (const e of elements) {
138
+ seen.push(e);
139
+ }
140
+ if (seen.length === 1) {
141
+ context.q?.pause(); // Pause after first element
142
+ paused++;
143
+ expect(context.q?.tryEnqueue(2)).toBe(true);
144
+ expect(context.q?.tryEnqueue(3)).toBe(true);
145
+ } else if (seen.length === 3) {
146
+ expect(seen[1]).toBe(2);
147
+ expect(seen[2]).toBe(3);
148
+ } else {
149
+ throw new Error("unexpected elements");
150
+ }
151
+
152
+ return Promise.resolve();
153
+ };
154
+
155
+ context.q = new MultiAsyncQueue<number>(process);
156
+ expect(context.q).toBeDefined();
157
+ expect(context.q.tryEnqueue(1)).toBe(true);
158
+
159
+ await new Promise<void>((r) => {
160
+ setTimeout(() => {
161
+ expect(seen).toEqual([1]);
162
+ r();
163
+ }, 100);
164
+ });
165
+
166
+ expect(context.q).toBeDefined();
167
+ expect(seen).toEqual([1]);
168
+ expect(paused).toEqual(1);
169
+
170
+ context.q.unpause();
171
+
172
+ await new Promise<void>((r) => {
173
+ setTimeout(() => {
174
+ expect(seen).toEqual([1, 2, 3]);
175
+ r();
176
+ }, 100);
177
+ });
178
+
179
+ expect(context.q).toBeDefined();
180
+ expect(seen).toEqual([1, 2, 3]);
181
+ expect(paused).toEqual(1);
182
+ });
125
183
  });
@@ -5,7 +5,7 @@ import {
5
5
  ChatCompletionMessageParam,
6
6
  ChatCompletionMessageToolCall,
7
7
  ChatCompletionToolMessageParam,
8
- } from "../agent/agent";
8
+ } from "../agent/llm";
9
9
  import { IAgentEventHandler } from "../agent/iAgentEventHandler";
10
10
  import { IPlatform } from "../agent/iplatform";
11
11
  import { DummyLLM } from "../agent/dummyLLM";
@@ -29,7 +29,9 @@ export const DUMMY_PLATFORM: IPlatform = {
29
29
  export class TestAgentEventHandler implements IAgentEventHandler {
30
30
  private all: ChatCompletionMessageParam[] = [];
31
31
  private completions: ChatCompletionAssistantMessageParam[] = [];
32
+ private images: OpenAI.Chat.Completions.ChatCompletionContentPartImage[] = [];
32
33
  private agentMessages: string[] = [""];
34
+ private reasoning: string[] = [""];
33
35
  private toolCalls: ChatCompletionMessageToolCall[] = [];
34
36
  private agentToolCalls: ChatCompletionMessageToolCall[] = [];
35
37
  private toolCallResults: OpenAI.ChatCompletionToolMessageParam[] = [];
@@ -39,6 +41,10 @@ export class TestAgentEventHandler implements IAgentEventHandler {
39
41
  this.all.push(result);
40
42
  }
41
43
 
44
+ onImage(image: OpenAI.Chat.Completions.ChatCompletionContentPartImage) {
45
+ this.images.push(image);
46
+ }
47
+
42
48
  onToolCallResult(result: ChatCompletionToolMessageParam): void {
43
49
  this.toolCallResults.push(result);
44
50
  this.all.push(result);
@@ -48,12 +54,20 @@ export class TestAgentEventHandler implements IAgentEventHandler {
48
54
  this.agentMessages[this.agentMessages.length - 1] += chunk;
49
55
  if (isEnd) {
50
56
  this.agentMessages.push("");
57
+ this.reasoning.push("");
51
58
  }
52
59
  return new Promise<void>((r) => {
53
60
  r();
54
61
  });
55
62
  }
56
63
 
64
+ onReasoning(reasoning: string): Promise<void> {
65
+ this.reasoning[this.reasoning.length - 1] += reasoning;
66
+ return new Promise<void>((r) => {
67
+ r();
68
+ });
69
+ }
70
+
57
71
  onToolCall(
58
72
  toolCall: ChatCompletionMessageToolCall,
59
73
  isAgentTool: boolean
@@ -10,14 +10,18 @@ import { AgentProfile } from "../agent/agent";
10
10
  import { createAgentWithSkills } from "../agent/agentUtils";
11
11
  import { IAgentEventHandler } from "../agent/iAgentEventHandler";
12
12
 
13
- import { loadImageB64OrUndefined } from "./files";
13
+ import { loadImageAsDataUrlOrUndefined } from "./files";
14
14
  import { NODE_PLATFORM } from "./nodePlatform";
15
15
  import { CommandPrompt } from "./commandPrompt";
16
16
  import { IPrompt, Prompt } from "./prompt";
17
17
  import { ContextManager } from "../agent/context";
18
+ import { ChatCompletionMessageParam } from "../agent/llm";
18
19
 
19
20
  const logger = getLogger();
20
21
 
22
+ export const DEFAULT_AGENT_LLM_MODEL =
23
+ process.env["DEFAULT_LLM_MODEL"] || "anthropic/claude-sonnet-4.5";
24
+
21
25
  async function write(msg: string): Promise<void> {
22
26
  return new Promise((resolve, err) => {
23
27
  process.stdout.write(msg, (e) => {
@@ -33,7 +37,7 @@ async function write(msg: string): Promise<void> {
33
37
  export async function runChat(
34
38
  llmUrl: string,
35
39
  agentProfile: AgentProfile,
36
- conversation: OpenAI.ChatCompletionMessageParam[] | undefined,
40
+ conversation: ChatCompletionMessageParam[] | undefined,
37
41
  prompt: string | undefined,
38
42
  image: string | undefined,
39
43
  llmApiKey: string | undefined,
@@ -46,15 +50,18 @@ export async function runChat(
46
50
 
47
51
  const spinner: Spinner = yocto();
48
52
  let first = true;
53
+ let reasoningFirst = true;
49
54
 
50
55
  const onAgentMessage = async (msg: string, msgEnd: boolean) => {
51
56
  if (first) {
52
57
  first = false;
53
- await write("AGENT: ");
54
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
55
- if (spinner) {
56
- spinner.stop().clear();
58
+
59
+ if (!reasoningFirst) {
60
+ await write(`${chalk.grey("]")}\n`);
57
61
  }
62
+
63
+ await write("AGENT: ");
64
+ spinner.stop().clear();
58
65
  }
59
66
 
60
67
  if (msg) {
@@ -64,9 +71,20 @@ export async function runChat(
64
71
  if (msgEnd) {
65
72
  await write("\n");
66
73
  first = true;
74
+ reasoningFirst = true;
67
75
  }
68
76
  };
69
77
 
78
+ const onReasoning = async (reasoning: string) => {
79
+ logger.debug(`[AgentChat.onReasoning]: ${reasoning}`);
80
+ if (reasoningFirst) {
81
+ spinner.stop().clear();
82
+ await write(chalk.grey("[REASONING: "));
83
+ reasoningFirst = false;
84
+ }
85
+ await write(chalk.grey(reasoning));
86
+ };
87
+
70
88
  const repl: IPrompt = new Prompt();
71
89
  const cmdPrompt = new CommandPrompt(repl);
72
90
 
@@ -88,10 +106,19 @@ export async function runChat(
88
106
  logger.debug(`Tool call result: ${JSON.stringify(result)}`);
89
107
  };
90
108
 
109
+ const onImage = (
110
+ image: OpenAI.Chat.Completions.ChatCompletionContentPartImage
111
+ ) => {
112
+ const dataUrl = image.image_url.url;
113
+ void NODE_PLATFORM.renderHTML(`<img src="${dataUrl}" />`);
114
+ };
115
+
91
116
  // Create event handler for CLI agent
92
117
  const eventHandler: IAgentEventHandler = {
93
118
  onCompletion: () => {},
119
+ onImage,
94
120
  onAgentMessage,
121
+ onReasoning,
95
122
  onToolCall,
96
123
  onToolCallResult,
97
124
  };
@@ -101,6 +128,7 @@ export async function runChat(
101
128
  const [agent, sudoMcpServerManager] = await createAgentWithSkills(
102
129
  llmUrl,
103
130
  agentProfile,
131
+ DEFAULT_AGENT_LLM_MODEL,
104
132
  eventHandler,
105
133
  NODE_PLATFORM,
106
134
  new ContextManager(agentProfile.system_prompt, conversation || []),
@@ -136,7 +164,7 @@ export async function runChat(
136
164
  image = img;
137
165
  }
138
166
 
139
- image = loadImageB64OrUndefined(image);
167
+ image = loadImageAsDataUrlOrUndefined(image);
140
168
 
141
169
  // Pass the prompt and image to the Agent
142
170
  try {
@@ -1,7 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import * as dotenv from "dotenv";
3
3
  import { command, option, flag, optional, string } from "cmd-ts";
4
- import OpenAI from "openai";
5
4
  import { strict as assert } from "assert";
6
5
 
7
6
  import { configuration, utils } from "@xalia/xmcp/tool";
@@ -22,10 +21,11 @@ import {
22
21
  import {
23
22
  loadFileOrUndefined,
24
23
  loadFileOrStdin,
25
- loadImageB64OrUndefined,
24
+ loadImageAsDataUrlOrUndefined,
26
25
  } from "./files";
27
- import { runChat } from "./agentChat";
26
+ import { DEFAULT_AGENT_LLM_MODEL, runChat } from "./agentChat";
28
27
  import { NODE_PLATFORM } from "./nodePlatform";
28
+ import { ChatCompletionMessageParam } from "../agent/llm";
29
29
 
30
30
  dotenv.config();
31
31
 
@@ -128,9 +128,8 @@ export const agentMain = command({
128
128
 
129
129
  // Restore conversation from value or file.
130
130
 
131
- const startingConversation:
132
- | OpenAI.ChatCompletionMessageParam[]
133
- | undefined = utils.loadContentOrFileOrUndefined(conversationFile);
131
+ const startingConversation: ChatCompletionMessageParam[] | undefined =
132
+ utils.loadContentOrFileOrUndefined(conversationFile);
134
133
  logger.debug(
135
134
  `startingConversation: ${JSON.stringify(startingConversation)}`
136
135
  );
@@ -146,10 +145,11 @@ export const agentMain = command({
146
145
  const { response, conversation } = await runOneShot(
147
146
  llmUrl,
148
147
  agentProfile,
148
+ DEFAULT_AGENT_LLM_MODEL,
149
149
  startingConversation,
150
150
  NODE_PLATFORM,
151
151
  prompt,
152
- loadImageB64OrUndefined(imageFile),
152
+ loadImageAsDataUrlOrUndefined(imageFile),
153
153
  llmApiKey,
154
154
  sudomcpConfig,
155
155
  approveToolsUpTo