@kortyx/runtime 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +51 -0
- package/dist/index.d.ts +162 -8
- package/dist/index.js +1263 -139
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
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
|
|
194
|
-
|
|
271
|
+
const pruneExpired = (t, batchSize) => {
|
|
272
|
+
let deleted = 0;
|
|
195
273
|
for (const [k, v] of store.entries()) {
|
|
196
|
-
if (
|
|
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
|
|
271
|
-
var
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
750
|
-
var
|
|
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
|
|
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,
|
|
1862
|
+
return `${prefix}-${(0, import_node_crypto5.randomUUID)()}`;
|
|
762
1863
|
};
|
|
763
|
-
var createResumeToken = () => (0,
|
|
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 ?
|
|
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 ?
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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 } =
|
|
2010
|
+
const { parentCheckpointId: _parentCheckpointId, ...sourceRecord } = clone2(source);
|
|
910
2011
|
const runId = source.activePendingRequests.some(
|
|
911
2012
|
(request) => request.graphSnapshot
|
|
912
|
-
) ? `run-${(0,
|
|
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:
|
|
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:
|
|
2036
|
+
checkpoint: clone2(forked)
|
|
936
2037
|
};
|
|
937
2038
|
}
|
|
938
2039
|
};
|
|
939
2040
|
}
|
|
940
2041
|
|
|
941
2042
|
// src/framework/session-checkpoints.ts
|
|
942
|
-
var
|
|
943
|
-
var
|
|
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,
|
|
946
|
-
var createSessionId = () => `session-${(0,
|
|
947
|
-
var createResumeToken2 = () => (0,
|
|
948
|
-
var createRequestId = () => `human-${(0,
|
|
949
|
-
var
|
|
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,
|
|
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 ?
|
|
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 ?
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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 } =
|
|
2187
|
+
const { parentCheckpointId: _parentCheckpointId, ...sourceRecord } = clone3(source);
|
|
1087
2188
|
const runId = source.activePendingRequests.some(
|
|
1088
2189
|
(request) => request.graphSnapshot
|
|
1089
|
-
) ? `run-${(0,
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
2263
|
+
const adapter = {
|
|
1146
2264
|
kind: "redis",
|
|
1147
2265
|
ttlMs,
|
|
2266
|
+
maintenance: {
|
|
2267
|
+
setup: async () => {
|
|
2268
|
+
},
|
|
2269
|
+
prune: async (options2) => {
|
|
2270
|
+
resolvePruneOptions(options2);
|
|
2271
|
+
return emptyPruneResult();
|
|
2272
|
+
}
|
|
2273
|
+
},
|
|
2274
|
+
close: async () => {
|
|
2275
|
+
await store.close?.();
|
|
2276
|
+
},
|
|
1148
2277
|
pendingRequests: createRedisPendingRequestStore({
|
|
1149
2278
|
store,
|
|
1150
2279
|
prefix: "kortyx:pending:"
|
|
@@ -1177,52 +2306,45 @@ function createRedisFrameworkAdapter(options) {
|
|
|
1177
2306
|
);
|
|
1178
2307
|
}
|
|
1179
2308
|
};
|
|
2309
|
+
registerFrameworkCache(
|
|
2310
|
+
adapter,
|
|
2311
|
+
{
|
|
2312
|
+
get: (key) => store.get(`kortyx:runtime-cache:${key}`),
|
|
2313
|
+
set: (key, value, ttl) => store.set(`kortyx:runtime-cache:${key}`, value, ttl),
|
|
2314
|
+
close: () => adapter.close()
|
|
2315
|
+
},
|
|
2316
|
+
ttlMs
|
|
2317
|
+
);
|
|
2318
|
+
return adapter;
|
|
1180
2319
|
}
|
|
1181
2320
|
function createFrameworkAdapterFromEnv(env = process.env) {
|
|
1182
2321
|
const url = env.KORTYX_REDIS_URL || env.REDIS_URL || env.KORTYX_FRAMEWORK_REDIS_URL || "";
|
|
1183
2322
|
const ttlMsRaw = env.KORTYX_FRAMEWORK_TTL_MS || env.KORTYX_TTL_MS || "";
|
|
1184
2323
|
const ttlMs = ttlMsRaw ? Number(ttlMsRaw) : void 0;
|
|
2324
|
+
if (env.KORTYX_POSTGRES_URL) {
|
|
2325
|
+
const storage = createPostgresFrameworkAdapter({
|
|
2326
|
+
connectionString: env.KORTYX_POSTGRES_URL,
|
|
2327
|
+
...ttlMs !== void 0 ? { ttlMs } : {}
|
|
2328
|
+
});
|
|
2329
|
+
return url ? createCachingFrameworkAdapter({
|
|
2330
|
+
storage,
|
|
2331
|
+
cache: createRedisFrameworkAdapter({ url })
|
|
2332
|
+
}) : storage;
|
|
2333
|
+
}
|
|
1185
2334
|
if (url)
|
|
1186
|
-
return createRedisFrameworkAdapter({
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
|
2347
|
+
var import_errors13 = require("@kortyx/core/errors");
|
|
1226
2348
|
var import_hooks2 = require("@kortyx/hooks");
|
|
1227
2349
|
var import_utils2 = require("@kortyx/utils");
|
|
1228
2350
|
var import_langgraph3 = require("@langchain/langgraph");
|
|
@@ -1309,7 +2431,7 @@ async function resolveNodeHandler(args) {
|
|
|
1309
2431
|
|
|
1310
2432
|
// src/graph/call-workflow.ts
|
|
1311
2433
|
var import_core = require("@kortyx/core");
|
|
1312
|
-
var
|
|
2434
|
+
var import_errors11 = require("@kortyx/core/errors");
|
|
1313
2435
|
var import_hooks = require("@kortyx/hooks");
|
|
1314
2436
|
var import_singletons = require("@langchain/core/singletons");
|
|
1315
2437
|
var import_langgraph = require("@langchain/langgraph");
|
|
@@ -1319,26 +2441,26 @@ function createWorkflowCallService(config, _parent, execution = {}) {
|
|
|
1319
2441
|
(0, import_core.throwIfExecutionAborted)(execution.abortSignal);
|
|
1320
2442
|
const depth = (config.workflowCallDepth ?? 0) + 1;
|
|
1321
2443
|
if (depth > 16)
|
|
1322
|
-
throw new
|
|
2444
|
+
throw new import_errors11.WorkflowContractError(
|
|
1323
2445
|
"Maximum child workflow depth (16) exceeded."
|
|
1324
2446
|
);
|
|
1325
2447
|
const workflow = await config.selectWorkflow?.(args.workflow);
|
|
1326
2448
|
if (!workflow || workflow.id !== args.workflow)
|
|
1327
|
-
throw new
|
|
2449
|
+
throw new import_errors11.WorkflowContractError(
|
|
1328
2450
|
`Workflow '${args.workflow}' is not registered.`
|
|
1329
2451
|
);
|
|
1330
2452
|
if (!workflow.inputSchema || !workflow.outputSchema)
|
|
1331
|
-
throw new
|
|
2453
|
+
throw new import_errors11.WorkflowContractError(
|
|
1332
2454
|
`Callable workflow '${workflow.id}' requires inputSchema and outputSchema.`
|
|
1333
2455
|
);
|
|
1334
2456
|
if (args.definition && (args.definition.version !== workflow.version || args.definition.inputSchema !== workflow.inputSchema || args.definition.outputSchema !== workflow.outputSchema)) {
|
|
1335
|
-
throw new
|
|
2457
|
+
throw new import_errors11.WorkflowContractError(
|
|
1336
2458
|
"The typed workflow contract does not match the registered definition."
|
|
1337
2459
|
);
|
|
1338
2460
|
}
|
|
1339
2461
|
const snapshot = args.snapshot;
|
|
1340
2462
|
if (snapshot && snapshot.version !== workflow.version)
|
|
1341
|
-
throw new
|
|
2463
|
+
throw new import_errors11.WorkflowContractError(
|
|
1342
2464
|
`Workflow '${workflow.id}' changed since the call was suspended.`
|
|
1343
2465
|
);
|
|
1344
2466
|
const input = snapshot ? void 0 : workflow.inputSchema.parse(args.input);
|
|
@@ -1397,10 +2519,10 @@ function createWorkflowCallService(config, _parent, execution = {}) {
|
|
|
1397
2519
|
}
|
|
1398
2520
|
if (event === "error") {
|
|
1399
2521
|
const failure = payload?.failure;
|
|
1400
|
-
throw (0,
|
|
2522
|
+
throw (0, import_errors11.isFailureDescriptor)(failure) ? (0, import_errors11.errorFromFailure)(failure) : new Error("Child workflow failed.");
|
|
1401
2523
|
}
|
|
1402
2524
|
if (event === "transition")
|
|
1403
|
-
throw new
|
|
2525
|
+
throw new import_errors11.WorkflowContractError(
|
|
1404
2526
|
"transitionTo is not supported inside child workflows; useWorkflow returns to its caller."
|
|
1405
2527
|
);
|
|
1406
2528
|
const value = payload;
|
|
@@ -1455,7 +2577,7 @@ function createWorkflowCallService(config, _parent, execution = {}) {
|
|
|
1455
2577
|
const checkpoint = await captureGraphSnapshot(saver, threadId).catch(
|
|
1456
2578
|
(cause) => {
|
|
1457
2579
|
if ((0, import_core.isExecutionLimitReached)(error))
|
|
1458
|
-
throw new
|
|
2580
|
+
throw new import_errors11.PersistenceError(
|
|
1459
2581
|
"Cannot checkpoint a suspended child workflow.",
|
|
1460
2582
|
cause
|
|
1461
2583
|
);
|
|
@@ -1492,7 +2614,7 @@ function createWorkflowCallService(config, _parent, execution = {}) {
|
|
|
1492
2614
|
if (request) {
|
|
1493
2615
|
const checkpoint = await captureGraphSnapshot(saver, threadId);
|
|
1494
2616
|
if (!checkpoint)
|
|
1495
|
-
throw new
|
|
2617
|
+
throw new import_errors11.WorkflowContractError(
|
|
1496
2618
|
"Child interrupted without a graph checkpoint."
|
|
1497
2619
|
);
|
|
1498
2620
|
return {
|
|
@@ -1510,8 +2632,8 @@ function createWorkflowCallService(config, _parent, execution = {}) {
|
|
|
1510
2632
|
try {
|
|
1511
2633
|
data = workflow.outputSchema.parse(result.data ?? {});
|
|
1512
2634
|
} catch (cause) {
|
|
1513
|
-
if (!Array.isArray((0,
|
|
1514
|
-
throw new
|
|
2635
|
+
if (!Array.isArray((0, import_errors11.errorProperty)(cause, "issues"))) throw cause;
|
|
2636
|
+
throw new import_errors11.ValidationError(
|
|
1515
2637
|
"INVALID_OUTPUT",
|
|
1516
2638
|
"Child output validation failed.",
|
|
1517
2639
|
cause
|
|
@@ -1545,7 +2667,7 @@ function createWorkflowCallService(config, _parent, execution = {}) {
|
|
|
1545
2667
|
|
|
1546
2668
|
// src/graph/parallel-graph.ts
|
|
1547
2669
|
var import_core2 = require("@kortyx/core");
|
|
1548
|
-
var
|
|
2670
|
+
var import_errors12 = require("@kortyx/core/errors");
|
|
1549
2671
|
var import_utils = require("@kortyx/utils");
|
|
1550
2672
|
var import_langgraph2 = require("@langchain/langgraph");
|
|
1551
2673
|
var WORK = "__kortyx_parallel_work";
|
|
@@ -1553,7 +2675,7 @@ var WAIT = "__kortyx_parallel_wait";
|
|
|
1553
2675
|
var FINISH = "__kortyx_parallel_finish";
|
|
1554
2676
|
var KEY = "__kortyxParallelGraph";
|
|
1555
2677
|
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1556
|
-
var
|
|
2678
|
+
var clone4 = (value) => JSON.parse(JSON.stringify(value));
|
|
1557
2679
|
var zeroUsage = () => ({ input: 0, output: 0, total: 0 });
|
|
1558
2680
|
function hasParallelEdges(workflow) {
|
|
1559
2681
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -1572,7 +2694,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
|
|
|
1572
2694
|
const outgoing = /* @__PURE__ */ new Map();
|
|
1573
2695
|
for (const [from, to, condition] of workflow.edges) {
|
|
1574
2696
|
if (from === "__end__" || to === "__start__" || from !== "__start__" && !Object.hasOwn(workflow.nodes, from) || !incoming.has(to))
|
|
1575
|
-
throw new
|
|
2697
|
+
throw new import_errors12.WorkflowContractError(
|
|
1576
2698
|
`Invalid parallel graph edge '${from}' \u2192 '${to}'.`
|
|
1577
2699
|
);
|
|
1578
2700
|
if (!incoming.get(to)?.includes(from)) incoming.get(to)?.push(from);
|
|
@@ -1589,7 +2711,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
|
|
|
1589
2711
|
(id) => incoming.get(id)?.every((from) => ancestors.has(from))
|
|
1590
2712
|
);
|
|
1591
2713
|
if (!ready.length)
|
|
1592
|
-
throw new
|
|
2714
|
+
throw new import_errors12.KortyxError(
|
|
1593
2715
|
"GRAPH_CYCLE",
|
|
1594
2716
|
"Parallel graph back-edges are not supported: use a sequential child workflow for loops.",
|
|
1595
2717
|
{
|
|
@@ -1611,7 +2733,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
|
|
|
1611
2733
|
}
|
|
1612
2734
|
}
|
|
1613
2735
|
ids.splice(0, ids.length, ...ordered);
|
|
1614
|
-
const failure = (code, message, node, cause) => new
|
|
2736
|
+
const failure = (code, message, node, cause) => new import_errors12.KortyxError(code, message, {
|
|
1615
2737
|
category: "workflow",
|
|
1616
2738
|
retryable: false,
|
|
1617
2739
|
safeMessage: message,
|
|
@@ -1670,21 +2792,21 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
|
|
|
1670
2792
|
const load = (state) => {
|
|
1671
2793
|
const saved = state.runtime[KEY];
|
|
1672
2794
|
if (saved && (config.resume === true || !config.executionRunId || saved.runId === config.executionRunId)) {
|
|
1673
|
-
const journal =
|
|
2795
|
+
const journal = clone4(saved);
|
|
1674
2796
|
if (journal.version !== 1 || journal.workflowVersion !== workflow.version)
|
|
1675
|
-
throw new
|
|
2797
|
+
throw new import_errors12.WorkflowContractError(
|
|
1676
2798
|
"Parallel workflow changed since its execution was suspended."
|
|
1677
2799
|
);
|
|
1678
2800
|
return journal;
|
|
1679
2801
|
}
|
|
1680
|
-
const initialRuntime =
|
|
2802
|
+
const initialRuntime = clone4(state.runtime);
|
|
1681
2803
|
delete initialRuntime[KEY];
|
|
1682
2804
|
return {
|
|
1683
2805
|
version: 1,
|
|
1684
2806
|
runId: config.executionRunId,
|
|
1685
2807
|
workflowVersion: workflow.version,
|
|
1686
2808
|
initialInput: state.input,
|
|
1687
|
-
initialData:
|
|
2809
|
+
initialData: clone4(state.data ?? {}),
|
|
1688
2810
|
initialRuntime,
|
|
1689
2811
|
nodes: Object.fromEntries(
|
|
1690
2812
|
ids.map((id) => [
|
|
@@ -1759,9 +2881,9 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
|
|
|
1759
2881
|
);
|
|
1760
2882
|
privateState = {
|
|
1761
2883
|
...state,
|
|
1762
|
-
input: nodeInput === void 0 ? void 0 :
|
|
1763
|
-
data:
|
|
1764
|
-
runtime:
|
|
2884
|
+
input: nodeInput === void 0 ? void 0 : clone4(nodeInput),
|
|
2885
|
+
data: clone4(data),
|
|
2886
|
+
runtime: clone4({
|
|
1765
2887
|
...inheritedRuntime,
|
|
1766
2888
|
tokenUsage: zeroUsage(),
|
|
1767
2889
|
...node.runtime,
|
|
@@ -1785,8 +2907,8 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
|
|
|
1785
2907
|
throw new import_langgraph2.GraphInterrupt([]);
|
|
1786
2908
|
},
|
|
1787
2909
|
(nodeState) => {
|
|
1788
|
-
const snapshot =
|
|
1789
|
-
snapshot.nodes[id].runtime =
|
|
2910
|
+
const snapshot = clone4(journal);
|
|
2911
|
+
snapshot.nodes[id].runtime = clone4(
|
|
1790
2912
|
nodeState.runtime
|
|
1791
2913
|
);
|
|
1792
2914
|
for (const activation of Object.values(snapshot.nodes))
|
|
@@ -1853,10 +2975,10 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
|
|
|
1853
2975
|
control ??= error;
|
|
1854
2976
|
} else if ((0, import_core2.isExecutionCancelled)(error) || execution.abortSignal?.aborted) {
|
|
1855
2977
|
control ??= error;
|
|
1856
|
-
} else if ((0,
|
|
2978
|
+
} else if ((0, import_errors12.serializeFailure)(error).category === "persistence")
|
|
1857
2979
|
control ??= error;
|
|
1858
2980
|
else {
|
|
1859
|
-
node.failure = (0,
|
|
2981
|
+
node.failure = (0, import_errors12.serializeFailure)(error);
|
|
1860
2982
|
node.failure.context = {
|
|
1861
2983
|
...node.failure.context,
|
|
1862
2984
|
node: id,
|
|
@@ -1949,7 +3071,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
|
|
|
1949
3071
|
const id = journal.waitingNode;
|
|
1950
3072
|
const node = id ? journal.nodes[id] : void 0;
|
|
1951
3073
|
if (!id || !node?.request)
|
|
1952
|
-
throw new
|
|
3074
|
+
throw new import_errors12.WorkflowContractError("Missing waiting parallel node.");
|
|
1953
3075
|
const request = {
|
|
1954
3076
|
...node.request,
|
|
1955
3077
|
meta: {
|
|
@@ -1979,7 +3101,7 @@ function addParallelGraphCoordinator(builder, workflow, config, execution, execu
|
|
|
1979
3101
|
if (failed.length) {
|
|
1980
3102
|
const relevant = [...failed].reverse().find((id) => id !== "__end__") ?? failed[0];
|
|
1981
3103
|
const detail = journal.nodes[relevant]?.failure;
|
|
1982
|
-
const error = (0,
|
|
3104
|
+
const error = (0, import_errors12.errorFromFailure)({
|
|
1983
3105
|
...detail,
|
|
1984
3106
|
results: failed.map((id) => ({
|
|
1985
3107
|
status: "rejected",
|
|
@@ -2304,7 +3426,7 @@ async function createExecutionGraph(workflow, config, execution = {}) {
|
|
|
2304
3426
|
if (res.transitionTo) {
|
|
2305
3427
|
if (awaitInput)
|
|
2306
3428
|
throw Object.assign(
|
|
2307
|
-
new
|
|
3429
|
+
new import_errors13.KortyxError(
|
|
2308
3430
|
"GRAPH_BRANCH_HANDOFF",
|
|
2309
3431
|
"transitionTo cannot hand off from a parallel branch; useWorkflow returns to its caller.",
|
|
2310
3432
|
{
|
|
@@ -2450,19 +3572,19 @@ async function createExecutionGraph(workflow, config, execution = {}) {
|
|
|
2450
3572
|
}
|
|
2451
3573
|
|
|
2452
3574
|
// src/interrupt/tokens.ts
|
|
2453
|
-
var
|
|
3575
|
+
var import_node_crypto7 = require("crypto");
|
|
2454
3576
|
function makeResumeToken() {
|
|
2455
|
-
return (0,
|
|
3577
|
+
return (0, import_node_crypto7.randomUUID)();
|
|
2456
3578
|
}
|
|
2457
3579
|
function makeRequestId(prefix = "req") {
|
|
2458
|
-
return `${prefix}-${(0,
|
|
3580
|
+
return `${prefix}-${(0, import_node_crypto7.randomUUID)()}`;
|
|
2459
3581
|
}
|
|
2460
3582
|
|
|
2461
3583
|
// src/registry/file-registry.ts
|
|
2462
3584
|
var import_promises2 = require("fs/promises");
|
|
2463
3585
|
var import_node_path2 = require("path");
|
|
2464
3586
|
var import_core4 = require("@kortyx/core");
|
|
2465
|
-
var
|
|
3587
|
+
var import_errors14 = require("@kortyx/core/errors");
|
|
2466
3588
|
var DEFAULT_EXTENSIONS = [
|
|
2467
3589
|
".workflow.ts",
|
|
2468
3590
|
".workflow.mts",
|
|
@@ -2536,7 +3658,7 @@ function createFileWorkflowRegistry(options) {
|
|
|
2536
3658
|
const fallback = selectOptions?.fallbackId ?? fallbackId;
|
|
2537
3659
|
const selected = workflows[id] ?? (fallback ? workflows[fallback] : null);
|
|
2538
3660
|
if (!selected) {
|
|
2539
|
-
throw new
|
|
3661
|
+
throw new import_errors14.KortyxError(
|
|
2540
3662
|
"UNKNOWN_WORKFLOW",
|
|
2541
3663
|
`No workflow found for id "${id}" and no fallback "${fallback}" workflow available`,
|
|
2542
3664
|
{
|
|
@@ -2561,7 +3683,7 @@ function createFileWorkflowRegistry(options) {
|
|
|
2561
3683
|
}
|
|
2562
3684
|
|
|
2563
3685
|
// src/registry/in-memory-registry.ts
|
|
2564
|
-
var
|
|
3686
|
+
var import_errors15 = require("@kortyx/core/errors");
|
|
2565
3687
|
function createInMemoryWorkflowRegistry(workflows, options) {
|
|
2566
3688
|
const byId = /* @__PURE__ */ new Map();
|
|
2567
3689
|
for (const wf of workflows) byId.set(wf.id, wf);
|
|
@@ -2577,7 +3699,7 @@ function createInMemoryWorkflowRegistry(workflows, options) {
|
|
|
2577
3699
|
const fallback = selectOptions?.fallbackId ?? fallbackId;
|
|
2578
3700
|
const wf = byId.get(id) ?? (fallback ? byId.get(fallback) : void 0);
|
|
2579
3701
|
if (!wf) {
|
|
2580
|
-
throw new
|
|
3702
|
+
throw new import_errors15.KortyxError(
|
|
2581
3703
|
"UNKNOWN_WORKFLOW",
|
|
2582
3704
|
`Workflow "${id}" not found${fallback ? ` (fallback "${fallback}" missing too)` : ""}.`,
|
|
2583
3705
|
{
|
|
@@ -2620,6 +3742,7 @@ async function buildInitialGraphState({
|
|
|
2620
3742
|
buildInitialGraphState,
|
|
2621
3743
|
captureGraphSnapshot,
|
|
2622
3744
|
clearRegisteredNodes,
|
|
3745
|
+
createCachingFrameworkAdapter,
|
|
2623
3746
|
createExecutionGraph,
|
|
2624
3747
|
createFileWorkflowRegistry,
|
|
2625
3748
|
createFrameworkAdapterFromEnv,
|
|
@@ -2627,6 +3750,7 @@ async function buildInitialGraphState({
|
|
|
2627
3750
|
createInMemoryPendingRequestStore,
|
|
2628
3751
|
createInMemorySessionCheckpointStore,
|
|
2629
3752
|
createInMemoryWorkflowRegistry,
|
|
3753
|
+
createPostgresFrameworkAdapter,
|
|
2630
3754
|
createRedisFrameworkAdapter,
|
|
2631
3755
|
getCheckpointer,
|
|
2632
3756
|
getRegisteredNode,
|