@copilotkitnext/runtime 0.0.2 → 0.0.4

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 (40) hide show
  1. package/dist/index.js +1 -14
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +1 -14
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +4 -4
  6. package/.turbo/turbo-build.log +0 -23
  7. package/.turbo/turbo-check-types.log +0 -4
  8. package/.turbo/turbo-lint.log +0 -56
  9. package/.turbo/turbo-test$colon$coverage.log +0 -149
  10. package/.turbo/turbo-test.log +0 -107
  11. package/src/__tests__/get-runtime-info.test.ts +0 -117
  12. package/src/__tests__/handle-run.test.ts +0 -69
  13. package/src/__tests__/handle-transcribe.test.ts +0 -289
  14. package/src/__tests__/in-process-agent-runner-messages.test.ts +0 -599
  15. package/src/__tests__/in-process-agent-runner.test.ts +0 -726
  16. package/src/__tests__/middleware.test.ts +0 -432
  17. package/src/__tests__/routing.test.ts +0 -257
  18. package/src/endpoint.ts +0 -150
  19. package/src/handler.ts +0 -3
  20. package/src/handlers/get-runtime-info.ts +0 -50
  21. package/src/handlers/handle-connect.ts +0 -144
  22. package/src/handlers/handle-run.ts +0 -156
  23. package/src/handlers/handle-transcribe.ts +0 -126
  24. package/src/index.ts +0 -8
  25. package/src/middleware.ts +0 -232
  26. package/src/runner/__tests__/enterprise-runner.test.ts +0 -992
  27. package/src/runner/__tests__/event-compaction.test.ts +0 -253
  28. package/src/runner/__tests__/in-memory-runner.test.ts +0 -483
  29. package/src/runner/__tests__/sqlite-runner.test.ts +0 -975
  30. package/src/runner/agent-runner.ts +0 -27
  31. package/src/runner/enterprise.ts +0 -653
  32. package/src/runner/event-compaction.ts +0 -250
  33. package/src/runner/in-memory.ts +0 -341
  34. package/src/runner/index.ts +0 -0
  35. package/src/runner/sqlite.ts +0 -481
  36. package/src/runtime.ts +0 -53
  37. package/src/transcription-service/transcription-service-openai.ts +0 -29
  38. package/src/transcription-service/transcription-service.ts +0 -11
  39. package/tsconfig.json +0 -13
  40. package/tsup.config.ts +0 -11
@@ -1,992 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { Kysely, SqliteDialect } from 'kysely';
3
- import Database from 'better-sqlite3';
4
- import IORedisMock from 'ioredis-mock';
5
- import { EnterpriseAgentRunner } from '../enterprise';
6
- import { EventType } from '@ag-ui/client';
7
- import type { AbstractAgent, RunAgentInput, BaseEvent, Message } from '@ag-ui/client';
8
- import { firstValueFrom, toArray } from 'rxjs';
9
-
10
- // Mock agent that takes custom events
11
- class CustomEventAgent implements AbstractAgent {
12
- private events: BaseEvent[];
13
-
14
- constructor(events: BaseEvent[] = []) {
15
- this.events = events;
16
- }
17
-
18
- async runAgent(
19
- input: RunAgentInput,
20
- callbacks: {
21
- onEvent: (params: { event: any }) => void | Promise<void>;
22
- onNewMessage?: (params: { message: any }) => void | Promise<void>;
23
- onRunStartedEvent?: () => void | Promise<void>;
24
- }
25
- ): Promise<void> {
26
- if (callbacks.onRunStartedEvent) {
27
- await callbacks.onRunStartedEvent();
28
- }
29
-
30
- for (const event of this.events) {
31
- await callbacks.onEvent({ event });
32
- }
33
- }
34
- }
35
-
36
- // Mock agent for testing
37
- class MockAgent implements AbstractAgent {
38
- async runAgent(
39
- input: RunAgentInput,
40
- callbacks: {
41
- onEvent: (params: { event: any }) => void | Promise<void>;
42
- onNewMessage?: (params: { message: any }) => void | Promise<void>;
43
- onRunStartedEvent?: () => void | Promise<void>;
44
- }
45
- ): Promise<void> {
46
- // Emit run started
47
- if (callbacks.onRunStartedEvent) {
48
- await callbacks.onRunStartedEvent();
49
- }
50
-
51
- // Emit some events
52
- await callbacks.onEvent({
53
- event: {
54
- type: EventType.RUN_STARTED,
55
- threadId: input.threadId,
56
- runId: input.runId,
57
- }
58
- });
59
-
60
- // Emit a text message
61
- await callbacks.onEvent({
62
- event: {
63
- type: EventType.TEXT_MESSAGE_START,
64
- messageId: 'test-msg-1',
65
- role: 'assistant',
66
- }
67
- });
68
-
69
- await callbacks.onEvent({
70
- event: {
71
- type: EventType.TEXT_MESSAGE_CONTENT,
72
- messageId: 'test-msg-1',
73
- delta: 'Hello from agent',
74
- }
75
- });
76
-
77
- await callbacks.onEvent({
78
- event: {
79
- type: EventType.TEXT_MESSAGE_END,
80
- messageId: 'test-msg-1',
81
- }
82
- });
83
-
84
- // Emit run finished
85
- await callbacks.onEvent({
86
- event: {
87
- type: EventType.RUN_FINISHED,
88
- threadId: input.threadId,
89
- runId: input.runId,
90
- }
91
- });
92
- }
93
- }
94
-
95
- // Error agent for testing
96
- class ErrorAgent implements AbstractAgent {
97
- async runAgent(
98
- input: RunAgentInput,
99
- callbacks: {
100
- onEvent: (params: { event: any }) => void | Promise<void>;
101
- onNewMessage?: (params: { message: any }) => void | Promise<void>;
102
- onRunStartedEvent?: () => void | Promise<void>;
103
- }
104
- ): Promise<void> {
105
- await callbacks.onEvent({
106
- event: {
107
- type: EventType.RUN_STARTED,
108
- threadId: input.threadId,
109
- runId: input.runId,
110
- }
111
- });
112
-
113
- await callbacks.onEvent({
114
- event: {
115
- type: EventType.RUN_ERROR,
116
- error: 'Test error',
117
- }
118
- });
119
- }
120
- }
121
-
122
- describe('EnterpriseAgentRunner', () => {
123
- let db: Kysely<any>;
124
- let redis: any;
125
- let redisSub: any;
126
- let runner: EnterpriseAgentRunner;
127
-
128
- beforeEach(async () => {
129
- // In-memory SQLite for testing
130
- db = new Kysely({
131
- dialect: new SqliteDialect({
132
- database: new Database(':memory:')
133
- })
134
- });
135
-
136
- // Mock Redis for unit tests
137
- redis = new IORedisMock();
138
- redisSub = redis.duplicate();
139
-
140
- runner = new EnterpriseAgentRunner({
141
- kysely: db,
142
- redis,
143
- redisSub,
144
- streamRetentionMs: 60000, // 1 minute for tests
145
- streamActiveTTLMs: 10000, // 10 seconds for tests
146
- lockTTLMs: 30000 // 30 seconds for tests
147
- });
148
-
149
- // Allow schema to initialize
150
- await new Promise(resolve => setTimeout(resolve, 100));
151
- });
152
-
153
- afterEach(async () => {
154
- await runner.close();
155
- });
156
-
157
- it('should prevent concurrent runs on same thread', async () => {
158
- // Create a slow agent that takes time to complete
159
- const slowAgent: AbstractAgent = {
160
- async runAgent(input, callbacks) {
161
- if (callbacks.onRunStartedEvent) {
162
- await callbacks.onRunStartedEvent();
163
- }
164
-
165
- await callbacks.onEvent({
166
- event: {
167
- type: EventType.RUN_STARTED,
168
- threadId: input.threadId,
169
- runId: input.runId,
170
- }
171
- });
172
-
173
- // Simulate long running task
174
- await new Promise(resolve => setTimeout(resolve, 500));
175
-
176
- await callbacks.onEvent({
177
- event: {
178
- type: EventType.RUN_FINISHED,
179
- threadId: input.threadId,
180
- runId: input.runId,
181
- }
182
- });
183
- }
184
- };
185
-
186
- const threadId = 'test-thread-1';
187
- const input1: RunAgentInput = {
188
- runId: 'run-1',
189
- threadId,
190
- messages: [],
191
- };
192
- const input2: RunAgentInput = {
193
- runId: 'run-2',
194
- threadId,
195
- messages: [],
196
- };
197
-
198
- // Start first run
199
- const run1 = runner.run({ threadId, agent: slowAgent, input: input1 });
200
-
201
- // Wait a bit for first run to acquire lock
202
- await new Promise(resolve => setTimeout(resolve, 100));
203
-
204
- // Try to start second run on same thread - should error
205
- const run2 = runner.run({ threadId, agent: slowAgent, input: input2 });
206
-
207
- let errorReceived = false;
208
- let completedWithoutError = false;
209
-
210
- await new Promise<void>((resolve) => {
211
- run2.subscribe({
212
- next: () => {},
213
- error: (err) => {
214
- errorReceived = true;
215
- expect(err.message).toBe('Thread already running');
216
- resolve();
217
- },
218
- complete: () => {
219
- completedWithoutError = true;
220
- resolve();
221
- }
222
- });
223
- });
224
-
225
- expect(errorReceived).toBe(true);
226
- expect(completedWithoutError).toBe(false);
227
-
228
- // Let first run complete
229
- const events1 = await firstValueFrom(run1.pipe(toArray()));
230
- expect(events1.length).toBeGreaterThan(0);
231
- });
232
-
233
- it('should handle RUN_FINISHED event correctly', async () => {
234
- const agent = new MockAgent();
235
- const threadId = 'test-thread-2';
236
- const input: RunAgentInput = {
237
- runId: 'run-finished',
238
- threadId,
239
- messages: [],
240
- };
241
-
242
- const events = await firstValueFrom(
243
- runner.run({ threadId, agent, input }).pipe(toArray())
244
- );
245
-
246
- // Should contain RUN_FINISHED event
247
- const runFinishedEvent = events.find(e => e.type === EventType.RUN_FINISHED);
248
- expect(runFinishedEvent).toBeDefined();
249
-
250
- // Thread should not be running after completion
251
- const isRunning = await runner.isRunning({ threadId });
252
- expect(isRunning).toBe(false);
253
-
254
- // Stream should have retention TTL
255
- const streamKey = `stream:${threadId}:${input.runId}`;
256
- const ttl = await redis.pttl(streamKey);
257
- expect(ttl).toBeGreaterThan(0);
258
- expect(ttl).toBeLessThanOrEqual(60000); // retention period
259
- });
260
-
261
- it('should handle RUN_ERROR event correctly', async () => {
262
- const agent = new ErrorAgent();
263
- const threadId = 'test-thread-3';
264
- const input: RunAgentInput = {
265
- runId: 'run-error',
266
- threadId,
267
- messages: [],
268
- };
269
-
270
- const events = await firstValueFrom(
271
- runner.run({ threadId, agent, input }).pipe(toArray())
272
- );
273
-
274
- // Should contain RUN_ERROR event
275
- const runErrorEvent = events.find(e => e.type === EventType.RUN_ERROR);
276
- expect(runErrorEvent).toBeDefined();
277
-
278
- // Thread should not be running after error
279
- const isRunning = await runner.isRunning({ threadId });
280
- expect(isRunning).toBe(false);
281
- });
282
-
283
- it('should allow late readers to catch up during retention period', async () => {
284
- const agent = new MockAgent();
285
- const threadId = 'test-thread-4';
286
- const input: RunAgentInput = {
287
- runId: 'run-retention',
288
- threadId,
289
- messages: [],
290
- };
291
-
292
- // Start and complete a run
293
- const runEvents = await firstValueFrom(
294
- runner.run({ threadId, agent, input }).pipe(toArray())
295
- );
296
-
297
- // Wait a bit
298
- await new Promise(resolve => setTimeout(resolve, 100));
299
-
300
- // Connect should still get all events
301
- const connectEvents = await firstValueFrom(
302
- runner.connect({ threadId }).pipe(toArray())
303
- );
304
-
305
- // Should get the same events (after compaction)
306
- expect(connectEvents.length).toBeGreaterThan(0);
307
-
308
- // Should include text message events
309
- const textStartEvents = connectEvents.filter(e => e.type === EventType.TEXT_MESSAGE_START);
310
- expect(textStartEvents.length).toBeGreaterThan(0);
311
- });
312
-
313
- it('should handle connect() with no active runs', async () => {
314
- const threadId = 'test-thread-5';
315
-
316
- // Connect to thread with no runs
317
- const events = await firstValueFrom(
318
- runner.connect({ threadId }).pipe(toArray())
319
- );
320
-
321
- // Should complete with empty array
322
- expect(events).toEqual([]);
323
- });
324
-
325
- it('should handle connect() during active run', async () => {
326
- const agent = new MockAgent();
327
- const threadId = 'test-thread-6';
328
- const input: RunAgentInput = {
329
- runId: 'run-active',
330
- threadId,
331
- messages: [],
332
- };
333
-
334
- // Start a run but don't wait for it
335
- runner.run({ threadId, agent, input });
336
-
337
- // Wait for run to start
338
- await new Promise(resolve => setTimeout(resolve, 50));
339
-
340
- // Connect while run is active
341
- const connectPromise = firstValueFrom(
342
- runner.connect({ threadId }).pipe(toArray())
343
- );
344
-
345
- // Should eventually complete when run finishes
346
- const events = await connectPromise;
347
- expect(events.length).toBeGreaterThan(0);
348
-
349
- // Should include RUN_FINISHED
350
- const finishedEvent = events.find(e => e.type === EventType.RUN_FINISHED);
351
- expect(finishedEvent).toBeDefined();
352
- });
353
-
354
- it('should handle stop() correctly', async () => {
355
- const agent = new MockAgent();
356
- const threadId = 'test-thread-7';
357
- const input: RunAgentInput = {
358
- runId: 'run-stop',
359
- threadId,
360
- messages: [],
361
- };
362
-
363
- // Mock a slow agent
364
- const slowAgent: AbstractAgent = {
365
- async runAgent(input, callbacks) {
366
- await callbacks.onEvent({
367
- event: {
368
- type: EventType.RUN_STARTED,
369
- threadId: input.threadId,
370
- runId: input.runId,
371
- }
372
- });
373
-
374
- // Simulate long running task
375
- await new Promise(resolve => setTimeout(resolve, 1000));
376
- }
377
- };
378
-
379
- // Start run
380
- runner.run({ threadId, agent: slowAgent, input });
381
-
382
- // Wait for run to start
383
- await new Promise(resolve => setTimeout(resolve, 50));
384
-
385
- // Stop the run
386
- const stopped = await runner.stop({ threadId });
387
- expect(stopped).toBe(true);
388
-
389
- // Should not be running
390
- const isRunning = await runner.isRunning({ threadId });
391
- expect(isRunning).toBe(false);
392
-
393
- // Stream should contain RUN_ERROR event
394
- const streamKey = `stream:${threadId}:${input.runId}`;
395
- const stream = await redis.xrange(streamKey, '-', '+');
396
- const errorEvent = stream.find((entry: any) => {
397
- const fields = entry[1];
398
- for (let i = 0; i < fields.length; i += 2) {
399
- if (fields[i] === 'type' && fields[i + 1] === EventType.RUN_ERROR) {
400
- return true;
401
- }
402
- }
403
- return false;
404
- });
405
- expect(errorEvent).toBeDefined();
406
- });
407
-
408
- it('should handle multiple sequential runs on same thread', async () => {
409
- const agent = new MockAgent();
410
- const threadId = 'test-thread-8';
411
-
412
- // First run
413
- const input1: RunAgentInput = {
414
- runId: 'run-seq-1',
415
- threadId,
416
- messages: [],
417
- };
418
-
419
- const events1 = await firstValueFrom(
420
- runner.run({ threadId, agent, input: input1 }).pipe(toArray())
421
- );
422
- expect(events1.length).toBeGreaterThan(0);
423
-
424
- // Second run
425
- const input2: RunAgentInput = {
426
- runId: 'run-seq-2',
427
- threadId,
428
- messages: [],
429
- };
430
-
431
- const events2 = await firstValueFrom(
432
- runner.run({ threadId, agent, input: input2 }).pipe(toArray())
433
- );
434
- expect(events2.length).toBeGreaterThan(0);
435
-
436
- // Connect should get both runs' events
437
- const allEvents = await firstValueFrom(
438
- runner.connect({ threadId }).pipe(toArray())
439
- );
440
-
441
- // Should have events from both runs
442
- expect(allEvents.length).toBeGreaterThan(events1.length);
443
- });
444
-
445
- it('should handle input messages correctly', async () => {
446
- const agent = new MockAgent();
447
- const threadId = 'test-thread-9';
448
- const input: RunAgentInput = {
449
- runId: 'run-messages',
450
- threadId,
451
- messages: [
452
- {
453
- id: 'user-msg-1',
454
- role: 'user',
455
- content: 'Hello',
456
- }
457
- ],
458
- };
459
-
460
- const events = await firstValueFrom(
461
- runner.run({ threadId, agent, input }).pipe(toArray())
462
- );
463
-
464
- // Run events should not include input messages
465
- const userMessages = events.filter((e: any) =>
466
- e.type === EventType.TEXT_MESSAGE_START && e.role === 'user'
467
- );
468
- expect(userMessages.length).toBe(0);
469
-
470
- // But connect should include them
471
- const connectEvents = await firstValueFrom(
472
- runner.connect({ threadId }).pipe(toArray())
473
- );
474
-
475
- const userMessagesInConnect = connectEvents.filter((e: any) =>
476
- e.type === EventType.TEXT_MESSAGE_START && e.role === 'user'
477
- );
478
- expect(userMessagesInConnect.length).toBe(1);
479
- });
480
-
481
- // Additional comprehensive tests to match SQLite/InMemory coverage
482
-
483
- it('should persist events across runner instances', async () => {
484
- const threadId = 'test-thread-persistence';
485
- const events: BaseEvent[] = [
486
- { type: EventType.TEXT_MESSAGE_START, messageId: 'msg1', role: 'assistant' },
487
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: 'msg1', delta: 'Persisted' },
488
- { type: EventType.TEXT_MESSAGE_END, messageId: 'msg1' },
489
- { type: EventType.RUN_FINISHED, threadId, runId: 'run1' },
490
- ];
491
-
492
- const agent = new CustomEventAgent(events);
493
- const input: RunAgentInput = {
494
- threadId,
495
- runId: 'run1',
496
- messages: [],
497
- };
498
-
499
- // Run with first instance
500
- await firstValueFrom(
501
- runner.run({ threadId, agent, input }).pipe(toArray())
502
- );
503
-
504
- // Don't close the first runner's DB connection since we share it
505
- // Just disconnect Redis
506
- runner.redis.disconnect();
507
- runner.redisSub.disconnect();
508
-
509
- // Create new runner instance with same DB but new Redis
510
- const newRunner = new EnterpriseAgentRunner({
511
- kysely: db, // Reuse same DB connection
512
- redis: new IORedisMock(),
513
- streamRetentionMs: 60000,
514
- streamActiveTTLMs: 10000,
515
- lockTTLMs: 30000
516
- });
517
-
518
- // Connect should get persisted events
519
- const persistedEvents = await firstValueFrom(
520
- newRunner.connect({ threadId }).pipe(toArray())
521
- );
522
-
523
- expect(persistedEvents.length).toBeGreaterThan(0);
524
- const textContent = persistedEvents.find(
525
- e => e.type === EventType.TEXT_MESSAGE_CONTENT
526
- ) as any;
527
- expect(textContent?.delta).toBe('Persisted');
528
-
529
- // Clean up new runner's Redis connections only
530
- newRunner.redis.disconnect();
531
- newRunner.redisSub.disconnect();
532
- });
533
-
534
- it('should handle concurrent connections', async () => {
535
- const threadId = 'test-thread-concurrent';
536
- const agent = new MockAgent();
537
- const input: RunAgentInput = {
538
- threadId,
539
- runId: 'run1',
540
- messages: [],
541
- };
542
-
543
- // Start a run
544
- const runPromise = firstValueFrom(
545
- runner.run({ threadId, agent, input }).pipe(toArray())
546
- );
547
-
548
- // Start multiple connections while run is active
549
- await new Promise(resolve => setTimeout(resolve, 50));
550
-
551
- const conn1Promise = firstValueFrom(
552
- runner.connect({ threadId }).pipe(toArray())
553
- );
554
- const conn2Promise = firstValueFrom(
555
- runner.connect({ threadId }).pipe(toArray())
556
- );
557
- const conn3Promise = firstValueFrom(
558
- runner.connect({ threadId }).pipe(toArray())
559
- );
560
-
561
- // Wait for all to complete
562
- const [runEvents, conn1Events, conn2Events, conn3Events] = await Promise.all([
563
- runPromise,
564
- conn1Promise,
565
- conn2Promise,
566
- conn3Promise,
567
- ]);
568
-
569
- // All connections should receive events
570
- expect(conn1Events.length).toBeGreaterThan(0);
571
- expect(conn2Events.length).toBeGreaterThan(0);
572
- expect(conn3Events.length).toBeGreaterThan(0);
573
- });
574
-
575
- it('should store compacted events in the database', async () => {
576
- const threadId = 'test-thread-compaction';
577
- const messageId = 'msg-compact';
578
-
579
- // Create events that will be compacted
580
- const events: BaseEvent[] = [
581
- { type: EventType.TEXT_MESSAGE_START, messageId, role: 'assistant' },
582
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId, delta: 'Hello' },
583
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId, delta: ' ' },
584
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId, delta: 'World' },
585
- { type: EventType.TEXT_MESSAGE_END, messageId },
586
- { type: EventType.RUN_FINISHED, threadId, runId: 'run1' },
587
- ];
588
-
589
- const agent = new CustomEventAgent(events);
590
- const input: RunAgentInput = {
591
- threadId,
592
- runId: 'run1',
593
- messages: [],
594
- };
595
-
596
- // Run the agent
597
- await firstValueFrom(
598
- runner.run({ threadId, agent, input }).pipe(toArray())
599
- );
600
-
601
- // Check database has compacted events
602
- const dbRuns = await db
603
- .selectFrom('agent_runs')
604
- .where('thread_id', '=', threadId)
605
- .selectAll()
606
- .execute();
607
-
608
- expect(dbRuns).toHaveLength(1);
609
- const storedEvents = JSON.parse(dbRuns[0].events);
610
-
611
- // Should have compacted content into single delta
612
- const contentEvents = storedEvents.filter(
613
- (e: any) => e.type === EventType.TEXT_MESSAGE_CONTENT
614
- );
615
- expect(contentEvents).toHaveLength(1);
616
- expect(contentEvents[0].delta).toBe('Hello World');
617
- });
618
-
619
- it('should not store duplicate message IDs across multiple runs', async () => {
620
- const threadId = 'test-thread-nodupe';
621
- const messageId = 'shared-msg';
622
-
623
- // First run with a message
624
- const input1: RunAgentInput = {
625
- threadId,
626
- runId: 'run1',
627
- messages: [{
628
- id: messageId,
629
- role: 'user',
630
- content: 'First message',
631
- }],
632
- };
633
-
634
- const agent = new CustomEventAgent([
635
- { type: EventType.RUN_FINISHED, threadId, runId: 'run1' },
636
- ]);
637
-
638
- await firstValueFrom(
639
- runner.run({ threadId, agent, input: input1 }).pipe(toArray())
640
- );
641
-
642
- // Second run with same message ID
643
- const input2: RunAgentInput = {
644
- threadId,
645
- runId: 'run2',
646
- messages: [{
647
- id: messageId,
648
- role: 'user',
649
- content: 'First message',
650
- }],
651
- };
652
-
653
- await firstValueFrom(
654
- runner.run({ threadId, agent, input: input2 }).pipe(toArray())
655
- );
656
-
657
- // Check database - message should only be stored in first run
658
- const dbRuns = await db
659
- .selectFrom('agent_runs')
660
- .where('thread_id', '=', threadId)
661
- .selectAll()
662
- .orderBy('created_at', 'asc')
663
- .execute();
664
-
665
- expect(dbRuns).toHaveLength(2);
666
-
667
- const run1Events = JSON.parse(dbRuns[0].events);
668
- const run2Events = JSON.parse(dbRuns[1].events);
669
-
670
- // First run should have the message events
671
- const run1MessageEvents = run1Events.filter(
672
- (e: any) => e.messageId === messageId
673
- );
674
- expect(run1MessageEvents.length).toBeGreaterThan(0);
675
-
676
- // Second run should NOT have the message events
677
- const run2MessageEvents = run2Events.filter(
678
- (e: any) => e.messageId === messageId
679
- );
680
- expect(run2MessageEvents.length).toBe(0);
681
- });
682
-
683
- it('should handle all message types (user, assistant, tool, system, developer)', async () => {
684
- const threadId = 'test-thread-alltypes';
685
- const messages: Message[] = [
686
- { id: 'user-1', role: 'user', content: 'User message' },
687
- { id: 'assistant-1', role: 'assistant', content: 'Assistant message' },
688
- { id: 'system-1', role: 'system', content: 'System message' },
689
- { id: 'developer-1', role: 'developer', content: 'Developer message' },
690
- {
691
- id: 'tool-1',
692
- role: 'tool',
693
- content: 'Tool result',
694
- toolCallId: 'call-1'
695
- },
696
- ];
697
-
698
- const input: RunAgentInput = {
699
- threadId,
700
- runId: 'run1',
701
- messages,
702
- };
703
-
704
- const agent = new CustomEventAgent([
705
- { type: EventType.RUN_FINISHED, threadId, runId: 'run1' },
706
- ]);
707
-
708
- await firstValueFrom(
709
- runner.run({ threadId, agent, input }).pipe(toArray())
710
- );
711
-
712
- // Connect should get all message types
713
- const connectEvents = await firstValueFrom(
714
- runner.connect({ threadId }).pipe(toArray())
715
- );
716
-
717
- // Check each message type is present
718
- const userEvents = connectEvents.filter(
719
- (e: any) => e.type === EventType.TEXT_MESSAGE_START && e.role === 'user'
720
- );
721
- expect(userEvents.length).toBe(1);
722
-
723
- const assistantEvents = connectEvents.filter(
724
- (e: any) => e.type === EventType.TEXT_MESSAGE_START && e.role === 'assistant'
725
- );
726
- expect(assistantEvents.length).toBe(1);
727
-
728
- const systemEvents = connectEvents.filter(
729
- (e: any) => e.type === EventType.TEXT_MESSAGE_START && e.role === 'system'
730
- );
731
- expect(systemEvents.length).toBe(1);
732
-
733
- const developerEvents = connectEvents.filter(
734
- (e: any) => e.type === EventType.TEXT_MESSAGE_START && e.role === 'developer'
735
- );
736
- expect(developerEvents.length).toBe(1);
737
-
738
- const toolEvents = connectEvents.filter(
739
- (e: any) => e.type === EventType.TOOL_CALL_RESULT
740
- );
741
- expect(toolEvents.length).toBe(1);
742
- });
743
-
744
- it('should handle tool calls correctly', async () => {
745
- const threadId = 'test-thread-tools';
746
- const messageId = 'assistant-msg';
747
- const toolCallId = 'tool-call-1';
748
-
749
- const messages: Message[] = [
750
- {
751
- id: messageId,
752
- role: 'assistant',
753
- content: 'Let me help',
754
- toolCalls: [{
755
- id: toolCallId,
756
- function: {
757
- name: 'calculator',
758
- arguments: '{"a": 1, "b": 2}'
759
- }
760
- }]
761
- },
762
- {
763
- id: 'tool-result-1',
764
- role: 'tool',
765
- content: '3',
766
- toolCallId: toolCallId
767
- }
768
- ];
769
-
770
- const input: RunAgentInput = {
771
- threadId,
772
- runId: 'run1',
773
- messages,
774
- };
775
-
776
- const agent = new CustomEventAgent([
777
- { type: EventType.RUN_FINISHED, threadId, runId: 'run1' },
778
- ]);
779
-
780
- await firstValueFrom(
781
- runner.run({ threadId, agent, input }).pipe(toArray())
782
- );
783
-
784
- const connectEvents = await firstValueFrom(
785
- runner.connect({ threadId }).pipe(toArray())
786
- );
787
-
788
- // Check tool call events
789
- const toolCallStart = connectEvents.find(
790
- (e: any) => e.type === EventType.TOOL_CALL_START && e.toolCallId === toolCallId
791
- ) as any;
792
- expect(toolCallStart).toBeDefined();
793
- expect(toolCallStart.toolCallName).toBe('calculator');
794
-
795
- const toolCallArgs = connectEvents.find(
796
- (e: any) => e.type === EventType.TOOL_CALL_ARGS && e.toolCallId === toolCallId
797
- ) as any;
798
- expect(toolCallArgs).toBeDefined();
799
- expect(toolCallArgs.delta).toBe('{"a": 1, "b": 2}');
800
-
801
- const toolCallEnd = connectEvents.find(
802
- (e: any) => e.type === EventType.TOOL_CALL_END && e.toolCallId === toolCallId
803
- );
804
- expect(toolCallEnd).toBeDefined();
805
-
806
- const toolResult = connectEvents.find(
807
- (e: any) => e.type === EventType.TOOL_CALL_RESULT && e.toolCallId === toolCallId
808
- ) as any;
809
- expect(toolResult).toBeDefined();
810
- expect(toolResult.content).toBe('3');
811
- });
812
-
813
- it('should track running state correctly', async () => {
814
- const threadId = 'test-thread-state';
815
-
816
- // Use a slow agent to ensure we can check running state
817
- const slowAgent: AbstractAgent = {
818
- async runAgent(input, callbacks) {
819
- if (callbacks.onRunStartedEvent) {
820
- await callbacks.onRunStartedEvent();
821
- }
822
-
823
- await callbacks.onEvent({
824
- event: {
825
- type: EventType.RUN_STARTED,
826
- threadId: input.threadId,
827
- runId: input.runId,
828
- }
829
- });
830
-
831
- // Delay to ensure we can check running state
832
- await new Promise(resolve => setTimeout(resolve, 200));
833
-
834
- await callbacks.onEvent({
835
- event: {
836
- type: EventType.RUN_FINISHED,
837
- threadId: input.threadId,
838
- runId: input.runId,
839
- }
840
- });
841
- }
842
- };
843
-
844
- const input: RunAgentInput = {
845
- threadId,
846
- runId: 'run1',
847
- messages: [],
848
- };
849
-
850
- // Check not running initially
851
- let isRunning = await runner.isRunning({ threadId });
852
- expect(isRunning).toBe(false);
853
-
854
- // Start run
855
- const runPromise = runner.run({ threadId, agent: slowAgent, input });
856
-
857
- // Check running state during execution
858
- await new Promise(resolve => setTimeout(resolve, 50));
859
- isRunning = await runner.isRunning({ threadId });
860
- expect(isRunning).toBe(true);
861
-
862
- // Wait for completion
863
- await firstValueFrom(runPromise.pipe(toArray()));
864
-
865
- // Check not running after completion
866
- isRunning = await runner.isRunning({ threadId });
867
- expect(isRunning).toBe(false);
868
- });
869
-
870
- it('should handle empty events arrays correctly', async () => {
871
- const threadId = 'test-thread-empty';
872
- const agent = new CustomEventAgent([]); // No events
873
- const input: RunAgentInput = {
874
- threadId,
875
- runId: 'run1',
876
- messages: [],
877
- };
878
-
879
- await firstValueFrom(
880
- runner.run({ threadId, agent, input }).pipe(toArray())
881
- );
882
-
883
- // Check database - should still create a run record
884
- const dbRuns = await db
885
- .selectFrom('agent_runs')
886
- .where('thread_id', '=', threadId)
887
- .selectAll()
888
- .execute();
889
-
890
- expect(dbRuns).toHaveLength(1);
891
- const storedEvents = JSON.parse(dbRuns[0].events);
892
- expect(storedEvents).toEqual([]);
893
- });
894
-
895
- it('should handle parent-child run relationships', async () => {
896
- const threadId = 'test-thread-parent-child';
897
- const agent = new CustomEventAgent([
898
- { type: EventType.RUN_FINISHED, threadId, runId: 'run1' },
899
- ]);
900
-
901
- // First run (parent)
902
- await firstValueFrom(
903
- runner.run({
904
- threadId,
905
- agent,
906
- input: { threadId, runId: 'run1', messages: [] }
907
- }).pipe(toArray())
908
- );
909
-
910
- // Second run (child)
911
- await firstValueFrom(
912
- runner.run({
913
- threadId,
914
- agent,
915
- input: { threadId, runId: 'run2', messages: [] }
916
- }).pipe(toArray())
917
- );
918
-
919
- // Check parent-child relationship in database
920
- const dbRuns = await db
921
- .selectFrom('agent_runs')
922
- .where('thread_id', '=', threadId)
923
- .selectAll()
924
- .orderBy('created_at', 'asc')
925
- .execute();
926
-
927
- expect(dbRuns).toHaveLength(2);
928
- expect(dbRuns[0].parent_run_id).toBeNull();
929
- expect(dbRuns[1].parent_run_id).toBe('run1');
930
- });
931
-
932
- it('should handle database initialization correctly', async () => {
933
- // Check all tables exist
934
- const tables = await db
935
- .selectFrom('sqlite_master')
936
- .where('type', '=', 'table')
937
- .select('name')
938
- .execute();
939
-
940
- const tableNames = tables.map(t => t.name);
941
- expect(tableNames).toContain('agent_runs');
942
- expect(tableNames).toContain('run_state');
943
- expect(tableNames).toContain('schema_version');
944
-
945
- // Check schema version
946
- const schemaVersion = await db
947
- .selectFrom('schema_version')
948
- .selectAll()
949
- .executeTakeFirst();
950
-
951
- expect(schemaVersion).toBeDefined();
952
- expect(schemaVersion?.version).toBe(1);
953
- });
954
-
955
- it('should handle Redis stream expiry correctly', async () => {
956
- const threadId = 'test-thread-expiry';
957
- const agent = new MockAgent();
958
- const input: RunAgentInput = {
959
- threadId,
960
- runId: 'run1',
961
- messages: [],
962
- };
963
-
964
- // Run with short TTL
965
- const shortTTLRunner = new EnterpriseAgentRunner({
966
- kysely: db,
967
- redis,
968
- redisSub,
969
- streamRetentionMs: 100, // 100ms retention
970
- streamActiveTTLMs: 50, // 50ms active TTL
971
- lockTTLMs: 1000
972
- });
973
-
974
- await firstValueFrom(
975
- shortTTLRunner.run({ threadId, agent, input }).pipe(toArray())
976
- );
977
-
978
- // Stream should exist immediately after run
979
- const streamKey = `stream:${threadId}:run1`;
980
- let exists = await redis.exists(streamKey);
981
- expect(exists).toBe(1);
982
-
983
- // Wait for retention period to expire
984
- await new Promise(resolve => setTimeout(resolve, 150));
985
-
986
- // Stream should be gone
987
- exists = await redis.exists(streamKey);
988
- expect(exists).toBe(0);
989
-
990
- await shortTTLRunner.close();
991
- });
992
- });