@copilotkit/runtime 1.55.1 → 1.55.2-next.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 (74) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/agent/converters/aisdk.cjs +215 -0
  3. package/dist/agent/converters/aisdk.cjs.map +1 -0
  4. package/dist/agent/converters/aisdk.d.cts +18 -0
  5. package/dist/agent/converters/aisdk.d.cts.map +1 -0
  6. package/dist/agent/converters/aisdk.d.mts +18 -0
  7. package/dist/agent/converters/aisdk.d.mts.map +1 -0
  8. package/dist/agent/converters/aisdk.mjs +214 -0
  9. package/dist/agent/converters/aisdk.mjs.map +1 -0
  10. package/dist/agent/converters/index.d.mts +3 -0
  11. package/dist/agent/converters/tanstack.cjs +180 -0
  12. package/dist/agent/converters/tanstack.cjs.map +1 -0
  13. package/dist/agent/converters/tanstack.d.cts +68 -0
  14. package/dist/agent/converters/tanstack.d.cts.map +1 -0
  15. package/dist/agent/converters/tanstack.d.mts +68 -0
  16. package/dist/agent/converters/tanstack.d.mts.map +1 -0
  17. package/dist/agent/converters/tanstack.mjs +178 -0
  18. package/dist/agent/converters/tanstack.mjs.map +1 -0
  19. package/dist/agent/index.cjs +111 -17
  20. package/dist/agent/index.cjs.map +1 -1
  21. package/dist/agent/index.d.cts +61 -4
  22. package/dist/agent/index.d.cts.map +1 -1
  23. package/dist/agent/index.d.mts +62 -4
  24. package/dist/agent/index.d.mts.map +1 -1
  25. package/dist/agent/index.mjs +111 -17
  26. package/dist/agent/index.mjs.map +1 -1
  27. package/dist/lib/integrations/nextjs/pages-router.cjs.map +1 -1
  28. package/dist/lib/integrations/nextjs/pages-router.d.cts.map +1 -1
  29. package/dist/lib/integrations/nextjs/pages-router.d.mts.map +1 -1
  30. package/dist/lib/integrations/nextjs/pages-router.mjs.map +1 -1
  31. package/dist/lib/runtime/copilot-runtime.cjs +4 -2
  32. package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
  33. package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
  34. package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
  35. package/dist/lib/runtime/copilot-runtime.mjs +4 -2
  36. package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
  37. package/dist/lib/runtime/mcp-tools-utils.cjs +1 -1
  38. package/dist/lib/runtime/mcp-tools-utils.cjs.map +1 -1
  39. package/dist/lib/runtime/mcp-tools-utils.mjs +1 -1
  40. package/dist/lib/runtime/mcp-tools-utils.mjs.map +1 -1
  41. package/dist/package.cjs +3 -2
  42. package/dist/package.mjs +3 -2
  43. package/dist/service-adapters/anthropic/utils.cjs +1 -1
  44. package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
  45. package/dist/service-adapters/anthropic/utils.mjs +1 -1
  46. package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
  47. package/dist/service-adapters/openai/utils.cjs +1 -1
  48. package/dist/service-adapters/openai/utils.cjs.map +1 -1
  49. package/dist/service-adapters/openai/utils.mjs +1 -1
  50. package/dist/service-adapters/openai/utils.mjs.map +1 -1
  51. package/dist/v2/index.cjs +5 -0
  52. package/dist/v2/index.d.cts +4 -2
  53. package/dist/v2/index.d.mts +4 -2
  54. package/dist/v2/index.mjs +3 -1
  55. package/package.json +4 -3
  56. package/src/agent/__tests__/agent-test-helpers.ts +446 -0
  57. package/src/agent/__tests__/agent.test.ts +593 -0
  58. package/src/agent/__tests__/converter-aisdk.test.ts +692 -0
  59. package/src/agent/__tests__/converter-custom.test.ts +319 -0
  60. package/src/agent/__tests__/converter-tanstack-input.test.ts +211 -0
  61. package/src/agent/__tests__/converter-tanstack.test.ts +314 -0
  62. package/src/agent/__tests__/mcp-servers-integration.test.ts +373 -0
  63. package/src/agent/__tests__/multimodal-tanstack.test.ts +284 -0
  64. package/src/agent/__tests__/test-helpers.ts +12 -8
  65. package/src/agent/converters/aisdk.ts +326 -0
  66. package/src/agent/converters/index.ts +7 -0
  67. package/src/agent/converters/tanstack.ts +286 -0
  68. package/src/agent/index.ts +245 -26
  69. package/src/lib/integrations/nextjs/pages-router.ts +1 -0
  70. package/src/lib/runtime/copilot-runtime.ts +21 -12
  71. package/src/lib/runtime/mcp-tools-utils.ts +1 -1
  72. package/src/service-adapters/anthropic/utils.ts +1 -1
  73. package/src/service-adapters/openai/utils.ts +1 -1
  74. package/src/v2/runtime/__tests__/mcp-apps-middleware-integration.test.ts +275 -0
@@ -64,7 +64,7 @@ import {
64
64
  type MCPTool,
65
65
  extractParametersFromSchema,
66
66
  } from "./mcp-tools-utils";
67
- import { BuiltInAgent, type BuiltInAgentConfiguration } from "../../agent";
67
+ import { BuiltInAgent, type BuiltInAgentClassicConfig } from "../../agent";
68
68
  // Define the function type alias here or import if defined elsewhere
69
69
  type CreateMCPClientFunction = (
70
70
  config: MCPEndpointConfig,
@@ -328,7 +328,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
328
328
  params?: CopilotRuntimeConstructorParams<T>;
329
329
  private observability?: CopilotObservabilityConfig;
330
330
  // Cache MCP tools per endpoint to avoid re-fetching repeatedly
331
- private mcpToolsCache: Map<string, BuiltInAgentConfiguration["tools"]> =
331
+ private mcpToolsCache: Map<string, BuiltInAgentClassicConfig["tools"]> =
332
332
  new Map();
333
333
  private runtimeArgs: CopilotRuntimeOptions;
334
334
  private _instance: CopilotRuntimeVNext;
@@ -449,7 +449,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
449
449
  // Receive this.params.action and turn it into the AbstractAgent tools
450
450
  private getToolsFromActions(
451
451
  actions: ActionsConfiguration<any>,
452
- ): BuiltInAgentConfiguration["tools"] {
452
+ ): BuiltInAgentClassicConfig["tools"] {
453
453
  // Resolve actions to an array (handle function case)
454
454
  const actionsArray =
455
455
  typeof actions === "function"
@@ -472,7 +472,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
472
472
 
473
473
  private assignToolsToAgents(
474
474
  agents: Record<string, AbstractAgent>,
475
- tools: BuiltInAgentConfiguration["tools"],
475
+ tools: BuiltInAgentClassicConfig["tools"],
476
476
  ): Record<string, AbstractAgent> {
477
477
  if (!tools?.length) {
478
478
  return agents;
@@ -481,12 +481,21 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
481
481
  const enrichedAgents: Record<string, AbstractAgent> = { ...agents };
482
482
 
483
483
  for (const [agentId, agent] of Object.entries(enrichedAgents)) {
484
- const existingConfig = (Reflect.get(agent, "config") ??
485
- {}) as BuiltInAgentConfiguration;
486
- const existingTools = existingConfig.tools ?? [];
484
+ const existingConfig = (Reflect.get(agent, "config") ?? {}) as Record<
485
+ string,
486
+ unknown
487
+ >;
487
488
 
488
- const updatedConfig: BuiltInAgentConfiguration = {
489
- ...existingConfig,
489
+ // Skip factory-mode agents — they don't have a tools property
490
+ if ("factory" in existingConfig) {
491
+ continue;
492
+ }
493
+
494
+ const classicConfig = existingConfig as BuiltInAgentClassicConfig;
495
+ const existingTools = classicConfig.tools ?? [];
496
+
497
+ const updatedConfig: BuiltInAgentClassicConfig = {
498
+ ...classicConfig,
490
499
  tools: [...existingTools, ...tools],
491
500
  };
492
501
 
@@ -657,7 +666,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
657
666
  // Optionally accepts request-scoped properties to merge request-provided mcpServers
658
667
  private async getToolsFromMCP(options?: {
659
668
  properties?: Record<string, unknown>;
660
- }): Promise<BuiltInAgentConfiguration["tools"]> {
669
+ }): Promise<BuiltInAgentClassicConfig["tools"]> {
661
670
  const runtimeMcpServers = (this.params?.mcpServers ??
662
671
  []) as MCPEndpointConfig[];
663
672
  const createMCPClient = this.params?.createMCPClient as
@@ -702,7 +711,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
702
711
  return Array.from(byUrl.values());
703
712
  })();
704
713
 
705
- const allTools: BuiltInAgentConfiguration["tools"] = [];
714
+ const allTools: BuiltInAgentClassicConfig["tools"] = [];
706
715
 
707
716
  for (const config of effectiveEndpoints) {
708
717
  const endpointUrl = config.endpoint;
@@ -717,7 +726,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
717
726
  const client = await createMCPClient(config);
718
727
  const toolsMap = await client.tools();
719
728
 
720
- const toolDefs: BuiltInAgentConfiguration["tools"] = Object.entries(
729
+ const toolDefs: BuiltInAgentClassicConfig["tools"] = Object.entries(
721
730
  toolsMap,
722
731
  ).map(([toolName, tool]: [string, MCPTool]) => {
723
732
  const params: Parameter[] = extractParametersFromSchema(tool);
@@ -145,7 +145,7 @@ export function convertMCPToolsToActions(
145
145
  throw new Error(
146
146
  `Execution failed for MCP tool '${toolName}': ${
147
147
  error instanceof Error ? error.message : String(error)
148
- }`,
148
+ }`, { cause: error },
149
149
  );
150
150
  }
151
151
  };
@@ -32,7 +32,7 @@ export function limitMessagesToTokenCount(
32
32
 
33
33
  let cutoff: boolean = false;
34
34
 
35
- const reversedMessages = [...messages].reverse();
35
+ const reversedMessages = [...messages].toReversed();
36
36
  for (const message of reversedMessages) {
37
37
  if (message.role === "system") {
38
38
  result.unshift(message);
@@ -40,7 +40,7 @@ export function limitMessagesToTokenCount(
40
40
 
41
41
  let cutoff: boolean = false;
42
42
 
43
- const reversedMessages = [...messages].reverse();
43
+ const reversedMessages = [...messages].toReversed();
44
44
  for (const message of reversedMessages) {
45
45
  if (["system", "developer"].includes(message.role)) {
46
46
  result.unshift(message);
@@ -0,0 +1,275 @@
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
+ import {
3
+ AbstractAgent,
4
+ RunAgentInput,
5
+ BaseEvent,
6
+ EventType,
7
+ } from "@ag-ui/client";
8
+ import { Observable } from "rxjs";
9
+ import { LLMock, MCPMock } from "@copilotkit/aimock";
10
+ import { MCPAppsMiddleware, getServerHash } from "@ag-ui/mcp-apps-middleware";
11
+
12
+ /**
13
+ * A minimal next-agent that emits RUN_STARTED and RUN_FINISHED.
14
+ * Used as the downstream agent when the middleware should NOT delegate.
15
+ */
16
+ class MockNextAgent extends AbstractAgent {
17
+ run(input: RunAgentInput): Observable<BaseEvent> {
18
+ return new Observable((subscriber) => {
19
+ subscriber.next({
20
+ type: EventType.RUN_STARTED,
21
+ threadId: input.threadId,
22
+ runId: input.runId,
23
+ } as BaseEvent);
24
+ subscriber.next({
25
+ type: EventType.RUN_FINISHED,
26
+ threadId: input.threadId,
27
+ runId: input.runId,
28
+ } as BaseEvent);
29
+ subscriber.complete();
30
+ });
31
+ }
32
+
33
+ clone(): AbstractAgent {
34
+ return new MockNextAgent();
35
+ }
36
+
37
+ protected connect(): ReturnType<AbstractAgent["connect"]> {
38
+ throw new Error("not used");
39
+ }
40
+ }
41
+
42
+ function createRunInput(overrides: Partial<RunAgentInput> = {}): RunAgentInput {
43
+ return {
44
+ threadId: "thread-1",
45
+ runId: "run-1",
46
+ state: {},
47
+ messages: [],
48
+ tools: [],
49
+ context: [],
50
+ forwardedProps: undefined,
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ async function collectEvents(
56
+ observable: Observable<BaseEvent>,
57
+ ): Promise<BaseEvent[]> {
58
+ const events: BaseEvent[] = [];
59
+ await new Promise<void>((resolve, reject) => {
60
+ observable.subscribe({
61
+ next: (event) => events.push(event),
62
+ error: reject,
63
+ complete: resolve,
64
+ });
65
+ });
66
+ return events;
67
+ }
68
+
69
+ describe("MCPAppsMiddleware integration", () => {
70
+ let llm: LLMock;
71
+ let mcpMock: MCPMock;
72
+
73
+ afterEach(async () => {
74
+ if (llm) {
75
+ await llm.stop().catch(() => {});
76
+ }
77
+ });
78
+
79
+ async function startMcpServer(): Promise<string> {
80
+ mcpMock = new MCPMock();
81
+ mcpMock.addTool({
82
+ name: "get_weather",
83
+ description: "Get the weather",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: { city: { type: "string" } },
87
+ },
88
+ });
89
+ mcpMock.onToolCall("get_weather", (args: unknown) => {
90
+ const parsed = args as { city?: string };
91
+ return `Weather in ${parsed.city || "unknown"}: sunny`;
92
+ });
93
+ mcpMock.addResource(
94
+ {
95
+ uri: "app://dashboard",
96
+ name: "Dashboard",
97
+ mimeType: "text/plain",
98
+ },
99
+ { text: "Dashboard content here" },
100
+ );
101
+
102
+ llm = new LLMock({ port: 0 });
103
+ llm.mount("/mcp", mcpMock);
104
+ await llm.start();
105
+ return `${llm.url}/mcp`;
106
+ }
107
+
108
+ it("can be created with mcpServers config pointing at MCPMock URL", async () => {
109
+ const mcpUrl = await startMcpServer();
110
+
111
+ const middleware = new MCPAppsMiddleware({
112
+ mcpServers: [{ type: "http", url: mcpUrl }],
113
+ });
114
+
115
+ expect(middleware).toBeInstanceOf(MCPAppsMiddleware);
116
+ });
117
+
118
+ it("proxies tools/call through to MCPMock and returns results", async () => {
119
+ const mcpUrl = await startMcpServer();
120
+
121
+ const serverConfig = { type: "http" as const, url: mcpUrl };
122
+ const serverHash = getServerHash(serverConfig);
123
+
124
+ const middleware = new MCPAppsMiddleware({
125
+ mcpServers: [serverConfig],
126
+ });
127
+
128
+ const input = createRunInput({
129
+ forwardedProps: {
130
+ __proxiedMCPRequest: {
131
+ serverHash,
132
+ method: "tools/call",
133
+ params: {
134
+ name: "get_weather",
135
+ arguments: { city: "NYC" },
136
+ },
137
+ },
138
+ },
139
+ });
140
+
141
+ const mockAgent = new MockNextAgent();
142
+ const events = await collectEvents(middleware.run(input, mockAgent));
143
+
144
+ // Should have RUN_STARTED and RUN_FINISHED
145
+ const types = events.map((e) => e.type);
146
+ expect(types).toContain(EventType.RUN_STARTED);
147
+ expect(types).toContain(EventType.RUN_FINISHED);
148
+
149
+ // RUN_FINISHED should contain the MCP tool result
150
+ const runFinished = events.find(
151
+ (e) => e.type === EventType.RUN_FINISHED,
152
+ ) as BaseEvent & { result?: unknown };
153
+ expect(runFinished).toBeDefined();
154
+ expect(runFinished.result).toBeDefined();
155
+
156
+ // The result should contain the tool's text content
157
+ const result = runFinished.result as { content?: unknown[] };
158
+ expect(result.content).toBeDefined();
159
+ expect(Array.isArray(result.content)).toBe(true);
160
+
161
+ const textContent = (
162
+ result.content as Array<{ type: string; text?: string }>
163
+ ).find((c) => c.type === "text");
164
+ expect(textContent).toBeDefined();
165
+ expect(textContent!.text).toContain("sunny");
166
+ });
167
+
168
+ it("non-proxied request delegates to next agent", async () => {
169
+ const mcpUrl = await startMcpServer();
170
+
171
+ const middleware = new MCPAppsMiddleware({
172
+ mcpServers: [{ type: "http", url: mcpUrl }],
173
+ });
174
+
175
+ // Input WITHOUT __proxiedMCPRequest — should delegate to MockNextAgent
176
+ const input = createRunInput();
177
+
178
+ const mockAgent = new MockNextAgent();
179
+
180
+ const events = await collectEvents(middleware.run(input, mockAgent));
181
+
182
+ // MockNextAgent's run should have been called (delegation happened)
183
+ // The middleware calls runNextWithState which internally calls next.run,
184
+ // but since processStream wraps it, we check the output events instead
185
+ const types = events.map((e) => e.type);
186
+ expect(types).toContain(EventType.RUN_STARTED);
187
+ expect(types).toContain(EventType.RUN_FINISHED);
188
+ });
189
+
190
+ it("wrong serverHash returns error in RUN_FINISHED result", async () => {
191
+ const mcpUrl = await startMcpServer();
192
+
193
+ const middleware = new MCPAppsMiddleware({
194
+ mcpServers: [{ type: "http", url: mcpUrl }],
195
+ });
196
+
197
+ const input = createRunInput({
198
+ forwardedProps: {
199
+ __proxiedMCPRequest: {
200
+ serverHash: "nonexistent-hash-value",
201
+ method: "tools/call",
202
+ params: {
203
+ name: "get_weather",
204
+ arguments: { city: "NYC" },
205
+ },
206
+ },
207
+ },
208
+ });
209
+
210
+ const mockAgent = new MockNextAgent();
211
+ const events = await collectEvents(middleware.run(input, mockAgent));
212
+
213
+ // Should still get RUN_STARTED and RUN_FINISHED
214
+ const types = events.map((e) => e.type);
215
+ expect(types).toContain(EventType.RUN_STARTED);
216
+ expect(types).toContain(EventType.RUN_FINISHED);
217
+
218
+ // RUN_FINISHED should contain an error about unknown server
219
+ const runFinished = events.find(
220
+ (e) => e.type === EventType.RUN_FINISHED,
221
+ ) as BaseEvent & { result?: unknown };
222
+ expect(runFinished).toBeDefined();
223
+ const result = runFinished.result as { error?: string };
224
+ expect(result.error).toBeDefined();
225
+ expect(result.error).toContain("nonexistent-hash-value");
226
+ });
227
+
228
+ it("proxies resources/read through to MCPMock and returns results", async () => {
229
+ const mcpUrl = await startMcpServer();
230
+
231
+ const serverConfig = { type: "http" as const, url: mcpUrl };
232
+ const serverHash = getServerHash(serverConfig);
233
+
234
+ const middleware = new MCPAppsMiddleware({
235
+ mcpServers: [serverConfig],
236
+ });
237
+
238
+ const input = createRunInput({
239
+ forwardedProps: {
240
+ __proxiedMCPRequest: {
241
+ serverHash,
242
+ method: "resources/read",
243
+ params: { uri: "app://dashboard" },
244
+ },
245
+ },
246
+ });
247
+
248
+ const mockAgent = new MockNextAgent();
249
+ const events = await collectEvents(middleware.run(input, mockAgent));
250
+
251
+ // Should have RUN_STARTED and RUN_FINISHED
252
+ const types = events.map((e) => e.type);
253
+ expect(types).toContain(EventType.RUN_STARTED);
254
+ expect(types).toContain(EventType.RUN_FINISHED);
255
+
256
+ // RUN_FINISHED should contain the resource content
257
+ const runFinished = events.find(
258
+ (e) => e.type === EventType.RUN_FINISHED,
259
+ ) as BaseEvent & { result?: unknown };
260
+ expect(runFinished).toBeDefined();
261
+ expect(runFinished.result).toBeDefined();
262
+
263
+ // The result should contain resource contents
264
+ const result = runFinished.result as { contents?: unknown[] };
265
+ expect(result.contents).toBeDefined();
266
+ expect(Array.isArray(result.contents)).toBe(true);
267
+
268
+ const resource = (
269
+ result.contents as Array<{ uri: string; text?: string }>
270
+ )[0];
271
+ expect(resource).toBeDefined();
272
+ expect(resource.uri).toBe("app://dashboard");
273
+ expect(resource.text).toContain("Dashboard content here");
274
+ });
275
+ });