@copilotkitnext/runtime 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/runtime.always.mdc +9 -0
- package/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-check-types.log +4 -0
- package/.turbo/turbo-lint.log +56 -0
- package/.turbo/turbo-test$colon$coverage.log +149 -0
- package/.turbo/turbo-test.log +107 -0
- package/LICENSE +11 -0
- package/README-RUNNERS.md +78 -0
- package/dist/index.d.mts +245 -0
- package/dist/index.d.ts +245 -0
- package/dist/index.js +1873 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1841 -0
- package/dist/index.mjs.map +1 -0
- package/eslint.config.mjs +3 -0
- package/package.json +62 -0
- package/src/__tests__/get-runtime-info.test.ts +117 -0
- package/src/__tests__/handle-run.test.ts +69 -0
- package/src/__tests__/handle-transcribe.test.ts +289 -0
- package/src/__tests__/in-process-agent-runner-messages.test.ts +599 -0
- package/src/__tests__/in-process-agent-runner.test.ts +726 -0
- package/src/__tests__/middleware.test.ts +432 -0
- package/src/__tests__/routing.test.ts +257 -0
- package/src/endpoint.ts +150 -0
- package/src/handler.ts +3 -0
- package/src/handlers/get-runtime-info.ts +50 -0
- package/src/handlers/handle-connect.ts +144 -0
- package/src/handlers/handle-run.ts +156 -0
- package/src/handlers/handle-transcribe.ts +126 -0
- package/src/index.ts +8 -0
- package/src/middleware.ts +232 -0
- package/src/runner/__tests__/enterprise-runner.test.ts +992 -0
- package/src/runner/__tests__/event-compaction.test.ts +253 -0
- package/src/runner/__tests__/in-memory-runner.test.ts +483 -0
- package/src/runner/__tests__/sqlite-runner.test.ts +975 -0
- package/src/runner/agent-runner.ts +27 -0
- package/src/runner/enterprise.ts +653 -0
- package/src/runner/event-compaction.ts +250 -0
- package/src/runner/in-memory.ts +322 -0
- package/src/runner/index.ts +0 -0
- package/src/runner/sqlite.ts +481 -0
- package/src/runtime.ts +53 -0
- package/src/transcription-service/transcription-service-openai.ts +29 -0
- package/src/transcription-service/transcription-service.ts +11 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.mjs +15 -0
|
@@ -0,0 +1,992 @@
|
|
|
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
|
+
});
|