@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
@@ -18,26 +18,7 @@ var InMemoryEventStore = class {
18
18
  this.currentEvents = null;
19
19
  }
20
20
  };
21
- const GLOBAL_STORE_KEY = Symbol.for("@copilotkit/runtime/in-memory-store");
22
- function getGlobalStore() {
23
- const globalAny = globalThis;
24
- if (!globalAny[GLOBAL_STORE_KEY]) globalAny[GLOBAL_STORE_KEY] = {
25
- stores: /* @__PURE__ */ new Map(),
26
- historicRunsBackup: /* @__PURE__ */ new Map()
27
- };
28
- const data = globalAny[GLOBAL_STORE_KEY];
29
- if (data.stores.size === 0 && data.historicRunsBackup.size > 0) for (const [threadId, historicRuns] of data.historicRunsBackup) {
30
- const store = new InMemoryEventStore(threadId);
31
- store.historicRuns = historicRuns;
32
- data.stores.set(threadId, store);
33
- }
34
- return data.stores;
35
- }
36
- function backupHistoricRuns(threadId, historicRuns) {
37
- const globalAny = globalThis;
38
- if (globalAny[GLOBAL_STORE_KEY]) globalAny[GLOBAL_STORE_KEY].historicRunsBackup.set(threadId, historicRuns);
39
- }
40
- const GLOBAL_STORE = getGlobalStore();
21
+ const GLOBAL_STORE = /* @__PURE__ */ new Map();
41
22
  var InMemoryAgentRunner = class extends AgentRunner {
42
23
  run(request) {
43
24
  let existingStore = GLOBAL_STORE.get(request.threadId);
@@ -110,11 +91,12 @@ var InMemoryAgentRunner = class extends AgentRunner {
110
91
  store.historicRuns.push({
111
92
  threadId: request.threadId,
112
93
  runId: store.currentRunId,
94
+ agentId: request.agent.agentId ?? "default",
113
95
  parentRunId,
114
96
  events: compactedEvents,
97
+ messages: Array.isArray(request.agent.messages) ? [...request.agent.messages] : [],
115
98
  createdAt: Date.now()
116
99
  });
117
- backupHistoricRuns(request.threadId, store.historicRuns);
118
100
  }
119
101
  store.currentEvents = null;
120
102
  store.currentRunId = null;
@@ -139,11 +121,12 @@ var InMemoryAgentRunner = class extends AgentRunner {
139
121
  store.historicRuns.push({
140
122
  threadId: request.threadId,
141
123
  runId: store.currentRunId,
124
+ agentId: request.agent.agentId ?? "default",
142
125
  parentRunId,
143
126
  events: compactedEvents,
127
+ messages: Array.isArray(request.agent.messages) ? [...request.agent.messages] : [],
144
128
  createdAt: Date.now()
145
129
  });
146
- backupHistoricRuns(request.threadId, store.historicRuns);
147
130
  }
148
131
  store.currentEvents = null;
149
132
  store.currentRunId = null;
@@ -215,6 +198,95 @@ var InMemoryAgentRunner = class extends AgentRunner {
215
198
  return Promise.resolve(false);
216
199
  }
217
200
  }
201
+ /**
202
+ * Returns a summary of every thread that has been run through this runner.
203
+ *
204
+ * This powers the local-dev fallback for `GET /threads` when the Intelligence
205
+ * platform is not configured. Each entry mirrors the shape of a platform
206
+ * `ThreadRecord` so the HTTP handler can use the same response envelope.
207
+ */
208
+ listThreads() {
209
+ const threads = [];
210
+ for (const [threadId, store] of GLOBAL_STORE) {
211
+ if (store.historicRuns.length === 0) continue;
212
+ const firstRun = store.historicRuns[0];
213
+ const lastRun = store.historicRuns[store.historicRuns.length - 1];
214
+ threads.push({
215
+ id: threadId,
216
+ name: null,
217
+ agentId: lastRun.agentId,
218
+ organizationId: "",
219
+ createdById: "",
220
+ archived: false,
221
+ createdAt: new Date(firstRun.createdAt).toISOString(),
222
+ updatedAt: new Date(lastRun.createdAt).toISOString()
223
+ });
224
+ }
225
+ return threads.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
226
+ }
227
+ /**
228
+ * Returns all messages for a thread, using the snapshot captured at the end
229
+ * of the most recent run.
230
+ *
231
+ * This powers the local-dev fallback for `GET /threads/:threadId/messages`
232
+ * when the Intelligence platform is not configured. The returned `Message[]`
233
+ * objects come directly from the ag-ui agent, so their shape is compatible
234
+ * with the Intelligence platform's `ThreadMessage` type.
235
+ */
236
+ getThreadMessages(threadId) {
237
+ const store = GLOBAL_STORE.get(threadId);
238
+ if (!store || store.historicRuns.length === 0) return [];
239
+ return store.historicRuns[store.historicRuns.length - 1].messages;
240
+ }
241
+ /**
242
+ * Returns all AG-UI events for a thread, compacted across historic runs.
243
+ *
244
+ * Powers the local-dev fallback for `GET /threads/:threadId/events` when the
245
+ * Intelligence platform is not configured. The compaction logic matches
246
+ * the connection-replay path in {@link connect}, so the stream a
247
+ * late-joining inspector sees matches what this method returns.
248
+ */
249
+ getThreadEvents(threadId) {
250
+ const store = GLOBAL_STORE.get(threadId);
251
+ if (!store || store.historicRuns.length === 0) return [];
252
+ const all = [];
253
+ for (const run of store.historicRuns) all.push(...run.events);
254
+ return compactEvents(all);
255
+ }
256
+ /**
257
+ * Returns the agent state snapshot for a thread.
258
+ *
259
+ * Derived from the last `STATE_SNAPSHOT` in the compacted event stream. The
260
+ * AG-UI `compactEvents` helper consolidates STATE_DELTA events and produces
261
+ * a single trailing STATE_SNAPSHOT when state changes exist, so this is a
262
+ * faithful view of state at the end of the most recent run.
263
+ *
264
+ * Returns `null` when the thread has never emitted a STATE_SNAPSHOT.
265
+ */
266
+ getThreadState(threadId) {
267
+ const events = this.getThreadEvents(threadId);
268
+ for (let i = events.length - 1; i >= 0; i--) {
269
+ const event = events[i];
270
+ if (event.type === EventType.STATE_SNAPSHOT) {
271
+ const snapshot = event.snapshot;
272
+ if (snapshot && typeof snapshot === "object") return snapshot;
273
+ return null;
274
+ }
275
+ }
276
+ return null;
277
+ }
278
+ /**
279
+ * Clears all in-memory thread history.
280
+ *
281
+ * Powers the local-dev fallback for `POST /threads/clear`, letting consumers
282
+ * (e.g. the demo's Clear button) reset to an empty thread list without
283
+ * restarting the runtime. Intentionally not exposed on the Intelligence
284
+ * platform path: there, thread history lives in a real database and must
285
+ * not be wiped this way.
286
+ */
287
+ clearThreads() {
288
+ GLOBAL_STORE.clear();
289
+ }
218
290
  };
219
291
 
220
292
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"in-memory.mjs","names":[],"sources":["../../../../src/v2/runtime/runner/in-memory.ts"],"sourcesContent":["import {\n AgentRunner,\n AgentRunnerConnectRequest,\n AgentRunnerIsRunningRequest,\n AgentRunnerRunRequest,\n type AgentRunnerStopRequest,\n} from \"./agent-runner\";\nimport { Observable, ReplaySubject } from \"rxjs\";\nimport {\n AbstractAgent,\n BaseEvent,\n EventType,\n MessagesSnapshotEvent,\n RunStartedEvent,\n compactEvents,\n} from \"@ag-ui/client\";\nimport { finalizeRunEvents } from \"@copilotkit/shared\";\n\ninterface HistoricRun {\n threadId: string;\n runId: string;\n parentRunId: string | null;\n events: BaseEvent[];\n createdAt: number;\n}\n\nclass InMemoryEventStore {\n constructor(public threadId: string) {}\n\n /** The subject that current consumers subscribe to. */\n subject: ReplaySubject<BaseEvent> | null = null;\n\n /** True while a run is actively producing events. */\n isRunning = false;\n\n /** Current run ID */\n currentRunId: string | null = null;\n\n /** Historic completed runs */\n historicRuns: HistoricRun[] = [];\n\n /** Currently running agent instance (if any). */\n agent: AbstractAgent | null = null;\n\n /** Subject returned from run() while the run is active. */\n runSubject: ReplaySubject<BaseEvent> | null = null;\n\n /** True once stop() has been requested but the run has not yet finalized. */\n stopRequested = false;\n\n /** Reference to the events emitted in the current run. */\n currentEvents: BaseEvent[] | null = null;\n}\n\n// Use a symbol key on globalThis to survive hot reloads in development\nconst GLOBAL_STORE_KEY = Symbol.for(\"@copilotkit/runtime/in-memory-store\");\n\ninterface GlobalStoreData {\n stores: Map<string, InMemoryEventStore>;\n historicRunsBackup: Map<string, HistoricRun[]>;\n}\n\nfunction getGlobalStore(): Map<string, InMemoryEventStore> {\n const globalAny = globalThis as unknown as Record<symbol, GlobalStoreData>;\n\n if (!globalAny[GLOBAL_STORE_KEY]) {\n globalAny[GLOBAL_STORE_KEY] = {\n stores: new Map<string, InMemoryEventStore>(),\n historicRunsBackup: new Map<string, HistoricRun[]>(),\n };\n }\n\n const data = globalAny[GLOBAL_STORE_KEY];\n\n // Restore historic runs from backup after hot reload\n // (when stores map is empty but backup has data)\n if (data.stores.size === 0 && data.historicRunsBackup.size > 0) {\n for (const [threadId, historicRuns] of data.historicRunsBackup) {\n const store = new InMemoryEventStore(threadId);\n store.historicRuns = historicRuns;\n data.stores.set(threadId, store);\n }\n }\n\n return data.stores;\n}\n\nfunction backupHistoricRuns(\n threadId: string,\n historicRuns: HistoricRun[],\n): void {\n const globalAny = globalThis as unknown as Record<symbol, GlobalStoreData>;\n if (globalAny[GLOBAL_STORE_KEY]) {\n globalAny[GLOBAL_STORE_KEY].historicRunsBackup.set(threadId, historicRuns);\n }\n}\n\nconst GLOBAL_STORE = getGlobalStore();\n\nexport class InMemoryAgentRunner extends AgentRunner {\n run(request: AgentRunnerRunRequest): Observable<BaseEvent> {\n let existingStore = GLOBAL_STORE.get(request.threadId);\n if (!existingStore) {\n existingStore = new InMemoryEventStore(request.threadId);\n GLOBAL_STORE.set(request.threadId, existingStore);\n }\n const store = existingStore; // Now store is const and non-null\n\n if (store.isRunning) {\n throw new Error(\"Thread already running\");\n }\n store.isRunning = true;\n store.currentRunId = request.input.runId;\n store.agent = request.agent;\n store.stopRequested = false;\n\n // Track seen message IDs and current run events for this run\n const seenMessageIds = new Set<string>();\n const currentRunEvents: BaseEvent[] = [];\n store.currentEvents = currentRunEvents;\n\n // Get all previously seen message IDs from historic runs\n const historicMessageIds = new Set<string>();\n for (const run of store.historicRuns) {\n for (const event of run.events) {\n if (\"messageId\" in event && typeof event.messageId === \"string\") {\n historicMessageIds.add(event.messageId);\n }\n if (event.type === EventType.RUN_STARTED) {\n const runStarted = event as RunStartedEvent;\n const messages = runStarted.input?.messages ?? [];\n for (const message of messages) {\n historicMessageIds.add(message.id);\n }\n }\n }\n }\n\n const nextSubject = new ReplaySubject<BaseEvent>(Infinity);\n const prevSubject = store.subject;\n\n // Update the store's subject immediately\n store.subject = nextSubject;\n\n // Create a subject for run() return value\n const runSubject = new ReplaySubject<BaseEvent>(Infinity);\n store.runSubject = runSubject;\n\n // Helper function to run the agent and handle errors\n const runAgent = async () => {\n // Get parent run ID for chaining\n const lastRun = store.historicRuns[store.historicRuns.length - 1];\n const parentRunId = lastRun?.runId ?? null;\n\n try {\n await request.agent.runAgent(request.input, {\n onEvent: ({ event }) => {\n let processedEvent: BaseEvent = event;\n if (event.type === EventType.RUN_STARTED) {\n const runStartedEvent = event as RunStartedEvent;\n if (!runStartedEvent.input) {\n const sanitizedMessages = request.input.messages\n ? request.input.messages.filter(\n (message) => !historicMessageIds.has(message.id),\n )\n : undefined;\n const updatedInput = {\n ...request.input,\n ...(sanitizedMessages !== undefined\n ? { messages: sanitizedMessages }\n : {}),\n };\n processedEvent = {\n ...runStartedEvent,\n input: updatedInput,\n } as RunStartedEvent;\n }\n }\n\n runSubject.next(processedEvent); // For run() return - only agent events\n nextSubject.next(processedEvent); // For connect() / store - all events\n currentRunEvents.push(processedEvent); // Accumulate for storage\n },\n onNewMessage: ({ message }) => {\n // Called for each new message\n if (!seenMessageIds.has(message.id)) {\n seenMessageIds.add(message.id);\n }\n },\n onRunStartedEvent: () => {\n // Mark any messages from the input as seen so they aren't emitted twice\n if (request.input.messages) {\n for (const message of request.input.messages) {\n if (!seenMessageIds.has(message.id)) {\n seenMessageIds.add(message.id);\n }\n }\n }\n },\n });\n\n const appendedEvents = finalizeRunEvents(currentRunEvents, {\n stopRequested: store.stopRequested,\n });\n for (const event of appendedEvents) {\n runSubject.next(event);\n nextSubject.next(event);\n }\n\n // Store the completed run in memory with ONLY its events\n if (store.currentRunId) {\n // Compact the events before storing (like SQLite does)\n const compactedEvents = compactEvents(currentRunEvents);\n\n store.historicRuns.push({\n threadId: request.threadId,\n runId: store.currentRunId,\n parentRunId,\n events: compactedEvents,\n createdAt: Date.now(),\n });\n\n // Backup for hot reload survival\n backupHistoricRuns(request.threadId, store.historicRuns);\n }\n\n // Complete the run\n store.currentEvents = null;\n store.currentRunId = null;\n store.agent = null;\n store.runSubject = null;\n store.stopRequested = false;\n store.isRunning = false;\n runSubject.complete();\n nextSubject.complete();\n } catch (error) {\n const interruptionMessage =\n error instanceof Error ? error.message : String(error);\n const appendedEvents = finalizeRunEvents(currentRunEvents, {\n stopRequested: store.stopRequested,\n interruptionMessage,\n });\n for (const event of appendedEvents) {\n runSubject.next(event);\n nextSubject.next(event);\n }\n\n // Store the run even if it failed (partial events)\n if (store.currentRunId && currentRunEvents.length > 0) {\n // Compact the events before storing (like SQLite does)\n const compactedEvents = compactEvents(currentRunEvents);\n store.historicRuns.push({\n threadId: request.threadId,\n runId: store.currentRunId,\n parentRunId,\n events: compactedEvents,\n createdAt: Date.now(),\n });\n\n // Backup for hot reload survival\n backupHistoricRuns(request.threadId, store.historicRuns);\n }\n\n // Complete the run\n store.currentEvents = null;\n store.currentRunId = null;\n store.agent = null;\n store.runSubject = null;\n store.stopRequested = false;\n store.isRunning = false;\n runSubject.complete();\n nextSubject.complete();\n }\n };\n\n // Bridge previous events if they exist\n if (prevSubject) {\n prevSubject.subscribe({\n next: (e) => nextSubject.next(e),\n error: (err) => nextSubject.error(err),\n complete: () => {\n // Don't complete nextSubject here - it needs to stay open for new events\n },\n });\n }\n\n // Start the agent execution immediately (not lazily)\n runAgent();\n\n // Return the run subject (only agent events, no injected messages)\n return runSubject.asObservable();\n }\n\n connect(request: AgentRunnerConnectRequest): Observable<BaseEvent> {\n const store = GLOBAL_STORE.get(request.threadId);\n const connectionSubject = new ReplaySubject<BaseEvent>(Infinity);\n\n if (!store) {\n // No store means no events\n connectionSubject.complete();\n return connectionSubject.asObservable();\n }\n\n // Collect all historic events from memory\n const allHistoricEvents: BaseEvent[] = [];\n for (const run of store.historicRuns) {\n allHistoricEvents.push(...run.events);\n }\n\n // Apply compaction to all historic events together (like SQLite)\n const compactedEvents = compactEvents(allHistoricEvents);\n\n // Emit compacted events and track message IDs\n const emittedMessageIds = new Set<string>();\n for (const event of compactedEvents) {\n connectionSubject.next(event);\n if (\"messageId\" in event && typeof event.messageId === \"string\") {\n emittedMessageIds.add(event.messageId);\n }\n }\n\n // Bridge active run to connection if exists\n if (store.subject && (store.isRunning || store.stopRequested)) {\n store.subject.subscribe({\n next: (event) => {\n // Skip message events that we've already emitted from historic\n if (\n \"messageId\" in event &&\n typeof event.messageId === \"string\" &&\n emittedMessageIds.has(event.messageId)\n ) {\n return;\n }\n connectionSubject.next(event);\n },\n complete: () => connectionSubject.complete(),\n error: (err) => connectionSubject.error(err),\n });\n } else {\n // No active run, complete after historic events\n connectionSubject.complete();\n }\n\n return connectionSubject.asObservable();\n }\n\n isRunning(request: AgentRunnerIsRunningRequest): Promise<boolean> {\n const store = GLOBAL_STORE.get(request.threadId);\n return Promise.resolve(store?.isRunning ?? false);\n }\n\n stop(request: AgentRunnerStopRequest): Promise<boolean | undefined> {\n const store = GLOBAL_STORE.get(request.threadId);\n if (!store || !store.isRunning) {\n return Promise.resolve(false);\n }\n if (store.stopRequested) {\n return Promise.resolve(false);\n }\n\n store.stopRequested = true;\n store.isRunning = false;\n\n const agent = store.agent;\n if (!agent) {\n store.stopRequested = false;\n store.isRunning = false;\n return Promise.resolve(false);\n }\n\n try {\n agent.abortRun();\n return Promise.resolve(true);\n } catch (error) {\n console.error(\"Failed to abort agent run\", error);\n store.stopRequested = false;\n store.isRunning = true;\n return Promise.resolve(false);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"in-memory.mjs","names":[],"sources":["../../../../src/v2/runtime/runner/in-memory.ts"],"sourcesContent":["import {\n AgentRunner,\n AgentRunnerConnectRequest,\n AgentRunnerIsRunningRequest,\n AgentRunnerRunRequest,\n type AgentRunnerStopRequest,\n} from \"./agent-runner\";\nimport { Observable, ReplaySubject } from \"rxjs\";\nimport {\n AbstractAgent,\n BaseEvent,\n EventType,\n Message,\n RunStartedEvent,\n StateSnapshotEvent,\n compactEvents,\n} from \"@ag-ui/client\";\nimport { finalizeRunEvents } from \"@copilotkit/shared\";\n\ninterface HistoricRun {\n threadId: string;\n runId: string;\n /** ID of the agent that executed this run. */\n agentId: string;\n parentRunId: string | null;\n events: BaseEvent[];\n /**\n * Snapshot of all messages (input + generated) at the end of this run.\n * Used by the local thread-messages fallback endpoint.\n */\n messages: Message[];\n createdAt: number;\n}\n\n/**\n * Lightweight thread summary returned by {@link InMemoryAgentRunner.listThreads}.\n * Shape matches the Intelligence platform's ThreadRecord so the same HTTP\n * response envelope can be used for both backends.\n */\nexport interface InMemoryThread {\n id: string;\n name: string | null;\n agentId: string;\n organizationId: \"\"; // always empty in in-memory mode\n createdById: \"\"; // always empty in in-memory mode\n archived: false; // always false in in-memory mode\n createdAt: string;\n updatedAt: string;\n}\n\nclass InMemoryEventStore {\n constructor(public threadId: string) {}\n\n /** The subject that current consumers subscribe to. */\n subject: ReplaySubject<BaseEvent> | null = null;\n\n /** True while a run is actively producing events. */\n isRunning = false;\n\n /** Current run ID */\n currentRunId: string | null = null;\n\n /** Historic completed runs */\n historicRuns: HistoricRun[] = [];\n\n /** Currently running agent instance (if any). */\n agent: AbstractAgent | null = null;\n\n /** Subject returned from run() while the run is active. */\n runSubject: ReplaySubject<BaseEvent> | null = null;\n\n /** True once stop() has been requested but the run has not yet finalized. */\n stopRequested = false;\n\n /** Reference to the events emitted in the current run. */\n currentEvents: BaseEvent[] | null = null;\n}\n\nconst GLOBAL_STORE = new Map<string, InMemoryEventStore>();\n\nexport class InMemoryAgentRunner extends AgentRunner {\n run(request: AgentRunnerRunRequest): Observable<BaseEvent> {\n let existingStore = GLOBAL_STORE.get(request.threadId);\n if (!existingStore) {\n existingStore = new InMemoryEventStore(request.threadId);\n GLOBAL_STORE.set(request.threadId, existingStore);\n }\n const store = existingStore; // Now store is const and non-null\n\n if (store.isRunning) {\n throw new Error(\"Thread already running\");\n }\n store.isRunning = true;\n store.currentRunId = request.input.runId;\n store.agent = request.agent;\n store.stopRequested = false;\n\n // Track seen message IDs and current run events for this run\n const seenMessageIds = new Set<string>();\n const currentRunEvents: BaseEvent[] = [];\n store.currentEvents = currentRunEvents;\n\n // Get all previously seen message IDs from historic runs\n const historicMessageIds = new Set<string>();\n for (const run of store.historicRuns) {\n for (const event of run.events) {\n if (\"messageId\" in event && typeof event.messageId === \"string\") {\n historicMessageIds.add(event.messageId);\n }\n if (event.type === EventType.RUN_STARTED) {\n const runStarted = event as RunStartedEvent;\n const messages = runStarted.input?.messages ?? [];\n for (const message of messages) {\n historicMessageIds.add(message.id);\n }\n }\n }\n }\n\n const nextSubject = new ReplaySubject<BaseEvent>(Infinity);\n const prevSubject = store.subject;\n\n // Update the store's subject immediately\n store.subject = nextSubject;\n\n // Create a subject for run() return value\n const runSubject = new ReplaySubject<BaseEvent>(Infinity);\n store.runSubject = runSubject;\n\n // Helper function to run the agent and handle errors\n const runAgent = async () => {\n // Get parent run ID for chaining\n const lastRun = store.historicRuns[store.historicRuns.length - 1];\n const parentRunId = lastRun?.runId ?? null;\n\n try {\n await request.agent.runAgent(request.input, {\n onEvent: ({ event }) => {\n let processedEvent: BaseEvent = event;\n if (event.type === EventType.RUN_STARTED) {\n const runStartedEvent = event as RunStartedEvent;\n if (!runStartedEvent.input) {\n const sanitizedMessages = request.input.messages\n ? request.input.messages.filter(\n (message) => !historicMessageIds.has(message.id),\n )\n : undefined;\n const updatedInput = {\n ...request.input,\n ...(sanitizedMessages !== undefined\n ? { messages: sanitizedMessages }\n : {}),\n };\n processedEvent = {\n ...runStartedEvent,\n input: updatedInput,\n } as RunStartedEvent;\n }\n }\n\n runSubject.next(processedEvent); // For run() return - only agent events\n nextSubject.next(processedEvent); // For connect() / store - all events\n currentRunEvents.push(processedEvent); // Accumulate for storage\n },\n onNewMessage: ({ message }) => {\n // Called for each new message\n if (!seenMessageIds.has(message.id)) {\n seenMessageIds.add(message.id);\n }\n },\n onRunStartedEvent: () => {\n // Mark any messages from the input as seen so they aren't emitted twice\n if (request.input.messages) {\n for (const message of request.input.messages) {\n if (!seenMessageIds.has(message.id)) {\n seenMessageIds.add(message.id);\n }\n }\n }\n },\n });\n\n const appendedEvents = finalizeRunEvents(currentRunEvents, {\n stopRequested: store.stopRequested,\n });\n for (const event of appendedEvents) {\n runSubject.next(event);\n nextSubject.next(event);\n }\n\n // Store the completed run in memory with ONLY its events\n if (store.currentRunId) {\n // Compact the events before storing (like SQLite does)\n const compactedEvents = compactEvents(currentRunEvents);\n\n store.historicRuns.push({\n threadId: request.threadId,\n runId: store.currentRunId,\n agentId: request.agent.agentId ?? \"default\",\n parentRunId,\n events: compactedEvents,\n // Snapshot all messages (input + generated) for the thread-messages endpoint\n messages: Array.isArray(request.agent.messages)\n ? [...request.agent.messages]\n : [],\n createdAt: Date.now(),\n });\n }\n\n // Complete the run\n store.currentEvents = null;\n store.currentRunId = null;\n store.agent = null;\n store.runSubject = null;\n store.stopRequested = false;\n store.isRunning = false;\n runSubject.complete();\n nextSubject.complete();\n } catch (error) {\n const interruptionMessage =\n error instanceof Error ? error.message : String(error);\n const appendedEvents = finalizeRunEvents(currentRunEvents, {\n stopRequested: store.stopRequested,\n interruptionMessage,\n });\n for (const event of appendedEvents) {\n runSubject.next(event);\n nextSubject.next(event);\n }\n\n // Store the run even if it failed (partial events)\n if (store.currentRunId && currentRunEvents.length > 0) {\n // Compact the events before storing (like SQLite does)\n const compactedEvents = compactEvents(currentRunEvents);\n store.historicRuns.push({\n threadId: request.threadId,\n runId: store.currentRunId,\n agentId: request.agent.agentId ?? \"default\",\n parentRunId,\n events: compactedEvents,\n messages: Array.isArray(request.agent.messages)\n ? [...request.agent.messages]\n : [],\n createdAt: Date.now(),\n });\n }\n\n // Complete the run\n store.currentEvents = null;\n store.currentRunId = null;\n store.agent = null;\n store.runSubject = null;\n store.stopRequested = false;\n store.isRunning = false;\n runSubject.complete();\n nextSubject.complete();\n }\n };\n\n // Bridge previous events if they exist\n if (prevSubject) {\n prevSubject.subscribe({\n next: (e) => nextSubject.next(e),\n error: (err) => nextSubject.error(err),\n complete: () => {\n // Don't complete nextSubject here - it needs to stay open for new events\n },\n });\n }\n\n // Start the agent execution immediately (not lazily)\n runAgent();\n\n // Return the run subject (only agent events, no injected messages)\n return runSubject.asObservable();\n }\n\n connect(request: AgentRunnerConnectRequest): Observable<BaseEvent> {\n const store = GLOBAL_STORE.get(request.threadId);\n const connectionSubject = new ReplaySubject<BaseEvent>(Infinity);\n\n if (!store) {\n // No store means no events\n connectionSubject.complete();\n return connectionSubject.asObservable();\n }\n\n // Collect all historic events from memory\n const allHistoricEvents: BaseEvent[] = [];\n for (const run of store.historicRuns) {\n allHistoricEvents.push(...run.events);\n }\n\n // Apply compaction to all historic events together (like SQLite)\n const compactedEvents = compactEvents(allHistoricEvents);\n\n // Emit compacted events and track message IDs\n const emittedMessageIds = new Set<string>();\n for (const event of compactedEvents) {\n connectionSubject.next(event);\n if (\"messageId\" in event && typeof event.messageId === \"string\") {\n emittedMessageIds.add(event.messageId);\n }\n }\n\n // Bridge active run to connection if exists\n if (store.subject && (store.isRunning || store.stopRequested)) {\n store.subject.subscribe({\n next: (event) => {\n // Skip message events that we've already emitted from historic\n if (\n \"messageId\" in event &&\n typeof event.messageId === \"string\" &&\n emittedMessageIds.has(event.messageId)\n ) {\n return;\n }\n connectionSubject.next(event);\n },\n complete: () => connectionSubject.complete(),\n error: (err) => connectionSubject.error(err),\n });\n } else {\n // No active run, complete after historic events\n connectionSubject.complete();\n }\n\n return connectionSubject.asObservable();\n }\n\n isRunning(request: AgentRunnerIsRunningRequest): Promise<boolean> {\n const store = GLOBAL_STORE.get(request.threadId);\n return Promise.resolve(store?.isRunning ?? false);\n }\n\n stop(request: AgentRunnerStopRequest): Promise<boolean | undefined> {\n const store = GLOBAL_STORE.get(request.threadId);\n if (!store || !store.isRunning) {\n return Promise.resolve(false);\n }\n if (store.stopRequested) {\n return Promise.resolve(false);\n }\n\n store.stopRequested = true;\n store.isRunning = false;\n\n const agent = store.agent;\n if (!agent) {\n store.stopRequested = false;\n store.isRunning = false;\n return Promise.resolve(false);\n }\n\n try {\n agent.abortRun();\n return Promise.resolve(true);\n } catch (error) {\n console.error(\"Failed to abort agent run\", error);\n store.stopRequested = false;\n store.isRunning = true;\n return Promise.resolve(false);\n }\n }\n\n /**\n * Returns a summary of every thread that has been run through this runner.\n *\n * This powers the local-dev fallback for `GET /threads` when the Intelligence\n * platform is not configured. Each entry mirrors the shape of a platform\n * `ThreadRecord` so the HTTP handler can use the same response envelope.\n */\n listThreads(): InMemoryThread[] {\n const threads: InMemoryThread[] = [];\n for (const [threadId, store] of GLOBAL_STORE) {\n if (store.historicRuns.length === 0) continue;\n const firstRun = store.historicRuns[0]!;\n const lastRun = store.historicRuns[store.historicRuns.length - 1]!;\n threads.push({\n id: threadId,\n name: null,\n agentId: lastRun.agentId,\n organizationId: \"\",\n createdById: \"\",\n archived: false,\n createdAt: new Date(firstRun.createdAt).toISOString(),\n updatedAt: new Date(lastRun.createdAt).toISOString(),\n });\n }\n // Most recently updated first\n return threads.sort(\n (a, b) =>\n new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),\n );\n }\n\n /**\n * Returns all messages for a thread, using the snapshot captured at the end\n * of the most recent run.\n *\n * This powers the local-dev fallback for `GET /threads/:threadId/messages`\n * when the Intelligence platform is not configured. The returned `Message[]`\n * objects come directly from the ag-ui agent, so their shape is compatible\n * with the Intelligence platform's `ThreadMessage` type.\n */\n getThreadMessages(threadId: string): Message[] {\n const store = GLOBAL_STORE.get(threadId);\n if (!store || store.historicRuns.length === 0) return [];\n // The last run's snapshot has the complete conversation history\n return store.historicRuns[store.historicRuns.length - 1]!.messages;\n }\n\n /**\n * Returns all AG-UI events for a thread, compacted across historic runs.\n *\n * Powers the local-dev fallback for `GET /threads/:threadId/events` when the\n * Intelligence platform is not configured. The compaction logic matches\n * the connection-replay path in {@link connect}, so the stream a\n * late-joining inspector sees matches what this method returns.\n */\n getThreadEvents(threadId: string): BaseEvent[] {\n const store = GLOBAL_STORE.get(threadId);\n if (!store || store.historicRuns.length === 0) return [];\n const all: BaseEvent[] = [];\n for (const run of store.historicRuns) all.push(...run.events);\n return compactEvents(all);\n }\n\n /**\n * Returns the agent state snapshot for a thread.\n *\n * Derived from the last `STATE_SNAPSHOT` in the compacted event stream. The\n * AG-UI `compactEvents` helper consolidates STATE_DELTA events and produces\n * a single trailing STATE_SNAPSHOT when state changes exist, so this is a\n * faithful view of state at the end of the most recent run.\n *\n * Returns `null` when the thread has never emitted a STATE_SNAPSHOT.\n */\n getThreadState(threadId: string): Record<string, unknown> | null {\n const events = this.getThreadEvents(threadId);\n // Walk backwards — the last snapshot wins.\n for (let i = events.length - 1; i >= 0; i--) {\n const event = events[i]!;\n if (event.type === EventType.STATE_SNAPSHOT) {\n const snapshot = (event as StateSnapshotEvent).snapshot;\n if (snapshot && typeof snapshot === \"object\") {\n return snapshot as Record<string, unknown>;\n }\n return null;\n }\n }\n return null;\n }\n\n /**\n * Clears all in-memory thread history.\n *\n * Powers the local-dev fallback for `POST /threads/clear`, letting consumers\n * (e.g. the demo's Clear button) reset to an empty thread list without\n * restarting the runtime. Intentionally not exposed on the Intelligence\n * platform path: there, thread history lives in a real database and must\n * not be wiped this way.\n */\n clearThreads(): void {\n GLOBAL_STORE.clear();\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { AgentRunner, AgentRunnerConnectRequest, AgentRunnerIsRunningRequest, AgentRunnerRunRequest, AgentRunnerStopRequest } from "./agent-runner.cjs";
3
- import { InMemoryAgentRunner } from "./in-memory.cjs";
3
+ import { InMemoryAgentRunner, InMemoryThread } from "./in-memory.cjs";
4
4
  import { IntelligenceAgentRunner, IntelligenceAgentRunnerOptions, RunnerStartupBoundary } from "./intelligence.cjs";
5
5
  import { finalizeRunEvents } from "@copilotkit/shared";
6
6
  export { finalizeRunEvents };
@@ -1,6 +1,6 @@
1
1
  import "reflect-metadata";
2
2
  import { AgentRunner, AgentRunnerConnectRequest, AgentRunnerIsRunningRequest, AgentRunnerRunRequest, AgentRunnerStopRequest } from "./agent-runner.mjs";
3
- import { InMemoryAgentRunner } from "./in-memory.mjs";
3
+ import { InMemoryAgentRunner, InMemoryThread } from "./in-memory.mjs";
4
4
  import { IntelligenceAgentRunner, IntelligenceAgentRunnerOptions, RunnerStartupBoundary } from "./intelligence.mjs";
5
5
  import { finalizeRunEvents as finalizeRunEvents$1 } from "@copilotkit/shared";
6
6
  export { finalizeRunEvents$1 as finalizeRunEvents };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/runtime",
3
- "version": "1.56.5",
3
+ "version": "1.57.0-canary.1778082736",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -115,7 +115,7 @@
115
115
  "uuid": "^10.0.0",
116
116
  "ws": "^8.18.0",
117
117
  "zod": "^3.23.3",
118
- "@copilotkit/shared": "1.56.5"
118
+ "@copilotkit/shared": "1.57.0-canary.1778082736"
119
119
  },
120
120
  "devDependencies": {
121
121
  "@copilotkit/aimock": "latest",
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { BasicAgent, type MCPClientProvider } from "../index";
3
3
  import { EventType, type RunAgentInput } from "@ag-ui/client";
4
- import { streamText, type ToolSet } from "ai";
4
+ import { streamText } from "ai";
5
5
  import {
6
6
  mockStreamTextResponse,
7
7
  textDelta,
@@ -26,16 +26,7 @@ vi.mock("@ai-sdk/openai", () => ({
26
26
 
27
27
  // Mock MCP imports so mcpServers code path doesn't fail when tested alongside mcpClients
28
28
  vi.mock("@ai-sdk/mcp", () => ({
29
- experimental_createMCPClient: vi.fn(),
30
- }));
31
-
32
- // Transport mocks must return truthy objects so `if (transport)` check passes in run()
33
- vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
34
- StreamableHTTPClientTransport: vi.fn(() => ({ type: "mock-http-transport" })),
35
- }));
36
-
37
- vi.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
38
- SSEClientTransport: vi.fn(() => ({ type: "mock-sse-transport" })),
29
+ createMCPClient: vi.fn(),
39
30
  }));
40
31
 
41
32
  describe("mcpClients — user-managed MCP clients", () => {
@@ -120,8 +111,8 @@ describe("mcpClients — user-managed MCP clients", () => {
120
111
  });
121
112
 
122
113
  // Mock mcpServers flow: createMCPClient returns a client with tools()
123
- const { experimental_createMCPClient } = await import("@ai-sdk/mcp");
124
- vi.mocked(experimental_createMCPClient).mockResolvedValue({
114
+ const { createMCPClient } = await import("@ai-sdk/mcp");
115
+ vi.mocked(createMCPClient).mockResolvedValue({
125
116
  tools: vi.fn().mockResolvedValue({
126
117
  sharedTool: { description: "from server", execute: serverExecute },
127
118
  }),
@@ -241,20 +232,15 @@ describe("mcpClients — user-managed MCP clients", () => {
241
232
  });
242
233
 
243
234
  it("type compatibility: @ai-sdk/mcp MCPClient satisfies MCPClientProvider", async () => {
244
- // This is a compile-time check if MCPClientProvider's .tools() signature
245
- // is incompatible with @ai-sdk/mcp's MCPClient.tools(), this file won't compile.
246
- //
247
- // We use a dynamic import + type assertion rather than a static import because
248
- // @ai-sdk/mcp is mocked in this test file. The type check happens at compile time
249
- // regardless.
235
+ // Compile-time check that `MCPClientProvider` is structurally compatible
236
+ // with `@ai-sdk/mcp`'s `MCPClient`. After the refactor `MCPClientProvider`
237
+ // is an alias for `Pick<MCPClient, "tools">`, so this is trivially true —
238
+ // but keeping the test guards against future divergence.
250
239
  type MCPClient = Awaited<
251
- ReturnType<typeof import("@ai-sdk/mcp").experimental_createMCPClient>
240
+ ReturnType<typeof import("@ai-sdk/mcp").createMCPClient>
252
241
  >;
253
-
254
- // If this line causes a type error, MCPClientProvider needs to be widened
255
242
  const _assignable: MCPClientProvider = {} as MCPClient;
256
- void _assignable; // suppress unused warning
257
-
258
- expect(true).toBe(true); // runtime no-op
243
+ void _assignable;
244
+ expect(true).toBe(true);
259
245
  });
260
246
  });
@@ -1,8 +1,6 @@
1
- import {
2
- AbstractAgent,
1
+ import type {
3
2
  BaseEvent,
4
3
  RunAgentInput,
5
- EventType,
6
4
  Message,
7
5
  ReasoningEndEvent,
8
6
  ReasoningMessageContentEvent,
@@ -20,9 +18,9 @@ import {
20
18
  StateSnapshotEvent,
21
19
  StateDeltaEvent,
22
20
  } from "@ag-ui/client";
21
+ import { AbstractAgent, EventType } from "@ag-ui/client";
23
22
  import type { AgentCapabilities } from "@ag-ui/core";
24
- import {
25
- streamText,
23
+ import type {
26
24
  LanguageModel,
27
25
  ModelMessage,
28
26
  AssistantModelMessage,
@@ -34,12 +32,12 @@ import {
34
32
  TextPart,
35
33
  ImagePart,
36
34
  FilePart,
37
- tool as createVercelAISDKTool,
38
35
  ToolChoice,
39
36
  ToolSet,
40
- stepCountIs,
41
37
  } from "ai";
42
- import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp";
38
+ import { streamText, tool as createVercelAISDKTool, stepCountIs } from "ai";
39
+ import { createMCPClient } from "@ai-sdk/mcp";
40
+ import type { MCPClient } from "@ai-sdk/mcp";
43
41
  import { Observable } from "rxjs";
44
42
  import { createOpenAI } from "@ai-sdk/openai";
45
43
  import { createAnthropic } from "@ai-sdk/anthropic";
@@ -52,10 +50,8 @@ import { schemaToJsonSchema } from "@copilotkit/shared";
52
50
  import { jsonSchema as aiJsonSchema } from "ai";
53
51
  import { convertAISDKStream } from "./converters/aisdk";
54
52
  import { convertTanStackStream } from "./converters/tanstack";
55
- import {
56
- StreamableHTTPClientTransport,
57
- StreamableHTTPClientTransportOptions,
58
- } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
53
+ import type { StreamableHTTPClientTransportOptions } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
54
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
59
55
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
60
56
  import { randomUUID } from "@copilotkit/shared";
61
57
 
@@ -117,16 +113,15 @@ export type ModelSpecifier = string | LanguageModel;
117
113
  * MCP Client configuration for HTTP transport
118
114
  */
119
115
  export interface MCPClientConfigHTTP {
120
- /**
121
- * Type of MCP client
122
- */
116
+ /** Type of MCP client */
123
117
  type: "http";
124
- /**
125
- * URL of the MCP server
126
- */
118
+ /** URL of the MCP server */
127
119
  url: string;
128
120
  /**
129
- * Optional transport options for HTTP client
121
+ * Optional transport options for the underlying
122
+ * `StreamableHTTPClientTransport`. The SDK's documented extension point
123
+ * for per-request customization is `options.fetch` — pass a wrapped fetch
124
+ * here if you need static + dynamic headers on outbound MCP requests.
130
125
  */
131
126
  options?: StreamableHTTPClientTransportOptions;
132
127
  }
@@ -135,17 +130,11 @@ export interface MCPClientConfigHTTP {
135
130
  * MCP Client configuration for SSE transport
136
131
  */
137
132
  export interface MCPClientConfigSSE {
138
- /**
139
- * Type of MCP client
140
- */
133
+ /** Type of MCP client */
141
134
  type: "sse";
142
- /**
143
- * URL of the MCP server
144
- */
135
+ /** URL of the MCP server */
145
136
  url: string;
146
- /**
147
- * Optional HTTP headers (e.g., for authentication)
148
- */
137
+ /** Optional HTTP headers (e.g., for authentication) */
149
138
  headers?: Record<string, string>;
150
139
  }
151
140
 
@@ -1104,7 +1093,7 @@ export class BuiltInAgent extends AbstractAgent {
1104
1093
  }
1105
1094
 
1106
1095
  // Set up MCP clients if configured and process the stream
1107
- const mcpClients: Array<{ close: () => Promise<void> }> = [];
1096
+ const mcpClients: MCPClient[] = [];
1108
1097
 
1109
1098
  (async () => {
1110
1099
  let terminalEventEmitted = false;
@@ -1187,9 +1176,55 @@ export class BuiltInAgent extends AbstractAgent {
1187
1176
  }
1188
1177
  }
1189
1178
 
1190
- // Initialize MCP clients and get their tools
1191
- if (this.config.mcpServers && this.config.mcpServers.length > 0) {
1192
- for (const serverConfig of this.config.mcpServers) {
1179
+ // Initialize MCP clients and get their tools.
1180
+ //
1181
+ // Servers come from two sources, concatenated in order:
1182
+ // - `config.mcpServers` — user-supplied static array.
1183
+ // - The CopilotKit Intelligence MCP server, auto-attached when
1184
+ // the runtime forwards a `copilotkitIntelligence` bag via
1185
+ // `input.forwardedProps`. The bag carries `userId` + `apiKey`
1186
+ // + `mcpUrl`. We build a per-request MCPClientConfigHTTP
1187
+ // whose `options.fetch` closes over `apiKey` + `userId` and
1188
+ // stamps `Authorization: Bearer <apiKey>` and `X-Cpki-User-Id:
1189
+ // <userId>` on every outbound MCP call. Skipped when the user
1190
+ // already configured a server pointing at the same URL.
1191
+ const allMcpServers: MCPClientConfig[] = [
1192
+ ...(this.config.mcpServers ?? []),
1193
+ ];
1194
+ const cki = (
1195
+ input.forwardedProps as
1196
+ | { copilotkitIntelligence?: unknown }
1197
+ | undefined
1198
+ )?.copilotkitIntelligence as
1199
+ | { userId?: unknown; apiKey?: unknown; mcpUrl?: unknown }
1200
+ | undefined;
1201
+ const ckiUserId =
1202
+ typeof cki?.userId === "string" ? cki.userId : undefined;
1203
+ const ckiApiKey =
1204
+ typeof cki?.apiKey === "string" ? cki.apiKey : undefined;
1205
+ const ckiMcpUrl =
1206
+ typeof cki?.mcpUrl === "string" ? cki.mcpUrl : undefined;
1207
+ if (
1208
+ ckiUserId &&
1209
+ ckiApiKey &&
1210
+ ckiMcpUrl &&
1211
+ !allMcpServers.some((s) => s.type === "http" && s.url === ckiMcpUrl)
1212
+ ) {
1213
+ allMcpServers.push({
1214
+ type: "http",
1215
+ url: ckiMcpUrl,
1216
+ options: {
1217
+ fetch: async (req, init) => {
1218
+ const headers = new Headers(init?.headers);
1219
+ headers.set("Authorization", `Bearer ${ckiApiKey}`);
1220
+ headers.set("X-Cpki-User-Id", ckiUserId);
1221
+ return globalThis.fetch(req, { ...init, headers });
1222
+ },
1223
+ },
1224
+ });
1225
+ }
1226
+ if (allMcpServers.length > 0) {
1227
+ for (const serverConfig of allMcpServers) {
1193
1228
  let transport;
1194
1229
 
1195
1230
  if (serverConfig.type === "http") {
@@ -370,3 +370,71 @@ describe("fetch-handler validation — multi-route edge cases", () => {
370
370
  expect(body.message).toBeUndefined();
371
371
  });
372
372
  });
373
+
374
+ /* ------------------------------------------------------------------------------------------------
375
+ * Multi-route: HTTP method enforcement on per-thread GET endpoints
376
+ *
377
+ * /threads/:threadId/events and /threads/:threadId/state are read-only and
378
+ * must reject anything other than GET with 405 + Allow: GET. These tests
379
+ * pin that contract so a future refactor cannot quietly downgrade it.
380
+ * --------------------------------------------------------------------------------------------- */
381
+
382
+ describe("fetch-handler validation — GET-only enforcement on threads read endpoints", () => {
383
+ const runtime = createRuntime();
384
+ const handler = createCopilotRuntimeHandler({
385
+ runtime,
386
+ basePath: "/api",
387
+ });
388
+
389
+ const expectMethodNotAllowed = async (
390
+ response: Response,
391
+ expectedAllow: string,
392
+ ) => {
393
+ expect(response.status).toBe(405);
394
+ expect(response.headers.get("Allow")).toBe(expectedAllow);
395
+ };
396
+
397
+ for (const method of ["POST", "PATCH", "DELETE"]) {
398
+ it(`returns 405 with Allow: GET for ${method} /threads/:id/events`, async () => {
399
+ const response = await handler(
400
+ new Request("http://localhost/api/threads/thread-1/events", {
401
+ method,
402
+ // PATCH/POST without a body or content-type would otherwise hit
403
+ // a different validation branch; this exercises pure method check.
404
+ headers: { "Content-Type": "application/json" },
405
+ }),
406
+ );
407
+ await expectMethodNotAllowed(response, "GET");
408
+ });
409
+
410
+ it(`returns 405 with Allow: GET for ${method} /threads/:id/state`, async () => {
411
+ const response = await handler(
412
+ new Request("http://localhost/api/threads/thread-1/state", {
413
+ method,
414
+ headers: { "Content-Type": "application/json" },
415
+ }),
416
+ );
417
+ await expectMethodNotAllowed(response, "GET");
418
+ });
419
+ }
420
+
421
+ it("accepts GET on /threads/:id/events", async () => {
422
+ const response = await handler(
423
+ new Request("http://localhost/api/threads/thread-1/events", {
424
+ method: "GET",
425
+ }),
426
+ );
427
+ // The route may 422 (no Intelligence configured) or 200 — either way it
428
+ // is NOT a 405, which is the contract we are pinning here.
429
+ expect(response.status).not.toBe(405);
430
+ });
431
+
432
+ it("accepts GET on /threads/:id/state", async () => {
433
+ const response = await handler(
434
+ new Request("http://localhost/api/threads/thread-1/state", {
435
+ method: "GET",
436
+ }),
437
+ );
438
+ expect(response.status).not.toBe(405);
439
+ });
440
+ });
@@ -90,6 +90,52 @@ describe("fetch-router", () => {
90
90
  });
91
91
  });
92
92
 
93
+ it("matches GET /threads/:threadId/events", () => {
94
+ const result = matchRoute(
95
+ "/api/copilotkit/threads/thread-abc/events",
96
+ basePath,
97
+ );
98
+ expect(result).toEqual({
99
+ method: "threads/events",
100
+ threadId: "thread-abc",
101
+ });
102
+ });
103
+
104
+ it("matches GET /threads/:threadId/events with URL-encoded threadId", () => {
105
+ const result = matchRoute(
106
+ "/api/copilotkit/threads/thread%2F123/events",
107
+ basePath,
108
+ );
109
+ expect(result).toEqual({
110
+ method: "threads/events",
111
+ threadId: "thread/123",
112
+ });
113
+ });
114
+
115
+ it("matches GET /threads/:threadId/state", () => {
116
+ const result = matchRoute(
117
+ "/api/copilotkit/threads/thread-abc/state",
118
+ basePath,
119
+ );
120
+ expect(result).toEqual({
121
+ method: "threads/state",
122
+ threadId: "thread-abc",
123
+ });
124
+ });
125
+
126
+ it("matches POST /threads/clear (and does not collide with threads/update)", () => {
127
+ // Critical: the threads/update route also matches /threads/:threadId,
128
+ // so we must verify that "/threads/clear" never falls through to that
129
+ // arm with threadId="clear". The router has explicit guards (the
130
+ // segment[len-1] !== "clear" check) — this test pins them.
131
+ const result = matchRoute("/api/copilotkit/threads/clear", basePath);
132
+ expect(result).toEqual({ method: "threads/clear" });
133
+ expect(result).not.toEqual({
134
+ method: "threads/update",
135
+ threadId: "clear",
136
+ });
137
+ });
138
+
93
139
  it("handles URL-encoded threadId in thread routes", () => {
94
140
  const result = matchRoute(
95
141
  "/api/copilotkit/threads/thread%2F123",