@copilotkit/runtime 1.56.0 → 1.56.2-canary.pin-to-send

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 (194) 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/graphql/resolvers/copilot.resolver.cjs +2 -1
  8. package/dist/graphql/resolvers/copilot.resolver.cjs.map +1 -1
  9. package/dist/graphql/resolvers/copilot.resolver.mjs +2 -1
  10. package/dist/graphql/resolvers/copilot.resolver.mjs.map +1 -1
  11. package/dist/graphql/resolvers/resolve-message-id.cjs +19 -0
  12. package/dist/graphql/resolvers/resolve-message-id.cjs.map +1 -0
  13. package/dist/graphql/resolvers/resolve-message-id.mjs +18 -0
  14. package/dist/graphql/resolvers/resolve-message-id.mjs.map +1 -0
  15. package/dist/lib/integrations/node-http/index.cjs +4 -1
  16. package/dist/lib/integrations/node-http/index.cjs.map +1 -1
  17. package/dist/lib/integrations/node-http/index.d.cts.map +1 -1
  18. package/dist/lib/integrations/node-http/index.d.mts.map +1 -1
  19. package/dist/lib/integrations/node-http/index.mjs +4 -1
  20. package/dist/lib/integrations/node-http/index.mjs.map +1 -1
  21. package/dist/lib/runtime/copilot-runtime.cjs +15 -3
  22. package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
  23. package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
  24. package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
  25. package/dist/lib/runtime/copilot-runtime.mjs +15 -3
  26. package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
  27. package/dist/lib/runtime/mcp-tools-utils.cjs +21 -4
  28. package/dist/lib/runtime/mcp-tools-utils.cjs.map +1 -1
  29. package/dist/lib/runtime/mcp-tools-utils.d.cts.map +1 -1
  30. package/dist/lib/runtime/mcp-tools-utils.d.mts.map +1 -1
  31. package/dist/lib/runtime/mcp-tools-utils.mjs +21 -4
  32. package/dist/lib/runtime/mcp-tools-utils.mjs.map +1 -1
  33. package/dist/package.cjs +2 -2
  34. package/dist/package.mjs +2 -2
  35. package/dist/service-adapters/anthropic/anthropic-adapter.cjs +11 -3
  36. package/dist/service-adapters/anthropic/anthropic-adapter.cjs.map +1 -1
  37. package/dist/service-adapters/anthropic/anthropic-adapter.d.cts +6 -0
  38. package/dist/service-adapters/anthropic/anthropic-adapter.d.cts.map +1 -1
  39. package/dist/service-adapters/anthropic/anthropic-adapter.d.mts +6 -0
  40. package/dist/service-adapters/anthropic/anthropic-adapter.d.mts.map +1 -1
  41. package/dist/service-adapters/anthropic/anthropic-adapter.mjs +11 -3
  42. package/dist/service-adapters/anthropic/anthropic-adapter.mjs.map +1 -1
  43. package/dist/service-adapters/anthropic/utils.cjs +27 -1
  44. package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
  45. package/dist/service-adapters/anthropic/utils.mjs +27 -1
  46. package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
  47. package/dist/service-adapters/langchain/utils.cjs +1 -1
  48. package/dist/service-adapters/langchain/utils.cjs.map +1 -1
  49. package/dist/service-adapters/langchain/utils.mjs +1 -1
  50. package/dist/service-adapters/langchain/utils.mjs.map +1 -1
  51. package/dist/service-adapters/openai/openai-adapter.cjs +2 -1
  52. package/dist/service-adapters/openai/openai-adapter.cjs.map +1 -1
  53. package/dist/service-adapters/openai/openai-adapter.d.cts +6 -0
  54. package/dist/service-adapters/openai/openai-adapter.d.cts.map +1 -1
  55. package/dist/service-adapters/openai/openai-adapter.d.mts +6 -0
  56. package/dist/service-adapters/openai/openai-adapter.d.mts.map +1 -1
  57. package/dist/service-adapters/openai/openai-adapter.mjs +2 -1
  58. package/dist/service-adapters/openai/openai-adapter.mjs.map +1 -1
  59. package/dist/v2/runtime/core/debug-event-bus.cjs +36 -0
  60. package/dist/v2/runtime/core/debug-event-bus.cjs.map +1 -0
  61. package/dist/v2/runtime/core/debug-event-bus.d.cts +19 -0
  62. package/dist/v2/runtime/core/debug-event-bus.d.cts.map +1 -0
  63. package/dist/v2/runtime/core/debug-event-bus.d.mts +19 -0
  64. package/dist/v2/runtime/core/debug-event-bus.d.mts.map +1 -0
  65. package/dist/v2/runtime/core/debug-event-bus.mjs +35 -0
  66. package/dist/v2/runtime/core/debug-event-bus.mjs.map +1 -0
  67. package/dist/v2/runtime/core/fetch-handler.cjs +6 -0
  68. package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
  69. package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
  70. package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
  71. package/dist/v2/runtime/core/fetch-handler.mjs +6 -0
  72. package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
  73. package/dist/v2/runtime/core/fetch-router.cjs +1 -0
  74. package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
  75. package/dist/v2/runtime/core/fetch-router.mjs +1 -0
  76. package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
  77. package/dist/v2/runtime/core/hooks.cjs.map +1 -1
  78. package/dist/v2/runtime/core/hooks.d.cts +2 -0
  79. package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
  80. package/dist/v2/runtime/core/hooks.d.mts +2 -0
  81. package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
  82. package/dist/v2/runtime/core/hooks.mjs.map +1 -1
  83. package/dist/v2/runtime/core/middleware-sse-parser.cjs +5 -2
  84. package/dist/v2/runtime/core/middleware-sse-parser.cjs.map +1 -1
  85. package/dist/v2/runtime/core/middleware-sse-parser.mjs +5 -2
  86. package/dist/v2/runtime/core/middleware-sse-parser.mjs.map +1 -1
  87. package/dist/v2/runtime/core/runtime.cjs +5 -0
  88. package/dist/v2/runtime/core/runtime.cjs.map +1 -1
  89. package/dist/v2/runtime/core/runtime.d.cts +5 -0
  90. package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
  91. package/dist/v2/runtime/core/runtime.d.mts +5 -0
  92. package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
  93. package/dist/v2/runtime/core/runtime.mjs +5 -0
  94. package/dist/v2/runtime/core/runtime.mjs.map +1 -1
  95. package/dist/v2/runtime/handlers/handle-connect.cjs +2 -0
  96. package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
  97. package/dist/v2/runtime/handlers/handle-connect.mjs +2 -0
  98. package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
  99. package/dist/v2/runtime/handlers/handle-debug-events.cjs +33 -0
  100. package/dist/v2/runtime/handlers/handle-debug-events.cjs.map +1 -0
  101. package/dist/v2/runtime/handlers/handle-debug-events.mjs +32 -0
  102. package/dist/v2/runtime/handlers/handle-debug-events.mjs.map +1 -0
  103. package/dist/v2/runtime/handlers/handle-run.cjs +1 -0
  104. package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
  105. package/dist/v2/runtime/handlers/handle-run.mjs +1 -0
  106. package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
  107. package/dist/v2/runtime/handlers/intelligence/connect.cjs +32 -2
  108. package/dist/v2/runtime/handlers/intelligence/connect.cjs.map +1 -1
  109. package/dist/v2/runtime/handlers/intelligence/connect.mjs +31 -2
  110. package/dist/v2/runtime/handlers/intelligence/connect.mjs.map +1 -1
  111. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs +5 -1
  112. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.cjs.map +1 -1
  113. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs +5 -1
  114. package/dist/v2/runtime/handlers/shared/resolve-intelligence-user.mjs.map +1 -1
  115. package/dist/v2/runtime/handlers/shared/sse-response.cjs +21 -1
  116. package/dist/v2/runtime/handlers/shared/sse-response.cjs.map +1 -1
  117. package/dist/v2/runtime/handlers/shared/sse-response.mjs +21 -1
  118. package/dist/v2/runtime/handlers/shared/sse-response.mjs.map +1 -1
  119. package/dist/v2/runtime/handlers/sse/connect.cjs +3 -1
  120. package/dist/v2/runtime/handlers/sse/connect.cjs.map +1 -1
  121. package/dist/v2/runtime/handlers/sse/connect.mjs +3 -1
  122. package/dist/v2/runtime/handlers/sse/connect.mjs.map +1 -1
  123. package/dist/v2/runtime/handlers/sse/run.cjs +3 -1
  124. package/dist/v2/runtime/handlers/sse/run.cjs.map +1 -1
  125. package/dist/v2/runtime/handlers/sse/run.mjs +3 -1
  126. package/dist/v2/runtime/handlers/sse/run.mjs.map +1 -1
  127. package/dist/v2/runtime/intelligence-platform/client.cjs +2 -7
  128. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  129. package/dist/v2/runtime/intelligence-platform/client.d.cts +1 -4
  130. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  131. package/dist/v2/runtime/intelligence-platform/client.d.mts +1 -4
  132. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  133. package/dist/v2/runtime/intelligence-platform/client.mjs +2 -7
  134. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  135. package/dist/v2/runtime/runner/intelligence.cjs +17 -5
  136. package/dist/v2/runtime/runner/intelligence.cjs.map +1 -1
  137. package/dist/v2/runtime/runner/intelligence.d.cts +1 -0
  138. package/dist/v2/runtime/runner/intelligence.d.cts.map +1 -1
  139. package/dist/v2/runtime/runner/intelligence.d.mts +1 -0
  140. package/dist/v2/runtime/runner/intelligence.d.mts.map +1 -1
  141. package/dist/v2/runtime/runner/intelligence.mjs +17 -5
  142. package/dist/v2/runtime/runner/intelligence.mjs.map +1 -1
  143. package/package.json +3 -3
  144. package/src/agent/__tests__/provider-id-collision.test.ts +195 -0
  145. package/src/agent/index.ts +19 -11
  146. package/src/agents/langgraph/__tests__/event-source.test.ts +256 -0
  147. package/src/graphql/resolvers/__tests__/resolve-message-id.test.ts +25 -0
  148. package/src/graphql/resolvers/copilot.resolver.ts +2 -1
  149. package/src/graphql/resolvers/resolve-message-id.ts +14 -0
  150. package/src/lib/integrations/node-http/__tests__/request-duck-type.test.ts +66 -0
  151. package/src/lib/integrations/node-http/index.ts +15 -1
  152. package/src/lib/runtime/__tests__/handle-service-adapter.test.ts +108 -0
  153. package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +30 -1
  154. package/src/lib/runtime/__tests__/on-after-request.test.ts +122 -0
  155. package/src/lib/runtime/__tests__/retry-utils.test.ts +137 -0
  156. package/src/lib/runtime/agent-integrations/langgraph/__tests__/dispatch-event-filtering.test.ts +190 -0
  157. package/src/lib/runtime/copilot-runtime.ts +36 -7
  158. package/src/lib/runtime/mcp-tools-utils.ts +41 -6
  159. package/src/lib/runtime/retry-utils.ts +41 -1
  160. package/src/service-adapters/anthropic/anthropic-adapter.ts +22 -2
  161. package/src/service-adapters/anthropic/utils.ts +60 -1
  162. package/src/service-adapters/langchain/utils.ts +1 -1
  163. package/src/service-adapters/openai/openai-adapter.ts +14 -1
  164. package/src/v2/runtime/__tests__/fetch-router.test.ts +22 -0
  165. package/src/v2/runtime/__tests__/handle-connect.test.ts +58 -5
  166. package/src/v2/runtime/__tests__/handle-run.test.ts +31 -4
  167. package/src/v2/runtime/__tests__/handle-threads.test.ts +66 -4
  168. package/src/v2/runtime/__tests__/integration/node-servers.integration.test.ts +19 -0
  169. package/src/v2/runtime/__tests__/integration/suites/debug-events.suite.ts +253 -0
  170. package/src/v2/runtime/__tests__/middleware-sse-parser.test.ts +50 -0
  171. package/src/v2/runtime/__tests__/runtime.test.ts +3 -1
  172. package/src/v2/runtime/core/__tests__/debug-event-bus.test.ts +156 -0
  173. package/src/v2/runtime/core/debug-event-bus.ts +45 -0
  174. package/src/v2/runtime/core/fetch-handler.ts +4 -0
  175. package/src/v2/runtime/core/fetch-router.ts +11 -0
  176. package/src/v2/runtime/core/hooks.ts +2 -1
  177. package/src/v2/runtime/core/middleware-sse-parser.ts +12 -2
  178. package/src/v2/runtime/core/runtime.ts +12 -0
  179. package/src/v2/runtime/handlers/__tests__/handle-debug-events.test.ts +176 -0
  180. package/src/v2/runtime/handlers/handle-connect.ts +2 -0
  181. package/src/v2/runtime/handlers/handle-debug-events.ts +52 -0
  182. package/src/v2/runtime/handlers/handle-run.ts +1 -0
  183. package/src/v2/runtime/handlers/intelligence/connect.ts +58 -1
  184. package/src/v2/runtime/handlers/shared/resolve-intelligence-user.ts +4 -1
  185. package/src/v2/runtime/handlers/shared/sse-response.ts +46 -0
  186. package/src/v2/runtime/handlers/sse/__tests__/sse-connect-agent-id.test.ts +71 -0
  187. package/src/v2/runtime/handlers/sse/connect.ts +6 -0
  188. package/src/v2/runtime/handlers/sse/run.ts +4 -0
  189. package/src/v2/runtime/intelligence-platform/__tests__/client.test.ts +13 -11
  190. package/src/v2/runtime/intelligence-platform/client.ts +3 -11
  191. package/src/v2/runtime/runner/__tests__/intelligence-runner.test.ts +51 -1
  192. package/src/v2/runtime/runner/intelligence.ts +27 -9
  193. package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +268 -0
  194. package/tests/service-adapters/anthropic/utils-token-trimming.test.ts +301 -0
@@ -0,0 +1,108 @@
1
+ import type { LanguageModel } from "ai";
2
+ import { CopilotKitMisuseError } from "@copilotkit/shared";
3
+ import { describe, expect, it } from "vitest";
4
+ import { BuiltInAgent } from "../../../agent";
5
+ import type { CopilotServiceAdapter } from "../../../service-adapters";
6
+ import { CopilotRuntime } from "../copilot-runtime";
7
+
8
+ function makeAdapter(
9
+ overrides?: Partial<CopilotServiceAdapter>,
10
+ ): CopilotServiceAdapter {
11
+ return {
12
+ name: "TestAdapter",
13
+ async process() {
14
+ throw new Error("process() is not expected to be called in these tests");
15
+ },
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ async function getDefaultAgent(runtime: CopilotRuntime) {
21
+ const agents = await runtime.instance.agents;
22
+ return agents.default;
23
+ }
24
+
25
+ // `BuiltInAgent.config` is private; reading it is the only way to verify the
26
+ // correct model was passed through without running the entire agent pipeline.
27
+ // This narrow accessor is the Rule 2 exception, documented here once rather
28
+ // than inline at each call site.
29
+ function getBuiltInAgentModel(agent: BuiltInAgent): unknown {
30
+ return (agent as unknown as { config: { model: unknown } }).config.model;
31
+ }
32
+
33
+ describe("CopilotRuntime#handleServiceAdapter (#3217)", () => {
34
+ it("uses the adapter's pre-configured LanguageModel when getLanguageModel() returns one", async () => {
35
+ const fakeLanguageModel = {
36
+ specificationVersion: "v1",
37
+ } as unknown as LanguageModel;
38
+ const runtime = new CopilotRuntime();
39
+
40
+ runtime.handleServiceAdapter(
41
+ makeAdapter({
42
+ name: "OpenAIAdapter",
43
+ provider: "openai",
44
+ model: "gpt-4o",
45
+ getLanguageModel: () => fakeLanguageModel,
46
+ }),
47
+ );
48
+
49
+ const agent = await getDefaultAgent(runtime);
50
+ expect(agent).toBeInstanceOf(BuiltInAgent);
51
+ expect(getBuiltInAgentModel(agent as BuiltInAgent)).toBe(fakeLanguageModel);
52
+ });
53
+
54
+ it("builds a 'provider/model' string when only provider+model are exposed", async () => {
55
+ const runtime = new CopilotRuntime();
56
+
57
+ runtime.handleServiceAdapter(
58
+ makeAdapter({
59
+ name: "GroqAdapter",
60
+ provider: "groq",
61
+ model: "llama-3.3-70b-versatile",
62
+ }),
63
+ );
64
+
65
+ const agent = await getDefaultAgent(runtime);
66
+ expect(agent).toBeInstanceOf(BuiltInAgent);
67
+ expect(getBuiltInAgentModel(agent as BuiltInAgent)).toBe(
68
+ "groq/llama-3.3-70b-versatile",
69
+ );
70
+ });
71
+
72
+ it("throws CopilotKitMisuseError when no model source is available (LangChainAdapter regression)", async () => {
73
+ const runtime = new CopilotRuntime();
74
+
75
+ runtime.handleServiceAdapter(makeAdapter({ name: "LangChainAdapter" }));
76
+
77
+ await expect(runtime.instance.agents).rejects.toBeInstanceOf(
78
+ CopilotKitMisuseError,
79
+ );
80
+ await expect(runtime.instance.agents).rejects.toThrow(
81
+ /Service adapter "LangChainAdapter" does not provide model information/,
82
+ );
83
+ });
84
+
85
+ it("falls back to 'unknown' in the thrown error when the adapter has no name", async () => {
86
+ const runtime = new CopilotRuntime();
87
+
88
+ runtime.handleServiceAdapter(makeAdapter({ name: undefined }));
89
+
90
+ await expect(runtime.instance.agents).rejects.toThrow(
91
+ /Service adapter "unknown" does not provide model information/,
92
+ );
93
+ });
94
+
95
+ it("does not throw when provider is set without a model — but must not emit 'undefined/undefined'", async () => {
96
+ // Guards the specific #3217 regression: when only one half of the pair is
97
+ // present, we must NOT synthesize a bogus "provider/undefined" string.
98
+ const runtime = new CopilotRuntime();
99
+
100
+ runtime.handleServiceAdapter(
101
+ makeAdapter({ name: "PartialAdapter", provider: "openai" }),
102
+ );
103
+
104
+ await expect(runtime.instance.agents).rejects.toThrow(
105
+ CopilotKitMisuseError,
106
+ );
107
+ });
108
+ });
@@ -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
+ });
@@ -0,0 +1,137 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { fetchWithRetry, parseRetryAfter, RETRY_CONFIG } from "../retry-utils";
3
+
4
+ function responseWithRetryAfter(
5
+ headerValue: string | null,
6
+ status = 429,
7
+ ): Response {
8
+ const headers = new Headers();
9
+ if (headerValue !== null) headers.set("Retry-After", headerValue);
10
+ return new Response(null, { status, headers });
11
+ }
12
+
13
+ describe("parseRetryAfter", () => {
14
+ it("returns undefined when the Retry-After header is absent", () => {
15
+ expect(parseRetryAfter(responseWithRetryAfter(null))).toBeUndefined();
16
+ });
17
+
18
+ it("parses integer seconds into milliseconds", () => {
19
+ expect(parseRetryAfter(responseWithRetryAfter("5"))).toBe(5000);
20
+ });
21
+
22
+ it("treats zero seconds as zero delay", () => {
23
+ expect(parseRetryAfter(responseWithRetryAfter("0"))).toBe(0);
24
+ });
25
+
26
+ it("clamps a negative numeric value to zero", () => {
27
+ // `-1` fails the `seconds >= 0` guard and falls through to Date.parse,
28
+ // which interprets it as a year-in-the-past timestamp; the past-date
29
+ // branch then clamps to 0. The behavior is lenient rather than strict.
30
+ expect(parseRetryAfter(responseWithRetryAfter("-1"))).toBe(0);
31
+ });
32
+
33
+ it("parses an HTTP-date in the future as the delta to now", () => {
34
+ const now = Date.parse("2026-04-22T12:00:00Z");
35
+ vi.useFakeTimers();
36
+ vi.setSystemTime(now);
37
+ try {
38
+ const future = new Date(now + 30_000).toUTCString();
39
+ expect(parseRetryAfter(responseWithRetryAfter(future))).toBe(30_000);
40
+ } finally {
41
+ vi.useRealTimers();
42
+ }
43
+ });
44
+
45
+ it("clamps an HTTP-date in the past to zero", () => {
46
+ const now = Date.parse("2026-04-22T12:00:00Z");
47
+ vi.useFakeTimers();
48
+ vi.setSystemTime(now);
49
+ try {
50
+ const past = new Date(now - 60_000).toUTCString();
51
+ expect(parseRetryAfter(responseWithRetryAfter(past))).toBe(0);
52
+ } finally {
53
+ vi.useRealTimers();
54
+ }
55
+ });
56
+
57
+ it("returns undefined for unparseable values", () => {
58
+ expect(parseRetryAfter(responseWithRetryAfter("soon"))).toBeUndefined();
59
+ });
60
+ });
61
+
62
+ describe("fetchWithRetry Retry-After handling (#3637)", () => {
63
+ let fetchMock: ReturnType<typeof vi.fn>;
64
+
65
+ beforeEach(() => {
66
+ vi.useFakeTimers();
67
+ fetchMock = vi.fn();
68
+ vi.stubGlobal("fetch", fetchMock);
69
+ });
70
+
71
+ afterEach(() => {
72
+ vi.useRealTimers();
73
+ vi.unstubAllGlobals();
74
+ });
75
+
76
+ it("honors Retry-After within the allowed maximum on 429", async () => {
77
+ fetchMock
78
+ .mockResolvedValueOnce(responseWithRetryAfter("2"))
79
+ .mockResolvedValueOnce(new Response("ok", { status: 200 }));
80
+
81
+ const promise = fetchWithRetry("https://example.com", {});
82
+ await vi.advanceTimersByTimeAsync(1999);
83
+ expect(fetchMock).toHaveBeenCalledTimes(1);
84
+ await vi.advanceTimersByTimeAsync(1);
85
+ const response = await promise;
86
+
87
+ expect(response.status).toBe(200);
88
+ expect(fetchMock).toHaveBeenCalledTimes(2);
89
+ });
90
+
91
+ it("throws when Retry-After exceeds maxRetryAfterSeconds", async () => {
92
+ const excessive = RETRY_CONFIG.maxRetryAfterSeconds + 1;
93
+ fetchMock.mockResolvedValue(responseWithRetryAfter(String(excessive)));
94
+
95
+ // The oversized-Retry-After branch throws before sleeping, and the
96
+ // resulting Error doesn't match any retryable pattern, so the loop
97
+ // breaks out without consuming the remaining attempts.
98
+ await expect(fetchWithRetry("https://example.com", {})).rejects.toThrow(
99
+ new RegExp(
100
+ `Retry-After of ${excessive}s.*exceeds the maximum of ${RETRY_CONFIG.maxRetryAfterSeconds}s`,
101
+ ),
102
+ );
103
+ expect(fetchMock).toHaveBeenCalledTimes(1);
104
+ });
105
+
106
+ it("falls back to exponential backoff when Retry-After is missing on 429", async () => {
107
+ fetchMock
108
+ .mockResolvedValueOnce(new Response(null, { status: 429 }))
109
+ .mockResolvedValueOnce(new Response("ok", { status: 200 }));
110
+
111
+ const promise = fetchWithRetry("https://example.com", {});
112
+ // calculateDelay(0) === RETRY_CONFIG.baseDelayMs
113
+ await vi.advanceTimersByTimeAsync(RETRY_CONFIG.baseDelayMs - 1);
114
+ expect(fetchMock).toHaveBeenCalledTimes(1);
115
+ await vi.advanceTimersByTimeAsync(1);
116
+ const response = await promise;
117
+
118
+ expect(response.status).toBe(200);
119
+ expect(fetchMock).toHaveBeenCalledTimes(2);
120
+ });
121
+
122
+ it("ignores Retry-After on non-429 retryable responses (e.g. 503)", async () => {
123
+ const longRetryAfter = String(RETRY_CONFIG.maxRetryAfterSeconds + 600);
124
+ fetchMock
125
+ .mockResolvedValueOnce(responseWithRetryAfter(longRetryAfter, 503))
126
+ .mockResolvedValueOnce(new Response("ok", { status: 200 }));
127
+
128
+ const promise = fetchWithRetry("https://example.com", {});
129
+ // Exponential backoff applies, not the header value — otherwise this
130
+ // would wait 10 minutes and the test would time out.
131
+ await vi.advanceTimersByTimeAsync(RETRY_CONFIG.baseDelayMs);
132
+ const response = await promise;
133
+
134
+ expect(response.status).toBe(200);
135
+ expect(fetchMock).toHaveBeenCalledTimes(2);
136
+ });
137
+ });
@@ -1,5 +1,7 @@
1
1
  import { EventType } from "@ag-ui/client";
2
2
  import { LangGraphAgent } from "../agent";
3
+ import { CustomEventNames } from "../consts";
4
+ import { vi } from "vitest";
3
5
 
4
6
  function createAgent() {
5
7
  const agent = new LangGraphAgent({
@@ -33,6 +35,25 @@ function makeTextEvent(
33
35
  };
34
36
  }
35
37
 
38
+ function makeCustomEvent(name: string, value: any) {
39
+ return {
40
+ type: EventType.CUSTOM,
41
+ name,
42
+ value,
43
+ } as any;
44
+ }
45
+
46
+ /**
47
+ * Mock the parent class's langGraphDefaultMergeState for a single test,
48
+ * using vi.spyOn for automatic cleanup.
49
+ */
50
+ function withMockedParentMerge(agent: LangGraphAgent, returnValue: any) {
51
+ const parentProto = Object.getPrototypeOf(Object.getPrototypeOf(agent));
52
+ return vi
53
+ .spyOn(parentProto, "langGraphDefaultMergeState")
54
+ .mockReturnValue(returnValue);
55
+ }
56
+
36
57
  describe("dispatchEvent emit-messages filtering", () => {
37
58
  it("suppresses message events when copilotkit:emit-messages is false", () => {
38
59
  const { agent, events } = createAgent();
@@ -153,3 +174,172 @@ describe("dispatchEvent emit-tool-calls filtering", () => {
153
174
  expect(events).toHaveLength(1);
154
175
  });
155
176
  });
177
+
178
+ // ---------- CopilotKit custom event dispatch ----------
179
+
180
+ describe("dispatchEvent custom CopilotKit events", () => {
181
+ it("manually_emit_message produces TextMessage event sequence", () => {
182
+ const { agent, events } = createAgent();
183
+
184
+ const result = agent.dispatchEvent(
185
+ makeCustomEvent(CustomEventNames.CopilotKitManuallyEmitMessage, {
186
+ message_id: "msg-manual-1",
187
+ message: "Hello from agent",
188
+ role: "assistant",
189
+ }),
190
+ );
191
+
192
+ expect(result).toBe(true);
193
+ expect(events).toHaveLength(3);
194
+ expect(events[0].type).toBe(EventType.TEXT_MESSAGE_START);
195
+ expect(events[0].messageId).toBe("msg-manual-1");
196
+ expect(events[0].role).toBe("assistant");
197
+ expect(events[1].type).toBe(EventType.TEXT_MESSAGE_CONTENT);
198
+ expect(events[1].delta).toBe("Hello from agent");
199
+ expect(events[2].type).toBe(EventType.TEXT_MESSAGE_END);
200
+ expect(events[2].messageId).toBe("msg-manual-1");
201
+ });
202
+
203
+ it("manually_emit_tool_call produces ToolCall event sequence", () => {
204
+ const { agent, events } = createAgent();
205
+
206
+ const result = agent.dispatchEvent(
207
+ makeCustomEvent(CustomEventNames.CopilotKitManuallyEmitToolCall, {
208
+ id: "tc-manual-1",
209
+ name: "SearchTool",
210
+ args: { query: "test" },
211
+ }),
212
+ );
213
+
214
+ expect(result).toBe(true);
215
+ expect(events).toHaveLength(3);
216
+ expect(events[0].type).toBe(EventType.TOOL_CALL_START);
217
+ expect(events[0].toolCallId).toBe("tc-manual-1");
218
+ expect(events[0].toolCallName).toBe("SearchTool");
219
+ expect(events[1].type).toBe(EventType.TOOL_CALL_ARGS);
220
+ expect(events[1].toolCallId).toBe("tc-manual-1");
221
+ expect(events[1].delta).toEqual({ query: "test" });
222
+ expect(events[2].type).toBe(EventType.TOOL_CALL_END);
223
+ expect(events[2].toolCallId).toBe("tc-manual-1");
224
+ });
225
+
226
+ it("manually_emit_state produces StateSnapshot event", () => {
227
+ const { agent, events } = createAgent();
228
+
229
+ // Mock getStateSnapshot since it depends on thread state
230
+ (agent as any).getStateSnapshot = (state: any) => ({
231
+ values: state.values,
232
+ });
233
+
234
+ const result = agent.dispatchEvent(
235
+ makeCustomEvent(
236
+ CustomEventNames.CopilotKitManuallyEmitIntermediateState,
237
+ {
238
+ progress: 75,
239
+ },
240
+ ),
241
+ );
242
+
243
+ expect(result).toBe(true);
244
+ expect((agent as any).activeRun.manuallyEmittedState).toEqual({
245
+ progress: 75,
246
+ });
247
+ const snapshotEvents = events.filter(
248
+ (e) => e.type === EventType.STATE_SNAPSHOT,
249
+ );
250
+ expect(snapshotEvents.length).toBeGreaterThanOrEqual(1);
251
+ });
252
+
253
+ it("copilotkit_exit produces Exit custom event", () => {
254
+ const { agent, events } = createAgent();
255
+
256
+ const result = agent.dispatchEvent(
257
+ makeCustomEvent(CustomEventNames.CopilotKitExit, {}),
258
+ );
259
+
260
+ expect(result).toBe(true);
261
+ expect(events).toHaveLength(1);
262
+ expect(events[0].type).toBe(EventType.CUSTOM);
263
+ // "Exit" is the hardcoded downstream name in agent.ts — not a constant,
264
+ // because it's the value the frontend listens for, not an internal enum.
265
+ expect(events[0].name).toBe("Exit");
266
+ expect(events[0].value).toBe(true);
267
+ });
268
+
269
+ it("events without rawEvent pass through to subscriber", () => {
270
+ const { agent, events } = createAgent();
271
+
272
+ const result = agent.dispatchEvent({
273
+ type: EventType.TEXT_MESSAGE_START,
274
+ messageId: "msg-no-raw",
275
+ role: "assistant",
276
+ } as any);
277
+
278
+ expect(result).toBe(true);
279
+ expect(events).toHaveLength(1);
280
+ expect(events[0].messageId).toBe("msg-no-raw");
281
+ });
282
+ });
283
+
284
+ // ---------- langGraphDefaultMergeState ----------
285
+
286
+ describe("langGraphDefaultMergeState", () => {
287
+ afterEach(() => {
288
+ vi.restoreAllMocks();
289
+ });
290
+
291
+ it("merges copilotkit actions from ag-ui tools", () => {
292
+ const { agent } = createAgent();
293
+ const tools = [{ name: "tool1" }, { name: "tool2" }];
294
+
295
+ withMockedParentMerge(agent, {
296
+ "ag-ui": { tools, context: [] },
297
+ tools: [],
298
+ messages: [],
299
+ });
300
+
301
+ const result = agent.langGraphDefaultMergeState({} as any, [], {} as any);
302
+ expect(result.copilotkit).toBeDefined();
303
+ expect(result.copilotkit.actions).toEqual(expect.arrayContaining(tools));
304
+ });
305
+
306
+ it("merges copilotkit context from ag-ui", () => {
307
+ const { agent } = createAgent();
308
+ const context = [{ description: "user info", value: "test" }];
309
+
310
+ withMockedParentMerge(agent, {
311
+ "ag-ui": { tools: [], context },
312
+ tools: [],
313
+ messages: [],
314
+ });
315
+
316
+ const result = agent.langGraphDefaultMergeState({} as any, [], {} as any);
317
+ expect(result.copilotkit.context).toEqual(context);
318
+ });
319
+
320
+ it("handles missing ag-ui key without crashing", () => {
321
+ const { agent } = createAgent();
322
+
323
+ withMockedParentMerge(agent, { messages: [] });
324
+
325
+ const result = agent.langGraphDefaultMergeState({} as any, [], {} as any);
326
+ expect(result.copilotkit).toBeDefined();
327
+ expect(result.copilotkit.actions).toEqual([]);
328
+ expect(result.copilotkit.context).toEqual([]);
329
+ });
330
+
331
+ it("deduplicates tools from returnedTools and ag-ui tools", () => {
332
+ const { agent } = createAgent();
333
+ const tool = { name: "SharedTool", id: "shared-1" };
334
+
335
+ withMockedParentMerge(agent, {
336
+ "ag-ui": { tools: [tool], context: [] },
337
+ tools: [tool],
338
+ messages: [],
339
+ });
340
+
341
+ const result = agent.langGraphDefaultMergeState({} as any, [], {} as any);
342
+ expect(result.copilotkit.actions).toHaveLength(1);
343
+ expect(result.copilotkit.actions[0].name).toBe("SharedTool");
344
+ });
345
+ });
@@ -463,10 +463,26 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
463
463
  }
464
464
 
465
465
  if (isAgentsListEmpty) {
466
- const model =
467
- serviceAdapter.getLanguageModel?.() ??
468
- `${serviceAdapter.provider}/${serviceAdapter.model}`;
469
- agentsList.default = new BuiltInAgent({ model });
466
+ const languageModel = serviceAdapter.getLanguageModel?.();
467
+ if (languageModel) {
468
+ // Adapter exposes a pre-configured LanguageModel (e.g. OpenAI/Anthropic adapters)
469
+ agentsList.default = new BuiltInAgent({ model: languageModel });
470
+ } else if (serviceAdapter.provider && serviceAdapter.model) {
471
+ // Adapter exposes provider/model strings
472
+ agentsList.default = new BuiltInAgent({
473
+ model: `${serviceAdapter.provider}/${serviceAdapter.model}`,
474
+ });
475
+ } else {
476
+ throw new CopilotKitMisuseError({
477
+ message:
478
+ `Service adapter "${serviceAdapter.name ?? "unknown"}" does not provide model information. ` +
479
+ `When using adapters like LangChainAdapter without an explicit agents list, ` +
480
+ `please provide a default agent in the runtime config. Example:\n` +
481
+ ` new CopilotRuntime({\n` +
482
+ ` agents: { default: new BuiltInAgent({ model: "openai/gpt-4o" }) }\n` +
483
+ ` })`,
484
+ });
485
+ }
470
486
  }
471
487
 
472
488
  const actions = this.params?.actions;
@@ -626,9 +642,22 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
626
642
  params?.afterRequestMiddleware?.(hookParams);
627
643
 
628
644
  if (params?.middleware?.onAfterRequest) {
629
- // TODO: provide old expected params here when available
630
- // @ts-expect-error -- missing arguments.
631
- params.middleware.onAfterRequest({});
645
+ const messages = hookParams.messages ?? [];
646
+ params.middleware.onAfterRequest({
647
+ threadId: hookParams.threadId ?? "",
648
+ runId: hookParams.runId,
649
+ inputMessages: messages.filter(
650
+ (m): m is typeof m & { role: string } =>
651
+ "role" in m && m.role === "user",
652
+ ) as unknown as Message[],
653
+ outputMessages: messages.filter(
654
+ (m): m is typeof m & { role: string } =>
655
+ "role" in m && m.role !== "user",
656
+ ) as unknown as Message[],
657
+ // TODO: forward actual properties once the after-request hook has access to the request body
658
+ properties: {},
659
+ url: hookParams.path,
660
+ } satisfies OnAfterRequestOptions);
632
661
  }
633
662
  };
634
663
  }