@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.
- package/CHANGELOG.md +176 -0
- package/dist/{chunk-Z5GYTKMD.mjs → chunk-EK5RTZVJ.mjs} +225 -149
- package/dist/chunk-EK5RTZVJ.mjs.map +1 -0
- package/dist/{chunk-SMDVD4VG.mjs → chunk-KCYFFRJY.mjs} +2 -2
- package/dist/{chunk-4JBKY7XT.mjs → chunk-QLLV2QVK.mjs} +48 -28
- package/dist/chunk-QLLV2QVK.mjs.map +1 -0
- package/dist/{chunk-5YGKE5SN.mjs → chunk-R5D7D7YN.mjs} +2 -2
- package/dist/{chunk-UUXRYAB4.mjs → chunk-RCCT2GOF.mjs} +2 -2
- package/dist/{chunk-ALZ5H3VD.mjs → chunk-YGS5B7PN.mjs} +2 -2
- package/dist/{groq-adapter-172a2ca4.d.ts → groq-adapter-742818f2.d.ts} +5 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +267 -171
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -9
- package/dist/{langserve-fc5cac89.d.ts → langserve-3e8d0e06.d.ts} +6 -0
- package/dist/lib/index.d.ts +155 -5
- package/dist/lib/index.js +221 -168
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/index.mjs +9 -9
- package/dist/lib/integrations/index.d.ts +3 -3
- package/dist/lib/integrations/index.js +11 -11
- package/dist/lib/integrations/index.js.map +1 -1
- package/dist/lib/integrations/index.mjs +8 -8
- package/dist/lib/integrations/nest/index.d.ts +2 -2
- package/dist/lib/integrations/nest/index.js +11 -11
- package/dist/lib/integrations/nest/index.js.map +1 -1
- package/dist/lib/integrations/nest/index.mjs +4 -4
- package/dist/lib/integrations/node-express/index.d.ts +2 -2
- package/dist/lib/integrations/node-express/index.js +11 -11
- package/dist/lib/integrations/node-express/index.js.map +1 -1
- package/dist/lib/integrations/node-express/index.mjs +4 -4
- package/dist/lib/integrations/node-http/index.d.ts +2 -2
- package/dist/lib/integrations/node-http/index.js +11 -11
- package/dist/lib/integrations/node-http/index.js.map +1 -1
- package/dist/lib/integrations/node-http/index.mjs +3 -3
- package/dist/service-adapters/index.d.ts +5 -4
- package/dist/service-adapters/index.js +47 -27
- package/dist/service-adapters/index.js.map +1 -1
- package/dist/service-adapters/index.mjs +1 -1
- package/dist/{shared-bd953ebf.d.ts → shared-96b46379.d.ts} +16 -18
- package/package.json +11 -11
- package/src/graphql/resolvers/copilot.resolver.ts +1 -2
- package/src/lib/runtime/__tests__/{copilot-runtime-trace.test.ts → copilot-runtime-error.test.ts} +27 -27
- package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +464 -0
- package/src/lib/runtime/agui-action.ts +9 -3
- package/src/lib/runtime/copilot-runtime.ts +112 -124
- package/src/lib/runtime/mcp-tools-utils.ts +84 -18
- package/src/lib/runtime/remote-actions.ts +6 -0
- package/src/service-adapters/anthropic/anthropic-adapter.ts +64 -4
- package/src/service-adapters/anthropic/utils.ts +3 -8
- package/src/service-adapters/events.ts +40 -1
- package/src/service-adapters/google/google-genai-adapter.ts +5 -0
- package/src/service-adapters/openai/openai-adapter.ts +0 -14
- package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +172 -387
- package/dist/chunk-4JBKY7XT.mjs.map +0 -1
- package/dist/chunk-Z5GYTKMD.mjs.map +0 -1
- /package/dist/{chunk-SMDVD4VG.mjs.map → chunk-KCYFFRJY.mjs.map} +0 -0
- /package/dist/{chunk-5YGKE5SN.mjs.map → chunk-R5D7D7YN.mjs.map} +0 -0
- /package/dist/{chunk-UUXRYAB4.mjs.map → chunk-RCCT2GOF.mjs.map} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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("
|
|
164
|
-
it("should filter out
|
|
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", "
|
|
149
|
+
const userMessage = new TextMessage("user", "Set theme to orange");
|
|
175
150
|
|
|
176
|
-
//
|
|
177
|
-
const
|
|
178
|
-
id: "
|
|
179
|
-
name: "
|
|
180
|
-
arguments: '{"
|
|
151
|
+
// Tool execution
|
|
152
|
+
const toolExecution = new ActionExecutionMessage({
|
|
153
|
+
id: "tool-123",
|
|
154
|
+
name: "setThemeColor",
|
|
155
|
+
arguments: '{"themeColor": "orange"}',
|
|
181
156
|
});
|
|
182
157
|
|
|
183
|
-
//
|
|
184
|
-
const
|
|
185
|
-
actionExecutionId: "
|
|
186
|
-
result:
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
{
|
|
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
|
-
|
|
242
|
-
|
|
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:
|
|
252
|
-
tool_use_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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
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
|
|
300
|
-
const
|
|
301
|
-
id: "tool
|
|
302
|
-
name: "
|
|
303
|
-
arguments:
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
//
|
|
334
|
-
const
|
|
335
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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", "
|
|
289
|
+
const userMessage = new TextMessage("user", "Set theme to orange");
|
|
399
290
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
309
|
+
const mockStream = {
|
|
310
|
+
sendTextMessageStart: jest.fn(),
|
|
311
|
+
sendTextMessageContent: jest.fn(),
|
|
312
|
+
sendTextMessageEnd: jest.fn(),
|
|
313
|
+
complete: jest.fn(),
|
|
479
314
|
};
|
|
480
315
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
532
|
-
expect(
|
|
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
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
arguments: '{"arg":"value"}',
|
|
347
|
+
const toolExecution = new ActionExecutionMessage({
|
|
348
|
+
id: "tool-123",
|
|
349
|
+
name: "someAction",
|
|
350
|
+
arguments: "{}",
|
|
552
351
|
});
|
|
553
352
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
result: '{"result":"success"}',
|
|
353
|
+
const toolResult = new ResultMessage({
|
|
354
|
+
actionExecutionId: "tool-123",
|
|
355
|
+
result: "", // Empty result
|
|
558
356
|
});
|
|
559
357
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
actionExecutionId: "invalid-tool-id",
|
|
563
|
-
result: '{"result":"failure"}',
|
|
358
|
+
mockAnthropicCreate.mockResolvedValue({
|
|
359
|
+
[Symbol.asyncIterator]: async function* () {},
|
|
564
360
|
});
|
|
565
361
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
590
|
-
|
|
369
|
+
mockEventSource.stream.mockImplementation((callback) => {
|
|
370
|
+
callback(mockStream);
|
|
371
|
+
return Promise.resolve();
|
|
372
|
+
});
|
|
591
373
|
|
|
592
|
-
|
|
374
|
+
await adapter.process({
|
|
593
375
|
threadId: "test-thread",
|
|
594
|
-
|
|
595
|
-
messages: [systemMessage],
|
|
376
|
+
messages: [systemMessage, toolExecution, toolResult],
|
|
596
377
|
actions: [],
|
|
597
378
|
eventSource: mockEventSource,
|
|
598
379
|
forwardedParameters: {},
|
|
599
380
|
});
|
|
600
381
|
|
|
601
|
-
|
|
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
|
});
|