@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.
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/.turbo/turbo-build.log +0 -23
- package/.turbo/turbo-check-types.log +0 -4
- package/.turbo/turbo-lint.log +0 -56
- package/.turbo/turbo-test$colon$coverage.log +0 -149
- package/.turbo/turbo-test.log +0 -108
- package/src/__tests__/get-runtime-info.test.ts +0 -117
- package/src/__tests__/handle-run.test.ts +0 -69
- package/src/__tests__/handle-transcribe.test.ts +0 -289
- package/src/__tests__/in-process-agent-runner-messages.test.ts +0 -599
- package/src/__tests__/in-process-agent-runner.test.ts +0 -726
- package/src/__tests__/middleware.test.ts +0 -432
- package/src/__tests__/routing.test.ts +0 -257
- package/src/endpoint.ts +0 -150
- package/src/handler.ts +0 -3
- package/src/handlers/get-runtime-info.ts +0 -50
- package/src/handlers/handle-connect.ts +0 -144
- package/src/handlers/handle-run.ts +0 -156
- package/src/handlers/handle-transcribe.ts +0 -126
- package/src/index.ts +0 -8
- package/src/middleware.ts +0 -232
- package/src/runner/__tests__/enterprise-runner.test.ts +0 -992
- package/src/runner/__tests__/event-compaction.test.ts +0 -253
- package/src/runner/__tests__/in-memory-runner.test.ts +0 -483
- package/src/runner/__tests__/sqlite-runner.test.ts +0 -975
- package/src/runner/agent-runner.ts +0 -27
- package/src/runner/enterprise.ts +0 -653
- package/src/runner/event-compaction.ts +0 -250
- package/src/runner/in-memory.ts +0 -328
- package/src/runner/index.ts +0 -0
- package/src/runner/sqlite.ts +0 -481
- package/src/runtime.ts +0 -53
- package/src/transcription-service/transcription-service-openai.ts +0 -29
- package/src/transcription-service/transcription-service.ts +0 -11
- package/tsconfig.json +0 -13
- 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
|
-
});
|