@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.
Files changed (122) hide show
  1. package/.eslintrc.js +7 -0
  2. package/CHANGELOG.md +2905 -0
  3. package/LICENSE +21 -0
  4. package/README.md +76 -0
  5. package/__snapshots__/schema/schema.graphql +371 -0
  6. package/dist/index.d.ts +1495 -0
  7. package/dist/index.js +5644 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/index.mjs +5601 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/dist/langgraph.d.ts +284 -0
  12. package/dist/langgraph.js +211 -0
  13. package/dist/langgraph.js.map +1 -0
  14. package/dist/langgraph.mjs +206 -0
  15. package/dist/langgraph.mjs.map +1 -0
  16. package/dist/v2/index.d.ts +2 -0
  17. package/dist/v2/index.js +22 -0
  18. package/dist/v2/index.js.map +1 -0
  19. package/dist/v2/index.mjs +5 -0
  20. package/dist/v2/index.mjs.map +1 -0
  21. package/jest.config.js +10 -0
  22. package/package.json +143 -0
  23. package/scripts/generate-gql-schema.ts +13 -0
  24. package/src/agents/langgraph/event-source.ts +329 -0
  25. package/src/agents/langgraph/events.ts +377 -0
  26. package/src/graphql/inputs/action.input.ts +16 -0
  27. package/src/graphql/inputs/agent-session.input.ts +13 -0
  28. package/src/graphql/inputs/agent-state.input.ts +13 -0
  29. package/src/graphql/inputs/cloud-guardrails.input.ts +16 -0
  30. package/src/graphql/inputs/cloud.input.ts +8 -0
  31. package/src/graphql/inputs/context-property.input.ts +10 -0
  32. package/src/graphql/inputs/copilot-context.input.ts +10 -0
  33. package/src/graphql/inputs/custom-property.input.ts +15 -0
  34. package/src/graphql/inputs/extensions.input.ts +21 -0
  35. package/src/graphql/inputs/forwarded-parameters.input.ts +22 -0
  36. package/src/graphql/inputs/frontend.input.ts +14 -0
  37. package/src/graphql/inputs/generate-copilot-response.input.ts +59 -0
  38. package/src/graphql/inputs/load-agent-state.input.ts +10 -0
  39. package/src/graphql/inputs/message.input.ts +110 -0
  40. package/src/graphql/inputs/meta-event.input.ts +18 -0
  41. package/src/graphql/message-conversion/agui-to-gql.test.ts +1263 -0
  42. package/src/graphql/message-conversion/agui-to-gql.ts +333 -0
  43. package/src/graphql/message-conversion/gql-to-agui.test.ts +1580 -0
  44. package/src/graphql/message-conversion/gql-to-agui.ts +278 -0
  45. package/src/graphql/message-conversion/index.ts +2 -0
  46. package/src/graphql/message-conversion/roundtrip-conversion.test.ts +526 -0
  47. package/src/graphql/resolvers/copilot.resolver.ts +708 -0
  48. package/src/graphql/resolvers/state.resolver.ts +27 -0
  49. package/src/graphql/types/agents-response.type.ts +19 -0
  50. package/src/graphql/types/base/index.ts +10 -0
  51. package/src/graphql/types/converted/index.ts +176 -0
  52. package/src/graphql/types/copilot-response.type.ts +138 -0
  53. package/src/graphql/types/enums.ts +38 -0
  54. package/src/graphql/types/extensions-response.type.ts +23 -0
  55. package/src/graphql/types/guardrails-result.type.ts +20 -0
  56. package/src/graphql/types/load-agent-state-response.type.ts +17 -0
  57. package/src/graphql/types/message-status.type.ts +42 -0
  58. package/src/graphql/types/meta-events.type.ts +71 -0
  59. package/src/graphql/types/response-status.type.ts +66 -0
  60. package/src/index.ts +4 -0
  61. package/src/langgraph.ts +1 -0
  62. package/src/lib/cloud/index.ts +4 -0
  63. package/src/lib/error-messages.ts +200 -0
  64. package/src/lib/index.ts +52 -0
  65. package/src/lib/integrations/index.ts +6 -0
  66. package/src/lib/integrations/nest/index.ts +14 -0
  67. package/src/lib/integrations/nextjs/app-router.ts +38 -0
  68. package/src/lib/integrations/nextjs/pages-router.ts +39 -0
  69. package/src/lib/integrations/node-express/index.ts +14 -0
  70. package/src/lib/integrations/node-http/index.ts +143 -0
  71. package/src/lib/integrations/node-http/request-handler.ts +111 -0
  72. package/src/lib/integrations/shared.ts +161 -0
  73. package/src/lib/logger.ts +28 -0
  74. package/src/lib/observability.ts +160 -0
  75. package/src/lib/runtime/__tests__/copilot-runtime-error.test.ts +169 -0
  76. package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +464 -0
  77. package/src/lib/runtime/agent-integrations/langgraph/agent.ts +209 -0
  78. package/src/lib/runtime/agent-integrations/langgraph/consts.ts +34 -0
  79. package/src/lib/runtime/agent-integrations/langgraph/index.ts +2 -0
  80. package/src/lib/runtime/copilot-runtime.ts +710 -0
  81. package/src/lib/runtime/mcp-tools-utils.ts +254 -0
  82. package/src/lib/runtime/retry-utils.ts +96 -0
  83. package/src/lib/runtime/telemetry-agent-runner.ts +139 -0
  84. package/src/lib/runtime/types.ts +49 -0
  85. package/src/lib/runtime/utils.ts +87 -0
  86. package/src/lib/streaming.ts +202 -0
  87. package/src/lib/telemetry-client.ts +64 -0
  88. package/src/service-adapters/anthropic/anthropic-adapter.ts +452 -0
  89. package/src/service-adapters/anthropic/utils.ts +152 -0
  90. package/src/service-adapters/bedrock/bedrock-adapter.ts +73 -0
  91. package/src/service-adapters/conversion.ts +67 -0
  92. package/src/service-adapters/empty/empty-adapter.ts +38 -0
  93. package/src/service-adapters/events.ts +294 -0
  94. package/src/service-adapters/experimental/ollama/ollama-adapter.ts +84 -0
  95. package/src/service-adapters/google/google-genai-adapter.test.ts +104 -0
  96. package/src/service-adapters/google/google-genai-adapter.ts +88 -0
  97. package/src/service-adapters/groq/groq-adapter.ts +203 -0
  98. package/src/service-adapters/index.ts +18 -0
  99. package/src/service-adapters/langchain/langchain-adapter.ts +111 -0
  100. package/src/service-adapters/langchain/langserve.ts +88 -0
  101. package/src/service-adapters/langchain/types.ts +14 -0
  102. package/src/service-adapters/langchain/utils.ts +313 -0
  103. package/src/service-adapters/openai/openai-adapter.ts +283 -0
  104. package/src/service-adapters/openai/openai-assistant-adapter.ts +344 -0
  105. package/src/service-adapters/openai/utils.ts +199 -0
  106. package/src/service-adapters/service-adapter.ts +41 -0
  107. package/src/service-adapters/shared/error-utils.ts +61 -0
  108. package/src/service-adapters/shared/index.ts +1 -0
  109. package/src/service-adapters/unify/unify-adapter.ts +151 -0
  110. package/src/utils/failed-response-status-reasons.ts +70 -0
  111. package/src/utils/index.ts +1 -0
  112. package/src/v2/index.ts +3 -0
  113. package/tests/global.d.ts +13 -0
  114. package/tests/service-adapters/anthropic/allowlist-approach.test.ts +226 -0
  115. package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +389 -0
  116. package/tests/service-adapters/openai/allowlist-approach.test.ts +238 -0
  117. package/tests/service-adapters/openai/openai-adapter.test.ts +301 -0
  118. package/tests/setup.jest.ts +21 -0
  119. package/tests/tsconfig.json +10 -0
  120. package/tsconfig.json +13 -0
  121. package/tsup.config.ts +20 -0
  122. package/typedoc.json +4 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * CopilotKit Adapter for Unify
3
+ *
4
+ * <RequestExample>
5
+ * ```jsx CopilotRuntime Example
6
+ * const copilotKit = new CopilotRuntime();
7
+ * return copilotKit.response(req, new UnifyAdapter());
8
+ * ```
9
+ * </RequestExample>
10
+ *
11
+ * You can easily set the model to use by passing it to the constructor.
12
+ * ```jsx
13
+ * const copilotKit = new CopilotRuntime();
14
+ * return copilotKit.response(
15
+ * req,
16
+ * new UnifyAdapter({ model: "llama-3-8b-chat@fireworks-ai" }),
17
+ * );
18
+ * ```
19
+ */
20
+ import {
21
+ CopilotRuntimeChatCompletionRequest,
22
+ CopilotRuntimeChatCompletionResponse,
23
+ CopilotServiceAdapter,
24
+ } from "../service-adapter";
25
+ import { randomId, randomUUID } from "@copilotkit/shared";
26
+ import { convertActionInputToOpenAITool, convertMessageToOpenAIMessage } from "../openai/utils";
27
+
28
+ export interface UnifyAdapterParams {
29
+ apiKey?: string;
30
+ model: string;
31
+ }
32
+
33
+ export class UnifyAdapter implements CopilotServiceAdapter {
34
+ private apiKey: string;
35
+ public model: string;
36
+ private start: boolean;
37
+ public provider = "unify";
38
+
39
+ public get name() {
40
+ return "UnifyAdapter";
41
+ }
42
+
43
+ constructor(options?: UnifyAdapterParams) {
44
+ if (options?.apiKey) {
45
+ this.apiKey = options.apiKey;
46
+ } else {
47
+ this.apiKey = "UNIFY_API_KEY";
48
+ }
49
+ this.model = options?.model;
50
+ this.start = true;
51
+ }
52
+
53
+ async process(
54
+ request: CopilotRuntimeChatCompletionRequest,
55
+ ): Promise<CopilotRuntimeChatCompletionResponse> {
56
+ const tools = request.actions.map(convertActionInputToOpenAITool);
57
+ // Lazy require for optional peer dependency
58
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
59
+ const OpenAI = require("openai").default;
60
+ const openai = new OpenAI({
61
+ apiKey: this.apiKey,
62
+ baseURL: "https://api.unify.ai/v0/",
63
+ });
64
+ const forwardedParameters = request.forwardedParameters;
65
+
66
+ const messages = request.messages.map((m) => convertMessageToOpenAIMessage(m));
67
+
68
+ const stream = await openai.chat.completions.create({
69
+ model: this.model,
70
+ messages: messages,
71
+ stream: true,
72
+ ...(tools.length > 0 && { tools }),
73
+ ...(forwardedParameters?.temperature && { temperature: forwardedParameters.temperature }),
74
+ });
75
+
76
+ let model = null;
77
+ let currentMessageId: string;
78
+ let currentToolCallId: string;
79
+ request.eventSource.stream(async (eventStream$) => {
80
+ let mode: "function" | "message" | null = null;
81
+ for await (const chunk of stream) {
82
+ if (this.start) {
83
+ model = chunk.model;
84
+ currentMessageId = randomId();
85
+ eventStream$.sendTextMessageStart({ messageId: currentMessageId });
86
+ eventStream$.sendTextMessageContent({
87
+ messageId: currentMessageId,
88
+ content: `Model used: ${model}\n`,
89
+ });
90
+ eventStream$.sendTextMessageEnd({ messageId: currentMessageId });
91
+ this.start = false;
92
+ }
93
+ const toolCall = chunk.choices[0].delta.tool_calls?.[0];
94
+ const content = chunk.choices[0].delta.content;
95
+
96
+ // When switching from message to function or vice versa,
97
+ // send the respective end event.
98
+ // If toolCall?.id is defined, it means a new tool call starts.
99
+ if (mode === "message" && toolCall?.id) {
100
+ mode = null;
101
+ eventStream$.sendTextMessageEnd({ messageId: currentMessageId });
102
+ } else if (mode === "function" && (toolCall === undefined || toolCall?.id)) {
103
+ mode = null;
104
+ eventStream$.sendActionExecutionEnd({ actionExecutionId: currentToolCallId });
105
+ }
106
+
107
+ // If we send a new message type, send the appropriate start event.
108
+ if (mode === null) {
109
+ if (toolCall?.id) {
110
+ mode = "function";
111
+ currentToolCallId = toolCall!.id;
112
+ eventStream$.sendActionExecutionStart({
113
+ actionExecutionId: currentToolCallId,
114
+ actionName: toolCall!.function!.name,
115
+ });
116
+ } else if (content) {
117
+ mode = "message";
118
+ currentMessageId = chunk.id;
119
+ eventStream$.sendTextMessageStart({ messageId: currentMessageId });
120
+ }
121
+ }
122
+
123
+ // send the content events
124
+ if (mode === "message" && content) {
125
+ eventStream$.sendTextMessageContent({
126
+ messageId: currentMessageId,
127
+ content: content,
128
+ });
129
+ } else if (mode === "function" && toolCall?.function?.arguments) {
130
+ eventStream$.sendActionExecutionArgs({
131
+ actionExecutionId: currentToolCallId,
132
+ args: toolCall.function.arguments,
133
+ });
134
+ }
135
+ }
136
+
137
+ // send the end events
138
+ if (mode === "message") {
139
+ eventStream$.sendTextMessageEnd({ messageId: currentMessageId });
140
+ } else if (mode === "function") {
141
+ eventStream$.sendActionExecutionEnd({ actionExecutionId: currentToolCallId });
142
+ }
143
+
144
+ eventStream$.complete();
145
+ });
146
+
147
+ return {
148
+ threadId: request.threadId || randomUUID(),
149
+ };
150
+ }
151
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ FailedResponseStatus,
3
+ FailedResponseStatusReason,
4
+ } from "../graphql/types/response-status.type";
5
+
6
+ export class GuardrailsValidationFailureResponse extends FailedResponseStatus {
7
+ reason = FailedResponseStatusReason.GUARDRAILS_VALIDATION_FAILED;
8
+ declare details: {
9
+ guardrailsReason: string;
10
+ };
11
+
12
+ constructor({ guardrailsReason }) {
13
+ super();
14
+ this.details = {
15
+ guardrailsReason,
16
+ };
17
+ }
18
+ }
19
+
20
+ export class MessageStreamInterruptedResponse extends FailedResponseStatus {
21
+ reason = FailedResponseStatusReason.MESSAGE_STREAM_INTERRUPTED;
22
+ declare details: {
23
+ messageId: string;
24
+ description: string;
25
+ };
26
+
27
+ constructor({ messageId }: { messageId: string }) {
28
+ super();
29
+ this.details = {
30
+ messageId,
31
+ description: "Check the message for mode details",
32
+ };
33
+ }
34
+ }
35
+
36
+ export class UnknownErrorResponse extends FailedResponseStatus {
37
+ reason = FailedResponseStatusReason.UNKNOWN_ERROR;
38
+ declare details: {
39
+ description?: string;
40
+ originalError?: {
41
+ code?: string;
42
+ statusCode?: number;
43
+ severity?: string;
44
+ visibility?: string;
45
+ originalErrorType?: string;
46
+ extensions?: any;
47
+ };
48
+ };
49
+
50
+ constructor({
51
+ description,
52
+ originalError,
53
+ }: {
54
+ description?: string;
55
+ originalError?: {
56
+ code?: string;
57
+ statusCode?: number;
58
+ severity?: string;
59
+ visibility?: string;
60
+ originalErrorType?: string;
61
+ extensions?: any;
62
+ };
63
+ }) {
64
+ super();
65
+ this.details = {
66
+ description,
67
+ originalError,
68
+ };
69
+ }
70
+ }
@@ -0,0 +1 @@
1
+ export * from "./failed-response-status-reasons";
@@ -0,0 +1,3 @@
1
+ import "reflect-metadata";
2
+ export * from "@copilotkitnext/runtime";
3
+ export * from "@copilotkitnext/agent";
@@ -0,0 +1,13 @@
1
+ import "@jest/globals";
2
+
3
+ declare global {
4
+ const jest: (typeof import("@jest/globals"))["jest"];
5
+ const expect: (typeof import("@jest/globals"))["expect"];
6
+ const test: (typeof import("@jest/globals"))["test"];
7
+ const describe: (typeof import("@jest/globals"))["describe"];
8
+ const beforeEach: (typeof import("@jest/globals"))["beforeEach"];
9
+ const afterEach: (typeof import("@jest/globals"))["afterEach"];
10
+ const beforeAll: (typeof import("@jest/globals"))["beforeAll"];
11
+ const afterAll: (typeof import("@jest/globals"))["afterAll"];
12
+ const it: (typeof import("@jest/globals"))["it"];
13
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
5
+ import { describe, it, expect } from "@jest/globals";
6
+
7
+ describe("Anthropic Adapter - Allowlist Approach", () => {
8
+ it("should filter out tool_result messages with no corresponding tool_use ID", () => {
9
+ // Setup test data
10
+ const validToolUseIds = 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
+ // Apply the allowlist filter approach
22
+ const filteredMessages = [];
23
+ const processedIds = new Set<string>();
24
+
25
+ for (const message of messages) {
26
+ if (message.type === "tool_result") {
27
+ // Skip if no corresponding valid tool_use ID
28
+ if (!validToolUseIds.has(message.actionExecutionId)) {
29
+ continue;
30
+ }
31
+
32
+ // Skip if we've already processed this ID
33
+ if (processedIds.has(message.actionExecutionId)) {
34
+ continue;
35
+ }
36
+
37
+ // Mark this ID as processed
38
+ processedIds.add(message.actionExecutionId);
39
+ }
40
+
41
+ // Include all non-tool-result messages and valid tool results
42
+ filteredMessages.push(message);
43
+ }
44
+
45
+ // Verify results
46
+ expect(filteredMessages.length).toBe(3); // text + 2 valid tool results (no duplicates or invalid)
47
+
48
+ // Valid results should be included
49
+ expect(
50
+ filteredMessages.some(
51
+ (m) => m.type === "tool_result" && m.actionExecutionId === "valid-id-1",
52
+ ),
53
+ ).toBe(true);
54
+
55
+ expect(
56
+ filteredMessages.some(
57
+ (m) => m.type === "tool_result" && m.actionExecutionId === "valid-id-2",
58
+ ),
59
+ ).toBe(true);
60
+
61
+ // Invalid result should be excluded
62
+ expect(
63
+ filteredMessages.some(
64
+ (m) => m.type === "tool_result" && m.actionExecutionId === "invalid-id",
65
+ ),
66
+ ).toBe(false);
67
+
68
+ // Duplicate should be excluded
69
+ const validId1Count = filteredMessages.filter(
70
+ (m) => m.type === "tool_result" && m.actionExecutionId === "valid-id-1",
71
+ ).length;
72
+
73
+ expect(validId1Count).toBe(1);
74
+ });
75
+
76
+ it("should maintain correct order of messages when filtering", () => {
77
+ // Setup test data with specific ordering
78
+ const validToolUseIds = new Set<string>(["tool-1", "tool-2", "tool-3"]);
79
+
80
+ // Messages in a specific order, with some invalid/duplicate results
81
+ const messages = [
82
+ { type: "text", role: "user", content: "Initial message" },
83
+ { type: "text", role: "assistant", content: "I'll help with that" },
84
+ { type: "tool_use", id: "tool-1", name: "firstTool" },
85
+ { type: "tool_result", actionExecutionId: "tool-1", result: "result1" },
86
+ { type: "text", role: "assistant", content: "Got the first result" },
87
+ { type: "tool_use", id: "tool-2", name: "secondTool" },
88
+ { type: "tool_result", actionExecutionId: "tool-2", result: "result2" },
89
+ { type: "tool_result", actionExecutionId: "invalid-id", result: "invalid-result" },
90
+ { type: "tool_use", id: "tool-3", name: "thirdTool" },
91
+ { type: "tool_result", actionExecutionId: "tool-1", result: "duplicate-result" }, // Duplicate
92
+ { type: "tool_result", actionExecutionId: "tool-3", result: "result3" },
93
+ { type: "text", role: "user", content: "Final message" },
94
+ ];
95
+
96
+ // Apply the allowlist filter approach
97
+ const filteredMessages = [];
98
+ const processedIds = new Set<string>();
99
+
100
+ for (const message of messages) {
101
+ if (message.type === "tool_result") {
102
+ // Skip if no corresponding valid tool_use ID
103
+ if (!validToolUseIds.has(message.actionExecutionId)) {
104
+ continue;
105
+ }
106
+
107
+ // Skip if we've already processed this ID
108
+ if (processedIds.has(message.actionExecutionId)) {
109
+ continue;
110
+ }
111
+
112
+ // Mark this ID as processed
113
+ processedIds.add(message.actionExecutionId);
114
+ }
115
+
116
+ // Include all non-tool-result messages and valid tool results
117
+ filteredMessages.push(message);
118
+ }
119
+
120
+ // Verify results
121
+ expect(filteredMessages.length).toBe(10); // 12 original - 2 filtered out
122
+
123
+ // Check that the order is preserved
124
+ expect(filteredMessages[0].type).toBe("text"); // Initial user message
125
+ expect(filteredMessages[1].type).toBe("text"); // Assistant response
126
+ expect(filteredMessages[2].type).toBe("tool_use"); // First tool
127
+ expect(filteredMessages[3].type).toBe("tool_result"); // First result
128
+ expect(filteredMessages[3].actionExecutionId).toBe("tool-1"); // First result
129
+ expect(filteredMessages[4].type).toBe("text"); // Assistant comment
130
+ expect(filteredMessages[5].type).toBe("tool_use"); // Second tool
131
+ expect(filteredMessages[6].type).toBe("tool_result"); // Second result
132
+ expect(filteredMessages[6].actionExecutionId).toBe("tool-2"); // Second result
133
+ expect(filteredMessages[7].type).toBe("tool_use"); // Third tool
134
+ expect(filteredMessages[8].type).toBe("tool_result"); // Third result
135
+ expect(filteredMessages[8].actionExecutionId).toBe("tool-3"); // Third result
136
+ expect(filteredMessages[9].type).toBe("text"); // Final user message
137
+
138
+ // Each valid tool ID should appear exactly once in the results
139
+ const toolResultCounts = {
140
+ "tool-1": 0,
141
+ "tool-2": 0,
142
+ "tool-3": 0,
143
+ };
144
+
145
+ filteredMessages.forEach((message) => {
146
+ if (message.type === "tool_result" && message.actionExecutionId in toolResultCounts) {
147
+ toolResultCounts[message.actionExecutionId]++;
148
+ }
149
+ });
150
+
151
+ expect(toolResultCounts["tool-1"]).toBe(1);
152
+ expect(toolResultCounts["tool-2"]).toBe(1);
153
+ expect(toolResultCounts["tool-3"]).toBe(1);
154
+ });
155
+
156
+ it("should handle an empty message array", () => {
157
+ const validToolUseIds = new Set<string>(["valid-id-1", "valid-id-2"]);
158
+ const messages = [];
159
+
160
+ // Apply the filtering logic
161
+ const filteredMessages = [];
162
+ const processedIds = new Set<string>();
163
+
164
+ for (const message of messages) {
165
+ if (message.type === "tool_result") {
166
+ if (
167
+ !validToolUseIds.has(message.actionExecutionId) ||
168
+ processedIds.has(message.actionExecutionId)
169
+ ) {
170
+ continue;
171
+ }
172
+ processedIds.add(message.actionExecutionId);
173
+ }
174
+ filteredMessages.push(message);
175
+ }
176
+
177
+ expect(filteredMessages.length).toBe(0);
178
+ });
179
+
180
+ it("should handle edge cases with mixed message types", () => {
181
+ // Setup with mixed message types
182
+ const validToolUseIds = new Set<string>(["valid-id-1"]);
183
+
184
+ const messages = [
185
+ { type: "text", role: "user", content: "Hello" },
186
+ { type: "image", url: "https://example.com/image.jpg" }, // Non-tool message type
187
+ { type: "tool_result", actionExecutionId: "valid-id-1", result: "result1" },
188
+ { type: "custom", data: { key: "value" } }, // Another custom type
189
+ { type: "tool_result", actionExecutionId: "valid-id-1", result: "duplicate" }, // Duplicate
190
+ { type: "null", value: null }, // Edge case
191
+ { type: "undefined" }, // Edge case
192
+ ];
193
+
194
+ // Apply the filtering logic
195
+ const filteredMessages = [];
196
+ const processedIds = new Set<string>();
197
+
198
+ for (const message of messages) {
199
+ if (message.type === "tool_result") {
200
+ if (
201
+ !validToolUseIds.has(message.actionExecutionId) ||
202
+ processedIds.has(message.actionExecutionId)
203
+ ) {
204
+ continue;
205
+ }
206
+ processedIds.add(message.actionExecutionId);
207
+ }
208
+ filteredMessages.push(message);
209
+ }
210
+
211
+ // Should have all non-tool_result messages + 1 valid tool_result
212
+ expect(filteredMessages.length).toBe(6); // 7 original - 1 duplicate
213
+
214
+ // Valid tool_result should be included exactly once
215
+ const toolResults = filteredMessages.filter((m) => m.type === "tool_result");
216
+ expect(toolResults.length).toBe(1);
217
+ expect(toolResults[0].actionExecutionId).toBe("valid-id-1");
218
+
219
+ // All other message types should be preserved
220
+ expect(filteredMessages.filter((m) => m.type === "text").length).toBe(1);
221
+ expect(filteredMessages.filter((m) => m.type === "image").length).toBe(1);
222
+ expect(filteredMessages.filter((m) => m.type === "custom").length).toBe(1);
223
+ expect(filteredMessages.filter((m) => m.type === "null").length).toBe(1);
224
+ expect(filteredMessages.filter((m) => m.type === "undefined").length).toBe(1);
225
+ });
226
+ });