@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.
Files changed (47) hide show
  1. package/.cursor/rules/runtime.always.mdc +9 -0
  2. package/.turbo/turbo-build.log +22 -0
  3. package/.turbo/turbo-check-types.log +4 -0
  4. package/.turbo/turbo-lint.log +56 -0
  5. package/.turbo/turbo-test$colon$coverage.log +149 -0
  6. package/.turbo/turbo-test.log +107 -0
  7. package/LICENSE +11 -0
  8. package/README-RUNNERS.md +78 -0
  9. package/dist/index.d.mts +245 -0
  10. package/dist/index.d.ts +245 -0
  11. package/dist/index.js +1873 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/index.mjs +1841 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/eslint.config.mjs +3 -0
  16. package/package.json +62 -0
  17. package/src/__tests__/get-runtime-info.test.ts +117 -0
  18. package/src/__tests__/handle-run.test.ts +69 -0
  19. package/src/__tests__/handle-transcribe.test.ts +289 -0
  20. package/src/__tests__/in-process-agent-runner-messages.test.ts +599 -0
  21. package/src/__tests__/in-process-agent-runner.test.ts +726 -0
  22. package/src/__tests__/middleware.test.ts +432 -0
  23. package/src/__tests__/routing.test.ts +257 -0
  24. package/src/endpoint.ts +150 -0
  25. package/src/handler.ts +3 -0
  26. package/src/handlers/get-runtime-info.ts +50 -0
  27. package/src/handlers/handle-connect.ts +144 -0
  28. package/src/handlers/handle-run.ts +156 -0
  29. package/src/handlers/handle-transcribe.ts +126 -0
  30. package/src/index.ts +8 -0
  31. package/src/middleware.ts +232 -0
  32. package/src/runner/__tests__/enterprise-runner.test.ts +992 -0
  33. package/src/runner/__tests__/event-compaction.test.ts +253 -0
  34. package/src/runner/__tests__/in-memory-runner.test.ts +483 -0
  35. package/src/runner/__tests__/sqlite-runner.test.ts +975 -0
  36. package/src/runner/agent-runner.ts +27 -0
  37. package/src/runner/enterprise.ts +653 -0
  38. package/src/runner/event-compaction.ts +250 -0
  39. package/src/runner/in-memory.ts +322 -0
  40. package/src/runner/index.ts +0 -0
  41. package/src/runner/sqlite.ts +481 -0
  42. package/src/runtime.ts +53 -0
  43. package/src/transcription-service/transcription-service-openai.ts +29 -0
  44. package/src/transcription-service/transcription-service.ts +11 -0
  45. package/tsconfig.json +13 -0
  46. package/tsup.config.ts +11 -0
  47. package/vitest.config.mjs +15 -0
@@ -0,0 +1,27 @@
1
+ import { AbstractAgent, BaseEvent, RunAgentInput } from "@ag-ui/client";
2
+ import { Observable } from "rxjs";
3
+
4
+ export interface AgentRunnerRunRequest {
5
+ threadId: string;
6
+ agent: AbstractAgent;
7
+ input: RunAgentInput;
8
+ }
9
+
10
+ export interface AgentRunnerConnectRequest {
11
+ threadId: string;
12
+ }
13
+
14
+ export interface AgentRunnerIsRunningRequest {
15
+ threadId: string;
16
+ }
17
+
18
+ export interface AgentRunnerStopRequest {
19
+ threadId: string;
20
+ }
21
+
22
+ export abstract class AgentRunner {
23
+ abstract run(request: AgentRunnerRunRequest): Observable<BaseEvent>;
24
+ abstract connect(request: AgentRunnerConnectRequest): Observable<BaseEvent>;
25
+ abstract isRunning(request: AgentRunnerIsRunningRequest): Promise<boolean>;
26
+ abstract stop(request: AgentRunnerStopRequest): Promise<boolean | undefined>;
27
+ }
@@ -0,0 +1,653 @@
1
+ import {
2
+ AgentRunner,
3
+ AgentRunnerConnectRequest,
4
+ AgentRunnerIsRunningRequest,
5
+ AgentRunnerRunRequest,
6
+ type AgentRunnerStopRequest,
7
+ } from "./agent-runner";
8
+ import { Observable, ReplaySubject } from "rxjs";
9
+ import {
10
+ BaseEvent,
11
+ RunAgentInput,
12
+ Message,
13
+ EventType,
14
+ TextMessageStartEvent,
15
+ TextMessageContentEvent,
16
+ TextMessageEndEvent,
17
+ ToolCallStartEvent,
18
+ ToolCallArgsEvent,
19
+ ToolCallEndEvent,
20
+ ToolCallResultEvent,
21
+ } from "@ag-ui/client";
22
+ import { compactEvents } from "./event-compaction";
23
+ import { Kysely, Generated } from "kysely";
24
+ import { Redis } from "ioredis";
25
+
26
+ const SCHEMA_VERSION = 1;
27
+
28
+ interface AgentDatabase {
29
+ agent_runs: {
30
+ id: Generated<number>;
31
+ thread_id: string;
32
+ run_id: string;
33
+ parent_run_id: string | null;
34
+ events: string;
35
+ input: string;
36
+ created_at: number;
37
+ version: number;
38
+ };
39
+
40
+ run_state: {
41
+ thread_id: string;
42
+ is_running: number;
43
+ current_run_id: string | null;
44
+ server_id: string | null;
45
+ updated_at: number;
46
+ };
47
+
48
+ schema_version: {
49
+ version: number;
50
+ applied_at: number;
51
+ };
52
+ }
53
+
54
+ interface AgentRunRecord {
55
+ id: number;
56
+ thread_id: string;
57
+ run_id: string;
58
+ parent_run_id: string | null;
59
+ events: string;
60
+ input: string;
61
+ created_at: number;
62
+ version: number;
63
+ }
64
+
65
+ const redisKeys = {
66
+ stream: (threadId: string, runId: string) => `stream:${threadId}:${runId}`,
67
+ active: (threadId: string) => `active:${threadId}`,
68
+ lock: (threadId: string) => `lock:${threadId}`,
69
+ };
70
+
71
+ export interface EnterpriseAgentRunnerOptions {
72
+ kysely: Kysely<AgentDatabase>;
73
+ redis: Redis;
74
+ redisSub?: Redis;
75
+ streamRetentionMs?: number;
76
+ streamActiveTTLMs?: number;
77
+ lockTTLMs?: number;
78
+ serverId?: string;
79
+ }
80
+
81
+ export class EnterpriseAgentRunner extends AgentRunner {
82
+ private db: Kysely<AgentDatabase>;
83
+ public redis: Redis;
84
+ public redisSub: Redis;
85
+ private serverId: string;
86
+ private streamRetentionMs: number;
87
+ private streamActiveTTLMs: number;
88
+ private lockTTLMs: number;
89
+
90
+ constructor(options: EnterpriseAgentRunnerOptions) {
91
+ super();
92
+ this.db = options.kysely;
93
+ this.redis = options.redis;
94
+ this.redisSub = options.redisSub || options.redis.duplicate();
95
+ this.serverId = options.serverId || this.generateServerId();
96
+ this.streamRetentionMs = options.streamRetentionMs ?? 3600000; // 1 hour
97
+ this.streamActiveTTLMs = options.streamActiveTTLMs ?? 300000; // 5 minutes
98
+ this.lockTTLMs = options.lockTTLMs ?? 300000; // 5 minutes
99
+
100
+ this.initializeSchema();
101
+ }
102
+
103
+ run(request: AgentRunnerRunRequest): Observable<BaseEvent> {
104
+ const runSubject = new ReplaySubject<BaseEvent>(Infinity);
105
+
106
+ const executeRun = async () => {
107
+ const { threadId, input, agent } = request;
108
+ const runId = input.runId;
109
+ const streamKey = redisKeys.stream(threadId, runId);
110
+
111
+ // Check if thread already running (do this check synchronously for consistency with SQLite)
112
+ // For now we'll just check after, but in production you might want a sync check
113
+ const activeRunId = await this.redis.get(redisKeys.active(threadId));
114
+ if (activeRunId) {
115
+ throw new Error("Thread already running");
116
+ }
117
+
118
+ // Acquire distributed lock
119
+ const lockAcquired = await this.redis.set(
120
+ redisKeys.lock(threadId),
121
+ this.serverId,
122
+ 'PX', this.lockTTLMs,
123
+ 'NX'
124
+ );
125
+
126
+ if (!lockAcquired) {
127
+ throw new Error("Thread already running");
128
+ }
129
+
130
+ // Mark as active
131
+ await this.redis.setex(
132
+ redisKeys.active(threadId),
133
+ Math.floor(this.lockTTLMs / 1000),
134
+ runId
135
+ );
136
+
137
+ // Update database state
138
+ await this.setRunState(threadId, true, runId);
139
+
140
+ // Track events and message IDs
141
+ const currentRunEvents: BaseEvent[] = [];
142
+ const seenMessageIds = new Set<string>();
143
+
144
+ // Get historic message IDs
145
+ const historicRuns = await this.getHistoricRuns(threadId);
146
+ const historicMessageIds = new Set<string>();
147
+ for (const run of historicRuns) {
148
+ const events = JSON.parse(run.events) as BaseEvent[];
149
+ for (const event of events) {
150
+ if ('messageId' in event && typeof event.messageId === 'string') {
151
+ historicMessageIds.add(event.messageId);
152
+ }
153
+ }
154
+ }
155
+
156
+ const parentRunId = historicRuns[historicRuns.length - 1]?.run_id ?? null;
157
+
158
+ try {
159
+ await agent.runAgent(input, {
160
+ onEvent: async ({ event }) => {
161
+ // Emit to run() caller
162
+ runSubject.next(event);
163
+
164
+ // Collect for database
165
+ currentRunEvents.push(event);
166
+
167
+ // Stream to Redis for connect() subscribers
168
+ await this.redis.xadd(
169
+ streamKey,
170
+ 'MAXLEN', '~', '10000',
171
+ '*',
172
+ 'type', event.type,
173
+ 'data', JSON.stringify(event)
174
+ );
175
+
176
+ // Refresh TTL with sliding window during active writes
177
+ await this.redis.pexpire(streamKey, this.streamActiveTTLMs);
178
+
179
+ // Check for completion events
180
+ if (event.type === EventType.RUN_FINISHED ||
181
+ event.type === EventType.RUN_ERROR) {
182
+ // Switch to retention TTL for late readers
183
+ await this.redis.pexpire(streamKey, this.streamRetentionMs);
184
+ }
185
+ },
186
+
187
+ onNewMessage: ({ message }) => {
188
+ if (!seenMessageIds.has(message.id)) {
189
+ seenMessageIds.add(message.id);
190
+ }
191
+ },
192
+
193
+ onRunStartedEvent: async () => {
194
+ // Process input messages
195
+ if (input.messages) {
196
+ for (const message of input.messages) {
197
+ if (!seenMessageIds.has(message.id)) {
198
+ seenMessageIds.add(message.id);
199
+ const events = this.convertMessageToEvents(message);
200
+ const isNewMessage = !historicMessageIds.has(message.id);
201
+
202
+ for (const event of events) {
203
+ // Stream to Redis for context
204
+ await this.redis.xadd(
205
+ streamKey,
206
+ 'MAXLEN', '~', '10000',
207
+ '*',
208
+ 'type', event.type,
209
+ 'data', JSON.stringify(event)
210
+ );
211
+
212
+ if (isNewMessage) {
213
+ currentRunEvents.push(event);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // Refresh TTL
221
+ await this.redis.pexpire(streamKey, this.streamActiveTTLMs);
222
+ },
223
+ });
224
+
225
+ // Store to database
226
+ const compactedEvents = compactEvents(currentRunEvents);
227
+ await this.storeRun(threadId, runId, compactedEvents, input, parentRunId);
228
+
229
+ } finally {
230
+ // Clean up (even on error)
231
+ await this.setRunState(threadId, false);
232
+ await this.redis.del(redisKeys.active(threadId));
233
+ await this.redis.del(redisKeys.lock(threadId));
234
+
235
+ // Ensure stream has retention TTL for late readers
236
+ const exists = await this.redis.exists(streamKey);
237
+ if (exists) {
238
+ await this.redis.pexpire(streamKey, this.streamRetentionMs);
239
+ }
240
+
241
+ runSubject.complete();
242
+ }
243
+ };
244
+
245
+ executeRun().catch((error) => {
246
+ runSubject.error(error);
247
+ });
248
+
249
+ return runSubject.asObservable();
250
+ }
251
+
252
+ connect(request: AgentRunnerConnectRequest): Observable<BaseEvent> {
253
+ const connectionSubject = new ReplaySubject<BaseEvent>(Infinity);
254
+
255
+ const streamConnection = async () => {
256
+ const { threadId } = request;
257
+
258
+ // Load and emit historic runs from database
259
+ const historicRuns = await this.getHistoricRuns(threadId);
260
+ const allHistoricEvents: BaseEvent[] = [];
261
+
262
+ for (const run of historicRuns) {
263
+ const events = JSON.parse(run.events) as BaseEvent[];
264
+ allHistoricEvents.push(...events);
265
+ }
266
+
267
+ // Compact and emit historic events
268
+ const compactedEvents = compactEvents(allHistoricEvents);
269
+ const emittedMessageIds = new Set<string>();
270
+
271
+ for (const event of compactedEvents) {
272
+ connectionSubject.next(event);
273
+ if ('messageId' in event && typeof event.messageId === 'string') {
274
+ emittedMessageIds.add(event.messageId);
275
+ }
276
+ }
277
+
278
+ // Check for active run
279
+ const activeRunId = await this.redis.get(redisKeys.active(threadId));
280
+
281
+ if (activeRunId) {
282
+ // Tail the run-specific Redis stream
283
+ const streamKey = redisKeys.stream(threadId, activeRunId);
284
+ let lastId = '0-0';
285
+ let consecutiveEmptyReads = 0;
286
+
287
+ while (true) {
288
+ try {
289
+ // Read with blocking using call method for better compatibility
290
+ const result = await this.redis.call(
291
+ 'XREAD',
292
+ 'BLOCK', '5000',
293
+ 'COUNT', '100',
294
+ 'STREAMS', streamKey, lastId
295
+ ) as [string, [string, string[]][]][] | null;
296
+
297
+ if (!result || result.length === 0) {
298
+ consecutiveEmptyReads++;
299
+
300
+ // Check if stream still exists
301
+ const exists = await this.redis.exists(streamKey);
302
+ if (!exists) {
303
+ // Stream expired, we're done
304
+ break;
305
+ }
306
+
307
+ // Check if thread still active
308
+ const stillActive = await this.redis.get(redisKeys.active(threadId));
309
+ if (stillActive !== activeRunId) {
310
+ // Different run started or thread stopped
311
+ break;
312
+ }
313
+
314
+ // After multiple empty reads, assume completion
315
+ if (consecutiveEmptyReads > 3) {
316
+ break;
317
+ }
318
+
319
+ continue;
320
+ }
321
+
322
+ consecutiveEmptyReads = 0;
323
+ const [, messages] = result[0] || [null, []];
324
+
325
+ for (const [id, fields] of messages || []) {
326
+ lastId = id;
327
+
328
+ // Extract event data (fields is array: [key, value, key, value, ...])
329
+ let eventData: string | null = null;
330
+ let eventType: string | null = null;
331
+
332
+ for (let i = 0; i < fields.length; i += 2) {
333
+ if (fields[i] === 'data') {
334
+ eventData = fields[i + 1] ?? null;
335
+ } else if (fields[i] === 'type') {
336
+ eventType = fields[i + 1] ?? null;
337
+ }
338
+ }
339
+
340
+ if (eventData) {
341
+ const event = JSON.parse(eventData) as BaseEvent;
342
+
343
+ // Skip already emitted messages
344
+ if ('messageId' in event &&
345
+ typeof event.messageId === 'string' &&
346
+ emittedMessageIds.has(event.messageId)) {
347
+ continue;
348
+ }
349
+
350
+ connectionSubject.next(event);
351
+
352
+ // Check for completion events
353
+ if (eventType === EventType.RUN_FINISHED ||
354
+ eventType === EventType.RUN_ERROR) {
355
+ connectionSubject.complete();
356
+ return;
357
+ }
358
+ }
359
+ }
360
+ } catch {
361
+ // Redis error, complete the stream
362
+ break;
363
+ }
364
+ }
365
+ }
366
+
367
+ connectionSubject.complete();
368
+ };
369
+
370
+ streamConnection().catch(() => connectionSubject.complete());
371
+ return connectionSubject.asObservable();
372
+ }
373
+
374
+ async isRunning(request: AgentRunnerIsRunningRequest): Promise<boolean> {
375
+ const { threadId } = request;
376
+
377
+ // Check Redis first for speed
378
+ const activeRunId = await this.redis.get(redisKeys.active(threadId));
379
+ if (activeRunId) return true;
380
+
381
+ // Check lock
382
+ const lockExists = await this.redis.exists(redisKeys.lock(threadId));
383
+ if (lockExists) return true;
384
+
385
+ // Fallback to database
386
+ const state = await this.db
387
+ .selectFrom('run_state')
388
+ .where('thread_id', '=', threadId)
389
+ .selectAll()
390
+ .executeTakeFirst();
391
+
392
+ return state?.is_running === 1;
393
+ }
394
+
395
+ async stop(request: AgentRunnerStopRequest): Promise<boolean> {
396
+ const { threadId } = request;
397
+
398
+ // Get active run ID
399
+ const activeRunId = await this.redis.get(redisKeys.active(threadId));
400
+ if (!activeRunId) {
401
+ return false;
402
+ }
403
+
404
+ // Add RUN_ERROR event to stream
405
+ const streamKey = redisKeys.stream(threadId, activeRunId);
406
+ await this.redis.xadd(
407
+ streamKey,
408
+ '*',
409
+ 'type', EventType.RUN_ERROR,
410
+ 'data', JSON.stringify({
411
+ type: EventType.RUN_ERROR,
412
+ error: 'Run stopped by user'
413
+ })
414
+ );
415
+
416
+ // Set retention TTL
417
+ await this.redis.pexpire(streamKey, this.streamRetentionMs);
418
+
419
+ // Clean up
420
+ await this.setRunState(threadId, false);
421
+ await this.redis.del(redisKeys.active(threadId));
422
+ await this.redis.del(redisKeys.lock(threadId));
423
+
424
+ return true;
425
+ }
426
+
427
+ // Helper methods
428
+ private convertMessageToEvents(message: Message): BaseEvent[] {
429
+ const events: BaseEvent[] = [];
430
+
431
+ if (
432
+ (message.role === "assistant" ||
433
+ message.role === "user" ||
434
+ message.role === "developer" ||
435
+ message.role === "system") &&
436
+ message.content
437
+ ) {
438
+ const textStartEvent: TextMessageStartEvent = {
439
+ type: EventType.TEXT_MESSAGE_START,
440
+ messageId: message.id,
441
+ role: message.role,
442
+ };
443
+ events.push(textStartEvent);
444
+
445
+ const textContentEvent: TextMessageContentEvent = {
446
+ type: EventType.TEXT_MESSAGE_CONTENT,
447
+ messageId: message.id,
448
+ delta: message.content,
449
+ };
450
+ events.push(textContentEvent);
451
+
452
+ const textEndEvent: TextMessageEndEvent = {
453
+ type: EventType.TEXT_MESSAGE_END,
454
+ messageId: message.id,
455
+ };
456
+ events.push(textEndEvent);
457
+ }
458
+
459
+ if (message.role === "assistant" && message.toolCalls) {
460
+ for (const toolCall of message.toolCalls) {
461
+ const toolStartEvent: ToolCallStartEvent = {
462
+ type: EventType.TOOL_CALL_START,
463
+ toolCallId: toolCall.id,
464
+ toolCallName: toolCall.function.name,
465
+ parentMessageId: message.id,
466
+ };
467
+ events.push(toolStartEvent);
468
+
469
+ const toolArgsEvent: ToolCallArgsEvent = {
470
+ type: EventType.TOOL_CALL_ARGS,
471
+ toolCallId: toolCall.id,
472
+ delta: toolCall.function.arguments,
473
+ };
474
+ events.push(toolArgsEvent);
475
+
476
+ const toolEndEvent: ToolCallEndEvent = {
477
+ type: EventType.TOOL_CALL_END,
478
+ toolCallId: toolCall.id,
479
+ };
480
+ events.push(toolEndEvent);
481
+ }
482
+ }
483
+
484
+ if (message.role === "tool" && message.toolCallId) {
485
+ const toolResultEvent: ToolCallResultEvent = {
486
+ type: EventType.TOOL_CALL_RESULT,
487
+ messageId: message.id,
488
+ toolCallId: message.toolCallId,
489
+ content: message.content,
490
+ role: "tool",
491
+ };
492
+ events.push(toolResultEvent);
493
+ }
494
+
495
+ return events;
496
+ }
497
+
498
+ private async initializeSchema(): Promise<void> {
499
+ try {
500
+ // Create agent_runs table
501
+ await this.db.schema
502
+ .createTable('agent_runs')
503
+ .ifNotExists()
504
+ .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
505
+ .addColumn('thread_id', 'text', (col) => col.notNull())
506
+ .addColumn('run_id', 'text', (col) => col.notNull().unique())
507
+ .addColumn('parent_run_id', 'text')
508
+ .addColumn('events', 'text', (col) => col.notNull())
509
+ .addColumn('input', 'text', (col) => col.notNull())
510
+ .addColumn('created_at', 'integer', (col) => col.notNull())
511
+ .addColumn('version', 'integer', (col) => col.notNull())
512
+ .execute()
513
+ .catch(() => {}); // Ignore if already exists
514
+
515
+ // Create run_state table
516
+ await this.db.schema
517
+ .createTable('run_state')
518
+ .ifNotExists()
519
+ .addColumn('thread_id', 'text', (col) => col.primaryKey())
520
+ .addColumn('is_running', 'integer', (col) => col.defaultTo(0))
521
+ .addColumn('current_run_id', 'text')
522
+ .addColumn('server_id', 'text')
523
+ .addColumn('updated_at', 'integer', (col) => col.notNull())
524
+ .execute()
525
+ .catch(() => {}); // Ignore if already exists
526
+
527
+ // Create schema_version table
528
+ await this.db.schema
529
+ .createTable('schema_version')
530
+ .ifNotExists()
531
+ .addColumn('version', 'integer', (col) => col.primaryKey())
532
+ .addColumn('applied_at', 'integer', (col) => col.notNull())
533
+ .execute()
534
+ .catch(() => {}); // Ignore if already exists
535
+
536
+ // Create indexes
537
+ await this.db.schema
538
+ .createIndex('idx_thread_id')
539
+ .ifNotExists()
540
+ .on('agent_runs')
541
+ .column('thread_id')
542
+ .execute()
543
+ .catch(() => {});
544
+
545
+ await this.db.schema
546
+ .createIndex('idx_parent_run_id')
547
+ .ifNotExists()
548
+ .on('agent_runs')
549
+ .column('parent_run_id')
550
+ .execute()
551
+ .catch(() => {});
552
+
553
+ // Check and set schema version
554
+ const currentVersion = await this.db
555
+ .selectFrom('schema_version')
556
+ .orderBy('version', 'desc')
557
+ .limit(1)
558
+ .selectAll()
559
+ .executeTakeFirst();
560
+
561
+ if (!currentVersion || currentVersion.version < SCHEMA_VERSION) {
562
+ await this.db
563
+ .insertInto('schema_version')
564
+ .values({
565
+ version: SCHEMA_VERSION,
566
+ applied_at: Date.now()
567
+ })
568
+ .onConflict((oc) => oc
569
+ .column('version')
570
+ .doUpdateSet({ applied_at: Date.now() })
571
+ )
572
+ .execute();
573
+ }
574
+ } catch {
575
+ // Schema initialization might fail if DB is closed, ignore
576
+ }
577
+ }
578
+
579
+ private async storeRun(
580
+ threadId: string,
581
+ runId: string,
582
+ events: BaseEvent[],
583
+ input: RunAgentInput,
584
+ parentRunId: string | null
585
+ ): Promise<void> {
586
+ await this.db.insertInto('agent_runs')
587
+ .values({
588
+ thread_id: threadId,
589
+ run_id: runId,
590
+ parent_run_id: parentRunId,
591
+ events: JSON.stringify(events),
592
+ input: JSON.stringify(input),
593
+ created_at: Date.now(),
594
+ version: SCHEMA_VERSION
595
+ })
596
+ .execute();
597
+ }
598
+
599
+ private async getHistoricRuns(threadId: string): Promise<AgentRunRecord[]> {
600
+ const rows = await this.db
601
+ .selectFrom('agent_runs')
602
+ .where('thread_id', '=', threadId)
603
+ .orderBy('created_at', 'asc')
604
+ .selectAll()
605
+ .execute();
606
+
607
+ return rows.map(row => ({
608
+ id: Number(row.id),
609
+ thread_id: row.thread_id,
610
+ run_id: row.run_id,
611
+ parent_run_id: row.parent_run_id,
612
+ events: row.events,
613
+ input: row.input,
614
+ created_at: row.created_at,
615
+ version: row.version
616
+ }));
617
+ }
618
+
619
+ private async setRunState(
620
+ threadId: string,
621
+ isRunning: boolean,
622
+ runId?: string
623
+ ): Promise<void> {
624
+ await this.db.insertInto('run_state')
625
+ .values({
626
+ thread_id: threadId,
627
+ is_running: isRunning ? 1 : 0,
628
+ current_run_id: runId ?? null,
629
+ server_id: this.serverId,
630
+ updated_at: Date.now()
631
+ })
632
+ .onConflict((oc) => oc
633
+ .column('thread_id')
634
+ .doUpdateSet({
635
+ is_running: isRunning ? 1 : 0,
636
+ current_run_id: runId ?? null,
637
+ server_id: this.serverId,
638
+ updated_at: Date.now()
639
+ })
640
+ )
641
+ .execute();
642
+ }
643
+
644
+ async close(): Promise<void> {
645
+ await this.db.destroy();
646
+ this.redis.disconnect();
647
+ this.redisSub.disconnect();
648
+ }
649
+
650
+ private generateServerId(): string {
651
+ return `server-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
652
+ }
653
+ }