@kortyx/runtime 0.18.0 → 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(
@@ -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,9 +1859,9 @@ 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)();
1864
+ var createResumeToken = () => (0, import_node_crypto5.randomUUID)();
764
1865
  var toSummary = (record2) => ({
765
1866
  id: record2.id,
766
1867
  sessionId: record2.sessionId,
@@ -826,7 +1927,7 @@ function createRedisSessionCheckpointStore(options) {
826
1927
  const headId = await store.get(headKey(sessionId));
827
1928
  if (!headId) return null;
828
1929
  const record2 = await getRecord(headId);
829
- return record2 ? clone(record2) : null;
1930
+ return record2 ? clone2(record2) : null;
830
1931
  };
831
1932
  return {
832
1933
  async list(sessionId) {
@@ -834,7 +1935,7 @@ function createRedisSessionCheckpointStore(options) {
834
1935
  },
835
1936
  async get(id) {
836
1937
  const record2 = await getRecord(id);
837
- return record2 ? clone(record2) : null;
1938
+ return record2 ? clone2(record2) : null;
838
1939
  },
839
1940
  getHead,
840
1941
  async append(args) {
@@ -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,7 +1957,7 @@ 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 } : {},
@@ -864,12 +1965,12 @@ function createRedisSessionCheckpointStore(options) {
864
1965
  };
865
1966
  await saveRecord(record2);
866
1967
  await prune(args.sessionId);
867
- return clone(record2);
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."
@@ -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,21 +2033,21 @@ 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));
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));
950
2051
  var toSummary2 = (record2) => ({
951
2052
  id: record2.id,
952
2053
  sessionId: record2.sessionId,
@@ -975,7 +2076,7 @@ function createInMemorySessionCheckpointStore(options) {
975
2076
  const headBySession = /* @__PURE__ */ new Map();
976
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);
977
2078
  const saveRecord = (record2) => {
978
- byId.set(record2.id, clone2(record2));
2079
+ byId.set(record2.id, clone3(record2));
979
2080
  const ids = bySession.get(record2.sessionId) ?? [];
980
2081
  if (!ids.includes(record2.id)) ids.push(record2.id);
981
2082
  bySession.set(record2.sessionId, ids);
@@ -1000,7 +2101,7 @@ function createInMemorySessionCheckpointStore(options) {
1000
2101
  const headId = headBySession.get(sessionId);
1001
2102
  if (!headId) return null;
1002
2103
  const record2 = byId.get(headId);
1003
- return record2 ? clone2(record2) : null;
2104
+ return record2 ? clone3(record2) : null;
1004
2105
  };
1005
2106
  return {
1006
2107
  async list(sessionId) {
@@ -1008,7 +2109,7 @@ function createInMemorySessionCheckpointStore(options) {
1008
2109
  },
1009
2110
  async get(id) {
1010
2111
  const record2 = byId.get(id);
1011
- return record2 ? clone2(record2) : null;
2112
+ return record2 ? clone3(record2) : null;
1012
2113
  },
1013
2114
  getHead,
1014
2115
  async append(args) {
@@ -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,7 +2132,7 @@ 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 } : {},
@@ -1039,12 +2140,12 @@ function createInMemorySessionCheckpointStore(options) {
1039
2140
  };
1040
2141
  saveRecord(record2);
1041
2142
  prune(args.sessionId);
1042
- return clone2(record2);
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."
@@ -1070,23 +2171,23 @@ function createInMemorySessionCheckpointStore(options) {
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,52 +2306,45 @@ 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
2346
  var import_core3 = require("@kortyx/core");
1225
- var import_errors8 = require("@kortyx/core/errors");
2347
+ var import_errors13 = require("@kortyx/core/errors");
1226
2348
  var import_hooks2 = require("@kortyx/hooks");
1227
2349
  var import_utils2 = require("@kortyx/utils");
1228
2350
  var import_langgraph3 = require("@langchain/langgraph");
@@ -1309,7 +2431,7 @@ async function resolveNodeHandler(args) {
1309
2431
 
1310
2432
  // src/graph/call-workflow.ts
1311
2433
  var import_core = require("@kortyx/core");
1312
- var import_errors6 = require("@kortyx/core/errors");
2434
+ var import_errors11 = require("@kortyx/core/errors");
1313
2435
  var import_hooks = require("@kortyx/hooks");
1314
2436
  var import_singletons = require("@langchain/core/singletons");
1315
2437
  var import_langgraph = require("@langchain/langgraph");
@@ -1319,26 +2441,26 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1319
2441
  (0, import_core.throwIfExecutionAborted)(execution.abortSignal);
1320
2442
  const depth = (config.workflowCallDepth ?? 0) + 1;
1321
2443
  if (depth > 16)
1322
- throw new import_errors6.WorkflowContractError(
2444
+ throw new import_errors11.WorkflowContractError(
1323
2445
  "Maximum child workflow depth (16) exceeded."
1324
2446
  );
1325
2447
  const workflow = await config.selectWorkflow?.(args.workflow);
1326
2448
  if (!workflow || workflow.id !== args.workflow)
1327
- throw new import_errors6.WorkflowContractError(
2449
+ throw new import_errors11.WorkflowContractError(
1328
2450
  `Workflow '${args.workflow}' is not registered.`
1329
2451
  );
1330
2452
  if (!workflow.inputSchema || !workflow.outputSchema)
1331
- throw new import_errors6.WorkflowContractError(
2453
+ throw new import_errors11.WorkflowContractError(
1332
2454
  `Callable workflow '${workflow.id}' requires inputSchema and outputSchema.`
1333
2455
  );
1334
2456
  if (args.definition && (args.definition.version !== workflow.version || args.definition.inputSchema !== workflow.inputSchema || args.definition.outputSchema !== workflow.outputSchema)) {
1335
- throw new import_errors6.WorkflowContractError(
2457
+ throw new import_errors11.WorkflowContractError(
1336
2458
  "The typed workflow contract does not match the registered definition."
1337
2459
  );
1338
2460
  }
1339
2461
  const snapshot = args.snapshot;
1340
2462
  if (snapshot && snapshot.version !== workflow.version)
1341
- throw new import_errors6.WorkflowContractError(
2463
+ throw new import_errors11.WorkflowContractError(
1342
2464
  `Workflow '${workflow.id}' changed since the call was suspended.`
1343
2465
  );
1344
2466
  const input = snapshot ? void 0 : workflow.inputSchema.parse(args.input);
@@ -1397,10 +2519,10 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1397
2519
  }
1398
2520
  if (event === "error") {
1399
2521
  const failure = payload?.failure;
1400
- 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.");
1401
2523
  }
1402
2524
  if (event === "transition")
1403
- throw new import_errors6.WorkflowContractError(
2525
+ throw new import_errors11.WorkflowContractError(
1404
2526
  "transitionTo is not supported inside child workflows; useWorkflow returns to its caller."
1405
2527
  );
1406
2528
  const value = payload;
@@ -1455,7 +2577,7 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1455
2577
  const checkpoint = await captureGraphSnapshot(saver, threadId).catch(
1456
2578
  (cause) => {
1457
2579
  if ((0, import_core.isExecutionLimitReached)(error))
1458
- throw new import_errors6.PersistenceError(
2580
+ throw new import_errors11.PersistenceError(
1459
2581
  "Cannot checkpoint a suspended child workflow.",
1460
2582
  cause
1461
2583
  );
@@ -1492,7 +2614,7 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1492
2614
  if (request) {
1493
2615
  const checkpoint = await captureGraphSnapshot(saver, threadId);
1494
2616
  if (!checkpoint)
1495
- throw new import_errors6.WorkflowContractError(
2617
+ throw new import_errors11.WorkflowContractError(
1496
2618
  "Child interrupted without a graph checkpoint."
1497
2619
  );
1498
2620
  return {
@@ -1510,8 +2632,8 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1510
2632
  try {
1511
2633
  data = workflow.outputSchema.parse(result.data ?? {});
1512
2634
  } catch (cause) {
1513
- if (!Array.isArray((0, import_errors6.errorProperty)(cause, "issues"))) throw cause;
1514
- throw new import_errors6.ValidationError(
2635
+ if (!Array.isArray((0, import_errors11.errorProperty)(cause, "issues"))) throw cause;
2636
+ throw new import_errors11.ValidationError(
1515
2637
  "INVALID_OUTPUT",
1516
2638
  "Child output validation failed.",
1517
2639
  cause
@@ -1545,7 +2667,7 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1545
2667
 
1546
2668
  // src/graph/parallel-graph.ts
1547
2669
  var import_core2 = require("@kortyx/core");
1548
- var import_errors7 = require("@kortyx/core/errors");
2670
+ var import_errors12 = require("@kortyx/core/errors");
1549
2671
  var import_utils = require("@kortyx/utils");
1550
2672
  var import_langgraph2 = require("@langchain/langgraph");
1551
2673
  var WORK = "__kortyx_parallel_work";
@@ -1553,7 +2675,7 @@ var WAIT = "__kortyx_parallel_wait";
1553
2675
  var FINISH = "__kortyx_parallel_finish";
1554
2676
  var KEY = "__kortyxParallelGraph";
1555
2677
  var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1556
- var clone3 = (value) => JSON.parse(JSON.stringify(value));
2678
+ var clone4 = (value) => JSON.parse(JSON.stringify(value));
1557
2679
  var zeroUsage = () => ({ input: 0, output: 0, total: 0 });
1558
2680
  function hasParallelEdges(workflow) {
1559
2681
  const groups = /* @__PURE__ */ new Map();
@@ -1572,7 +2694,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1572
2694
  const outgoing = /* @__PURE__ */ new Map();
1573
2695
  for (const [from, to, condition] of workflow.edges) {
1574
2696
  if (from === "__end__" || to === "__start__" || from !== "__start__" && !Object.hasOwn(workflow.nodes, from) || !incoming.has(to))
1575
- throw new import_errors7.WorkflowContractError(
2697
+ throw new import_errors12.WorkflowContractError(
1576
2698
  `Invalid parallel graph edge '${from}' \u2192 '${to}'.`
1577
2699
  );
1578
2700
  if (!incoming.get(to)?.includes(from)) incoming.get(to)?.push(from);
@@ -1589,7 +2711,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1589
2711
  (id) => incoming.get(id)?.every((from) => ancestors.has(from))
1590
2712
  );
1591
2713
  if (!ready.length)
1592
- throw new import_errors7.KortyxError(
2714
+ throw new import_errors12.KortyxError(
1593
2715
  "GRAPH_CYCLE",
1594
2716
  "Parallel graph back-edges are not supported: use a sequential child workflow for loops.",
1595
2717
  {
@@ -1611,7 +2733,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1611
2733
  }
1612
2734
  }
1613
2735
  ids.splice(0, ids.length, ...ordered);
1614
- const failure = (code, message, node, cause) => new import_errors7.KortyxError(code, message, {
2736
+ const failure = (code, message, node, cause) => new import_errors12.KortyxError(code, message, {
1615
2737
  category: "workflow",
1616
2738
  retryable: false,
1617
2739
  safeMessage: message,
@@ -1670,21 +2792,21 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1670
2792
  const load = (state) => {
1671
2793
  const saved = state.runtime[KEY];
1672
2794
  if (saved && (config.resume === true || !config.executionRunId || saved.runId === config.executionRunId)) {
1673
- const journal = clone3(saved);
2795
+ const journal = clone4(saved);
1674
2796
  if (journal.version !== 1 || journal.workflowVersion !== workflow.version)
1675
- throw new import_errors7.WorkflowContractError(
2797
+ throw new import_errors12.WorkflowContractError(
1676
2798
  "Parallel workflow changed since its execution was suspended."
1677
2799
  );
1678
2800
  return journal;
1679
2801
  }
1680
- const initialRuntime = clone3(state.runtime);
2802
+ const initialRuntime = clone4(state.runtime);
1681
2803
  delete initialRuntime[KEY];
1682
2804
  return {
1683
2805
  version: 1,
1684
2806
  runId: config.executionRunId,
1685
2807
  workflowVersion: workflow.version,
1686
2808
  initialInput: state.input,
1687
- initialData: clone3(state.data ?? {}),
2809
+ initialData: clone4(state.data ?? {}),
1688
2810
  initialRuntime,
1689
2811
  nodes: Object.fromEntries(
1690
2812
  ids.map((id) => [
@@ -1759,9 +2881,9 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1759
2881
  );
1760
2882
  privateState = {
1761
2883
  ...state,
1762
- input: nodeInput === void 0 ? void 0 : clone3(nodeInput),
1763
- data: clone3(data),
1764
- runtime: clone3({
2884
+ input: nodeInput === void 0 ? void 0 : clone4(nodeInput),
2885
+ data: clone4(data),
2886
+ runtime: clone4({
1765
2887
  ...inheritedRuntime,
1766
2888
  tokenUsage: zeroUsage(),
1767
2889
  ...node.runtime,
@@ -1785,8 +2907,8 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1785
2907
  throw new import_langgraph2.GraphInterrupt([]);
1786
2908
  },
1787
2909
  (nodeState) => {
1788
- const snapshot = clone3(journal);
1789
- snapshot.nodes[id].runtime = clone3(
2910
+ const snapshot = clone4(journal);
2911
+ snapshot.nodes[id].runtime = clone4(
1790
2912
  nodeState.runtime
1791
2913
  );
1792
2914
  for (const activation of Object.values(snapshot.nodes))
@@ -1853,10 +2975,10 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1853
2975
  control ??= error;
1854
2976
  } else if ((0, import_core2.isExecutionCancelled)(error) || execution.abortSignal?.aborted) {
1855
2977
  control ??= error;
1856
- } else if ((0, import_errors7.serializeFailure)(error).category === "persistence")
2978
+ } else if ((0, import_errors12.serializeFailure)(error).category === "persistence")
1857
2979
  control ??= error;
1858
2980
  else {
1859
- node.failure = (0, import_errors7.serializeFailure)(error);
2981
+ node.failure = (0, import_errors12.serializeFailure)(error);
1860
2982
  node.failure.context = {
1861
2983
  ...node.failure.context,
1862
2984
  node: id,
@@ -1949,7 +3071,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1949
3071
  const id = journal.waitingNode;
1950
3072
  const node = id ? journal.nodes[id] : void 0;
1951
3073
  if (!id || !node?.request)
1952
- throw new import_errors7.WorkflowContractError("Missing waiting parallel node.");
3074
+ throw new import_errors12.WorkflowContractError("Missing waiting parallel node.");
1953
3075
  const request = {
1954
3076
  ...node.request,
1955
3077
  meta: {
@@ -1979,7 +3101,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1979
3101
  if (failed.length) {
1980
3102
  const relevant = [...failed].reverse().find((id) => id !== "__end__") ?? failed[0];
1981
3103
  const detail = journal.nodes[relevant]?.failure;
1982
- const error = (0, import_errors7.errorFromFailure)({
3104
+ const error = (0, import_errors12.errorFromFailure)({
1983
3105
  ...detail,
1984
3106
  results: failed.map((id) => ({
1985
3107
  status: "rejected",
@@ -2304,7 +3426,7 @@ async function createExecutionGraph(workflow, config, execution = {}) {
2304
3426
  if (res.transitionTo) {
2305
3427
  if (awaitInput)
2306
3428
  throw Object.assign(
2307
- new import_errors8.KortyxError(
3429
+ new import_errors13.KortyxError(
2308
3430
  "GRAPH_BRANCH_HANDOFF",
2309
3431
  "transitionTo cannot hand off from a parallel branch; useWorkflow returns to its caller.",
2310
3432
  {
@@ -2450,19 +3572,19 @@ async function createExecutionGraph(workflow, config, execution = {}) {
2450
3572
  }
2451
3573
 
2452
3574
  // src/interrupt/tokens.ts
2453
- var import_node_crypto3 = require("crypto");
3575
+ var import_node_crypto7 = require("crypto");
2454
3576
  function makeResumeToken() {
2455
- return (0, import_node_crypto3.randomUUID)();
3577
+ return (0, import_node_crypto7.randomUUID)();
2456
3578
  }
2457
3579
  function makeRequestId(prefix = "req") {
2458
- return `${prefix}-${(0, import_node_crypto3.randomUUID)()}`;
3580
+ return `${prefix}-${(0, import_node_crypto7.randomUUID)()}`;
2459
3581
  }
2460
3582
 
2461
3583
  // src/registry/file-registry.ts
2462
3584
  var import_promises2 = require("fs/promises");
2463
3585
  var import_node_path2 = require("path");
2464
3586
  var import_core4 = require("@kortyx/core");
2465
- var import_errors9 = require("@kortyx/core/errors");
3587
+ var import_errors14 = require("@kortyx/core/errors");
2466
3588
  var DEFAULT_EXTENSIONS = [
2467
3589
  ".workflow.ts",
2468
3590
  ".workflow.mts",
@@ -2536,7 +3658,7 @@ function createFileWorkflowRegistry(options) {
2536
3658
  const fallback = selectOptions?.fallbackId ?? fallbackId;
2537
3659
  const selected = workflows[id] ?? (fallback ? workflows[fallback] : null);
2538
3660
  if (!selected) {
2539
- throw new import_errors9.KortyxError(
3661
+ throw new import_errors14.KortyxError(
2540
3662
  "UNKNOWN_WORKFLOW",
2541
3663
  `No workflow found for id "${id}" and no fallback "${fallback}" workflow available`,
2542
3664
  {
@@ -2561,7 +3683,7 @@ function createFileWorkflowRegistry(options) {
2561
3683
  }
2562
3684
 
2563
3685
  // src/registry/in-memory-registry.ts
2564
- var import_errors10 = require("@kortyx/core/errors");
3686
+ var import_errors15 = require("@kortyx/core/errors");
2565
3687
  function createInMemoryWorkflowRegistry(workflows, options) {
2566
3688
  const byId = /* @__PURE__ */ new Map();
2567
3689
  for (const wf of workflows) byId.set(wf.id, wf);
@@ -2577,7 +3699,7 @@ function createInMemoryWorkflowRegistry(workflows, options) {
2577
3699
  const fallback = selectOptions?.fallbackId ?? fallbackId;
2578
3700
  const wf = byId.get(id) ?? (fallback ? byId.get(fallback) : void 0);
2579
3701
  if (!wf) {
2580
- throw new import_errors10.KortyxError(
3702
+ throw new import_errors15.KortyxError(
2581
3703
  "UNKNOWN_WORKFLOW",
2582
3704
  `Workflow "${id}" not found${fallback ? ` (fallback "${fallback}" missing too)` : ""}.`,
2583
3705
  {
@@ -2620,6 +3742,7 @@ async function buildInitialGraphState({
2620
3742
  buildInitialGraphState,
2621
3743
  captureGraphSnapshot,
2622
3744
  clearRegisteredNodes,
3745
+ createCachingFrameworkAdapter,
2623
3746
  createExecutionGraph,
2624
3747
  createFileWorkflowRegistry,
2625
3748
  createFrameworkAdapterFromEnv,
@@ -2627,6 +3750,7 @@ async function buildInitialGraphState({
2627
3750
  createInMemoryPendingRequestStore,
2628
3751
  createInMemorySessionCheckpointStore,
2629
3752
  createInMemoryWorkflowRegistry,
3753
+ createPostgresFrameworkAdapter,
2630
3754
  createRedisFrameworkAdapter,
2631
3755
  getCheckpointer,
2632
3756
  getRegisteredNode,