@kortyx/runtime 0.17.2 → 0.19.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/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
+ var __create = Object.create;
1
2
  var __defProp = Object.defineProperty;
2
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
4
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
7
  var __export = (target, all) => {
6
8
  for (var name in all)
@@ -14,6 +16,14 @@ var __copyProps = (to, from, except, desc) => {
14
16
  }
15
17
  return to;
16
18
  };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
17
27
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
28
 
19
29
  // src/index.ts
@@ -22,6 +32,7 @@ __export(index_exports, {
22
32
  buildInitialGraphState: () => buildInitialGraphState,
23
33
  captureGraphSnapshot: () => captureGraphSnapshot,
24
34
  clearRegisteredNodes: () => clearRegisteredNodes,
35
+ createCachingFrameworkAdapter: () => createCachingFrameworkAdapter,
25
36
  createExecutionGraph: () => createExecutionGraph,
26
37
  createFileWorkflowRegistry: () => createFileWorkflowRegistry,
27
38
  createFrameworkAdapterFromEnv: () => createFrameworkAdapterFromEnv,
@@ -29,6 +40,7 @@ __export(index_exports, {
29
40
  createInMemoryPendingRequestStore: () => createInMemoryPendingRequestStore,
30
41
  createInMemorySessionCheckpointStore: () => createInMemorySessionCheckpointStore,
31
42
  createInMemoryWorkflowRegistry: () => createInMemoryWorkflowRegistry,
43
+ createPostgresFrameworkAdapter: () => createPostgresFrameworkAdapter,
32
44
  createRedisFrameworkAdapter: () => createRedisFrameworkAdapter,
33
45
  getCheckpointer: () => getCheckpointer,
34
46
  getRegisteredNode: () => getRegisteredNode,
@@ -187,16 +199,89 @@ function getCheckpointer(key) {
187
199
  return saver;
188
200
  }
189
201
 
202
+ // src/framework/caching.ts
203
+ var providers = /* @__PURE__ */ new WeakMap();
204
+ var attachedCaches = /* @__PURE__ */ new WeakSet();
205
+ var storageBindings = /* @__PURE__ */ new WeakMap();
206
+ function registerFrameworkCache(adapter, cache, ttlMs) {
207
+ providers.set(adapter, { cache, ttlMs });
208
+ }
209
+ function registerFrameworkCacheStorage(adapter, bind) {
210
+ storageBindings.set(adapter, bind);
211
+ }
212
+ function validateFrameworkCacheOptions(ttlMs, timeoutMs) {
213
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 1)
214
+ throw new TypeError("cache ttlMs must be a positive safe integer.");
215
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 1e3)
216
+ throw new TypeError(
217
+ "cache timeoutMs must be a positive integer no greater than 1000."
218
+ );
219
+ }
220
+ function createCachingFrameworkAdapter(options) {
221
+ const bind = storageBindings.get(options.storage);
222
+ if (!bind)
223
+ throw new TypeError(
224
+ "storage must support framework payload caching (currently PostgreSQL)."
225
+ );
226
+ const provider = providers.get(options.cache);
227
+ if (!provider)
228
+ throw new TypeError(
229
+ "cache must provide framework payload caching (currently Redis)."
230
+ );
231
+ if (attachedCaches.has(options.cache))
232
+ throw new TypeError("The cache adapter already has a storage owner.");
233
+ const timeoutMs = options.timeoutMs ?? 25;
234
+ validateFrameworkCacheOptions(provider.ttlMs, timeoutMs);
235
+ bind(provider.cache, provider.ttlMs, timeoutMs);
236
+ attachedCaches.add(options.cache);
237
+ return options.storage;
238
+ }
239
+
240
+ // src/framework/maintenance.ts
241
+ function resolvePruneOptions(options = {}) {
242
+ const now = options.now?.getTime() ?? Date.now();
243
+ if (!Number.isSafeInteger(now) || now < 0 || now > Date.now())
244
+ throw new TypeError(
245
+ "prune now must be a valid date no later than the current time."
246
+ );
247
+ const batchSize = options.batchSize ?? 500;
248
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 1e3)
249
+ throw new TypeError(
250
+ "batchSize must be a positive integer no greater than 1000."
251
+ );
252
+ return { now, batchSize };
253
+ }
254
+ function emptyPruneResult() {
255
+ return {
256
+ skipped: false,
257
+ hasMore: false,
258
+ deleted: {
259
+ pendingRequests: 0,
260
+ sessionCheckpoints: 0,
261
+ graphCheckpoints: 0,
262
+ sessions: 0,
263
+ runs: 0
264
+ }
265
+ };
266
+ }
267
+
190
268
  // src/framework/pending-requests.ts
191
269
  function createInMemoryPendingRequestStore() {
192
270
  const store = /* @__PURE__ */ new Map();
193
- const prune = () => {
194
- const t = Date.now();
271
+ const pruneExpired = (t, batchSize) => {
272
+ let deleted = 0;
195
273
  for (const [k, v] of store.entries()) {
196
- if (t - v.createdAt > v.ttlMs) store.delete(k);
274
+ if (deleted === batchSize) break;
275
+ if (t - v.createdAt > v.ttlMs) {
276
+ store.delete(k);
277
+ deleted++;
278
+ }
197
279
  }
280
+ return deleted;
198
281
  };
282
+ const prune = () => pruneExpired(Date.now(), Number.POSITIVE_INFINITY);
199
283
  return {
284
+ pruneExpired,
200
285
  async list() {
201
286
  prune();
202
287
  return JSON.parse(
@@ -205,9 +290,9 @@ function createInMemoryPendingRequestStore() {
205
290
  },
206
291
  async take(token) {
207
292
  prune();
208
- const record = store.get(token);
293
+ const record2 = store.get(token);
209
294
  store.delete(token);
210
- return record ? JSON.parse(JSON.stringify(record)) : null;
295
+ return record2 ? JSON.parse(JSON.stringify(record2)) : null;
211
296
  },
212
297
  async save(rec) {
213
298
  prune();
@@ -228,6 +313,1008 @@ function createInMemoryPendingRequestStore() {
228
313
  };
229
314
  }
230
315
 
316
+ // src/framework/postgres/adapter.ts
317
+ var import_node_crypto4 = require("crypto");
318
+ var import_errors5 = require("@kortyx/core/errors");
319
+
320
+ // src/framework/postgres/checkpointer.ts
321
+ var import_errors3 = require("@kortyx/core/errors");
322
+ var import_langgraph_checkpoint2 = require("@langchain/langgraph-checkpoint");
323
+
324
+ // src/framework/postgres/store.ts
325
+ var import_node_crypto2 = require("crypto");
326
+ var import_errors2 = require("@kortyx/core/errors");
327
+ var import_postgres = __toESM(require("postgres"));
328
+
329
+ // src/framework/postgres/migrations/index.ts
330
+ var import_node_crypto = require("crypto");
331
+ var import_errors = require("@kortyx/core/errors");
332
+
333
+ // src/framework/postgres/migrations/0001-initial-runtime.ts
334
+ var initialRuntimeMigration = Object.freeze({
335
+ version: 1,
336
+ description: "Initial runtime persistence tables",
337
+ sql: `
338
+ CREATE TABLE IF NOT EXISTS kortyx_runtime_sessions (
339
+ scope text NOT NULL, id text NOT NULL, head_id text,
340
+ last_activity bigint NOT NULL, next_turn bigint NOT NULL DEFAULT 0,
341
+ PRIMARY KEY (scope, id)
342
+ );
343
+ CREATE INDEX IF NOT EXISTS kortyx_runtime_sessions_activity
344
+ ON kortyx_runtime_sessions (scope, last_activity);
345
+ CREATE TABLE IF NOT EXISTS kortyx_runtime_runs (
346
+ scope text NOT NULL, id text NOT NULL, session_id text,
347
+ last_activity bigint NOT NULL, revision bigint NOT NULL DEFAULT 0,
348
+ lease_token text, lease_until bigint NOT NULL DEFAULT 0,
349
+ PRIMARY KEY (scope, id)
350
+ );
351
+ CREATE INDEX IF NOT EXISTS kortyx_runtime_runs_activity
352
+ ON kortyx_runtime_runs (scope, last_activity);
353
+ CREATE INDEX IF NOT EXISTS kortyx_runtime_runs_session
354
+ ON kortyx_runtime_runs (scope, session_id);
355
+ CREATE TABLE IF NOT EXISTS kortyx_runtime_graph_checkpoints (
356
+ scope text NOT NULL, run_id text NOT NULL, ns text NOT NULL, id text NOT NULL,
357
+ position bigserial NOT NULL, record jsonb NOT NULL,
358
+ PRIMARY KEY (scope, run_id, ns, id),
359
+ FOREIGN KEY (scope, run_id) REFERENCES kortyx_runtime_runs (scope, id) ON DELETE CASCADE
360
+ );
361
+ CREATE INDEX IF NOT EXISTS kortyx_runtime_graph_latest
362
+ ON kortyx_runtime_graph_checkpoints (scope, run_id, ns, position DESC);
363
+ CREATE TABLE IF NOT EXISTS kortyx_runtime_graph_writes (
364
+ scope text NOT NULL, run_id text NOT NULL, ns text NOT NULL, checkpoint_id text NOT NULL,
365
+ task_id text NOT NULL, idx integer NOT NULL, channel text NOT NULL, value jsonb NOT NULL,
366
+ PRIMARY KEY (scope, run_id, ns, checkpoint_id, task_id, idx),
367
+ FOREIGN KEY (scope, run_id, ns, checkpoint_id)
368
+ REFERENCES kortyx_runtime_graph_checkpoints (scope, run_id, ns, id) ON DELETE CASCADE
369
+ );
370
+ CREATE TABLE IF NOT EXISTS kortyx_runtime_session_checkpoints (
371
+ scope text NOT NULL, id text NOT NULL, session_id text NOT NULL, run_id text NOT NULL,
372
+ created_at bigint NOT NULL, turn_index bigint NOT NULL, active boolean NOT NULL DEFAULT true,
373
+ record jsonb NOT NULL,
374
+ PRIMARY KEY (scope, id),
375
+ FOREIGN KEY (scope, session_id) REFERENCES kortyx_runtime_sessions (scope, id) ON DELETE CASCADE
376
+ );
377
+ CREATE INDEX IF NOT EXISTS kortyx_runtime_session_history
378
+ ON kortyx_runtime_session_checkpoints (scope, session_id, turn_index);
379
+ CREATE INDEX IF NOT EXISTS kortyx_runtime_session_runs
380
+ ON kortyx_runtime_session_checkpoints (scope, run_id);
381
+ CREATE INDEX IF NOT EXISTS kortyx_runtime_checkpoint_age
382
+ ON kortyx_runtime_session_checkpoints (scope, created_at);
383
+ CREATE TABLE IF NOT EXISTS kortyx_runtime_pending_requests (
384
+ scope text NOT NULL, token text NOT NULL, run_id text NOT NULL, session_id text,
385
+ expires_at bigint NOT NULL, record jsonb NOT NULL,
386
+ PRIMARY KEY (scope, token)
387
+ );
388
+ CREATE INDEX IF NOT EXISTS kortyx_runtime_pending_expiry
389
+ ON kortyx_runtime_pending_requests (scope, expires_at);
390
+ CREATE INDEX IF NOT EXISTS kortyx_runtime_pending_runs
391
+ ON kortyx_runtime_pending_requests (scope, run_id);
392
+ CREATE INDEX IF NOT EXISTS kortyx_runtime_pending_sessions
393
+ ON kortyx_runtime_pending_requests (scope, session_id);
394
+ `
395
+ });
396
+
397
+ // src/framework/postgres/migrations/index.ts
398
+ var runtimeMigrations = Object.freeze([
399
+ initialRuntimeMigration
400
+ ]);
401
+ async function runRuntimeMigrations(database, migrations = runtimeMigrations) {
402
+ if (!migrations.length)
403
+ throw new TypeError("Runtime migrations must include version 1.");
404
+ for (const [index, migration] of migrations.entries()) {
405
+ if (migration.version !== index + 1 || !migration.sql.trim())
406
+ throw new TypeError(
407
+ "Runtime migrations must contain nonempty SQL with consecutive versions starting at 1."
408
+ );
409
+ }
410
+ const checksums = migrations.map(
411
+ (migration) => (0, import_node_crypto.createHash)("sha256").update(migration.sql).digest("hex")
412
+ );
413
+ for (const migration of migrations) {
414
+ try {
415
+ await database.begin("isolation level read committed", async (sql) => {
416
+ await sql`SELECT set_config('lock_timeout', CASE WHEN current_setting('lock_timeout')::interval = interval '0' OR current_setting('lock_timeout')::interval > interval '5 seconds' THEN '5s' ELSE current_setting('lock_timeout') END, true)`;
417
+ await sql`SELECT pg_advisory_xact_lock(hashtextextended('kortyx:runtime:schema', 0))`;
418
+ await sql`CREATE TABLE IF NOT EXISTS kortyx_runtime_migrations (version integer PRIMARY KEY, checksum text)`;
419
+ const checksumColumn = await sql`SELECT 1 FROM pg_attribute WHERE attrelid = 'kortyx_runtime_migrations'::regclass AND attname = 'checksum' AND NOT attisdropped`;
420
+ const legacyLedger = !checksumColumn.length;
421
+ if (legacyLedger)
422
+ await sql`ALTER TABLE kortyx_runtime_migrations ADD COLUMN checksum text`;
423
+ const applied = await sql`SELECT version, checksum FROM kortyx_runtime_migrations ORDER BY version`;
424
+ if (applied.some((row) => row.version > migrations.length))
425
+ throw new import_errors.PersistenceError(
426
+ "The runtime schema is newer than this SDK supports."
427
+ );
428
+ if (applied.some((row, index) => row.version !== index + 1) || applied.length < migration.version - 1)
429
+ throw new import_errors.PersistenceError(
430
+ "The runtime migration history is not a consecutive sequence starting at version 1."
431
+ );
432
+ for (const row of applied) {
433
+ const expected = checksums[row.version - 1];
434
+ if (legacyLedger && applied.length === 1 && row.version === 1) {
435
+ await sql`UPDATE kortyx_runtime_migrations SET checksum = ${expected} WHERE version = 1`;
436
+ } else if (row.checksum !== expected) {
437
+ throw new import_errors.PersistenceError(
438
+ `Runtime migration ${row.version} checksum does not match this SDK. Restore the original migration; add a new version for changes.`
439
+ );
440
+ }
441
+ }
442
+ if (migration.version <= applied.length) return;
443
+ await sql.unsafe(migration.sql);
444
+ await sql`INSERT INTO kortyx_runtime_migrations (version, checksum) VALUES (${migration.version}, ${checksums[migration.version - 1]})`;
445
+ });
446
+ } catch (cause) {
447
+ if (cause instanceof import_errors.PersistenceError) throw cause;
448
+ throw new import_errors.PersistenceError(
449
+ `Runtime migration ${migration.version} (${migration.description}) failed.`,
450
+ cause
451
+ );
452
+ }
453
+ }
454
+ }
455
+
456
+ // src/framework/postgres/store.ts
457
+ var positiveInteger = (name, value) => {
458
+ if (!Number.isSafeInteger(value) || value <= 0)
459
+ throw new TypeError(`${name} must be a positive safe integer.`);
460
+ return value;
461
+ };
462
+ var clone = (value) => JSON.parse(JSON.stringify(value));
463
+ var PostgresRuntimeStore = class {
464
+ constructor(connectionString, namespace, retention, cache, cacheTtlMs = 15 * 60 * 1e3, cacheTimeoutMs = 25) {
465
+ this.cache = cache;
466
+ this.cacheTtlMs = cacheTtlMs;
467
+ this.cacheTimeoutMs = cacheTimeoutMs;
468
+ const url = new URL(connectionString);
469
+ if (!["postgres:", "postgresql:"].includes(url.protocol))
470
+ throw new TypeError("connectionString must be a PostgreSQL URL.");
471
+ if (!namespace) throw new TypeError("namespace must not be empty.");
472
+ this.scope = namespace;
473
+ this.historyMs = positiveInteger(
474
+ "checkpointHistoryDays",
475
+ retention.checkpointHistoryDays ?? 30
476
+ ) * 864e5;
477
+ this.sessionMs = positiveInteger(
478
+ "inactiveSessionDays",
479
+ retention.inactiveSessionDays ?? 30
480
+ ) * 864e5;
481
+ positiveInteger("checkpoint history duration", this.historyMs);
482
+ positiveInteger("session retention duration", this.sessionMs);
483
+ validateFrameworkCacheOptions(cacheTtlMs, cacheTimeoutMs);
484
+ this.sql = (0, import_postgres.default)(connectionString, {
485
+ max: 10,
486
+ connect_timeout: 5,
487
+ onnotice: () => {
488
+ }
489
+ });
490
+ this.cachePrefix = (0, import_node_crypto2.createHash)("sha256").update(`${connectionString}\0${namespace}`).digest("hex");
491
+ }
492
+ cache;
493
+ cacheTtlMs;
494
+ cacheTimeoutMs;
495
+ sql;
496
+ scope;
497
+ historyMs;
498
+ sessionMs;
499
+ cachePrefix;
500
+ cacheRetryAfter = 0;
501
+ cacheClosed = false;
502
+ cachePopulations = /* @__PURE__ */ new Map();
503
+ get cacheEnabled() {
504
+ return Boolean(this.cache);
505
+ }
506
+ attachCache(cache, ttlMs, timeoutMs) {
507
+ if (this.cacheClosed)
508
+ throw new TypeError("Cannot configure a closed storage adapter.");
509
+ if (this.cache)
510
+ throw new TypeError("Storage already has a framework cache.");
511
+ validateFrameworkCacheOptions(ttlMs, timeoutMs);
512
+ this.cache = cache;
513
+ this.cacheTtlMs = ttlMs;
514
+ this.cacheTimeoutMs = timeoutMs;
515
+ }
516
+ async setup() {
517
+ await this.query(runRuntimeMigrations(this.sql));
518
+ }
519
+ async query(query) {
520
+ try {
521
+ return await query;
522
+ } catch (cause) {
523
+ if (cause instanceof import_errors2.KortyxError) throw cause;
524
+ throw new import_errors2.PersistenceError("PostgreSQL runtime operation failed.", cause);
525
+ }
526
+ }
527
+ async transaction(operation) {
528
+ try {
529
+ return await this.sql.begin(async (sql) => {
530
+ await sql`SELECT pg_advisory_xact_lock_shared(hashtextextended(${`kortyx:runtime:${this.scope}`}, 0))`;
531
+ return operation(sql);
532
+ });
533
+ } catch (cause) {
534
+ if (cause instanceof import_errors2.KortyxError) throw cause;
535
+ throw new import_errors2.PersistenceError("PostgreSQL runtime operation failed.", cause);
536
+ }
537
+ }
538
+ liveSession(sql, now) {
539
+ return sql`(s.last_activity > ${now - this.sessionMs}
540
+ OR EXISTS (SELECT 1 FROM kortyx_runtime_pending_requests p
541
+ WHERE p.scope = s.scope AND p.session_id = s.id AND p.expires_at > ${now})
542
+ OR EXISTS (SELECT 1 FROM kortyx_runtime_runs r
543
+ WHERE r.scope = s.scope AND r.session_id = s.id AND r.lease_until > ${now}))`;
544
+ }
545
+ liveCheckpoint(sql, now) {
546
+ return sql`(c.created_at > ${now - this.historyMs} OR s.head_id = c.id
547
+ OR EXISTS (SELECT 1 FROM kortyx_runtime_pending_requests p
548
+ WHERE p.scope = c.scope AND p.run_id = c.run_id AND p.expires_at > ${now})
549
+ OR EXISTS (SELECT 1 FROM kortyx_runtime_runs r
550
+ WHERE r.scope = c.scope AND r.id = c.run_id AND r.lease_until > ${now}))`;
551
+ }
552
+ liveRun(sql, now) {
553
+ return sql`(r.last_activity > ${now - this.historyMs} OR r.lease_until > ${now}
554
+ OR EXISTS (SELECT 1 FROM kortyx_runtime_pending_requests p
555
+ WHERE p.scope = r.scope AND p.run_id = r.id AND p.expires_at > ${now})
556
+ OR EXISTS (SELECT 1 FROM kortyx_runtime_session_checkpoints c
557
+ JOIN kortyx_runtime_sessions s ON s.scope = c.scope AND s.id = c.session_id
558
+ WHERE c.scope = r.scope AND c.run_id = r.id
559
+ AND ${this.liveSession(sql, now)} AND ${this.liveCheckpoint(sql, now)}))`;
560
+ }
561
+ async touchRun(sql, runId) {
562
+ await sql`INSERT INTO kortyx_runtime_runs (scope, id, last_activity)
563
+ VALUES (${this.scope}, ${runId}, ${Date.now()})
564
+ ON CONFLICT (scope, id) DO UPDATE SET last_activity = EXCLUDED.last_activity,
565
+ revision = kortyx_runtime_runs.revision + 1`;
566
+ }
567
+ async touchSession(sql, sessionId) {
568
+ const updated = await sql`INSERT INTO kortyx_runtime_sessions AS s (scope, id, last_activity)
569
+ VALUES (${this.scope}, ${sessionId}, ${Date.now()})
570
+ ON CONFLICT (scope, id) DO UPDATE SET last_activity = EXCLUDED.last_activity
571
+ WHERE ${this.liveSession(sql, Date.now())} RETURNING id`;
572
+ if (!updated.length)
573
+ throw new import_errors2.KortyxError(
574
+ "SESSION_EXPIRED",
575
+ "The runtime session has expired. Start a new session.",
576
+ {
577
+ category: "request",
578
+ retryable: false,
579
+ safeMessage: "This session has expired. Start a new session."
580
+ }
581
+ );
582
+ }
583
+ async payload(key, load) {
584
+ const cacheKey = `${this.cachePrefix}:${(0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(key)).digest("hex")}`;
585
+ const cache = this.cache;
586
+ if (cache && !this.cacheClosed && Date.now() >= this.cacheRetryAfter) {
587
+ try {
588
+ const raw = await this.cacheCall(() => cache.get(cacheKey));
589
+ if (raw) return JSON.parse(raw);
590
+ } catch {
591
+ this.cacheRetryAfter = Date.now() + 5e3;
592
+ }
593
+ }
594
+ const value = await load();
595
+ if (cache && !this.cacheClosed && Date.now() >= this.cacheRetryAfter && value !== void 0 && value !== null && !this.cachePopulations.has(cacheKey) && this.cachePopulations.size < 10) {
596
+ const population = this.cacheCall(
597
+ () => cache.set(cacheKey, JSON.stringify(value), this.cacheTtlMs)
598
+ ).then(() => {
599
+ }).catch(() => {
600
+ this.cacheRetryAfter = Date.now() + 5e3;
601
+ }).finally(() => {
602
+ this.cachePopulations.delete(cacheKey);
603
+ });
604
+ this.cachePopulations.set(cacheKey, population);
605
+ }
606
+ return value;
607
+ }
608
+ async cacheCall(operation) {
609
+ let timer;
610
+ try {
611
+ return await Promise.race([
612
+ operation(),
613
+ new Promise((_resolve, reject) => {
614
+ timer = setTimeout(() => {
615
+ void this.cache?.close?.().catch(() => {
616
+ });
617
+ reject(new Error("Runtime cache operation timed out."));
618
+ }, this.cacheTimeoutMs);
619
+ timer.unref();
620
+ })
621
+ ]);
622
+ } finally {
623
+ clearTimeout(timer);
624
+ }
625
+ }
626
+ async close() {
627
+ this.cacheClosed = true;
628
+ try {
629
+ await Promise.all(this.cachePopulations.values());
630
+ await this.cache?.close?.();
631
+ } finally {
632
+ await this.sql.end({ timeout: 5 });
633
+ }
634
+ }
635
+ };
636
+
637
+ // src/framework/postgres/checkpointer.ts
638
+ var configFor = (runId, ns, id) => ({
639
+ configurable: { thread_id: runId, checkpoint_ns: ns, checkpoint_id: id }
640
+ });
641
+ var PostgresCheckpointSaver = class extends import_langgraph_checkpoint2.BaseCheckpointSaver {
642
+ constructor(store) {
643
+ super({
644
+ dumpsTyped: async (data) => [
645
+ "json",
646
+ Buffer.from(JSON.stringify(data))
647
+ ],
648
+ loadsTyped: async (_type, data) => JSON.parse(
649
+ typeof data === "string" ? data : Buffer.from(data).toString("utf8")
650
+ )
651
+ });
652
+ this.store = store;
653
+ }
654
+ store;
655
+ async getTuple(config) {
656
+ const runId = config.configurable?.thread_id;
657
+ if (!runId) return void 0;
658
+ const ns = config.configurable?.checkpoint_ns ?? "";
659
+ const id = (0, import_langgraph_checkpoint2.getCheckpointId)(config);
660
+ const { sql, scope } = this.store;
661
+ const rows = await this.store.query(sql`SELECT g.id, g.position, r.revision
662
+ FROM kortyx_runtime_graph_checkpoints g
663
+ JOIN kortyx_runtime_runs r ON r.scope = g.scope AND r.id = g.run_id
664
+ WHERE g.scope = ${scope} AND g.run_id = ${runId} AND g.ns = ${ns}
665
+ AND g.record ? 'checkpoint'
666
+ ${id ? sql`AND g.id = ${id}` : sql``}
667
+ AND ${this.store.liveRun(sql, Date.now())}
668
+ ORDER BY g.position DESC LIMIT 1`);
669
+ const row = rows[0];
670
+ if (!row) return void 0;
671
+ return this.store.payload(
672
+ ["graph", runId, ns, row.id, row.position, row.revision],
673
+ async () => {
674
+ const [records, writes] = await Promise.all([
675
+ this.store.query(sql`SELECT record FROM kortyx_runtime_graph_checkpoints
676
+ WHERE scope = ${scope} AND run_id = ${runId} AND ns = ${ns} AND id = ${row.id}`),
677
+ this.store.query(sql`SELECT task_id, channel, value FROM kortyx_runtime_graph_writes
678
+ WHERE scope = ${scope} AND run_id = ${runId} AND ns = ${ns} AND checkpoint_id = ${row.id}
679
+ ORDER BY task_id, idx`)
680
+ ]);
681
+ if (!records[0]) return void 0;
682
+ const record2 = records[0].record;
683
+ return {
684
+ config: configFor(runId, ns, row.id),
685
+ checkpoint: record2.checkpoint,
686
+ metadata: record2.metadata,
687
+ ...record2.parentCheckpointId ? { parentConfig: configFor(runId, ns, record2.parentCheckpointId) } : {},
688
+ pendingWrites: writes.map(
689
+ (write) => [
690
+ write.task_id,
691
+ write.channel,
692
+ write.value.value
693
+ ]
694
+ )
695
+ };
696
+ }
697
+ );
698
+ }
699
+ async *list(config, options) {
700
+ const { sql, scope } = this.store;
701
+ const runId = config.configurable?.thread_id;
702
+ const ns = config.configurable?.checkpoint_ns;
703
+ const id = (0, import_langgraph_checkpoint2.getCheckpointId)(config);
704
+ const before = options?.before ? (0, import_langgraph_checkpoint2.getCheckpointId)(options.before) : void 0;
705
+ const rows = await this.store.query(sql`SELECT g.run_id, g.ns, g.id FROM kortyx_runtime_graph_checkpoints g
706
+ JOIN kortyx_runtime_runs r ON r.scope = g.scope AND r.id = g.run_id
707
+ WHERE g.scope = ${scope} AND ${this.store.liveRun(sql, Date.now())}
708
+ AND g.record ? 'checkpoint'
709
+ ${runId ? sql`AND g.run_id = ${runId}` : sql``}
710
+ ${ns !== void 0 ? sql`AND g.ns = ${ns}` : sql``}
711
+ ${id ? sql`AND g.id = ${id}` : sql``}
712
+ ${before ? sql`AND g.id < ${before}` : sql``}
713
+ ${options?.filter ? sql`AND g.record->'metadata' @> ${sql.json(clone(options.filter))}` : sql``}
714
+ ORDER BY g.id DESC ${options?.limit !== void 0 ? sql`LIMIT ${options.limit}` : sql``}`);
715
+ for (const row of rows) {
716
+ const tuple = await this.getTuple(configFor(row.run_id, row.ns, row.id));
717
+ if (tuple) yield tuple;
718
+ }
719
+ }
720
+ async put(config, checkpoint, metadata, _newVersions) {
721
+ const runId = config.configurable?.thread_id;
722
+ if (!runId)
723
+ throw new import_errors3.PersistenceError(
724
+ 'Failed to put checkpoint: missing "thread_id".'
725
+ );
726
+ const ns = config.configurable?.checkpoint_ns ?? "";
727
+ const parentCheckpointId = config.configurable?.checkpoint_id;
728
+ const record2 = {
729
+ checkpoint: (0, import_langgraph_checkpoint2.copyCheckpoint)(checkpoint),
730
+ metadata,
731
+ ...parentCheckpointId ? { parentCheckpointId } : {}
732
+ };
733
+ await this.store.transaction(async (sql) => {
734
+ await this.store.touchRun(sql, runId);
735
+ await sql`INSERT INTO kortyx_runtime_graph_checkpoints (scope, run_id, ns, id, record)
736
+ VALUES (${this.store.scope}, ${runId}, ${ns}, ${checkpoint.id}, ${sql.json(clone(record2))})
737
+ ON CONFLICT (scope, run_id, ns, id) DO UPDATE SET record = EXCLUDED.record,
738
+ position = EXCLUDED.position`;
739
+ });
740
+ return configFor(runId, ns, checkpoint.id);
741
+ }
742
+ async putWrites(config, writes, taskId) {
743
+ const runId = config.configurable?.thread_id;
744
+ const id = (0, import_langgraph_checkpoint2.getCheckpointId)(config);
745
+ if (!runId || !id)
746
+ throw new import_errors3.PersistenceError(
747
+ 'Failed to put writes: missing "thread_id" or "checkpoint_id".'
748
+ );
749
+ const ns = config.configurable?.checkpoint_ns ?? "";
750
+ await this.store.transaction(async (sql) => {
751
+ await this.store.touchRun(sql, runId);
752
+ await sql`INSERT INTO kortyx_runtime_graph_checkpoints (scope, run_id, ns, id, record)
753
+ VALUES (${this.store.scope}, ${runId}, ${ns}, ${id}, ${sql.json({})})
754
+ ON CONFLICT (scope, run_id, ns, id) DO NOTHING`;
755
+ const rows = /* @__PURE__ */ new Map();
756
+ for (const [idx, [channel, value]] of writes.entries()) {
757
+ const mapped = import_langgraph_checkpoint2.WRITES_IDX_MAP[channel] ?? idx;
758
+ rows.set(mapped, {
759
+ scope: this.store.scope,
760
+ run_id: runId,
761
+ ns,
762
+ checkpoint_id: id,
763
+ task_id: taskId,
764
+ idx: mapped,
765
+ channel,
766
+ value: sql.json({
767
+ value: clone(value ?? null)
768
+ })
769
+ });
770
+ }
771
+ if (rows.size)
772
+ await sql`INSERT INTO kortyx_runtime_graph_writes
773
+ ${sql([...rows.values()], "scope", "run_id", "ns", "checkpoint_id", "task_id", "idx", "channel", "value")}
774
+ ON CONFLICT (scope, run_id, ns, checkpoint_id, task_id, idx)
775
+ DO UPDATE SET channel = EXCLUDED.channel, value = EXCLUDED.value WHERE EXCLUDED.idx < 0`;
776
+ });
777
+ }
778
+ async deleteThread(runId) {
779
+ await this.store.transaction(async (sql) => {
780
+ await sql`DELETE FROM kortyx_runtime_runs WHERE scope = ${this.store.scope} AND id = ${runId}`;
781
+ });
782
+ }
783
+ async deleteCheckpointWrites(runId, ns, id) {
784
+ await this.store.transaction(async (sql) => {
785
+ await this.store.touchRun(sql, runId);
786
+ await sql`DELETE FROM kortyx_runtime_graph_writes
787
+ WHERE scope = ${this.store.scope} AND run_id = ${runId} AND ns = ${ns} AND checkpoint_id = ${id}`;
788
+ });
789
+ }
790
+ async getLatestCheckpointId(runId, ns = "") {
791
+ return (await this.getTuple({
792
+ configurable: { thread_id: runId, checkpoint_ns: ns }
793
+ }))?.checkpoint.id;
794
+ }
795
+ };
796
+
797
+ // src/framework/postgres/maintenance.ts
798
+ function createRuntimeMaintenance(store) {
799
+ return {
800
+ setup: () => store.setup(),
801
+ async prune(options = {}) {
802
+ const { now, batchSize: batch } = resolvePruneOptions(options);
803
+ return await store.query(
804
+ store.sql.begin(async (sql) => {
805
+ const deleted = {
806
+ pendingRequests: 0,
807
+ sessionCheckpoints: 0,
808
+ graphCheckpoints: 0,
809
+ sessions: 0,
810
+ runs: 0
811
+ };
812
+ const locks = await sql`SELECT pg_try_advisory_xact_lock(hashtextextended(${`kortyx:runtime:${store.scope}`}, 0)) AS acquired`;
813
+ if (!locks[0]?.acquired)
814
+ return { skipped: true, hasMore: false, deleted };
815
+ const pending = await sql`DELETE FROM kortyx_runtime_pending_requests WHERE (scope, token) IN (
816
+ SELECT scope, token FROM kortyx_runtime_pending_requests
817
+ WHERE scope = ${store.scope} AND expires_at <= ${now} ORDER BY expires_at LIMIT ${batch}
818
+ ) RETURNING token`;
819
+ deleted.pendingRequests = pending.length;
820
+ const checkpoints = await sql`DELETE FROM kortyx_runtime_session_checkpoints WHERE (scope, id) IN (
821
+ SELECT c.scope, c.id FROM kortyx_runtime_session_checkpoints c
822
+ JOIN kortyx_runtime_sessions s ON s.scope = c.scope AND s.id = c.session_id
823
+ WHERE c.scope = ${store.scope}
824
+ AND (NOT ${store.liveSession(sql, now)} OR NOT ${store.liveCheckpoint(sql, now)})
825
+ ORDER BY c.created_at LIMIT ${batch}
826
+ ) RETURNING id`;
827
+ deleted.sessionCheckpoints = checkpoints.length;
828
+ const graph = await sql`DELETE FROM kortyx_runtime_graph_checkpoints WHERE (scope, run_id, ns, id) IN (
829
+ SELECT g.scope, g.run_id, g.ns, g.id FROM kortyx_runtime_graph_checkpoints g
830
+ JOIN kortyx_runtime_runs r ON r.scope = g.scope AND r.id = g.run_id
831
+ WHERE g.scope = ${store.scope} AND NOT ${store.liveRun(sql, now)}
832
+ AND NOT EXISTS (SELECT 1 FROM kortyx_runtime_session_checkpoints c WHERE c.scope = r.scope AND c.run_id = r.id)
833
+ ORDER BY g.position LIMIT ${batch}
834
+ ) RETURNING id`;
835
+ deleted.graphCheckpoints = graph.length;
836
+ const sessions = await sql`DELETE FROM kortyx_runtime_sessions WHERE (scope, id) IN (
837
+ SELECT s.scope, s.id FROM kortyx_runtime_sessions s
838
+ WHERE s.scope = ${store.scope} AND NOT ${store.liveSession(sql, now)}
839
+ AND NOT EXISTS (SELECT 1 FROM kortyx_runtime_session_checkpoints c WHERE c.scope = s.scope AND c.session_id = s.id)
840
+ AND NOT EXISTS (SELECT 1 FROM kortyx_runtime_pending_requests p WHERE p.scope = s.scope AND p.session_id = s.id)
841
+ ORDER BY s.last_activity LIMIT ${batch}
842
+ ) RETURNING id`;
843
+ deleted.sessions = sessions.length;
844
+ const runs = await sql`DELETE FROM kortyx_runtime_runs WHERE (scope, id) IN (
845
+ SELECT r.scope, r.id FROM kortyx_runtime_runs r
846
+ WHERE r.scope = ${store.scope} AND NOT ${store.liveRun(sql, now)}
847
+ AND NOT EXISTS (SELECT 1 FROM kortyx_runtime_graph_checkpoints g WHERE g.scope = r.scope AND g.run_id = r.id)
848
+ AND NOT EXISTS (SELECT 1 FROM kortyx_runtime_session_checkpoints c WHERE c.scope = r.scope AND c.run_id = r.id)
849
+ AND NOT EXISTS (SELECT 1 FROM kortyx_runtime_pending_requests p WHERE p.scope = r.scope AND p.run_id = r.id)
850
+ ORDER BY r.last_activity LIMIT ${batch}
851
+ ) RETURNING id`;
852
+ deleted.runs = runs.length;
853
+ return {
854
+ skipped: false,
855
+ hasMore: Object.values(deleted).some((count) => count === batch),
856
+ deleted
857
+ };
858
+ })
859
+ );
860
+ }
861
+ };
862
+ }
863
+
864
+ // src/framework/postgres/pending-request-store.ts
865
+ async function savePendingRequest(store, sql, record2) {
866
+ positiveInteger("interrupt ttlMs", record2.ttlMs);
867
+ const expiresAt = record2.createdAt + record2.ttlMs;
868
+ if (!Number.isSafeInteger(record2.createdAt) || record2.createdAt < 0 || !Number.isSafeInteger(expiresAt))
869
+ throw new TypeError("Interrupt timestamps must be safe integers.");
870
+ if (expiresAt <= Date.now()) {
871
+ await store.query(
872
+ sql`DELETE FROM kortyx_runtime_pending_requests WHERE scope = ${store.scope} AND token = ${record2.token}`
873
+ );
874
+ return;
875
+ }
876
+ if (record2.sessionId) await store.touchSession(sql, record2.sessionId);
877
+ await store.touchRun(sql, record2.runId);
878
+ await store.query(sql`INSERT INTO kortyx_runtime_pending_requests (scope, token, run_id, session_id, expires_at, record)
879
+ VALUES (${store.scope}, ${record2.token}, ${record2.runId}, ${record2.sessionId ?? null}, ${expiresAt},
880
+ ${sql.json(clone(record2))})
881
+ ON CONFLICT (scope, token) DO UPDATE SET run_id = EXCLUDED.run_id, session_id = EXCLUDED.session_id,
882
+ expires_at = EXCLUDED.expires_at, record = EXCLUDED.record`);
883
+ }
884
+ function createPostgresPendingRequestStore(store) {
885
+ return {
886
+ async list() {
887
+ const rows = await store.query(store.sql`SELECT record FROM kortyx_runtime_pending_requests
888
+ WHERE scope = ${store.scope} AND expires_at > ${Date.now()} ORDER BY token`);
889
+ return rows.map((row) => row.record);
890
+ },
891
+ async get(token) {
892
+ const rows = await store.query(store.sql`SELECT record FROM kortyx_runtime_pending_requests
893
+ WHERE scope = ${store.scope} AND token = ${token} AND expires_at > ${Date.now()}`);
894
+ return rows[0] ? rows[0].record : null;
895
+ },
896
+ async take(token) {
897
+ return store.transaction(async (sql) => {
898
+ const rows = await sql`DELETE FROM kortyx_runtime_pending_requests
899
+ WHERE scope = ${store.scope} AND token = ${token} RETURNING record, expires_at`;
900
+ return rows[0] && Number(rows[0].expires_at) > Date.now() ? rows[0].record : null;
901
+ });
902
+ },
903
+ async save(record2) {
904
+ await store.transaction((sql) => savePendingRequest(store, sql, record2));
905
+ },
906
+ async delete(token) {
907
+ await store.transaction(async (sql) => {
908
+ await sql`DELETE FROM kortyx_runtime_pending_requests WHERE scope = ${store.scope} AND token = ${token}`;
909
+ });
910
+ },
911
+ async update(token, patch) {
912
+ await store.transaction(async (sql) => {
913
+ const found = await sql`SELECT run_id, session_id FROM kortyx_runtime_pending_requests
914
+ WHERE scope = ${store.scope} AND token = ${token}`;
915
+ if (!found[0]) return;
916
+ if (found[0].session_id)
917
+ await store.touchSession(sql, found[0].session_id);
918
+ await store.touchRun(sql, found[0].run_id);
919
+ const rows = await sql`SELECT record FROM kortyx_runtime_pending_requests
920
+ WHERE scope = ${store.scope} AND token = ${token} AND expires_at > ${Date.now()} FOR UPDATE`;
921
+ if (!rows[0]) return;
922
+ const previous = rows[0].record;
923
+ await savePendingRequest(store, sql, {
924
+ ...previous,
925
+ ...patch,
926
+ token: previous.token,
927
+ runId: previous.runId,
928
+ sessionId: previous.sessionId,
929
+ createdAt: previous.createdAt,
930
+ ttlMs: previous.ttlMs
931
+ });
932
+ });
933
+ }
934
+ };
935
+ }
936
+
937
+ // src/framework/postgres/session-checkpoint-store.ts
938
+ var import_node_crypto3 = require("crypto");
939
+ var import_errors4 = require("@kortyx/core/errors");
940
+
941
+ // src/framework/graph-snapshot.ts
942
+ async function captureGraphSnapshot(saver, threadId, checkpointId) {
943
+ const tuple = await saver.getTuple({
944
+ configurable: {
945
+ thread_id: threadId,
946
+ checkpoint_ns: "",
947
+ ...checkpointId ? { checkpoint_id: checkpointId } : {}
948
+ }
949
+ });
950
+ return tuple ? JSON.parse(JSON.stringify(tuple)) : void 0;
951
+ }
952
+ async function restoreGraphSnapshot(saver, snapshot, threadId) {
953
+ const copy = JSON.parse(JSON.stringify(snapshot));
954
+ const config = await saver.put(
955
+ { configurable: { thread_id: threadId, checkpoint_ns: "" } },
956
+ copy.checkpoint,
957
+ copy.metadata,
958
+ copy.checkpoint.channel_versions
959
+ );
960
+ const reset = saver;
961
+ if (reset.deleteCheckpointWrites)
962
+ await reset.deleteCheckpointWrites(threadId, "", copy.checkpoint.id);
963
+ const writes = /* @__PURE__ */ new Map();
964
+ for (const [task, channel, value] of copy.pendingWrites ?? []) {
965
+ const group = writes.get(task) ?? [];
966
+ group.push([channel, value]);
967
+ writes.set(task, group);
968
+ }
969
+ for (const [task, group] of writes)
970
+ await saver.putWrites(config, group, task);
971
+ }
972
+
973
+ // src/framework/postgres/session-checkpoint-store.ts
974
+ var unique = (values) => [
975
+ ...new Set(values.filter((value) => Boolean(value)))
976
+ ];
977
+ var notFound = (id) => new import_errors4.KortyxError(
978
+ "NOT_FOUND",
979
+ `Checkpoint "${id}" was not found or has expired.`,
980
+ {
981
+ category: "request",
982
+ retryable: false,
983
+ safeMessage: "The requested checkpoint was not found or has expired."
984
+ }
985
+ );
986
+ var assertInterruptsLive = (requests) => {
987
+ if (requests.some((request) => request.createdAt + request.ttlMs <= Date.now()))
988
+ throw new import_errors4.KortyxError(
989
+ "INTERRUPT_EXPIRED",
990
+ "The checkpoint contains an expired interrupt.",
991
+ {
992
+ category: "request",
993
+ retryable: false,
994
+ safeMessage: "This checkpoint's approval or input request has expired."
995
+ }
996
+ );
997
+ };
998
+ function createPostgresSessionCheckpointStore(store, saver) {
999
+ const visible = (sql, now) => sql`c.scope = ${store.scope}
1000
+ AND ${store.liveSession(sql, now)} AND ${store.liveCheckpoint(sql, now)}`;
1001
+ const read = async (row) => {
1002
+ if (!row) return null;
1003
+ const record2 = store.cacheEnabled ? await store.payload(["session", row.id], async () => {
1004
+ const records = await store.query(store.sql`SELECT record FROM kortyx_runtime_session_checkpoints
1005
+ WHERE scope = ${store.scope} AND id = ${row.id}`);
1006
+ return records[0] ? records[0].record : null;
1007
+ }) : row.record;
1008
+ return record2 ? {
1009
+ ...record2,
1010
+ branchStatus: row.active ? "active" : "abandoned"
1011
+ } : null;
1012
+ };
1013
+ const get = async (id) => {
1014
+ const { sql } = store;
1015
+ const rows = await store.query(sql`SELECT c.id, c.active ${store.cacheEnabled ? sql`` : sql`, c.record`} FROM kortyx_runtime_session_checkpoints c
1016
+ JOIN kortyx_runtime_sessions s ON s.scope = c.scope AND s.id = c.session_id
1017
+ WHERE c.id = ${id} AND ${visible(sql, Date.now())}`);
1018
+ return read(
1019
+ rows[0]
1020
+ );
1021
+ };
1022
+ const sealRequests = async (requests) => {
1023
+ return Promise.all(
1024
+ requests.map(async (request) => {
1025
+ if (request.graphSnapshot) return clone(request);
1026
+ const graphSnapshot = await captureGraphSnapshot(
1027
+ saver,
1028
+ request.runId,
1029
+ request.graphCheckpointId
1030
+ );
1031
+ if (!graphSnapshot)
1032
+ throw new import_errors4.KortyxError(
1033
+ "CHECKPOINT_INCOMPLETE",
1034
+ "A paused checkpoint needs a complete execution snapshot.",
1035
+ {
1036
+ category: "persistence",
1037
+ retryable: false,
1038
+ safeMessage: "The paused execution could not be saved completely."
1039
+ }
1040
+ );
1041
+ return { ...clone(request), graphSnapshot };
1042
+ })
1043
+ );
1044
+ };
1045
+ const insert = async (sql, record2) => {
1046
+ await store.query(sql`INSERT INTO kortyx_runtime_session_checkpoints (scope, id, session_id, run_id, created_at, turn_index, record)
1047
+ VALUES (${store.scope}, ${record2.id}, ${record2.sessionId}, ${record2.runId}, ${record2.createdAt}, ${record2.turnIndex},
1048
+ ${sql.json(clone(record2))})`);
1049
+ await store.query(sql`UPDATE kortyx_runtime_sessions SET head_id = ${record2.id}, next_turn = ${record2.turnIndex + 1}
1050
+ WHERE scope = ${store.scope} AND id = ${record2.sessionId}`);
1051
+ };
1052
+ return {
1053
+ managesPendingRequests: true,
1054
+ get,
1055
+ async list(sessionId) {
1056
+ const { sql } = store;
1057
+ const rows = await store.query(sql`SELECT c.record, c.active FROM kortyx_runtime_session_checkpoints c
1058
+ JOIN kortyx_runtime_sessions s ON s.scope = c.scope AND s.id = c.session_id
1059
+ WHERE c.session_id = ${sessionId} AND ${visible(sql, Date.now())} ORDER BY c.turn_index`);
1060
+ return rows.map((row) => {
1061
+ const {
1062
+ state: _state,
1063
+ effects: _effects,
1064
+ activePendingRequests: _pending,
1065
+ runId: _runId,
1066
+ graphCheckpointId: _graphId,
1067
+ ...summary
1068
+ } = row.record;
1069
+ return {
1070
+ ...summary,
1071
+ branchStatus: row.active ? "active" : "abandoned"
1072
+ };
1073
+ });
1074
+ },
1075
+ async getHead(sessionId) {
1076
+ const { sql } = store;
1077
+ const rows = await store.query(sql`SELECT c.id, c.active ${store.cacheEnabled ? sql`` : sql`, c.record`}
1078
+ FROM kortyx_runtime_sessions s JOIN kortyx_runtime_session_checkpoints c
1079
+ ON c.scope = s.scope AND c.id = s.head_id
1080
+ WHERE s.id = ${sessionId} AND ${visible(sql, Date.now())}`);
1081
+ return read(
1082
+ rows[0]
1083
+ );
1084
+ },
1085
+ async append(args) {
1086
+ const requests = await sealRequests(args.pendingRequests ?? []);
1087
+ return store.transaction(async (sql) => {
1088
+ await store.touchSession(sql, args.sessionId);
1089
+ const sessions = await sql`SELECT head_id, next_turn FROM kortyx_runtime_sessions
1090
+ WHERE scope = ${store.scope} AND id = ${args.sessionId} FOR UPDATE`;
1091
+ const session = sessions[0];
1092
+ if (!session)
1093
+ throw new import_errors4.PersistenceError(
1094
+ "The session disappeared during checkpoint append."
1095
+ );
1096
+ const record2 = {
1097
+ id: `cp-${(0, import_node_crypto3.randomUUID)()}`,
1098
+ sessionId: args.sessionId,
1099
+ runId: args.runId,
1100
+ turnIndex: Number(session.next_turn),
1101
+ createdAt: Date.now(),
1102
+ workflow: args.workflow,
1103
+ state: clone(args.state),
1104
+ nodes: unique(args.nodes ?? []),
1105
+ effects: {
1106
+ structuredStreamIds: unique(args.structuredStreamIds ?? []),
1107
+ interruptTokens: unique(requests.map((request) => request.token))
1108
+ },
1109
+ activePendingRequests: requests,
1110
+ ...session.head_id ? { parentCheckpointId: session.head_id } : {},
1111
+ ...args.graphCheckpointId ? { graphCheckpointId: args.graphCheckpointId } : {},
1112
+ ...args.label ? { label: args.label } : {},
1113
+ ...args.workflowVersion ? { workflowVersion: args.workflowVersion } : {},
1114
+ ...args.buildId ? { buildId: args.buildId } : {}
1115
+ };
1116
+ await insert(sql, record2);
1117
+ return record2;
1118
+ });
1119
+ },
1120
+ async rollbackTo(id, options) {
1121
+ return store.transaction(async (sql) => {
1122
+ const targets = await sql`SELECT c.record FROM kortyx_runtime_session_checkpoints c
1123
+ JOIN kortyx_runtime_sessions s ON s.scope = c.scope AND s.id = c.session_id
1124
+ WHERE c.id = ${id} AND ${visible(sql, Date.now())} FOR UPDATE OF s`;
1125
+ if (!targets[0]) throw notFound(id);
1126
+ const target = targets[0].record;
1127
+ assertInterruptsLive(target.activePendingRequests);
1128
+ const liveRuns = await sql`SELECT id FROM kortyx_runtime_runs
1129
+ WHERE scope = ${store.scope} AND session_id = ${target.sessionId} AND lease_until > ${Date.now()}`;
1130
+ if (liveRuns.length)
1131
+ throw new import_errors4.KortyxError(
1132
+ "SESSION_BUSY",
1133
+ "Cannot roll back a session while execution is active.",
1134
+ { category: "request", retryable: true }
1135
+ );
1136
+ const ancestry = await sql`WITH RECURSIVE ancestors(id) AS (
1137
+ SELECT ${id}::text UNION
1138
+ SELECT c.record->>'parentCheckpointId' FROM kortyx_runtime_session_checkpoints c
1139
+ JOIN ancestors a ON a.id = c.id WHERE c.scope = ${store.scope} AND c.record ? 'parentCheckpointId'
1140
+ ) SELECT id FROM ancestors`;
1141
+ const ancestorIds = ancestry.map((row) => row.id);
1142
+ const trailing = await sql`SELECT record FROM kortyx_runtime_session_checkpoints
1143
+ WHERE scope = ${store.scope} AND session_id = ${target.sessionId} AND active AND id NOT IN ${sql(ancestorIds)}`;
1144
+ const records = trailing.map(
1145
+ (row) => row.record
1146
+ );
1147
+ const streams = unique(
1148
+ records.flatMap((record2) => record2.effects.structuredStreamIds)
1149
+ );
1150
+ const tokens = unique(
1151
+ records.flatMap((record2) => [
1152
+ ...record2.effects.interruptTokens,
1153
+ ...record2.activePendingRequests.map((request) => request.token)
1154
+ ])
1155
+ );
1156
+ await sql`UPDATE kortyx_runtime_session_checkpoints SET active = (id IN ${sql(ancestorIds)})
1157
+ WHERE scope = ${store.scope} AND session_id = ${target.sessionId}`;
1158
+ await store.touchSession(sql, target.sessionId);
1159
+ await sql`UPDATE kortyx_runtime_sessions SET head_id = ${id} WHERE scope = ${store.scope} AND id = ${target.sessionId}`;
1160
+ if (tokens.length)
1161
+ await sql`DELETE FROM kortyx_runtime_pending_requests WHERE scope = ${store.scope} AND token IN ${sql(tokens)}`;
1162
+ const requests = clone(target.activePendingRequests);
1163
+ options?.preparePendingRequests?.(requests);
1164
+ for (const request of requests)
1165
+ await savePendingRequest(store, sql, request);
1166
+ return {
1167
+ sessionId: target.sessionId,
1168
+ head: id,
1169
+ invalidatedStructuredStreamIds: streams,
1170
+ invalidatedInterruptTokens: tokens,
1171
+ activePendingRequests: requests
1172
+ };
1173
+ });
1174
+ },
1175
+ async fork(id, options) {
1176
+ const source = await get(id);
1177
+ if (!source) throw notFound(id);
1178
+ assertInterruptsLive(source.activePendingRequests);
1179
+ const sessionId = options?.newSessionId || `session-${(0, import_node_crypto3.randomUUID)()}`;
1180
+ const runId = source.activePendingRequests.length ? `run-${(0, import_node_crypto3.randomUUID)()}` : source.runId;
1181
+ const {
1182
+ parentCheckpointId: _parent,
1183
+ branchStatus: _branch,
1184
+ ...sourceRecord
1185
+ } = source;
1186
+ const checkpoint = {
1187
+ ...clone(sourceRecord),
1188
+ id: `cp-${(0, import_node_crypto3.randomUUID)()}`,
1189
+ sessionId,
1190
+ runId,
1191
+ createdAt: Date.now(),
1192
+ parentSessionId: source.sessionId,
1193
+ forkedFrom: source.id,
1194
+ activePendingRequests: source.activePendingRequests.map((request) => ({
1195
+ ...clone(request),
1196
+ runId,
1197
+ sessionId,
1198
+ token: (0, import_node_crypto3.randomUUID)(),
1199
+ requestId: `human-${(0, import_node_crypto3.randomUUID)()}`
1200
+ }))
1201
+ };
1202
+ checkpoint.effects.interruptTokens = checkpoint.activePendingRequests.map(
1203
+ (request) => request.token
1204
+ );
1205
+ options?.preparePendingRequests?.(checkpoint.activePendingRequests);
1206
+ await store.transaction(async (sql) => {
1207
+ const existing = await sql`SELECT id FROM kortyx_runtime_sessions WHERE scope = ${store.scope} AND id = ${sessionId}`;
1208
+ if (existing.length)
1209
+ throw new import_errors4.KortyxError(
1210
+ "SESSION_EXISTS",
1211
+ "The fork session already exists.",
1212
+ { category: "request", retryable: false }
1213
+ );
1214
+ await sql`INSERT INTO kortyx_runtime_sessions (scope, id, last_activity) VALUES (${store.scope}, ${sessionId}, ${Date.now()})`;
1215
+ await insert(sql, checkpoint);
1216
+ for (const request of checkpoint.activePendingRequests)
1217
+ await savePendingRequest(store, sql, request);
1218
+ });
1219
+ return {
1220
+ sessionId,
1221
+ parentSessionId: source.sessionId,
1222
+ forkedFrom: source.id,
1223
+ checkpoint
1224
+ };
1225
+ }
1226
+ };
1227
+ }
1228
+
1229
+ // src/framework/postgres/adapter.ts
1230
+ function createPostgresFrameworkAdapter(options) {
1231
+ const ttlMs = positiveInteger("ttlMs", options.ttlMs ?? 15 * 60 * 1e3);
1232
+ const store = new PostgresRuntimeStore(
1233
+ options.connectionString,
1234
+ options.namespace ?? "default",
1235
+ options.retention ?? {}
1236
+ );
1237
+ const saver = new PostgresCheckpointSaver(store);
1238
+ const adapter = {
1239
+ kind: "postgres",
1240
+ ttlMs,
1241
+ checkpointer: saver,
1242
+ pendingRequests: createPostgresPendingRequestStore(store),
1243
+ sessionCheckpoints: createPostgresSessionCheckpointStore(store, saver),
1244
+ maintenance: createRuntimeMaintenance(store),
1245
+ // Completed runs remain available until maintenance applies retention.
1246
+ cleanupRun: async (runId) => store.transaction((sql) => store.touchRun(sql, runId)),
1247
+ acquireRunLease: async ({ runId, sessionId, onLost }) => {
1248
+ const token = (0, import_node_crypto4.randomUUID)();
1249
+ const renew = async () => {
1250
+ await store.transaction(async (sql) => {
1251
+ if (sessionId) await store.touchSession(sql, sessionId);
1252
+ const rows = await sql`UPDATE kortyx_runtime_runs SET lease_until = ${Date.now() + 12e4}, last_activity = ${Date.now()}
1253
+ WHERE scope = ${store.scope} AND id = ${runId} AND lease_token = ${token}
1254
+ AND lease_until > ${Date.now()} RETURNING id`;
1255
+ if (!rows.length)
1256
+ throw new import_errors5.PersistenceError("The runtime execution lease was lost.");
1257
+ });
1258
+ };
1259
+ await store.transaction(async (sql) => {
1260
+ if (sessionId) await store.touchSession(sql, sessionId);
1261
+ const rows = await sql`INSERT INTO kortyx_runtime_runs AS r (scope, id, last_activity, lease_token, lease_until, session_id)
1262
+ VALUES (${store.scope}, ${runId}, ${Date.now()}, ${token}, ${Date.now() + 12e4}, ${sessionId ?? null})
1263
+ ON CONFLICT (scope, id) DO UPDATE SET last_activity = EXCLUDED.last_activity,
1264
+ revision = r.revision + 1, lease_token = EXCLUDED.lease_token,
1265
+ lease_until = EXCLUDED.lease_until, session_id = EXCLUDED.session_id
1266
+ WHERE r.lease_until <= ${Date.now()} RETURNING id`;
1267
+ if (!rows.length)
1268
+ throw new import_errors5.PersistenceError("This runtime run is already executing.");
1269
+ });
1270
+ let renewing;
1271
+ let stopped = false;
1272
+ let deadline;
1273
+ const lost = (cause) => {
1274
+ if (stopped) return;
1275
+ stopped = true;
1276
+ clearInterval(timer);
1277
+ clearTimeout(deadline);
1278
+ onLost(cause);
1279
+ };
1280
+ const armDeadline = () => {
1281
+ clearTimeout(deadline);
1282
+ deadline = setTimeout(
1283
+ () => lost(new import_errors5.PersistenceError("Execution lease renewal timed out.")),
1284
+ 9e4
1285
+ );
1286
+ deadline.unref();
1287
+ };
1288
+ const timer = setInterval(() => {
1289
+ if (stopped || renewing) return;
1290
+ renewing = renew().then(() => {
1291
+ if (!stopped) armDeadline();
1292
+ }).catch(lost).finally(() => {
1293
+ renewing = void 0;
1294
+ });
1295
+ }, 3e4);
1296
+ timer.unref();
1297
+ armDeadline();
1298
+ return async () => {
1299
+ stopped = true;
1300
+ clearInterval(timer);
1301
+ clearTimeout(deadline);
1302
+ await renewing;
1303
+ await store.transaction(async (sql) => {
1304
+ await sql`UPDATE kortyx_runtime_runs SET lease_token = NULL, lease_until = 0, last_activity = ${Date.now()}
1305
+ WHERE scope = ${store.scope} AND id = ${runId} AND lease_token = ${token}`;
1306
+ });
1307
+ };
1308
+ },
1309
+ close: () => store.close()
1310
+ };
1311
+ registerFrameworkCacheStorage(
1312
+ adapter,
1313
+ (cache, ttlMs2, timeoutMs) => store.attachCache(cache, ttlMs2, timeoutMs)
1314
+ );
1315
+ return adapter;
1316
+ }
1317
+
231
1318
  // src/framework/redis/pending-request-store.ts
232
1319
  function createRedisPendingRequestStore(options) {
233
1320
  const prefix = options.prefix ?? "kortyx:pending:";
@@ -267,8 +1354,8 @@ function createRedisPendingRequestStore(options) {
267
1354
  }
268
1355
 
269
1356
  // src/framework/redis/redis-checkpointer.ts
270
- var import_errors = require("@kortyx/core/errors");
271
- var import_langgraph_checkpoint2 = require("@langchain/langgraph-checkpoint");
1357
+ var import_errors6 = require("@kortyx/core/errors");
1358
+ var import_langgraph_checkpoint3 = require("@langchain/langgraph-checkpoint");
272
1359
  var utf8Bytes2 = (s) => new Uint8Array(Buffer.from(s, "utf8"));
273
1360
  var utf8String2 = (u8) => typeof u8 === "string" ? u8 : Buffer.from(u8).toString("utf8");
274
1361
  var jsonSerde2 = {
@@ -297,7 +1384,7 @@ function createRedisCheckpointSaver(options) {
297
1384
  },
298
1385
  getNextVersion(current) {
299
1386
  if (typeof current === "string") {
300
- throw new import_errors.PersistenceError(
1387
+ throw new import_errors6.PersistenceError(
301
1388
  "Please override this method to use string versions."
302
1389
  );
303
1390
  }
@@ -307,7 +1394,7 @@ function createRedisCheckpointSaver(options) {
307
1394
  const threadId = config.configurable?.thread_id;
308
1395
  const checkpointNs = config.configurable?.checkpoint_ns ?? "";
309
1396
  if (!threadId) return void 0;
310
- const requested = (0, import_langgraph_checkpoint2.getCheckpointId)(config);
1397
+ const requested = (0, import_langgraph_checkpoint3.getCheckpointId)(config);
311
1398
  const checkpointId = requested || await store.get(latestKey(threadId, checkpointNs)) || "";
312
1399
  if (!checkpointId) return void 0;
313
1400
  const raw = await store.get(
@@ -365,11 +1452,11 @@ function createRedisCheckpointSaver(options) {
365
1452
  const threadId = config.configurable?.thread_id;
366
1453
  const checkpointNs = config.configurable?.checkpoint_ns ?? "";
367
1454
  if (!threadId) {
368
- throw new import_errors.PersistenceError(
1455
+ throw new import_errors6.PersistenceError(
369
1456
  'Failed to put checkpoint: missing "thread_id" in config.configurable.'
370
1457
  );
371
1458
  }
372
- const prepared = (0, import_langgraph_checkpoint2.copyCheckpoint)(checkpoint);
1459
+ const prepared = (0, import_langgraph_checkpoint3.copyCheckpoint)(checkpoint);
373
1460
  const [[, chkBytes], [, metaBytes]] = await Promise.all([
374
1461
  serde.dumpsTyped(prepared),
375
1462
  serde.dumpsTyped(metadata)
@@ -396,12 +1483,12 @@ function createRedisCheckpointSaver(options) {
396
1483
  const checkpointNs = config.configurable?.checkpoint_ns;
397
1484
  const checkpointId = config.configurable?.checkpoint_id;
398
1485
  if (!threadId) {
399
- throw new import_errors.PersistenceError(
1486
+ throw new import_errors6.PersistenceError(
400
1487
  'Failed to put writes: missing "thread_id" in config.configurable.'
401
1488
  );
402
1489
  }
403
1490
  if (!checkpointId) {
404
- throw new import_errors.PersistenceError(
1491
+ throw new import_errors6.PersistenceError(
405
1492
  'Failed to put writes: missing "checkpoint_id" in config.configurable.'
406
1493
  );
407
1494
  }
@@ -410,7 +1497,7 @@ function createRedisCheckpointSaver(options) {
410
1497
  await Promise.all(
411
1498
  writes.map(async ([channel, value], idx) => {
412
1499
  const [, vBytes] = await serde.dumpsTyped(value);
413
- const mappedIdx = import_langgraph_checkpoint2.WRITES_IDX_MAP[channel] ?? idx;
1500
+ const mappedIdx = import_langgraph_checkpoint3.WRITES_IDX_MAP[channel] ?? idx;
414
1501
  const innerKeyStr = `${taskId},${mappedIdx}`;
415
1502
  const payload = {
416
1503
  taskId,
@@ -443,12 +1530,12 @@ function createRedisCheckpointSaver(options) {
443
1530
  }
444
1531
 
445
1532
  // src/framework/redis/redis-store.ts
446
- var import_errors3 = require("@kortyx/core/errors");
1533
+ var import_errors8 = require("@kortyx/core/errors");
447
1534
 
448
1535
  // src/framework/redis/redis-client.ts
449
1536
  var import_node_net = require("net");
450
1537
  var import_node_tls = require("tls");
451
- var import_errors2 = require("@kortyx/core/errors");
1538
+ var import_errors7 = require("@kortyx/core/errors");
452
1539
  var encodeBulk = (s) => {
453
1540
  const b = Buffer.from(s, "utf8");
454
1541
  return Buffer.concat([
@@ -555,15 +1642,20 @@ function createRedisClient(options) {
555
1642
  s.setNoDelay(true);
556
1643
  s.setKeepAlive(true);
557
1644
  s.on("data", (chunk) => {
1645
+ if (socket !== s) return;
558
1646
  buffer = Buffer.concat([buffer, chunk]);
559
1647
  drainReplies();
560
1648
  });
561
1649
  s.on("error", (err) => {
1650
+ if (socket !== s) return;
562
1651
  ready = false;
563
1652
  while (inflight.length) inflight.shift()?.reject(err);
564
1653
  });
565
1654
  s.on("close", () => {
1655
+ if (socket !== s) return;
566
1656
  ready = false;
1657
+ socket = null;
1658
+ buffer = Buffer.alloc(0);
567
1659
  while (inflight.length)
568
1660
  inflight.shift()?.reject(new Error("Redis socket closed"));
569
1661
  });
@@ -586,7 +1678,7 @@ function createRedisClient(options) {
586
1678
  if (password) {
587
1679
  const r = await send("AUTH", [password]);
588
1680
  if (typeof r === "object" && r && r.type === "error") {
589
- throw new import_errors2.PersistenceError(
1681
+ throw new import_errors7.PersistenceError(
590
1682
  `Redis AUTH failed: ${r.message}`
591
1683
  );
592
1684
  }
@@ -594,7 +1686,7 @@ function createRedisClient(options) {
594
1686
  if (db && Number.isFinite(db) && db > 0) {
595
1687
  const r = await send("SELECT", [String(db)]);
596
1688
  if (typeof r === "object" && r && r.type === "error") {
597
- throw new import_errors2.PersistenceError(
1689
+ throw new import_errors7.PersistenceError(
598
1690
  `Redis SELECT failed: ${r.message}`
599
1691
  );
600
1692
  }
@@ -608,6 +1700,14 @@ function createRedisClient(options) {
608
1700
  }
609
1701
  };
610
1702
  return {
1703
+ async close() {
1704
+ socket?.destroy();
1705
+ socket = null;
1706
+ buffer = Buffer.alloc(0);
1707
+ ready = false;
1708
+ while (inflight.length)
1709
+ inflight.shift()?.reject(new Error("Redis socket closed"));
1710
+ },
611
1711
  async command(cmd, args = []) {
612
1712
  await ensureConnected();
613
1713
  return send(cmd, args);
@@ -625,20 +1725,21 @@ function createRedisFrameworkStore(options) {
625
1725
  try {
626
1726
  return await rawClient.command(...args);
627
1727
  } catch (cause) {
628
- throw new import_errors3.PersistenceError("Redis command failed.", cause);
1728
+ throw new import_errors8.PersistenceError("Redis command failed.", cause);
629
1729
  }
630
1730
  }
631
1731
  };
632
1732
  const prefix = options.prefix ?? "kortyx:fw:";
633
1733
  const k = (key) => `${prefix}${key}`;
634
1734
  return {
1735
+ close: async () => rawClient.close?.(),
635
1736
  async list(prefixKey) {
636
1737
  const keys = await this.scanKeys(prefixKey);
637
1738
  const values = await Promise.all(
638
1739
  [...new Set(keys)].map(async (key) => {
639
1740
  const value = await client.command("GET", [key]);
640
1741
  if (isRedisError(value))
641
- throw new import_errors3.PersistenceError(`Redis GET error: ${value.message}`);
1742
+ throw new import_errors8.PersistenceError(`Redis GET error: ${value.message}`);
642
1743
  return value;
643
1744
  })
644
1745
  );
@@ -649,7 +1750,7 @@ function createRedisFrameworkStore(options) {
649
1750
  async take(key) {
650
1751
  const r = await client.command("GETDEL", [k(key)]);
651
1752
  if (isRedisError(r))
652
- throw new import_errors3.PersistenceError(`Redis GETDEL error: ${r.message}`);
1753
+ throw new import_errors8.PersistenceError(`Redis GETDEL error: ${r.message}`);
653
1754
  return typeof r === "string" ? r : null;
654
1755
  },
655
1756
  async get(key) {
@@ -657,7 +1758,7 @@ function createRedisFrameworkStore(options) {
657
1758
  if (r === null) return null;
658
1759
  if (typeof r === "string") return r;
659
1760
  if (isRedisError(r)) {
660
- throw new import_errors3.PersistenceError(`Redis GET error: ${r.message}`);
1761
+ throw new import_errors8.PersistenceError(`Redis GET error: ${r.message}`);
661
1762
  }
662
1763
  return null;
663
1764
  },
@@ -665,32 +1766,32 @@ function createRedisFrameworkStore(options) {
665
1766
  const args = [k(key), value, "PX", String(Math.max(1, ttlMs))];
666
1767
  const r = await client.command("SET", args);
667
1768
  if (isRedisError(r)) {
668
- throw new import_errors3.PersistenceError(`Redis SET error: ${r.message}`);
1769
+ throw new import_errors8.PersistenceError(`Redis SET error: ${r.message}`);
669
1770
  }
670
1771
  },
671
1772
  async del(key) {
672
1773
  const r = await client.command("DEL", [k(key)]);
673
1774
  if (isRedisError(r)) {
674
- throw new import_errors3.PersistenceError(`Redis DEL error: ${r.message}`);
1775
+ throw new import_errors8.PersistenceError(`Redis DEL error: ${r.message}`);
675
1776
  }
676
1777
  },
677
1778
  async hset(key, field, value) {
678
1779
  const r = await client.command("HSET", [k(key), field, value]);
679
1780
  if (isRedisError(r)) {
680
- throw new import_errors3.PersistenceError(`Redis HSET error: ${r.message}`);
1781
+ throw new import_errors8.PersistenceError(`Redis HSET error: ${r.message}`);
681
1782
  }
682
1783
  },
683
1784
  async hsetnx(key, field, value) {
684
1785
  const r = await client.command("HSETNX", [k(key), field, value]);
685
1786
  if (isRedisError(r)) {
686
- throw new import_errors3.PersistenceError(`Redis HSETNX error: ${r.message}`);
1787
+ throw new import_errors8.PersistenceError(`Redis HSETNX error: ${r.message}`);
687
1788
  }
688
1789
  return typeof r === "number" ? r : 0;
689
1790
  },
690
1791
  async hgetall(key) {
691
1792
  const r = await client.command("HGETALL", [k(key)]);
692
1793
  if (isRedisError(r)) {
693
- throw new import_errors3.PersistenceError(`Redis HGETALL error: ${r.message}`);
1794
+ throw new import_errors8.PersistenceError(`Redis HGETALL error: ${r.message}`);
694
1795
  }
695
1796
  if (!Array.isArray(r)) return {};
696
1797
  const out = {};
@@ -707,7 +1808,7 @@ function createRedisFrameworkStore(options) {
707
1808
  String(Math.max(1, ttlMs))
708
1809
  ]);
709
1810
  if (isRedisError(r)) {
710
- throw new import_errors3.PersistenceError(`Redis PEXPIRE error: ${r.message}`);
1811
+ throw new import_errors8.PersistenceError(`Redis PEXPIRE error: ${r.message}`);
711
1812
  }
712
1813
  },
713
1814
  async scanKeys(prefixKey) {
@@ -723,7 +1824,7 @@ function createRedisFrameworkStore(options) {
723
1824
  "200"
724
1825
  ]);
725
1826
  if (isRedisError(r))
726
- throw new import_errors3.PersistenceError(`Redis SCAN error: ${r.message}`);
1827
+ throw new import_errors8.PersistenceError(`Redis SCAN error: ${r.message}`);
727
1828
  if (!Array.isArray(r) || r.length < 2) break;
728
1829
  cursor = typeof r[0] === "string" ? r[0] : "0";
729
1830
  const batch = r[1];
@@ -739,17 +1840,17 @@ function createRedisFrameworkStore(options) {
739
1840
  if (keys.length === 0) return;
740
1841
  const r = await client.command("DEL", keys);
741
1842
  if (isRedisError(r)) {
742
- throw new import_errors3.PersistenceError(`Redis DEL error: ${r.message}`);
1843
+ throw new import_errors8.PersistenceError(`Redis DEL error: ${r.message}`);
743
1844
  }
744
1845
  }
745
1846
  };
746
1847
  }
747
1848
 
748
1849
  // src/framework/redis/session-checkpoint-store.ts
749
- var import_node_crypto = require("crypto");
750
- var import_errors4 = require("@kortyx/core/errors");
1850
+ var import_node_crypto5 = require("crypto");
1851
+ var import_errors9 = require("@kortyx/core/errors");
751
1852
  var DEFAULT_MAX_CHECKPOINTS_PER_SESSION = 50;
752
- var clone = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
1853
+ var clone2 = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
753
1854
  var uniqueStrings = (values) => Array.from(
754
1855
  new Set(
755
1856
  Array.from(values).filter(
@@ -758,22 +1859,22 @@ var uniqueStrings = (values) => Array.from(
758
1859
  )
759
1860
  );
760
1861
  var createId = (prefix) => {
761
- return `${prefix}-${(0, import_node_crypto.randomUUID)()}`;
1862
+ return `${prefix}-${(0, import_node_crypto5.randomUUID)()}`;
762
1863
  };
763
- var createResumeToken = () => (0, import_node_crypto.randomUUID)();
764
- var toSummary = (record) => ({
765
- id: record.id,
766
- sessionId: record.sessionId,
767
- turnIndex: record.turnIndex,
768
- createdAt: record.createdAt,
769
- nodes: [...record.nodes],
770
- workflow: record.workflow,
771
- ...record.label ? { label: record.label } : {},
772
- ...record.parentCheckpointId ? { parentCheckpointId: record.parentCheckpointId } : {},
773
- ...record.parentSessionId ? { parentSessionId: record.parentSessionId } : {},
774
- ...record.forkedFrom ? { forkedFrom: record.forkedFrom } : {},
775
- ...record.workflowVersion ? { workflowVersion: record.workflowVersion } : {},
776
- ...record.buildId ? { buildId: record.buildId } : {}
1864
+ var createResumeToken = () => (0, import_node_crypto5.randomUUID)();
1865
+ var toSummary = (record2) => ({
1866
+ id: record2.id,
1867
+ sessionId: record2.sessionId,
1868
+ turnIndex: record2.turnIndex,
1869
+ createdAt: record2.createdAt,
1870
+ nodes: [...record2.nodes],
1871
+ workflow: record2.workflow,
1872
+ ...record2.label ? { label: record2.label } : {},
1873
+ ...record2.parentCheckpointId ? { parentCheckpointId: record2.parentCheckpointId } : {},
1874
+ ...record2.parentSessionId ? { parentSessionId: record2.parentSessionId } : {},
1875
+ ...record2.forkedFrom ? { forkedFrom: record2.forkedFrom } : {},
1876
+ ...record2.workflowVersion ? { workflowVersion: record2.workflowVersion } : {},
1877
+ ...record2.buildId ? { buildId: record2.buildId } : {}
777
1878
  });
778
1879
  function createRedisSessionCheckpointStore(options) {
779
1880
  const store = options.store;
@@ -784,22 +1885,22 @@ function createRedisSessionCheckpointStore(options) {
784
1885
  const sessionRecordKey = (sessionId, id) => `${prefix}session:${sessionId}:${id}`;
785
1886
  const sessionPrefix = (sessionId) => `${prefix}session:${sessionId}:`;
786
1887
  const headKey = (sessionId) => `${prefix}head:${sessionId}`;
787
- const saveRecord = async (record) => {
788
- const serialized = JSON.stringify(record);
1888
+ const saveRecord = async (record2) => {
1889
+ const serialized = JSON.stringify(record2);
789
1890
  await Promise.all([
790
- store.set(byIdKey(record.id), serialized, ttlMs),
1891
+ store.set(byIdKey(record2.id), serialized, ttlMs),
791
1892
  store.set(
792
- sessionRecordKey(record.sessionId, record.id),
1893
+ sessionRecordKey(record2.sessionId, record2.id),
793
1894
  serialized,
794
1895
  ttlMs
795
1896
  ),
796
- store.set(headKey(record.sessionId), record.id, ttlMs)
1897
+ store.set(headKey(record2.sessionId), record2.id, ttlMs)
797
1898
  ]);
798
1899
  };
799
- const deleteRecord = async (record) => {
1900
+ const deleteRecord = async (record2) => {
800
1901
  await Promise.all([
801
- store.del(byIdKey(record.id)),
802
- store.del(sessionRecordKey(record.sessionId, record.id))
1902
+ store.del(byIdKey(record2.id)),
1903
+ store.del(sessionRecordKey(record2.sessionId, record2.id))
803
1904
  ]);
804
1905
  };
805
1906
  const getRecord = async (id) => {
@@ -815,7 +1916,7 @@ function createRedisSessionCheckpointStore(options) {
815
1916
  return raw ? JSON.parse(raw) : null;
816
1917
  })
817
1918
  );
818
- return records.filter((record) => Boolean(record)).sort((a, b) => a.turnIndex - b.turnIndex || a.createdAt - b.createdAt);
1919
+ return records.filter((record2) => Boolean(record2)).sort((a, b) => a.turnIndex - b.turnIndex || a.createdAt - b.createdAt);
819
1920
  };
820
1921
  const prune = async (sessionId) => {
821
1922
  const records = await sortedRecords(sessionId);
@@ -825,22 +1926,22 @@ function createRedisSessionCheckpointStore(options) {
825
1926
  const getHead = async (sessionId) => {
826
1927
  const headId = await store.get(headKey(sessionId));
827
1928
  if (!headId) return null;
828
- const record = await getRecord(headId);
829
- return record ? clone(record) : null;
1929
+ const record2 = await getRecord(headId);
1930
+ return record2 ? clone2(record2) : null;
830
1931
  };
831
1932
  return {
832
1933
  async list(sessionId) {
833
1934
  return (await sortedRecords(sessionId)).map(toSummary);
834
1935
  },
835
1936
  async get(id) {
836
- const record = await getRecord(id);
837
- return record ? clone(record) : null;
1937
+ const record2 = await getRecord(id);
1938
+ return record2 ? clone2(record2) : null;
838
1939
  },
839
1940
  getHead,
840
1941
  async append(args) {
841
1942
  const previousHead = await getHead(args.sessionId);
842
1943
  const pendingRequests = args.pendingRequests ?? [];
843
- const record = {
1944
+ const record2 = {
844
1945
  id: createId("cp"),
845
1946
  sessionId: args.sessionId,
846
1947
  runId: args.runId,
@@ -848,7 +1949,7 @@ function createRedisSessionCheckpointStore(options) {
848
1949
  turnIndex: previousHead ? previousHead.turnIndex + 1 : 0,
849
1950
  createdAt: Date.now(),
850
1951
  workflow: args.workflow,
851
- state: clone(args.state),
1952
+ state: clone2(args.state),
852
1953
  nodes: uniqueStrings(args.nodes ?? []),
853
1954
  effects: {
854
1955
  structuredStreamIds: uniqueStrings(args.structuredStreamIds ?? []),
@@ -856,35 +1957,35 @@ function createRedisSessionCheckpointStore(options) {
856
1957
  pendingRequests.map((request) => request.token)
857
1958
  )
858
1959
  },
859
- activePendingRequests: clone(pendingRequests),
1960
+ activePendingRequests: clone2(pendingRequests),
860
1961
  ...previousHead ? { parentCheckpointId: previousHead.id } : {},
861
1962
  ...args.label ? { label: args.label } : {},
862
1963
  ...args.workflowVersion ? { workflowVersion: args.workflowVersion } : {},
863
1964
  ...args.buildId ? { buildId: args.buildId } : {}
864
1965
  };
865
- await saveRecord(record);
1966
+ await saveRecord(record2);
866
1967
  await prune(args.sessionId);
867
- return clone(record);
1968
+ return clone2(record2);
868
1969
  },
869
1970
  async rollbackTo(id) {
870
1971
  const target = await getRecord(id);
871
1972
  if (!target)
872
- throw new import_errors4.KortyxError("NOT_FOUND", `Checkpoint "${id}" not found.`, {
1973
+ throw new import_errors9.KortyxError("NOT_FOUND", `Checkpoint "${id}" not found.`, {
873
1974
  category: "request",
874
1975
  retryable: false,
875
1976
  safeMessage: "The requested checkpoint was not found."
876
1977
  });
877
1978
  const records = await sortedRecords(target.sessionId);
878
1979
  const trailing = records.filter(
879
- (record) => record.turnIndex > target.turnIndex
1980
+ (record2) => record2.turnIndex > target.turnIndex
880
1981
  );
881
1982
  const invalidatedStructuredStreamIds = uniqueStrings(
882
- trailing.flatMap((record) => record.effects.structuredStreamIds)
1983
+ trailing.flatMap((record2) => record2.effects.structuredStreamIds)
883
1984
  );
884
1985
  const invalidatedInterruptTokens = uniqueStrings(
885
- trailing.flatMap((record) => [
886
- ...record.effects.interruptTokens,
887
- ...record.activePendingRequests.map((request) => request.token)
1986
+ trailing.flatMap((record2) => [
1987
+ ...record2.effects.interruptTokens,
1988
+ ...record2.activePendingRequests.map((request) => request.token)
888
1989
  ])
889
1990
  );
890
1991
  await Promise.all(trailing.map(deleteRecord));
@@ -894,22 +1995,22 @@ function createRedisSessionCheckpointStore(options) {
894
1995
  head: target.id,
895
1996
  invalidatedStructuredStreamIds,
896
1997
  invalidatedInterruptTokens,
897
- activePendingRequests: clone(target.activePendingRequests)
1998
+ activePendingRequests: clone2(target.activePendingRequests)
898
1999
  };
899
2000
  },
900
2001
  async fork(id, options2) {
901
2002
  const source = await getRecord(id);
902
2003
  if (!source)
903
- throw new import_errors4.KortyxError("NOT_FOUND", `Checkpoint "${id}" not found.`, {
2004
+ throw new import_errors9.KortyxError("NOT_FOUND", `Checkpoint "${id}" not found.`, {
904
2005
  category: "request",
905
2006
  retryable: false,
906
2007
  safeMessage: "The requested checkpoint was not found."
907
2008
  });
908
2009
  const sessionId = options2?.newSessionId || createId("session");
909
- const { parentCheckpointId: _parentCheckpointId, ...sourceRecord } = clone(source);
2010
+ const { parentCheckpointId: _parentCheckpointId, ...sourceRecord } = clone2(source);
910
2011
  const runId = source.activePendingRequests.some(
911
2012
  (request) => request.graphSnapshot
912
- ) ? `run-${(0, import_node_crypto.randomUUID)()}` : source.runId;
2013
+ ) ? `run-${(0, import_node_crypto5.randomUUID)()}` : source.runId;
913
2014
  const forked = {
914
2015
  ...sourceRecord,
915
2016
  runId,
@@ -917,7 +2018,7 @@ function createRedisSessionCheckpointStore(options) {
917
2018
  sessionId,
918
2019
  parentSessionId: source.sessionId,
919
2020
  forkedFrom: source.id,
920
- activePendingRequests: clone(source.activePendingRequests).map(
2021
+ activePendingRequests: clone2(source.activePendingRequests).map(
921
2022
  (request) => ({
922
2023
  ...request,
923
2024
  runId,
@@ -932,34 +2033,34 @@ function createRedisSessionCheckpointStore(options) {
932
2033
  sessionId,
933
2034
  parentSessionId: source.sessionId,
934
2035
  forkedFrom: source.id,
935
- checkpoint: clone(forked)
2036
+ checkpoint: clone2(forked)
936
2037
  };
937
2038
  }
938
2039
  };
939
2040
  }
940
2041
 
941
2042
  // src/framework/session-checkpoints.ts
942
- var import_node_crypto2 = require("crypto");
943
- var import_errors5 = require("@kortyx/core/errors");
2043
+ var import_node_crypto6 = require("crypto");
2044
+ var import_errors10 = require("@kortyx/core/errors");
944
2045
  var DEFAULT_MAX_CHECKPOINTS_PER_SESSION2 = 50;
945
- var createCheckpointId = () => `cp-${(0, import_node_crypto2.randomUUID)()}`;
946
- var createSessionId = () => `session-${(0, import_node_crypto2.randomUUID)()}`;
947
- var createResumeToken2 = () => (0, import_node_crypto2.randomUUID)();
948
- var createRequestId = () => `human-${(0, import_node_crypto2.randomUUID)()}`;
949
- var clone2 = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
950
- var toSummary2 = (record) => ({
951
- id: record.id,
952
- sessionId: record.sessionId,
953
- turnIndex: record.turnIndex,
954
- createdAt: record.createdAt,
955
- nodes: [...record.nodes],
956
- workflow: record.workflow,
957
- ...record.label ? { label: record.label } : {},
958
- ...record.parentCheckpointId ? { parentCheckpointId: record.parentCheckpointId } : {},
959
- ...record.parentSessionId ? { parentSessionId: record.parentSessionId } : {},
960
- ...record.forkedFrom ? { forkedFrom: record.forkedFrom } : {},
961
- ...record.workflowVersion ? { workflowVersion: record.workflowVersion } : {},
962
- ...record.buildId ? { buildId: record.buildId } : {}
2046
+ var createCheckpointId = () => `cp-${(0, import_node_crypto6.randomUUID)()}`;
2047
+ var createSessionId = () => `session-${(0, import_node_crypto6.randomUUID)()}`;
2048
+ var createResumeToken2 = () => (0, import_node_crypto6.randomUUID)();
2049
+ var createRequestId = () => `human-${(0, import_node_crypto6.randomUUID)()}`;
2050
+ var clone3 = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
2051
+ var toSummary2 = (record2) => ({
2052
+ id: record2.id,
2053
+ sessionId: record2.sessionId,
2054
+ turnIndex: record2.turnIndex,
2055
+ createdAt: record2.createdAt,
2056
+ nodes: [...record2.nodes],
2057
+ workflow: record2.workflow,
2058
+ ...record2.label ? { label: record2.label } : {},
2059
+ ...record2.parentCheckpointId ? { parentCheckpointId: record2.parentCheckpointId } : {},
2060
+ ...record2.parentSessionId ? { parentSessionId: record2.parentSessionId } : {},
2061
+ ...record2.forkedFrom ? { forkedFrom: record2.forkedFrom } : {},
2062
+ ...record2.workflowVersion ? { workflowVersion: record2.workflowVersion } : {},
2063
+ ...record2.buildId ? { buildId: record2.buildId } : {}
963
2064
  });
964
2065
  var uniqueStrings2 = (values) => Array.from(
965
2066
  new Set(
@@ -973,49 +2074,49 @@ function createInMemorySessionCheckpointStore(options) {
973
2074
  const byId = /* @__PURE__ */ new Map();
974
2075
  const bySession = /* @__PURE__ */ new Map();
975
2076
  const headBySession = /* @__PURE__ */ new Map();
976
- const sortedRecords = (sessionId) => (bySession.get(sessionId) ?? []).map((id) => byId.get(id)).filter((record) => Boolean(record)).sort((a, b) => a.turnIndex - b.turnIndex || a.createdAt - b.createdAt);
977
- const saveRecord = (record) => {
978
- byId.set(record.id, clone2(record));
979
- const ids = bySession.get(record.sessionId) ?? [];
980
- if (!ids.includes(record.id)) ids.push(record.id);
981
- bySession.set(record.sessionId, ids);
982
- headBySession.set(record.sessionId, record.id);
2077
+ const sortedRecords = (sessionId) => (bySession.get(sessionId) ?? []).map((id) => byId.get(id)).filter((record2) => Boolean(record2)).sort((a, b) => a.turnIndex - b.turnIndex || a.createdAt - b.createdAt);
2078
+ const saveRecord = (record2) => {
2079
+ byId.set(record2.id, clone3(record2));
2080
+ const ids = bySession.get(record2.sessionId) ?? [];
2081
+ if (!ids.includes(record2.id)) ids.push(record2.id);
2082
+ bySession.set(record2.sessionId, ids);
2083
+ headBySession.set(record2.sessionId, record2.id);
983
2084
  };
984
- const deleteRecord = (record) => {
985
- byId.delete(record.id);
2085
+ const deleteRecord = (record2) => {
2086
+ byId.delete(record2.id);
986
2087
  bySession.set(
987
- record.sessionId,
2088
+ record2.sessionId,
988
2089
  /* v8 ignore next -- deleteRecord only receives records that came from this session index. */
989
- (bySession.get(record.sessionId) ?? []).filter((id) => id !== record.id)
2090
+ (bySession.get(record2.sessionId) ?? []).filter((id) => id !== record2.id)
990
2091
  );
991
2092
  };
992
2093
  const prune = (sessionId) => {
993
2094
  const records = sortedRecords(sessionId);
994
2095
  const overflow = Math.max(0, records.length - maxCheckpointsPerSession);
995
- for (const record of records.slice(0, overflow)) {
996
- deleteRecord(record);
2096
+ for (const record2 of records.slice(0, overflow)) {
2097
+ deleteRecord(record2);
997
2098
  }
998
2099
  };
999
2100
  const getHead = async (sessionId) => {
1000
2101
  const headId = headBySession.get(sessionId);
1001
2102
  if (!headId) return null;
1002
- const record = byId.get(headId);
1003
- return record ? clone2(record) : null;
2103
+ const record2 = byId.get(headId);
2104
+ return record2 ? clone3(record2) : null;
1004
2105
  };
1005
2106
  return {
1006
2107
  async list(sessionId) {
1007
2108
  return sortedRecords(sessionId).map(toSummary2);
1008
2109
  },
1009
2110
  async get(id) {
1010
- const record = byId.get(id);
1011
- return record ? clone2(record) : null;
2111
+ const record2 = byId.get(id);
2112
+ return record2 ? clone3(record2) : null;
1012
2113
  },
1013
2114
  getHead,
1014
2115
  async append(args) {
1015
2116
  const previousHead = await getHead(args.sessionId);
1016
2117
  const turnIndex = previousHead ? previousHead.turnIndex + 1 : 0;
1017
2118
  const pendingRequests = args.pendingRequests ?? [];
1018
- const record = {
2119
+ const record2 = {
1019
2120
  id: createCheckpointId(),
1020
2121
  sessionId: args.sessionId,
1021
2122
  runId: args.runId,
@@ -1023,7 +2124,7 @@ function createInMemorySessionCheckpointStore(options) {
1023
2124
  turnIndex,
1024
2125
  createdAt: Date.now(),
1025
2126
  workflow: args.workflow,
1026
- state: clone2(args.state),
2127
+ state: clone3(args.state),
1027
2128
  nodes: uniqueStrings2(args.nodes ?? []),
1028
2129
  effects: {
1029
2130
  structuredStreamIds: uniqueStrings2(args.structuredStreamIds ?? []),
@@ -1031,20 +2132,20 @@ function createInMemorySessionCheckpointStore(options) {
1031
2132
  pendingRequests.map((request) => request.token)
1032
2133
  )
1033
2134
  },
1034
- activePendingRequests: clone2(pendingRequests),
2135
+ activePendingRequests: clone3(pendingRequests),
1035
2136
  ...previousHead ? { parentCheckpointId: previousHead.id } : {},
1036
2137
  ...args.label ? { label: args.label } : {},
1037
2138
  ...args.workflowVersion ? { workflowVersion: args.workflowVersion } : {},
1038
2139
  ...args.buildId ? { buildId: args.buildId } : {}
1039
2140
  };
1040
- saveRecord(record);
2141
+ saveRecord(record2);
1041
2142
  prune(args.sessionId);
1042
- return clone2(record);
2143
+ return clone3(record2);
1043
2144
  },
1044
2145
  async rollbackTo(id) {
1045
2146
  const target = byId.get(id);
1046
2147
  if (!target) {
1047
- throw new import_errors5.KortyxError("NOT_FOUND", `Checkpoint "${id}" not found.`, {
2148
+ throw new import_errors10.KortyxError("NOT_FOUND", `Checkpoint "${id}" not found.`, {
1048
2149
  category: "request",
1049
2150
  retryable: false,
1050
2151
  safeMessage: "The requested checkpoint was not found."
@@ -1052,41 +2153,41 @@ function createInMemorySessionCheckpointStore(options) {
1052
2153
  }
1053
2154
  const records = sortedRecords(target.sessionId);
1054
2155
  const trailing = records.filter(
1055
- (record) => record.turnIndex > target.turnIndex
2156
+ (record2) => record2.turnIndex > target.turnIndex
1056
2157
  );
1057
2158
  const invalidatedStructuredStreamIds = uniqueStrings2(
1058
- trailing.flatMap((record) => record.effects.structuredStreamIds)
2159
+ trailing.flatMap((record2) => record2.effects.structuredStreamIds)
1059
2160
  );
1060
2161
  const invalidatedInterruptTokens = uniqueStrings2(
1061
- trailing.flatMap((record) => [
1062
- ...record.effects.interruptTokens,
1063
- ...record.activePendingRequests.map((request) => request.token)
2162
+ trailing.flatMap((record2) => [
2163
+ ...record2.effects.interruptTokens,
2164
+ ...record2.activePendingRequests.map((request) => request.token)
1064
2165
  ])
1065
2166
  );
1066
- for (const record of trailing) deleteRecord(record);
2167
+ for (const record2 of trailing) deleteRecord(record2);
1067
2168
  headBySession.set(target.sessionId, target.id);
1068
2169
  return {
1069
2170
  sessionId: target.sessionId,
1070
2171
  head: target.id,
1071
2172
  invalidatedStructuredStreamIds,
1072
2173
  invalidatedInterruptTokens,
1073
- activePendingRequests: clone2(target.activePendingRequests)
2174
+ activePendingRequests: clone3(target.activePendingRequests)
1074
2175
  };
1075
2176
  },
1076
2177
  async fork(id, options2) {
1077
2178
  const source = byId.get(id);
1078
2179
  if (!source) {
1079
- throw new import_errors5.KortyxError("NOT_FOUND", `Checkpoint "${id}" not found.`, {
2180
+ throw new import_errors10.KortyxError("NOT_FOUND", `Checkpoint "${id}" not found.`, {
1080
2181
  category: "request",
1081
2182
  retryable: false,
1082
2183
  safeMessage: "The requested checkpoint was not found."
1083
2184
  });
1084
2185
  }
1085
2186
  const sessionId = options2?.newSessionId || createSessionId();
1086
- const { parentCheckpointId: _parentCheckpointId, ...sourceRecord } = clone2(source);
2187
+ const { parentCheckpointId: _parentCheckpointId, ...sourceRecord } = clone3(source);
1087
2188
  const runId = source.activePendingRequests.some(
1088
2189
  (request) => request.graphSnapshot
1089
- ) ? `run-${(0, import_node_crypto2.randomUUID)()}` : source.runId;
2190
+ ) ? `run-${(0, import_node_crypto6.randomUUID)()}` : source.runId;
1090
2191
  const forked = {
1091
2192
  ...sourceRecord,
1092
2193
  runId,
@@ -1094,7 +2195,7 @@ function createInMemorySessionCheckpointStore(options) {
1094
2195
  sessionId,
1095
2196
  parentSessionId: source.sessionId,
1096
2197
  forkedFrom: source.id,
1097
- activePendingRequests: clone2(source.activePendingRequests).map(
2198
+ activePendingRequests: clone3(source.activePendingRequests).map(
1098
2199
  (request) => ({
1099
2200
  ...request,
1100
2201
  runId,
@@ -1109,7 +2210,7 @@ function createInMemorySessionCheckpointStore(options) {
1109
2210
  sessionId,
1110
2211
  parentSessionId: source.sessionId,
1111
2212
  forkedFrom: source.id,
1112
- checkpoint: clone2(forked)
2213
+ checkpoint: clone3(forked)
1113
2214
  };
1114
2215
  }
1115
2216
  };
@@ -1119,10 +2220,27 @@ function createInMemorySessionCheckpointStore(options) {
1119
2220
  function createInMemoryFrameworkAdapter(options) {
1120
2221
  const ttlMs = options?.ttlMs ?? 15 * 60 * 1e3;
1121
2222
  const checkpointer = createInMemoryCheckpointSaver();
2223
+ const pendingRequests = createInMemoryPendingRequestStore();
1122
2224
  return {
1123
2225
  kind: "in-memory",
1124
2226
  ttlMs,
1125
- pendingRequests: createInMemoryPendingRequestStore(),
2227
+ pendingRequests,
2228
+ maintenance: {
2229
+ setup: async () => {
2230
+ },
2231
+ prune: async (options2) => {
2232
+ const { now, batchSize } = resolvePruneOptions(options2);
2233
+ const result = emptyPruneResult();
2234
+ result.deleted.pendingRequests = pendingRequests.pruneExpired(
2235
+ now,
2236
+ batchSize
2237
+ );
2238
+ result.hasMore = result.deleted.pendingRequests === batchSize;
2239
+ return result;
2240
+ }
2241
+ },
2242
+ close: async () => {
2243
+ },
1126
2244
  sessionCheckpoints: createInMemorySessionCheckpointStore({
1127
2245
  ...options?.maxSessionCheckpoints !== void 0 ? { maxCheckpointsPerSession: options.maxSessionCheckpoints } : {}
1128
2246
  }),
@@ -1142,9 +2260,20 @@ function createRedisFrameworkAdapter(options) {
1142
2260
  prefix: options.prefix ?? "kortyx:fw:"
1143
2261
  });
1144
2262
  const cpPrefix = "kortyx:cp:";
1145
- return {
2263
+ const adapter = {
1146
2264
  kind: "redis",
1147
2265
  ttlMs,
2266
+ maintenance: {
2267
+ setup: async () => {
2268
+ },
2269
+ prune: async (options2) => {
2270
+ resolvePruneOptions(options2);
2271
+ return emptyPruneResult();
2272
+ }
2273
+ },
2274
+ close: async () => {
2275
+ await store.close?.();
2276
+ },
1148
2277
  pendingRequests: createRedisPendingRequestStore({
1149
2278
  store,
1150
2279
  prefix: "kortyx:pending:"
@@ -1177,54 +2306,48 @@ function createRedisFrameworkAdapter(options) {
1177
2306
  );
1178
2307
  }
1179
2308
  };
2309
+ registerFrameworkCache(
2310
+ adapter,
2311
+ {
2312
+ get: (key) => store.get(`kortyx:runtime-cache:${key}`),
2313
+ set: (key, value, ttl) => store.set(`kortyx:runtime-cache:${key}`, value, ttl),
2314
+ close: () => adapter.close()
2315
+ },
2316
+ ttlMs
2317
+ );
2318
+ return adapter;
1180
2319
  }
1181
2320
  function createFrameworkAdapterFromEnv(env = process.env) {
1182
2321
  const url = env.KORTYX_REDIS_URL || env.REDIS_URL || env.KORTYX_FRAMEWORK_REDIS_URL || "";
1183
2322
  const ttlMsRaw = env.KORTYX_FRAMEWORK_TTL_MS || env.KORTYX_TTL_MS || "";
1184
2323
  const ttlMs = ttlMsRaw ? Number(ttlMsRaw) : void 0;
2324
+ if (env.KORTYX_POSTGRES_URL) {
2325
+ const storage = createPostgresFrameworkAdapter({
2326
+ connectionString: env.KORTYX_POSTGRES_URL,
2327
+ ...ttlMs !== void 0 ? { ttlMs } : {}
2328
+ });
2329
+ return url ? createCachingFrameworkAdapter({
2330
+ storage,
2331
+ cache: createRedisFrameworkAdapter({ url })
2332
+ }) : storage;
2333
+ }
1185
2334
  if (url)
1186
- return createRedisFrameworkAdapter({ url, ...ttlMs ? { ttlMs } : {} });
1187
- return createInMemoryFrameworkAdapter({ ...ttlMs ? { ttlMs } : {} });
1188
- }
1189
-
1190
- // src/framework/graph-snapshot.ts
1191
- async function captureGraphSnapshot(saver, threadId, checkpointId) {
1192
- const tuple = await saver.getTuple({
1193
- configurable: {
1194
- thread_id: threadId,
1195
- checkpoint_ns: "",
1196
- ...checkpointId ? { checkpoint_id: checkpointId } : {}
1197
- }
2335
+ return createRedisFrameworkAdapter({
2336
+ url,
2337
+ ...ttlMs ? { ttlMs } : {}
2338
+ });
2339
+ return createInMemoryFrameworkAdapter({
2340
+ ...ttlMs ? { ttlMs } : {}
1198
2341
  });
1199
- return tuple ? JSON.parse(JSON.stringify(tuple)) : void 0;
1200
- }
1201
- async function restoreGraphSnapshot(saver, snapshot, threadId) {
1202
- const copy = JSON.parse(JSON.stringify(snapshot));
1203
- const config = await saver.put(
1204
- { configurable: { thread_id: threadId, checkpoint_ns: "" } },
1205
- copy.checkpoint,
1206
- copy.metadata,
1207
- copy.checkpoint.channel_versions
1208
- );
1209
- const reset = saver;
1210
- if (reset.deleteCheckpointWrites)
1211
- await reset.deleteCheckpointWrites(threadId, "", copy.checkpoint.id);
1212
- const writes = /* @__PURE__ */ new Map();
1213
- for (const [task, channel, value] of copy.pendingWrites ?? []) {
1214
- const group = writes.get(task) ?? [];
1215
- group.push([channel, value]);
1216
- writes.set(task, group);
1217
- }
1218
- for (const [task, group] of writes)
1219
- await saver.putWrites(config, group, task);
1220
2342
  }
1221
2343
 
1222
2344
  // src/graph/create-execution-graph.ts
1223
2345
  var import_promises = require("timers/promises");
1224
- var import_core2 = require("@kortyx/core");
2346
+ var import_core3 = require("@kortyx/core");
2347
+ var import_errors13 = require("@kortyx/core/errors");
1225
2348
  var import_hooks2 = require("@kortyx/hooks");
1226
- var import_utils = require("@kortyx/utils");
1227
- var import_langgraph2 = require("@langchain/langgraph");
2349
+ var import_utils2 = require("@kortyx/utils");
2350
+ var import_langgraph3 = require("@langchain/langgraph");
1228
2351
 
1229
2352
  // src/node-loader.ts
1230
2353
  var import_node_path = require("path");
@@ -1308,52 +2431,36 @@ async function resolveNodeHandler(args) {
1308
2431
 
1309
2432
  // src/graph/call-workflow.ts
1310
2433
  var import_core = require("@kortyx/core");
1311
- var import_errors6 = require("@kortyx/core/errors");
2434
+ var import_errors11 = require("@kortyx/core/errors");
1312
2435
  var import_hooks = require("@kortyx/hooks");
1313
2436
  var import_singletons = require("@langchain/core/singletons");
1314
2437
  var import_langgraph = require("@langchain/langgraph");
1315
- function assertSequentialWorkflow(workflow) {
1316
- const groups = /* @__PURE__ */ new Map();
1317
- for (const [from, , condition] of workflow.edges) {
1318
- const group = groups.get(from) ?? { plain: 0, conditional: 0 };
1319
- if (condition) group.conditional++;
1320
- else group.plain++;
1321
- groups.set(from, group);
1322
- if (group.plain > 1 || group.plain > 0 && group.conditional > 0) {
1323
- throw new import_errors6.WorkflowContractError(
1324
- `Workflow '${workflow.id}' has parallel edges; child calls require sequential graphs.`
1325
- );
1326
- }
1327
- }
1328
- }
1329
- function createWorkflowCallService(config, parent, execution = {}) {
2438
+ function createWorkflowCallService(config, _parent, execution = {}) {
1330
2439
  return async (args) => {
1331
2440
  const execute = async () => {
1332
2441
  (0, import_core.throwIfExecutionAborted)(execution.abortSignal);
1333
- if (parent) assertSequentialWorkflow(parent);
1334
2442
  const depth = (config.workflowCallDepth ?? 0) + 1;
1335
2443
  if (depth > 16)
1336
- throw new import_errors6.WorkflowContractError(
2444
+ throw new import_errors11.WorkflowContractError(
1337
2445
  "Maximum child workflow depth (16) exceeded."
1338
2446
  );
1339
2447
  const workflow = await config.selectWorkflow?.(args.workflow);
1340
2448
  if (!workflow || workflow.id !== args.workflow)
1341
- throw new import_errors6.WorkflowContractError(
2449
+ throw new import_errors11.WorkflowContractError(
1342
2450
  `Workflow '${args.workflow}' is not registered.`
1343
2451
  );
1344
2452
  if (!workflow.inputSchema || !workflow.outputSchema)
1345
- throw new import_errors6.WorkflowContractError(
2453
+ throw new import_errors11.WorkflowContractError(
1346
2454
  `Callable workflow '${workflow.id}' requires inputSchema and outputSchema.`
1347
2455
  );
1348
2456
  if (args.definition && (args.definition.version !== workflow.version || args.definition.inputSchema !== workflow.inputSchema || args.definition.outputSchema !== workflow.outputSchema)) {
1349
- throw new import_errors6.WorkflowContractError(
2457
+ throw new import_errors11.WorkflowContractError(
1350
2458
  "The typed workflow contract does not match the registered definition."
1351
2459
  );
1352
2460
  }
1353
- assertSequentialWorkflow(workflow);
1354
2461
  const snapshot = args.snapshot;
1355
2462
  if (snapshot && snapshot.version !== workflow.version)
1356
- throw new import_errors6.WorkflowContractError(
2463
+ throw new import_errors11.WorkflowContractError(
1357
2464
  `Workflow '${workflow.id}' changed since the call was suspended.`
1358
2465
  );
1359
2466
  const input = snapshot ? void 0 : workflow.inputSchema.parse(args.input);
@@ -1363,6 +2470,7 @@ function createWorkflowCallService(config, parent, execution = {}) {
1363
2470
  let request;
1364
2471
  const invocationPath = `${config.invocationPath ?? "root"}/${encodeURIComponent(args.id)}:${args.invocationId}`;
1365
2472
  let childConfig = {
2473
+ resume: Boolean(snapshot),
1366
2474
  executionBranchId: config.executionBranchId,
1367
2475
  prepareChildTelemetry: config.prepareChildTelemetry,
1368
2476
  context: config.context,
@@ -1411,10 +2519,10 @@ function createWorkflowCallService(config, parent, execution = {}) {
1411
2519
  }
1412
2520
  if (event === "error") {
1413
2521
  const failure = payload?.failure;
1414
- throw (0, import_errors6.isFailureDescriptor)(failure) ? (0, import_errors6.errorFromFailure)(failure) : new Error("Child workflow failed.");
2522
+ throw (0, import_errors11.isFailureDescriptor)(failure) ? (0, import_errors11.errorFromFailure)(failure) : new Error("Child workflow failed.");
1415
2523
  }
1416
2524
  if (event === "transition")
1417
- throw new import_errors6.WorkflowContractError(
2525
+ throw new import_errors11.WorkflowContractError(
1418
2526
  "transitionTo is not supported inside child workflows; useWorkflow returns to its caller."
1419
2527
  );
1420
2528
  const value = payload;
@@ -1469,7 +2577,7 @@ function createWorkflowCallService(config, parent, execution = {}) {
1469
2577
  const checkpoint = await captureGraphSnapshot(saver, threadId).catch(
1470
2578
  (cause) => {
1471
2579
  if ((0, import_core.isExecutionLimitReached)(error))
1472
- throw new import_errors6.PersistenceError(
2580
+ throw new import_errors11.PersistenceError(
1473
2581
  "Cannot checkpoint a suspended child workflow.",
1474
2582
  cause
1475
2583
  );
@@ -1506,7 +2614,7 @@ function createWorkflowCallService(config, parent, execution = {}) {
1506
2614
  if (request) {
1507
2615
  const checkpoint = await captureGraphSnapshot(saver, threadId);
1508
2616
  if (!checkpoint)
1509
- throw new import_errors6.WorkflowContractError(
2617
+ throw new import_errors11.WorkflowContractError(
1510
2618
  "Child interrupted without a graph checkpoint."
1511
2619
  );
1512
2620
  return {
@@ -1524,8 +2632,8 @@ function createWorkflowCallService(config, parent, execution = {}) {
1524
2632
  try {
1525
2633
  data = workflow.outputSchema.parse(result.data ?? {});
1526
2634
  } catch (cause) {
1527
- if (!Array.isArray((0, import_errors6.errorProperty)(cause, "issues"))) throw cause;
1528
- throw new import_errors6.ValidationError(
2635
+ if (!Array.isArray((0, import_errors11.errorProperty)(cause, "issues"))) throw cause;
2636
+ throw new import_errors11.ValidationError(
1529
2637
  "INVALID_OUTPUT",
1530
2638
  "Child output validation failed.",
1531
2639
  cause
@@ -1557,34 +2665,504 @@ function createWorkflowCallService(config, parent, execution = {}) {
1557
2665
  };
1558
2666
  }
1559
2667
 
2668
+ // src/graph/parallel-graph.ts
2669
+ var import_core2 = require("@kortyx/core");
2670
+ var import_errors12 = require("@kortyx/core/errors");
2671
+ var import_utils = require("@kortyx/utils");
2672
+ var import_langgraph2 = require("@langchain/langgraph");
2673
+ var WORK = "__kortyx_parallel_work";
2674
+ var WAIT = "__kortyx_parallel_wait";
2675
+ var FINISH = "__kortyx_parallel_finish";
2676
+ var KEY = "__kortyxParallelGraph";
2677
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
2678
+ var clone4 = (value) => JSON.parse(JSON.stringify(value));
2679
+ var zeroUsage = () => ({ input: 0, output: 0, total: 0 });
2680
+ function hasParallelEdges(workflow) {
2681
+ const groups = /* @__PURE__ */ new Map();
2682
+ for (const [from, , condition] of workflow.edges) {
2683
+ const group = groups.get(from) ?? { plain: 0, conditional: 0 };
2684
+ if (condition?.when) group.conditional++;
2685
+ else group.plain++;
2686
+ groups.set(from, group);
2687
+ if (group.plain > 1 || group.plain && group.conditional) return true;
2688
+ }
2689
+ return false;
2690
+ }
2691
+ function addParallelGraphCoordinator(builder, workflow, config, execution, executors) {
2692
+ const ids = [...Object.keys(workflow.nodes), "__end__"];
2693
+ const incoming = new Map(ids.map((id) => [id, []]));
2694
+ const outgoing = /* @__PURE__ */ new Map();
2695
+ for (const [from, to, condition] of workflow.edges) {
2696
+ if (from === "__end__" || to === "__start__" || from !== "__start__" && !Object.hasOwn(workflow.nodes, from) || !incoming.has(to))
2697
+ throw new import_errors12.WorkflowContractError(
2698
+ `Invalid parallel graph edge '${from}' \u2192 '${to}'.`
2699
+ );
2700
+ if (!incoming.get(to)?.includes(from)) incoming.get(to)?.push(from);
2701
+ const edges = outgoing.get(from) ?? [];
2702
+ if (!edges.some((edge) => edge.to === to && edge.when === condition?.when))
2703
+ edges.push({ to, ...condition ? { when: condition.when } : {} });
2704
+ outgoing.set(from, edges);
2705
+ }
2706
+ const ancestors = /* @__PURE__ */ new Map([["__start__", /* @__PURE__ */ new Set()]]);
2707
+ const remaining = new Set(ids);
2708
+ const ordered = [];
2709
+ while (remaining.size) {
2710
+ const ready = [...remaining].filter(
2711
+ (id) => incoming.get(id)?.every((from) => ancestors.has(from))
2712
+ );
2713
+ if (!ready.length)
2714
+ throw new import_errors12.KortyxError(
2715
+ "GRAPH_CYCLE",
2716
+ "Parallel graph back-edges are not supported: use a sequential child workflow for loops.",
2717
+ {
2718
+ category: "workflow",
2719
+ retryable: false,
2720
+ safeMessage: "Parallel graph back-edges are not supported: use a sequential child workflow for loops."
2721
+ }
2722
+ );
2723
+ for (const id of ready) {
2724
+ const parents = incoming.get(id) ?? [];
2725
+ ancestors.set(
2726
+ id,
2727
+ new Set(
2728
+ parents.flatMap((from) => [from, ...ancestors.get(from) ?? []])
2729
+ )
2730
+ );
2731
+ remaining.delete(id);
2732
+ ordered.push(id);
2733
+ }
2734
+ }
2735
+ ids.splice(0, ids.length, ...ordered);
2736
+ const failure = (code, message, node, cause) => new import_errors12.KortyxError(code, message, {
2737
+ category: "workflow",
2738
+ retryable: false,
2739
+ safeMessage: message,
2740
+ context: { workflow: workflow.id, node },
2741
+ ...cause ? { cause } : {}
2742
+ });
2743
+ const mergeFields = (parents, node, property, journal) => {
2744
+ const merged = {};
2745
+ for (const parent of parents) {
2746
+ for (const [key, field] of Object.entries(parent[property])) {
2747
+ const previous = Object.hasOwn(merged, key) ? merged[key] : void 0;
2748
+ if (previous && previous.writer !== field.writer) {
2749
+ if (journal.nodes[previous.writer]?.ancestors.includes(field.writer))
2750
+ continue;
2751
+ if (!journal.nodes[field.writer]?.ancestors.includes(previous.writer))
2752
+ throw failure(
2753
+ "GRAPH_OUTPUT_CONFLICT",
2754
+ `Node '${node}' cannot combine ${property === "fields" ? "data" : property === "workflowFields" ? "workflow state" : "runtime"} field '${key}': branches '${previous.writer}' and '${field.writer}' both wrote it. Return separate fields in application logic.`,
2755
+ node
2756
+ );
2757
+ }
2758
+ Object.defineProperty(merged, key, {
2759
+ value: field,
2760
+ enumerable: true,
2761
+ configurable: true,
2762
+ writable: true
2763
+ });
2764
+ }
2765
+ }
2766
+ return merged;
2767
+ };
2768
+ const values = (fields) => Object.fromEntries(
2769
+ Object.entries(fields).map(([key, field]) => [key, field.value])
2770
+ );
2771
+ const initialWorkflowState = (journal) => {
2772
+ const internal = journal.initialRuntime.__kortyx;
2773
+ return record(internal) && record(internal.workflowState) ? internal.workflowState : {};
2774
+ };
2775
+ const usage = (journal) => {
2776
+ const total = {
2777
+ ...zeroUsage(),
2778
+ ...journal.initialRuntime.tokenUsage
2779
+ };
2780
+ for (const node of Object.values(journal.nodes)) {
2781
+ const used = node.runtime.tokenUsage;
2782
+ if (used)
2783
+ for (const key of ["input", "output", "total"])
2784
+ total[key] += used[key] ?? 0;
2785
+ }
2786
+ return total;
2787
+ };
2788
+ const runtimePatch = (journal) => ({
2789
+ [KEY]: journal,
2790
+ tokenUsage: usage(journal)
2791
+ });
2792
+ const load = (state) => {
2793
+ const saved = state.runtime[KEY];
2794
+ if (saved && (config.resume === true || !config.executionRunId || saved.runId === config.executionRunId)) {
2795
+ const journal = clone4(saved);
2796
+ if (journal.version !== 1 || journal.workflowVersion !== workflow.version)
2797
+ throw new import_errors12.WorkflowContractError(
2798
+ "Parallel workflow changed since its execution was suspended."
2799
+ );
2800
+ return journal;
2801
+ }
2802
+ const initialRuntime = clone4(state.runtime);
2803
+ delete initialRuntime[KEY];
2804
+ return {
2805
+ version: 1,
2806
+ runId: config.executionRunId,
2807
+ workflowVersion: workflow.version,
2808
+ initialInput: state.input,
2809
+ initialData: clone4(state.data ?? {}),
2810
+ initialRuntime,
2811
+ nodes: Object.fromEntries(
2812
+ ids.map((id) => [
2813
+ id,
2814
+ {
2815
+ id,
2816
+ ancestors: [],
2817
+ status: "pending",
2818
+ responses: [],
2819
+ runtime: {},
2820
+ fields: {},
2821
+ workflowFields: {},
2822
+ runtimeFields: {},
2823
+ selected: []
2824
+ }
2825
+ ])
2826
+ )
2827
+ };
2828
+ };
2829
+ const terminal = (node) => ["completed", "failed", "skipped"].includes(node.status);
2830
+ builder.addNode(WORK, async (state) => {
2831
+ const journal = load(state);
2832
+ let control;
2833
+ const running = /* @__PURE__ */ new Map();
2834
+ for (const node of Object.values(journal.nodes))
2835
+ if (node.status === "limited") node.status = "pending";
2836
+ const start = async (id, parents) => {
2837
+ const node = journal.nodes[id];
2838
+ let privateState;
2839
+ try {
2840
+ node.ancestors = [
2841
+ ...new Set(
2842
+ parents.flatMap((parent) => [parent.id, ...parent.ancestors])
2843
+ )
2844
+ ];
2845
+ const failed = parents.find((parent) => parent.status === "failed");
2846
+ if (failed)
2847
+ throw failure(
2848
+ "GRAPH_DEPENDENCY_FAILED",
2849
+ `Node '${id}' did not run because required dependency '${failed.id}' failed.`,
2850
+ id,
2851
+ failed.failure
2852
+ );
2853
+ node.fields = mergeFields(parents, id, "fields", journal);
2854
+ node.workflowFields = mergeFields(
2855
+ parents,
2856
+ id,
2857
+ "workflowFields",
2858
+ journal
2859
+ );
2860
+ node.runtimeFields = mergeFields(parents, id, "runtimeFields", journal);
2861
+ if (id === "__end__") {
2862
+ node.status = "completed";
2863
+ return;
2864
+ }
2865
+ const data = (0, import_utils.deepMergeWithArrayOverwrite)(
2866
+ journal.initialData,
2867
+ values(node.fields)
2868
+ );
2869
+ const nodeInput = Object.keys(node.fields).length ? (0, import_utils.deepMergeWithArrayOverwrite)(
2870
+ record(journal.initialInput) ? journal.initialInput : { rawInput: journal.initialInput },
2871
+ values(node.fields)
2872
+ ) : journal.initialInput;
2873
+ const workflowState = (0, import_utils.deepMergeWithArrayOverwrite)(
2874
+ initialWorkflowState(journal),
2875
+ values(node.workflowFields)
2876
+ );
2877
+ const ownInternal = node.runtime.__kortyx;
2878
+ const inheritedRuntime = (0, import_utils.deepMergeWithArrayOverwrite)(
2879
+ journal.initialRuntime,
2880
+ values(node.runtimeFields)
2881
+ );
2882
+ privateState = {
2883
+ ...state,
2884
+ input: nodeInput === void 0 ? void 0 : clone4(nodeInput),
2885
+ data: clone4(data),
2886
+ runtime: clone4({
2887
+ ...inheritedRuntime,
2888
+ tokenUsage: zeroUsage(),
2889
+ ...node.runtime,
2890
+ __kortyx: {
2891
+ ...record(ownInternal) ? ownInternal : {},
2892
+ workflowState: record(ownInternal) && record(ownInternal.workflowState) ? ownInternal.workflowState : workflowState
2893
+ }
2894
+ }),
2895
+ conversationHistory: [],
2896
+ awaitingHumanInput: false
2897
+ };
2898
+ let index = 0;
2899
+ node.status = "running";
2900
+ const updates = await executors[id](
2901
+ privateState,
2902
+ (request) => {
2903
+ const position = index++;
2904
+ if (position < node.responses.length)
2905
+ return node.responses[position];
2906
+ node.request = request;
2907
+ throw new import_langgraph2.GraphInterrupt([]);
2908
+ },
2909
+ (nodeState) => {
2910
+ const snapshot = clone4(journal);
2911
+ snapshot.nodes[id].runtime = clone4(
2912
+ nodeState.runtime
2913
+ );
2914
+ for (const activation of Object.values(snapshot.nodes))
2915
+ if (activation.status === "running")
2916
+ activation.status = "pending";
2917
+ return {
2918
+ ...state,
2919
+ lastNode: id,
2920
+ input: nodeState.input,
2921
+ data: nodeState.data ?? {},
2922
+ conversationHistory: [
2923
+ ...state.conversationHistory,
2924
+ ...ids.flatMap(
2925
+ (nodeId) => snapshot.nodes[nodeId]?.history ?? []
2926
+ )
2927
+ ],
2928
+ runtime: { ...nodeState.runtime, ...runtimePatch(snapshot) }
2929
+ };
2930
+ }
2931
+ );
2932
+ node.runtime = updates.runtime ?? privateState.runtime;
2933
+ const delta = updates.__kortyxNodeData;
2934
+ const updatedData = updates.data;
2935
+ for (const key of Object.keys(delta))
2936
+ Object.defineProperty(node.fields, key, {
2937
+ value: { writer: id, value: updatedData?.[key] },
2938
+ enumerable: true,
2939
+ configurable: true,
2940
+ writable: true
2941
+ });
2942
+ const internal = node.runtime.__kortyx;
2943
+ for (const [key, value] of Object.entries(node.runtime)) {
2944
+ if ([KEY, "__kortyx", "tokenUsage"].includes(key)) continue;
2945
+ if (JSON.stringify(value) !== JSON.stringify(inheritedRuntime[key]))
2946
+ Object.defineProperty(node.runtimeFields, key, {
2947
+ value: { writer: id, value },
2948
+ enumerable: true,
2949
+ configurable: true,
2950
+ writable: true
2951
+ });
2952
+ }
2953
+ const updatedWorkflow = record(internal) && record(internal.workflowState) ? internal.workflowState : {};
2954
+ for (const [key, value] of Object.entries(updatedWorkflow)) {
2955
+ if (JSON.stringify(value) !== JSON.stringify(workflowState[key]))
2956
+ Object.defineProperty(node.workflowFields, key, {
2957
+ value: { writer: id, value },
2958
+ enumerable: true,
2959
+ configurable: true,
2960
+ writable: true
2961
+ });
2962
+ }
2963
+ node.history = updates.conversationHistory ?? [];
2964
+ node.selected = (outgoing.get(id) ?? []).filter(
2965
+ (edge) => !edge.when || edge.when === (updates.lastCondition ?? updates.lastIntent)
2966
+ ).map((edge) => edge.to);
2967
+ node.status = "completed";
2968
+ delete node.request;
2969
+ } catch (error) {
2970
+ const patch = error?.__kortyxHookStatePatch;
2971
+ if (privateState) node.runtime = { ...privateState.runtime, ...patch };
2972
+ if ((0, import_langgraph2.isGraphInterrupt)(error)) node.status = "waiting";
2973
+ else if ((0, import_core2.isExecutionLimitReached)(error)) {
2974
+ node.status = "limited";
2975
+ control ??= error;
2976
+ } else if ((0, import_core2.isExecutionCancelled)(error) || execution.abortSignal?.aborted) {
2977
+ control ??= error;
2978
+ } else if ((0, import_errors12.serializeFailure)(error).category === "persistence")
2979
+ control ??= error;
2980
+ else {
2981
+ node.failure = (0, import_errors12.serializeFailure)(error);
2982
+ node.failure.context = {
2983
+ ...node.failure.context,
2984
+ node: id,
2985
+ workflow: workflow.id
2986
+ };
2987
+ node.status = "failed";
2988
+ node.selected = (outgoing.get(id) ?? []).map((edge) => edge.to);
2989
+ }
2990
+ }
2991
+ };
2992
+ const launch = () => {
2993
+ if (control || execution.abortSignal?.aborted) return;
2994
+ for (const id of ids) {
2995
+ if (id === "__end__") continue;
2996
+ const node = journal.nodes[id];
2997
+ if (node.status !== "pending" || running.has(id)) continue;
2998
+ const sources = incoming.get(id) ?? [];
2999
+ if (!sources.every(
3000
+ (from) => from === "__start__" || terminal(journal.nodes[from])
3001
+ ))
3002
+ continue;
3003
+ const activeSources = sources.filter(
3004
+ (from) => from === "__start__" ? (outgoing.get(from) ?? []).some(
3005
+ (edge) => edge.to === id && (!edge.when || edge.when === (state.lastCondition ?? state.lastIntent))
3006
+ ) : journal.nodes[from]?.selected.includes(id)
3007
+ );
3008
+ if (!activeSources.length) {
3009
+ node.status = "skipped";
3010
+ continue;
3011
+ }
3012
+ const task = start(
3013
+ id,
3014
+ activeSources.filter((from) => from !== "__start__").map((from) => journal.nodes[from])
3015
+ );
3016
+ running.set(
3017
+ id,
3018
+ task.finally(() => running.delete(id))
3019
+ );
3020
+ }
3021
+ };
3022
+ try {
3023
+ while (true) {
3024
+ launch();
3025
+ if (!running.size) break;
3026
+ await Promise.race(running.values());
3027
+ }
3028
+ (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
3029
+ if (control) throw control;
3030
+ } catch (error) {
3031
+ await Promise.allSettled(running.values());
3032
+ if (error && typeof error === "object")
3033
+ Object.assign(error, { __kortyxHookStatePatch: runtimePatch(journal) });
3034
+ throw error;
3035
+ }
3036
+ journal.waitingNode = ids.find(
3037
+ (id) => journal.nodes[id]?.status === "waiting"
3038
+ );
3039
+ if (!journal.waitingNode) {
3040
+ const leaves = ids.filter(
3041
+ (id) => id !== "__end__" && journal.nodes[id]?.status !== "skipped" && !journal.nodes[id]?.selected.some((to) => to !== "__end__")
3042
+ );
3043
+ await start(
3044
+ "__end__",
3045
+ leaves.map((id) => journal.nodes[id])
3046
+ );
3047
+ }
3048
+ const end = journal.nodes.__end__;
3049
+ return {
3050
+ runtime: {
3051
+ ...end.status === "completed" ? values(end.runtimeFields) : {},
3052
+ ...runtimePatch(journal)
3053
+ },
3054
+ ...end.status === "completed" ? {
3055
+ input: Object.keys(end.fields).length ? (0, import_utils.deepMergeWithArrayOverwrite)(
3056
+ record(journal.initialInput) ? journal.initialInput : { rawInput: journal.initialInput },
3057
+ values(end.fields)
3058
+ ) : journal.initialInput,
3059
+ data: (0, import_utils.deepMergeWithArrayOverwrite)(
3060
+ journal.initialData,
3061
+ values(end.fields)
3062
+ ),
3063
+ conversationHistory: ids.flatMap(
3064
+ (id) => journal.nodes[id]?.history ?? []
3065
+ )
3066
+ } : {}
3067
+ };
3068
+ });
3069
+ builder.addNode(WAIT, (state) => {
3070
+ const journal = load(state);
3071
+ const id = journal.waitingNode;
3072
+ const node = id ? journal.nodes[id] : void 0;
3073
+ if (!id || !node?.request)
3074
+ throw new import_errors12.WorkflowContractError("Missing waiting parallel node.");
3075
+ const request = {
3076
+ ...node.request,
3077
+ meta: {
3078
+ ...node.request.meta,
3079
+ __kortyxResumeStatePatch: runtimePatch(journal)
3080
+ }
3081
+ };
3082
+ let response;
3083
+ try {
3084
+ response = (0, import_langgraph2.interrupt)(request);
3085
+ } catch (error) {
3086
+ config.emit?.("interrupt", {
3087
+ node: id,
3088
+ workflow: workflow.id,
3089
+ input: request
3090
+ });
3091
+ throw error;
3092
+ }
3093
+ node.responses.push(response);
3094
+ node.status = "pending";
3095
+ delete journal.waitingNode;
3096
+ return { runtime: runtimePatch(journal) };
3097
+ });
3098
+ builder.addNode(FINISH, (state) => {
3099
+ const journal = load(state);
3100
+ const failed = ids.filter((id) => journal.nodes[id]?.status === "failed");
3101
+ if (failed.length) {
3102
+ const relevant = [...failed].reverse().find((id) => id !== "__end__") ?? failed[0];
3103
+ const detail = journal.nodes[relevant]?.failure;
3104
+ const error = (0, import_errors12.errorFromFailure)({
3105
+ ...detail,
3106
+ results: failed.map((id) => ({
3107
+ status: "rejected",
3108
+ failure: journal.nodes[id]?.failure
3109
+ }))
3110
+ });
3111
+ Object.assign(error, { __kortyxHookStatePatch: runtimePatch(journal) });
3112
+ throw error;
3113
+ }
3114
+ return {
3115
+ runtime: {
3116
+ [KEY]: null,
3117
+ __kortyx: {
3118
+ workflowState: (0, import_utils.deepMergeWithArrayOverwrite)(
3119
+ initialWorkflowState(journal),
3120
+ values(journal.nodes.__end__?.workflowFields ?? {})
3121
+ )
3122
+ }
3123
+ }
3124
+ };
3125
+ });
3126
+ builder.addEdge("__start__", WORK);
3127
+ builder.addConditionalEdges(
3128
+ WORK,
3129
+ (state) => load(state).waitingNode ? "wait" : "finish",
3130
+ { wait: WAIT, finish: FINISH }
3131
+ );
3132
+ builder.addEdge(WAIT, WORK);
3133
+ builder.addEdge(FINISH, "__end__");
3134
+ }
3135
+
1560
3136
  // src/graph/create-execution-graph.ts
1561
3137
  async function createExecutionGraph(workflow, config, execution = {}) {
1562
- const StateAnnotation = import_langgraph2.Annotation.Root({
1563
- currentWorkflow: import_langgraph2.Annotation,
1564
- input: import_langgraph2.Annotation,
1565
- output: import_langgraph2.Annotation,
1566
- lastNode: import_langgraph2.Annotation,
1567
- lastCondition: import_langgraph2.Annotation,
1568
- lastIntent: import_langgraph2.Annotation,
1569
- runtime: (0, import_langgraph2.Annotation)({
1570
- reducer: (l, r) => (0, import_utils.deepMergeWithArrayOverwrite)(l ?? {}, r ?? {}),
3138
+ const StateAnnotation = import_langgraph3.Annotation.Root({
3139
+ currentWorkflow: import_langgraph3.Annotation,
3140
+ input: import_langgraph3.Annotation,
3141
+ output: import_langgraph3.Annotation,
3142
+ lastNode: import_langgraph3.Annotation,
3143
+ lastCondition: import_langgraph3.Annotation,
3144
+ lastIntent: import_langgraph3.Annotation,
3145
+ runtime: (0, import_langgraph3.Annotation)({
3146
+ reducer: (l, r) => (0, import_utils2.deepMergeWithArrayOverwrite)(l ?? {}, r ?? {}),
1571
3147
  default: () => ({})
1572
3148
  }),
1573
- config: import_langgraph2.Annotation,
1574
- transitionTo: import_langgraph2.Annotation,
1575
- data: (0, import_langgraph2.Annotation)({
1576
- reducer: (l, r) => (0, import_utils.deepMergeWithArrayOverwrite)(l ?? {}, r ?? {}),
3149
+ config: import_langgraph3.Annotation,
3150
+ transitionTo: import_langgraph3.Annotation,
3151
+ data: (0, import_langgraph3.Annotation)({
3152
+ reducer: (l, r) => (0, import_utils2.deepMergeWithArrayOverwrite)(l ?? {}, r ?? {}),
1577
3153
  default: () => ({})
1578
3154
  }),
1579
- conversationHistory: (0, import_langgraph2.Annotation)({
3155
+ conversationHistory: (0, import_langgraph3.Annotation)({
1580
3156
  reducer: (l, r) => [...l ?? [], ...r ?? []],
1581
3157
  default: () => []
1582
3158
  }),
1583
- awaitingHumanInput: import_langgraph2.Annotation
3159
+ awaitingHumanInput: import_langgraph3.Annotation
1584
3160
  });
1585
- const builder = new import_langgraph2.StateGraph(StateAnnotation);
3161
+ const builder = new import_langgraph3.StateGraph(StateAnnotation);
1586
3162
  const edgeApi = builder;
1587
3163
  const workflowName = workflow.id;
3164
+ const parallelGraph = hasParallelEdges(workflow);
3165
+ const nodeExecutors = {};
1588
3166
  const runtimeConfig = config;
1589
3167
  if (!runtimeConfig.emit) {
1590
3168
  runtimeConfig.emit = () => {
@@ -1626,22 +3204,32 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1626
3204
  nodeDef.behavior ?? void 0
1627
3205
  );
1628
3206
  const behavior = nodeConfig.behavior ?? {};
1629
- builder.addNode(nodeId, async (state) => {
1630
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
3207
+ const executeNode = async (state, awaitInput, snapshotGraph) => {
3208
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1631
3209
  const emitRuntimeEvent = (event, payload) => {
1632
3210
  runtimeConfig.emit(event, payload);
1633
3211
  };
3212
+ if (awaitInput)
3213
+ emitRuntimeEvent("status", {
3214
+ node: nodeId,
3215
+ message: `Processing node: ${nodeId}`
3216
+ });
1634
3217
  let suspension;
1635
3218
  let deniedAdmission;
1636
3219
  const hookNodeContext = {
1637
- completeResponse: runtimeConfig.workflowCallDepth ? void 0 : execution.completeResponse,
3220
+ completeResponse: runtimeConfig.workflowCallDepth ? void 0 : execution.completeResponse ? async (options, nodeState) => {
3221
+ await execution.completeResponse?.(
3222
+ options,
3223
+ snapshotGraph ? snapshotGraph(nodeState) : nodeState
3224
+ );
3225
+ } : void 0,
1638
3226
  abortSignal: execution.abortSignal,
1639
3227
  consumeExecution: (limit) => {
1640
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
3228
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1641
3229
  if (suspension) throw suspension;
1642
3230
  try {
1643
3231
  if (execution.budget)
1644
- (0, import_core2.consumeExecutionBudget)(execution.budget, limit);
3232
+ (0, import_core3.consumeExecutionBudget)(execution.budget, limit);
1645
3233
  } catch (error) {
1646
3234
  deniedAdmission = error;
1647
3235
  throw error;
@@ -1669,8 +3257,8 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1669
3257
  config: nodeConfig,
1670
3258
  emit: emitRuntimeEvent,
1671
3259
  awaitInterrupt: (interruptConfig) => {
1672
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
1673
- if (execution.budget) (0, import_core2.assertExecutionBudget)(execution.budget);
3260
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
3261
+ if (execution.budget) (0, import_core3.assertExecutionBudget)(execution.budget);
1674
3262
  const { kind, question } = interruptConfig;
1675
3263
  const isMulti = kind === "multi-choice" || interruptConfig.kind !== "text" && interruptConfig.multiple === true;
1676
3264
  const sharedInterruptFields = {
@@ -1692,14 +3280,15 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1692
3280
  };
1693
3281
  let resumed;
1694
3282
  try {
1695
- resumed = (0, import_langgraph2.interrupt)(payload);
3283
+ resumed = awaitInput ? awaitInput(payload) : (0, import_langgraph3.interrupt)(payload);
1696
3284
  } catch (error) {
1697
3285
  suspension = error;
1698
- emitRuntimeEvent("interrupt", {
1699
- node: nodeId,
1700
- workflow: workflowName,
1701
- input: payload
1702
- });
3286
+ if (!awaitInput)
3287
+ emitRuntimeEvent("interrupt", {
3288
+ node: nodeId,
3289
+ workflow: workflowName,
3290
+ input: payload
3291
+ });
1703
3292
  throw error;
1704
3293
  }
1705
3294
  if (isMulti) {
@@ -1728,11 +3317,11 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1728
3317
  let retryHookRuntime = null;
1729
3318
  while (attempt < maxAttempts) {
1730
3319
  try {
1731
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
3320
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1732
3321
  attempt++;
1733
3322
  const attemptState = retryHookRuntime && typeof retryHookRuntime === "object" ? {
1734
3323
  ...state,
1735
- runtime: (0, import_utils.deepMergeWithArrayOverwrite)(
3324
+ runtime: (0, import_utils2.deepMergeWithArrayOverwrite)(
1736
3325
  state.runtime ?? {},
1737
3326
  retryHookRuntime
1738
3327
  )
@@ -1750,12 +3339,12 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1750
3339
  input: state.input,
1751
3340
  params: nodeParams ?? {}
1752
3341
  });
1753
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
3342
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1754
3343
  if (deniedAdmission) throw deniedAdmission;
1755
3344
  if (suspension) throw suspension;
1756
3345
  return result;
1757
3346
  } catch (error) {
1758
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
3347
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1759
3348
  if (deniedAdmission) throw deniedAdmission;
1760
3349
  throw error;
1761
3350
  }
@@ -1794,21 +3383,21 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1794
3383
  } catch (err) {
1795
3384
  const patch = err?.__kortyxHookStatePatch;
1796
3385
  if (patch && typeof patch === "object") {
1797
- state.runtime = (0, import_utils.deepMergeWithArrayOverwrite)(
3386
+ state.runtime = (0, import_utils2.deepMergeWithArrayOverwrite)(
1798
3387
  state.runtime ?? {},
1799
3388
  patch
1800
3389
  );
1801
- retryHookRuntime = (0, import_utils.deepMergeWithArrayOverwrite)(
3390
+ retryHookRuntime = (0, import_utils2.deepMergeWithArrayOverwrite)(
1802
3391
  retryHookRuntime ?? {},
1803
3392
  patch
1804
3393
  );
1805
3394
  }
1806
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
1807
- if ((0, import_core2.isExecutionLimitReached)(err)) {
3395
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
3396
+ if ((0, import_core3.isExecutionLimitReached)(err)) {
1808
3397
  Object.assign(err, { nodeId, workflowId: workflowName });
1809
3398
  throw err;
1810
3399
  }
1811
- if ((0, import_core2.isExecutionCancelled)(err) || (0, import_langgraph2.isGraphInterrupt)(err)) throw err;
3400
+ if ((0, import_core3.isExecutionCancelled)(err) || (0, import_langgraph3.isGraphInterrupt)(err)) throw err;
1812
3401
  const hasMore = attempt < maxAttempts;
1813
3402
  const delayMs = behavior.retry?.delayMs ?? 0;
1814
3403
  if (hasMore && delayMs > 0) {
@@ -1818,12 +3407,12 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1818
3407
  if (!hasMore) throw err;
1819
3408
  }
1820
3409
  }
1821
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
3410
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1822
3411
  const res = nodeResult ?? {};
1823
3412
  if (hookRuntimeUpdates) {
1824
3413
  res.infra = {
1825
3414
  ...res.infra ?? {},
1826
- runtime: (0, import_utils.deepMergeWithArrayOverwrite)(
3415
+ runtime: (0, import_utils2.deepMergeWithArrayOverwrite)(
1827
3416
  (res.infra?.runtime ?? {}) || {},
1828
3417
  hookRuntimeUpdates
1829
3418
  )
@@ -1835,6 +3424,19 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1835
3424
  emitRuntimeEvent("message", { node: nodeId, content: uiMessage });
1836
3425
  }
1837
3426
  if (res.transitionTo) {
3427
+ if (awaitInput)
3428
+ throw Object.assign(
3429
+ new import_errors13.KortyxError(
3430
+ "GRAPH_BRANCH_HANDOFF",
3431
+ "transitionTo cannot hand off from a parallel branch; useWorkflow returns to its caller.",
3432
+ {
3433
+ category: "workflow",
3434
+ retryable: false,
3435
+ safeMessage: "transitionTo cannot hand off from a parallel branch; useWorkflow returns to its caller."
3436
+ }
3437
+ ),
3438
+ { __kortyxHookStatePatch: hookRuntimeUpdates }
3439
+ );
1838
3440
  emitRuntimeEvent("transition", {
1839
3441
  node: nodeId,
1840
3442
  workflow: workflowName,
@@ -1847,17 +3449,17 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1847
3449
  updates.lastCondition = res.condition;
1848
3450
  if (typeof res.intent === "string") updates.lastIntent = res.intent;
1849
3451
  if (res.data && typeof res.data === "object") {
1850
- updates.input = (0, import_utils.deepMergeWithArrayOverwrite)(
3452
+ updates.input = (0, import_utils2.deepMergeWithArrayOverwrite)(
1851
3453
  toRecordInput(state.input),
1852
3454
  res.data
1853
3455
  );
1854
- updates.data = (0, import_utils.deepMergeWithArrayOverwrite)(
3456
+ updates.data = (0, import_utils2.deepMergeWithArrayOverwrite)(
1855
3457
  state.data ?? {},
1856
3458
  res.data
1857
3459
  );
1858
3460
  }
1859
3461
  if (res.infra?.runtime && typeof res.infra.runtime === "object") {
1860
- updates.runtime = (0, import_utils.deepMergeWithArrayOverwrite)(
3462
+ updates.runtime = (0, import_utils2.deepMergeWithArrayOverwrite)(
1861
3463
  state.runtime ?? {},
1862
3464
  res.infra.runtime
1863
3465
  );
@@ -1865,7 +3467,7 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1865
3467
  if (res.ui) {
1866
3468
  updates.ui = {
1867
3469
  message: typeof res.ui.message === "string" ? res.ui.message : state.ui?.message ?? "",
1868
- structured: (0, import_utils.deepMergeWithArrayOverwrite)(
3470
+ structured: (0, import_utils2.deepMergeWithArrayOverwrite)(
1869
3471
  state.ui?.structured ?? {},
1870
3472
  res.ui.structured ?? {}
1871
3473
  )
@@ -1890,8 +3492,20 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1890
3492
  data: payload
1891
3493
  });
1892
3494
  }
3495
+ if (awaitInput) updates.__kortyxNodeData = res.data ?? {};
3496
+ if (awaitInput)
3497
+ emitRuntimeEvent("status", {
3498
+ node: nodeId,
3499
+ message: `\u2705 Completed node: ${nodeId}`
3500
+ });
1893
3501
  return updates;
3502
+ };
3503
+ Object.defineProperty(nodeExecutors, nodeId, {
3504
+ value: executeNode,
3505
+ enumerable: true
1894
3506
  });
3507
+ if (!parallelGraph)
3508
+ builder.addNode(nodeId, (state) => executeNode(state));
1895
3509
  }
1896
3510
  const condGroups = {};
1897
3511
  const plainEdges = [];
@@ -1904,26 +3518,36 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1904
3518
  plainEdges.push([from, to]);
1905
3519
  }
1906
3520
  }
1907
- for (const [from, to] of plainEdges) {
1908
- edgeApi.addEdge(from, to);
1909
- }
1910
- for (const [from, rules] of Object.entries(condGroups)) {
1911
- const mapping = Object.fromEntries(
1912
- rules.map((r) => [r.when, r.to])
1913
- );
1914
- const fallbackKey = `__default__:${from}`;
1915
- mapping[fallbackKey] = "__end__";
1916
- edgeApi.addConditionalEdges(
1917
- from,
1918
- (s) => {
1919
- const trigger = s.lastCondition ?? s.lastIntent;
1920
- if (trigger && Object.hasOwn(mapping, trigger)) {
1921
- return trigger;
1922
- }
1923
- return fallbackKey;
1924
- },
1925
- mapping
3521
+ if (parallelGraph) {
3522
+ addParallelGraphCoordinator(
3523
+ builder,
3524
+ workflow,
3525
+ runtimeConfig,
3526
+ execution,
3527
+ nodeExecutors
1926
3528
  );
3529
+ } else {
3530
+ for (const [from, to] of plainEdges) {
3531
+ edgeApi.addEdge(from, to);
3532
+ }
3533
+ for (const [from, rules] of Object.entries(condGroups)) {
3534
+ const mapping = Object.fromEntries(
3535
+ rules.map((r) => [r.when, r.to])
3536
+ );
3537
+ const fallbackKey = `__default__:${from}`;
3538
+ mapping[fallbackKey] = "__end__";
3539
+ edgeApi.addConditionalEdges(
3540
+ from,
3541
+ (s) => {
3542
+ const trigger = s.lastCondition ?? s.lastIntent;
3543
+ if (trigger && Object.hasOwn(mapping, trigger)) {
3544
+ return trigger;
3545
+ }
3546
+ return fallbackKey;
3547
+ },
3548
+ mapping
3549
+ );
3550
+ }
1927
3551
  }
1928
3552
  const sessionId = typeof config.session === "object" && config.session && "id" in config.session && typeof config.session.id === "string" ? config.session.id : "__default__";
1929
3553
  const checkpointer = config.checkpointer ?? getCheckpointer(sessionId);
@@ -1948,19 +3572,19 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1948
3572
  }
1949
3573
 
1950
3574
  // src/interrupt/tokens.ts
1951
- var import_node_crypto3 = require("crypto");
3575
+ var import_node_crypto7 = require("crypto");
1952
3576
  function makeResumeToken() {
1953
- return (0, import_node_crypto3.randomUUID)();
3577
+ return (0, import_node_crypto7.randomUUID)();
1954
3578
  }
1955
3579
  function makeRequestId(prefix = "req") {
1956
- return `${prefix}-${(0, import_node_crypto3.randomUUID)()}`;
3580
+ return `${prefix}-${(0, import_node_crypto7.randomUUID)()}`;
1957
3581
  }
1958
3582
 
1959
3583
  // src/registry/file-registry.ts
1960
3584
  var import_promises2 = require("fs/promises");
1961
3585
  var import_node_path2 = require("path");
1962
- var import_core3 = require("@kortyx/core");
1963
- var import_errors7 = require("@kortyx/core/errors");
3586
+ var import_core4 = require("@kortyx/core");
3587
+ var import_errors14 = require("@kortyx/core/errors");
1964
3588
  var DEFAULT_EXTENSIONS = [
1965
3589
  ".workflow.ts",
1966
3590
  ".workflow.mts",
@@ -1992,7 +3616,7 @@ function createFileWorkflowRegistry(options) {
1992
3616
  const modulePath = (0, import_node_path2.join)(workflowsDir, file);
1993
3617
  if (isDeclarativeWorkflowFile(file)) {
1994
3618
  const raw = await (0, import_promises2.readFile)(modulePath, "utf8");
1995
- const workflow = (0, import_core3.loadWorkflow)(raw);
3619
+ const workflow = (0, import_core4.loadWorkflow)(raw);
1996
3620
  workflow.metadata = {
1997
3621
  ...workflow.metadata ?? {},
1998
3622
  __filePath: modulePath
@@ -2034,7 +3658,7 @@ function createFileWorkflowRegistry(options) {
2034
3658
  const fallback = selectOptions?.fallbackId ?? fallbackId;
2035
3659
  const selected = workflows[id] ?? (fallback ? workflows[fallback] : null);
2036
3660
  if (!selected) {
2037
- throw new import_errors7.KortyxError(
3661
+ throw new import_errors14.KortyxError(
2038
3662
  "UNKNOWN_WORKFLOW",
2039
3663
  `No workflow found for id "${id}" and no fallback "${fallback}" workflow available`,
2040
3664
  {
@@ -2059,7 +3683,7 @@ function createFileWorkflowRegistry(options) {
2059
3683
  }
2060
3684
 
2061
3685
  // src/registry/in-memory-registry.ts
2062
- var import_errors8 = require("@kortyx/core/errors");
3686
+ var import_errors15 = require("@kortyx/core/errors");
2063
3687
  function createInMemoryWorkflowRegistry(workflows, options) {
2064
3688
  const byId = /* @__PURE__ */ new Map();
2065
3689
  for (const wf of workflows) byId.set(wf.id, wf);
@@ -2075,7 +3699,7 @@ function createInMemoryWorkflowRegistry(workflows, options) {
2075
3699
  const fallback = selectOptions?.fallbackId ?? fallbackId;
2076
3700
  const wf = byId.get(id) ?? (fallback ? byId.get(fallback) : void 0);
2077
3701
  if (!wf) {
2078
- throw new import_errors8.KortyxError(
3702
+ throw new import_errors15.KortyxError(
2079
3703
  "UNKNOWN_WORKFLOW",
2080
3704
  `Workflow "${id}" not found${fallback ? ` (fallback "${fallback}" missing too)` : ""}.`,
2081
3705
  {
@@ -2118,6 +3742,7 @@ async function buildInitialGraphState({
2118
3742
  buildInitialGraphState,
2119
3743
  captureGraphSnapshot,
2120
3744
  clearRegisteredNodes,
3745
+ createCachingFrameworkAdapter,
2121
3746
  createExecutionGraph,
2122
3747
  createFileWorkflowRegistry,
2123
3748
  createFrameworkAdapterFromEnv,
@@ -2125,6 +3750,7 @@ async function buildInitialGraphState({
2125
3750
  createInMemoryPendingRequestStore,
2126
3751
  createInMemorySessionCheckpointStore,
2127
3752
  createInMemoryWorkflowRegistry,
3753
+ createPostgresFrameworkAdapter,
2128
3754
  createRedisFrameworkAdapter,
2129
3755
  getCheckpointer,
2130
3756
  getRegisteredNode,