@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,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";
|
package/src/v2/index.ts
ADDED
|
@@ -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
|
+
});
|