@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
@@ -0,0 +1,593 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { EventType, type BaseEvent } from "@ag-ui/client";
3
+ import {
4
+ BuiltInAgent,
5
+ type AgentFactoryContext,
6
+ type BuiltInAgentFactoryConfig,
7
+ createDefaultInput,
8
+ createAgent,
9
+ createThrowingAgent,
10
+ createMidStreamErrorAgent,
11
+ collectEvents,
12
+ collectEventsIncludingErrors,
13
+ expectLifecycleWrapped,
14
+ eventField,
15
+ textDelta,
16
+ finish,
17
+ tanstackTextChunk,
18
+ type AgentType,
19
+ type MockStreamEvent,
20
+ } from "./agent-test-helpers";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Local helpers for parameterized tests
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const allTypes: AgentType[] = ["aisdk", "tanstack", "custom"];
27
+
28
+ function minimalStreamData(
29
+ type: AgentType,
30
+ ): MockStreamEvent[] | Record<string, unknown>[] | BaseEvent[] {
31
+ switch (type) {
32
+ case "aisdk":
33
+ return [textDelta("hi"), finish()];
34
+ case "tanstack":
35
+ return [tanstackTextChunk("hi")];
36
+ case "custom":
37
+ return [
38
+ {
39
+ type: EventType.TEXT_MESSAGE_CHUNK,
40
+ role: "assistant",
41
+ delta: "hi",
42
+ } as BaseEvent,
43
+ ];
44
+ }
45
+ }
46
+
47
+ function emptyStreamData(
48
+ type: AgentType,
49
+ ): MockStreamEvent[] | Record<string, unknown>[] | BaseEvent[] {
50
+ switch (type) {
51
+ case "aisdk":
52
+ return [finish()];
53
+ case "tanstack":
54
+ return [];
55
+ case "custom":
56
+ return [];
57
+ }
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Parameterized test suites
62
+ // ---------------------------------------------------------------------------
63
+
64
+ describe.each(allTypes)("Agent [%s]", (type) => {
65
+ // -------------------------------------------------------------------------
66
+ // Lifecycle
67
+ // -------------------------------------------------------------------------
68
+ describe("lifecycle", () => {
69
+ it("emits RUN_STARTED as the first event with correct threadId/runId", async () => {
70
+ const agent = createAgent(type, minimalStreamData(type));
71
+ const input = createDefaultInput({ threadId: "t1", runId: "r1" });
72
+ const events = await collectEvents(agent.run(input));
73
+
74
+ expect(events.length).toBeGreaterThanOrEqual(2);
75
+ const first = events[0];
76
+ expect(first.type).toBe(EventType.RUN_STARTED);
77
+ expect(eventField<string>(first, "threadId")).toBe("t1");
78
+ expect(eventField<string>(first, "runId")).toBe("r1");
79
+ });
80
+
81
+ it("emits RUN_FINISHED as the last event with correct threadId/runId", async () => {
82
+ const agent = createAgent(type, minimalStreamData(type));
83
+ const input = createDefaultInput({ threadId: "t2", runId: "r2" });
84
+ const events = await collectEvents(agent.run(input));
85
+
86
+ const last = events[events.length - 1];
87
+ expect(last.type).toBe(EventType.RUN_FINISHED);
88
+ expect(eventField<string>(last, "threadId")).toBe("t2");
89
+ expect(eventField<string>(last, "runId")).toBe("r2");
90
+ });
91
+
92
+ it("emits RUN_FINISHED for an empty stream", async () => {
93
+ const agent = createAgent(type, emptyStreamData(type));
94
+ const input = createDefaultInput();
95
+ const events = await collectEvents(agent.run(input));
96
+
97
+ expect(events.length).toBeGreaterThanOrEqual(2);
98
+ expect(events[0].type).toBe(EventType.RUN_STARTED);
99
+ expect(events[events.length - 1].type).toBe(EventType.RUN_FINISHED);
100
+ });
101
+
102
+ it("wraps content with lifecycle events", async () => {
103
+ const agent = createAgent(type, minimalStreamData(type));
104
+ const input = createDefaultInput({ threadId: "wrap-t", runId: "wrap-r" });
105
+ const events = await collectEvents(agent.run(input));
106
+
107
+ expectLifecycleWrapped(events, "wrap-t", "wrap-r");
108
+
109
+ // There should be content events between the lifecycle bookends
110
+ const contentEvents = events.slice(1, -1);
111
+ expect(contentEvents.length).toBeGreaterThan(0);
112
+ for (const e of contentEvents) {
113
+ expect(e.type).not.toBe(EventType.RUN_STARTED);
114
+ expect(e.type).not.toBe(EventType.RUN_FINISHED);
115
+ }
116
+ });
117
+ });
118
+
119
+ // -------------------------------------------------------------------------
120
+ // RUN_ERROR
121
+ // -------------------------------------------------------------------------
122
+ describe("RUN_ERROR", () => {
123
+ it("emits RUN_ERROR when factory throws", async () => {
124
+ const agent = createThrowingAgent(type, "factory-boom");
125
+ const input = createDefaultInput();
126
+ const { events, errored } = await collectEventsIncludingErrors(
127
+ agent.run(input),
128
+ );
129
+
130
+ expect(errored).toBe(true);
131
+ const errorEvents = events.filter((e) => e.type === EventType.RUN_ERROR);
132
+ expect(errorEvents.length).toBe(1);
133
+ expect(eventField<string>(errorEvents[0], "message")).toBe(
134
+ "factory-boom",
135
+ );
136
+ });
137
+
138
+ it("emits RUN_ERROR when stream throws mid-iteration", async () => {
139
+ const agent = createMidStreamErrorAgent(type, "mid-stream-boom");
140
+ const input = createDefaultInput();
141
+ const { events, errored } = await collectEventsIncludingErrors(
142
+ agent.run(input),
143
+ );
144
+
145
+ expect(errored).toBe(true);
146
+ const errorEvents = events.filter((e) => e.type === EventType.RUN_ERROR);
147
+ expect(errorEvents.length).toBe(1);
148
+ expect(eventField<string>(errorEvents[0], "message")).toBe(
149
+ "mid-stream-boom",
150
+ );
151
+ });
152
+
153
+ it("does not emit RUN_FINISHED after RUN_ERROR", async () => {
154
+ const agent = createThrowingAgent(type, "no-finish");
155
+ const input = createDefaultInput();
156
+ const { events } = await collectEventsIncludingErrors(agent.run(input));
157
+
158
+ const errorIdx = events.findIndex((e) => e.type === EventType.RUN_ERROR);
159
+ expect(errorIdx).toBeGreaterThanOrEqual(0);
160
+
161
+ const eventsAfterError = events.slice(errorIdx + 1);
162
+ const finishAfterError = eventsAfterError.filter(
163
+ (e) => e.type === EventType.RUN_FINISHED,
164
+ );
165
+ expect(finishAfterError.length).toBe(0);
166
+ });
167
+ });
168
+
169
+ // -------------------------------------------------------------------------
170
+ // Abort
171
+ // -------------------------------------------------------------------------
172
+ describe("abort", () => {
173
+ it("completes without error after abortRun()", async () => {
174
+ // Use a signal to synchronize: abort after the first chunk is emitted
175
+ let emittedFirstChunk: () => void;
176
+ const firstChunkEmitted = new Promise<void>(
177
+ (r) => (emittedFirstChunk = r),
178
+ );
179
+
180
+ let config: BuiltInAgentFactoryConfig;
181
+ switch (type) {
182
+ case "aisdk":
183
+ config = {
184
+ type: "aisdk",
185
+ factory: ({ abortSignal }: AgentFactoryContext) => ({
186
+ fullStream: (async function* () {
187
+ yield { type: "text-delta", text: "tick" };
188
+ emittedFirstChunk();
189
+ // Wait for abort — use a promise that resolves on abort
190
+ await new Promise<void>((r) => {
191
+ if (abortSignal.aborted) return r();
192
+ abortSignal.addEventListener("abort", () => r(), {
193
+ once: true,
194
+ });
195
+ });
196
+ })(),
197
+ }),
198
+ };
199
+ break;
200
+ case "tanstack":
201
+ config = {
202
+ type: "tanstack",
203
+ factory: ({ abortSignal }: AgentFactoryContext) => ({
204
+ [Symbol.asyncIterator]: async function* () {
205
+ yield { type: "TEXT_MESSAGE_CONTENT", delta: "tick" };
206
+ emittedFirstChunk();
207
+ await new Promise<void>((r) => {
208
+ if (abortSignal.aborted) return r();
209
+ abortSignal.addEventListener("abort", () => r(), {
210
+ once: true,
211
+ });
212
+ });
213
+ },
214
+ }),
215
+ };
216
+ break;
217
+ case "custom":
218
+ config = {
219
+ type: "custom",
220
+ factory: ({ abortSignal }: AgentFactoryContext) => ({
221
+ [Symbol.asyncIterator]: async function* () {
222
+ yield {
223
+ type: EventType.TEXT_MESSAGE_CHUNK,
224
+ role: "assistant",
225
+ delta: "tick",
226
+ } as BaseEvent;
227
+ emittedFirstChunk();
228
+ await new Promise<void>((r) => {
229
+ if (abortSignal.aborted) return r();
230
+ abortSignal.addEventListener("abort", () => r(), {
231
+ once: true,
232
+ });
233
+ });
234
+ },
235
+ }),
236
+ };
237
+ break;
238
+ }
239
+
240
+ const agent = new BuiltInAgent(config);
241
+ const input = createDefaultInput();
242
+
243
+ const completed = await new Promise<boolean>((resolve) => {
244
+ agent.run(input).subscribe({
245
+ next: () => {},
246
+ error: () => resolve(false),
247
+ complete: () => resolve(true),
248
+ });
249
+
250
+ // Wait for the first chunk to be emitted, then abort
251
+ firstChunkEmitted.then(() => agent.abortRun());
252
+ });
253
+
254
+ expect(completed).toBe(true);
255
+ });
256
+ });
257
+
258
+ // -------------------------------------------------------------------------
259
+ // Factory Context
260
+ // -------------------------------------------------------------------------
261
+ describe("factory context", () => {
262
+ it("receives correct input with threadId, runId, and forwardedProps", async () => {
263
+ let capturedCtx: AgentFactoryContext | null = null;
264
+
265
+ let config: BuiltInAgentFactoryConfig;
266
+ switch (type) {
267
+ case "aisdk":
268
+ config = {
269
+ type: "aisdk",
270
+ factory: (ctx: AgentFactoryContext) => {
271
+ capturedCtx = ctx;
272
+ return {
273
+ fullStream: (async function* () {
274
+ yield { type: "finish", finishReason: "stop" };
275
+ })(),
276
+ };
277
+ },
278
+ };
279
+ break;
280
+ case "tanstack":
281
+ config = {
282
+ type: "tanstack",
283
+ factory: (ctx: AgentFactoryContext) => {
284
+ capturedCtx = ctx;
285
+ return (async function* () {
286
+ // empty stream
287
+ })();
288
+ },
289
+ };
290
+ break;
291
+ case "custom":
292
+ config = {
293
+ type: "custom",
294
+ factory: (ctx: AgentFactoryContext) => {
295
+ capturedCtx = ctx;
296
+ return (async function* () {
297
+ // empty stream
298
+ })();
299
+ },
300
+ };
301
+ break;
302
+ }
303
+
304
+ const agent = new BuiltInAgent(config);
305
+ const input = createDefaultInput({
306
+ threadId: "ctx-thread",
307
+ runId: "ctx-run",
308
+ forwardedProps: { model: "gpt-4" },
309
+ });
310
+
311
+ await collectEvents(agent.run(input));
312
+
313
+ expect(capturedCtx).not.toBeNull();
314
+ expect(capturedCtx!.input.threadId).toBe("ctx-thread");
315
+ expect(capturedCtx!.input.runId).toBe("ctx-run");
316
+ expect(capturedCtx!.input.forwardedProps).toEqual({ model: "gpt-4" });
317
+ });
318
+
319
+ it("receives abortController and abortSignal", async () => {
320
+ let capturedCtx: AgentFactoryContext | null = null;
321
+
322
+ let config: BuiltInAgentFactoryConfig;
323
+ switch (type) {
324
+ case "aisdk":
325
+ config = {
326
+ type: "aisdk",
327
+ factory: (ctx: AgentFactoryContext) => {
328
+ capturedCtx = ctx;
329
+ return {
330
+ fullStream: (async function* () {
331
+ yield { type: "finish", finishReason: "stop" };
332
+ })(),
333
+ };
334
+ },
335
+ };
336
+ break;
337
+ case "tanstack":
338
+ config = {
339
+ type: "tanstack",
340
+ factory: (ctx: AgentFactoryContext) => {
341
+ capturedCtx = ctx;
342
+ return (async function* () {
343
+ // empty
344
+ })();
345
+ },
346
+ };
347
+ break;
348
+ case "custom":
349
+ config = {
350
+ type: "custom",
351
+ factory: (ctx: AgentFactoryContext) => {
352
+ capturedCtx = ctx;
353
+ return (async function* () {
354
+ // empty
355
+ })();
356
+ },
357
+ };
358
+ break;
359
+ }
360
+
361
+ const agent = new BuiltInAgent(config);
362
+ const input = createDefaultInput();
363
+ await collectEvents(agent.run(input));
364
+
365
+ expect(capturedCtx!.abortController).toBeInstanceOf(AbortController);
366
+ expect(capturedCtx!.abortSignal).toBe(
367
+ capturedCtx!.abortController.signal,
368
+ );
369
+ });
370
+ });
371
+
372
+ // -------------------------------------------------------------------------
373
+ // clone()
374
+ // -------------------------------------------------------------------------
375
+ describe("clone()", () => {
376
+ it("returns a new Agent instance (not the same reference)", () => {
377
+ const agent = createAgent(type, minimalStreamData(type));
378
+ const cloned = agent.clone();
379
+
380
+ expect(cloned).toBeInstanceOf(BuiltInAgent);
381
+ expect(cloned).not.toBe(agent);
382
+ });
383
+
384
+ it("produces correct lifecycle events from a cloned agent", async () => {
385
+ const agent = createAgent(type, minimalStreamData(type));
386
+ const cloned = agent.clone();
387
+ const input = createDefaultInput({
388
+ threadId: "clone-t",
389
+ runId: "clone-r",
390
+ });
391
+
392
+ const events = await collectEvents(cloned.run(input));
393
+
394
+ expectLifecycleWrapped(events, "clone-t", "clone-r");
395
+ });
396
+ });
397
+ });
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Type Discrimination (NOT parameterized)
401
+ // ---------------------------------------------------------------------------
402
+
403
+ describe("Agent type discrimination", () => {
404
+ it('"aisdk" routes to AI SDK converter and produces text content', async () => {
405
+ const agent = createAgent("aisdk", [
406
+ textDelta("hello from aisdk"),
407
+ finish(),
408
+ ]);
409
+ const input = createDefaultInput();
410
+ const events = await collectEvents(agent.run(input));
411
+
412
+ const textEvents = events.filter(
413
+ (e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
414
+ );
415
+ expect(textEvents.length).toBe(1);
416
+ expect(eventField<string>(textEvents[0], "delta")).toBe("hello from aisdk");
417
+ });
418
+
419
+ it('"tanstack" routes to TanStack converter and produces text content', async () => {
420
+ const agent = createAgent("tanstack", [
421
+ tanstackTextChunk("hello from tanstack"),
422
+ ]);
423
+ const input = createDefaultInput();
424
+ const events = await collectEvents(agent.run(input));
425
+
426
+ const textEvents = events.filter(
427
+ (e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
428
+ );
429
+ expect(textEvents.length).toBe(1);
430
+ expect(eventField<string>(textEvents[0], "delta")).toBe(
431
+ "hello from tanstack",
432
+ );
433
+ });
434
+
435
+ it('"custom" forwards events directly without conversion', async () => {
436
+ const customEvent: BaseEvent = {
437
+ type: EventType.TEXT_MESSAGE_CHUNK,
438
+ role: "assistant",
439
+ delta: "hello from custom",
440
+ } as BaseEvent;
441
+
442
+ const agent = createAgent("custom", [customEvent]);
443
+ const input = createDefaultInput();
444
+ const events = await collectEvents(agent.run(input));
445
+
446
+ const textEvents = events.filter(
447
+ (e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
448
+ );
449
+ expect(textEvents.length).toBe(1);
450
+ expect(eventField<string>(textEvents[0], "delta")).toBe(
451
+ "hello from custom",
452
+ );
453
+ });
454
+ });
455
+
456
+ // ---------------------------------------------------------------------------
457
+ // Async Factory (Promise-returning)
458
+ // ---------------------------------------------------------------------------
459
+
460
+ describe("Async factory (Promise-returning)", () => {
461
+ it("aisdk: async factory resolves and streams correctly", async () => {
462
+ const agent = new BuiltInAgent({
463
+ type: "aisdk",
464
+ factory: async () => {
465
+ // Simulate async setup (e.g., fetching API key)
466
+ await new Promise((r) => setTimeout(r, 5));
467
+ return {
468
+ fullStream: (async function* () {
469
+ yield { type: "text-delta", text: "async-aisdk" };
470
+ yield { type: "finish", finishReason: "stop" };
471
+ })(),
472
+ };
473
+ },
474
+ });
475
+ const input = createDefaultInput();
476
+ const events = await collectEvents(agent.run(input));
477
+
478
+ expectLifecycleWrapped(events);
479
+ const textEvents = events.filter(
480
+ (e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
481
+ );
482
+ expect(textEvents).toHaveLength(1);
483
+ expect(eventField<string>(textEvents[0], "delta")).toBe("async-aisdk");
484
+ });
485
+
486
+ it("tanstack: async factory resolves and streams correctly", async () => {
487
+ const agent = new BuiltInAgent({
488
+ type: "tanstack",
489
+ factory: async () => {
490
+ await new Promise((r) => setTimeout(r, 5));
491
+ return (async function* () {
492
+ yield { type: "TEXT_MESSAGE_CONTENT", delta: "async-tanstack" };
493
+ })();
494
+ },
495
+ });
496
+ const input = createDefaultInput();
497
+ const events = await collectEvents(agent.run(input));
498
+
499
+ expectLifecycleWrapped(events);
500
+ const textEvents = events.filter(
501
+ (e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
502
+ );
503
+ expect(textEvents).toHaveLength(1);
504
+ expect(eventField<string>(textEvents[0], "delta")).toBe("async-tanstack");
505
+ });
506
+
507
+ it("custom: async factory resolves and streams correctly", async () => {
508
+ const agent = new BuiltInAgent({
509
+ type: "custom",
510
+ factory: async () => {
511
+ await new Promise((r) => setTimeout(r, 5));
512
+ return (async function* () {
513
+ yield {
514
+ type: EventType.TEXT_MESSAGE_CHUNK,
515
+ role: "assistant",
516
+ delta: "async-custom",
517
+ } as BaseEvent;
518
+ })();
519
+ },
520
+ });
521
+ const input = createDefaultInput();
522
+ const events = await collectEvents(agent.run(input));
523
+
524
+ expectLifecycleWrapped(events);
525
+ const textEvents = events.filter(
526
+ (e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
527
+ );
528
+ expect(textEvents).toHaveLength(1);
529
+ expect(eventField<string>(textEvents[0], "delta")).toBe("async-custom");
530
+ });
531
+ });
532
+
533
+ // ---------------------------------------------------------------------------
534
+ // RUN_ERROR includes threadId and runId
535
+ // ---------------------------------------------------------------------------
536
+
537
+ describe("RUN_ERROR correlation fields", () => {
538
+ it("RUN_ERROR includes threadId and runId for run correlation", async () => {
539
+ const agent = new BuiltInAgent({
540
+ type: "aisdk",
541
+ factory: () => {
542
+ throw new Error("test-error");
543
+ },
544
+ });
545
+ const input = createDefaultInput({
546
+ threadId: "err-thread",
547
+ runId: "err-run",
548
+ });
549
+ const { events, errored } = await collectEventsIncludingErrors(
550
+ agent.run(input),
551
+ );
552
+
553
+ expect(errored).toBe(true);
554
+ const errorEvents = events.filter((e) => e.type === EventType.RUN_ERROR);
555
+ expect(errorEvents).toHaveLength(1);
556
+ expect(eventField<string>(errorEvents[0], "threadId")).toBe("err-thread");
557
+ expect(eventField<string>(errorEvents[0], "runId")).toBe("err-run");
558
+ });
559
+ });
560
+
561
+ // ---------------------------------------------------------------------------
562
+ // Concurrent run guard
563
+ // ---------------------------------------------------------------------------
564
+
565
+ describe("Concurrent run guard", () => {
566
+ it("throws when run() is called while another run is in progress", async () => {
567
+ let resolveFactory: () => void;
568
+ const factoryBlocked = new Promise<void>((r) => (resolveFactory = r));
569
+
570
+ const agent = new BuiltInAgent({
571
+ type: "custom",
572
+ factory: async function* ({ abortSignal }) {
573
+ // Block until resolved externally
574
+ await new Promise<void>((r) => {
575
+ if (abortSignal.aborted) return r();
576
+ abortSignal.addEventListener("abort", () => r(), { once: true });
577
+ factoryBlocked.then(() => r());
578
+ });
579
+ },
580
+ });
581
+ const input = createDefaultInput();
582
+
583
+ // Start first run — abortController is now set synchronously in run()
584
+ const sub = agent.run(input).subscribe({ next: () => {} });
585
+
586
+ // Second run should throw immediately (no timing dependency)
587
+ expect(() => agent.run(input)).toThrow("Agent is already running");
588
+
589
+ // Cleanup
590
+ resolveFactory!();
591
+ sub.unsubscribe();
592
+ });
593
+ });