@copilotkit/runtime 1.56.0 → 1.56.2
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/dist/agent/index.cjs +2 -2
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +2 -2
- package/dist/agent/index.mjs.map +1 -1
- package/dist/lib/integrations/node-http/index.cjs +4 -1
- package/dist/lib/integrations/node-http/index.cjs.map +1 -1
- package/dist/lib/integrations/node-http/index.d.cts.map +1 -1
- package/dist/lib/integrations/node-http/index.d.mts.map +1 -1
- package/dist/lib/integrations/node-http/index.mjs +4 -1
- package/dist/lib/integrations/node-http/index.mjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.cjs +11 -1
- package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.mjs +11 -1
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.cjs +21 -4
- package/dist/lib/runtime/mcp-tools-utils.cjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.d.cts.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.d.mts.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.mjs +21 -4
- package/dist/lib/runtime/mcp-tools-utils.mjs.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/service-adapters/anthropic/anthropic-adapter.cjs +11 -3
- package/dist/service-adapters/anthropic/anthropic-adapter.cjs.map +1 -1
- package/dist/service-adapters/anthropic/anthropic-adapter.d.cts +6 -0
- package/dist/service-adapters/anthropic/anthropic-adapter.d.cts.map +1 -1
- package/dist/service-adapters/anthropic/anthropic-adapter.d.mts +6 -0
- package/dist/service-adapters/anthropic/anthropic-adapter.d.mts.map +1 -1
- package/dist/service-adapters/anthropic/anthropic-adapter.mjs +11 -3
- package/dist/service-adapters/anthropic/anthropic-adapter.mjs.map +1 -1
- package/dist/service-adapters/anthropic/utils.cjs +27 -1
- package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
- package/dist/service-adapters/anthropic/utils.mjs +27 -1
- package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
- package/dist/service-adapters/langchain/utils.cjs +1 -1
- package/dist/service-adapters/langchain/utils.cjs.map +1 -1
- package/dist/service-adapters/langchain/utils.mjs +1 -1
- package/dist/service-adapters/langchain/utils.mjs.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.cjs +2 -1
- package/dist/service-adapters/openai/openai-adapter.cjs.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.d.cts +6 -0
- package/dist/service-adapters/openai/openai-adapter.d.cts.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.d.mts +6 -0
- package/dist/service-adapters/openai/openai-adapter.d.mts.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.mjs +2 -1
- package/dist/service-adapters/openai/openai-adapter.mjs.map +1 -1
- package/dist/v2/runtime/core/middleware-sse-parser.cjs +5 -2
- package/dist/v2/runtime/core/middleware-sse-parser.cjs.map +1 -1
- package/dist/v2/runtime/core/middleware-sse-parser.mjs +5 -2
- package/dist/v2/runtime/core/middleware-sse-parser.mjs.map +1 -1
- package/package.json +2 -2
- package/src/agent/__tests__/provider-id-collision.test.ts +195 -0
- package/src/agent/index.ts +19 -11
- package/src/lib/integrations/node-http/__tests__/request-duck-type.test.ts +66 -0
- package/src/lib/integrations/node-http/index.ts +15 -1
- package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +30 -1
- package/src/lib/runtime/__tests__/on-after-request.test.ts +122 -0
- package/src/lib/runtime/copilot-runtime.ts +16 -3
- package/src/lib/runtime/mcp-tools-utils.ts +41 -6
- package/src/service-adapters/anthropic/anthropic-adapter.ts +22 -2
- package/src/service-adapters/anthropic/utils.ts +60 -1
- package/src/service-adapters/langchain/utils.ts +1 -1
- package/src/service-adapters/openai/openai-adapter.ts +14 -1
- package/src/v2/runtime/__tests__/middleware-sse-parser.test.ts +50 -0
- package/src/v2/runtime/core/middleware-sse-parser.ts +12 -2
- package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +268 -0
- package/tests/service-adapters/anthropic/utils-token-trimming.test.ts +301 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { BuiltInAgent } from "../index";
|
|
3
|
+
import { EventType, type RunAgentInput } from "@ag-ui/client";
|
|
4
|
+
import { streamText } from "ai";
|
|
5
|
+
import {
|
|
6
|
+
mockStreamTextResponse,
|
|
7
|
+
textDelta,
|
|
8
|
+
finish,
|
|
9
|
+
collectEvents,
|
|
10
|
+
} from "./test-helpers";
|
|
11
|
+
|
|
12
|
+
// Mock the ai module
|
|
13
|
+
vi.mock("ai", () => ({
|
|
14
|
+
streamText: vi.fn(),
|
|
15
|
+
tool: vi.fn((config) => config),
|
|
16
|
+
stepCountIs: vi.fn((count: number) => ({ type: "stepCount", count })),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("@ai-sdk/openai", () => ({
|
|
20
|
+
createOpenAI: vi.fn(() => (modelId: string) => ({
|
|
21
|
+
specificationVersion: "v3",
|
|
22
|
+
modelId,
|
|
23
|
+
provider: "openai",
|
|
24
|
+
})),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("@ai-sdk/anthropic", () => ({
|
|
28
|
+
createAnthropic: vi.fn(() => (modelId: string) => ({
|
|
29
|
+
specificationVersion: "v3",
|
|
30
|
+
modelId,
|
|
31
|
+
provider: "anthropic",
|
|
32
|
+
})),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("@ai-sdk/google", () => ({
|
|
36
|
+
createGoogleGenerativeAI: vi.fn(() => (modelId: string) => ({
|
|
37
|
+
specificationVersion: "v3",
|
|
38
|
+
modelId,
|
|
39
|
+
provider: "google",
|
|
40
|
+
})),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
describe("Provider ID collision (#3410, #3623)", () => {
|
|
44
|
+
const originalEnv = process.env;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
process.env = { ...originalEnv };
|
|
49
|
+
process.env.OPENAI_API_KEY = "test-key";
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
process.env = originalEnv;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should replace text-start providedId "txt-0" with a UUID', async () => {
|
|
57
|
+
const agent = new BuiltInAgent({ model: "openai:gpt-4o-mini" });
|
|
58
|
+
|
|
59
|
+
vi.mocked(streamText).mockReturnValue(
|
|
60
|
+
mockStreamTextResponse([
|
|
61
|
+
{ type: "text-start", id: "txt-0" },
|
|
62
|
+
textDelta("Hello"),
|
|
63
|
+
finish(),
|
|
64
|
+
]) as any,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const input: RunAgentInput = {
|
|
68
|
+
threadId: "thread-1",
|
|
69
|
+
runId: "run-1",
|
|
70
|
+
messages: [{ id: "1", role: "user", content: "Hi" }],
|
|
71
|
+
tools: [],
|
|
72
|
+
context: [],
|
|
73
|
+
state: {},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const events = await collectEvents(agent["run"](input));
|
|
77
|
+
|
|
78
|
+
// Find the TEXT_MESSAGE_CHUNK event and check its messageId
|
|
79
|
+
const textChunks = events.filter(
|
|
80
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
81
|
+
);
|
|
82
|
+
expect(textChunks.length).toBeGreaterThan(0);
|
|
83
|
+
const messageId = (textChunks[0] as any).messageId;
|
|
84
|
+
|
|
85
|
+
// The messageId should NOT be "txt-0" — it should be a UUID
|
|
86
|
+
expect(messageId).not.toBe("txt-0");
|
|
87
|
+
// UUID v4 pattern
|
|
88
|
+
expect(messageId).toMatch(
|
|
89
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should replace reasoning-start providedId "reasoning-0" with a UUID', async () => {
|
|
94
|
+
const agent = new BuiltInAgent({ model: "openai:gpt-4o-mini" });
|
|
95
|
+
|
|
96
|
+
vi.mocked(streamText).mockReturnValue(
|
|
97
|
+
mockStreamTextResponse([
|
|
98
|
+
{ type: "reasoning-start", id: "reasoning-0" },
|
|
99
|
+
{ type: "reasoning-delta", text: "Thinking..." },
|
|
100
|
+
{ type: "reasoning-end" },
|
|
101
|
+
{ type: "text-start", id: "txt-0" },
|
|
102
|
+
textDelta("Answer"),
|
|
103
|
+
finish(),
|
|
104
|
+
]) as any,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const input: RunAgentInput = {
|
|
108
|
+
threadId: "thread-2",
|
|
109
|
+
runId: "run-2",
|
|
110
|
+
messages: [{ id: "1", role: "user", content: "Hi" }],
|
|
111
|
+
tools: [],
|
|
112
|
+
context: [],
|
|
113
|
+
state: {},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const events = await collectEvents(agent["run"](input));
|
|
117
|
+
|
|
118
|
+
// Find REASONING_START event
|
|
119
|
+
const reasoningStarts = events.filter(
|
|
120
|
+
(e) => e.type === EventType.REASONING_START,
|
|
121
|
+
);
|
|
122
|
+
expect(reasoningStarts.length).toBeGreaterThan(0);
|
|
123
|
+
const reasoningId = (reasoningStarts[0] as any).messageId;
|
|
124
|
+
|
|
125
|
+
// Should NOT be "reasoning-0"
|
|
126
|
+
expect(reasoningId).not.toBe("reasoning-0");
|
|
127
|
+
expect(reasoningId).toMatch(
|
|
128
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should replace providedId "msg-0" with a UUID', async () => {
|
|
133
|
+
const agent = new BuiltInAgent({ model: "openai:gpt-4o-mini" });
|
|
134
|
+
|
|
135
|
+
vi.mocked(streamText).mockReturnValue(
|
|
136
|
+
mockStreamTextResponse([
|
|
137
|
+
{ type: "text-start", id: "msg-0" },
|
|
138
|
+
textDelta("Hello"),
|
|
139
|
+
finish(),
|
|
140
|
+
]) as any,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const input: RunAgentInput = {
|
|
144
|
+
threadId: "thread-3",
|
|
145
|
+
runId: "run-3",
|
|
146
|
+
messages: [{ id: "1", role: "user", content: "Hi" }],
|
|
147
|
+
tools: [],
|
|
148
|
+
context: [],
|
|
149
|
+
state: {},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const events = await collectEvents(agent["run"](input));
|
|
153
|
+
|
|
154
|
+
const textChunks = events.filter(
|
|
155
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
156
|
+
);
|
|
157
|
+
expect(textChunks.length).toBeGreaterThan(0);
|
|
158
|
+
const messageId = (textChunks[0] as any).messageId;
|
|
159
|
+
|
|
160
|
+
expect(messageId).not.toBe("msg-0");
|
|
161
|
+
expect(messageId).toMatch(
|
|
162
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should preserve legitimate provider IDs", async () => {
|
|
167
|
+
const agent = new BuiltInAgent({ model: "openai:gpt-4o-mini" });
|
|
168
|
+
|
|
169
|
+
vi.mocked(streamText).mockReturnValue(
|
|
170
|
+
mockStreamTextResponse([
|
|
171
|
+
{ type: "text-start", id: "custom-msg-id-123" },
|
|
172
|
+
textDelta("Hello"),
|
|
173
|
+
finish(),
|
|
174
|
+
]) as any,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const input: RunAgentInput = {
|
|
178
|
+
threadId: "thread-4",
|
|
179
|
+
runId: "run-4",
|
|
180
|
+
messages: [{ id: "1", role: "user", content: "Hi" }],
|
|
181
|
+
tools: [],
|
|
182
|
+
context: [],
|
|
183
|
+
state: {},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const events = await collectEvents(agent["run"](input));
|
|
187
|
+
|
|
188
|
+
const textChunks = events.filter(
|
|
189
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
190
|
+
);
|
|
191
|
+
expect(textChunks.length).toBeGreaterThan(0);
|
|
192
|
+
// Legitimate IDs should be preserved
|
|
193
|
+
expect((textChunks[0] as any).messageId).toBe("custom-msg-id-123");
|
|
194
|
+
});
|
|
195
|
+
});
|
package/src/agent/index.ts
CHANGED
|
@@ -1267,13 +1267,17 @@ export class BuiltInAgent extends AbstractAgent {
|
|
|
1267
1267
|
break;
|
|
1268
1268
|
}
|
|
1269
1269
|
case "reasoning-start": {
|
|
1270
|
-
// Use SDK-provided id, or generate a fresh UUID if id is falsy
|
|
1271
|
-
//
|
|
1270
|
+
// Use SDK-provided id, or generate a fresh UUID if the id is falsy,
|
|
1271
|
+
// "0", or matches the non-unique pattern emitted by @ai-sdk/openai-compatible
|
|
1272
|
+
// (e.g. "txt-0", "reasoning-0", "msg-0").
|
|
1272
1273
|
const providedId = "id" in part ? part.id : undefined;
|
|
1273
|
-
|
|
1274
|
-
providedId
|
|
1275
|
-
|
|
1276
|
-
|
|
1274
|
+
const isNonUniqueId =
|
|
1275
|
+
!providedId ||
|
|
1276
|
+
providedId === "0" ||
|
|
1277
|
+
/^(txt|reasoning|msg)-0$/.test(providedId);
|
|
1278
|
+
reasoningMessageId = isNonUniqueId
|
|
1279
|
+
? randomUUID()
|
|
1280
|
+
: (providedId as typeof reasoningMessageId);
|
|
1277
1281
|
const reasoningStartEvent: ReasoningStartEvent = {
|
|
1278
1282
|
type: EventType.REASONING_START,
|
|
1279
1283
|
messageId: reasoningMessageId,
|
|
@@ -1341,12 +1345,16 @@ export class BuiltInAgent extends AbstractAgent {
|
|
|
1341
1345
|
|
|
1342
1346
|
case "text-start": {
|
|
1343
1347
|
// New text message starting - use the SDK-provided id
|
|
1344
|
-
// Use randomUUID() if part.id is falsy
|
|
1348
|
+
// Use randomUUID() if part.id is falsy, "0", or matches the non-unique
|
|
1349
|
+
// pattern emitted by @ai-sdk/openai-compatible (e.g. "txt-0", "msg-0").
|
|
1345
1350
|
const providedId = "id" in part ? part.id : undefined;
|
|
1346
|
-
|
|
1347
|
-
providedId
|
|
1348
|
-
|
|
1349
|
-
|
|
1351
|
+
const isNonUniqueTextId =
|
|
1352
|
+
!providedId ||
|
|
1353
|
+
providedId === "0" ||
|
|
1354
|
+
/^(txt|reasoning|msg)-0$/.test(providedId);
|
|
1355
|
+
messageId = isNonUniqueTextId
|
|
1356
|
+
? randomUUID()
|
|
1357
|
+
: (providedId as typeof messageId);
|
|
1350
1358
|
break;
|
|
1351
1359
|
}
|
|
1352
1360
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test for #2986: instanceof Request fails with @hono/node-server polyfill
|
|
5
|
+
*
|
|
6
|
+
* When Hono polyfills the Request class, `instanceof Request` fails because
|
|
7
|
+
* the polyfilled Request has a different prototype. We need duck-type checking.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Simulates a polyfilled Request object that does NOT pass instanceof Request
|
|
11
|
+
function createPolyfillRequest(url: string, method: string = "GET"): object {
|
|
12
|
+
return {
|
|
13
|
+
url,
|
|
14
|
+
method,
|
|
15
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
16
|
+
body: null,
|
|
17
|
+
clone: () => createPolyfillRequest(url, method),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// This is the duck-type check that should replace instanceof
|
|
22
|
+
function isRequestLike(obj: unknown): obj is Request {
|
|
23
|
+
return (
|
|
24
|
+
typeof obj === "object" &&
|
|
25
|
+
obj !== null &&
|
|
26
|
+
"url" in obj &&
|
|
27
|
+
"method" in obj &&
|
|
28
|
+
"headers" in obj &&
|
|
29
|
+
typeof (obj as any).url === "string" &&
|
|
30
|
+
typeof (obj as any).method === "string"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("Request duck-type detection (#2986)", () => {
|
|
35
|
+
it("should detect a native Request object", () => {
|
|
36
|
+
const req = new Request("http://localhost:3000/api/copilotkit", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
});
|
|
39
|
+
expect(isRequestLike(req)).toBe(true);
|
|
40
|
+
expect(req instanceof Request).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should detect a polyfilled Request object that fails instanceof", () => {
|
|
44
|
+
const polyfilled = createPolyfillRequest(
|
|
45
|
+
"http://localhost:3000/api/copilotkit",
|
|
46
|
+
"POST",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// instanceof fails for polyfilled objects
|
|
50
|
+
expect(polyfilled instanceof Request).toBe(false);
|
|
51
|
+
|
|
52
|
+
// But duck-type check succeeds
|
|
53
|
+
expect(isRequestLike(polyfilled)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should NOT match null or undefined", () => {
|
|
57
|
+
expect(isRequestLike(null)).toBe(false);
|
|
58
|
+
expect(isRequestLike(undefined)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should NOT match an object missing required properties", () => {
|
|
62
|
+
expect(isRequestLike({ url: "http://test.com" })).toBe(false);
|
|
63
|
+
expect(isRequestLike({ method: "GET" })).toBe(false);
|
|
64
|
+
expect(isRequestLike({})).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -158,11 +158,25 @@ export function copilotRuntimeNodeHttpEndpoint(
|
|
|
158
158
|
}
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
+
// Duck-type check for Request-like objects (handles polyfilled Request from @hono/node-server)
|
|
162
|
+
function isRequestLike(obj: unknown): obj is Request {
|
|
163
|
+
return (
|
|
164
|
+
obj instanceof Request ||
|
|
165
|
+
(typeof obj === "object" &&
|
|
166
|
+
obj !== null &&
|
|
167
|
+
"url" in obj &&
|
|
168
|
+
"method" in obj &&
|
|
169
|
+
"headers" in obj &&
|
|
170
|
+
typeof (obj as any).url === "string" &&
|
|
171
|
+
typeof (obj as any).method === "string")
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
161
175
|
return function (
|
|
162
176
|
reqOrRequest: IncomingMessage | Request,
|
|
163
177
|
res?: ServerResponse,
|
|
164
178
|
): Promise<void> | Promise<Response> | Response {
|
|
165
|
-
if (reqOrRequest
|
|
179
|
+
if (isRequestLike(reqOrRequest) && !res) {
|
|
166
180
|
return honoApp.fetch(reqOrRequest as Request);
|
|
167
181
|
}
|
|
168
182
|
if (!res) {
|
|
@@ -110,10 +110,14 @@ describe("MCP Tools Utils", () => {
|
|
|
110
110
|
});
|
|
111
111
|
expect(result[1]).toEqual({
|
|
112
112
|
name: "objectArray",
|
|
113
|
-
type: "
|
|
113
|
+
type: "object[]",
|
|
114
114
|
description:
|
|
115
115
|
"Array of objects Array of objects with properties: name, value",
|
|
116
116
|
required: false,
|
|
117
|
+
attributes: [
|
|
118
|
+
{ name: "name", type: "string", description: "", required: false },
|
|
119
|
+
{ name: "value", type: "number", description: "", required: false },
|
|
120
|
+
],
|
|
117
121
|
});
|
|
118
122
|
});
|
|
119
123
|
|
|
@@ -147,6 +151,7 @@ describe("MCP Tools Utils", () => {
|
|
|
147
151
|
type: "string",
|
|
148
152
|
description: "Status value Allowed values: active | inactive | pending",
|
|
149
153
|
required: true,
|
|
154
|
+
enum: ["active", "inactive", "pending"],
|
|
150
155
|
});
|
|
151
156
|
expect(result[1]).toEqual({
|
|
152
157
|
name: "priority",
|
|
@@ -192,6 +197,30 @@ describe("MCP Tools Utils", () => {
|
|
|
192
197
|
description:
|
|
193
198
|
"User object Object with properties: name, email, preferences",
|
|
194
199
|
required: true,
|
|
200
|
+
attributes: [
|
|
201
|
+
{ name: "name", type: "string", description: "", required: false },
|
|
202
|
+
{ name: "email", type: "string", description: "", required: false },
|
|
203
|
+
{
|
|
204
|
+
name: "preferences",
|
|
205
|
+
type: "object",
|
|
206
|
+
description: "Object with properties: theme, notifications",
|
|
207
|
+
required: false,
|
|
208
|
+
attributes: [
|
|
209
|
+
{
|
|
210
|
+
name: "theme",
|
|
211
|
+
type: "string",
|
|
212
|
+
description: "",
|
|
213
|
+
required: false,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "notifications",
|
|
217
|
+
type: "boolean",
|
|
218
|
+
description: "",
|
|
219
|
+
required: false,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
],
|
|
195
224
|
});
|
|
196
225
|
});
|
|
197
226
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { CopilotRuntime } from "../copilot-runtime";
|
|
3
|
+
|
|
4
|
+
describe("onAfterRequest middleware (#2124)", () => {
|
|
5
|
+
it("should pass hookParams to onAfterRequest, not an empty object", async () => {
|
|
6
|
+
const onAfterRequest = vi.fn();
|
|
7
|
+
|
|
8
|
+
const runtime = new CopilotRuntime({
|
|
9
|
+
middleware: {
|
|
10
|
+
onAfterRequest,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Access the internal afterRequestMiddleware function
|
|
15
|
+
const afterRequestMw = runtime.instance.afterRequestMiddleware;
|
|
16
|
+
expect(afterRequestMw).toBeDefined();
|
|
17
|
+
|
|
18
|
+
// Simulate calling the middleware with hookParams (as the v2 runtime would)
|
|
19
|
+
const fakeHookParams = {
|
|
20
|
+
runtime: {} as any,
|
|
21
|
+
response: new Response("test"),
|
|
22
|
+
path: "/api/copilotkit",
|
|
23
|
+
messages: [
|
|
24
|
+
{ id: "msg-1", role: "user", content: "Hi there" },
|
|
25
|
+
{ id: "msg-2", role: "assistant", content: "Hello" },
|
|
26
|
+
{ id: "msg-3", role: "tool", content: "result", toolCallId: "tc-1" },
|
|
27
|
+
],
|
|
28
|
+
threadId: "thread-123",
|
|
29
|
+
runId: "run-456",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
await (afterRequestMw as Function)(fakeHookParams);
|
|
33
|
+
|
|
34
|
+
expect(onAfterRequest).toHaveBeenCalledTimes(1);
|
|
35
|
+
|
|
36
|
+
const callArg = onAfterRequest.mock.calls[0][0];
|
|
37
|
+
|
|
38
|
+
// Should NOT be called with an empty object
|
|
39
|
+
expect(callArg).not.toEqual({});
|
|
40
|
+
|
|
41
|
+
// Verify all OnAfterRequestOptions fields are present
|
|
42
|
+
expect(callArg).toHaveProperty("threadId", "thread-123");
|
|
43
|
+
expect(callArg).toHaveProperty("runId", "run-456");
|
|
44
|
+
expect(callArg).toHaveProperty("url", "/api/copilotkit");
|
|
45
|
+
expect(callArg).toHaveProperty("properties");
|
|
46
|
+
expect(callArg.properties).toEqual({});
|
|
47
|
+
|
|
48
|
+
// Verify message splitting: user messages → inputMessages, others → outputMessages
|
|
49
|
+
expect(callArg.inputMessages).toHaveLength(1);
|
|
50
|
+
expect(callArg.inputMessages[0]).toMatchObject({
|
|
51
|
+
id: "msg-1",
|
|
52
|
+
role: "user",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(callArg.outputMessages).toHaveLength(2);
|
|
56
|
+
expect(callArg.outputMessages[0]).toMatchObject({
|
|
57
|
+
id: "msg-2",
|
|
58
|
+
role: "assistant",
|
|
59
|
+
});
|
|
60
|
+
expect(callArg.outputMessages[1]).toMatchObject({
|
|
61
|
+
id: "msg-3",
|
|
62
|
+
role: "tool",
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should handle undefined messages gracefully", async () => {
|
|
67
|
+
const onAfterRequest = vi.fn();
|
|
68
|
+
|
|
69
|
+
const runtime = new CopilotRuntime({
|
|
70
|
+
middleware: {
|
|
71
|
+
onAfterRequest,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const afterRequestMw = runtime.instance.afterRequestMiddleware;
|
|
76
|
+
|
|
77
|
+
const fakeHookParams = {
|
|
78
|
+
runtime: {} as any,
|
|
79
|
+
response: new Response("test"),
|
|
80
|
+
path: "/api/copilotkit",
|
|
81
|
+
// messages intentionally omitted (undefined)
|
|
82
|
+
threadId: "thread-789",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
await (afterRequestMw as Function)(fakeHookParams);
|
|
86
|
+
|
|
87
|
+
expect(onAfterRequest).toHaveBeenCalledTimes(1);
|
|
88
|
+
|
|
89
|
+
const callArg = onAfterRequest.mock.calls[0][0];
|
|
90
|
+
expect(callArg.threadId).toBe("thread-789");
|
|
91
|
+
expect(callArg.inputMessages).toEqual([]);
|
|
92
|
+
expect(callArg.outputMessages).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should default threadId to empty string when undefined", async () => {
|
|
96
|
+
const onAfterRequest = vi.fn();
|
|
97
|
+
|
|
98
|
+
const runtime = new CopilotRuntime({
|
|
99
|
+
middleware: {
|
|
100
|
+
onAfterRequest,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const afterRequestMw = runtime.instance.afterRequestMiddleware;
|
|
105
|
+
|
|
106
|
+
const fakeHookParams = {
|
|
107
|
+
runtime: {} as any,
|
|
108
|
+
response: new Response("test"),
|
|
109
|
+
path: "/api/copilotkit",
|
|
110
|
+
messages: [],
|
|
111
|
+
// threadId intentionally omitted
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
await (afterRequestMw as Function)(fakeHookParams);
|
|
115
|
+
|
|
116
|
+
expect(onAfterRequest).toHaveBeenCalledTimes(1);
|
|
117
|
+
|
|
118
|
+
const callArg = onAfterRequest.mock.calls[0][0];
|
|
119
|
+
expect(callArg.threadId).toBe("");
|
|
120
|
+
expect(callArg.runId).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -626,9 +626,22 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
|
|
|
626
626
|
params?.afterRequestMiddleware?.(hookParams);
|
|
627
627
|
|
|
628
628
|
if (params?.middleware?.onAfterRequest) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
629
|
+
const messages = hookParams.messages ?? [];
|
|
630
|
+
params.middleware.onAfterRequest({
|
|
631
|
+
threadId: hookParams.threadId ?? "",
|
|
632
|
+
runId: hookParams.runId,
|
|
633
|
+
inputMessages: messages.filter(
|
|
634
|
+
(m): m is typeof m & { role: string } =>
|
|
635
|
+
"role" in m && m.role === "user",
|
|
636
|
+
) as unknown as Message[],
|
|
637
|
+
outputMessages: messages.filter(
|
|
638
|
+
(m): m is typeof m & { role: string } =>
|
|
639
|
+
"role" in m && m.role !== "user",
|
|
640
|
+
) as unknown as Message[],
|
|
641
|
+
// TODO: forward actual properties once the after-request hook has access to the request body
|
|
642
|
+
properties: {},
|
|
643
|
+
url: hookParams.path,
|
|
644
|
+
} satisfies OnAfterRequestOptions);
|
|
632
645
|
}
|
|
633
646
|
};
|
|
634
647
|
}
|
|
@@ -85,30 +85,65 @@ export function extractParametersFromSchema(
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Handle enums
|
|
88
|
+
// Handle enums — preserve as structured data for Zod conversion
|
|
89
|
+
let enumValues: string[] | undefined;
|
|
89
90
|
if (paramDef.enum && Array.isArray(paramDef.enum)) {
|
|
90
|
-
|
|
91
|
+
enumValues = paramDef.enum.map(String);
|
|
92
|
+
const enumDisplay = enumValues.join(" | ");
|
|
91
93
|
description =
|
|
92
94
|
description +
|
|
93
95
|
(description ? " " : "") +
|
|
94
|
-
`Allowed values: ${
|
|
96
|
+
`Allowed values: ${enumDisplay}`;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
// Handle objects with properties
|
|
99
|
+
// Handle objects with properties — recurse to preserve nested structure
|
|
100
|
+
let attributes: Parameter[] | undefined;
|
|
98
101
|
if (type === "object" && paramDef.properties) {
|
|
99
102
|
const objectProperties = Object.keys(paramDef.properties).join(", ");
|
|
100
103
|
description =
|
|
101
104
|
description +
|
|
102
105
|
(description ? " " : "") +
|
|
103
106
|
`Object with properties: ${objectProperties}`;
|
|
107
|
+
// Recursively extract nested parameters
|
|
108
|
+
attributes = extractParametersFromSchema({
|
|
109
|
+
parameters: {
|
|
110
|
+
properties: paramDef.properties,
|
|
111
|
+
required: paramDef.required || [],
|
|
112
|
+
},
|
|
113
|
+
});
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
|
|
116
|
+
// Handle object arrays — recurse into item schema
|
|
117
|
+
if (type === "array" && paramDef.items?.type === "object" && paramDef.items?.properties) {
|
|
118
|
+
attributes = extractParametersFromSchema({
|
|
119
|
+
parameters: {
|
|
120
|
+
properties: paramDef.items.properties,
|
|
121
|
+
required: paramDef.items.required || [],
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const param: any = {
|
|
107
127
|
name: paramName,
|
|
108
128
|
type: type,
|
|
109
129
|
description: description,
|
|
110
130
|
required: requiredParams.has(paramName),
|
|
111
|
-
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Preserve enum values for string parameters
|
|
134
|
+
if (type === "string" && enumValues) {
|
|
135
|
+
param.enum = enumValues;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Preserve nested attributes for object and object[] types
|
|
139
|
+
if (attributes && attributes.length > 0) {
|
|
140
|
+
param.attributes = attributes;
|
|
141
|
+
if (type === "array") {
|
|
142
|
+
param.type = "object[]";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
parameters.push(param);
|
|
112
147
|
}
|
|
113
148
|
}
|
|
114
149
|
|