@copilotkitnext/sqlite-runner 0.0.0-max-changeset-20260109174803

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.
@@ -0,0 +1,765 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { SqliteAgentRunner } from "..";
3
+ import {
4
+ AbstractAgent,
5
+ BaseEvent,
6
+ EventType,
7
+ Message,
8
+ RunAgentInput,
9
+ RunErrorEvent,
10
+ RunFinishedEvent,
11
+ RunStartedEvent,
12
+ } from "@ag-ui/client";
13
+ import { EMPTY, Subscription, firstValueFrom, from } from "rxjs";
14
+ import { toArray } from "rxjs/operators";
15
+
16
+ type RunCallbacks = {
17
+ onEvent: (event: { event: BaseEvent }) => void;
18
+ onNewMessage?: (args: { message: Message }) => void;
19
+ onRunStartedEvent?: () => void;
20
+ };
21
+
22
+ const createdRunners: SqliteAgentRunner[] = [];
23
+
24
+ afterEach(() => {
25
+ while (createdRunners.length > 0) {
26
+ const runner = createdRunners.pop();
27
+ runner?.close();
28
+ }
29
+ });
30
+
31
+ function createRunner(): SqliteAgentRunner {
32
+ const runner = new SqliteAgentRunner();
33
+ createdRunners.push(runner);
34
+ return runner;
35
+ }
36
+
37
+ interface EmitAgentOptions {
38
+ events?: BaseEvent[];
39
+ emitDefaultRunStarted?: boolean;
40
+ includeRunFinished?: boolean;
41
+ runFinishedEvent?: RunFinishedEvent;
42
+ afterEvent?: (args: { event: BaseEvent; index: number }) => void | Promise<void>;
43
+ }
44
+
45
+ class EmitAgent extends AbstractAgent {
46
+ constructor(private readonly options: EmitAgentOptions = {}) {
47
+ super();
48
+ }
49
+
50
+ async runAgent(input: RunAgentInput, callbacks: RunCallbacks): Promise<void> {
51
+ const {
52
+ emitDefaultRunStarted = true,
53
+ includeRunFinished = true,
54
+ runFinishedEvent,
55
+ afterEvent,
56
+ } = this.options;
57
+ const scriptedEvents = this.options.events ?? [];
58
+
59
+ let index = 0;
60
+ const emit = async (event: BaseEvent) => {
61
+ callbacks.onEvent({ event });
62
+ if (event.type === EventType.RUN_STARTED) {
63
+ callbacks.onRunStartedEvent?.();
64
+ }
65
+ await afterEvent?.({ event, index });
66
+ index += 1;
67
+ };
68
+
69
+ if (emitDefaultRunStarted) {
70
+ const runStarted: RunStartedEvent = {
71
+ type: EventType.RUN_STARTED,
72
+ threadId: input.threadId,
73
+ runId: input.runId,
74
+ parentRunId: input.parentRunId,
75
+ };
76
+ await emit(runStarted);
77
+ }
78
+
79
+ for (const event of scriptedEvents) {
80
+ await emit(event);
81
+ }
82
+
83
+ const hasRunFinishedEvent =
84
+ scriptedEvents.some((event) => event.type === EventType.RUN_FINISHED) ||
85
+ runFinishedEvent?.type === EventType.RUN_FINISHED;
86
+
87
+ if (includeRunFinished && !hasRunFinishedEvent) {
88
+ const finishEvent: RunFinishedEvent =
89
+ runFinishedEvent ?? {
90
+ type: EventType.RUN_FINISHED,
91
+ threadId: input.threadId,
92
+ runId: input.runId,
93
+ };
94
+ await emit(finishEvent);
95
+ }
96
+ }
97
+
98
+ clone(): AbstractAgent {
99
+ return new EmitAgent({
100
+ ...this.options,
101
+ events: this.options.events ? [...this.options.events] : undefined,
102
+ });
103
+ }
104
+
105
+ protected run(): ReturnType<AbstractAgent["run"]> {
106
+ return EMPTY;
107
+ }
108
+
109
+ protected connect(): ReturnType<AbstractAgent["connect"]> {
110
+ return EMPTY;
111
+ }
112
+ }
113
+
114
+ class ReplayAgent extends AbstractAgent {
115
+ constructor(private readonly replayEvents: BaseEvent[], threadId: string) {
116
+ super({ threadId });
117
+ }
118
+
119
+ async runAgent(): Promise<void> {
120
+ throw new Error("not used");
121
+ }
122
+
123
+ protected run(): ReturnType<AbstractAgent["run"]> {
124
+ return EMPTY;
125
+ }
126
+
127
+ protected connect(): ReturnType<AbstractAgent["connect"]> {
128
+ return from(this.replayEvents);
129
+ }
130
+ }
131
+
132
+ class RunnerConnectAgent extends AbstractAgent {
133
+ constructor(private readonly runner: SqliteAgentRunner, threadId: string) {
134
+ super({ threadId });
135
+ }
136
+
137
+ async runAgent(): Promise<void> {
138
+ throw new Error("not used");
139
+ }
140
+
141
+ protected run(): ReturnType<AbstractAgent["run"]> {
142
+ return EMPTY;
143
+ }
144
+
145
+ protected connect(input: RunAgentInput): ReturnType<AbstractAgent["connect"]> {
146
+ return this.runner.connect({ threadId: input.threadId });
147
+ }
148
+ }
149
+
150
+ type Deferred<T> = {
151
+ promise: Promise<T>;
152
+ resolve: (value: T) => void;
153
+ reject: (reason?: unknown) => void;
154
+ };
155
+
156
+ function createDeferred<T>(): Deferred<T> {
157
+ let resolve!: (value: T) => void;
158
+ let reject!: (reason?: unknown) => void;
159
+ const promise = new Promise<T>((res, rej) => {
160
+ resolve = res;
161
+ reject = rej;
162
+ });
163
+ return { promise, resolve, reject };
164
+ }
165
+
166
+ async function collectEvents(observable: ReturnType<SqliteAgentRunner["run"]> | ReturnType<SqliteAgentRunner["connect"]>) {
167
+ return firstValueFrom(observable.pipe(toArray()));
168
+ }
169
+
170
+ function createRunInput({
171
+ threadId,
172
+ runId,
173
+ messages,
174
+ state,
175
+ parentRunId,
176
+ }: {
177
+ threadId: string;
178
+ runId: string;
179
+ messages: Message[];
180
+ state?: Record<string, unknown>;
181
+ parentRunId?: string | null;
182
+ }): RunAgentInput {
183
+ return {
184
+ threadId,
185
+ runId,
186
+ parentRunId: parentRunId ?? undefined,
187
+ state: state ?? {},
188
+ messages,
189
+ tools: [],
190
+ context: [],
191
+ forwardedProps: undefined,
192
+ };
193
+ }
194
+
195
+ function expectRunStartedEvent(event: BaseEvent, expectedMessages: Message[]) {
196
+ expect(event.type).toBe(EventType.RUN_STARTED);
197
+ const runStarted = event as RunStartedEvent;
198
+ expect(runStarted.input?.messages).toEqual(expectedMessages);
199
+ }
200
+
201
+ function createTextMessageEvents({
202
+ messageId,
203
+ role = "assistant",
204
+ content,
205
+ }: {
206
+ messageId: string;
207
+ role?: "assistant" | "developer" | "system" | "user";
208
+ content: string;
209
+ }): BaseEvent[] {
210
+ return [
211
+ {
212
+ type: EventType.TEXT_MESSAGE_START,
213
+ messageId,
214
+ role,
215
+ },
216
+ {
217
+ type: EventType.TEXT_MESSAGE_CONTENT,
218
+ messageId,
219
+ delta: content,
220
+ },
221
+ {
222
+ type: EventType.TEXT_MESSAGE_END,
223
+ messageId,
224
+ },
225
+ ] as BaseEvent[];
226
+ }
227
+
228
+ function createToolCallEvents({
229
+ toolCallId,
230
+ parentMessageId,
231
+ toolName,
232
+ argsJson,
233
+ resultMessageId,
234
+ resultContent,
235
+ }: {
236
+ toolCallId: string;
237
+ parentMessageId: string;
238
+ toolName: string;
239
+ argsJson: string;
240
+ resultMessageId: string;
241
+ resultContent: string;
242
+ }): BaseEvent[] {
243
+ return [
244
+ {
245
+ type: EventType.TOOL_CALL_START,
246
+ toolCallId,
247
+ toolCallName: toolName,
248
+ parentMessageId,
249
+ },
250
+ {
251
+ type: EventType.TOOL_CALL_ARGS,
252
+ toolCallId,
253
+ delta: argsJson,
254
+ },
255
+ {
256
+ type: EventType.TOOL_CALL_END,
257
+ toolCallId,
258
+ },
259
+ {
260
+ type: EventType.TOOL_CALL_RESULT,
261
+ toolCallId,
262
+ messageId: resultMessageId,
263
+ content: resultContent,
264
+ role: "tool",
265
+ },
266
+ ] as BaseEvent[];
267
+ }
268
+
269
+ describe("SqliteAgentRunner e2e", () => {
270
+ describe("Fresh Replay After Single Run", () => {
271
+ it("replays sanitized message history on connectAgent", async () => {
272
+ const runner = createRunner();
273
+ const threadId = "thread-fresh-replay";
274
+ const existingMessage: Message = {
275
+ id: "message-existing",
276
+ role: "user",
277
+ content: "Hello there",
278
+ };
279
+
280
+ const runEvents = await collectEvents(
281
+ runner.run({
282
+ threadId,
283
+ agent: new EmitAgent(),
284
+ input: createRunInput({
285
+ threadId,
286
+ runId: "run-0",
287
+ messages: [existingMessage],
288
+ }),
289
+ }),
290
+ );
291
+
292
+ expectRunStartedEvent(runEvents[0], [existingMessage]);
293
+ expect(runEvents.at(-1)?.type).toBe(EventType.RUN_FINISHED);
294
+
295
+ const replayEvents = await collectEvents(runner.connect({ threadId }));
296
+ const replayAgent = new ReplayAgent(replayEvents, threadId);
297
+ await replayAgent.connectAgent({ runId: "replay-run" });
298
+
299
+ expect(replayAgent.messages).toEqual([existingMessage]);
300
+ });
301
+ });
302
+
303
+ describe("New Messages on Subsequent Runs", () => {
304
+ it("merges new message IDs without duplicating history", async () => {
305
+ const runner = createRunner();
306
+ const threadId = "thread-subsequent-runs";
307
+ const existingMessage: Message = {
308
+ id: "msg-existing",
309
+ role: "user",
310
+ content: "First turn",
311
+ };
312
+
313
+ const initialRunEvents = await collectEvents(
314
+ runner.run({
315
+ threadId,
316
+ agent: new EmitAgent(),
317
+ input: createRunInput({
318
+ threadId,
319
+ runId: "run-0",
320
+ messages: [existingMessage],
321
+ }),
322
+ }),
323
+ );
324
+ expectRunStartedEvent(initialRunEvents[0], [existingMessage]);
325
+
326
+ const newMessage: Message = {
327
+ id: "msg-new",
328
+ role: "user",
329
+ content: "Second turn",
330
+ };
331
+
332
+ const secondRunEvents = await collectEvents(
333
+ runner.run({
334
+ threadId,
335
+ agent: new EmitAgent(),
336
+ input: createRunInput({
337
+ threadId,
338
+ runId: "run-1",
339
+ messages: [existingMessage, newMessage],
340
+ }),
341
+ }),
342
+ );
343
+
344
+ expectRunStartedEvent(secondRunEvents[0], [newMessage]);
345
+
346
+ const replayEvents = await collectEvents(runner.connect({ threadId }));
347
+ const replayAgent = new ReplayAgent(replayEvents, threadId);
348
+ await replayAgent.connectAgent({ runId: "replay-run" });
349
+
350
+ expect(replayAgent.messages).toEqual([existingMessage, newMessage]);
351
+ expect(new Set(replayAgent.messages.map((message) => message.id)).size).toBe(
352
+ replayAgent.messages.length,
353
+ );
354
+ });
355
+ });
356
+
357
+ describe("Fresh Agent Connection After Prior Runs", () => {
358
+ it("hydrates a brand-new agent via connect()", async () => {
359
+ const runner = createRunner();
360
+ const threadId = "thread-new-agent-connection";
361
+ const existingMessage: Message = {
362
+ id: "existing-connection",
363
+ role: "user",
364
+ content: "Persist me",
365
+ };
366
+
367
+ const runEvents = await collectEvents(
368
+ runner.run({
369
+ threadId,
370
+ agent: new EmitAgent(),
371
+ input: createRunInput({
372
+ threadId,
373
+ runId: "run-0",
374
+ messages: [existingMessage],
375
+ }),
376
+ }),
377
+ );
378
+ expectRunStartedEvent(runEvents[0], [existingMessage]);
379
+
380
+ const connectingAgent = new RunnerConnectAgent(runner, threadId);
381
+ await connectingAgent.connectAgent({ runId: "connect-run" });
382
+
383
+ expect(connectingAgent.messages).toEqual([existingMessage]);
384
+ });
385
+ });
386
+
387
+ describe("Mixed Roles and Tool Results", () => {
388
+ it("preserves agent-emitted tool events alongside heterogeneous inputs", async () => {
389
+ const runner = createRunner();
390
+ const threadId = "thread-mixed-roles";
391
+
392
+ const systemMessage: Message = {
393
+ id: "sys-1",
394
+ role: "system",
395
+ content: "Global directive",
396
+ };
397
+ const developerMessage: Message = {
398
+ id: "dev-1",
399
+ role: "developer",
400
+ content: "Internal guidance",
401
+ };
402
+ const userMessage: Message = {
403
+ id: "user-1",
404
+ role: "user",
405
+ content: "Need the weather",
406
+ };
407
+ const baseMessages = [systemMessage, developerMessage, userMessage];
408
+
409
+ const assistantMessageId = "assistant-1";
410
+ const toolCallId = "tool-call-1";
411
+ const toolMessageId = "tool-msg-1";
412
+
413
+ const agentEvents: BaseEvent[] = [
414
+ ...createTextMessageEvents({
415
+ messageId: assistantMessageId,
416
+ content: "Calling the weather tool",
417
+ }),
418
+ ...createToolCallEvents({
419
+ toolCallId,
420
+ parentMessageId: assistantMessageId,
421
+ toolName: "getWeather",
422
+ argsJson: '{"location":"NYC"}',
423
+ resultMessageId: toolMessageId,
424
+ resultContent: '{"temp":72}',
425
+ }),
426
+ ];
427
+
428
+ const runEvents = await collectEvents(
429
+ runner.run({
430
+ threadId,
431
+ agent: new EmitAgent({ events: agentEvents }),
432
+ input: createRunInput({
433
+ threadId,
434
+ runId: "run-0",
435
+ messages: baseMessages,
436
+ }),
437
+ }),
438
+ );
439
+
440
+ expectRunStartedEvent(runEvents[0], baseMessages);
441
+ expect(runEvents.filter((event) => event.type === EventType.TOOL_CALL_RESULT)).toHaveLength(1);
442
+
443
+ const replayEvents = await collectEvents(runner.connect({ threadId }));
444
+ const replayAgent = new ReplayAgent(replayEvents, threadId);
445
+ await replayAgent.connectAgent({ runId: "replay-run" });
446
+
447
+ expect(replayAgent.messages).toEqual([
448
+ systemMessage,
449
+ developerMessage,
450
+ userMessage,
451
+ {
452
+ id: assistantMessageId,
453
+ role: "assistant",
454
+ content: "Calling the weather tool",
455
+ toolCalls: [
456
+ {
457
+ id: toolCallId,
458
+ type: "function",
459
+ function: {
460
+ name: "getWeather",
461
+ arguments: '{"location":"NYC"}',
462
+ },
463
+ },
464
+ ],
465
+ },
466
+ {
467
+ id: toolMessageId,
468
+ role: "tool",
469
+ content: '{"temp":72}',
470
+ toolCallId,
471
+ },
472
+ ]);
473
+ expect(replayAgent.messages.filter((message) => message.role === "tool")).toHaveLength(1);
474
+ });
475
+ });
476
+
477
+ describe("Multiple Consecutive Runs with Agent Output", () => {
478
+ it("deduplicates input history while emitting each agent message once", async () => {
479
+ const runner = createRunner();
480
+ const threadId = "thread-multi-runs";
481
+ const systemMessage: Message = {
482
+ id: "system-shared",
483
+ role: "system",
484
+ content: "System context",
485
+ };
486
+ const userMessages: Message[] = [];
487
+
488
+ for (let index = 0; index < 3; index += 1) {
489
+ const userMessage: Message = {
490
+ id: `user-${index + 1}`,
491
+ role: "user",
492
+ content: `User message ${index + 1}`,
493
+ };
494
+ userMessages.push(userMessage);
495
+
496
+ const messagesForRun = [systemMessage, ...userMessages];
497
+ const assistantId = `assistant-${index + 1}`;
498
+ const toolCallId = `tool-call-${index + 1}`;
499
+ const toolMessageId = `tool-msg-${index + 1}`;
500
+
501
+ const events: BaseEvent[] = [
502
+ ...createTextMessageEvents({
503
+ messageId: assistantId,
504
+ content: `Assistant reply ${index + 1}`,
505
+ }),
506
+ ...createToolCallEvents({
507
+ toolCallId,
508
+ parentMessageId: assistantId,
509
+ toolName: `tool-${index + 1}`,
510
+ argsJson: `{"step":${index + 1}}`,
511
+ resultMessageId: toolMessageId,
512
+ resultContent: `{"ok":${index + 1}}`,
513
+ }),
514
+ ];
515
+
516
+ const runEvents = await collectEvents(
517
+ runner.run({
518
+ threadId,
519
+ agent: new EmitAgent({ events }),
520
+ input: createRunInput({
521
+ threadId,
522
+ runId: `run-${index}`,
523
+ messages: messagesForRun,
524
+ }),
525
+ }),
526
+ );
527
+
528
+ if (index === 0) {
529
+ expectRunStartedEvent(runEvents[0], messagesForRun);
530
+ } else {
531
+ expectRunStartedEvent(runEvents[0], [userMessage]);
532
+ }
533
+ expect(runEvents.at(-1)?.type).toBe(EventType.RUN_FINISHED);
534
+ }
535
+
536
+ const replayEvents = await collectEvents(runner.connect({ threadId }));
537
+ const replayAgent = new ReplayAgent(replayEvents, threadId);
538
+ await replayAgent.connectAgent({ runId: "replay-final" });
539
+
540
+ const finalMessages = replayAgent.messages;
541
+ expect(new Set(finalMessages.map((message) => message.id)).size).toBe(finalMessages.length);
542
+ const roleCounts = finalMessages.reduce<Record<string, number>>((counts, message) => {
543
+ counts[message.role] = (counts[message.role] ?? 0) + 1;
544
+ return counts;
545
+ }, {});
546
+ expect(roleCounts.system).toBe(1);
547
+ expect(roleCounts.user).toBe(3);
548
+ expect(roleCounts.assistant).toBe(3);
549
+ expect(roleCounts.tool).toBe(3);
550
+ });
551
+ });
552
+
553
+ describe("Agent-Provided RUN_STARTED input", () => {
554
+ it("forwards the agent-specified payload without sanitizing", async () => {
555
+ const runner = createRunner();
556
+ const threadId = "thread-custom-run-started";
557
+ const runId = "run-0";
558
+
559
+ const customMessages: Message[] = [
560
+ {
561
+ id: "custom-user",
562
+ role: "user",
563
+ content: "Pre-sent content",
564
+ },
565
+ ];
566
+ const customInput: RunAgentInput = {
567
+ threadId,
568
+ runId,
569
+ parentRunId: undefined,
570
+ state: { injected: true },
571
+ messages: customMessages,
572
+ tools: [],
573
+ context: [],
574
+ forwardedProps: { source: "agent" },
575
+ };
576
+ const customRunStarted: RunStartedEvent = {
577
+ type: EventType.RUN_STARTED,
578
+ threadId,
579
+ runId,
580
+ parentRunId: null,
581
+ input: customInput,
582
+ };
583
+
584
+ const agentEvents: BaseEvent[] = [
585
+ customRunStarted,
586
+ ...createTextMessageEvents({
587
+ messageId: "agent-message",
588
+ content: "Custom start acknowledged",
589
+ }),
590
+ ];
591
+
592
+ const runEvents = await collectEvents(
593
+ runner.run({
594
+ threadId,
595
+ agent: new EmitAgent({
596
+ events: agentEvents,
597
+ emitDefaultRunStarted: false,
598
+ }),
599
+ input: createRunInput({
600
+ threadId,
601
+ runId,
602
+ messages: [],
603
+ }),
604
+ }),
605
+ );
606
+
607
+ expect(runEvents[0]).toEqual(customRunStarted);
608
+ expect(runEvents.filter((event) => event.type === EventType.RUN_FINISHED)).toHaveLength(1);
609
+
610
+ const replayEvents = await collectEvents(runner.connect({ threadId }));
611
+ const replayAgent = new ReplayAgent(replayEvents, threadId);
612
+ await replayAgent.connectAgent({ runId: "replay-run" });
613
+ expect(replayAgent.messages.find((message) => message.id === "custom-user")).toEqual(
614
+ customMessages[0],
615
+ );
616
+ });
617
+ });
618
+
619
+ describe("Concurrent Connections During Run", () => {
620
+ it("streams in-flight events to live subscribers and persists final history", async () => {
621
+ const runner = createRunner();
622
+ const threadId = "thread-concurrency";
623
+ const runId = "run-live";
624
+ const initialMessage: Message = {
625
+ id: "initial-user",
626
+ role: "user",
627
+ content: "Start run",
628
+ };
629
+
630
+ const runStartedSignal = createDeferred<void>();
631
+ const resumeSignal = createDeferred<void>();
632
+
633
+ const agent = new EmitAgent({
634
+ events: [
635
+ ...createTextMessageEvents({
636
+ messageId: "assistant-live",
637
+ content: "Streaming content",
638
+ }),
639
+ ],
640
+ afterEvent: async ({ event }) => {
641
+ if (event.type === EventType.RUN_STARTED) {
642
+ runStartedSignal.resolve();
643
+ await resumeSignal.promise;
644
+ }
645
+ },
646
+ });
647
+
648
+ const runEvents: BaseEvent[] = [];
649
+ const run$ = runner.run({
650
+ threadId,
651
+ agent,
652
+ input: createRunInput({
653
+ threadId,
654
+ runId,
655
+ messages: [initialMessage],
656
+ }),
657
+ });
658
+
659
+ let runSubscription: Subscription;
660
+ const runCompletion = new Promise<void>((resolve, reject) => {
661
+ runSubscription = run$.subscribe({
662
+ next: (event) => runEvents.push(event),
663
+ error: (error) => {
664
+ runSubscription.unsubscribe();
665
+ reject(error);
666
+ },
667
+ complete: () => {
668
+ runSubscription.unsubscribe();
669
+ resolve();
670
+ },
671
+ });
672
+ });
673
+
674
+ await runStartedSignal.promise;
675
+
676
+ const liveEvents: BaseEvent[] = [];
677
+ const connect$ = runner.connect({ threadId });
678
+ let connectSubscription: Subscription;
679
+ const connectCompletion = new Promise<void>((resolve, reject) => {
680
+ connectSubscription = connect$.subscribe({
681
+ next: (event) => liveEvents.push(event),
682
+ error: (error) => {
683
+ connectSubscription.unsubscribe();
684
+ reject(error);
685
+ },
686
+ complete: () => {
687
+ connectSubscription.unsubscribe();
688
+ resolve();
689
+ },
690
+ });
691
+ });
692
+
693
+ resumeSignal.resolve();
694
+
695
+ await Promise.all([runCompletion, connectCompletion]);
696
+
697
+ expectRunStartedEvent(runEvents[0], [initialMessage]);
698
+ expect(runEvents.at(-1)?.type).toBe(EventType.RUN_FINISHED);
699
+ expect(liveEvents).toEqual(runEvents);
700
+
701
+ const persistedEvents = await collectEvents(runner.connect({ threadId }));
702
+ expect(persistedEvents).toEqual(runEvents);
703
+
704
+ const replayAgent = new ReplayAgent(persistedEvents, threadId);
705
+ await replayAgent.connectAgent({ runId: "replay-run" });
706
+ expect(replayAgent.messages.map((message) => message.id)).toEqual([
707
+ initialMessage.id,
708
+ "assistant-live",
709
+ ]);
710
+ });
711
+ });
712
+
713
+ describe("Error Handling", () => {
714
+ it("propagates RUN_ERROR while retaining input history", async () => {
715
+ const runner = createRunner();
716
+ const threadId = "thread-run-error";
717
+ const userMessage: Message = {
718
+ id: "error-user",
719
+ role: "user",
720
+ content: "Trigger error",
721
+ };
722
+
723
+ const runErrorEvent: RunErrorEvent = {
724
+ type: EventType.RUN_ERROR,
725
+ message: "Agent failure",
726
+ };
727
+
728
+ const runEvents = await collectEvents(
729
+ runner.run({
730
+ threadId,
731
+ agent: new EmitAgent({
732
+ events: [runErrorEvent],
733
+ includeRunFinished: false,
734
+ }),
735
+ input: createRunInput({
736
+ threadId,
737
+ runId: "run-error",
738
+ messages: [userMessage],
739
+ }),
740
+ }),
741
+ );
742
+
743
+ expectRunStartedEvent(runEvents[0], [userMessage]);
744
+ expect(runEvents.at(-1)).toEqual(runErrorEvent);
745
+
746
+ const replayEvents = await collectEvents(runner.connect({ threadId }));
747
+ const replayAgent = new ReplayAgent(replayEvents, threadId);
748
+ const capturedRunErrors: RunErrorEvent[] = [];
749
+ const result = await replayAgent.connectAgent(
750
+ { runId: "replay-run" },
751
+ {
752
+ onRunErrorEvent: ({ event }) => {
753
+ capturedRunErrors.push(event);
754
+ },
755
+ },
756
+ );
757
+
758
+ expect(runEvents.at(-1)?.type).toBe(EventType.RUN_ERROR);
759
+ expect(capturedRunErrors).toHaveLength(1);
760
+ expect(capturedRunErrors[0]).toMatchObject(runErrorEvent);
761
+ expect(result.newMessages).toEqual([userMessage]);
762
+ expect(replayAgent.messages).toEqual([userMessage]);
763
+ });
764
+ });
765
+ });