@copilotkit/runtime 1.9.2-next.9 → 1.9.3-next.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 (60) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/dist/{chunk-Z5GYTKMD.mjs → chunk-EK5RTZVJ.mjs} +225 -149
  3. package/dist/chunk-EK5RTZVJ.mjs.map +1 -0
  4. package/dist/{chunk-SMDVD4VG.mjs → chunk-KCYFFRJY.mjs} +2 -2
  5. package/dist/{chunk-4JBKY7XT.mjs → chunk-QLLV2QVK.mjs} +48 -28
  6. package/dist/chunk-QLLV2QVK.mjs.map +1 -0
  7. package/dist/{chunk-5YGKE5SN.mjs → chunk-R5D7D7YN.mjs} +2 -2
  8. package/dist/{chunk-UUXRYAB4.mjs → chunk-RCCT2GOF.mjs} +2 -2
  9. package/dist/{chunk-ALZ5H3VD.mjs → chunk-YGS5B7PN.mjs} +2 -2
  10. package/dist/{groq-adapter-172a2ca4.d.ts → groq-adapter-742818f2.d.ts} +5 -1
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.js +267 -171
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.mjs +9 -9
  15. package/dist/{langserve-fc5cac89.d.ts → langserve-3e8d0e06.d.ts} +6 -0
  16. package/dist/lib/index.d.ts +155 -5
  17. package/dist/lib/index.js +221 -168
  18. package/dist/lib/index.js.map +1 -1
  19. package/dist/lib/index.mjs +9 -9
  20. package/dist/lib/integrations/index.d.ts +3 -3
  21. package/dist/lib/integrations/index.js +11 -11
  22. package/dist/lib/integrations/index.js.map +1 -1
  23. package/dist/lib/integrations/index.mjs +8 -8
  24. package/dist/lib/integrations/nest/index.d.ts +2 -2
  25. package/dist/lib/integrations/nest/index.js +11 -11
  26. package/dist/lib/integrations/nest/index.js.map +1 -1
  27. package/dist/lib/integrations/nest/index.mjs +4 -4
  28. package/dist/lib/integrations/node-express/index.d.ts +2 -2
  29. package/dist/lib/integrations/node-express/index.js +11 -11
  30. package/dist/lib/integrations/node-express/index.js.map +1 -1
  31. package/dist/lib/integrations/node-express/index.mjs +4 -4
  32. package/dist/lib/integrations/node-http/index.d.ts +2 -2
  33. package/dist/lib/integrations/node-http/index.js +11 -11
  34. package/dist/lib/integrations/node-http/index.js.map +1 -1
  35. package/dist/lib/integrations/node-http/index.mjs +3 -3
  36. package/dist/service-adapters/index.d.ts +5 -4
  37. package/dist/service-adapters/index.js +47 -27
  38. package/dist/service-adapters/index.js.map +1 -1
  39. package/dist/service-adapters/index.mjs +1 -1
  40. package/dist/{shared-bd953ebf.d.ts → shared-96b46379.d.ts} +16 -18
  41. package/package.json +11 -11
  42. package/src/graphql/resolvers/copilot.resolver.ts +1 -2
  43. package/src/lib/runtime/__tests__/{copilot-runtime-trace.test.ts → copilot-runtime-error.test.ts} +27 -27
  44. package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +464 -0
  45. package/src/lib/runtime/agui-action.ts +9 -3
  46. package/src/lib/runtime/copilot-runtime.ts +112 -124
  47. package/src/lib/runtime/mcp-tools-utils.ts +84 -18
  48. package/src/lib/runtime/remote-actions.ts +6 -0
  49. package/src/service-adapters/anthropic/anthropic-adapter.ts +64 -4
  50. package/src/service-adapters/anthropic/utils.ts +3 -8
  51. package/src/service-adapters/events.ts +40 -1
  52. package/src/service-adapters/google/google-genai-adapter.ts +5 -0
  53. package/src/service-adapters/openai/openai-adapter.ts +0 -14
  54. package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +172 -387
  55. package/dist/chunk-4JBKY7XT.mjs.map +0 -1
  56. package/dist/chunk-Z5GYTKMD.mjs.map +0 -1
  57. /package/dist/{chunk-SMDVD4VG.mjs.map → chunk-KCYFFRJY.mjs.map} +0 -0
  58. /package/dist/{chunk-5YGKE5SN.mjs.map → chunk-R5D7D7YN.mjs.map} +0 -0
  59. /package/dist/{chunk-UUXRYAB4.mjs.map → chunk-RCCT2GOF.mjs.map} +0 -0
  60. /package/dist/{chunk-ALZ5H3VD.mjs.map → chunk-YGS5B7PN.mjs.map} +0 -0
@@ -2,59 +2,21 @@
2
2
  * @jest-environment node
3
3
  */
4
4
 
5
- // Mock the modules first
6
- jest.mock("@anthropic-ai/sdk", () => {
7
- const mockAnthropic = jest.fn().mockImplementation(() => ({
8
- messages: {
9
- create: jest.fn().mockResolvedValue({
10
- [Symbol.asyncIterator]: () => ({
11
- next: async () => ({ done: true, value: undefined }),
12
- }),
13
- }),
14
- },
15
- }));
16
-
17
- return { default: mockAnthropic };
18
- });
19
-
20
- // Mock the AnthropicAdapter class to avoid the "new Anthropic()" issue
21
- jest.mock("../../../src/service-adapters/anthropic/anthropic-adapter", () => {
22
- class MockAnthropicAdapter {
23
- _anthropic: any;
24
- model: string = "claude-3-5-sonnet-latest";
25
-
26
- constructor() {
27
- this._anthropic = {
28
- messages: {
29
- create: jest.fn(),
30
- },
31
- };
32
- }
33
-
34
- get anthropic() {
35
- return this._anthropic;
36
- }
37
-
38
- async process(request: any) {
39
- // Mock implementation that calls our event source but doesn't do the actual processing
40
- request.eventSource.stream((stream: any) => {
41
- stream.complete();
42
- });
43
-
44
- return { threadId: request.threadId || "mock-thread-id" };
45
- }
46
- }
5
+ import { AnthropicAdapter } from "../../../src/service-adapters/anthropic/anthropic-adapter";
47
6
 
48
- return { AnthropicAdapter: MockAnthropicAdapter };
7
+ // Mock only the Anthropic SDK, not our adapter
8
+ jest.mock("@anthropic-ai/sdk", () => {
9
+ return {
10
+ default: jest.fn().mockImplementation(() => ({
11
+ messages: {
12
+ create: jest.fn(),
13
+ },
14
+ })),
15
+ };
49
16
  });
50
17
 
51
- // Now import the modules
52
- import { AnthropicAdapter } from "../../../src/service-adapters/anthropic/anthropic-adapter";
53
- import { ActionInput } from "../../../src/graphql/inputs/action.input";
54
-
55
- // Mock the Message classes since they use TypeGraphQL decorators
18
+ // Mock the message classes
56
19
  jest.mock("../../../src/graphql/types/converted", () => {
57
- // Create minimal implementations of the message classes
58
20
  class MockTextMessage {
59
21
  content: string;
60
22
  role: string;
@@ -140,10 +102,24 @@ jest.mock("../../../src/graphql/types/converted", () => {
140
102
  describe("AnthropicAdapter", () => {
141
103
  let adapter: AnthropicAdapter;
142
104
  let mockEventSource: any;
105
+ let mockAnthropicCreate: jest.Mock;
143
106
 
144
107
  beforeEach(() => {
145
108
  jest.clearAllMocks();
146
- adapter = new AnthropicAdapter();
109
+
110
+ // Create a mock Anthropic instance
111
+ const mockAnthropic = {
112
+ messages: {
113
+ create: jest.fn(),
114
+ },
115
+ };
116
+
117
+ // Create adapter with the mocked instance
118
+ adapter = new AnthropicAdapter({ anthropic: mockAnthropic as any });
119
+
120
+ // Mock the create method to capture what's being sent
121
+ mockAnthropicCreate = mockAnthropic.messages.create;
122
+
147
123
  mockEventSource = {
148
124
  stream: jest.fn((callback) => {
149
125
  const mockStream = {
@@ -156,449 +132,258 @@ describe("AnthropicAdapter", () => {
156
132
  complete: jest.fn(),
157
133
  };
158
134
  callback(mockStream);
135
+ return Promise.resolve();
159
136
  }),
160
137
  };
161
138
  });
162
139
 
163
- describe("Tool ID handling", () => {
164
- it("should filter out tool_result messages that don't have corresponding tool_use IDs", async () => {
165
- // Import dynamically after mocking
140
+ describe("Deduplication Logic", () => {
141
+ it("should filter out duplicate result messages", async () => {
166
142
  const {
167
143
  TextMessage,
168
144
  ActionExecutionMessage,
169
145
  ResultMessage,
170
146
  } = require("../../../src/graphql/types/converted");
171
147
 
172
- // Create messages including one valid pair and one invalid tool_result
173
148
  const systemMessage = new TextMessage("system", "System message");
174
- const userMessage = new TextMessage("user", "User message");
149
+ const userMessage = new TextMessage("user", "Set theme to orange");
175
150
 
176
- // Valid tool execution message
177
- const validToolExecution = new ActionExecutionMessage({
178
- id: "valid-tool-id",
179
- name: "validTool",
180
- arguments: '{"arg":"value"}',
151
+ // Tool execution
152
+ const toolExecution = new ActionExecutionMessage({
153
+ id: "tool-123",
154
+ name: "setThemeColor",
155
+ arguments: '{"themeColor": "orange"}',
181
156
  });
182
157
 
183
- // Valid result for the above tool
184
- const validToolResult = new ResultMessage({
185
- actionExecutionId: "valid-tool-id",
186
- result: '{"result":"success"}',
158
+ // Multiple duplicate results (this was causing the infinite loop)
159
+ const result1 = new ResultMessage({
160
+ actionExecutionId: "tool-123",
161
+ result: "Theme color set to orange",
187
162
  });
188
-
189
- // Invalid tool result with no corresponding tool execution
190
- const invalidToolResult = new ResultMessage({
191
- actionExecutionId: "invalid-tool-id",
192
- result: '{"result":"failure"}',
163
+ const result2 = new ResultMessage({
164
+ actionExecutionId: "tool-123",
165
+ result: "Theme color set to orange",
193
166
  });
194
-
195
- // Spy on the actual message conversion to verify what's being sent
196
- const mockCreate = jest.fn().mockImplementation((params) => {
197
- // We'll check what messages are being sent
198
- expect(params.messages.length).toBe(4); // Messages passed directly in our mock implementation
199
-
200
- // Verify the valid tool result is included
201
- const toolResults = params.messages.filter(
202
- (m: any) => m.role === "user" && m.content[0]?.type === "tool_result",
203
- );
204
- expect(toolResults.length).toBe(1);
205
- expect(toolResults[0].content[0].tool_use_id).toBe("valid-tool-id");
206
-
207
- return {
208
- [Symbol.asyncIterator]: () => ({
209
- next: async () => ({ done: true, value: undefined }),
210
- }),
211
- };
167
+ const result3 = new ResultMessage({
168
+ actionExecutionId: "tool-123",
169
+ result: "Theme color set to orange",
212
170
  });
213
171
 
214
- // Mock the anthropic property to use our mock create function
215
- const anthropicMock = {
216
- messages: { create: mockCreate },
217
- };
172
+ // Mock Anthropic to return empty stream (simulating the original problem)
173
+ mockAnthropicCreate.mockResolvedValue({
174
+ [Symbol.asyncIterator]: async function* () {
175
+ // Empty stream - no content from Anthropic
176
+ },
177
+ });
218
178
 
219
- // Use Object.defineProperty to mock the anthropic getter
220
- Object.defineProperty(adapter, "_anthropic", {
221
- value: anthropicMock,
222
- writable: true,
179
+ await adapter.process({
180
+ threadId: "test-thread",
181
+ model: "claude-3-5-sonnet-latest",
182
+ messages: [systemMessage, userMessage, toolExecution, result1, result2, result3],
183
+ actions: [],
184
+ eventSource: mockEventSource,
185
+ forwardedParameters: {},
223
186
  });
224
187
 
225
- // Ensure process method will call our mock
226
- jest.spyOn(adapter, "process").mockImplementation(async (request) => {
227
- const { eventSource } = request;
228
-
229
- // Direct call to the mocked create method
230
- mockCreate({
231
- messages: [
232
- // Include the actual messages for better testing
233
- { role: "assistant", content: [{ type: "text", text: "System message" }] },
234
- { role: "user", content: [{ type: "text", text: "User message" }] },
235
- {
188
+ // Check that only one result message was sent to Anthropic
189
+ expect(mockAnthropicCreate).toHaveBeenCalledWith(
190
+ expect.objectContaining({
191
+ messages: expect.arrayContaining([
192
+ expect.objectContaining({
193
+ role: "user",
194
+ content: [{ type: "text", text: "Set theme to orange" }],
195
+ }),
196
+ expect.objectContaining({
236
197
  role: "assistant",
237
198
  content: [
238
- {
239
- id: "valid-tool-id",
199
+ expect.objectContaining({
240
200
  type: "tool_use",
241
- name: "validTool",
242
- input: '{"arg":"value"}',
243
- },
201
+ id: "tool-123",
202
+ name: "setThemeColor",
203
+ }),
244
204
  ],
245
- },
246
- {
205
+ }),
206
+ expect.objectContaining({
247
207
  role: "user",
248
208
  content: [
249
- {
209
+ expect.objectContaining({
250
210
  type: "tool_result",
251
- content: '{"result":"success"}',
252
- tool_use_id: "valid-tool-id",
253
- },
211
+ content: "Theme color set to orange",
212
+ tool_use_id: "tool-123",
213
+ }),
254
214
  ],
255
- },
256
- ],
257
- });
258
-
259
- // Call the event source with an async callback that returns a Promise
260
- eventSource.stream(async (stream: any) => {
261
- stream.complete();
262
- return Promise.resolve();
263
- });
264
-
265
- return { threadId: request.threadId || "mock-thread-id" };
266
- });
215
+ }),
216
+ ]),
217
+ }),
218
+ );
267
219
 
268
- await adapter.process({
269
- threadId: "test-thread",
270
- model: "claude-3-5-sonnet-latest",
271
- messages: [
272
- systemMessage,
273
- userMessage,
274
- validToolExecution,
275
- validToolResult,
276
- invalidToolResult,
277
- ],
278
- actions: [],
279
- eventSource: mockEventSource,
280
- forwardedParameters: {},
281
- });
220
+ // Verify only 3 messages sent (user, assistant tool_use, user tool_result) - no duplicates
221
+ const sentMessages = mockAnthropicCreate.mock.calls[0][0].messages;
222
+ expect(sentMessages).toHaveLength(3);
282
223
 
283
- // Verify the stream function was called
284
- expect(mockEventSource.stream).toHaveBeenCalled();
285
- expect(mockCreate).toHaveBeenCalled();
224
+ // Count tool_result messages - should be only 1
225
+ const toolResults = sentMessages.filter(
226
+ (msg: any) => msg.role === "user" && msg.content.some((c: any) => c.type === "tool_result"),
227
+ );
228
+ expect(toolResults).toHaveLength(1);
286
229
  });
287
230
 
288
- it("should handle duplicate tool IDs by only using each once", async () => {
289
- // Import dynamically after mocking
231
+ it("should filter out invalid result messages without corresponding tool_use", async () => {
290
232
  const {
291
233
  TextMessage,
292
234
  ActionExecutionMessage,
293
235
  ResultMessage,
294
236
  } = require("../../../src/graphql/types/converted");
295
237
 
296
- // Create messages including duplicate tool results for the same ID
297
238
  const systemMessage = new TextMessage("system", "System message");
298
239
 
299
- // Valid tool execution message
300
- const toolExecution = new ActionExecutionMessage({
301
- id: "tool-id-1",
302
- name: "someTool",
303
- arguments: '{"arg":"value"}',
304
- });
305
-
306
- // Two results for the same tool ID
307
- const firstToolResult = new ResultMessage({
308
- actionExecutionId: "tool-id-1",
309
- result: '{"result":"first"}',
240
+ // Valid tool execution
241
+ const validTool = new ActionExecutionMessage({
242
+ id: "valid-tool",
243
+ name: "validAction",
244
+ arguments: "{}",
310
245
  });
311
-
312
- const duplicateToolResult = new ResultMessage({
313
- actionExecutionId: "tool-id-1",
314
- result: '{"result":"duplicate"}',
315
- });
316
-
317
- // Spy on the message create call
318
- const mockCreate = jest.fn().mockImplementation((params) => {
319
- // Verify only one tool result is included despite two being provided
320
- const toolResults = params.messages.filter(
321
- (m: any) => m.role === "user" && m.content[0]?.type === "tool_result",
322
- );
323
- expect(toolResults.length).toBe(1);
324
- expect(toolResults[0].content[0].tool_use_id).toBe("tool-id-1");
325
-
326
- return {
327
- [Symbol.asyncIterator]: () => ({
328
- next: async () => ({ done: true, value: undefined }),
329
- }),
330
- };
246
+ const validResult = new ResultMessage({
247
+ actionExecutionId: "valid-tool",
248
+ result: "Valid result",
331
249
  });
332
250
 
333
- // Mock the anthropic property to use our mock create function
334
- const anthropicMock = {
335
- messages: { create: mockCreate },
336
- };
337
-
338
- // Use Object.defineProperty to mock the anthropic getter
339
- Object.defineProperty(adapter, "_anthropic", {
340
- value: anthropicMock,
341
- writable: true,
251
+ // Invalid result with no corresponding tool_use
252
+ const invalidResult = new ResultMessage({
253
+ actionExecutionId: "nonexistent-tool",
254
+ result: "Invalid result",
342
255
  });
343
256
 
344
- // Ensure process method will call our mock
345
- jest.spyOn(adapter, "process").mockImplementation(async (request) => {
346
- const { eventSource } = request;
347
-
348
- // Direct call to the mocked create method
349
- mockCreate({
350
- messages: [
351
- { role: "assistant", content: [{ type: "text", text: "System message" }] },
352
- {
353
- role: "assistant",
354
- content: [
355
- { id: "tool-id-1", type: "tool_use", name: "someTool", input: '{"arg":"value"}' },
356
- ],
357
- },
358
- {
359
- role: "user",
360
- content: [
361
- { type: "tool_result", content: '{"result":"first"}', tool_use_id: "tool-id-1" },
362
- ],
363
- },
364
- ],
365
- });
366
-
367
- // Call the event source with an async callback that returns a Promise
368
- eventSource.stream(async (stream: any) => {
369
- stream.complete();
370
- return Promise.resolve();
371
- });
372
-
373
- return { threadId: request.threadId || "mock-thread-id" };
257
+ mockAnthropicCreate.mockResolvedValue({
258
+ [Symbol.asyncIterator]: async function* () {},
374
259
  });
375
260
 
376
261
  await adapter.process({
377
262
  threadId: "test-thread",
378
- model: "claude-3-5-sonnet-latest",
379
- messages: [systemMessage, toolExecution, firstToolResult, duplicateToolResult],
263
+ messages: [systemMessage, validTool, validResult, invalidResult],
380
264
  actions: [],
381
265
  eventSource: mockEventSource,
382
266
  forwardedParameters: {},
383
267
  });
384
268
 
385
- expect(mockCreate).toHaveBeenCalled();
269
+ const sentMessages = mockAnthropicCreate.mock.calls[0][0].messages;
270
+
271
+ // Should only include the valid tool result
272
+ const toolResults = sentMessages.filter(
273
+ (msg: any) => msg.role === "user" && msg.content.some((c: any) => c.type === "tool_result"),
274
+ );
275
+ expect(toolResults).toHaveLength(1);
276
+ expect(toolResults[0].content[0].tool_use_id).toBe("valid-tool");
386
277
  });
278
+ });
387
279
 
388
- it("should correctly handle complex message patterns with multiple tool calls and results", async () => {
389
- // Import dynamically after mocking
280
+ describe("Fallback Response Logic", () => {
281
+ it("should generate contextual fallback when Anthropic returns no content", async () => {
390
282
  const {
391
283
  TextMessage,
392
284
  ActionExecutionMessage,
393
285
  ResultMessage,
394
286
  } = require("../../../src/graphql/types/converted");
395
287
 
396
- // Setup a complex conversation with multiple tools and results, including duplicates and invalids
397
288
  const systemMessage = new TextMessage("system", "System message");
398
- const userMessage = new TextMessage("user", "Initial user message");
289
+ const userMessage = new TextMessage("user", "Set theme to orange");
399
290
 
400
- // First tool execution and result (valid pair)
401
- const toolExecution1 = new ActionExecutionMessage({
402
- id: "tool-id-1",
403
- name: "firstTool",
404
- arguments: '{"param":"value1"}',
405
- });
406
- const toolResult1 = new ResultMessage({
407
- actionExecutionId: "tool-id-1",
408
- result: '{"success":true,"data":"result1"}',
409
- });
410
-
411
- // Assistant response after first tool
412
- const assistantResponse = new TextMessage("assistant", "I got the first result");
413
-
414
- // Second and third tool executions
415
- const toolExecution2 = new ActionExecutionMessage({
416
- id: "tool-id-2",
417
- name: "secondTool",
418
- arguments: '{"param":"value2"}',
419
- });
420
- const toolExecution3 = new ActionExecutionMessage({
421
- id: "tool-id-3",
422
- name: "thirdTool",
423
- arguments: '{"param":"value3"}',
424
- });
425
-
426
- // Results for second and third tools
427
- const toolResult2 = new ResultMessage({
428
- actionExecutionId: "tool-id-2",
429
- result: '{"success":true,"data":"result2"}',
430
- });
431
- const toolResult3 = new ResultMessage({
432
- actionExecutionId: "tool-id-3",
433
- result: '{"success":true,"data":"result3"}',
434
- });
435
-
436
- // Invalid result (no corresponding execution)
437
- const invalidToolResult = new ResultMessage({
438
- actionExecutionId: "invalid-tool-id",
439
- result: '{"success":false,"error":"No such tool"}',
291
+ const toolExecution = new ActionExecutionMessage({
292
+ id: "tool-123",
293
+ name: "setThemeColor",
294
+ arguments: '{"themeColor": "orange"}',
440
295
  });
441
296
 
442
- // Duplicate result for first tool
443
- const duplicateToolResult1 = new ResultMessage({
444
- actionExecutionId: "tool-id-1",
445
- result: '{"success":true,"data":"duplicate-result1"}',
297
+ const toolResult = new ResultMessage({
298
+ actionExecutionId: "tool-123",
299
+ result: "Theme color set to orange",
446
300
  });
447
301
 
448
- // User follow-up
449
- const userFollowUp = new TextMessage("user", "Follow-up question");
450
-
451
- // Fourth tool execution with two competing results
452
- const toolExecution4 = new ActionExecutionMessage({
453
- id: "tool-id-4",
454
- name: "fourthTool",
455
- arguments: '{"param":"value4"}',
456
- });
457
- const toolResult4a = new ResultMessage({
458
- actionExecutionId: "tool-id-4",
459
- result: '{"success":true,"data":"result4-version-a"}',
460
- });
461
- const toolResult4b = new ResultMessage({
462
- actionExecutionId: "tool-id-4",
463
- result: '{"success":true,"data":"result4-version-b"}',
464
- });
465
-
466
- // Spy on the message create call
467
- const mockCreate = jest.fn().mockImplementation((params) => {
468
- // Return a valid mock response
469
- return {
470
- [Symbol.asyncIterator]: () => ({
471
- next: async () => ({ done: true, value: undefined }),
472
- }),
473
- };
302
+ // Mock Anthropic to return empty stream
303
+ mockAnthropicCreate.mockResolvedValue({
304
+ [Symbol.asyncIterator]: async function* () {
305
+ // No content blocks - simulates Anthropic not responding
306
+ },
474
307
  });
475
308
 
476
- // Mock the anthropic property to use our mock create function
477
- const anthropicMock = {
478
- messages: { create: mockCreate },
309
+ const mockStream = {
310
+ sendTextMessageStart: jest.fn(),
311
+ sendTextMessageContent: jest.fn(),
312
+ sendTextMessageEnd: jest.fn(),
313
+ complete: jest.fn(),
479
314
  };
480
315
 
481
- // Use Object.defineProperty to mock the anthropic getter
482
- Object.defineProperty(adapter, "_anthropic", {
483
- value: anthropicMock,
484
- writable: true,
485
- });
486
-
487
- // Ensure process method will call our mock
488
- jest.spyOn(adapter, "process").mockImplementation(async (request) => {
489
- const { eventSource } = request;
490
-
491
- // Direct call to the mocked create method to ensure it's called
492
- mockCreate({
493
- messages: [{ role: "user", content: [{ type: "text", text: "Mock message" }] }],
494
- });
495
-
496
- // Call the event source with an async callback that returns a Promise
497
- eventSource.stream(async (stream: any) => {
498
- stream.complete();
499
- return Promise.resolve();
500
- });
501
-
502
- return { threadId: request.threadId || "mock-thread-id" };
316
+ mockEventSource.stream.mockImplementation((callback) => {
317
+ callback(mockStream);
318
+ return Promise.resolve();
503
319
  });
504
320
 
505
- // Process the complex message sequence
506
321
  await adapter.process({
507
322
  threadId: "test-thread",
508
- model: "claude-3-5-sonnet-latest",
509
- messages: [
510
- systemMessage,
511
- userMessage,
512
- toolExecution1,
513
- toolResult1,
514
- assistantResponse,
515
- toolExecution2,
516
- toolExecution3,
517
- toolResult2,
518
- toolResult3,
519
- invalidToolResult,
520
- duplicateToolResult1,
521
- userFollowUp,
522
- toolExecution4,
523
- toolResult4a,
524
- toolResult4b,
525
- ],
323
+ messages: [systemMessage, userMessage, toolExecution, toolResult],
526
324
  actions: [],
527
325
  eventSource: mockEventSource,
528
326
  forwardedParameters: {},
529
327
  });
530
328
 
531
- // Verify our mock was called
532
- expect(mockCreate).toHaveBeenCalled();
329
+ // Should generate fallback response with the tool result content
330
+ expect(mockStream.sendTextMessageStart).toHaveBeenCalled();
331
+ expect(mockStream.sendTextMessageContent).toHaveBeenCalledWith({
332
+ messageId: expect.any(String),
333
+ content: "Theme color set to orange", // Uses the actual result content
334
+ });
335
+ expect(mockStream.sendTextMessageEnd).toHaveBeenCalled();
533
336
  });
534
337
 
535
- it("should call the stream method on eventSource", async () => {
536
- // Import dynamically after mocking
338
+ it("should use generic fallback when no tool result content available", async () => {
537
339
  const {
538
340
  TextMessage,
539
341
  ActionExecutionMessage,
540
342
  ResultMessage,
541
343
  } = require("../../../src/graphql/types/converted");
542
344
 
543
- // Create messages including one valid pair and one invalid tool_result
544
345
  const systemMessage = new TextMessage("system", "System message");
545
- const userMessage = new TextMessage("user", "User message");
546
346
 
547
- // Valid tool execution message
548
- const validToolExecution = new ActionExecutionMessage({
549
- id: "valid-tool-id",
550
- name: "validTool",
551
- arguments: '{"arg":"value"}',
347
+ const toolExecution = new ActionExecutionMessage({
348
+ id: "tool-123",
349
+ name: "someAction",
350
+ arguments: "{}",
552
351
  });
553
352
 
554
- // Valid result for the above tool
555
- const validToolResult = new ResultMessage({
556
- actionExecutionId: "valid-tool-id",
557
- result: '{"result":"success"}',
353
+ const toolResult = new ResultMessage({
354
+ actionExecutionId: "tool-123",
355
+ result: "", // Empty result
558
356
  });
559
357
 
560
- // Invalid tool result with no corresponding tool execution
561
- const invalidToolResult = new ResultMessage({
562
- actionExecutionId: "invalid-tool-id",
563
- result: '{"result":"failure"}',
358
+ mockAnthropicCreate.mockResolvedValue({
359
+ [Symbol.asyncIterator]: async function* () {},
564
360
  });
565
361
 
566
- await adapter.process({
567
- threadId: "test-thread",
568
- model: "claude-3-5-sonnet-latest",
569
- messages: [
570
- systemMessage,
571
- userMessage,
572
- validToolExecution,
573
- validToolResult,
574
- invalidToolResult,
575
- ],
576
- actions: [],
577
- eventSource: mockEventSource,
578
- forwardedParameters: {},
579
- });
580
-
581
- // Verify the stream function was called
582
- expect(mockEventSource.stream).toHaveBeenCalled();
583
- });
584
-
585
- it("should return the provided threadId", async () => {
586
- // Import dynamically after mocking
587
- const { TextMessage } = require("../../../src/graphql/types/converted");
362
+ const mockStream = {
363
+ sendTextMessageStart: jest.fn(),
364
+ sendTextMessageContent: jest.fn(),
365
+ sendTextMessageEnd: jest.fn(),
366
+ complete: jest.fn(),
367
+ };
588
368
 
589
- // Create messages including duplicate tool results for the same ID
590
- const systemMessage = new TextMessage("system", "System message");
369
+ mockEventSource.stream.mockImplementation((callback) => {
370
+ callback(mockStream);
371
+ return Promise.resolve();
372
+ });
591
373
 
592
- const result = await adapter.process({
374
+ await adapter.process({
593
375
  threadId: "test-thread",
594
- model: "claude-3-5-sonnet-latest",
595
- messages: [systemMessage],
376
+ messages: [systemMessage, toolExecution, toolResult],
596
377
  actions: [],
597
378
  eventSource: mockEventSource,
598
379
  forwardedParameters: {},
599
380
  });
600
381
 
601
- expect(result.threadId).toBe("test-thread");
382
+ // Should use generic fallback
383
+ expect(mockStream.sendTextMessageContent).toHaveBeenCalledWith({
384
+ messageId: expect.any(String),
385
+ content: "Task completed successfully.",
386
+ });
602
387
  });
603
388
  });
604
389
  });