@copilotkit/runtime 1.56.5 → 1.57.0-canary.1778082736

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 (84) hide show
  1. package/dist/agent/index.cjs +20 -2
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.d.cts +9 -16
  4. package/dist/agent/index.d.cts.map +1 -1
  5. package/dist/agent/index.d.mts +9 -16
  6. package/dist/agent/index.d.mts.map +1 -1
  7. package/dist/agent/index.mjs +21 -3
  8. package/dist/agent/index.mjs.map +1 -1
  9. package/dist/package.cjs +1 -1
  10. package/dist/package.mjs +1 -1
  11. package/dist/v2/index.d.cts +3 -3
  12. package/dist/v2/index.d.mts +3 -3
  13. package/dist/v2/runtime/core/fetch-handler.cjs +16 -0
  14. package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
  15. package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
  16. package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
  17. package/dist/v2/runtime/core/fetch-handler.mjs +17 -1
  18. package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
  19. package/dist/v2/runtime/core/fetch-router.cjs +18 -1
  20. package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
  21. package/dist/v2/runtime/core/fetch-router.mjs +18 -1
  22. package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
  23. package/dist/v2/runtime/core/hooks.cjs.map +1 -1
  24. package/dist/v2/runtime/core/hooks.d.cts +8 -0
  25. package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
  26. package/dist/v2/runtime/core/hooks.d.mts +8 -0
  27. package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
  28. package/dist/v2/runtime/core/hooks.mjs.map +1 -1
  29. package/dist/v2/runtime/handlers/handle-run.cjs +1 -0
  30. package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
  31. package/dist/v2/runtime/handlers/handle-run.mjs +1 -0
  32. package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
  33. package/dist/v2/runtime/handlers/intelligence/run.cjs +10 -1
  34. package/dist/v2/runtime/handlers/intelligence/run.cjs.map +1 -1
  35. package/dist/v2/runtime/handlers/intelligence/run.mjs +10 -1
  36. package/dist/v2/runtime/handlers/intelligence/run.mjs.map +1 -1
  37. package/dist/v2/runtime/handlers/intelligence/threads.cjs +124 -12
  38. package/dist/v2/runtime/handlers/intelligence/threads.cjs.map +1 -1
  39. package/dist/v2/runtime/handlers/intelligence/threads.mjs +122 -13
  40. package/dist/v2/runtime/handlers/intelligence/threads.mjs.map +1 -1
  41. package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
  42. package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
  43. package/dist/v2/runtime/index.d.cts +3 -2
  44. package/dist/v2/runtime/index.d.cts.map +1 -1
  45. package/dist/v2/runtime/index.d.mts +3 -2
  46. package/dist/v2/runtime/index.d.mts.map +1 -1
  47. package/dist/v2/runtime/intelligence-platform/client.cjs +40 -0
  48. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  49. package/dist/v2/runtime/intelligence-platform/client.d.cts +83 -0
  50. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  51. package/dist/v2/runtime/intelligence-platform/client.d.mts +83 -0
  52. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  53. package/dist/v2/runtime/intelligence-platform/client.mjs +40 -0
  54. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  55. package/dist/v2/runtime/runner/in-memory.cjs +94 -22
  56. package/dist/v2/runtime/runner/in-memory.cjs.map +1 -1
  57. package/dist/v2/runtime/runner/in-memory.d.cts +65 -2
  58. package/dist/v2/runtime/runner/in-memory.d.cts.map +1 -1
  59. package/dist/v2/runtime/runner/in-memory.d.mts +65 -2
  60. package/dist/v2/runtime/runner/in-memory.d.mts.map +1 -1
  61. package/dist/v2/runtime/runner/in-memory.mjs +94 -22
  62. package/dist/v2/runtime/runner/in-memory.mjs.map +1 -1
  63. package/dist/v2/runtime/runner/index.d.cts +1 -1
  64. package/dist/v2/runtime/runner/index.d.mts +1 -1
  65. package/package.json +2 -2
  66. package/src/agent/__tests__/mcp-clients.test.ts +11 -25
  67. package/src/agent/index.ts +67 -32
  68. package/src/v2/runtime/__tests__/fetch-handler-validation.test.ts +68 -0
  69. package/src/v2/runtime/__tests__/fetch-router.test.ts +46 -0
  70. package/src/v2/runtime/__tests__/handle-run.test.ts +97 -1
  71. package/src/v2/runtime/__tests__/handle-threads.test.ts +493 -13
  72. package/src/v2/runtime/core/fetch-handler.ts +19 -0
  73. package/src/v2/runtime/core/fetch-router.ts +33 -1
  74. package/src/v2/runtime/core/hooks.ts +3 -0
  75. package/src/v2/runtime/handlers/handle-run.ts +4 -0
  76. package/src/v2/runtime/handlers/handle-threads.ts +3 -0
  77. package/src/v2/runtime/handlers/intelligence/run.ts +27 -5
  78. package/src/v2/runtime/handlers/intelligence/threads.ts +200 -41
  79. package/src/v2/runtime/handlers/shared/agent-utils.ts +4 -6
  80. package/src/v2/runtime/index.ts +5 -0
  81. package/src/v2/runtime/intelligence-platform/__tests__/intelligence-mcp-helper.test.ts +239 -0
  82. package/src/v2/runtime/intelligence-platform/client.ts +113 -0
  83. package/src/v2/runtime/runner/__tests__/in-memory-runner.test.ts +417 -3
  84. package/src/v2/runtime/runner/in-memory.ts +137 -51
@@ -0,0 +1,239 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { BasicAgent } from "../../../../agent";
3
+ import { LLMock, MCPMock } from "@copilotkit/aimock";
4
+ import { streamText } from "ai";
5
+ import {
6
+ mockStreamTextResponse,
7
+ textDelta,
8
+ finish,
9
+ collectEvents,
10
+ } from "../../../../agent/__tests__/test-helpers";
11
+
12
+ vi.mock("ai", () => ({
13
+ streamText: vi.fn(),
14
+ tool: vi.fn((config) => config),
15
+ stepCountIs: vi.fn((count: number) => ({ type: "stepCount", count })),
16
+ }));
17
+
18
+ vi.mock("@ai-sdk/openai", () => ({
19
+ createOpenAI: vi.fn(() => (modelId: string) => ({
20
+ modelId,
21
+ provider: "openai",
22
+ })),
23
+ }));
24
+
25
+ async function startMcpMock(): Promise<{ url: string; server: LLMock }> {
26
+ const mock = new MCPMock();
27
+ mock.addTool({
28
+ name: "bash",
29
+ description: "Run a bash command",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: { command: { type: "string" } },
33
+ },
34
+ });
35
+ mock.onToolCall("bash", () => "ok");
36
+ const server = new LLMock({ port: 0 });
37
+ server.mount("/mcp", mock);
38
+ await server.start();
39
+ return { url: server.url, server };
40
+ }
41
+
42
+ /**
43
+ * aimock redacts `Authorization` to `[REDACTED]` in its journal. Spy on
44
+ * `globalThis.fetch` to read unredacted headers off each outbound request to
45
+ * `mcpUrl`. The spy delegates to the real fetch so the round-trip completes.
46
+ */
47
+ function spyOnFetch(mcpUrl: string): {
48
+ records: Array<Record<string, string>>;
49
+ restore: () => void;
50
+ } {
51
+ const records: Array<Record<string, string>> = [];
52
+ const realFetch = globalThis.fetch;
53
+ const spy = vi
54
+ .spyOn(globalThis, "fetch")
55
+ .mockImplementation(async (input, init) => {
56
+ const url =
57
+ typeof input === "string"
58
+ ? input
59
+ : input instanceof URL
60
+ ? input.toString()
61
+ : input.url;
62
+ if (url.startsWith(mcpUrl)) {
63
+ const seen: Record<string, string> = {};
64
+ new Headers(init?.headers ?? {}).forEach((value, key) => {
65
+ seen[key.toLowerCase()] = value;
66
+ });
67
+ records.push(seen);
68
+ }
69
+ return realFetch(input, init);
70
+ });
71
+ return { records, restore: () => spy.mockRestore() };
72
+ }
73
+
74
+ const baseInput = {
75
+ threadId: "thread1",
76
+ runId: "run1",
77
+ messages: [],
78
+ tools: [],
79
+ context: [],
80
+ state: {},
81
+ };
82
+
83
+ describe("BuiltInAgent — Intelligence MCP auto-attach via forwardedProps", () => {
84
+ let llm: LLMock | undefined;
85
+ const originalEnv = process.env;
86
+
87
+ beforeEach(() => {
88
+ vi.clearAllMocks();
89
+ process.env = { ...originalEnv };
90
+ process.env.OPENAI_API_KEY = "test-key";
91
+ });
92
+
93
+ afterEach(async () => {
94
+ process.env = originalEnv;
95
+ if (llm) {
96
+ await llm.stop().catch(() => {});
97
+ llm = undefined;
98
+ }
99
+ });
100
+
101
+ it("attaches the Intelligence MCP server when forwardedProps carries userId + apiKey + mcpUrl", async () => {
102
+ const { url, server } = await startMcpMock();
103
+ llm = server;
104
+
105
+ const recorder = spyOnFetch(`${url}/mcp`);
106
+ try {
107
+ const agent = new BasicAgent({ model: "openai/gpt-4o" });
108
+
109
+ vi.mocked(streamText).mockReturnValue(
110
+ mockStreamTextResponse([textDelta("hi"), finish()]) as any,
111
+ );
112
+
113
+ await collectEvents(
114
+ agent["run"]({
115
+ ...baseInput,
116
+ forwardedProps: {
117
+ copilotkitIntelligence: {
118
+ userId: "jordan-beamson",
119
+ apiKey: "cpk-proj_short_long",
120
+ mcpUrl: `${url}/mcp`,
121
+ },
122
+ },
123
+ }),
124
+ );
125
+
126
+ expect(recorder.records.length).toBeGreaterThan(0);
127
+ for (const headers of recorder.records) {
128
+ expect(headers["authorization"]).toBe("Bearer cpk-proj_short_long");
129
+ expect(headers["x-cpki-user-id"]).toBe("jordan-beamson");
130
+ }
131
+ } finally {
132
+ recorder.restore();
133
+ }
134
+ });
135
+
136
+ it("does NOT attach when forwardedProps is empty (no Intelligence wiring this run)", async () => {
137
+ const { url, server } = await startMcpMock();
138
+ llm = server;
139
+
140
+ const recorder = spyOnFetch(`${url}/mcp`);
141
+ try {
142
+ const agent = new BasicAgent({ model: "openai/gpt-4o" });
143
+
144
+ vi.mocked(streamText).mockReturnValue(
145
+ mockStreamTextResponse([finish()]) as any,
146
+ );
147
+ await collectEvents(agent["run"](baseInput));
148
+
149
+ expect(recorder.records.length).toBe(0);
150
+ } finally {
151
+ recorder.restore();
152
+ }
153
+ });
154
+
155
+ it("does NOT attach when only some of the three props are present", async () => {
156
+ const { url, server } = await startMcpMock();
157
+ llm = server;
158
+
159
+ const recorder = spyOnFetch(`${url}/mcp`);
160
+ try {
161
+ const agent = new BasicAgent({ model: "openai/gpt-4o" });
162
+
163
+ vi.mocked(streamText).mockReturnValue(
164
+ mockStreamTextResponse([finish()]) as any,
165
+ );
166
+ await collectEvents(
167
+ agent["run"]({
168
+ ...baseInput,
169
+ forwardedProps: {
170
+ copilotkitIntelligence: {
171
+ // userId + apiKey but no mcpUrl — should not attach.
172
+ userId: "jordan",
173
+ apiKey: "cpk-proj_xx",
174
+ },
175
+ },
176
+ }),
177
+ );
178
+
179
+ expect(recorder.records.length).toBe(0);
180
+ } finally {
181
+ recorder.restore();
182
+ }
183
+ });
184
+
185
+ it("does NOT attach when the user has already configured a server pointing at the same URL (explicit opt-in wins)", async () => {
186
+ const { url, server } = await startMcpMock();
187
+ llm = server;
188
+ const mcpUrl = `${url}/mcp`;
189
+
190
+ let userFetchCalls = 0;
191
+ const agent = new BasicAgent({
192
+ model: "openai/gpt-4o",
193
+ mcpServers: [
194
+ {
195
+ type: "http",
196
+ url: mcpUrl,
197
+ options: {
198
+ fetch: async (input, init) => {
199
+ userFetchCalls++;
200
+ const h = new Headers(init?.headers ?? {});
201
+ h.set("Authorization", "Bearer user-supplied");
202
+ h.set("X-Cpki-User-Id", "explicit-user");
203
+ return globalThis.fetch(input, { ...init, headers: h });
204
+ },
205
+ },
206
+ },
207
+ ],
208
+ });
209
+
210
+ const recorder = spyOnFetch(mcpUrl);
211
+ try {
212
+ vi.mocked(streamText).mockReturnValue(
213
+ mockStreamTextResponse([finish()]) as any,
214
+ );
215
+ await collectEvents(
216
+ agent["run"]({
217
+ ...baseInput,
218
+ forwardedProps: {
219
+ copilotkitIntelligence: {
220
+ userId: "from-runtime",
221
+ apiKey: "cpk-proj_runtime",
222
+ mcpUrl,
223
+ },
224
+ },
225
+ }),
226
+ );
227
+
228
+ expect(recorder.records.length).toBeGreaterThan(0);
229
+ // Only the user's fetch wrapper hit the wire — auto-attach skipped.
230
+ for (const headers of recorder.records) {
231
+ expect(headers["authorization"]).toBe("Bearer user-supplied");
232
+ expect(headers["x-cpki-user-id"]).toBe("explicit-user");
233
+ }
234
+ expect(userFetchCalls).toBeGreaterThan(0);
235
+ } finally {
236
+ recorder.restore();
237
+ }
238
+ });
239
+ });
@@ -1,5 +1,17 @@
1
1
  import { logger } from "@copilotkit/shared";
2
2
 
3
+ /**
4
+ * Header name carrying the per-call end-user identity that the CopilotKit
5
+ * Intelligence `/mcp` endpoint requires. Internal CopilotKit machinery — the
6
+ * runtime stamps this onto `agent.headers` after `identifyUser` resolves,
7
+ * and the auto-attach in `configureAgentForRequest` reads it back to gate
8
+ * MCP-server attachment and to populate the outbound `X-Cpki-User-Id`
9
+ * header on every MCP request. Not part of the public user API.
10
+ *
11
+ * @internal
12
+ */
13
+ export const INTELLIGENCE_USER_ID_HEADER = "x-cpki-user-id";
14
+
3
15
  /**
4
16
  * Error thrown when an Intelligence platform HTTP request returns a non-2xx
5
17
  * status. Carries the HTTP {@link status} code so callers can branch on
@@ -64,6 +76,19 @@ export interface CopilotKitIntelligenceConfig {
64
76
  wsUrl: string;
65
77
  /** API key for authenticating with the intelligence platform */
66
78
  apiKey: string;
79
+ /**
80
+ * Enable the Intelligence platform's MCP server (bash + thread tools) on
81
+ * every `BuiltInAgent` run that resolves a user. The auto-attach is
82
+ * implemented in `configureAgentForRequest`: when this flag is `true`
83
+ * AND the runtime's `identifyUser` callback has placed a user-id onto
84
+ * the agent's forwarded headers AND the user has not already configured
85
+ * an MCP server pointing at the same URL, the server is appended to the
86
+ * agent's effective MCP server list for that run.
87
+ *
88
+ * Defaults to `false` — opt-in. Existing intelligence setups continue to
89
+ * work without the bash MCP server unless they flip this flag.
90
+ */
91
+ mcpServer?: boolean;
67
92
  /**
68
93
  * Initial listener invoked after a thread is created.
69
94
  * Prefer {@link CopilotKitIntelligence.onThreadCreated} for multiple listeners.
@@ -198,6 +223,40 @@ export interface ThreadMessagesResponse {
198
223
  messages: ThreadMessage[];
199
224
  }
200
225
 
226
+ /**
227
+ * Persisted AG-UI event for the inspector's debugging views. The platform
228
+ * stores raw events keyed by run; the `_inspect` route returns them in
229
+ * replay order (oldest first) across every run that targeted the thread.
230
+ */
231
+ export interface ThreadInspectEvent {
232
+ type: string;
233
+ [key: string]: unknown;
234
+ }
235
+
236
+ /**
237
+ * Response from {@link CopilotKitIntelligence.getThreadEvents}. Mirrors the
238
+ * `ThreadEventsResult` shape returned by the platform's
239
+ * `GET /api/_inspect/threads/:id/events` endpoint.
240
+ */
241
+ export interface ThreadEventsResponse {
242
+ events: ThreadInspectEvent[];
243
+ /** Row IDs the platform failed to decode (raw column corrupted). */
244
+ decodeErrorRowIds: string[];
245
+ /** True when the platform hit its per-thread event cap. */
246
+ truncated: boolean;
247
+ }
248
+
249
+ /**
250
+ * Response from {@link CopilotKitIntelligence.getThreadState}. Mirrors the
251
+ * discriminated `ThreadStateResult` returned by the platform's
252
+ * `GET /api/_inspect/threads/:id/state` endpoint, which folds RFC 6902
253
+ * STATE_DELTA events on top of the latest STATE_SNAPSHOT.
254
+ */
255
+ export type ThreadStateResponse =
256
+ | { kind: "no-snapshot" }
257
+ | { kind: "snapshot-decode-error" }
258
+ | { kind: "snapshot"; state: unknown; skippedDeltas: number };
259
+
201
260
  export interface AcquireThreadLockRequest {
202
261
  threadId: string;
203
262
  runId: string;
@@ -241,6 +300,7 @@ export class CopilotKitIntelligence {
241
300
  #runnerWsUrl: string;
242
301
  #clientWsUrl: string;
243
302
  #apiKey: string;
303
+ #mcpServerEnabled: boolean;
244
304
  #threadCreatedListeners = new Set<(thread: ThreadSummary) => void>();
245
305
  #threadUpdatedListeners = new Set<(thread: ThreadSummary) => void>();
246
306
  #threadDeletedListeners = new Set<(params: ThreadDeletedPayload) => void>();
@@ -252,6 +312,7 @@ export class CopilotKitIntelligence {
252
312
  this.#runnerWsUrl = deriveRunnerWsUrl(intelligenceWsUrl);
253
313
  this.#clientWsUrl = deriveClientWsUrl(intelligenceWsUrl);
254
314
  this.#apiKey = config.apiKey;
315
+ this.#mcpServerEnabled = config.mcpServer ?? false;
255
316
 
256
317
  if (config.onThreadCreated) {
257
318
  this.onThreadCreated(config.onThreadCreated);
@@ -340,6 +401,16 @@ export class CopilotKitIntelligence {
340
401
  return this.#apiKey;
341
402
  }
342
403
 
404
+ /** @internal Used by the runtime's auto-attach to populate `Authorization`. */
405
+ ɵgetApiKey(): string {
406
+ return this.#apiKey;
407
+ }
408
+
409
+ /** @internal Used by the runtime's auto-attach to gate MCP attachment. */
410
+ ɵisMcpServerEnabled(): boolean {
411
+ return this.#mcpServerEnabled;
412
+ }
413
+
343
414
  async #request<T>(method: string, path: string, body?: unknown): Promise<T> {
344
415
  const url = `${this.#apiUrl}${path}`;
345
416
 
@@ -556,6 +627,48 @@ export class CopilotKitIntelligence {
556
627
  );
557
628
  }
558
629
 
630
+ /**
631
+ * Fetch the persisted AG-UI event stream for a thread.
632
+ *
633
+ * Backed by the platform's `GET /api/_inspect/threads/:id/events`
634
+ * introspection endpoint (see Intelligence PR #144). Events are returned
635
+ * in replay order across every run that targeted the thread. The
636
+ * `_inspect/` prefix flags this as debug-only — production code paths
637
+ * must not depend on it.
638
+ *
639
+ * @throws {@link PlatformRequestError} on non-2xx responses.
640
+ */
641
+ async getThreadEvents(params: {
642
+ threadId: string;
643
+ }): Promise<ThreadEventsResponse> {
644
+ return this.#request<ThreadEventsResponse>(
645
+ "GET",
646
+ `/api/_inspect/threads/${encodeURIComponent(params.threadId)}/events`,
647
+ );
648
+ }
649
+
650
+ /**
651
+ * Fetch the current agent state for a thread.
652
+ *
653
+ * Backed by the platform's `GET /api/_inspect/threads/:id/state`
654
+ * introspection endpoint (see Intelligence PR #144). The platform folds
655
+ * RFC 6902 STATE_DELTA events on top of the latest STATE_SNAPSHOT, so
656
+ * the returned state reflects the thread's current state — not just the
657
+ * last snapshot. The discriminated response distinguishes "no snapshot
658
+ * persisted yet" from "snapshot present" so consumers can render the
659
+ * correct empty state.
660
+ *
661
+ * @throws {@link PlatformRequestError} on non-2xx responses.
662
+ */
663
+ async getThreadState(params: {
664
+ threadId: string;
665
+ }): Promise<ThreadStateResponse> {
666
+ return this.#request<ThreadStateResponse>(
667
+ "GET",
668
+ `/api/_inspect/threads/${encodeURIComponent(params.threadId)}/state`,
669
+ );
670
+ }
671
+
559
672
  /**
560
673
  * Mark a thread as archived.
561
674
  *