@kortyx/runtime 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  var index_exports = {};
21
21
  __export(index_exports, {
22
22
  buildInitialGraphState: () => buildInitialGraphState,
23
+ captureGraphSnapshot: () => captureGraphSnapshot,
23
24
  clearRegisteredNodes: () => clearRegisteredNodes,
24
25
  createExecutionGraph: () => createExecutionGraph,
25
26
  createFileWorkflowRegistry: () => createFileWorkflowRegistry,
@@ -35,7 +36,8 @@ __export(index_exports, {
35
36
  makeRequestId: () => makeRequestId,
36
37
  makeResumeToken: () => makeResumeToken,
37
38
  registerNode: () => registerNode,
38
- resolveNode: () => resolveNode
39
+ resolveNode: () => resolveNode,
40
+ restoreGraphSnapshot: () => restoreGraphSnapshot
39
41
  });
40
42
  module.exports = __toCommonJS(index_exports);
41
43
 
@@ -100,7 +102,7 @@ function createInMemoryCheckpointSaver(options) {
100
102
  }
101
103
  };
102
104
  }
103
- return tuple;
105
+ return JSON.parse(JSON.stringify(tuple));
104
106
  },
105
107
  async *list(_config, _options) {
106
108
  },
@@ -195,6 +197,12 @@ function createInMemoryPendingRequestStore() {
195
197
  }
196
198
  };
197
199
  return {
200
+ async take(token) {
201
+ prune();
202
+ const record = store.get(token);
203
+ store.delete(token);
204
+ return record ? JSON.parse(JSON.stringify(record)) : null;
205
+ },
198
206
  async save(rec) {
199
207
  prune();
200
208
  store.set(rec.token, rec);
@@ -219,6 +227,12 @@ function createRedisPendingRequestStore(options) {
219
227
  const prefix = options.prefix ?? "kortyx:pending:";
220
228
  const key = (token) => `${prefix}${token}`;
221
229
  return {
230
+ ...options.store.take ? {
231
+ take: async (token) => {
232
+ const raw = await options.store.take(key(token));
233
+ return raw ? JSON.parse(raw) : null;
234
+ }
235
+ } : {},
222
236
  async save(rec) {
223
237
  await options.store.set(key(rec.token), JSON.stringify(rec), rec.ttlMs);
224
238
  },
@@ -586,6 +600,11 @@ function createRedisFrameworkStore(options) {
586
600
  const prefix = options.prefix ?? "kortyx:fw:";
587
601
  const k = (key) => `${prefix}${key}`;
588
602
  return {
603
+ async take(key) {
604
+ const r = await client.command("GETDEL", [k(key)]);
605
+ if (isRedisError(r)) throw new Error(`Redis GETDEL error: ${r.message}`);
606
+ return typeof r === "string" ? r : null;
607
+ },
589
608
  async get(key) {
590
609
  const r = await client.command("GET", [k(key)]);
591
610
  if (r === null) return null;
@@ -828,8 +847,12 @@ function createRedisSessionCheckpointStore(options) {
828
847
  if (!source) throw new Error(`Checkpoint "${id}" not found.`);
829
848
  const sessionId = options2?.newSessionId || createId("session");
830
849
  const { parentCheckpointId: _parentCheckpointId, ...sourceRecord } = clone(source);
850
+ const runId = source.activePendingRequests.some(
851
+ (request) => request.graphSnapshot
852
+ ) ? `run-${(0, import_node_crypto.randomUUID)()}` : source.runId;
831
853
  const forked = {
832
854
  ...sourceRecord,
855
+ runId,
833
856
  id: createId("cp"),
834
857
  sessionId,
835
858
  parentSessionId: source.sessionId,
@@ -837,6 +860,7 @@ function createRedisSessionCheckpointStore(options) {
837
860
  activePendingRequests: clone(source.activePendingRequests).map(
838
861
  (request) => ({
839
862
  ...request,
863
+ runId,
840
864
  token: createResumeToken(),
841
865
  requestId: createId("human"),
842
866
  sessionId
@@ -991,8 +1015,12 @@ function createInMemorySessionCheckpointStore(options) {
991
1015
  }
992
1016
  const sessionId = options2?.newSessionId || createSessionId();
993
1017
  const { parentCheckpointId: _parentCheckpointId, ...sourceRecord } = clone2(source);
1018
+ const runId = source.activePendingRequests.some(
1019
+ (request) => request.graphSnapshot
1020
+ ) ? `run-${(0, import_node_crypto2.randomUUID)()}` : source.runId;
994
1021
  const forked = {
995
1022
  ...sourceRecord,
1023
+ runId,
996
1024
  id: createCheckpointId(),
997
1025
  sessionId,
998
1026
  parentSessionId: source.sessionId,
@@ -1000,6 +1028,7 @@ function createInMemorySessionCheckpointStore(options) {
1000
1028
  activePendingRequests: clone2(source.activePendingRequests).map(
1001
1029
  (request) => ({
1002
1030
  ...request,
1031
+ runId,
1003
1032
  token: createResumeToken2(),
1004
1033
  requestId: createRequestId(),
1005
1034
  sessionId
@@ -1089,10 +1118,42 @@ function createFrameworkAdapterFromEnv(env = process.env) {
1089
1118
  return createInMemoryFrameworkAdapter({ ...ttlMs ? { ttlMs } : {} });
1090
1119
  }
1091
1120
 
1121
+ // src/framework/graph-snapshot.ts
1122
+ async function captureGraphSnapshot(saver, threadId, checkpointId) {
1123
+ const tuple = await saver.getTuple({
1124
+ configurable: {
1125
+ thread_id: threadId,
1126
+ checkpoint_ns: "",
1127
+ ...checkpointId ? { checkpoint_id: checkpointId } : {}
1128
+ }
1129
+ });
1130
+ return tuple ? JSON.parse(JSON.stringify(tuple)) : void 0;
1131
+ }
1132
+ async function restoreGraphSnapshot(saver, snapshot, threadId) {
1133
+ const copy = JSON.parse(JSON.stringify(snapshot));
1134
+ const config = await saver.put(
1135
+ { configurable: { thread_id: threadId, checkpoint_ns: "" } },
1136
+ copy.checkpoint,
1137
+ copy.metadata,
1138
+ copy.checkpoint.channel_versions
1139
+ );
1140
+ const reset = saver;
1141
+ if (reset.deleteCheckpointWrites)
1142
+ await reset.deleteCheckpointWrites(threadId, "", copy.checkpoint.id);
1143
+ const writes = /* @__PURE__ */ new Map();
1144
+ for (const [task, channel, value] of copy.pendingWrites ?? []) {
1145
+ const group = writes.get(task) ?? [];
1146
+ group.push([channel, value]);
1147
+ writes.set(task, group);
1148
+ }
1149
+ for (const [task, group] of writes)
1150
+ await saver.putWrites(config, group, task);
1151
+ }
1152
+
1092
1153
  // src/graph/create-execution-graph.ts
1093
- var import_hooks = require("@kortyx/hooks");
1154
+ var import_hooks2 = require("@kortyx/hooks");
1094
1155
  var import_utils = require("@kortyx/utils");
1095
- var import_langgraph = require("@langchain/langgraph");
1156
+ var import_langgraph2 = require("@langchain/langgraph");
1096
1157
 
1097
1158
  // src/node-loader.ts
1098
1159
  var import_node_path = require("path");
@@ -1174,31 +1235,217 @@ async function resolveNodeHandler(args) {
1174
1235
  return resolveNode(run);
1175
1236
  }
1176
1237
 
1238
+ // src/graph/call-workflow.ts
1239
+ var import_hooks = require("@kortyx/hooks");
1240
+ var import_singletons = require("@langchain/core/singletons");
1241
+ var import_langgraph = require("@langchain/langgraph");
1242
+ function assertSequentialWorkflow(workflow) {
1243
+ const groups = /* @__PURE__ */ new Map();
1244
+ for (const [from, , condition] of workflow.edges) {
1245
+ const group = groups.get(from) ?? { plain: 0, conditional: 0 };
1246
+ if (condition) group.conditional++;
1247
+ else group.plain++;
1248
+ groups.set(from, group);
1249
+ if (group.plain > 1 || group.plain > 0 && group.conditional > 0) {
1250
+ throw new Error(
1251
+ `Workflow '${workflow.id}' has parallel edges; child calls require sequential graphs.`
1252
+ );
1253
+ }
1254
+ }
1255
+ }
1256
+ function createWorkflowCallService(config, parent) {
1257
+ return async (args) => {
1258
+ const execute = async () => {
1259
+ if (parent) assertSequentialWorkflow(parent);
1260
+ const depth = (config.workflowCallDepth ?? 0) + 1;
1261
+ if (depth > 16)
1262
+ throw new Error("Maximum child workflow depth (16) exceeded.");
1263
+ const workflow = await config.selectWorkflow?.(args.workflow);
1264
+ if (!workflow || workflow.id !== args.workflow)
1265
+ throw new Error(`Workflow '${args.workflow}' is not registered.`);
1266
+ if (!workflow.inputSchema || !workflow.outputSchema)
1267
+ throw new Error(
1268
+ `Callable workflow '${workflow.id}' requires inputSchema and outputSchema.`
1269
+ );
1270
+ if (args.definition && (args.definition.version !== workflow.version || args.definition.inputSchema !== workflow.inputSchema || args.definition.outputSchema !== workflow.outputSchema)) {
1271
+ throw new Error(
1272
+ "The typed workflow contract does not match the registered definition."
1273
+ );
1274
+ }
1275
+ assertSequentialWorkflow(workflow);
1276
+ const snapshot = args.snapshot;
1277
+ if (snapshot && snapshot.version !== workflow.version)
1278
+ throw new Error(
1279
+ `Workflow '${workflow.id}' changed since the call was suspended.`
1280
+ );
1281
+ const input = snapshot ? void 0 : workflow.inputSchema.parse(args.input);
1282
+ if (!snapshot) (0, import_hooks.workflowCallFingerprint)(input);
1283
+ const saver = createInMemoryCheckpointSaver();
1284
+ const threadId = "child";
1285
+ if (snapshot)
1286
+ await restoreGraphSnapshot(saver, snapshot.checkpoint, threadId);
1287
+ let request;
1288
+ const invocationPath = `${config.invocationPath ?? "root"}/${encodeURIComponent(args.id)}:${args.invocationId}`;
1289
+ let childConfig = {
1290
+ executionBranchId: config.executionBranchId,
1291
+ prepareChildTelemetry: config.prepareChildTelemetry,
1292
+ context: config.context,
1293
+ session: config.session,
1294
+ selectWorkflow: config.selectWorkflow,
1295
+ getProvider: config.getProvider,
1296
+ telemetry: config.telemetry ? {
1297
+ ...config.telemetry,
1298
+ correlation: {
1299
+ runId: config.telemetry.correlation?.runId,
1300
+ sessionId: config.telemetry.correlation?.sessionId,
1301
+ workflowId: workflow.id,
1302
+ invocationId: args.invocationId,
1303
+ parentInvocationId: config.telemetry.correlation?.invocationId,
1304
+ branchId: config.executionBranchId ?? config.executionRunId
1305
+ }
1306
+ } : void 0,
1307
+ reasonTrace: config.reasonTrace,
1308
+ checkpointer: saver,
1309
+ executionRunId: config.executionRunId,
1310
+ workflowCallDepth: depth,
1311
+ invocationPath,
1312
+ emit: (event, payload) => {
1313
+ if (event === "interrupt") {
1314
+ const original = payload.input;
1315
+ request = {
1316
+ ...original,
1317
+ meta: {
1318
+ ...original.meta,
1319
+ workflowCallPath: [
1320
+ {
1321
+ workflowId: workflow.id,
1322
+ invocationId: args.invocationId,
1323
+ callId: args.id
1324
+ },
1325
+ ...Array.isArray(original.meta?.workflowCallPath) ? original.meta.workflowCallPath : []
1326
+ ],
1327
+ workflowCall: original.meta?.workflowCall ?? {
1328
+ invocationId: args.invocationId,
1329
+ workflowId: workflow.id,
1330
+ nodeId: payload.node
1331
+ }
1332
+ }
1333
+ };
1334
+ return;
1335
+ }
1336
+ if (event === "error")
1337
+ throw new Error(
1338
+ String(
1339
+ payload.message ?? "Child workflow failed."
1340
+ )
1341
+ );
1342
+ if (event === "transition")
1343
+ throw new Error(
1344
+ "transitionTo is not supported inside child workflows; useWorkflow returns to its caller."
1345
+ );
1346
+ const value = payload;
1347
+ const scoped = { ...value, workflow: workflow.id, invocationPath };
1348
+ for (const key of ["node", "streamId", "opId", "segmentId"]) {
1349
+ if (typeof value[key] === "string")
1350
+ scoped[key] = `${config.executionRunId ?? "run"}/${args.invocationId}/${value[key]}`;
1351
+ }
1352
+ config.emit?.(event, scoped);
1353
+ }
1354
+ };
1355
+ childConfig = config.prepareChildTelemetry?.(workflow, childConfig) ?? childConfig;
1356
+ const graph = await createExecutionGraph(workflow, childConfig);
1357
+ const initial = {
1358
+ input,
1359
+ data: {},
1360
+ runtime: {},
1361
+ config: childConfig,
1362
+ currentWorkflow: workflow.id,
1363
+ lastNode: "__start__",
1364
+ awaitingHumanInput: false,
1365
+ conversationHistory: []
1366
+ };
1367
+ const resumePatch = snapshot?.request.meta?.__kortyxResumeStatePatch;
1368
+ const command = snapshot ? new import_langgraph.Command({
1369
+ resume: args.response,
1370
+ update: {
1371
+ config: childConfig,
1372
+ ...resumePatch ? { runtime: resumePatch } : {}
1373
+ }
1374
+ }) : initial;
1375
+ const result = await import_singletons.AsyncLocalStorageProviderSingleton.runWithConfig(
1376
+ {},
1377
+ () => graph.invoke(command, {
1378
+ configurable: { thread_id: threadId, checkpoint_ns: "" },
1379
+ callbacks: []
1380
+ })
1381
+ );
1382
+ if (request) {
1383
+ const checkpoint = await captureGraphSnapshot(saver, threadId);
1384
+ if (!checkpoint)
1385
+ throw new Error("Child interrupted without a graph checkpoint.");
1386
+ return {
1387
+ status: "interrupted",
1388
+ request,
1389
+ snapshot: {
1390
+ version: workflow.version,
1391
+ checkpoint,
1392
+ request
1393
+ }
1394
+ };
1395
+ }
1396
+ const data = workflow.outputSchema.parse(result.data ?? {});
1397
+ (0, import_hooks.workflowCallFingerprint)(data);
1398
+ return {
1399
+ status: "completed",
1400
+ data,
1401
+ usage: result.runtime.tokenUsage
1402
+ };
1403
+ };
1404
+ const trace = config.telemetry?.trace ?? config.reasonTrace;
1405
+ return trace?.withSpan ? trace.withSpan(
1406
+ {
1407
+ name: "kortyx.workflow.call",
1408
+ attributes: {
1409
+ workflowId: args.workflow,
1410
+ runId: config.executionRunId,
1411
+ branchId: config.executionBranchId ?? config.executionRunId,
1412
+ parentInvocationId: config.telemetry?.correlation?.invocationId,
1413
+ callerNodeExecutionId: args.callerNodeExecutionId,
1414
+ callId: args.id,
1415
+ invocationId: args.invocationId
1416
+ }
1417
+ },
1418
+ execute
1419
+ ) : execute();
1420
+ };
1421
+ }
1422
+
1177
1423
  // src/graph/create-execution-graph.ts
1178
1424
  async function createExecutionGraph(workflow, config) {
1179
- const StateAnnotation = import_langgraph.Annotation.Root({
1180
- input: import_langgraph.Annotation,
1181
- output: import_langgraph.Annotation,
1182
- lastNode: import_langgraph.Annotation,
1183
- lastCondition: import_langgraph.Annotation,
1184
- lastIntent: import_langgraph.Annotation,
1185
- runtime: (0, import_langgraph.Annotation)({
1425
+ const StateAnnotation = import_langgraph2.Annotation.Root({
1426
+ currentWorkflow: import_langgraph2.Annotation,
1427
+ input: import_langgraph2.Annotation,
1428
+ output: import_langgraph2.Annotation,
1429
+ lastNode: import_langgraph2.Annotation,
1430
+ lastCondition: import_langgraph2.Annotation,
1431
+ lastIntent: import_langgraph2.Annotation,
1432
+ runtime: (0, import_langgraph2.Annotation)({
1186
1433
  reducer: (l, r) => (0, import_utils.deepMergeWithArrayOverwrite)(l ?? {}, r ?? {}),
1187
1434
  default: () => ({})
1188
1435
  }),
1189
- config: import_langgraph.Annotation,
1190
- transitionTo: import_langgraph.Annotation,
1191
- data: (0, import_langgraph.Annotation)({
1436
+ config: import_langgraph2.Annotation,
1437
+ transitionTo: import_langgraph2.Annotation,
1438
+ data: (0, import_langgraph2.Annotation)({
1192
1439
  reducer: (l, r) => (0, import_utils.deepMergeWithArrayOverwrite)(l ?? {}, r ?? {}),
1193
1440
  default: () => ({})
1194
1441
  }),
1195
- conversationHistory: (0, import_langgraph.Annotation)({
1442
+ conversationHistory: (0, import_langgraph2.Annotation)({
1196
1443
  reducer: (l, r) => [...l ?? [], ...r ?? []],
1197
1444
  default: () => []
1198
1445
  }),
1199
- awaitingHumanInput: import_langgraph.Annotation
1446
+ awaitingHumanInput: import_langgraph2.Annotation
1200
1447
  });
1201
- const builder = new import_langgraph.StateGraph(StateAnnotation);
1448
+ const builder = new import_langgraph2.StateGraph(StateAnnotation);
1202
1449
  const edgeApi = builder;
1203
1450
  const workflowName = workflow.id;
1204
1451
  const runtimeConfig = config;
@@ -1208,6 +1455,7 @@ async function createExecutionGraph(workflow, config) {
1208
1455
  }
1209
1456
  const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
1210
1457
  const traceAdapter = () => runtimeConfig.telemetry?.trace ?? runtimeConfig.reasonTrace;
1458
+ const telemetryCorrelation = () => runtimeConfig.telemetry?.correlation;
1211
1459
  const configContext = () => isRecord(runtimeConfig.context) ? runtimeConfig.context : void 0;
1212
1460
  const toRecordInput = (value) => {
1213
1461
  if (isRecord(value)) return value;
@@ -1245,7 +1493,25 @@ async function createExecutionGraph(workflow, config) {
1245
1493
  const emitRuntimeEvent = (event, payload) => {
1246
1494
  runtimeConfig.emit(event, payload);
1247
1495
  };
1496
+ let suspension;
1248
1497
  const hookNodeContext = {
1498
+ workflowCallTelemetry: runtimeConfig.telemetry ? {
1499
+ ...runtimeConfig.telemetry,
1500
+ correlation: {
1501
+ ...runtimeConfig.telemetry.correlation,
1502
+ runId: runtimeConfig.executionRunId,
1503
+ branchId: runtimeConfig.executionBranchId ?? runtimeConfig.executionRunId
1504
+ }
1505
+ } : void 0,
1506
+ ...runtimeConfig.selectWorkflow ? {
1507
+ callWorkflow: createWorkflowCallService(
1508
+ {
1509
+ ...runtimeConfig,
1510
+ context: state.config?.context ?? runtimeConfig.context
1511
+ },
1512
+ workflow
1513
+ )
1514
+ } : {},
1249
1515
  graph: { name: workflowName, node: nodeId },
1250
1516
  config: nodeConfig,
1251
1517
  emit: emitRuntimeEvent,
@@ -1271,8 +1537,9 @@ async function createExecutionGraph(workflow, config) {
1271
1537
  };
1272
1538
  let resumed;
1273
1539
  try {
1274
- resumed = (0, import_langgraph.interrupt)(payload);
1540
+ resumed = (0, import_langgraph2.interrupt)(payload);
1275
1541
  } catch (error) {
1542
+ suspension = error;
1276
1543
  emitRuntimeEvent("interrupt", {
1277
1544
  node: nodeId,
1278
1545
  workflow: workflowName,
@@ -1320,18 +1587,27 @@ async function createExecutionGraph(workflow, config) {
1320
1587
  state: attemptState,
1321
1588
  reasonTrace: activeTrace
1322
1589
  };
1323
- const runNode = () => (0, import_hooks.runWithHookContext)(
1324
- hookContext,
1325
- async () => resolvedRun({
1590
+ const runNode = () => (0, import_hooks2.runWithHookContext)(hookContext, async () => {
1591
+ const result = await resolvedRun({
1326
1592
  input: state.input,
1327
1593
  params: nodeParams ?? {}
1328
- })
1329
- );
1594
+ });
1595
+ if (suspension) throw suspension;
1596
+ return result;
1597
+ });
1330
1598
  const context = configContext();
1331
1599
  const spanArgs = {
1332
1600
  name: "kortyx.node",
1333
1601
  attributes: {
1602
+ ...typeof telemetryCorrelation()?.runId === "string" ? { runId: telemetryCorrelation()?.runId } : {},
1603
+ ...typeof telemetryCorrelation()?.sessionId === "string" ? { sessionId: telemetryCorrelation()?.sessionId } : {},
1334
1604
  workflowId: workflowName,
1605
+ ...typeof telemetryCorrelation()?.workflowRevisionId === "string" ? {
1606
+ workflowRevisionId: telemetryCorrelation()?.workflowRevisionId
1607
+ } : {},
1608
+ ...typeof telemetryCorrelation()?.topologyHash === "string" ? { topologyHash: telemetryCorrelation()?.topologyHash } : {},
1609
+ invocationId: runtimeConfig.telemetry?.correlation?.invocationId,
1610
+ branchId: runtimeConfig.executionBranchId ?? runtimeConfig.executionRunId,
1335
1611
  nodeId,
1336
1612
  attempt,
1337
1613
  ...typeof context?.userId === "string" ? { userId: context.userId } : {},
@@ -1362,6 +1638,7 @@ async function createExecutionGraph(workflow, config) {
1362
1638
  patch
1363
1639
  );
1364
1640
  }
1641
+ if ((0, import_langgraph2.isGraphInterrupt)(err)) throw err;
1365
1642
  const hasMore = attempt < maxAttempts;
1366
1643
  const delayMs = behavior.retry?.delayMs ?? 0;
1367
1644
  if (hasMore && delayMs > 0) {
@@ -1388,6 +1665,8 @@ async function createExecutionGraph(workflow, config) {
1388
1665
  }
1389
1666
  if (res.transitionTo) {
1390
1667
  emitRuntimeEvent("transition", {
1668
+ node: nodeId,
1669
+ workflow: workflowName,
1391
1670
  transitionTo: res.transitionTo,
1392
1671
  payload: res.data ?? {}
1393
1672
  });
@@ -1651,6 +1930,7 @@ async function buildInitialGraphState({
1651
1930
  // Annotate the CommonJS export names for ESM import in node:
1652
1931
  0 && (module.exports = {
1653
1932
  buildInitialGraphState,
1933
+ captureGraphSnapshot,
1654
1934
  clearRegisteredNodes,
1655
1935
  createExecutionGraph,
1656
1936
  createFileWorkflowRegistry,
@@ -1666,6 +1946,7 @@ async function buildInitialGraphState({
1666
1946
  makeRequestId,
1667
1947
  makeResumeToken,
1668
1948
  registerNode,
1669
- resolveNode
1949
+ resolveNode,
1950
+ restoreGraphSnapshot
1670
1951
  });
1671
1952
  //# sourceMappingURL=index.js.map