@copilotkit/runtime 0.0.0-0.0.0-max-changeset-10101010101010-20260109191632
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/.eslintrc.js +7 -0
- package/CHANGELOG.md +2905 -0
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/__snapshots__/schema/schema.graphql +371 -0
- package/dist/index.d.ts +1495 -0
- package/dist/index.js +5644 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5601 -0
- package/dist/index.mjs.map +1 -0
- package/dist/langgraph.d.ts +284 -0
- package/dist/langgraph.js +211 -0
- package/dist/langgraph.js.map +1 -0
- package/dist/langgraph.mjs +206 -0
- package/dist/langgraph.mjs.map +1 -0
- package/dist/v2/index.d.ts +2 -0
- package/dist/v2/index.js +22 -0
- package/dist/v2/index.js.map +1 -0
- package/dist/v2/index.mjs +5 -0
- package/dist/v2/index.mjs.map +1 -0
- package/jest.config.js +10 -0
- package/package.json +143 -0
- package/scripts/generate-gql-schema.ts +13 -0
- package/src/agents/langgraph/event-source.ts +329 -0
- package/src/agents/langgraph/events.ts +377 -0
- package/src/graphql/inputs/action.input.ts +16 -0
- package/src/graphql/inputs/agent-session.input.ts +13 -0
- package/src/graphql/inputs/agent-state.input.ts +13 -0
- package/src/graphql/inputs/cloud-guardrails.input.ts +16 -0
- package/src/graphql/inputs/cloud.input.ts +8 -0
- package/src/graphql/inputs/context-property.input.ts +10 -0
- package/src/graphql/inputs/copilot-context.input.ts +10 -0
- package/src/graphql/inputs/custom-property.input.ts +15 -0
- package/src/graphql/inputs/extensions.input.ts +21 -0
- package/src/graphql/inputs/forwarded-parameters.input.ts +22 -0
- package/src/graphql/inputs/frontend.input.ts +14 -0
- package/src/graphql/inputs/generate-copilot-response.input.ts +59 -0
- package/src/graphql/inputs/load-agent-state.input.ts +10 -0
- package/src/graphql/inputs/message.input.ts +110 -0
- package/src/graphql/inputs/meta-event.input.ts +18 -0
- package/src/graphql/message-conversion/agui-to-gql.test.ts +1263 -0
- package/src/graphql/message-conversion/agui-to-gql.ts +333 -0
- package/src/graphql/message-conversion/gql-to-agui.test.ts +1580 -0
- package/src/graphql/message-conversion/gql-to-agui.ts +278 -0
- package/src/graphql/message-conversion/index.ts +2 -0
- package/src/graphql/message-conversion/roundtrip-conversion.test.ts +526 -0
- package/src/graphql/resolvers/copilot.resolver.ts +708 -0
- package/src/graphql/resolvers/state.resolver.ts +27 -0
- package/src/graphql/types/agents-response.type.ts +19 -0
- package/src/graphql/types/base/index.ts +10 -0
- package/src/graphql/types/converted/index.ts +176 -0
- package/src/graphql/types/copilot-response.type.ts +138 -0
- package/src/graphql/types/enums.ts +38 -0
- package/src/graphql/types/extensions-response.type.ts +23 -0
- package/src/graphql/types/guardrails-result.type.ts +20 -0
- package/src/graphql/types/load-agent-state-response.type.ts +17 -0
- package/src/graphql/types/message-status.type.ts +42 -0
- package/src/graphql/types/meta-events.type.ts +71 -0
- package/src/graphql/types/response-status.type.ts +66 -0
- package/src/index.ts +4 -0
- package/src/langgraph.ts +1 -0
- package/src/lib/cloud/index.ts +4 -0
- package/src/lib/error-messages.ts +200 -0
- package/src/lib/index.ts +52 -0
- package/src/lib/integrations/index.ts +6 -0
- package/src/lib/integrations/nest/index.ts +14 -0
- package/src/lib/integrations/nextjs/app-router.ts +38 -0
- package/src/lib/integrations/nextjs/pages-router.ts +39 -0
- package/src/lib/integrations/node-express/index.ts +14 -0
- package/src/lib/integrations/node-http/index.ts +143 -0
- package/src/lib/integrations/node-http/request-handler.ts +111 -0
- package/src/lib/integrations/shared.ts +161 -0
- package/src/lib/logger.ts +28 -0
- package/src/lib/observability.ts +160 -0
- package/src/lib/runtime/__tests__/copilot-runtime-error.test.ts +169 -0
- package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +464 -0
- package/src/lib/runtime/agent-integrations/langgraph/agent.ts +209 -0
- package/src/lib/runtime/agent-integrations/langgraph/consts.ts +34 -0
- package/src/lib/runtime/agent-integrations/langgraph/index.ts +2 -0
- package/src/lib/runtime/copilot-runtime.ts +710 -0
- package/src/lib/runtime/mcp-tools-utils.ts +254 -0
- package/src/lib/runtime/retry-utils.ts +96 -0
- package/src/lib/runtime/telemetry-agent-runner.ts +139 -0
- package/src/lib/runtime/types.ts +49 -0
- package/src/lib/runtime/utils.ts +87 -0
- package/src/lib/streaming.ts +202 -0
- package/src/lib/telemetry-client.ts +64 -0
- package/src/service-adapters/anthropic/anthropic-adapter.ts +452 -0
- package/src/service-adapters/anthropic/utils.ts +152 -0
- package/src/service-adapters/bedrock/bedrock-adapter.ts +73 -0
- package/src/service-adapters/conversion.ts +67 -0
- package/src/service-adapters/empty/empty-adapter.ts +38 -0
- package/src/service-adapters/events.ts +294 -0
- package/src/service-adapters/experimental/ollama/ollama-adapter.ts +84 -0
- package/src/service-adapters/google/google-genai-adapter.test.ts +104 -0
- package/src/service-adapters/google/google-genai-adapter.ts +88 -0
- package/src/service-adapters/groq/groq-adapter.ts +203 -0
- package/src/service-adapters/index.ts +18 -0
- package/src/service-adapters/langchain/langchain-adapter.ts +111 -0
- package/src/service-adapters/langchain/langserve.ts +88 -0
- package/src/service-adapters/langchain/types.ts +14 -0
- package/src/service-adapters/langchain/utils.ts +313 -0
- package/src/service-adapters/openai/openai-adapter.ts +283 -0
- package/src/service-adapters/openai/openai-assistant-adapter.ts +344 -0
- package/src/service-adapters/openai/utils.ts +199 -0
- package/src/service-adapters/service-adapter.ts +41 -0
- package/src/service-adapters/shared/error-utils.ts +61 -0
- package/src/service-adapters/shared/index.ts +1 -0
- package/src/service-adapters/unify/unify-adapter.ts +151 -0
- package/src/utils/failed-response-status-reasons.ts +70 -0
- package/src/utils/index.ts +1 -0
- package/src/v2/index.ts +3 -0
- package/tests/global.d.ts +13 -0
- package/tests/service-adapters/anthropic/allowlist-approach.test.ts +226 -0
- package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +389 -0
- package/tests/service-adapters/openai/allowlist-approach.test.ts +238 -0
- package/tests/service-adapters/openai/openai-adapter.test.ts +301 -0
- package/tests/setup.jest.ts +21 -0
- package/tests/tsconfig.json +10 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +20 -0
- package/typedoc.json +4 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AnthropicAdapter } from "../../../src/service-adapters/anthropic/anthropic-adapter";
|
|
6
|
+
|
|
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
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Mock the message classes
|
|
19
|
+
jest.mock("../../../src/graphql/types/converted", () => {
|
|
20
|
+
class MockTextMessage {
|
|
21
|
+
content: string;
|
|
22
|
+
role: string;
|
|
23
|
+
id: string;
|
|
24
|
+
|
|
25
|
+
constructor(role: string, content: string) {
|
|
26
|
+
this.role = role;
|
|
27
|
+
this.content = content;
|
|
28
|
+
this.id = "mock-text-" + Math.random().toString(36).substring(7);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
isTextMessage() {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
isImageMessage() {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
isActionExecutionMessage() {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
isResultMessage() {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class MockActionExecutionMessage {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
arguments: string;
|
|
49
|
+
|
|
50
|
+
constructor(params: { id: string; name: string; arguments: string }) {
|
|
51
|
+
this.id = params.id;
|
|
52
|
+
this.name = params.name;
|
|
53
|
+
this.arguments = params.arguments;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isTextMessage() {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
isImageMessage() {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
isActionExecutionMessage() {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
isResultMessage() {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class MockResultMessage {
|
|
71
|
+
actionExecutionId: string;
|
|
72
|
+
result: string;
|
|
73
|
+
id: string;
|
|
74
|
+
|
|
75
|
+
constructor(params: { actionExecutionId: string; result: string }) {
|
|
76
|
+
this.actionExecutionId = params.actionExecutionId;
|
|
77
|
+
this.result = params.result;
|
|
78
|
+
this.id = "mock-result-" + Math.random().toString(36).substring(7);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
isTextMessage() {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
isImageMessage() {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
isActionExecutionMessage() {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
isResultMessage() {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
TextMessage: MockTextMessage,
|
|
97
|
+
ActionExecutionMessage: MockActionExecutionMessage,
|
|
98
|
+
ResultMessage: MockResultMessage,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("AnthropicAdapter", () => {
|
|
103
|
+
let adapter: AnthropicAdapter;
|
|
104
|
+
let mockEventSource: any;
|
|
105
|
+
let mockAnthropicCreate: jest.Mock;
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
jest.clearAllMocks();
|
|
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
|
+
|
|
123
|
+
mockEventSource = {
|
|
124
|
+
stream: jest.fn((callback) => {
|
|
125
|
+
const mockStream = {
|
|
126
|
+
sendTextMessageStart: jest.fn(),
|
|
127
|
+
sendTextMessageContent: jest.fn(),
|
|
128
|
+
sendTextMessageEnd: jest.fn(),
|
|
129
|
+
sendActionExecutionStart: jest.fn(),
|
|
130
|
+
sendActionExecutionArgs: jest.fn(),
|
|
131
|
+
sendActionExecutionEnd: jest.fn(),
|
|
132
|
+
complete: jest.fn(),
|
|
133
|
+
};
|
|
134
|
+
callback(mockStream);
|
|
135
|
+
return Promise.resolve();
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("Deduplication Logic", () => {
|
|
141
|
+
it("should filter out duplicate result messages", async () => {
|
|
142
|
+
const {
|
|
143
|
+
TextMessage,
|
|
144
|
+
ActionExecutionMessage,
|
|
145
|
+
ResultMessage,
|
|
146
|
+
} = require("../../../src/graphql/types/converted");
|
|
147
|
+
|
|
148
|
+
const systemMessage = new TextMessage("system", "System message");
|
|
149
|
+
const userMessage = new TextMessage("user", "Set theme to orange");
|
|
150
|
+
|
|
151
|
+
// Tool execution
|
|
152
|
+
const toolExecution = new ActionExecutionMessage({
|
|
153
|
+
id: "tool-123",
|
|
154
|
+
name: "setThemeColor",
|
|
155
|
+
arguments: '{"themeColor": "orange"}',
|
|
156
|
+
});
|
|
157
|
+
|
|
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",
|
|
162
|
+
});
|
|
163
|
+
const result2 = new ResultMessage({
|
|
164
|
+
actionExecutionId: "tool-123",
|
|
165
|
+
result: "Theme color set to orange",
|
|
166
|
+
});
|
|
167
|
+
const result3 = new ResultMessage({
|
|
168
|
+
actionExecutionId: "tool-123",
|
|
169
|
+
result: "Theme color set to orange",
|
|
170
|
+
});
|
|
171
|
+
|
|
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
|
+
});
|
|
178
|
+
|
|
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: {},
|
|
186
|
+
});
|
|
187
|
+
|
|
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({
|
|
197
|
+
role: "assistant",
|
|
198
|
+
content: [
|
|
199
|
+
expect.objectContaining({
|
|
200
|
+
type: "tool_use",
|
|
201
|
+
id: "tool-123",
|
|
202
|
+
name: "setThemeColor",
|
|
203
|
+
}),
|
|
204
|
+
],
|
|
205
|
+
}),
|
|
206
|
+
expect.objectContaining({
|
|
207
|
+
role: "user",
|
|
208
|
+
content: [
|
|
209
|
+
expect.objectContaining({
|
|
210
|
+
type: "tool_result",
|
|
211
|
+
content: "Theme color set to orange",
|
|
212
|
+
tool_use_id: "tool-123",
|
|
213
|
+
}),
|
|
214
|
+
],
|
|
215
|
+
}),
|
|
216
|
+
]),
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
|
|
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);
|
|
223
|
+
|
|
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);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should filter out invalid result messages without corresponding tool_use", async () => {
|
|
232
|
+
const {
|
|
233
|
+
TextMessage,
|
|
234
|
+
ActionExecutionMessage,
|
|
235
|
+
ResultMessage,
|
|
236
|
+
} = require("../../../src/graphql/types/converted");
|
|
237
|
+
|
|
238
|
+
const systemMessage = new TextMessage("system", "System message");
|
|
239
|
+
|
|
240
|
+
// Valid tool execution
|
|
241
|
+
const validTool = new ActionExecutionMessage({
|
|
242
|
+
id: "valid-tool",
|
|
243
|
+
name: "validAction",
|
|
244
|
+
arguments: "{}",
|
|
245
|
+
});
|
|
246
|
+
const validResult = new ResultMessage({
|
|
247
|
+
actionExecutionId: "valid-tool",
|
|
248
|
+
result: "Valid result",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Invalid result with no corresponding tool_use
|
|
252
|
+
const invalidResult = new ResultMessage({
|
|
253
|
+
actionExecutionId: "nonexistent-tool",
|
|
254
|
+
result: "Invalid result",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
mockAnthropicCreate.mockResolvedValue({
|
|
258
|
+
[Symbol.asyncIterator]: async function* () {},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await adapter.process({
|
|
262
|
+
threadId: "test-thread",
|
|
263
|
+
messages: [systemMessage, validTool, validResult, invalidResult],
|
|
264
|
+
actions: [],
|
|
265
|
+
eventSource: mockEventSource,
|
|
266
|
+
forwardedParameters: {},
|
|
267
|
+
});
|
|
268
|
+
|
|
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");
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("Fallback Response Logic", () => {
|
|
281
|
+
it("should generate contextual fallback when Anthropic returns no content", async () => {
|
|
282
|
+
const {
|
|
283
|
+
TextMessage,
|
|
284
|
+
ActionExecutionMessage,
|
|
285
|
+
ResultMessage,
|
|
286
|
+
} = require("../../../src/graphql/types/converted");
|
|
287
|
+
|
|
288
|
+
const systemMessage = new TextMessage("system", "System message");
|
|
289
|
+
const userMessage = new TextMessage("user", "Set theme to orange");
|
|
290
|
+
|
|
291
|
+
const toolExecution = new ActionExecutionMessage({
|
|
292
|
+
id: "tool-123",
|
|
293
|
+
name: "setThemeColor",
|
|
294
|
+
arguments: '{"themeColor": "orange"}',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const toolResult = new ResultMessage({
|
|
298
|
+
actionExecutionId: "tool-123",
|
|
299
|
+
result: "Theme color set to orange",
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Mock Anthropic to return empty stream
|
|
303
|
+
mockAnthropicCreate.mockResolvedValue({
|
|
304
|
+
[Symbol.asyncIterator]: async function* () {
|
|
305
|
+
// No content blocks - simulates Anthropic not responding
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const mockStream = {
|
|
310
|
+
sendTextMessageStart: jest.fn(),
|
|
311
|
+
sendTextMessageContent: jest.fn(),
|
|
312
|
+
sendTextMessageEnd: jest.fn(),
|
|
313
|
+
complete: jest.fn(),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
mockEventSource.stream.mockImplementation((callback) => {
|
|
317
|
+
callback(mockStream);
|
|
318
|
+
return Promise.resolve();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await adapter.process({
|
|
322
|
+
threadId: "test-thread",
|
|
323
|
+
messages: [systemMessage, userMessage, toolExecution, toolResult],
|
|
324
|
+
actions: [],
|
|
325
|
+
eventSource: mockEventSource,
|
|
326
|
+
forwardedParameters: {},
|
|
327
|
+
});
|
|
328
|
+
|
|
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();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("should use generic fallback when no tool result content available", async () => {
|
|
339
|
+
const {
|
|
340
|
+
TextMessage,
|
|
341
|
+
ActionExecutionMessage,
|
|
342
|
+
ResultMessage,
|
|
343
|
+
} = require("../../../src/graphql/types/converted");
|
|
344
|
+
|
|
345
|
+
const systemMessage = new TextMessage("system", "System message");
|
|
346
|
+
|
|
347
|
+
const toolExecution = new ActionExecutionMessage({
|
|
348
|
+
id: "tool-123",
|
|
349
|
+
name: "someAction",
|
|
350
|
+
arguments: "{}",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const toolResult = new ResultMessage({
|
|
354
|
+
actionExecutionId: "tool-123",
|
|
355
|
+
result: "", // Empty result
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
mockAnthropicCreate.mockResolvedValue({
|
|
359
|
+
[Symbol.asyncIterator]: async function* () {},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const mockStream = {
|
|
363
|
+
sendTextMessageStart: jest.fn(),
|
|
364
|
+
sendTextMessageContent: jest.fn(),
|
|
365
|
+
sendTextMessageEnd: jest.fn(),
|
|
366
|
+
complete: jest.fn(),
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
mockEventSource.stream.mockImplementation((callback) => {
|
|
370
|
+
callback(mockStream);
|
|
371
|
+
return Promise.resolve();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
await adapter.process({
|
|
375
|
+
threadId: "test-thread",
|
|
376
|
+
messages: [systemMessage, toolExecution, toolResult],
|
|
377
|
+
actions: [],
|
|
378
|
+
eventSource: mockEventSource,
|
|
379
|
+
forwardedParameters: {},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Should use generic fallback
|
|
383
|
+
expect(mockStream.sendTextMessageContent).toHaveBeenCalledWith({
|
|
384
|
+
messageId: expect.any(String),
|
|
385
|
+
content: "Task completed successfully.",
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "@jest/globals";
|
|
6
|
+
|
|
7
|
+
describe("OpenAI Adapter - Allowlist Approach", () => {
|
|
8
|
+
it("should filter out tool_result messages with no corresponding tool_call ID", () => {
|
|
9
|
+
// Setup test data
|
|
10
|
+
const validToolCallIds = new Set<string>(["valid-id-1", "valid-id-2"]);
|
|
11
|
+
|
|
12
|
+
// Messages to filter - valid and invalid ones
|
|
13
|
+
const messages = [
|
|
14
|
+
{ type: "text", role: "user", content: "Hello" },
|
|
15
|
+
{ type: "tool_result", actionExecutionId: "valid-id-1", result: "result1" },
|
|
16
|
+
{ type: "tool_result", actionExecutionId: "invalid-id", result: "invalid" },
|
|
17
|
+
{ type: "tool_result", actionExecutionId: "valid-id-2", result: "result2" },
|
|
18
|
+
{ type: "tool_result", actionExecutionId: "valid-id-1", result: "duplicate" }, // Duplicate ID
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Implement the filtering logic, similar to the adapter
|
|
22
|
+
const filteredMessages = messages.filter((message) => {
|
|
23
|
+
if (message.type === "tool_result") {
|
|
24
|
+
// Skip if there's no corresponding tool_call
|
|
25
|
+
if (!validToolCallIds.has(message.actionExecutionId)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Remove this ID from valid IDs so we don't process duplicates
|
|
30
|
+
validToolCallIds.delete(message.actionExecutionId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Keep all non-tool-result messages
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Verify results
|
|
38
|
+
expect(filteredMessages.length).toBe(3); // text + 2 valid tool results (no duplicates or invalid)
|
|
39
|
+
|
|
40
|
+
// Valid results should be included
|
|
41
|
+
expect(
|
|
42
|
+
filteredMessages.some(
|
|
43
|
+
(m) => m.type === "tool_result" && m.actionExecutionId === "valid-id-1",
|
|
44
|
+
),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
|
|
47
|
+
expect(
|
|
48
|
+
filteredMessages.some(
|
|
49
|
+
(m) => m.type === "tool_result" && m.actionExecutionId === "valid-id-2",
|
|
50
|
+
),
|
|
51
|
+
).toBe(true);
|
|
52
|
+
|
|
53
|
+
// Invalid result should be excluded
|
|
54
|
+
expect(
|
|
55
|
+
filteredMessages.some(
|
|
56
|
+
(m) => m.type === "tool_result" && m.actionExecutionId === "invalid-id",
|
|
57
|
+
),
|
|
58
|
+
).toBe(false);
|
|
59
|
+
|
|
60
|
+
// Duplicate should be excluded - we used a different approach than Anthropic
|
|
61
|
+
const validId1Count = filteredMessages.filter(
|
|
62
|
+
(m) => m.type === "tool_result" && m.actionExecutionId === "valid-id-1",
|
|
63
|
+
).length;
|
|
64
|
+
|
|
65
|
+
expect(validId1Count).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should maintain correct order of messages when filtering", () => {
|
|
69
|
+
// Setup test data
|
|
70
|
+
const validToolCallIds = new Set<string>(["tool-1", "tool-2", "tool-3"]);
|
|
71
|
+
|
|
72
|
+
// Test with a complex conversation pattern
|
|
73
|
+
const messages = [
|
|
74
|
+
{ type: "text", role: "user", content: "Initial message" },
|
|
75
|
+
{ type: "text", role: "assistant", content: "I'll help with that" },
|
|
76
|
+
{ type: "tool_call", id: "tool-1", name: "firstTool" },
|
|
77
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "result1" },
|
|
78
|
+
{ type: "text", role: "assistant", content: "Got the first result" },
|
|
79
|
+
{ type: "tool_call", id: "tool-2", name: "secondTool" },
|
|
80
|
+
{ type: "tool_result", actionExecutionId: "tool-2", result: "result2" },
|
|
81
|
+
{ type: "tool_result", actionExecutionId: "invalid-id", result: "invalid-result" },
|
|
82
|
+
{ type: "tool_call", id: "tool-3", name: "thirdTool" },
|
|
83
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "duplicate-result" }, // Duplicate
|
|
84
|
+
{ type: "tool_result", actionExecutionId: "tool-3", result: "result3" },
|
|
85
|
+
{ type: "text", role: "user", content: "Final message" },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Apply OpenAI's filter approach (using filter instead of loop)
|
|
89
|
+
const filteredMessages = messages.filter((message) => {
|
|
90
|
+
if (message.type === "tool_result") {
|
|
91
|
+
// Skip if there's no corresponding tool_call
|
|
92
|
+
if (!validToolCallIds.has(message.actionExecutionId)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Remove this ID from valid IDs so we don't process duplicates
|
|
97
|
+
validToolCallIds.delete(message.actionExecutionId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Keep all non-tool-result messages
|
|
101
|
+
return true;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Verify results
|
|
105
|
+
expect(filteredMessages.length).toBe(10); // 12 original - 2 filtered out
|
|
106
|
+
|
|
107
|
+
// Check that the message order is preserved
|
|
108
|
+
expect(filteredMessages[0].type).toBe("text"); // Initial user message
|
|
109
|
+
expect(filteredMessages[0].content).toBe("Initial message");
|
|
110
|
+
expect(filteredMessages[1].type).toBe("text"); // Assistant response
|
|
111
|
+
expect(filteredMessages[2].type).toBe("tool_call"); // First tool
|
|
112
|
+
expect(filteredMessages[3].type).toBe("tool_result"); // First result
|
|
113
|
+
expect(filteredMessages[3].actionExecutionId).toBe("tool-1");
|
|
114
|
+
expect(filteredMessages[4].type).toBe("text"); // Assistant comment
|
|
115
|
+
expect(filteredMessages[5].type).toBe("tool_call"); // Second tool
|
|
116
|
+
expect(filteredMessages[6].type).toBe("tool_result"); // Second result
|
|
117
|
+
expect(filteredMessages[6].actionExecutionId).toBe("tool-2");
|
|
118
|
+
expect(filteredMessages[7].type).toBe("tool_call"); // Third tool
|
|
119
|
+
expect(filteredMessages[8].type).toBe("tool_result"); // Third result
|
|
120
|
+
expect(filteredMessages[8].actionExecutionId).toBe("tool-3");
|
|
121
|
+
expect(filteredMessages[9].type).toBe("text"); // Final user message
|
|
122
|
+
|
|
123
|
+
// Each valid tool result should appear exactly once
|
|
124
|
+
const toolResultCounts = new Map();
|
|
125
|
+
filteredMessages.forEach((message) => {
|
|
126
|
+
if (message.type === "tool_result") {
|
|
127
|
+
const id = message.actionExecutionId;
|
|
128
|
+
toolResultCounts.set(id, (toolResultCounts.get(id) || 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(toolResultCounts.size).toBe(3); // Should have 3 different tool results
|
|
133
|
+
expect(toolResultCounts.get("tool-1")).toBe(1);
|
|
134
|
+
expect(toolResultCounts.get("tool-2")).toBe(1);
|
|
135
|
+
expect(toolResultCounts.get("tool-3")).toBe(1);
|
|
136
|
+
expect(toolResultCounts.has("invalid-id")).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should handle empty message array", () => {
|
|
140
|
+
const validToolCallIds = new Set<string>(["valid-id-1", "valid-id-2"]);
|
|
141
|
+
const messages = [];
|
|
142
|
+
|
|
143
|
+
// Apply OpenAI's filter approach
|
|
144
|
+
const filteredMessages = messages.filter((message) => {
|
|
145
|
+
if (message.type === "tool_result") {
|
|
146
|
+
if (!validToolCallIds.has(message.actionExecutionId)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
validToolCallIds.delete(message.actionExecutionId);
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(filteredMessages.length).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should handle edge cases with mixed message types", () => {
|
|
158
|
+
// Setup test data with various message types
|
|
159
|
+
const validToolCallIds = new Set<string>(["valid-id-1"]);
|
|
160
|
+
|
|
161
|
+
const messages = [
|
|
162
|
+
{ type: "text", role: "user", content: "Hello" },
|
|
163
|
+
{ type: "image", url: "https://example.com/image.jpg" }, // Non-tool message type
|
|
164
|
+
{ type: "tool_result", actionExecutionId: "valid-id-1", result: "result1" },
|
|
165
|
+
{ type: "custom", data: { key: "value" } }, // Another custom type
|
|
166
|
+
{ type: "tool_result", actionExecutionId: "valid-id-1", result: "duplicate" }, // Duplicate
|
|
167
|
+
{ type: "null", value: null }, // Edge case
|
|
168
|
+
{ type: "undefined" }, // Edge case
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// Apply OpenAI's filter approach
|
|
172
|
+
const filteredMessages = messages.filter((message) => {
|
|
173
|
+
if (message.type === "tool_result") {
|
|
174
|
+
if (!validToolCallIds.has(message.actionExecutionId)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
validToolCallIds.delete(message.actionExecutionId);
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Should have all non-tool_result messages + 1 valid tool_result
|
|
183
|
+
expect(filteredMessages.length).toBe(6); // 7 original - 1 duplicate
|
|
184
|
+
|
|
185
|
+
// Valid tool_result should be included exactly once
|
|
186
|
+
const toolResults = filteredMessages.filter((m) => m.type === "tool_result");
|
|
187
|
+
expect(toolResults.length).toBe(1);
|
|
188
|
+
expect(toolResults[0].actionExecutionId).toBe("valid-id-1");
|
|
189
|
+
|
|
190
|
+
// All non-tool_result messages should be preserved
|
|
191
|
+
expect(filteredMessages.filter((m) => m.type === "text").length).toBe(1);
|
|
192
|
+
expect(filteredMessages.filter((m) => m.type === "image").length).toBe(1);
|
|
193
|
+
expect(filteredMessages.filter((m) => m.type === "custom").length).toBe(1);
|
|
194
|
+
expect(filteredMessages.filter((m) => m.type === "null").length).toBe(1);
|
|
195
|
+
expect(filteredMessages.filter((m) => m.type === "undefined").length).toBe(1);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should properly handle multiple duplicate tool results", () => {
|
|
199
|
+
// Setup test data with multiple duplicates
|
|
200
|
+
const validToolCallIds = new Set<string>(["tool-1", "tool-2"]);
|
|
201
|
+
|
|
202
|
+
const messages = [
|
|
203
|
+
{ type: "text", role: "user", content: "Initial prompt" },
|
|
204
|
+
{ type: "tool_call", id: "tool-1", name: "firstTool" },
|
|
205
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "first-result" },
|
|
206
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "duplicate-1" }, // Duplicate 1
|
|
207
|
+
{ type: "tool_call", id: "tool-2", name: "secondTool" },
|
|
208
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "duplicate-2" }, // Duplicate 2
|
|
209
|
+
{ type: "tool_result", actionExecutionId: "tool-2", result: "second-result" },
|
|
210
|
+
{ type: "tool_result", actionExecutionId: "tool-2", result: "duplicate-3" }, // Duplicate 3
|
|
211
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "duplicate-4" }, // Duplicate 4
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
// Apply OpenAI's filter approach
|
|
215
|
+
const filteredMessages = messages.filter((message) => {
|
|
216
|
+
if (message.type === "tool_result") {
|
|
217
|
+
if (!validToolCallIds.has(message.actionExecutionId)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
validToolCallIds.delete(message.actionExecutionId);
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Should have text + tool calls + only the first occurrence of each tool result
|
|
226
|
+
expect(filteredMessages.length).toBe(5);
|
|
227
|
+
|
|
228
|
+
// Check that only the first occurrence of each tool result is kept
|
|
229
|
+
const toolResults = filteredMessages.filter((m) => m.type === "tool_result");
|
|
230
|
+
expect(toolResults.length).toBe(2);
|
|
231
|
+
|
|
232
|
+
expect(toolResults[0].actionExecutionId).toBe("tool-1");
|
|
233
|
+
expect(toolResults[0].result).toBe("first-result"); // First occurrence should be kept
|
|
234
|
+
|
|
235
|
+
expect(toolResults[1].actionExecutionId).toBe("tool-2");
|
|
236
|
+
expect(toolResults[1].result).toBe("second-result"); // First occurrence should be kept
|
|
237
|
+
});
|
|
238
|
+
});
|