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