@copilotkit/runtime 1.56.0 → 1.56.1

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 (71) hide show
  1. package/dist/agent/index.cjs +2 -2
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.d.cts.map +1 -1
  4. package/dist/agent/index.d.mts.map +1 -1
  5. package/dist/agent/index.mjs +2 -2
  6. package/dist/agent/index.mjs.map +1 -1
  7. package/dist/lib/integrations/node-http/index.cjs +4 -1
  8. package/dist/lib/integrations/node-http/index.cjs.map +1 -1
  9. package/dist/lib/integrations/node-http/index.d.cts.map +1 -1
  10. package/dist/lib/integrations/node-http/index.d.mts.map +1 -1
  11. package/dist/lib/integrations/node-http/index.mjs +4 -1
  12. package/dist/lib/integrations/node-http/index.mjs.map +1 -1
  13. package/dist/lib/runtime/copilot-runtime.cjs +11 -1
  14. package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
  15. package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
  16. package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
  17. package/dist/lib/runtime/copilot-runtime.mjs +11 -1
  18. package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
  19. package/dist/lib/runtime/mcp-tools-utils.cjs +21 -4
  20. package/dist/lib/runtime/mcp-tools-utils.cjs.map +1 -1
  21. package/dist/lib/runtime/mcp-tools-utils.d.cts.map +1 -1
  22. package/dist/lib/runtime/mcp-tools-utils.d.mts.map +1 -1
  23. package/dist/lib/runtime/mcp-tools-utils.mjs +21 -4
  24. package/dist/lib/runtime/mcp-tools-utils.mjs.map +1 -1
  25. package/dist/package.cjs +1 -1
  26. package/dist/package.mjs +1 -1
  27. package/dist/service-adapters/anthropic/anthropic-adapter.cjs +11 -3
  28. package/dist/service-adapters/anthropic/anthropic-adapter.cjs.map +1 -1
  29. package/dist/service-adapters/anthropic/anthropic-adapter.d.cts +6 -0
  30. package/dist/service-adapters/anthropic/anthropic-adapter.d.cts.map +1 -1
  31. package/dist/service-adapters/anthropic/anthropic-adapter.d.mts +6 -0
  32. package/dist/service-adapters/anthropic/anthropic-adapter.d.mts.map +1 -1
  33. package/dist/service-adapters/anthropic/anthropic-adapter.mjs +11 -3
  34. package/dist/service-adapters/anthropic/anthropic-adapter.mjs.map +1 -1
  35. package/dist/service-adapters/anthropic/utils.cjs +27 -1
  36. package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
  37. package/dist/service-adapters/anthropic/utils.mjs +27 -1
  38. package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
  39. package/dist/service-adapters/langchain/utils.cjs +1 -1
  40. package/dist/service-adapters/langchain/utils.cjs.map +1 -1
  41. package/dist/service-adapters/langchain/utils.mjs +1 -1
  42. package/dist/service-adapters/langchain/utils.mjs.map +1 -1
  43. package/dist/service-adapters/openai/openai-adapter.cjs +2 -1
  44. package/dist/service-adapters/openai/openai-adapter.cjs.map +1 -1
  45. package/dist/service-adapters/openai/openai-adapter.d.cts +6 -0
  46. package/dist/service-adapters/openai/openai-adapter.d.cts.map +1 -1
  47. package/dist/service-adapters/openai/openai-adapter.d.mts +6 -0
  48. package/dist/service-adapters/openai/openai-adapter.d.mts.map +1 -1
  49. package/dist/service-adapters/openai/openai-adapter.mjs +2 -1
  50. package/dist/service-adapters/openai/openai-adapter.mjs.map +1 -1
  51. package/dist/v2/runtime/core/middleware-sse-parser.cjs +5 -2
  52. package/dist/v2/runtime/core/middleware-sse-parser.cjs.map +1 -1
  53. package/dist/v2/runtime/core/middleware-sse-parser.mjs +5 -2
  54. package/dist/v2/runtime/core/middleware-sse-parser.mjs.map +1 -1
  55. package/package.json +2 -2
  56. package/src/agent/__tests__/provider-id-collision.test.ts +195 -0
  57. package/src/agent/index.ts +19 -11
  58. package/src/lib/integrations/node-http/__tests__/request-duck-type.test.ts +66 -0
  59. package/src/lib/integrations/node-http/index.ts +15 -1
  60. package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +30 -1
  61. package/src/lib/runtime/__tests__/on-after-request.test.ts +122 -0
  62. package/src/lib/runtime/copilot-runtime.ts +16 -3
  63. package/src/lib/runtime/mcp-tools-utils.ts +41 -6
  64. package/src/service-adapters/anthropic/anthropic-adapter.ts +22 -2
  65. package/src/service-adapters/anthropic/utils.ts +60 -1
  66. package/src/service-adapters/langchain/utils.ts +1 -1
  67. package/src/service-adapters/openai/openai-adapter.ts +14 -1
  68. package/src/v2/runtime/__tests__/middleware-sse-parser.test.ts +50 -0
  69. package/src/v2/runtime/core/middleware-sse-parser.ts +12 -2
  70. package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +268 -0
  71. 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
+ });
@@ -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/"0"
1271
- // to prevent consecutive reasoning blocks from sharing a messageId
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
- reasoningMessageId =
1274
- providedId && providedId !== "0"
1275
- ? (providedId as typeof reasoningMessageId)
1276
- : randomUUID();
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 or "0" to prevent message merging issues
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
- messageId =
1347
- providedId && providedId !== "0"
1348
- ? (providedId as typeof messageId)
1349
- : randomUUID();
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 instanceof Request) {
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: "array",
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
- // TODO: provide old expected params here when available
630
- // @ts-expect-error -- missing arguments.
631
- params.middleware.onAfterRequest({});
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
- const enumValues = paramDef.enum.join(" | ");
91
+ enumValues = paramDef.enum.map(String);
92
+ const enumDisplay = enumValues.join(" | ");
91
93
  description =
92
94
  description +
93
95
  (description ? " " : "") +
94
- `Allowed values: ${enumValues}`;
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
- parameters.push({
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