@copilotkit/runtime 1.8.14-next.4 → 1.8.14

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 (38) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/{chunk-EQX4WXGG.mjs → chunk-52XGL36X.mjs} +2 -2
  3. package/dist/{chunk-MVKCCH5U.mjs → chunk-GEZX4PJT.mjs} +16 -3
  4. package/dist/chunk-GEZX4PJT.mjs.map +1 -0
  5. package/dist/{chunk-BV7WYSOF.mjs → chunk-KPL64XRM.mjs} +2 -2
  6. package/dist/{chunk-JH3OA44N.mjs → chunk-LDXNIKFG.mjs} +2 -2
  7. package/dist/{chunk-IZAPLBCH.mjs → chunk-WS46QSSD.mjs} +3 -3
  8. package/dist/chunk-WS46QSSD.mjs.map +1 -0
  9. package/dist/index.js +18 -5
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +5 -5
  12. package/dist/lib/index.js +11 -4
  13. package/dist/lib/index.js.map +1 -1
  14. package/dist/lib/index.mjs +5 -5
  15. package/dist/lib/integrations/index.js +1 -1
  16. package/dist/lib/integrations/index.js.map +1 -1
  17. package/dist/lib/integrations/index.mjs +5 -5
  18. package/dist/lib/integrations/nest/index.js +1 -1
  19. package/dist/lib/integrations/nest/index.js.map +1 -1
  20. package/dist/lib/integrations/nest/index.mjs +3 -3
  21. package/dist/lib/integrations/node-express/index.js +1 -1
  22. package/dist/lib/integrations/node-express/index.js.map +1 -1
  23. package/dist/lib/integrations/node-express/index.mjs +3 -3
  24. package/dist/lib/integrations/node-http/index.js +1 -1
  25. package/dist/lib/integrations/node-http/index.js.map +1 -1
  26. package/dist/lib/integrations/node-http/index.mjs +2 -2
  27. package/dist/service-adapters/index.js +15 -2
  28. package/dist/service-adapters/index.js.map +1 -1
  29. package/dist/service-adapters/index.mjs +1 -1
  30. package/package.json +2 -2
  31. package/src/service-adapters/anthropic/anthropic-adapter.ts +14 -1
  32. package/src/service-adapters/google/google-genai-adapter.test.ts +104 -0
  33. package/src/service-adapters/google/google-genai-adapter.ts +19 -1
  34. package/dist/chunk-IZAPLBCH.mjs.map +0 -1
  35. package/dist/chunk-MVKCCH5U.mjs.map +0 -1
  36. /package/dist/{chunk-EQX4WXGG.mjs.map → chunk-52XGL36X.mjs.map} +0 -0
  37. /package/dist/{chunk-BV7WYSOF.mjs.map → chunk-KPL64XRM.mjs.map} +0 -0
  38. /package/dist/{chunk-JH3OA44N.mjs.map → chunk-LDXNIKFG.mjs.map} +0 -0
@@ -121,7 +121,20 @@ export class AnthropicAdapter implements CopilotServiceAdapter {
121
121
  // For non-tool-result messages, convert normally
122
122
  return convertMessageToAnthropicMessage(message);
123
123
  })
124
- .filter(Boolean) as Anthropic.Messages.MessageParam[]; // Explicitly cast after filtering nulls
124
+ .filter(Boolean) // Remove nulls
125
+ .filter((msg) => {
126
+ // Filter out assistant messages with empty text content
127
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
128
+ const hasEmptyTextOnly =
129
+ msg.content.length === 1 &&
130
+ msg.content[0].type === "text" &&
131
+ (!(msg.content[0] as any).text || (msg.content[0] as any).text.trim() === "");
132
+
133
+ // Keep messages that have tool_use or non-empty text
134
+ return !hasEmptyTextOnly;
135
+ }
136
+ return true;
137
+ }) as Anthropic.Messages.MessageParam[];
125
138
 
126
139
  // Apply token limits
127
140
  const limitedMessages = limitMessagesToTokenCount(anthropicMessages, tools, model);
@@ -0,0 +1,104 @@
1
+ import { GoogleGenerativeAIAdapter } from "./google-genai-adapter";
2
+ import { AIMessage, HumanMessage, SystemMessage } from "@langchain/core/messages";
3
+
4
+ // Mock ChatGoogle to capture what messages are passed to it
5
+ jest.mock("@langchain/google-gauth", () => ({
6
+ ChatGoogle: jest.fn().mockImplementation(() => ({
7
+ bindTools: jest.fn().mockReturnThis(),
8
+ stream: jest.fn().mockImplementation((messages) => {
9
+ // Return the messages so we can verify filtering
10
+ return Promise.resolve({ filteredMessages: messages });
11
+ }),
12
+ })),
13
+ }));
14
+
15
+ describe("GoogleGenerativeAIAdapter", () => {
16
+ it("should filter out empty AIMessages to prevent Gemini validation errors", async () => {
17
+ const adapter = new GoogleGenerativeAIAdapter();
18
+
19
+ // Create a mix of messages including an empty AIMessage (the problematic case)
20
+ const messages = [
21
+ new HumanMessage("Hello"),
22
+ new AIMessage(""), // This should be filtered out
23
+ new HumanMessage("How are you?"),
24
+ new AIMessage("I'm doing well!"), // This should be kept
25
+ new SystemMessage("You are a helpful assistant"), // This should be kept
26
+ new AIMessage(""), // Another empty one to filter out
27
+ ];
28
+
29
+ // Access the internal chainFn to test the filtering logic
30
+ const chainFnResult = await (adapter as any).options.chainFn({
31
+ messages,
32
+ tools: [],
33
+ threadId: "test-thread",
34
+ });
35
+
36
+ // The result should be filtered messages passed to ChatGoogle.stream()
37
+ const { filteredMessages } = chainFnResult;
38
+
39
+ // Should only contain non-empty messages
40
+ expect(filteredMessages).toHaveLength(4);
41
+ expect(filteredMessages[0]).toBeInstanceOf(HumanMessage);
42
+ expect(filteredMessages[0].content).toBe("Hello");
43
+ expect(filteredMessages[1]).toBeInstanceOf(HumanMessage);
44
+ expect(filteredMessages[1].content).toBe("How are you?");
45
+ expect(filteredMessages[2]).toBeInstanceOf(AIMessage);
46
+ expect(filteredMessages[2].content).toBe("I'm doing well!");
47
+ expect(filteredMessages[3]).toBeInstanceOf(SystemMessage);
48
+ expect(filteredMessages[3].content).toBe("You are a helpful assistant");
49
+ });
50
+
51
+ it("should keep AIMessages with tool_calls even if content is empty", async () => {
52
+ const adapter = new GoogleGenerativeAIAdapter();
53
+
54
+ const messagesWithToolCalls = [
55
+ new HumanMessage("Execute a function"),
56
+ new AIMessage({
57
+ content: "", // Empty content but has tool calls
58
+ tool_calls: [
59
+ {
60
+ id: "call_123",
61
+ name: "test_function",
62
+ args: { param: "value" },
63
+ },
64
+ ],
65
+ }),
66
+ ];
67
+
68
+ const chainFnResult = await (adapter as any).options.chainFn({
69
+ messages: messagesWithToolCalls,
70
+ tools: [],
71
+ threadId: "test-thread",
72
+ });
73
+
74
+ const { filteredMessages } = chainFnResult;
75
+
76
+ // Should keep both messages - the AIMessage has tool_calls so it's valid
77
+ expect(filteredMessages).toHaveLength(2);
78
+ expect(filteredMessages[1]).toBeInstanceOf(AIMessage);
79
+ expect(filteredMessages[1].tool_calls).toHaveLength(1);
80
+ });
81
+
82
+ it("should keep all non-AIMessage types regardless of content", async () => {
83
+ const adapter = new GoogleGenerativeAIAdapter();
84
+
85
+ const messages = [
86
+ new HumanMessage(""), // Empty human message should be kept
87
+ new SystemMessage(""), // Empty system message should be kept
88
+ new AIMessage(""), // Empty AI message should be filtered
89
+ ];
90
+
91
+ const chainFnResult = await (adapter as any).options.chainFn({
92
+ messages,
93
+ tools: [],
94
+ threadId: "test-thread",
95
+ });
96
+
97
+ const { filteredMessages } = chainFnResult;
98
+
99
+ // Should keep HumanMessage and SystemMessage, filter out empty AIMessage
100
+ expect(filteredMessages).toHaveLength(2);
101
+ expect(filteredMessages[0]).toBeInstanceOf(HumanMessage);
102
+ expect(filteredMessages[1]).toBeInstanceOf(SystemMessage);
103
+ });
104
+ });
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { ChatGoogle } from "@langchain/google-gauth";
18
18
  import { LangChainAdapter } from "../langchain/langchain-adapter";
19
+ import { AIMessage } from "@langchain/core/messages";
19
20
 
20
21
  interface GoogleGenerativeAIAdapterOptions {
21
22
  /**
@@ -28,11 +29,28 @@ export class GoogleGenerativeAIAdapter extends LangChainAdapter {
28
29
  constructor(options?: GoogleGenerativeAIAdapterOptions) {
29
30
  super({
30
31
  chainFn: async ({ messages, tools, threadId }) => {
32
+ // Filter out empty assistant messages to prevent Gemini validation errors
33
+ // Gemini specifically rejects conversations containing AIMessages with empty content
34
+ const filteredMessages = messages.filter((message) => {
35
+ // Keep all non-AI messages (HumanMessage, SystemMessage, ToolMessage, etc.)
36
+ if (!(message instanceof AIMessage)) {
37
+ return true;
38
+ }
39
+
40
+ // For AIMessages, only keep those with non-empty content
41
+ // Also keep AIMessages with tool_calls even if content is empty
42
+ return (
43
+ (message.content && String(message.content).trim().length > 0) ||
44
+ (message.tool_calls && message.tool_calls.length > 0)
45
+ );
46
+ });
47
+
31
48
  const model = new ChatGoogle({
32
49
  modelName: options?.model ?? "gemini-1.5-pro",
33
50
  apiVersion: "v1beta",
34
51
  }).bindTools(tools);
35
- return model.stream(messages, { metadata: { conversation_id: threadId } });
52
+
53
+ return model.stream(filteredMessages, { metadata: { conversation_id: threadId } });
36
54
  },
37
55
  });
38
56
  }