@copilotkitnext/sqlite-runner 0.0.21 → 0.0.22-alpha.0

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.
@@ -1,77 +1,39 @@
1
- import {
2
- AgentRunner,
3
- finalizeRunEvents,
4
- type AgentRunnerConnectRequest,
5
- type AgentRunnerIsRunningRequest,
6
- type AgentRunnerRunRequest,
7
- type AgentRunnerStopRequest,
8
- } from "@copilotkitnext/runtime";
9
- import { Observable, ReplaySubject } from "rxjs";
10
- import {
11
- AbstractAgent,
12
- BaseEvent,
13
- RunAgentInput,
14
- EventType,
15
- RunStartedEvent,
16
- compactEvents,
17
- } from "@ag-ui/client";
1
+ import { AgentRunnerBase } from "@copilotkitnext/runtime";
2
+ import { BaseEvent, RunAgentInput } from "@ag-ui/client";
18
3
  import Database from "better-sqlite3";
4
+ import { Observable, ReplaySubject } from "rxjs";
19
5
 
20
6
  const SCHEMA_VERSION = 1;
21
7
 
22
- interface AgentRunRecord {
23
- id: number;
24
- thread_id: string;
25
- run_id: string;
26
- parent_run_id: string | null;
27
- events: BaseEvent[];
28
- input: RunAgentInput;
29
- created_at: number;
30
- version: number;
31
- }
32
-
33
8
  export interface SqliteAgentRunnerOptions {
34
9
  dbPath?: string;
35
10
  }
36
-
37
- interface ActiveConnectionContext {
38
- subject: ReplaySubject<BaseEvent>;
39
- agent?: AbstractAgent;
40
- runSubject?: ReplaySubject<BaseEvent>;
41
- currentEvents?: BaseEvent[];
42
- stopRequested?: boolean;
43
- }
44
-
45
- // Active connections for streaming events and stop support
46
- const ACTIVE_CONNECTIONS = new Map<string, ActiveConnectionContext>();
47
-
48
- export class SqliteAgentRunner extends AgentRunner {
11
+ export class SqliteAgentRunner extends AgentRunnerBase {
49
12
  private db: any;
13
+ private channels = new Map<string, ReplaySubject<BaseEvent>>();
50
14
 
51
15
  constructor(options: SqliteAgentRunnerOptions = {}) {
52
16
  super();
53
17
  const dbPath = options.dbPath ?? ":memory:";
54
-
55
18
  if (!Database) {
56
19
  throw new Error(
57
20
  'better-sqlite3 is required for SqliteAgentRunner but was not found.\n' +
58
- 'Please install it in your project:\n' +
59
- ' npm install better-sqlite3\n' +
60
- ' or\n' +
61
- ' pnpm add better-sqlite3\n' +
62
- ' or\n' +
63
- ' yarn add better-sqlite3\n\n' +
64
- 'If you don\'t need persistence, use InMemoryAgentRunner instead.'
21
+ 'Please install it in your project:\n' +
22
+ ' npm install better-sqlite3\n' +
23
+ ' or\n' +
24
+ ' pnpm add better-sqlite3\n' +
25
+ ' or\n' +
26
+ ' yarn add better-sqlite3\n\n' +
27
+ "If you don't need persistence, use InMemoryAgentRunner instead.",
65
28
  );
66
29
  }
67
-
68
- this.db = new Database(dbPath);
69
- this.initializeSchema();
30
+ const db = new Database(dbPath);
31
+ SqliteAgentRunner.initializeSchema(db);
32
+ this.db = db;
70
33
  }
71
34
 
72
- private initializeSchema(): void {
73
- // Create the agent_runs table
74
- this.db.exec(`
35
+ private static initializeSchema(db: any): void {
36
+ db.exec(`
75
37
  CREATE TABLE IF NOT EXISTS agent_runs (
76
38
  id INTEGER PRIMARY KEY AUTOINCREMENT,
77
39
  thread_id TEXT NOT NULL,
@@ -84,8 +46,7 @@ export class SqliteAgentRunner extends AgentRunner {
84
46
  )
85
47
  `);
86
48
 
87
- // Create run_state table to track active runs
88
- this.db.exec(`
49
+ db.exec(`
89
50
  CREATE TABLE IF NOT EXISTS run_state (
90
51
  thread_id TEXT PRIMARY KEY,
91
52
  is_running INTEGER DEFAULT 0,
@@ -94,404 +55,179 @@ export class SqliteAgentRunner extends AgentRunner {
94
55
  )
95
56
  `);
96
57
 
97
- // Create indexes for efficient queries
98
- this.db.exec(`
58
+ db.exec(`
99
59
  CREATE INDEX IF NOT EXISTS idx_thread_id ON agent_runs(thread_id);
100
60
  CREATE INDEX IF NOT EXISTS idx_parent_run_id ON agent_runs(parent_run_id);
101
61
  `);
102
62
 
103
- // Create schema version table
104
- this.db.exec(`
63
+ db.exec(`
105
64
  CREATE TABLE IF NOT EXISTS schema_version (
106
65
  version INTEGER PRIMARY KEY,
107
66
  applied_at INTEGER NOT NULL
108
67
  )
109
68
  `);
110
69
 
111
- // Check and set schema version
112
- const currentVersion = this.db
70
+ const currentVersion = db
113
71
  .prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1")
114
72
  .get() as { version: number } | undefined;
115
-
116
73
  if (!currentVersion || currentVersion.version < SCHEMA_VERSION) {
117
- this.db
74
+ db
118
75
  .prepare("INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)")
119
76
  .run(SCHEMA_VERSION, Date.now());
120
77
  }
121
78
  }
122
79
 
123
- private storeRun(
80
+ // Hooks implementation using SQLite
81
+ protected async acquireRun(threadId: string, runId: string): Promise<boolean> {
82
+ const row = this.db.prepare("SELECT is_running FROM run_state WHERE thread_id = ?").get(threadId) as
83
+ | { is_running: number }
84
+ | undefined;
85
+ if (row?.is_running === 1) return false;
86
+ this.db
87
+ .prepare(
88
+ "INSERT OR REPLACE INTO run_state (thread_id, is_running, current_run_id, updated_at) VALUES (?, ?, ?, ?)",
89
+ )
90
+ .run(threadId, 1, runId, Date.now());
91
+ return true;
92
+ }
93
+
94
+ protected async releaseRun(threadId: string): Promise<void> {
95
+ this.db
96
+ .prepare("INSERT OR REPLACE INTO run_state (thread_id, is_running, current_run_id, updated_at) VALUES (?, 0, NULL, ?)")
97
+ .run(threadId, Date.now());
98
+ }
99
+
100
+ protected async isRunningState(threadId: string): Promise<boolean> {
101
+ const row = this.db.prepare("SELECT is_running FROM run_state WHERE thread_id = ?").get(threadId) as
102
+ | { is_running: number }
103
+ | undefined;
104
+ return row?.is_running === 1;
105
+ }
106
+
107
+ protected async listRuns(threadId: string): Promise<Array<{ runId: string; events: BaseEvent[]; createdAt: number }>> {
108
+ const rows = this.db
109
+ .prepare(
110
+ "SELECT run_id, events, created_at FROM agent_runs WHERE thread_id = ? ORDER BY created_at ASC",
111
+ )
112
+ .all(threadId) as Array<{ run_id: string; events: string; created_at: number }>;
113
+ return rows.map((r) => ({ runId: r.run_id, events: JSON.parse(r.events), createdAt: r.created_at }));
114
+ }
115
+
116
+ protected async saveRun(
124
117
  threadId: string,
125
118
  runId: string,
126
119
  events: BaseEvent[],
127
120
  input: RunAgentInput,
128
- parentRunId?: string | null
129
- ): void {
130
- // Compact ONLY the events from this run
131
- const compactedEvents = compactEvents(events);
132
-
133
- const stmt = this.db.prepare(`
134
- INSERT INTO agent_runs (thread_id, run_id, parent_run_id, events, input, created_at, version)
135
- VALUES (?, ?, ?, ?, ?, ?, ?)
136
- `);
137
-
121
+ parentRunId: string | null,
122
+ ): Promise<void> {
123
+ const stmt = this.db.prepare(
124
+ "INSERT INTO agent_runs (thread_id, run_id, parent_run_id, events, input, created_at, version) VALUES (?, ?, ?, ?, ?, ?, ?)",
125
+ );
138
126
  stmt.run(
139
127
  threadId,
140
128
  runId,
141
129
  parentRunId ?? null,
142
- JSON.stringify(compactedEvents), // Store only this run's compacted events
130
+ JSON.stringify(events),
143
131
  JSON.stringify(input),
144
132
  Date.now(),
145
- SCHEMA_VERSION
133
+ SCHEMA_VERSION,
146
134
  );
147
135
  }
148
136
 
149
- private getHistoricRuns(threadId: string): AgentRunRecord[] {
150
- const stmt = this.db.prepare(`
151
- WITH RECURSIVE run_chain AS (
152
- -- Base case: find the root runs (those without parent)
153
- SELECT * FROM agent_runs
154
- WHERE thread_id = ? AND parent_run_id IS NULL
155
-
156
- UNION ALL
157
-
158
- -- Recursive case: find children of current level
159
- SELECT ar.* FROM agent_runs ar
160
- INNER JOIN run_chain rc ON ar.parent_run_id = rc.run_id
161
- WHERE ar.thread_id = ?
137
+ protected async pageThreads(params: { scope?: any; limit?: number; offset?: number }): Promise<{ threadIds: string[]; total: number }> {
138
+ const limit = params.limit ?? 20;
139
+ const offset = params.offset ?? 0;
140
+ const totalRow = this.db
141
+ .prepare("SELECT COUNT(DISTINCT thread_id) AS total FROM agent_runs")
142
+ .get() as { total: number };
143
+ const rows = this.db
144
+ .prepare(
145
+ "SELECT thread_id, MAX(created_at) AS last FROM agent_runs GROUP BY thread_id ORDER BY last DESC LIMIT ? OFFSET ?",
162
146
  )
163
- SELECT * FROM run_chain
164
- ORDER BY created_at ASC
165
- `);
166
-
167
- const rows = stmt.all(threadId, threadId) as any[];
168
-
169
- return rows.map(row => ({
170
- id: row.id,
171
- thread_id: row.thread_id,
172
- run_id: row.run_id,
173
- parent_run_id: row.parent_run_id,
174
- events: JSON.parse(row.events),
175
- input: JSON.parse(row.input),
176
- created_at: row.created_at,
177
- version: row.version
178
- }));
179
- }
180
-
181
- private getLatestRunId(threadId: string): string | null {
182
- const stmt = this.db.prepare(`
183
- SELECT run_id FROM agent_runs
184
- WHERE thread_id = ?
185
- ORDER BY created_at DESC
186
- LIMIT 1
187
- `);
188
-
189
- const result = stmt.get(threadId) as { run_id: string } | undefined;
190
- return result?.run_id ?? null;
191
- }
192
-
193
- private setRunState(threadId: string, isRunning: boolean, runId?: string): void {
194
- const stmt = this.db.prepare(`
195
- INSERT OR REPLACE INTO run_state (thread_id, is_running, current_run_id, updated_at)
196
- VALUES (?, ?, ?, ?)
197
- `);
198
- stmt.run(threadId, isRunning ? 1 : 0, runId ?? null, Date.now());
147
+ .all(limit, offset) as Array<{ thread_id: string; last: number }>;
148
+ return { threadIds: rows.map((r) => r.thread_id), total: totalRow.total };
199
149
  }
200
150
 
201
- private getRunState(threadId: string): { isRunning: boolean; currentRunId: string | null } {
202
- const stmt = this.db.prepare(`
203
- SELECT is_running, current_run_id FROM run_state WHERE thread_id = ?
204
- `);
205
- const result = stmt.get(threadId) as { is_running: number; current_run_id: string | null } | undefined;
206
-
207
- return {
208
- isRunning: result?.is_running === 1,
209
- currentRunId: result?.current_run_id ?? null
210
- };
151
+ protected async deleteThreadStorage(threadId: string): Promise<void> {
152
+ this.db.prepare("DELETE FROM agent_runs WHERE thread_id = ?").run(threadId);
153
+ this.db.prepare("DELETE FROM run_state WHERE thread_id = ?").run(threadId);
211
154
  }
212
155
 
213
- run(request: AgentRunnerRunRequest): Observable<BaseEvent> {
214
- // Check if thread is already running in database
215
- const runState = this.getRunState(request.threadId);
216
- if (runState.isRunning) {
217
- throw new Error("Thread already running");
156
+ private ensureChannel(threadId: string): ReplaySubject<BaseEvent> {
157
+ let s = this.channels.get(threadId);
158
+ if (!s || s.closed) {
159
+ s = new ReplaySubject<BaseEvent>(Infinity);
160
+ this.channels.set(threadId, s);
218
161
  }
162
+ return s;
163
+ }
219
164
 
220
- // Mark thread as running in database
221
- this.setRunState(request.threadId, true, request.input.runId);
222
-
223
- // Track seen message IDs and current run events in memory for this run
224
- const seenMessageIds = new Set<string>();
225
- const currentRunEvents: BaseEvent[] = [];
226
-
227
- // Get all previously seen message IDs from historic runs
228
- const historicRuns = this.getHistoricRuns(request.threadId);
229
- const historicMessageIds = new Set<string>();
230
- for (const run of historicRuns) {
231
- for (const event of run.events) {
232
- if ('messageId' in event && typeof event.messageId === 'string') {
233
- historicMessageIds.add(event.messageId);
234
- }
235
- if (event.type === EventType.RUN_STARTED) {
236
- const runStarted = event as RunStartedEvent;
237
- const messages = runStarted.input?.messages ?? [];
238
- for (const message of messages) {
239
- historicMessageIds.add(message.id);
240
- }
241
- }
242
- }
165
+ protected publishLive(threadId: string, event: BaseEvent): void {
166
+ // Reset the live channel at the start of each run to avoid replaying
167
+ // previous runs' events to new concurrent connections.
168
+ if (event.type === 'RUN_STARTED') {
169
+ const s = new ReplaySubject<BaseEvent>(Infinity);
170
+ this.channels.set(threadId, s);
243
171
  }
172
+ this.ensureChannel(threadId).next(event);
173
+ }
244
174
 
245
- // Get or create subject for this thread's connections
246
- const nextSubject = new ReplaySubject<BaseEvent>(Infinity);
247
- const prevConnection = ACTIVE_CONNECTIONS.get(request.threadId);
248
- const prevSubject = prevConnection?.subject;
249
-
250
- // Create a subject for run() return value
251
- const runSubject = new ReplaySubject<BaseEvent>(Infinity);
252
-
253
- // Update the active connection for this thread
254
- ACTIVE_CONNECTIONS.set(request.threadId, {
255
- subject: nextSubject,
256
- agent: request.agent,
257
- runSubject,
258
- currentEvents: currentRunEvents,
259
- stopRequested: false,
260
- });
261
-
262
- // Helper function to run the agent and handle errors
263
- const runAgent = async () => {
264
- // Get parent run ID for chaining
265
- const parentRunId = this.getLatestRunId(request.threadId);
266
-
267
- try {
268
- await request.agent.runAgent(request.input, {
269
- onEvent: ({ event }) => {
270
- let processedEvent: BaseEvent = event;
271
- if (event.type === EventType.RUN_STARTED) {
272
- const runStartedEvent = event as RunStartedEvent;
273
- if (!runStartedEvent.input) {
274
- const sanitizedMessages = request.input.messages
275
- ? request.input.messages.filter(
276
- (message) => !historicMessageIds.has(message.id),
277
- )
278
- : undefined;
279
- const updatedInput = {
280
- ...request.input,
281
- ...(sanitizedMessages !== undefined
282
- ? { messages: sanitizedMessages }
283
- : {}),
284
- };
285
- processedEvent = {
286
- ...runStartedEvent,
287
- input: updatedInput,
288
- } as RunStartedEvent;
289
- }
290
- }
291
-
292
- runSubject.next(processedEvent); // For run() return - only agent events
293
- nextSubject.next(processedEvent); // For connect() / store - all events
294
- currentRunEvents.push(processedEvent); // Accumulate for database storage
295
- },
296
- onNewMessage: ({ message }) => {
297
- // Called for each new message
298
- if (!seenMessageIds.has(message.id)) {
299
- seenMessageIds.add(message.id);
300
- }
301
- },
302
- onRunStartedEvent: () => {
303
- // Mark input messages as seen without emitting duplicates
304
- if (request.input.messages) {
305
- for (const message of request.input.messages) {
306
- if (!seenMessageIds.has(message.id)) {
307
- seenMessageIds.add(message.id);
308
- }
309
- }
310
- }
311
- },
312
- });
313
-
314
- const connection = ACTIVE_CONNECTIONS.get(request.threadId);
315
- const appendedEvents = finalizeRunEvents(currentRunEvents, {
316
- stopRequested: connection?.stopRequested ?? false,
317
- });
318
- for (const event of appendedEvents) {
319
- runSubject.next(event);
320
- nextSubject.next(event);
321
- }
322
-
323
- // Store the run in database
324
- this.storeRun(
325
- request.threadId,
326
- request.input.runId,
327
- currentRunEvents,
328
- request.input,
329
- parentRunId
330
- );
331
-
332
- // Mark run as complete in database
333
- this.setRunState(request.threadId, false);
334
-
335
- if (connection) {
336
- connection.agent = undefined;
337
- connection.runSubject = undefined;
338
- connection.currentEvents = undefined;
339
- connection.stopRequested = false;
340
- }
341
-
342
- // Complete the subjects
343
- runSubject.complete();
344
- nextSubject.complete();
175
+ protected completeLive(threadId: string): void {
176
+ const s = this.ensureChannel(threadId);
177
+ if (!s.closed) s.complete();
178
+ }
345
179
 
346
- ACTIVE_CONNECTIONS.delete(request.threadId);
347
- } catch {
348
- const connection = ACTIVE_CONNECTIONS.get(request.threadId);
349
- const appendedEvents = finalizeRunEvents(currentRunEvents, {
350
- stopRequested: connection?.stopRequested ?? false,
351
- });
352
- for (const event of appendedEvents) {
353
- runSubject.next(event);
354
- nextSubject.next(event);
355
- }
180
+ protected subscribeLive(threadId: string): Observable<BaseEvent> {
181
+ return this.ensureChannel(threadId).asObservable();
182
+ }
356
183
 
357
- // Store the run even if it failed (partial events)
358
- if (currentRunEvents.length > 0) {
359
- this.storeRun(
360
- request.threadId,
361
- request.input.runId,
362
- currentRunEvents,
363
- request.input,
364
- parentRunId
365
- );
366
- }
367
-
368
- // Mark run as complete in database
369
- this.setRunState(request.threadId, false);
184
+ protected async closeLive(threadId: string): Promise<void> {
185
+ const s = this.channels.get(threadId);
186
+ if (s && !s.closed) s.complete();
187
+ this.channels.delete(threadId);
188
+ }
370
189
 
371
- if (connection) {
372
- connection.agent = undefined;
373
- connection.runSubject = undefined;
374
- connection.currentEvents = undefined;
375
- connection.stopRequested = false;
190
+ // Override connect to preserve original sqlite-runner ordering without re-compacting across runs
191
+ connect(request: { threadId: string }) {
192
+ const { threadId } = request;
193
+ const subject = new ReplaySubject<BaseEvent>(Infinity);
194
+ void (async () => {
195
+ // Load runs and emit stored events in order
196
+ const runs = await this.listRuns(threadId);
197
+ const emittedMessageIds = new Set<string>();
198
+ for (const r of runs) {
199
+ for (const e of r.events) {
200
+ subject.next(e);
201
+ const id = (e as any).messageId;
202
+ if (typeof id === "string") emittedMessageIds.add(id);
376
203
  }
377
-
378
- // Don't emit error to the subject, just complete it
379
- // This allows subscribers to get events emitted before the error
380
- runSubject.complete();
381
- nextSubject.complete();
382
-
383
- ACTIVE_CONNECTIONS.delete(request.threadId);
384
204
  }
385
- };
386
-
387
- // Bridge previous events if they exist
388
- if (prevSubject) {
389
- prevSubject.subscribe({
390
- next: (e) => nextSubject.next(e),
391
- error: (err) => nextSubject.error(err),
392
- complete: () => {
393
- // Don't complete nextSubject here - it needs to stay open for new events
394
- },
395
- });
396
- }
397
205
 
398
- // Start the agent execution immediately (not lazily)
399
- runAgent();
400
-
401
- // Return the run subject (only agent events, no injected messages)
402
- return runSubject.asObservable();
403
- }
404
-
405
- connect(request: AgentRunnerConnectRequest): Observable<BaseEvent> {
406
- const connectionSubject = new ReplaySubject<BaseEvent>(Infinity);
407
-
408
- // Load historic runs from database
409
- const historicRuns = this.getHistoricRuns(request.threadId);
410
-
411
- // Collect all historic events from database
412
- const allHistoricEvents: BaseEvent[] = [];
413
- for (const run of historicRuns) {
414
- allHistoricEvents.push(...run.events);
415
- }
416
-
417
- // Compact all events together before emitting
418
- const compactedEvents = compactEvents(allHistoricEvents);
419
-
420
- // Emit compacted events and track message IDs
421
- const emittedMessageIds = new Set<string>();
422
- for (const event of compactedEvents) {
423
- connectionSubject.next(event);
424
- if ('messageId' in event && typeof event.messageId === 'string') {
425
- emittedMessageIds.add(event.messageId);
206
+ const running = await this.isRunningState(threadId);
207
+ if (!running) {
208
+ subject.complete();
209
+ return;
426
210
  }
427
- }
428
-
429
- // Bridge active run to connection if exists
430
- const activeConnection = ACTIVE_CONNECTIONS.get(request.threadId);
431
- const runState = this.getRunState(request.threadId);
432
211
 
433
- if (activeConnection && (runState.isRunning || activeConnection.stopRequested)) {
434
- activeConnection.subject.subscribe({
435
- next: (event) => {
436
- // Skip message events that we've already emitted from historic
437
- if ('messageId' in event && typeof event.messageId === 'string' && emittedMessageIds.has(event.messageId)) {
438
- return;
212
+ const live$ = this.subscribeLive(threadId);
213
+ let completed = false;
214
+ live$.subscribe({
215
+ next: (e) => {
216
+ const id = (e as any).messageId;
217
+ if (typeof id === "string" && emittedMessageIds.has(id)) return;
218
+ subject.next(e);
219
+ if (e.type === 'RUN_FINISHED' || e.type === 'RUN_ERROR') {
220
+ if (!completed) { completed = true; subject.complete(); }
439
221
  }
440
- connectionSubject.next(event);
441
222
  },
442
- complete: () => connectionSubject.complete(),
443
- error: (err) => connectionSubject.error(err)
223
+ error: (err) => subject.error(err),
224
+ complete: () => { if (!completed) { completed = true; subject.complete(); } },
444
225
  });
445
- } else {
446
- // No active run, complete after historic events
447
- connectionSubject.complete();
448
- }
449
-
450
- return connectionSubject.asObservable();
226
+ })();
227
+ return subject.asObservable();
451
228
  }
452
229
 
453
- isRunning(request: AgentRunnerIsRunningRequest): Promise<boolean> {
454
- const runState = this.getRunState(request.threadId);
455
- return Promise.resolve(runState.isRunning);
456
- }
457
-
458
- stop(request: AgentRunnerStopRequest): Promise<boolean | undefined> {
459
- const runState = this.getRunState(request.threadId);
460
- if (!runState.isRunning) {
461
- return Promise.resolve(false);
462
- }
463
-
464
- const connection = ACTIVE_CONNECTIONS.get(request.threadId);
465
- const agent = connection?.agent;
466
-
467
- if (!connection || !agent) {
468
- return Promise.resolve(false);
469
- }
470
-
471
- if (connection.stopRequested) {
472
- return Promise.resolve(false);
473
- }
474
-
475
- connection.stopRequested = true;
476
- this.setRunState(request.threadId, false);
477
-
478
- try {
479
- agent.abortRun();
480
- return Promise.resolve(true);
481
- } catch (error) {
482
- console.error("Failed to abort sqlite agent run", error);
483
- connection.stopRequested = false;
484
- this.setRunState(request.threadId, true);
485
- return Promise.resolve(false);
486
- }
487
- }
488
-
489
- /**
490
- * Close the database connection (for cleanup)
491
- */
492
230
  close(): void {
493
- if (this.db) {
494
- this.db.close();
495
- }
231
+ if (this.db) this.db.close();
496
232
  }
497
233
  }