@kortyx/runtime 0.18.0 → 0.20.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,53 +2306,47 @@ 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");
2349
+ var import_internal2 = require("@kortyx/hooks/internal");
1227
2350
  var import_utils2 = require("@kortyx/utils");
1228
2351
  var import_langgraph3 = require("@langchain/langgraph");
1229
2352
 
@@ -1309,8 +2432,9 @@ async function resolveNodeHandler(args) {
1309
2432
 
1310
2433
  // src/graph/call-workflow.ts
1311
2434
  var import_core = require("@kortyx/core");
1312
- var import_errors6 = require("@kortyx/core/errors");
2435
+ var import_errors11 = require("@kortyx/core/errors");
1313
2436
  var import_hooks = require("@kortyx/hooks");
2437
+ var import_internal = require("@kortyx/hooks/internal");
1314
2438
  var import_singletons = require("@langchain/core/singletons");
1315
2439
  var import_langgraph = require("@langchain/langgraph");
1316
2440
  function createWorkflowCallService(config, _parent, execution = {}) {
@@ -1319,26 +2443,26 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1319
2443
  (0, import_core.throwIfExecutionAborted)(execution.abortSignal);
1320
2444
  const depth = (config.workflowCallDepth ?? 0) + 1;
1321
2445
  if (depth > 16)
1322
- throw new import_errors6.WorkflowContractError(
2446
+ throw new import_errors11.WorkflowContractError(
1323
2447
  "Maximum child workflow depth (16) exceeded."
1324
2448
  );
1325
2449
  const workflow = await config.selectWorkflow?.(args.workflow);
1326
2450
  if (!workflow || workflow.id !== args.workflow)
1327
- throw new import_errors6.WorkflowContractError(
2451
+ throw new import_errors11.WorkflowContractError(
1328
2452
  `Workflow '${args.workflow}' is not registered.`
1329
2453
  );
1330
2454
  if (!workflow.inputSchema || !workflow.outputSchema)
1331
- throw new import_errors6.WorkflowContractError(
2455
+ throw new import_errors11.WorkflowContractError(
1332
2456
  `Callable workflow '${workflow.id}' requires inputSchema and outputSchema.`
1333
2457
  );
1334
2458
  if (args.definition && (args.definition.version !== workflow.version || args.definition.inputSchema !== workflow.inputSchema || args.definition.outputSchema !== workflow.outputSchema)) {
1335
- throw new import_errors6.WorkflowContractError(
2459
+ throw new import_errors11.WorkflowContractError(
1336
2460
  "The typed workflow contract does not match the registered definition."
1337
2461
  );
1338
2462
  }
1339
2463
  const snapshot = args.snapshot;
1340
2464
  if (snapshot && snapshot.version !== workflow.version)
1341
- throw new import_errors6.WorkflowContractError(
2465
+ throw new import_errors11.WorkflowContractError(
1342
2466
  `Workflow '${workflow.id}' changed since the call was suspended.`
1343
2467
  );
1344
2468
  const input = snapshot ? void 0 : workflow.inputSchema.parse(args.input);
@@ -1397,10 +2521,10 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1397
2521
  }
1398
2522
  if (event === "error") {
1399
2523
  const failure = payload?.failure;
1400
- throw (0, import_errors6.isFailureDescriptor)(failure) ? (0, import_errors6.errorFromFailure)(failure) : new Error("Child workflow failed.");
2524
+ throw (0, import_errors11.isFailureDescriptor)(failure) ? (0, import_errors11.errorFromFailure)(failure) : new Error("Child workflow failed.");
1401
2525
  }
1402
2526
  if (event === "transition")
1403
- throw new import_errors6.WorkflowContractError(
2527
+ throw new import_errors11.WorkflowContractError(
1404
2528
  "transitionTo is not supported inside child workflows; useWorkflow returns to its caller."
1405
2529
  );
1406
2530
  const value = payload;
@@ -1455,7 +2579,7 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1455
2579
  const checkpoint = await captureGraphSnapshot(saver, threadId).catch(
1456
2580
  (cause) => {
1457
2581
  if ((0, import_core.isExecutionLimitReached)(error))
1458
- throw new import_errors6.PersistenceError(
2582
+ throw new import_errors11.PersistenceError(
1459
2583
  "Cannot checkpoint a suspended child workflow.",
1460
2584
  cause
1461
2585
  );
@@ -1492,7 +2616,7 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1492
2616
  if (request) {
1493
2617
  const checkpoint = await captureGraphSnapshot(saver, threadId);
1494
2618
  if (!checkpoint)
1495
- throw new import_errors6.WorkflowContractError(
2619
+ throw new import_errors11.WorkflowContractError(
1496
2620
  "Child interrupted without a graph checkpoint."
1497
2621
  );
1498
2622
  return {
@@ -1510,8 +2634,8 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1510
2634
  try {
1511
2635
  data = workflow.outputSchema.parse(result.data ?? {});
1512
2636
  } catch (cause) {
1513
- if (!Array.isArray((0, import_errors6.errorProperty)(cause, "issues"))) throw cause;
1514
- throw new import_errors6.ValidationError(
2637
+ if (!Array.isArray((0, import_errors11.errorProperty)(cause, "issues"))) throw cause;
2638
+ throw new import_errors11.ValidationError(
1515
2639
  "INVALID_OUTPUT",
1516
2640
  "Child output validation failed.",
1517
2641
  cause
@@ -1521,11 +2645,13 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1521
2645
  return {
1522
2646
  status: "completed",
1523
2647
  data,
2648
+ toolObservations: result.runtime.__kortyx?.workflowState?.__kortyxToolObservations ?? [],
1524
2649
  usage: result.runtime.tokenUsage
1525
2650
  };
1526
2651
  };
1527
2652
  const trace = config.telemetry?.trace ?? config.reasonTrace;
1528
- return trace?.withSpan ? trace.withSpan(
2653
+ return (0, import_internal.withSafeTraceSpan)(
2654
+ trace,
1529
2655
  {
1530
2656
  name: "kortyx.workflow.call",
1531
2657
  attributes: {
@@ -1539,13 +2665,13 @@ function createWorkflowCallService(config, _parent, execution = {}) {
1539
2665
  }
1540
2666
  },
1541
2667
  execute
1542
- ) : execute();
2668
+ );
1543
2669
  };
1544
2670
  }
1545
2671
 
1546
2672
  // src/graph/parallel-graph.ts
1547
2673
  var import_core2 = require("@kortyx/core");
1548
- var import_errors7 = require("@kortyx/core/errors");
2674
+ var import_errors12 = require("@kortyx/core/errors");
1549
2675
  var import_utils = require("@kortyx/utils");
1550
2676
  var import_langgraph2 = require("@langchain/langgraph");
1551
2677
  var WORK = "__kortyx_parallel_work";
@@ -1553,7 +2679,7 @@ var WAIT = "__kortyx_parallel_wait";
1553
2679
  var FINISH = "__kortyx_parallel_finish";
1554
2680
  var KEY = "__kortyxParallelGraph";
1555
2681
  var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1556
- var clone3 = (value) => JSON.parse(JSON.stringify(value));
2682
+ var clone4 = (value) => JSON.parse(JSON.stringify(value));
1557
2683
  var zeroUsage = () => ({ input: 0, output: 0, total: 0 });
1558
2684
  function hasParallelEdges(workflow) {
1559
2685
  const groups = /* @__PURE__ */ new Map();
@@ -1572,7 +2698,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1572
2698
  const outgoing = /* @__PURE__ */ new Map();
1573
2699
  for (const [from, to, condition] of workflow.edges) {
1574
2700
  if (from === "__end__" || to === "__start__" || from !== "__start__" && !Object.hasOwn(workflow.nodes, from) || !incoming.has(to))
1575
- throw new import_errors7.WorkflowContractError(
2701
+ throw new import_errors12.WorkflowContractError(
1576
2702
  `Invalid parallel graph edge '${from}' \u2192 '${to}'.`
1577
2703
  );
1578
2704
  if (!incoming.get(to)?.includes(from)) incoming.get(to)?.push(from);
@@ -1589,7 +2715,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1589
2715
  (id) => incoming.get(id)?.every((from) => ancestors.has(from))
1590
2716
  );
1591
2717
  if (!ready.length)
1592
- throw new import_errors7.KortyxError(
2718
+ throw new import_errors12.KortyxError(
1593
2719
  "GRAPH_CYCLE",
1594
2720
  "Parallel graph back-edges are not supported: use a sequential child workflow for loops.",
1595
2721
  {
@@ -1611,7 +2737,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1611
2737
  }
1612
2738
  }
1613
2739
  ids.splice(0, ids.length, ...ordered);
1614
- const failure = (code, message, node, cause) => new import_errors7.KortyxError(code, message, {
2740
+ const failure = (code, message, node, cause) => new import_errors12.KortyxError(code, message, {
1615
2741
  category: "workflow",
1616
2742
  retryable: false,
1617
2743
  safeMessage: message,
@@ -1623,6 +2749,20 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1623
2749
  for (const parent of parents) {
1624
2750
  for (const [key, field] of Object.entries(parent[property])) {
1625
2751
  const previous = Object.hasOwn(merged, key) ? merged[key] : void 0;
2752
+ if (property === "workflowFields" && key === "__kortyxToolObservations" && previous && Array.isArray(previous.value) && Array.isArray(field.value)) {
2753
+ const observations = /* @__PURE__ */ new Map();
2754
+ for (const observation of [...previous.value, ...field.value]) {
2755
+ if (record(observation) && typeof observation.attemptId === "string")
2756
+ observations.set(observation.attemptId, observation);
2757
+ }
2758
+ Object.defineProperty(merged, key, {
2759
+ value: { writer: field.writer, value: [...observations.values()] },
2760
+ enumerable: true,
2761
+ configurable: true,
2762
+ writable: true
2763
+ });
2764
+ continue;
2765
+ }
1626
2766
  if (previous && previous.writer !== field.writer) {
1627
2767
  if (journal.nodes[previous.writer]?.ancestors.includes(field.writer))
1628
2768
  continue;
@@ -1670,21 +2810,21 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1670
2810
  const load = (state) => {
1671
2811
  const saved = state.runtime[KEY];
1672
2812
  if (saved && (config.resume === true || !config.executionRunId || saved.runId === config.executionRunId)) {
1673
- const journal = clone3(saved);
2813
+ const journal = clone4(saved);
1674
2814
  if (journal.version !== 1 || journal.workflowVersion !== workflow.version)
1675
- throw new import_errors7.WorkflowContractError(
2815
+ throw new import_errors12.WorkflowContractError(
1676
2816
  "Parallel workflow changed since its execution was suspended."
1677
2817
  );
1678
2818
  return journal;
1679
2819
  }
1680
- const initialRuntime = clone3(state.runtime);
2820
+ const initialRuntime = clone4(state.runtime);
1681
2821
  delete initialRuntime[KEY];
1682
2822
  return {
1683
2823
  version: 1,
1684
2824
  runId: config.executionRunId,
1685
2825
  workflowVersion: workflow.version,
1686
2826
  initialInput: state.input,
1687
- initialData: clone3(state.data ?? {}),
2827
+ initialData: clone4(state.data ?? {}),
1688
2828
  initialRuntime,
1689
2829
  nodes: Object.fromEntries(
1690
2830
  ids.map((id) => [
@@ -1759,9 +2899,9 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1759
2899
  );
1760
2900
  privateState = {
1761
2901
  ...state,
1762
- input: nodeInput === void 0 ? void 0 : clone3(nodeInput),
1763
- data: clone3(data),
1764
- runtime: clone3({
2902
+ input: nodeInput === void 0 ? void 0 : clone4(nodeInput),
2903
+ data: clone4(data),
2904
+ runtime: clone4({
1765
2905
  ...inheritedRuntime,
1766
2906
  tokenUsage: zeroUsage(),
1767
2907
  ...node.runtime,
@@ -1785,8 +2925,8 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1785
2925
  throw new import_langgraph2.GraphInterrupt([]);
1786
2926
  },
1787
2927
  (nodeState) => {
1788
- const snapshot = clone3(journal);
1789
- snapshot.nodes[id].runtime = clone3(
2928
+ const snapshot = clone4(journal);
2929
+ snapshot.nodes[id].runtime = clone4(
1790
2930
  nodeState.runtime
1791
2931
  );
1792
2932
  for (const activation of Object.values(snapshot.nodes))
@@ -1853,10 +2993,10 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1853
2993
  control ??= error;
1854
2994
  } else if ((0, import_core2.isExecutionCancelled)(error) || execution.abortSignal?.aborted) {
1855
2995
  control ??= error;
1856
- } else if ((0, import_errors7.serializeFailure)(error).category === "persistence")
2996
+ } else if ((0, import_errors12.serializeFailure)(error).category === "persistence")
1857
2997
  control ??= error;
1858
2998
  else {
1859
- node.failure = (0, import_errors7.serializeFailure)(error);
2999
+ node.failure = (0, import_errors12.serializeFailure)(error);
1860
3000
  node.failure.context = {
1861
3001
  ...node.failure.context,
1862
3002
  node: id,
@@ -1949,7 +3089,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1949
3089
  const id = journal.waitingNode;
1950
3090
  const node = id ? journal.nodes[id] : void 0;
1951
3091
  if (!id || !node?.request)
1952
- throw new import_errors7.WorkflowContractError("Missing waiting parallel node.");
3092
+ throw new import_errors12.WorkflowContractError("Missing waiting parallel node.");
1953
3093
  const request = {
1954
3094
  ...node.request,
1955
3095
  meta: {
@@ -1979,7 +3119,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
1979
3119
  if (failed.length) {
1980
3120
  const relevant = [...failed].reverse().find((id) => id !== "__end__") ?? failed[0];
1981
3121
  const detail = journal.nodes[relevant]?.failure;
1982
- const error = (0, import_errors7.errorFromFailure)({
3122
+ const error = (0, import_errors12.errorFromFailure)({
1983
3123
  ...detail,
1984
3124
  results: failed.map((id) => ({
1985
3125
  status: "rejected",
@@ -2246,15 +3386,18 @@ async function createExecutionGraph(workflow, config, execution = {}) {
2246
3386
  ...typeof context?.tenantId === "string" ? { tenantId: context.tenantId } : {}
2247
3387
  },
2248
3388
  telemetry: {
2249
- metadata: {
2250
- ...runtimeConfig.telemetry?.metadata ?? {},
2251
- ...context ?? {}
2252
- },
3389
+ metadata: (0, import_internal2.safeTelemetryMetadata)({
3390
+ ...runtimeConfig.telemetry?.metadata ?? {}
3391
+ }),
2253
3392
  tags: runtimeConfig.telemetry?.tags,
2254
3393
  captureContent: runtimeConfig.telemetry?.captureContent
2255
3394
  }
2256
3395
  };
2257
- const hookRun = activeTrace?.withSpan ? await activeTrace.withSpan(spanArgs, runNode) : await runNode();
3396
+ const hookRun = await (0, import_internal2.withSafeTraceSpan)(
3397
+ activeTrace,
3398
+ spanArgs,
3399
+ runNode
3400
+ );
2258
3401
  nodeResult = hookRun.result;
2259
3402
  hookRuntimeUpdates = hookRun.runtimeUpdates;
2260
3403
  break;
@@ -2304,7 +3447,7 @@ async function createExecutionGraph(workflow, config, execution = {}) {
2304
3447
  if (res.transitionTo) {
2305
3448
  if (awaitInput)
2306
3449
  throw Object.assign(
2307
- new import_errors8.KortyxError(
3450
+ new import_errors13.KortyxError(
2308
3451
  "GRAPH_BRANCH_HANDOFF",
2309
3452
  "transitionTo cannot hand off from a parallel branch; useWorkflow returns to its caller.",
2310
3453
  {
@@ -2450,19 +3593,19 @@ async function createExecutionGraph(workflow, config, execution = {}) {
2450
3593
  }
2451
3594
 
2452
3595
  // src/interrupt/tokens.ts
2453
- var import_node_crypto3 = require("crypto");
3596
+ var import_node_crypto7 = require("crypto");
2454
3597
  function makeResumeToken() {
2455
- return (0, import_node_crypto3.randomUUID)();
3598
+ return (0, import_node_crypto7.randomUUID)();
2456
3599
  }
2457
3600
  function makeRequestId(prefix = "req") {
2458
- return `${prefix}-${(0, import_node_crypto3.randomUUID)()}`;
3601
+ return `${prefix}-${(0, import_node_crypto7.randomUUID)()}`;
2459
3602
  }
2460
3603
 
2461
3604
  // src/registry/file-registry.ts
2462
3605
  var import_promises2 = require("fs/promises");
2463
3606
  var import_node_path2 = require("path");
2464
3607
  var import_core4 = require("@kortyx/core");
2465
- var import_errors9 = require("@kortyx/core/errors");
3608
+ var import_errors14 = require("@kortyx/core/errors");
2466
3609
  var DEFAULT_EXTENSIONS = [
2467
3610
  ".workflow.ts",
2468
3611
  ".workflow.mts",
@@ -2536,7 +3679,7 @@ function createFileWorkflowRegistry(options) {
2536
3679
  const fallback = selectOptions?.fallbackId ?? fallbackId;
2537
3680
  const selected = workflows[id] ?? (fallback ? workflows[fallback] : null);
2538
3681
  if (!selected) {
2539
- throw new import_errors9.KortyxError(
3682
+ throw new import_errors14.KortyxError(
2540
3683
  "UNKNOWN_WORKFLOW",
2541
3684
  `No workflow found for id "${id}" and no fallback "${fallback}" workflow available`,
2542
3685
  {
@@ -2561,7 +3704,7 @@ function createFileWorkflowRegistry(options) {
2561
3704
  }
2562
3705
 
2563
3706
  // src/registry/in-memory-registry.ts
2564
- var import_errors10 = require("@kortyx/core/errors");
3707
+ var import_errors15 = require("@kortyx/core/errors");
2565
3708
  function createInMemoryWorkflowRegistry(workflows, options) {
2566
3709
  const byId = /* @__PURE__ */ new Map();
2567
3710
  for (const wf of workflows) byId.set(wf.id, wf);
@@ -2577,7 +3720,7 @@ function createInMemoryWorkflowRegistry(workflows, options) {
2577
3720
  const fallback = selectOptions?.fallbackId ?? fallbackId;
2578
3721
  const wf = byId.get(id) ?? (fallback ? byId.get(fallback) : void 0);
2579
3722
  if (!wf) {
2580
- throw new import_errors10.KortyxError(
3723
+ throw new import_errors15.KortyxError(
2581
3724
  "UNKNOWN_WORKFLOW",
2582
3725
  `Workflow "${id}" not found${fallback ? ` (fallback "${fallback}" missing too)` : ""}.`,
2583
3726
  {
@@ -2620,6 +3763,7 @@ async function buildInitialGraphState({
2620
3763
  buildInitialGraphState,
2621
3764
  captureGraphSnapshot,
2622
3765
  clearRegisteredNodes,
3766
+ createCachingFrameworkAdapter,
2623
3767
  createExecutionGraph,
2624
3768
  createFileWorkflowRegistry,
2625
3769
  createFrameworkAdapterFromEnv,
@@ -2627,6 +3771,7 @@ async function buildInitialGraphState({
2627
3771
  createInMemoryPendingRequestStore,
2628
3772
  createInMemorySessionCheckpointStore,
2629
3773
  createInMemoryWorkflowRegistry,
3774
+ createPostgresFrameworkAdapter,
2630
3775
  createRedisFrameworkAdapter,
2631
3776
  getCheckpointer,
2632
3777
  getRegisteredNode,