@langchain/langgraph-api 0.0.10

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.
@@ -0,0 +1,792 @@
1
+ import { HTTPException } from "hono/http-exception";
2
+ import { v4 as uuid4, v5 as uuid5 } from "uuid";
3
+ import { getGraph, NAMESPACE_GRAPH } from "../graph/load.mjs";
4
+ import { checkpointer } from "./checkpoint.mjs";
5
+ import { store } from "./store.mjs";
6
+ import { logger } from "../logging.mjs";
7
+ import { serializeError } from "../utils/serde.mjs";
8
+ import { FileSystemPersistence } from "./persist.mjs";
9
+ export const conn = new FileSystemPersistence(".langgraphjs_ops.json", () => ({
10
+ runs: {},
11
+ threads: {},
12
+ assistants: {},
13
+ assistant_versions: [],
14
+ retry_counter: {},
15
+ }));
16
+ class TimeoutError extends Error {
17
+ }
18
+ class AbortError extends Error {
19
+ }
20
+ class Queue {
21
+ buffer = [];
22
+ listeners = [];
23
+ push(item) {
24
+ this.buffer.push(item);
25
+ for (const listener of this.listeners) {
26
+ listener();
27
+ }
28
+ }
29
+ async get(options) {
30
+ if (this.buffer.length > 0) {
31
+ return this.buffer.shift();
32
+ }
33
+ return await new Promise((resolve, reject) => {
34
+ let listener = undefined;
35
+ const timer = setTimeout(() => {
36
+ this.listeners = this.listeners.filter((l) => l !== listener);
37
+ reject(new TimeoutError());
38
+ }, options.timeout);
39
+ listener = () => {
40
+ this.listeners = this.listeners.filter((l) => l !== listener);
41
+ clearTimeout(timer);
42
+ resolve();
43
+ };
44
+ // TODO: make sure we're not leaking callback here
45
+ if (options.signal != null) {
46
+ options.signal.addEventListener("abort", () => {
47
+ this.listeners = this.listeners.filter((l) => l !== listener);
48
+ clearTimeout(timer);
49
+ reject(new AbortError());
50
+ });
51
+ }
52
+ this.listeners.push(listener);
53
+ }).then(() => this.buffer.shift());
54
+ }
55
+ }
56
+ class CancellationAbortController extends AbortController {
57
+ abort(reason) {
58
+ super.abort(reason);
59
+ }
60
+ }
61
+ class StreamManagerImpl {
62
+ readers = {};
63
+ control = {};
64
+ getQueue(runId, options) {
65
+ if (this.readers[runId] == null) {
66
+ if (options?.ifNotFound === "create") {
67
+ this.readers[runId] = new Queue();
68
+ }
69
+ else {
70
+ return undefined;
71
+ }
72
+ }
73
+ return this.readers[runId];
74
+ }
75
+ getControl(runId) {
76
+ if (this.control[runId] == null)
77
+ return undefined;
78
+ return this.control[runId];
79
+ }
80
+ isLocked(runId) {
81
+ return this.control[runId] != null;
82
+ }
83
+ lock(runId) {
84
+ if (this.control[runId] != null) {
85
+ logger.warn("Run already locked", { run_id: runId });
86
+ }
87
+ this.control[runId] = new CancellationAbortController();
88
+ return this.control[runId].signal;
89
+ }
90
+ unlock(runId) {
91
+ delete this.control[runId];
92
+ }
93
+ }
94
+ export const StreamManager = new StreamManagerImpl();
95
+ export const truncate = (flags) => {
96
+ return conn.with((STORE) => {
97
+ if (flags.runs)
98
+ STORE.runs = {};
99
+ if (flags.threads)
100
+ STORE.threads = {};
101
+ if (flags.assistants) {
102
+ STORE.assistants = Object.fromEntries(Object.entries(STORE.assistants).filter(([key, assistant]) => assistant.metadata?.created_by === "system" &&
103
+ uuid5(assistant.graph_id, NAMESPACE_GRAPH) === key));
104
+ }
105
+ if (flags.checkpointer)
106
+ checkpointer.clear();
107
+ if (flags.store)
108
+ store.clear();
109
+ });
110
+ };
111
+ const isObject = (value) => {
112
+ return typeof value === "object" && value !== null;
113
+ };
114
+ const isJsonbContained = (superset, subset) => {
115
+ if (superset == null || subset == null)
116
+ return true;
117
+ for (const [key, value] of Object.entries(subset)) {
118
+ if (superset[key] == null)
119
+ return false;
120
+ if (isObject(value) && isObject(superset[key])) {
121
+ if (!isJsonbContained(superset[key], value))
122
+ return false;
123
+ }
124
+ else if (superset[key] !== value) {
125
+ return false;
126
+ }
127
+ }
128
+ return true;
129
+ };
130
+ export class Assistants {
131
+ static async *search(options) {
132
+ yield* conn.withGenerator(async function* (STORE) {
133
+ let filtered = Object.values(STORE.assistants)
134
+ .filter((assistant) => {
135
+ if (options.graph_id != null &&
136
+ assistant["graph_id"] !== options.graph_id) {
137
+ return false;
138
+ }
139
+ if (options.metadata != null &&
140
+ !isJsonbContained(assistant["metadata"], options.metadata)) {
141
+ return false;
142
+ }
143
+ return true;
144
+ })
145
+ .sort((a, b) => {
146
+ const aCreatedAt = a["created_at"]?.getTime() ?? 0;
147
+ const bCreatedAt = b["created_at"]?.getTime() ?? 0;
148
+ return bCreatedAt - aCreatedAt;
149
+ });
150
+ for (const assistant of filtered.slice(options.offset, options.offset + options.limit)) {
151
+ yield { ...assistant, name: assistant.name ?? assistant.graph_id };
152
+ }
153
+ });
154
+ }
155
+ static async get(assistantId) {
156
+ return conn.with((STORE) => {
157
+ const result = STORE.assistants[assistantId];
158
+ if (result == null)
159
+ throw new HTTPException(404, { message: "Assistant not found" });
160
+ return { ...result, name: result.name ?? result.graph_id };
161
+ });
162
+ }
163
+ static async put(assistantId, options) {
164
+ return conn.with((STORE) => {
165
+ if (STORE.assistants[assistantId] != null) {
166
+ if (options.if_exists === "raise") {
167
+ throw new HTTPException(409, { message: "Assistant already exists" });
168
+ }
169
+ return STORE.assistants[assistantId];
170
+ }
171
+ const now = new Date();
172
+ STORE.assistants[assistantId] ??= {
173
+ assistant_id: assistantId,
174
+ version: 1,
175
+ config: options.config ?? {},
176
+ created_at: now,
177
+ updated_at: now,
178
+ graph_id: options.graph_id,
179
+ metadata: options.metadata ?? {},
180
+ name: options.name || options.graph_id,
181
+ };
182
+ STORE.assistant_versions.push({
183
+ assistant_id: assistantId,
184
+ version: 1,
185
+ graph_id: options.graph_id,
186
+ config: options.config ?? {},
187
+ metadata: options.metadata ?? {},
188
+ created_at: now,
189
+ name: options.name || options.graph_id,
190
+ });
191
+ return STORE.assistants[assistantId];
192
+ });
193
+ }
194
+ static async patch(assistantId, options) {
195
+ return conn.with((STORE) => {
196
+ const assistant = STORE.assistants[assistantId];
197
+ if (!assistant)
198
+ throw new HTTPException(404, { message: "Assistant not found" });
199
+ const now = new Date();
200
+ const metadata = options?.metadata != null
201
+ ? {
202
+ ...assistant["metadata"],
203
+ ...options.metadata,
204
+ }
205
+ : null;
206
+ if (options?.graph_id != null) {
207
+ assistant["graph_id"] = options?.graph_id ?? assistant["graph_id"];
208
+ }
209
+ if (options?.config != null) {
210
+ assistant["config"] = options?.config ?? assistant["config"];
211
+ }
212
+ if (options?.name != null) {
213
+ assistant["name"] = options?.name ?? assistant["name"];
214
+ }
215
+ if (metadata != null) {
216
+ assistant["metadata"] = metadata ?? assistant["metadata"];
217
+ }
218
+ assistant["updated_at"] = now;
219
+ const newVersion = Math.max(...STORE.assistant_versions
220
+ .filter((v) => v["assistant_id"] === assistantId)
221
+ .map((v) => v["version"])) + 1;
222
+ assistant.version = newVersion;
223
+ const newVersionEntry = {
224
+ assistant_id: assistantId,
225
+ version: newVersion,
226
+ graph_id: options?.graph_id ?? assistant["graph_id"],
227
+ config: options?.config ?? assistant["config"],
228
+ name: options?.name ?? assistant["name"],
229
+ metadata: metadata ?? assistant["metadata"],
230
+ created_at: now,
231
+ };
232
+ STORE.assistant_versions.push(newVersionEntry);
233
+ return assistant;
234
+ });
235
+ }
236
+ static async delete(assistantId) {
237
+ return conn.with((STORE) => {
238
+ const assistant = STORE.assistants[assistantId];
239
+ if (!assistant)
240
+ throw new HTTPException(404, { message: "Assistant not found" });
241
+ delete STORE.assistants[assistantId];
242
+ // Cascade delete for assistant versions and crons
243
+ STORE.assistant_versions = STORE.assistant_versions.filter((v) => v["assistant_id"] !== assistantId);
244
+ for (const run of Object.values(STORE.runs)) {
245
+ if (run["assistant_id"] === assistantId) {
246
+ delete STORE.runs[run["run_id"]];
247
+ }
248
+ }
249
+ return [assistant.assistant_id];
250
+ });
251
+ }
252
+ static async setLatest(assistantId, version) {
253
+ return conn.with((STORE) => {
254
+ const assistant = STORE.assistants[assistantId];
255
+ if (!assistant)
256
+ throw new HTTPException(404, { message: "Assistant not found" });
257
+ const assistantVersion = STORE.assistant_versions.find((v) => v["assistant_id"] === assistantId && v["version"] === version);
258
+ if (!assistantVersion)
259
+ throw new HTTPException(404, {
260
+ message: "Assistant version not found",
261
+ });
262
+ const now = new Date();
263
+ STORE.assistants[assistantId] = {
264
+ ...assistant,
265
+ config: assistantVersion["config"],
266
+ metadata: assistantVersion["metadata"],
267
+ version: assistantVersion["version"],
268
+ name: assistantVersion["name"],
269
+ updated_at: now,
270
+ };
271
+ return STORE.assistants[assistantId];
272
+ });
273
+ }
274
+ static async getVersions(assistantId, options) {
275
+ return conn.with((STORE) => {
276
+ const versions = STORE.assistant_versions
277
+ .filter((version) => {
278
+ if (version["assistant_id"] !== assistantId)
279
+ return false;
280
+ if (options.metadata != null &&
281
+ !isJsonbContained(version["metadata"], options.metadata)) {
282
+ return false;
283
+ }
284
+ return true;
285
+ })
286
+ .sort((a, b) => b["version"] - a["version"]);
287
+ return versions.slice(options.offset, options.offset + options.limit);
288
+ });
289
+ }
290
+ }
291
+ export class Threads {
292
+ static async *search(options) {
293
+ yield* conn.withGenerator(async function* (STORE) {
294
+ const filtered = Object.values(STORE.threads)
295
+ .filter((thread) => {
296
+ if (options.metadata != null &&
297
+ !isJsonbContained(thread["metadata"], options.metadata))
298
+ return false;
299
+ if (options.values != null &&
300
+ typeof thread["values"] !== "undefined" &&
301
+ !isJsonbContained(thread["values"], options.values))
302
+ return false;
303
+ if (options.status != null && thread["status"] !== options.status)
304
+ return false;
305
+ return true;
306
+ })
307
+ .sort((a, b) => b["created_at"].getTime() - a["created_at"].getTime());
308
+ for (const thread of filtered.slice(options.offset, options.offset + options.limit)) {
309
+ yield thread;
310
+ }
311
+ });
312
+ }
313
+ static async get(threadId) {
314
+ return conn.with((STORE) => {
315
+ const result = STORE.threads[threadId];
316
+ if (result == null)
317
+ throw new HTTPException(404, {
318
+ message: `Thread with ID ${threadId} not found`,
319
+ });
320
+ return result;
321
+ });
322
+ }
323
+ static async put(threadId, options) {
324
+ return conn.with((STORE) => {
325
+ const now = new Date();
326
+ if (STORE.threads[threadId] != null) {
327
+ if (options?.if_exists === "raise") {
328
+ throw new HTTPException(409, { message: "Thread already exists" });
329
+ }
330
+ return STORE.threads[threadId];
331
+ }
332
+ STORE.threads[threadId] ??= {
333
+ thread_id: threadId,
334
+ created_at: now,
335
+ updated_at: now,
336
+ metadata: options?.metadata ?? {},
337
+ status: "idle",
338
+ config: {},
339
+ values: undefined,
340
+ };
341
+ return STORE.threads[threadId];
342
+ });
343
+ }
344
+ static async patch(threadId, options) {
345
+ return conn.with((STORE) => {
346
+ const thread = STORE.threads[threadId];
347
+ if (!thread)
348
+ throw new HTTPException(404, { message: "Thread not found" });
349
+ const now = new Date();
350
+ if (options?.metadata != null) {
351
+ thread["metadata"] = {
352
+ ...thread["metadata"],
353
+ ...options.metadata,
354
+ };
355
+ }
356
+ thread["updated_at"] = now;
357
+ return thread;
358
+ });
359
+ }
360
+ static async setStatus(threadId, options) {
361
+ return conn.with((STORE) => {
362
+ const thread = STORE.threads[threadId];
363
+ if (!thread)
364
+ throw new HTTPException(404, { message: "Thread not found" });
365
+ let hasNext = false;
366
+ if (options.checkpoint != null) {
367
+ hasNext = options.checkpoint.next.length > 0;
368
+ }
369
+ const hasPendingRuns = Object.values(STORE.runs).some((run) => run["thread_id"] === threadId && run["status"] === "pending");
370
+ let status = "idle";
371
+ if (options.exception != null) {
372
+ status = "error";
373
+ }
374
+ else if (hasNext) {
375
+ status = "interrupted";
376
+ }
377
+ else if (hasPendingRuns) {
378
+ status = "busy";
379
+ }
380
+ const now = new Date();
381
+ thread.updated_at = now;
382
+ thread.status = status;
383
+ thread.values =
384
+ options.checkpoint != null ? options.checkpoint.values : undefined;
385
+ thread.interrupts =
386
+ options.checkpoint != null
387
+ ? options.checkpoint.tasks.reduce((acc, task) => {
388
+ if (task.interrupts)
389
+ acc[task.id] = task.interrupts;
390
+ return acc;
391
+ }, {})
392
+ : undefined;
393
+ });
394
+ }
395
+ static async delete(threadId) {
396
+ return conn.with((STORE) => {
397
+ const thread = STORE.threads[threadId];
398
+ if (!thread)
399
+ throw new HTTPException(404, {
400
+ message: `Thread with ID ${threadId} not found`,
401
+ });
402
+ delete STORE.threads[threadId];
403
+ for (const run of Object.values(STORE.runs)) {
404
+ if (run["thread_id"] === threadId) {
405
+ delete STORE.runs[run["run_id"]];
406
+ }
407
+ }
408
+ checkpointer.delete(threadId, null);
409
+ return [thread.thread_id];
410
+ });
411
+ }
412
+ static async copy(threadId) {
413
+ return conn.with((STORE) => {
414
+ const thread = STORE.threads[threadId];
415
+ if (!thread)
416
+ throw new HTTPException(409, { message: "Thread not found" });
417
+ const newThreadId = uuid4();
418
+ const now = new Date();
419
+ STORE.threads[newThreadId] = {
420
+ thread_id: newThreadId,
421
+ created_at: now,
422
+ updated_at: now,
423
+ metadata: { ...thread.metadata, thread_id: newThreadId },
424
+ config: {},
425
+ status: "idle",
426
+ };
427
+ checkpointer.copy(threadId, newThreadId);
428
+ return STORE.threads[newThreadId];
429
+ });
430
+ }
431
+ static State = class {
432
+ static async get(config, options) {
433
+ const subgraphs = options.subgraphs ?? false;
434
+ const threadId = config.configurable?.thread_id;
435
+ const thread = threadId ? await Threads.get(threadId) : undefined;
436
+ const metadata = thread?.metadata ?? {};
437
+ const graphId = metadata?.graph_id;
438
+ if (!thread || graphId == null) {
439
+ return {
440
+ values: {},
441
+ next: [],
442
+ config: {},
443
+ metadata: undefined,
444
+ createdAt: undefined,
445
+ parentConfig: undefined,
446
+ tasks: [],
447
+ };
448
+ }
449
+ const graph = await getGraph(graphId, { checkpointer, store });
450
+ const result = await graph.getState(config, { subgraphs });
451
+ if (result.metadata != null &&
452
+ "checkpoint_ns" in result.metadata &&
453
+ result.metadata["checkpoint_ns"] === "") {
454
+ delete result.metadata["checkpoint_ns"];
455
+ }
456
+ return result;
457
+ }
458
+ static async post(config, values, asNode) {
459
+ const threadId = config.configurable?.thread_id;
460
+ const thread = threadId ? await Threads.get(threadId) : undefined;
461
+ if (!thread)
462
+ throw new HTTPException(404, {
463
+ message: `Thread ${threadId} not found`,
464
+ });
465
+ const graphId = thread.metadata?.graph_id;
466
+ if (graphId == null) {
467
+ throw new HTTPException(400, {
468
+ message: `Thread ${threadId} has no graph ID`,
469
+ });
470
+ }
471
+ config.configurable ??= {};
472
+ config.configurable.graph_id ??= graphId;
473
+ const graph = await getGraph(graphId, { checkpointer, store });
474
+ const updateConfig = structuredClone(config);
475
+ updateConfig.configurable ??= {};
476
+ updateConfig.configurable.checkpoint_ns ??= "";
477
+ const nextConfig = await graph.updateState(updateConfig, values, asNode);
478
+ const state = await Threads.State.get(config, { subgraphs: false });
479
+ // update thread values
480
+ await conn.with(async (STORE) => {
481
+ for (const thread of Object.values(STORE.threads)) {
482
+ if (thread.thread_id === threadId) {
483
+ thread.values = state.values;
484
+ break;
485
+ }
486
+ }
487
+ });
488
+ return { checkpoint: nextConfig.configurable };
489
+ }
490
+ static async list(config, options) {
491
+ const threadId = config.configurable?.thread_id;
492
+ if (!threadId)
493
+ return [];
494
+ const thread = await Threads.get(threadId);
495
+ const graphId = thread.metadata?.graph_id;
496
+ if (graphId == null)
497
+ return [];
498
+ const graph = await getGraph(graphId, { checkpointer, store });
499
+ const before = typeof options?.before === "string"
500
+ ? { configurable: { checkpoint_id: options.before } }
501
+ : options?.before;
502
+ const states = [];
503
+ for await (const state of graph.getStateHistory(config, {
504
+ limit: options?.limit ?? 10,
505
+ before,
506
+ filter: options?.metadata,
507
+ })) {
508
+ states.push(state);
509
+ }
510
+ return states;
511
+ }
512
+ };
513
+ }
514
+ export class Runs {
515
+ static async *next() {
516
+ yield* conn.withGenerator(async function* (STORE) {
517
+ const now = new Date();
518
+ const pendingRuns = Object.values(STORE.runs)
519
+ .filter((run) => run.status === "pending" && run.created_at < now)
520
+ .sort((a, b) => a.created_at.getTime() - b.created_at.getTime());
521
+ if (!pendingRuns.length) {
522
+ return;
523
+ }
524
+ for (const run of pendingRuns) {
525
+ const runId = run.run_id;
526
+ const threadId = run.thread_id;
527
+ const thread = STORE.threads[threadId];
528
+ if (!thread) {
529
+ await console.warn(`Unexpected missing thread in Runs.next: ${threadId}`);
530
+ continue;
531
+ }
532
+ if (StreamManager.isLocked(runId))
533
+ continue;
534
+ try {
535
+ const signal = StreamManager.lock(runId);
536
+ STORE.retry_counter[runId] ??= 0;
537
+ STORE.retry_counter[runId] += 1;
538
+ yield { run, attempt: STORE.retry_counter[runId], signal };
539
+ }
540
+ finally {
541
+ StreamManager.unlock(runId);
542
+ }
543
+ }
544
+ });
545
+ }
546
+ static async put(runId, assistantId, kwargs, options) {
547
+ return conn.with(async (STORE) => {
548
+ const assistant = STORE.assistants[assistantId];
549
+ if (!assistant) {
550
+ throw new HTTPException(404, {
551
+ message: `No assistant found for "${assistantId}". Make sure the assistant ID is for a valid assistant or a valid graph ID.`,
552
+ });
553
+ }
554
+ const ifNotExists = options?.ifNotExists ?? "reject";
555
+ const multitaskStrategy = options?.multitaskStrategy ?? "reject";
556
+ const afterSeconds = options?.afterSeconds ?? 0;
557
+ const status = options?.status ?? "pending";
558
+ let threadId = options?.threadId;
559
+ const metadata = options?.metadata ?? {};
560
+ const config = kwargs.config ?? {};
561
+ const existingThread = Object.values(STORE.threads).find((thread) => thread.thread_id === threadId);
562
+ const now = new Date();
563
+ if (!existingThread && (threadId == null || ifNotExists === "create")) {
564
+ threadId ??= uuid4();
565
+ const thread = {
566
+ thread_id: threadId,
567
+ status: "busy",
568
+ metadata: { graph_id: assistant.graph_id, assistant_id: assistantId },
569
+ config: Object.assign({}, assistant.config, config, {
570
+ configurable: Object.assign({}, assistant.config?.configurable, config?.configurable),
571
+ }),
572
+ created_at: now,
573
+ updated_at: now,
574
+ };
575
+ STORE.threads[threadId] = thread;
576
+ }
577
+ else if (existingThread) {
578
+ if (existingThread.status !== "busy") {
579
+ existingThread.status = "busy";
580
+ existingThread.metadata = Object.assign({}, existingThread.metadata, {
581
+ graph_id: assistant.graph_id,
582
+ assistant_id: assistantId,
583
+ });
584
+ existingThread.config = Object.assign({}, assistant.config, existingThread.config, config, {
585
+ configurable: Object.assign({}, assistant.config?.configurable, existingThread?.config?.configurable, config?.configurable),
586
+ });
587
+ existingThread.updated_at = now;
588
+ }
589
+ }
590
+ else {
591
+ return [];
592
+ }
593
+ // if multitask_mode = reject, check for inflight runs
594
+ // and if there are any, return them to reject putting a new run
595
+ const inflightRuns = Object.values(STORE.runs).filter((run) => run.thread_id === threadId && run.status === "pending");
596
+ if (options?.preventInsertInInflight) {
597
+ if (inflightRuns.length > 0)
598
+ return inflightRuns;
599
+ }
600
+ // create new run
601
+ const configurable = Object.assign({}, assistant.config?.configurable, existingThread?.config?.configurable, config?.configurable, {
602
+ run_id: runId,
603
+ thread_id: threadId,
604
+ graph_id: assistant.graph_id,
605
+ assistant_id: assistantId,
606
+ user_id: config.configurable?.user_id ??
607
+ existingThread?.config?.configurable?.user_id ??
608
+ assistant.config?.configurable?.user_id ??
609
+ options?.userId,
610
+ });
611
+ const mergedMetadata = Object.assign({}, assistant.metadata, existingThread?.metadata, metadata);
612
+ const newRun = {
613
+ run_id: runId,
614
+ thread_id: threadId,
615
+ assistant_id: assistantId,
616
+ metadata: mergedMetadata,
617
+ status: status,
618
+ kwargs: Object.assign({}, kwargs, {
619
+ config: Object.assign({}, assistant.config, config, { configurable }, { metadata: mergedMetadata }),
620
+ }),
621
+ multitask_strategy: multitaskStrategy,
622
+ created_at: new Date(now.valueOf() + afterSeconds * 1000),
623
+ updated_at: now,
624
+ };
625
+ STORE.runs[runId] = newRun;
626
+ return [newRun, ...inflightRuns];
627
+ });
628
+ }
629
+ static async get(runId, threadId) {
630
+ return conn.with(async (STORE) => {
631
+ const run = STORE.runs[runId];
632
+ if (!run ||
633
+ run.run_id !== runId ||
634
+ (threadId != null && run.thread_id !== threadId))
635
+ return null;
636
+ return run;
637
+ });
638
+ }
639
+ static async delete(runId, threadId) {
640
+ return conn.with(async (STORE) => {
641
+ const run = STORE.runs[runId];
642
+ if (!run || (threadId != null && run.thread_id !== threadId))
643
+ throw new Error("Run not found");
644
+ if (threadId != null)
645
+ checkpointer.delete(threadId, runId);
646
+ delete STORE.runs[runId];
647
+ return run.run_id;
648
+ });
649
+ }
650
+ static async wait(runId, threadId) {
651
+ const runStream = Runs.Stream.join(runId, threadId);
652
+ const lastChunk = new Promise(async (resolve, reject) => {
653
+ try {
654
+ let lastChunk = null;
655
+ for await (const { event, data } of runStream) {
656
+ if (event === "values") {
657
+ lastChunk = data;
658
+ }
659
+ else if (event === "error") {
660
+ lastChunk = { __error__: serializeError(data) };
661
+ }
662
+ }
663
+ resolve(lastChunk);
664
+ }
665
+ catch (error) {
666
+ reject(error);
667
+ }
668
+ });
669
+ return lastChunk;
670
+ }
671
+ static async join(runId, threadId) {
672
+ // check if thread exists
673
+ await Threads.get(threadId);
674
+ const lastChunk = await Runs.wait(runId, threadId);
675
+ if (lastChunk != null)
676
+ return lastChunk;
677
+ const thread = await Threads.get(threadId);
678
+ return thread.values;
679
+ }
680
+ static async cancel(threadId, runIds, options) {
681
+ return conn.with(async (STORE) => {
682
+ const action = options.action ?? "interrupt";
683
+ const promises = [];
684
+ let foundRunsCount = 0;
685
+ for (const runId of runIds) {
686
+ const run = STORE.runs[runId];
687
+ if (!run || (threadId != null && run.thread_id !== threadId))
688
+ continue;
689
+ foundRunsCount += 1;
690
+ // send cancellation message
691
+ const control = StreamManager.getControl(runId);
692
+ control?.abort(options.action ?? "interrupt");
693
+ if (run.status === "pending") {
694
+ if (control || action !== "rollback") {
695
+ run.status = "interrupted";
696
+ run.updated_at = new Date();
697
+ }
698
+ else {
699
+ logger.info("Eagerly deleting unscheduled run with rollback action", {
700
+ run_id: runId,
701
+ thread_id: threadId,
702
+ });
703
+ promises.push(Runs.delete(runId, threadId));
704
+ }
705
+ }
706
+ else {
707
+ logger.warn("Attempted to cancel non-pending run.", {
708
+ run_id: runId,
709
+ status: run.status,
710
+ });
711
+ }
712
+ }
713
+ await Promise.all(promises);
714
+ if (foundRunsCount === runIds.length) {
715
+ logger.info("Cancelled runs", {
716
+ run_ids: runIds,
717
+ thread_id: threadId,
718
+ action,
719
+ });
720
+ }
721
+ else {
722
+ throw new HTTPException(404, { message: "Run not found" });
723
+ }
724
+ });
725
+ }
726
+ static async search(threadId, options) {
727
+ return conn.with(async (STORE) => {
728
+ const runs = Object.values(STORE.runs).filter((run) => {
729
+ if (run.thread_id !== threadId)
730
+ return false;
731
+ if (options?.status != null && run.status !== options.status)
732
+ return false;
733
+ if (options?.metadata != null &&
734
+ !isJsonbContained(run.metadata, options.metadata))
735
+ return false;
736
+ return true;
737
+ });
738
+ return runs.slice(options?.offset ?? 0, options?.limit ?? 10);
739
+ });
740
+ }
741
+ static async setStatus(runId, status) {
742
+ return conn.with(async (STORE) => {
743
+ const run = STORE.runs[runId];
744
+ if (!run)
745
+ throw new Error(`Run ${runId} not found`);
746
+ run.status = status;
747
+ run.updated_at = new Date();
748
+ });
749
+ }
750
+ static Stream = class {
751
+ static async *join(runId, threadId, options) {
752
+ // TODO: what if we're joining an already completed run? Should we check before?
753
+ const signal = options?.cancelOnDisconnect;
754
+ const queue = StreamManager.getQueue(runId, { ifNotFound: "create" });
755
+ while (!signal?.aborted) {
756
+ try {
757
+ const message = await queue.get({ timeout: 500, signal });
758
+ if (message.topic === `run:${runId}:control`) {
759
+ if (message.data === "done")
760
+ break;
761
+ }
762
+ else {
763
+ const streamTopic = message.topic.substring(`run:${runId}:stream:`.length);
764
+ yield { event: streamTopic, data: message.data };
765
+ }
766
+ }
767
+ catch (error) {
768
+ if (error instanceof AbortError)
769
+ break;
770
+ const run = await Runs.get(runId, threadId);
771
+ if (run == null) {
772
+ if (!options?.ignore404)
773
+ yield { event: "error", data: "Run not found" };
774
+ break;
775
+ }
776
+ else if (run.status !== "pending") {
777
+ break;
778
+ }
779
+ }
780
+ }
781
+ if (signal?.aborted && threadId != null) {
782
+ await Runs.cancel(threadId, [runId], { action: "interrupt" });
783
+ }
784
+ }
785
+ static async publish(runId, topic, data) {
786
+ const queue = StreamManager.getQueue(runId, { ifNotFound: "create" });
787
+ queue.push({ topic: `run:${runId}:stream:${topic}`, data });
788
+ }
789
+ };
790
+ }
791
+ export class Crons {
792
+ }