@copilotkitnext/runtime 0.0.3 → 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 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +1 -1
  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 -108
  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 -328
  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,975 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { SqliteAgentRunner } from "../sqlite";
3
- import { InMemoryAgentRunner } from "../in-memory";
4
- import { AbstractAgent, BaseEvent, RunAgentInput, EventType } from "@ag-ui/client";
5
- import { firstValueFrom } from "rxjs";
6
- import { toArray } from "rxjs/operators";
7
- import Database from "better-sqlite3";
8
- import * as fs from "fs";
9
- import * as path from "path";
10
- import * as os from "os";
11
-
12
- // Mock agent for testing
13
- class MockAgent extends AbstractAgent {
14
- private events: BaseEvent[];
15
-
16
- constructor(events: BaseEvent[] = []) {
17
- super();
18
- this.events = events;
19
- }
20
-
21
- async runAgent(
22
- input: RunAgentInput,
23
- options: {
24
- onEvent: (event: { event: BaseEvent }) => void;
25
- onNewMessage?: (args: { message: any }) => void;
26
- onRunStartedEvent?: () => void;
27
- }
28
- ): Promise<void> {
29
- // Call onRunStartedEvent if provided
30
- if (options.onRunStartedEvent) {
31
- options.onRunStartedEvent();
32
- }
33
-
34
- // Emit all events
35
- for (const event of this.events) {
36
- options.onEvent({ event });
37
- }
38
- }
39
-
40
- clone(): AbstractAgent {
41
- return new MockAgent(this.events);
42
- }
43
- }
44
-
45
- describe("SqliteAgentRunner", () => {
46
- let tempDir: string;
47
- let dbPath: string;
48
- let runner: SqliteAgentRunner;
49
-
50
- beforeEach(() => {
51
- // Create a temporary directory for test database
52
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-test-"));
53
- dbPath = path.join(tempDir, "test.db");
54
- runner = new SqliteAgentRunner({ dbPath });
55
- });
56
-
57
- afterEach(() => {
58
- // Clean up test database
59
- if (fs.existsSync(dbPath)) {
60
- fs.unlinkSync(dbPath);
61
- }
62
- if (fs.existsSync(tempDir)) {
63
- fs.rmdirSync(tempDir);
64
- }
65
- });
66
-
67
- describe("Basic functionality", () => {
68
- it("should run an agent and emit events", async () => {
69
- const threadId = "test-thread-1";
70
- const events: BaseEvent[] = [
71
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "assistant" },
72
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Hello" },
73
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
74
- ];
75
-
76
- const agent = new MockAgent(events);
77
- const input: RunAgentInput = {
78
- threadId,
79
- runId: "run1",
80
- messages: [],
81
- };
82
-
83
- const runObservable = runner.run({ threadId, agent, input });
84
- const emittedEvents = await firstValueFrom(runObservable.pipe(toArray()));
85
-
86
- expect(emittedEvents).toHaveLength(3);
87
- expect(emittedEvents[0].type).toBe(EventType.TEXT_MESSAGE_START);
88
- });
89
-
90
- it("should persist events across runner instances", async () => {
91
- const threadId = "test-thread-persistence";
92
- const events: BaseEvent[] = [
93
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "assistant" },
94
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Persisted" },
95
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
96
- ];
97
-
98
- const agent = new MockAgent(events);
99
- const input: RunAgentInput = {
100
- threadId,
101
- runId: "run1",
102
- messages: [],
103
- };
104
-
105
- // Run with first instance
106
- await firstValueFrom(runner.run({ threadId, agent, input }).pipe(toArray()));
107
-
108
- // Create new runner instance with same database
109
- const newRunner = new SqliteAgentRunner({ dbPath });
110
-
111
- // Connect should return persisted events
112
- const persistedEvents = await firstValueFrom(
113
- newRunner.connect({ threadId }).pipe(toArray())
114
- );
115
-
116
- expect(persistedEvents).toHaveLength(3);
117
- expect(persistedEvents[1].type).toBe(EventType.TEXT_MESSAGE_CONTENT);
118
- expect((persistedEvents[1] as any).delta).toBe("Persisted");
119
- });
120
-
121
- it("should handle concurrent connections", async () => {
122
- const threadId = "test-thread-concurrent";
123
- const events: BaseEvent[] = [
124
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "assistant" },
125
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Test" },
126
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
127
- ];
128
-
129
- const agent = new MockAgent(events);
130
- const input: RunAgentInput = {
131
- threadId,
132
- runId: "run1",
133
- messages: [],
134
- };
135
-
136
- // Run the agent
137
- await firstValueFrom(runner.run({ threadId, agent, input }).pipe(toArray()));
138
-
139
- // Create multiple concurrent connections
140
- const connection1 = runner.connect({ threadId });
141
- const connection2 = runner.connect({ threadId });
142
- const connection3 = runner.connect({ threadId });
143
-
144
- const [events1, events2, events3] = await Promise.all([
145
- firstValueFrom(connection1.pipe(toArray())),
146
- firstValueFrom(connection2.pipe(toArray())),
147
- firstValueFrom(connection3.pipe(toArray())),
148
- ]);
149
-
150
- // All connections should receive the same events
151
- expect(events1).toHaveLength(3);
152
- expect(events2).toHaveLength(3);
153
- expect(events3).toHaveLength(3);
154
- expect(events1).toEqual(events2);
155
- expect(events2).toEqual(events3);
156
- });
157
-
158
- it("should track running state correctly", async () => {
159
- const threadId = "test-thread-running";
160
- const agent = new MockAgent([]);
161
- const input: RunAgentInput = {
162
- threadId,
163
- runId: "run1",
164
- messages: [],
165
- };
166
-
167
- // Initially not running
168
- expect(await runner.isRunning({ threadId })).toBe(false);
169
-
170
- // Start running
171
- const runPromise = firstValueFrom(
172
- runner.run({ threadId, agent, input }).pipe(toArray())
173
- );
174
-
175
- // Should be running now
176
- expect(await runner.isRunning({ threadId })).toBe(true);
177
-
178
- // Wait for completion
179
- await runPromise;
180
-
181
- // Should not be running after completion
182
- expect(await runner.isRunning({ threadId })).toBe(false);
183
- });
184
-
185
- it("should prevent concurrent runs on same thread", async () => {
186
- const threadId = "test-thread-no-concurrent";
187
- const agent = new MockAgent([]);
188
- const input: RunAgentInput = {
189
- threadId,
190
- runId: "run1",
191
- messages: [],
192
- };
193
-
194
- // Start first run (don't await)
195
- runner.run({ threadId, agent, input }).subscribe();
196
-
197
- // Try to start second run immediately
198
- expect(() => {
199
- runner.run({ threadId, agent, input: { ...input, runId: "run2" } });
200
- }).toThrow("Thread already running");
201
- });
202
- });
203
-
204
- describe("Event compaction", () => {
205
- it("should store compacted events in the database", async () => {
206
- const threadId = "test-thread-compaction";
207
-
208
- // Create events that should be compacted
209
- const events1: BaseEvent[] = [
210
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "assistant" },
211
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Hello " },
212
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "world" },
213
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
214
- ];
215
-
216
- const agent1 = new MockAgent(events1);
217
- const input1: RunAgentInput = {
218
- threadId,
219
- runId: "run1",
220
- messages: [],
221
- };
222
-
223
- // Run first agent
224
- await firstValueFrom(runner.run({ threadId, agent: agent1, input: input1 }).pipe(toArray()));
225
-
226
- // Add more events - each run stores only its own events
227
- const events2: BaseEvent[] = [
228
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg2", role: "assistant" },
229
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "Second " },
230
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "message" },
231
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg2" },
232
- ];
233
-
234
- const agent2 = new MockAgent(events2);
235
- const input2: RunAgentInput = {
236
- threadId,
237
- runId: "run2",
238
- messages: [],
239
- };
240
-
241
- // Run second agent
242
- await firstValueFrom(runner.run({ threadId, agent: agent2, input: input2 }).pipe(toArray()));
243
-
244
- // Check database directly to verify compaction
245
- const db = new Database(dbPath);
246
- const rows = db.prepare("SELECT events FROM agent_runs WHERE thread_id = ?").all(threadId) as any[];
247
- db.close();
248
-
249
- // Parse events from both runs
250
- const run1Events = JSON.parse(rows[0].events);
251
- const run2Events = JSON.parse(rows[1].events);
252
-
253
- // First run should have only its own compacted events
254
- // We expect: START, single CONTENT with "Hello world", END
255
- expect(run1Events).toHaveLength(3);
256
- const contentEvents1 = run1Events.filter((e: any) => e.type === EventType.TEXT_MESSAGE_CONTENT);
257
- expect(contentEvents1).toHaveLength(1);
258
- expect(contentEvents1[0].delta).toBe("Hello world");
259
- expect(run1Events[0].messageId).toBe("msg1");
260
-
261
- // Second run should have only its own compacted events
262
- expect(run2Events).toHaveLength(3);
263
- const contentEvents2 = run2Events.filter((e: any) => e.type === EventType.TEXT_MESSAGE_CONTENT);
264
- expect(contentEvents2).toHaveLength(1);
265
- expect(contentEvents2[0].delta).toBe("Second message");
266
- expect(run2Events[0].messageId).toBe("msg2");
267
-
268
- // Verify runs have different message IDs (no cross-contamination)
269
- const run1MessageIds = new Set(run1Events.filter((e: any) => e.messageId).map((e: any) => e.messageId));
270
- const run2MessageIds = new Set(run2Events.filter((e: any) => e.messageId).map((e: any) => e.messageId));
271
-
272
- // Ensure no overlap between message IDs in different runs
273
- for (const id of run1MessageIds) {
274
- expect(run2MessageIds.has(id)).toBe(false);
275
- }
276
- });
277
-
278
- it("should never store empty events after text message compaction", async () => {
279
- const threadId = "test-thread-text-compaction-not-empty";
280
-
281
- // First run: multiple text content events that will be compacted
282
- const events1: BaseEvent[] = [
283
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "user" },
284
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "H" },
285
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "e" },
286
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "l" },
287
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "l" },
288
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "o" },
289
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
290
- ];
291
-
292
- const agent1 = new MockAgent(events1);
293
- const input1: RunAgentInput = {
294
- threadId,
295
- runId: "run1",
296
- messages: [],
297
- };
298
-
299
- await firstValueFrom(runner.run({ threadId, agent: agent1, input: input1 }).pipe(toArray()));
300
-
301
- // Second run: more text content events
302
- const events2: BaseEvent[] = [
303
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg2", role: "assistant" },
304
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "W" },
305
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "o" },
306
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "r" },
307
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "l" },
308
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "d" },
309
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg2" },
310
- ];
311
-
312
- const agent2 = new MockAgent(events2);
313
- const input2: RunAgentInput = {
314
- threadId,
315
- runId: "run2",
316
- messages: [],
317
- };
318
-
319
- await firstValueFrom(runner.run({ threadId, agent: agent2, input: input2 }).pipe(toArray()));
320
-
321
- // Check database directly
322
- const db = new Database(dbPath);
323
- const rows = db.prepare("SELECT run_id, events FROM agent_runs WHERE thread_id = ? ORDER BY created_at").all(threadId) as any[];
324
- db.close();
325
-
326
- expect(rows).toHaveLength(2);
327
-
328
- // Both runs should have non-empty compacted events
329
- const run1Events = JSON.parse(rows[0].events);
330
- const run2Events = JSON.parse(rows[1].events);
331
-
332
- // Verify run1 events are not empty and properly compacted
333
- expect(run1Events).not.toHaveLength(0);
334
- expect(run1Events).toHaveLength(3); // START, compacted CONTENT, END
335
- expect(run1Events[0].type).toBe(EventType.TEXT_MESSAGE_START);
336
- expect(run1Events[1].type).toBe(EventType.TEXT_MESSAGE_CONTENT);
337
- expect(run1Events[1].delta).toBe("Hello"); // All characters concatenated
338
- expect(run1Events[2].type).toBe(EventType.TEXT_MESSAGE_END);
339
-
340
- // Verify run2 events are not empty and properly compacted
341
- expect(run2Events).not.toHaveLength(0);
342
- expect(run2Events).toHaveLength(3); // START, compacted CONTENT, END
343
- expect(run2Events[0].type).toBe(EventType.TEXT_MESSAGE_START);
344
- expect(run2Events[1].type).toBe(EventType.TEXT_MESSAGE_CONTENT);
345
- expect(run2Events[1].delta).toBe("World"); // All characters concatenated
346
- expect(run2Events[2].type).toBe(EventType.TEXT_MESSAGE_END);
347
- });
348
-
349
- it("should handle complex compaction scenarios with multiple message types", async () => {
350
- const threadId = "test-thread-complex-compaction";
351
-
352
- // First run: Mix of events including text messages and other events
353
- const events1: BaseEvent[] = [
354
- { type: EventType.RUN_STARTED, runId: "run1" },
355
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "user" },
356
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Part " },
357
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "1" },
358
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
359
- { type: EventType.RUN_FINISHED, runId: "run1" },
360
- ];
361
-
362
- const agent1 = new MockAgent(events1);
363
- const input1: RunAgentInput = {
364
- threadId,
365
- runId: "run1",
366
- messages: [],
367
- };
368
-
369
- await firstValueFrom(runner.run({ threadId, agent: agent1, input: input1 }).pipe(toArray()));
370
-
371
- // Second run: Another message that could potentially compact to empty
372
- const events2: BaseEvent[] = [
373
- { type: EventType.RUN_STARTED, runId: "run2" },
374
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg2", role: "assistant" },
375
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "Part " },
376
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "2" },
377
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg2" },
378
- { type: EventType.RUN_FINISHED, runId: "run2" },
379
- ];
380
-
381
- const agent2 = new MockAgent(events2);
382
- const input2: RunAgentInput = {
383
- threadId,
384
- runId: "run2",
385
- messages: [],
386
- };
387
-
388
- await firstValueFrom(runner.run({ threadId, agent: agent2, input: input2 }).pipe(toArray()));
389
-
390
- // Third run: Test with already compacted previous events
391
- const events3: BaseEvent[] = [
392
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg3", role: "user" },
393
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg3", delta: "Part " },
394
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg3", delta: "3" },
395
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg3" },
396
- ];
397
-
398
- const agent3 = new MockAgent(events3);
399
- const input3: RunAgentInput = {
400
- threadId,
401
- runId: "run3",
402
- messages: [],
403
- };
404
-
405
- await firstValueFrom(runner.run({ threadId, agent: agent3, input: input3 }).pipe(toArray()));
406
-
407
- // Check database directly
408
- const db = new Database(dbPath);
409
- const rows = db.prepare("SELECT run_id, events FROM agent_runs WHERE thread_id = ? ORDER BY created_at").all(threadId) as any[];
410
- db.close();
411
-
412
- expect(rows).toHaveLength(3);
413
-
414
- // All runs should have non-empty events
415
- for (let i = 0; i < rows.length; i++) {
416
- const events = JSON.parse(rows[i].events);
417
- expect(events).not.toHaveLength(0);
418
- expect(events.length).toBeGreaterThan(0);
419
-
420
- // Verify text messages are properly compacted
421
- const textContentEvents = events.filter((e: any) => e.type === EventType.TEXT_MESSAGE_CONTENT);
422
- textContentEvents.forEach((event: any) => {
423
- expect(event.delta).toBeTruthy();
424
- expect(event.delta).not.toBe("");
425
- });
426
- }
427
- });
428
-
429
- it("should retrieve already-compacted events without re-compacting", async () => {
430
- const threadId = "test-thread-no-recompact";
431
-
432
- // Create events that would be compacted
433
- const events: BaseEvent[] = [
434
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "assistant" },
435
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Part 1 " },
436
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Part 2 " },
437
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Part 3" },
438
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
439
- ];
440
-
441
- const agent = new MockAgent(events);
442
- const input: RunAgentInput = {
443
- threadId,
444
- runId: "run1",
445
- messages: [],
446
- };
447
-
448
- // Run and store compacted events
449
- await firstValueFrom(runner.run({ threadId, agent, input }).pipe(toArray()));
450
-
451
- // Connect and retrieve events
452
- const retrievedEvents = await firstValueFrom(
453
- runner.connect({ threadId }).pipe(toArray())
454
- );
455
-
456
- // Should have compacted format: START, single CONTENT, END
457
- expect(retrievedEvents).toHaveLength(3);
458
- expect(retrievedEvents[0].type).toBe(EventType.TEXT_MESSAGE_START);
459
- expect(retrievedEvents[1].type).toBe(EventType.TEXT_MESSAGE_CONTENT);
460
- expect((retrievedEvents[1] as any).delta).toBe("Part 1 Part 2 Part 3");
461
- expect(retrievedEvents[2].type).toBe(EventType.TEXT_MESSAGE_END);
462
- });
463
-
464
- it("should handle edge case where new events are identical to compacted previous events", async () => {
465
- const threadId = "test-thread-identical-after-compaction";
466
-
467
- // First run: send a compacted-looking message
468
- const events1: BaseEvent[] = [
469
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "user" },
470
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Hello World" },
471
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
472
- ];
473
-
474
- const agent1 = new MockAgent(events1);
475
- const input1: RunAgentInput = {
476
- threadId,
477
- runId: "run1",
478
- messages: [],
479
- };
480
-
481
- await firstValueFrom(runner.run({ threadId, agent: agent1, input: input1 }).pipe(toArray()));
482
-
483
- // Second run: send events that would compact to the same thing
484
- const events2: BaseEvent[] = [
485
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg2", role: "assistant" },
486
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "Hello " },
487
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "World" },
488
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg2" },
489
- ];
490
-
491
- const agent2 = new MockAgent(events2);
492
- const input2: RunAgentInput = {
493
- threadId,
494
- runId: "run2",
495
- messages: [],
496
- };
497
-
498
- await firstValueFrom(runner.run({ threadId, agent: agent2, input: input2 }).pipe(toArray()));
499
-
500
- // Check database directly
501
- const db = new Database(dbPath);
502
- const rows = db.prepare("SELECT run_id, events FROM agent_runs WHERE thread_id = ? ORDER BY created_at").all(threadId) as any[];
503
- db.close();
504
-
505
- expect(rows).toHaveLength(2);
506
-
507
- // Both runs should have events stored
508
- const run1Events = JSON.parse(rows[0].events);
509
- const run2Events = JSON.parse(rows[1].events);
510
-
511
- // Both should be non-empty
512
- expect(run1Events).not.toHaveLength(0);
513
- expect(run2Events).not.toHaveLength(0);
514
-
515
- // Both should have proper structure
516
- expect(run1Events).toHaveLength(3);
517
- expect(run2Events).toHaveLength(3);
518
-
519
- // Verify the content is correct
520
- expect(run1Events[1].delta).toBe("Hello World");
521
- expect(run2Events[1].delta).toBe("Hello World");
522
-
523
- // Messages should have different IDs
524
- expect(run1Events[0].messageId).toBe("msg1");
525
- expect(run2Events[0].messageId).toBe("msg2");
526
- });
527
- });
528
-
529
- describe("Comparison with InMemoryAgentRunner", () => {
530
- it("should behave identically to InMemoryAgentRunner for basic operations", async () => {
531
- const threadId = "test-thread-comparison";
532
- const events: BaseEvent[] = [
533
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "user" },
534
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Test message" },
535
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
536
- ];
537
-
538
- const agent = new MockAgent(events);
539
- const input: RunAgentInput = {
540
- threadId,
541
- runId: "run1",
542
- messages: [],
543
- };
544
-
545
- // Run with SQLite runner
546
- const sqliteRunner = new SqliteAgentRunner({ dbPath });
547
- const sqliteRunEvents = await firstValueFrom(
548
- sqliteRunner.run({ threadId, agent: agent.clone(), input }).pipe(toArray())
549
- );
550
- const sqliteConnectEvents = await firstValueFrom(
551
- sqliteRunner.connect({ threadId }).pipe(toArray())
552
- );
553
-
554
- // Run with InMemory runner
555
- const memoryRunner = new InMemoryAgentRunner();
556
- const memoryRunEvents = await firstValueFrom(
557
- memoryRunner.run({ threadId, agent: agent.clone(), input }).pipe(toArray())
558
- );
559
- const memoryConnectEvents = await firstValueFrom(
560
- memoryRunner.connect({ threadId }).pipe(toArray())
561
- );
562
-
563
- // Both should emit the same events
564
- expect(sqliteRunEvents).toEqual(memoryRunEvents);
565
- expect(sqliteConnectEvents).toEqual(memoryConnectEvents);
566
- });
567
- });
568
-
569
- describe("Input message handling", () => {
570
- it("should store NEW input messages but NOT old ones", async () => {
571
- const threadId = "test-thread-input-storage";
572
-
573
- // First run: create some messages
574
- const events1: BaseEvent[] = [
575
- { type: EventType.TEXT_MESSAGE_START, messageId: "first-run-msg", role: "assistant" },
576
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "first-run-msg", delta: "First run message" },
577
- { type: EventType.TEXT_MESSAGE_END, messageId: "first-run-msg" },
578
- ];
579
-
580
- const agent1 = new MockAgent(events1);
581
- const input1: RunAgentInput = {
582
- threadId,
583
- runId: "run1",
584
- messages: [],
585
- };
586
-
587
- await firstValueFrom(runner.run({ threadId, agent: agent1, input: input1 }).pipe(toArray()));
588
-
589
- // Second run: pass OLD message and a NEW message as input
590
- const events2: BaseEvent[] = [
591
- { type: EventType.TEXT_MESSAGE_START, messageId: "second-run-msg", role: "assistant" },
592
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "second-run-msg", delta: "Second run message" },
593
- { type: EventType.TEXT_MESSAGE_END, messageId: "second-run-msg" },
594
- ];
595
-
596
- const agent2 = new MockAgent(events2);
597
- const input2: RunAgentInput = {
598
- threadId,
599
- runId: "run2",
600
- messages: [
601
- {
602
- id: "first-run-msg", // This is OLD - from run1, should NOT be stored again
603
- role: "assistant",
604
- content: "First run message"
605
- },
606
- {
607
- id: "new-user-msg", // This is NEW - should be stored in run2
608
- role: "user",
609
- content: "This is a NEW user message that SHOULD be stored in run2"
610
- }
611
- ],
612
- };
613
-
614
- await firstValueFrom(runner.run({ threadId, agent: agent2, input: input2 }).pipe(toArray()));
615
-
616
- // Check database directly
617
- const db = new Database(dbPath);
618
- const rows = db.prepare("SELECT run_id, events FROM agent_runs WHERE thread_id = ? ORDER BY created_at").all(threadId) as any[];
619
- db.close();
620
-
621
- expect(rows).toHaveLength(2);
622
-
623
- // First run should only have its own message
624
- const run1Events = JSON.parse(rows[0].events);
625
- const run1MessageIds = run1Events.filter((e: any) => e.messageId).map((e: any) => e.messageId);
626
- expect(run1MessageIds).toContain("first-run-msg");
627
- expect(run1MessageIds).not.toContain("new-user-msg");
628
- expect(run1MessageIds).not.toContain("second-run-msg");
629
-
630
- // Second run should have the NEW user message and agent response, but NOT the old message
631
- const run2Events = JSON.parse(rows[1].events);
632
- const run2MessageIds = run2Events.filter((e: any) => e.messageId).map((e: any) => e.messageId);
633
- expect(run2MessageIds).toContain("second-run-msg"); // Agent's response
634
- expect(run2MessageIds).toContain("new-user-msg"); // NEW user message - SHOULD be stored
635
- expect(run2MessageIds).not.toContain("first-run-msg"); // OLD message - should NOT be stored again
636
-
637
- // Verify the second run has the right messages
638
- const uniqueRun2MessageIds = [...new Set(run2MessageIds)];
639
- expect(uniqueRun2MessageIds).toHaveLength(2); // Should have exactly 2 message IDs
640
- expect(uniqueRun2MessageIds).toContain("new-user-msg");
641
- expect(uniqueRun2MessageIds).toContain("second-run-msg");
642
- });
643
- });
644
-
645
- describe("Complete conversation flow", () => {
646
- it("should store ALL types of NEW messages including tool results", async () => {
647
- const threadId = "test-thread-all-message-types";
648
-
649
- // Run 1: User message with tool call and result
650
- const agent1 = new MockAgent([
651
- { type: EventType.TOOL_CALL_START, toolCallId: "tool-1", toolName: "calculator" },
652
- { type: EventType.TOOL_CALL_ARGS, toolCallId: "tool-1", delta: '{"a": 1, "b": 2}' },
653
- { type: EventType.TOOL_CALL_END, toolCallId: "tool-1" },
654
- ]);
655
-
656
- const input1: RunAgentInput = {
657
- threadId,
658
- runId: "run1",
659
- messages: [
660
- { id: "user-1", role: "user", content: "Calculate 1+2" },
661
- { id: "assistant-1", role: "assistant", content: "Let me calculate that", toolCalls: [
662
- { id: "tool-1", type: "function", function: { name: "calculator", arguments: JSON.stringify({ a: 1, b: 2 }) } }
663
- ]},
664
- { id: "tool-result-1", role: "tool", toolCallId: "tool-1", content: "3" }
665
- ],
666
- };
667
-
668
- await firstValueFrom(runner.run({ threadId, agent: agent1, input: input1 }).pipe(toArray()));
669
-
670
- // Run 2: Add more messages including system and developer
671
- const agent2 = new MockAgent([
672
- { type: EventType.TEXT_MESSAGE_START, messageId: "assistant-2", role: "assistant" },
673
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "assistant-2", delta: "The answer is 3" },
674
- { type: EventType.TEXT_MESSAGE_END, messageId: "assistant-2" },
675
- ]);
676
-
677
- const input2: RunAgentInput = {
678
- threadId,
679
- runId: "run2",
680
- messages: [
681
- // Old messages from run 1
682
- { id: "user-1", role: "user", content: "Calculate 1+2" },
683
- { id: "assistant-1", role: "assistant", content: "Let me calculate that", toolCalls: [
684
- { id: "tool-1", type: "function", function: { name: "calculator", arguments: JSON.stringify({ a: 1, b: 2 }) } }
685
- ]},
686
- { id: "tool-result-1", role: "tool", toolCallId: "tool-1", content: "3" },
687
- // New messages for run 2
688
- { id: "system-1", role: "system", content: "Be concise" },
689
- { id: "developer-1", role: "developer", content: "Use simple language" },
690
- { id: "user-2", role: "user", content: "What was the result?" }
691
- ],
692
- };
693
-
694
- await firstValueFrom(runner.run({ threadId, agent: agent2, input: input2 }).pipe(toArray()));
695
-
696
- // Check database
697
- const db = new Database(dbPath);
698
- const rows = db.prepare("SELECT run_id, events FROM agent_runs WHERE thread_id = ? ORDER BY created_at").all(threadId) as any[];
699
- db.close();
700
-
701
- expect(rows).toHaveLength(2);
702
-
703
- // Run 1 should have all the initial messages
704
- const run1Events = JSON.parse(rows[0].events);
705
- const run1MessageIds = [...new Set(run1Events.filter((e: any) => e.messageId).map((e: any) => e.messageId))];
706
- const run1ToolIds = [...new Set(run1Events.filter((e: any) => e.toolCallId).map((e: any) => e.toolCallId))];
707
-
708
- expect(run1MessageIds).toContain("user-1");
709
- expect(run1MessageIds).toContain("assistant-1");
710
- expect(run1ToolIds).toContain("tool-1"); // Tool events from both input and agent
711
-
712
- // Verify tool result event is stored
713
- const toolResultEvents = run1Events.filter((e: any) => e.type === EventType.TOOL_CALL_RESULT);
714
- expect(toolResultEvents).toHaveLength(1);
715
- expect(toolResultEvents[0].toolCallId).toBe("tool-1");
716
- expect(toolResultEvents[0].content).toBe("3");
717
-
718
- // Run 2 should have ONLY the new messages
719
- const run2Events = JSON.parse(rows[1].events);
720
- const run2MessageIds = [...new Set(run2Events.filter((e: any) => e.messageId).map((e: any) => e.messageId))];
721
-
722
- expect(run2MessageIds).toContain("system-1"); // NEW system message
723
- expect(run2MessageIds).toContain("developer-1"); // NEW developer message
724
- expect(run2MessageIds).toContain("user-2"); // NEW user message
725
- expect(run2MessageIds).toContain("assistant-2"); // NEW assistant response
726
-
727
- // Should NOT contain old messages
728
- expect(run2MessageIds).not.toContain("user-1");
729
- expect(run2MessageIds).not.toContain("assistant-1");
730
-
731
- // Should NOT contain old tool results
732
- const run2ToolResults = run2Events.filter((e: any) => e.type === EventType.TOOL_CALL_RESULT);
733
- expect(run2ToolResults.filter((e: any) => e.toolCallId === "tool-1")).toHaveLength(0);
734
-
735
- // Verify we captured all 4 new message types in run 2
736
- const run2EventTypes = new Set(run2Events.map((e: any) => e.type));
737
- expect(run2EventTypes.has(EventType.TEXT_MESSAGE_START)).toBe(true);
738
- expect(run2EventTypes.has(EventType.TEXT_MESSAGE_CONTENT)).toBe(true);
739
- expect(run2EventTypes.has(EventType.TEXT_MESSAGE_END)).toBe(true);
740
- });
741
-
742
- it("should correctly store a multi-turn conversation", async () => {
743
- const threadId = "test-thread-conversation";
744
-
745
- // Run 1: Initial user message and agent response
746
- const agent1 = new MockAgent([
747
- { type: EventType.TEXT_MESSAGE_START, messageId: "agent-1", role: "assistant" },
748
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "agent-1", delta: "Hello! How can I help?" },
749
- { type: EventType.TEXT_MESSAGE_END, messageId: "agent-1" },
750
- ]);
751
-
752
- const input1: RunAgentInput = {
753
- threadId,
754
- runId: "run1",
755
- messages: [
756
- { id: "user-1", role: "user", content: "Hi!" }
757
- ],
758
- };
759
-
760
- await firstValueFrom(runner.run({ threadId, agent: agent1, input: input1 }).pipe(toArray()));
761
-
762
- // Run 2: Second user message and agent response
763
- const agent2 = new MockAgent([
764
- { type: EventType.TEXT_MESSAGE_START, messageId: "agent-2", role: "assistant" },
765
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "agent-2", delta: "The weather is nice today!" },
766
- { type: EventType.TEXT_MESSAGE_END, messageId: "agent-2" },
767
- ]);
768
-
769
- const input2: RunAgentInput = {
770
- threadId,
771
- runId: "run2",
772
- messages: [
773
- { id: "user-1", role: "user", content: "Hi!" },
774
- { id: "agent-1", role: "assistant", content: "Hello! How can I help?" },
775
- { id: "user-2", role: "user", content: "What's the weather?" }
776
- ],
777
- };
778
-
779
- await firstValueFrom(runner.run({ threadId, agent: agent2, input: input2 }).pipe(toArray()));
780
-
781
- // Check database
782
- const db = new Database(dbPath);
783
- const rows = db.prepare("SELECT run_id, events FROM agent_runs WHERE thread_id = ? ORDER BY created_at").all(threadId) as any[];
784
- db.close();
785
-
786
- expect(rows).toHaveLength(2);
787
-
788
- // Run 1 should have user-1 and agent-1
789
- const run1Events = JSON.parse(rows[0].events);
790
- const run1MessageIds = [...new Set(run1Events.filter((e: any) => e.messageId).map((e: any) => e.messageId))];
791
- expect(run1MessageIds).toHaveLength(2);
792
- expect(run1MessageIds).toContain("user-1");
793
- expect(run1MessageIds).toContain("agent-1");
794
-
795
- // Run 2 should have ONLY user-2 and agent-2 (not the old messages)
796
- const run2Events = JSON.parse(rows[1].events);
797
- const run2MessageIds = [...new Set(run2Events.filter((e: any) => e.messageId).map((e: any) => e.messageId))];
798
- expect(run2MessageIds).toHaveLength(2);
799
- expect(run2MessageIds).toContain("user-2");
800
- expect(run2MessageIds).toContain("agent-2");
801
- expect(run2MessageIds).not.toContain("user-1");
802
- expect(run2MessageIds).not.toContain("agent-1");
803
- });
804
- });
805
-
806
- describe("Database integrity", () => {
807
- it("should create all required tables", () => {
808
- const db = new Database(dbPath);
809
-
810
- // Check agent_runs table exists
811
- const agentRunsTable = db.prepare(
812
- "SELECT name FROM sqlite_master WHERE type='table' AND name='agent_runs'"
813
- ).get();
814
- expect(agentRunsTable).toBeDefined();
815
-
816
- // Check run_state table exists
817
- const runStateTable = db.prepare(
818
- "SELECT name FROM sqlite_master WHERE type='table' AND name='run_state'"
819
- ).get();
820
- expect(runStateTable).toBeDefined();
821
-
822
- // Check schema_version table exists
823
- const schemaVersionTable = db.prepare(
824
- "SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'"
825
- ).get();
826
- expect(schemaVersionTable).toBeDefined();
827
-
828
- db.close();
829
- });
830
-
831
- it("should handle database file creation", () => {
832
- // Database file should be created
833
- expect(fs.existsSync(dbPath)).toBe(true);
834
- });
835
-
836
- it("should never store empty events array", async () => {
837
- const threadId = "test-thread-no-empty";
838
-
839
- // Test with events that should be stored
840
- const events: BaseEvent[] = [
841
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "assistant" },
842
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Test" },
843
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
844
- ];
845
-
846
- const agent = new MockAgent(events);
847
- const input: RunAgentInput = {
848
- threadId,
849
- runId: "run1",
850
- messages: [],
851
- };
852
-
853
- // Run the agent
854
- await firstValueFrom(runner.run({ threadId, agent, input }).pipe(toArray()));
855
-
856
- // Check database directly
857
- const db = new Database(dbPath);
858
- const rows = db.prepare("SELECT events FROM agent_runs WHERE thread_id = ?").all(threadId) as any[];
859
- db.close();
860
-
861
- // Should have one run
862
- expect(rows).toHaveLength(1);
863
-
864
- // Parse and check events are not empty
865
- const storedEvents = JSON.parse(rows[0].events);
866
- expect(storedEvents).not.toHaveLength(0);
867
- expect(storedEvents.length).toBeGreaterThan(0);
868
- expect(storedEvents).toEqual(expect.arrayContaining([
869
- expect.objectContaining({ type: EventType.TEXT_MESSAGE_START }),
870
- expect.objectContaining({ type: EventType.TEXT_MESSAGE_CONTENT }),
871
- expect.objectContaining({ type: EventType.TEXT_MESSAGE_END }),
872
- ]));
873
- });
874
-
875
- it("should store correct events after compaction on subsequent runs", async () => {
876
- const threadId = "test-thread-subsequent-runs";
877
-
878
- // First run with initial events
879
- const events1: BaseEvent[] = [
880
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg1", role: "user" },
881
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg1", delta: "Hello" },
882
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg1" },
883
- ];
884
-
885
- const agent1 = new MockAgent(events1);
886
- const input1: RunAgentInput = {
887
- threadId,
888
- runId: "run1",
889
- messages: [],
890
- };
891
-
892
- await firstValueFrom(runner.run({ threadId, agent: agent1, input: input1 }).pipe(toArray()));
893
-
894
- // Second run with new events
895
- const events2: BaseEvent[] = [
896
- { type: EventType.TEXT_MESSAGE_START, messageId: "msg2", role: "assistant" },
897
- { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "msg2", delta: "World" },
898
- { type: EventType.TEXT_MESSAGE_END, messageId: "msg2" },
899
- ];
900
-
901
- const agent2 = new MockAgent(events2);
902
- const input2: RunAgentInput = {
903
- threadId,
904
- runId: "run2",
905
- messages: [],
906
- };
907
-
908
- await firstValueFrom(runner.run({ threadId, agent: agent2, input: input2 }).pipe(toArray()));
909
-
910
- // Check database directly
911
- const db = new Database(dbPath);
912
- const rows = db.prepare("SELECT run_id, events FROM agent_runs WHERE thread_id = ? ORDER BY created_at").all(threadId) as any[];
913
- db.close();
914
-
915
- // Should have two runs
916
- expect(rows).toHaveLength(2);
917
-
918
- // Both runs should have non-empty events
919
- const run1Events = JSON.parse(rows[0].events);
920
- const run2Events = JSON.parse(rows[1].events);
921
-
922
- expect(run1Events).not.toHaveLength(0);
923
- expect(run2Events).not.toHaveLength(0);
924
-
925
- // First run should have ONLY the first message events
926
- expect(run1Events).toEqual(expect.arrayContaining([
927
- expect.objectContaining({ messageId: "msg1" }),
928
- ]));
929
- expect(run1Events.every((e: any) => !e.messageId || e.messageId === "msg1")).toBe(true);
930
-
931
- // Second run should have ONLY the second message events
932
- expect(run2Events).toEqual(expect.arrayContaining([
933
- expect.objectContaining({ messageId: "msg2" }),
934
- ]));
935
- expect(run2Events.every((e: any) => !e.messageId || e.messageId === "msg2")).toBe(true);
936
-
937
- // Verify no message duplication across runs
938
- const run1MessageIds = new Set(run1Events.filter((e: any) => e.messageId).map((e: any) => e.messageId));
939
- const run2MessageIds = new Set(run2Events.filter((e: any) => e.messageId).map((e: any) => e.messageId));
940
- const intersection = [...run1MessageIds].filter(id => run2MessageIds.has(id));
941
- expect(intersection).toHaveLength(0);
942
- });
943
-
944
- it("should handle edge case with no new events after compaction", async () => {
945
- const threadId = "test-thread-edge-case";
946
-
947
- // Run with duplicate events that might compact to nothing new
948
- const events: BaseEvent[] = [
949
- { type: EventType.RUN_STARTED, runId: "run1" },
950
- { type: EventType.RUN_FINISHED, runId: "run1" },
951
- ];
952
-
953
- const agent = new MockAgent(events);
954
- const input: RunAgentInput = {
955
- threadId,
956
- runId: "run1",
957
- messages: [],
958
- };
959
-
960
- await firstValueFrom(runner.run({ threadId, agent, input }).pipe(toArray()));
961
-
962
- // Check database
963
- const db = new Database(dbPath);
964
- const rows = db.prepare("SELECT events FROM agent_runs WHERE thread_id = ?").all(threadId) as any[];
965
- db.close();
966
-
967
- // Should have stored the run
968
- expect(rows).toHaveLength(1);
969
-
970
- // Events should be stored (even if they are minimal after compaction)
971
- const storedEvents = JSON.parse(rows[0].events);
972
- expect(Array.isArray(storedEvents)).toBe(true);
973
- });
974
- });
975
- });