@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.
- package/.turbo/turbo-build.log +11 -11
- package/.turbo/turbo-test.log +13 -21
- package/dist/index.d.mts +36 -16
- package/dist/index.d.ts +36 -16
- package/dist/index.js +113 -275
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +114 -282
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/sqlite-runner.ts +138 -402
package/src/sqlite-runner.ts
CHANGED
|
@@ -1,77 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
this.
|
|
30
|
+
const db = new Database(dbPath);
|
|
31
|
+
SqliteAgentRunner.initializeSchema(db);
|
|
32
|
+
this.db = db;
|
|
70
33
|
}
|
|
71
34
|
|
|
72
|
-
private initializeSchema(): void {
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
129
|
-
): void {
|
|
130
|
-
|
|
131
|
-
|
|
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(
|
|
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
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
443
|
-
|
|
223
|
+
error: (err) => subject.error(err),
|
|
224
|
+
complete: () => { if (!completed) { completed = true; subject.complete(); } },
|
|
444
225
|
});
|
|
445
|
-
}
|
|
446
|
-
|
|
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
|
}
|