@copilotkit/runtime 1.55.3 → 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 (169) hide show
  1. package/dist/agent/converters/tanstack.cjs.map +1 -1
  2. package/dist/agent/converters/tanstack.d.cts +6 -19
  3. package/dist/agent/converters/tanstack.d.cts.map +1 -1
  4. package/dist/agent/converters/tanstack.d.mts +6 -19
  5. package/dist/agent/converters/tanstack.d.mts.map +1 -1
  6. package/dist/agent/converters/tanstack.mjs.map +1 -1
  7. package/dist/agent/index.cjs +16 -2
  8. package/dist/agent/index.cjs.map +1 -1
  9. package/dist/agent/index.d.cts +12 -1
  10. package/dist/agent/index.d.cts.map +1 -1
  11. package/dist/agent/index.d.mts +12 -1
  12. package/dist/agent/index.d.mts.map +1 -1
  13. package/dist/agent/index.mjs +16 -2
  14. package/dist/agent/index.mjs.map +1 -1
  15. package/dist/index.cjs +1 -1
  16. package/dist/index.d.cts +3 -2
  17. package/dist/index.d.mts +3 -2
  18. package/dist/index.mjs +1 -1
  19. package/dist/lib/index.cjs +1 -1
  20. package/dist/lib/index.d.cts +2 -1
  21. package/dist/lib/index.d.cts.map +1 -1
  22. package/dist/lib/index.d.mts +2 -1
  23. package/dist/lib/index.d.mts.map +1 -1
  24. package/dist/lib/index.mjs +1 -1
  25. package/dist/lib/integrations/node-http/index.cjs +4 -1
  26. package/dist/lib/integrations/node-http/index.cjs.map +1 -1
  27. package/dist/lib/integrations/node-http/index.d.cts.map +1 -1
  28. package/dist/lib/integrations/node-http/index.d.mts.map +1 -1
  29. package/dist/lib/integrations/node-http/index.mjs +4 -1
  30. package/dist/lib/integrations/node-http/index.mjs.map +1 -1
  31. package/dist/lib/integrations/shared.cjs +1 -1
  32. package/dist/lib/integrations/shared.d.cts +1 -1
  33. package/dist/lib/integrations/shared.d.mts +1 -1
  34. package/dist/lib/integrations/shared.mjs +1 -1
  35. package/dist/lib/runtime/copilot-runtime.cjs +25 -5
  36. package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
  37. package/dist/lib/runtime/copilot-runtime.d.cts +15 -3
  38. package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
  39. package/dist/lib/runtime/copilot-runtime.d.mts +15 -3
  40. package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
  41. package/dist/lib/runtime/copilot-runtime.mjs +25 -5
  42. package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
  43. package/dist/lib/runtime/mcp-tools-utils.cjs +21 -4
  44. package/dist/lib/runtime/mcp-tools-utils.cjs.map +1 -1
  45. package/dist/lib/runtime/mcp-tools-utils.d.cts.map +1 -1
  46. package/dist/lib/runtime/mcp-tools-utils.d.mts.map +1 -1
  47. package/dist/lib/runtime/mcp-tools-utils.mjs +21 -4
  48. package/dist/lib/runtime/mcp-tools-utils.mjs.map +1 -1
  49. package/dist/package.cjs +6 -5
  50. package/dist/package.mjs +6 -5
  51. package/dist/service-adapters/anthropic/anthropic-adapter.cjs +11 -3
  52. package/dist/service-adapters/anthropic/anthropic-adapter.cjs.map +1 -1
  53. package/dist/service-adapters/anthropic/anthropic-adapter.d.cts +6 -0
  54. package/dist/service-adapters/anthropic/anthropic-adapter.d.cts.map +1 -1
  55. package/dist/service-adapters/anthropic/anthropic-adapter.d.mts +6 -0
  56. package/dist/service-adapters/anthropic/anthropic-adapter.d.mts.map +1 -1
  57. package/dist/service-adapters/anthropic/anthropic-adapter.mjs +11 -3
  58. package/dist/service-adapters/anthropic/anthropic-adapter.mjs.map +1 -1
  59. package/dist/service-adapters/anthropic/utils.cjs +27 -1
  60. package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
  61. package/dist/service-adapters/anthropic/utils.mjs +27 -1
  62. package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
  63. package/dist/service-adapters/langchain/utils.cjs +1 -1
  64. package/dist/service-adapters/langchain/utils.cjs.map +1 -1
  65. package/dist/service-adapters/langchain/utils.mjs +1 -1
  66. package/dist/service-adapters/langchain/utils.mjs.map +1 -1
  67. package/dist/service-adapters/openai/openai-adapter.cjs +3 -2
  68. package/dist/service-adapters/openai/openai-adapter.cjs.map +1 -1
  69. package/dist/service-adapters/openai/openai-adapter.d.cts +6 -0
  70. package/dist/service-adapters/openai/openai-adapter.d.cts.map +1 -1
  71. package/dist/service-adapters/openai/openai-adapter.d.mts +6 -0
  72. package/dist/service-adapters/openai/openai-adapter.d.mts.map +1 -1
  73. package/dist/service-adapters/openai/openai-adapter.mjs +4 -3
  74. package/dist/service-adapters/openai/openai-adapter.mjs.map +1 -1
  75. package/dist/service-adapters/openai/openai-assistant-adapter.cjs +8 -9
  76. package/dist/service-adapters/openai/openai-assistant-adapter.cjs.map +1 -1
  77. package/dist/service-adapters/openai/openai-assistant-adapter.d.cts.map +1 -1
  78. package/dist/service-adapters/openai/openai-assistant-adapter.d.mts.map +1 -1
  79. package/dist/service-adapters/openai/openai-assistant-adapter.mjs +9 -10
  80. package/dist/service-adapters/openai/openai-assistant-adapter.mjs.map +1 -1
  81. package/dist/service-adapters/openai/utils.cjs +53 -0
  82. package/dist/service-adapters/openai/utils.cjs.map +1 -1
  83. package/dist/service-adapters/openai/utils.mjs +51 -1
  84. package/dist/service-adapters/openai/utils.mjs.map +1 -1
  85. package/dist/v2/index.cjs +1 -0
  86. package/dist/v2/index.d.cts +3 -3
  87. package/dist/v2/index.d.mts +3 -3
  88. package/dist/v2/index.mjs +2 -2
  89. package/dist/v2/runtime/core/middleware-sse-parser.cjs +5 -2
  90. package/dist/v2/runtime/core/middleware-sse-parser.cjs.map +1 -1
  91. package/dist/v2/runtime/core/middleware-sse-parser.mjs +5 -2
  92. package/dist/v2/runtime/core/middleware-sse-parser.mjs.map +1 -1
  93. package/dist/v2/runtime/core/runtime.cjs +25 -0
  94. package/dist/v2/runtime/core/runtime.cjs.map +1 -1
  95. package/dist/v2/runtime/core/runtime.d.cts +53 -4
  96. package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
  97. package/dist/v2/runtime/core/runtime.d.mts +53 -4
  98. package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
  99. package/dist/v2/runtime/core/runtime.mjs +26 -2
  100. package/dist/v2/runtime/core/runtime.mjs.map +1 -1
  101. package/dist/v2/runtime/handlers/get-runtime-info.cjs +18 -10
  102. package/dist/v2/runtime/handlers/get-runtime-info.cjs.map +1 -1
  103. package/dist/v2/runtime/handlers/get-runtime-info.mjs +19 -11
  104. package/dist/v2/runtime/handlers/get-runtime-info.mjs.map +1 -1
  105. package/dist/v2/runtime/handlers/handle-connect.cjs +1 -1
  106. package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
  107. package/dist/v2/runtime/handlers/handle-connect.mjs +1 -1
  108. package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
  109. package/dist/v2/runtime/handlers/handle-run.cjs +8 -2
  110. package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
  111. package/dist/v2/runtime/handlers/handle-run.mjs +8 -2
  112. package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
  113. package/dist/v2/runtime/handlers/handle-stop.cjs +2 -1
  114. package/dist/v2/runtime/handlers/handle-stop.cjs.map +1 -1
  115. package/dist/v2/runtime/handlers/handle-stop.mjs +2 -1
  116. package/dist/v2/runtime/handlers/handle-stop.mjs.map +1 -1
  117. package/dist/v2/runtime/handlers/intelligence/thread-names.cjs +1 -1
  118. package/dist/v2/runtime/handlers/intelligence/thread-names.cjs.map +1 -1
  119. package/dist/v2/runtime/handlers/intelligence/thread-names.mjs +1 -1
  120. package/dist/v2/runtime/handlers/intelligence/thread-names.mjs.map +1 -1
  121. package/dist/v2/runtime/handlers/shared/agent-utils.cjs +3 -2
  122. package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
  123. package/dist/v2/runtime/handlers/shared/agent-utils.mjs +3 -2
  124. package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
  125. package/dist/v2/runtime/handlers/shared/sse-response.cjs +40 -1
  126. package/dist/v2/runtime/handlers/shared/sse-response.cjs.map +1 -1
  127. package/dist/v2/runtime/handlers/shared/sse-response.mjs +40 -1
  128. package/dist/v2/runtime/handlers/shared/sse-response.mjs.map +1 -1
  129. package/dist/v2/runtime/handlers/sse/run.cjs +3 -1
  130. package/dist/v2/runtime/handlers/sse/run.cjs.map +1 -1
  131. package/dist/v2/runtime/handlers/sse/run.mjs +3 -1
  132. package/dist/v2/runtime/handlers/sse/run.mjs.map +1 -1
  133. package/dist/v2/runtime/index.d.cts +1 -1
  134. package/dist/v2/runtime/index.d.mts +1 -1
  135. package/package.json +7 -6
  136. package/src/agent/__tests__/capabilities.test.ts +81 -0
  137. package/src/agent/__tests__/provider-id-collision.test.ts +195 -0
  138. package/src/agent/converters/tanstack.ts +15 -7
  139. package/src/agent/index.ts +52 -11
  140. package/src/lib/integrations/node-http/__tests__/request-duck-type.test.ts +66 -0
  141. package/src/lib/integrations/node-http/index.ts +15 -1
  142. package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +30 -1
  143. package/src/lib/runtime/__tests__/on-after-request.test.ts +122 -0
  144. package/src/lib/runtime/__tests__/v1-agent-factory.test.ts +109 -0
  145. package/src/lib/runtime/copilot-runtime.ts +54 -5
  146. package/src/lib/runtime/mcp-tools-utils.ts +41 -6
  147. package/src/service-adapters/anthropic/anthropic-adapter.ts +22 -2
  148. package/src/service-adapters/anthropic/utils.ts +60 -1
  149. package/src/service-adapters/langchain/utils.ts +1 -1
  150. package/src/service-adapters/openai/__tests__/openai-v5-compat.test.ts +177 -0
  151. package/src/service-adapters/openai/openai-adapter.ts +17 -2
  152. package/src/service-adapters/openai/openai-assistant-adapter.ts +7 -9
  153. package/src/service-adapters/openai/utils.ts +100 -0
  154. package/src/v2/runtime/__tests__/agents-factory.test.ts +136 -0
  155. package/src/v2/runtime/__tests__/debug-sse-response.test.ts +302 -0
  156. package/src/v2/runtime/__tests__/get-runtime-info.test.ts +134 -1
  157. package/src/v2/runtime/__tests__/middleware-sse-parser.test.ts +50 -0
  158. package/src/v2/runtime/core/middleware-sse-parser.ts +12 -2
  159. package/src/v2/runtime/core/runtime.ts +90 -2
  160. package/src/v2/runtime/handlers/get-runtime-info.ts +33 -8
  161. package/src/v2/runtime/handlers/handle-connect.ts +1 -1
  162. package/src/v2/runtime/handlers/handle-run.ts +16 -2
  163. package/src/v2/runtime/handlers/handle-stop.ts +2 -1
  164. package/src/v2/runtime/handlers/intelligence/thread-names.ts +1 -1
  165. package/src/v2/runtime/handlers/shared/agent-utils.ts +3 -2
  166. package/src/v2/runtime/handlers/shared/sse-response.ts +69 -0
  167. package/src/v2/runtime/handlers/sse/run.ts +9 -0
  168. package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +268 -0
  169. package/tests/service-adapters/anthropic/utils-token-trimming.test.ts +301 -0
@@ -0,0 +1,302 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { Observable, of } from "rxjs";
3
+ import { BaseEvent, EventType } from "@ag-ui/client";
4
+ import type { ResolvedDebugConfig } from "@copilotkit/shared";
5
+
6
+ const mockDebug = vi.fn();
7
+
8
+ vi.mock("pino", () => ({
9
+ default: vi.fn(() => ({
10
+ child: vi.fn(() => ({ debug: mockDebug })),
11
+ debug: mockDebug,
12
+ })),
13
+ }));
14
+
15
+ vi.mock("pino-pretty", () => ({ default: vi.fn() }));
16
+
17
+ vi.mock("../../telemetry", () => ({
18
+ telemetry: { capture: vi.fn() },
19
+ }));
20
+
21
+ import pino from "pino";
22
+ import { createSseEventResponse } from "../handlers/shared/sse-response";
23
+
24
+ function createTestObservable(events: BaseEvent[]): Observable<BaseEvent> {
25
+ return of(...events);
26
+ }
27
+
28
+ function createMockRequest(): Request {
29
+ return new Request("https://example.com/agent/test/run", {
30
+ method: "POST",
31
+ });
32
+ }
33
+
34
+ async function drainResponse(response: Response): Promise<void> {
35
+ const reader = response.body!.getReader();
36
+ while (true) {
37
+ const { done } = await reader.read();
38
+ if (done) break;
39
+ }
40
+ }
41
+
42
+ describe("createSseEventResponse debug logging", () => {
43
+ beforeEach(() => {
44
+ mockDebug.mockClear();
45
+ });
46
+
47
+ it("does not log when debug is undefined", async () => {
48
+ const response = createSseEventResponse({
49
+ request: createMockRequest(),
50
+ observableFactory: () => createTestObservable([]),
51
+ });
52
+
53
+ await drainResponse(response);
54
+
55
+ expect(mockDebug).not.toHaveBeenCalled();
56
+ });
57
+
58
+ it("logs lifecycle message on stream open", async () => {
59
+ const debug: ResolvedDebugConfig = {
60
+ enabled: true,
61
+ events: false,
62
+ lifecycle: true,
63
+ verbose: false,
64
+ };
65
+
66
+ const event: BaseEvent = {
67
+ type: EventType.RUN_STARTED,
68
+ threadId: "t-1",
69
+ runId: "r-1",
70
+ } as BaseEvent;
71
+
72
+ const response = createSseEventResponse({
73
+ request: createMockRequest(),
74
+ observableFactory: () => createTestObservable([event]),
75
+ debug,
76
+ });
77
+
78
+ await drainResponse(response);
79
+
80
+ expect(mockDebug).toHaveBeenCalledWith("SSE stream opened");
81
+ });
82
+
83
+ it("logs lifecycle completion with total eventCount even when event logging is off", async () => {
84
+ const debug: ResolvedDebugConfig = {
85
+ enabled: true,
86
+ events: false,
87
+ lifecycle: true,
88
+ verbose: false,
89
+ };
90
+
91
+ const events: BaseEvent[] = [
92
+ {
93
+ type: EventType.RUN_STARTED,
94
+ threadId: "t-1",
95
+ runId: "r-1",
96
+ } as BaseEvent,
97
+ {
98
+ type: EventType.RUN_FINISHED,
99
+ threadId: "t-1",
100
+ runId: "r-1",
101
+ } as BaseEvent,
102
+ ];
103
+
104
+ const response = createSseEventResponse({
105
+ request: createMockRequest(),
106
+ observableFactory: () => createTestObservable(events),
107
+ debug,
108
+ });
109
+
110
+ await drainResponse(response);
111
+
112
+ expect(mockDebug).toHaveBeenCalledWith(
113
+ { eventCount: 2, loggedEventCount: 0 },
114
+ "SSE stream completed",
115
+ );
116
+ });
117
+
118
+ it("logs lifecycle completion with matching eventCount and loggedEventCount when events enabled", async () => {
119
+ const debug: ResolvedDebugConfig = {
120
+ enabled: true,
121
+ events: true,
122
+ lifecycle: true,
123
+ verbose: false,
124
+ };
125
+
126
+ const events: BaseEvent[] = [
127
+ {
128
+ type: EventType.RUN_STARTED,
129
+ threadId: "t-1",
130
+ runId: "r-1",
131
+ } as BaseEvent,
132
+ {
133
+ type: EventType.RUN_FINISHED,
134
+ threadId: "t-1",
135
+ runId: "r-1",
136
+ } as BaseEvent,
137
+ ];
138
+
139
+ const response = createSseEventResponse({
140
+ request: createMockRequest(),
141
+ observableFactory: () => createTestObservable(events),
142
+ debug,
143
+ });
144
+
145
+ await drainResponse(response);
146
+
147
+ expect(mockDebug).toHaveBeenCalledWith(
148
+ { eventCount: 2, loggedEventCount: 2 },
149
+ "SSE stream completed",
150
+ );
151
+ });
152
+
153
+ it("logs events in summary mode when verbose is false", async () => {
154
+ const debug: ResolvedDebugConfig = {
155
+ enabled: true,
156
+ events: true,
157
+ lifecycle: false,
158
+ verbose: false,
159
+ };
160
+
161
+ const event: BaseEvent = {
162
+ type: EventType.TEXT_MESSAGE_START,
163
+ messageId: "msg-1",
164
+ role: "assistant",
165
+ } as BaseEvent;
166
+
167
+ const response = createSseEventResponse({
168
+ request: createMockRequest(),
169
+ observableFactory: () => createTestObservable([event]),
170
+ debug,
171
+ });
172
+
173
+ await drainResponse(response);
174
+
175
+ const eventEmittedCalls = mockDebug.mock.calls.filter(
176
+ (call) => call[call.length - 1] === "Event emitted",
177
+ );
178
+ expect(eventEmittedCalls.length).toBeGreaterThanOrEqual(1);
179
+
180
+ const [summaryArg] = eventEmittedCalls[0];
181
+ expect(summaryArg).toHaveProperty("type", EventType.TEXT_MESSAGE_START);
182
+ expect(summaryArg).toHaveProperty("messageId", "msg-1");
183
+ // In summary mode the full event object is not passed directly
184
+ expect(summaryArg).not.toHaveProperty("event");
185
+ });
186
+
187
+ it("logs events in verbose mode with full event object", async () => {
188
+ const debug: ResolvedDebugConfig = {
189
+ enabled: true,
190
+ events: true,
191
+ lifecycle: false,
192
+ verbose: true,
193
+ };
194
+
195
+ const event: BaseEvent = {
196
+ type: EventType.TEXT_MESSAGE_START,
197
+ messageId: "msg-1",
198
+ role: "assistant",
199
+ } as BaseEvent;
200
+
201
+ const response = createSseEventResponse({
202
+ request: createMockRequest(),
203
+ observableFactory: () => createTestObservable([event]),
204
+ debug,
205
+ });
206
+
207
+ await drainResponse(response);
208
+
209
+ const eventEmittedCalls = mockDebug.mock.calls.filter(
210
+ (call) => call[call.length - 1] === "Event emitted",
211
+ );
212
+ expect(eventEmittedCalls.length).toBeGreaterThanOrEqual(1);
213
+
214
+ const [verboseArg] = eventEmittedCalls[0];
215
+ expect(verboseArg).toHaveProperty("event");
216
+ expect(verboseArg.event).toEqual(event);
217
+ });
218
+
219
+ it("uses a pre-created logger instead of calling createLogger when one is provided", async () => {
220
+ const debug: ResolvedDebugConfig = {
221
+ enabled: true,
222
+ events: false,
223
+ lifecycle: true,
224
+ verbose: false,
225
+ };
226
+
227
+ const externalDebug = vi.fn();
228
+ const externalLogger = { debug: externalDebug } as any;
229
+
230
+ // Clear the pino mock call count so we can assert it was NOT called again
231
+ const pinoMock = pino as unknown as ReturnType<typeof vi.fn>;
232
+ pinoMock.mockClear();
233
+
234
+ const event: BaseEvent = {
235
+ type: EventType.RUN_STARTED,
236
+ threadId: "t-1",
237
+ runId: "r-1",
238
+ } as BaseEvent;
239
+
240
+ const response = createSseEventResponse({
241
+ request: createMockRequest(),
242
+ observableFactory: () => createTestObservable([event]),
243
+ debug,
244
+ logger: externalLogger,
245
+ });
246
+
247
+ await drainResponse(response);
248
+
249
+ // The external logger should have been used for lifecycle messages
250
+ expect(externalDebug).toHaveBeenCalledWith("SSE stream opened");
251
+ expect(externalDebug).toHaveBeenCalledWith(
252
+ { eventCount: 1, loggedEventCount: 0 },
253
+ "SSE stream completed",
254
+ );
255
+
256
+ // pino should NOT have been called – the pre-created logger was reused
257
+ expect(pinoMock).not.toHaveBeenCalled();
258
+ });
259
+
260
+ it("does not log events when events is disabled", async () => {
261
+ const debug: ResolvedDebugConfig = {
262
+ enabled: true,
263
+ events: false,
264
+ lifecycle: true,
265
+ verbose: false,
266
+ };
267
+
268
+ const events: BaseEvent[] = [
269
+ {
270
+ type: EventType.TEXT_MESSAGE_START,
271
+ messageId: "msg-1",
272
+ role: "assistant",
273
+ } as BaseEvent,
274
+ {
275
+ type: EventType.RUN_FINISHED,
276
+ threadId: "t-1",
277
+ runId: "r-1",
278
+ } as BaseEvent,
279
+ ];
280
+
281
+ const response = createSseEventResponse({
282
+ request: createMockRequest(),
283
+ observableFactory: () => createTestObservable(events),
284
+ debug,
285
+ });
286
+
287
+ await drainResponse(response);
288
+
289
+ const eventEmittedCalls = mockDebug.mock.calls.filter(
290
+ (call) => call[call.length - 1] === "Event emitted",
291
+ );
292
+ expect(eventEmittedCalls).toHaveLength(0);
293
+
294
+ // Only lifecycle calls should be present
295
+ const lifecycleCalls = mockDebug.mock.calls.filter(
296
+ (call) =>
297
+ call[call.length - 1] === "SSE stream opened" ||
298
+ call[call.length - 1] === "SSE stream completed",
299
+ );
300
+ expect(lifecycleCalls.length).toBeGreaterThanOrEqual(1);
301
+ });
302
+ });
@@ -1,7 +1,7 @@
1
1
  import { handleGetRuntimeInfo } from "../handlers/get-runtime-info";
2
2
  import { CopilotRuntime } from "../core/runtime";
3
3
  import { TranscriptionService } from "../transcription-service/transcription-service";
4
- import { describe, it, expect } from "vitest";
4
+ import { describe, it, expect, vi } from "vitest";
5
5
  import type { AbstractAgent } from "@ag-ui/client";
6
6
 
7
7
  // Mock transcription service
@@ -117,6 +117,139 @@ describe("handleGetRuntimeInfo", () => {
117
117
  expect(data.a2uiEnabled).toBe(true);
118
118
  });
119
119
 
120
+ it("should include capabilities when agent implements getCapabilities", async () => {
121
+ const mockCapabilities = {
122
+ tools: { supported: true, clientProvided: true },
123
+ transport: { streaming: true },
124
+ };
125
+
126
+ const mockAgent = {
127
+ description: "Capable agent",
128
+ constructor: { name: "CapableAgent" },
129
+ getCapabilities: async () => mockCapabilities,
130
+ };
131
+
132
+ const runtime = new CopilotRuntime({
133
+ agents: {
134
+ capableAgent: mockAgent as unknown as AbstractAgent,
135
+ },
136
+ });
137
+
138
+ const response = await handleGetRuntimeInfo({
139
+ runtime,
140
+ request: mockRequest,
141
+ });
142
+
143
+ expect(response.status).toBe(200);
144
+
145
+ const data = await response.json();
146
+ expect(data.agents.capableAgent.capabilities).toEqual(mockCapabilities);
147
+ });
148
+
149
+ it("should omit capabilities when agent does not implement getCapabilities", async () => {
150
+ const mockAgent = {
151
+ description: "Basic agent",
152
+ constructor: { name: "BasicAgent" },
153
+ };
154
+
155
+ const runtime = new CopilotRuntime({
156
+ agents: {
157
+ basicAgent: mockAgent as unknown as AbstractAgent,
158
+ },
159
+ });
160
+
161
+ const response = await handleGetRuntimeInfo({
162
+ runtime,
163
+ request: mockRequest,
164
+ });
165
+
166
+ expect(response.status).toBe(200);
167
+
168
+ const data = await response.json();
169
+ expect(data.agents.basicAgent.capabilities).toBeUndefined();
170
+ });
171
+
172
+ it("should include empty capabilities object when getCapabilities returns {}", async () => {
173
+ const mockAgent = {
174
+ description: "Empty caps agent",
175
+ constructor: { name: "EmptyCapsAgent" },
176
+ getCapabilities: async () => ({}),
177
+ };
178
+
179
+ const runtime = new CopilotRuntime({
180
+ agents: {
181
+ emptyAgent: mockAgent as unknown as AbstractAgent,
182
+ },
183
+ });
184
+
185
+ const response = await handleGetRuntimeInfo({
186
+ runtime,
187
+ request: mockRequest,
188
+ });
189
+
190
+ expect(response.status).toBe(200);
191
+
192
+ const data = await response.json();
193
+ // {} is truthy, so it should be included in the response
194
+ expect(data.agents.emptyAgent.capabilities).toEqual({});
195
+ });
196
+
197
+ it("should isolate per-agent getCapabilities failures and log a warning", async () => {
198
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
199
+
200
+ const failingAgent = {
201
+ description: "Failing agent",
202
+ constructor: { name: "FailingAgent" },
203
+ getCapabilities: async () => {
204
+ throw new Error("capability fetch failed");
205
+ },
206
+ };
207
+
208
+ const healthyAgent = {
209
+ description: "Healthy agent",
210
+ constructor: { name: "HealthyAgent" },
211
+ getCapabilities: async () => ({
212
+ tools: { supported: true },
213
+ }),
214
+ };
215
+
216
+ const runtime = new CopilotRuntime({
217
+ agents: {
218
+ failing: failingAgent as unknown as AbstractAgent,
219
+ healthy: healthyAgent as unknown as AbstractAgent,
220
+ },
221
+ });
222
+
223
+ const response = await handleGetRuntimeInfo({
224
+ runtime,
225
+ request: mockRequest,
226
+ });
227
+
228
+ expect(response.status).toBe(200);
229
+
230
+ const data = await response.json();
231
+ // Failing agent should still appear but without capabilities
232
+ expect(data.agents.failing).toEqual({
233
+ name: "failing",
234
+ description: "Failing agent",
235
+ className: "FailingAgent",
236
+ });
237
+ expect(data.agents.failing.capabilities).toBeUndefined();
238
+
239
+ // Healthy agent should have its capabilities
240
+ expect(data.agents.healthy.capabilities).toEqual({
241
+ tools: { supported: true },
242
+ });
243
+
244
+ // Error should be logged, not silently swallowed
245
+ expect(warnSpy).toHaveBeenCalledWith(
246
+ 'Failed to fetch capabilities for agent "failing":',
247
+ "capability fetch failed",
248
+ );
249
+
250
+ warnSpy.mockRestore();
251
+ });
252
+
120
253
  it("should return 500 error when runtime.agents throws an error", async () => {
121
254
  const runtime = {
122
255
  get agents(): Record<string, AbstractAgent> {
@@ -83,6 +83,56 @@ describe("parseSSEResponse", () => {
83
83
  });
84
84
  });
85
85
 
86
+ it("normalizes array content in TOOL_CALL_RESULT (MCP adapters)", async () => {
87
+ const response = buildSSEResponse([
88
+ { type: "RUN_STARTED", threadId: "t-1", runId: "r-1" },
89
+ {
90
+ type: "TOOL_CALL_RESULT",
91
+ toolCallId: "tc-1",
92
+ messageId: "m-result",
93
+ role: "tool",
94
+ content: [
95
+ { type: "text", text: '{"metric":"cpu","value":42}' },
96
+ { type: "text", text: " extra info" },
97
+ ],
98
+ },
99
+ { type: "RUN_FINISHED", threadId: "t-1", runId: "r-1" },
100
+ ]);
101
+ const result = await parseSSEResponse(response);
102
+ expect(result.messages).toContainEqual({
103
+ id: "m-result",
104
+ role: "tool",
105
+ content: '{"metric":"cpu","value":42} extra info',
106
+ toolCallId: "tc-1",
107
+ });
108
+ });
109
+
110
+ it("filters non-text parts when normalizing array content in TOOL_CALL_RESULT", async () => {
111
+ const response = buildSSEResponse([
112
+ { type: "RUN_STARTED", threadId: "t-1", runId: "r-1" },
113
+ {
114
+ type: "TOOL_CALL_RESULT",
115
+ toolCallId: "tc-1",
116
+ messageId: "m-result",
117
+ role: "tool",
118
+ content: [
119
+ { type: "text", text: "valid" },
120
+ { type: "image", data: "binary" },
121
+ null,
122
+ { type: "text", text: " part" },
123
+ ],
124
+ },
125
+ { type: "RUN_FINISHED", threadId: "t-1", runId: "r-1" },
126
+ ]);
127
+ const result = await parseSSEResponse(response);
128
+ expect(result.messages).toContainEqual({
129
+ id: "m-result",
130
+ role: "tool",
131
+ content: "valid part",
132
+ toolCallId: "tc-1",
133
+ });
134
+ });
135
+
86
136
  it("uses MESSAGES_SNAPSHOT when present", async () => {
87
137
  const snapshotMessages = [
88
138
  { id: "u-1", role: "user", content: "hi" },
@@ -167,14 +167,24 @@ export async function parseSSEResponse(
167
167
  break;
168
168
  }
169
169
 
170
- case "TOOL_CALL_RESULT":
170
+ case "TOOL_CALL_RESULT": {
171
+ // langchain-mcp-adapters may send content as an array of
172
+ // {type:"text", text:string} objects instead of a plain string.
173
+ let resultContent = event.content;
174
+ if (Array.isArray(resultContent)) {
175
+ resultContent = resultContent
176
+ .filter((part: any) => part && typeof part.text === "string")
177
+ .map((part: any) => part.text)
178
+ .join("");
179
+ }
171
180
  messagesById.set(event.messageId, {
172
181
  id: event.messageId,
173
182
  role: "tool",
174
- content: event.content,
183
+ content: resultContent,
175
184
  toolCallId: event.toolCallId,
176
185
  });
177
186
  break;
187
+ }
178
188
  }
179
189
  }
180
190
 
@@ -9,6 +9,11 @@ import {
9
9
  createLicenseChecker,
10
10
  type LicenseChecker,
11
11
  } from "@copilotkit/license-verifier";
12
+ import {
13
+ type ResolvedDebugConfig,
14
+ resolveDebugConfig,
15
+ type DebugConfig,
16
+ } from "@copilotkit/shared";
12
17
  import { AbstractAgent } from "@ag-ui/client";
13
18
  import type { MCPClientConfig } from "@ag-ui/mcp-apps-middleware";
14
19
  import { A2UIMiddlewareConfig } from "@ag-ui/a2ui-middleware";
@@ -17,6 +22,7 @@ import type {
17
22
  BeforeRequestMiddleware,
18
23
  AfterRequestMiddleware,
19
24
  } from "./middleware";
25
+ import { createLogger, type CopilotRuntimeLogger } from "../../../lib/logger";
20
26
  import { TranscriptionService } from "../transcription-service/transcription-service";
21
27
  import { AgentRunner } from "../runner/agent-runner";
22
28
  import { InMemoryAgentRunner } from "../runner/in-memory";
@@ -56,9 +62,70 @@ interface CopilotRuntimeMiddlewares {
56
62
  openGenerativeUI?: OpenGenerativeUIConfig;
57
63
  }
58
64
 
65
+ /**
66
+ * Context passed to agent factory functions for per-request agent resolution.
67
+ */
68
+ export interface AgentFactoryContext {
69
+ /** The incoming HTTP request. */
70
+ request: Request;
71
+ }
72
+
73
+ /**
74
+ * A function that dynamically creates agents on a per-request basis.
75
+ * Useful for multi-tenant scenarios or request-scoped agent configuration.
76
+ */
77
+ export type AgentsFactory = (
78
+ ctx: AgentFactoryContext,
79
+ ) => MaybePromise<NonEmptyRecord<Record<string, AbstractAgent>>>;
80
+
81
+ /**
82
+ * Agents can be provided as:
83
+ * - A static record of agents
84
+ * - A Promise that resolves to a record of agents
85
+ * - A factory function that receives request context and returns agents (or a Promise of agents)
86
+ */
87
+ export type AgentsConfig =
88
+ | MaybePromise<NonEmptyRecord<Record<string, AbstractAgent>>>
89
+ | AgentsFactory;
90
+
91
+ /**
92
+ * Resolve an AgentsConfig value to a concrete record of agents.
93
+ * If the config is a factory function, it is called with the given request context.
94
+ * Otherwise it is awaited directly (static record or Promise).
95
+ */
96
+ export async function resolveAgents(
97
+ agents: AgentsConfig,
98
+ request?: Request,
99
+ ): Promise<Record<string, AbstractAgent>> {
100
+ if (typeof agents === "function") {
101
+ if (!request) {
102
+ throw new Error(
103
+ "Agent factory function requires a request context, but none was provided.",
104
+ );
105
+ }
106
+ return agents({ request });
107
+ }
108
+ return agents;
109
+ }
110
+
59
111
  interface BaseCopilotRuntimeOptions extends CopilotRuntimeMiddlewares {
60
- /** Map of available agents (loaded lazily is fine). */
61
- agents: MaybePromise<NonEmptyRecord<Record<string, AbstractAgent>>>;
112
+ /**
113
+ * Map of available agents, or a factory function for per-request agent resolution.
114
+ *
115
+ * Static record:
116
+ * ```ts
117
+ * agents: { support: new SupportAgent(), technical: new TechnicalAgent() }
118
+ * ```
119
+ *
120
+ * Factory function (called per-request):
121
+ * ```ts
122
+ * agents: ({ request }) => {
123
+ * const tenantId = request.headers.get("x-tenant-id");
124
+ * return { default: createAgentForTenant(tenantId) };
125
+ * }
126
+ * ```
127
+ */
128
+ agents: AgentsConfig;
62
129
  /** Optional transcription service for audio processing. */
63
130
  transcriptionService?: TranscriptionService;
64
131
  /** Optional *before* middleware – callback function or webhook URL. */
@@ -67,6 +134,8 @@ interface BaseCopilotRuntimeOptions extends CopilotRuntimeMiddlewares {
67
134
  afterRequestMiddleware?: AfterRequestMiddleware;
68
135
  /** Signed license token for server-side feature verification. Falls back to COPILOTKIT_LICENSE_TOKEN env var. */
69
136
  licenseToken?: string;
137
+ /** Enable debug logging for the event pipeline. */
138
+ debug?: DebugConfig;
70
139
  }
71
140
 
72
141
  export interface CopilotRuntimeUser {
@@ -120,6 +189,8 @@ export interface CopilotRuntimeLike {
120
189
  identifyUser?: IdentifyUserCallback;
121
190
  mode: RuntimeMode;
122
191
  licenseChecker?: LicenseChecker;
192
+ debug: ResolvedDebugConfig;
193
+ debugLogger?: CopilotRuntimeLogger;
123
194
  }
124
195
 
125
196
  export interface CopilotSseRuntimeLike extends CopilotRuntimeLike {
@@ -147,6 +218,8 @@ abstract class BaseCopilotRuntime implements CopilotRuntimeLike {
147
218
  public mcpApps: CopilotRuntimeOptions["mcpApps"];
148
219
  public openGenerativeUI: CopilotRuntimeOptions["openGenerativeUI"];
149
220
  public licenseChecker?: LicenseChecker;
221
+ public debug: ResolvedDebugConfig;
222
+ public debugLogger?: CopilotRuntimeLogger;
150
223
 
151
224
  abstract readonly intelligence?: CopilotKitIntelligence;
152
225
  abstract readonly mode: RuntimeMode;
@@ -170,6 +243,13 @@ abstract class BaseCopilotRuntime implements CopilotRuntimeLike {
170
243
  this.mcpApps = mcpApps;
171
244
  this.openGenerativeUI = openGenerativeUI;
172
245
  this.runner = runner;
246
+ this.debug = resolveDebugConfig(options.debug);
247
+ if (this.debug.enabled) {
248
+ this.debugLogger = createLogger({
249
+ level: "debug",
250
+ component: "copilotkit-debug",
251
+ });
252
+ }
173
253
  }
174
254
  }
175
255
 
@@ -326,4 +406,12 @@ export class CopilotRuntime implements CopilotRuntimeLike {
326
406
  get licenseChecker() {
327
407
  return this.delegate.licenseChecker;
328
408
  }
409
+
410
+ get debug(): ResolvedDebugConfig {
411
+ return this.delegate.debug;
412
+ }
413
+
414
+ get debugLogger(): CopilotRuntimeLogger | undefined {
415
+ return this.delegate.debugLogger;
416
+ }
329
417
  }