@kortyx/runtime 0.17.2 → 0.18.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
@@ -205,9 +205,9 @@ function createInMemoryPendingRequestStore() {
205
205
  },
206
206
  async take(token) {
207
207
  prune();
208
- const record = store.get(token);
208
+ const record2 = store.get(token);
209
209
  store.delete(token);
210
- return record ? JSON.parse(JSON.stringify(record)) : null;
210
+ return record2 ? JSON.parse(JSON.stringify(record2)) : null;
211
211
  },
212
212
  async save(rec) {
213
213
  prune();
@@ -761,19 +761,19 @@ var createId = (prefix) => {
761
761
  return `${prefix}-${(0, import_node_crypto.randomUUID)()}`;
762
762
  };
763
763
  var createResumeToken = () => (0, import_node_crypto.randomUUID)();
764
- var toSummary = (record) => ({
765
- id: record.id,
766
- sessionId: record.sessionId,
767
- turnIndex: record.turnIndex,
768
- createdAt: record.createdAt,
769
- nodes: [...record.nodes],
770
- workflow: record.workflow,
771
- ...record.label ? { label: record.label } : {},
772
- ...record.parentCheckpointId ? { parentCheckpointId: record.parentCheckpointId } : {},
773
- ...record.parentSessionId ? { parentSessionId: record.parentSessionId } : {},
774
- ...record.forkedFrom ? { forkedFrom: record.forkedFrom } : {},
775
- ...record.workflowVersion ? { workflowVersion: record.workflowVersion } : {},
776
- ...record.buildId ? { buildId: record.buildId } : {}
764
+ var toSummary = (record2) => ({
765
+ id: record2.id,
766
+ sessionId: record2.sessionId,
767
+ turnIndex: record2.turnIndex,
768
+ createdAt: record2.createdAt,
769
+ nodes: [...record2.nodes],
770
+ workflow: record2.workflow,
771
+ ...record2.label ? { label: record2.label } : {},
772
+ ...record2.parentCheckpointId ? { parentCheckpointId: record2.parentCheckpointId } : {},
773
+ ...record2.parentSessionId ? { parentSessionId: record2.parentSessionId } : {},
774
+ ...record2.forkedFrom ? { forkedFrom: record2.forkedFrom } : {},
775
+ ...record2.workflowVersion ? { workflowVersion: record2.workflowVersion } : {},
776
+ ...record2.buildId ? { buildId: record2.buildId } : {}
777
777
  });
778
778
  function createRedisSessionCheckpointStore(options) {
779
779
  const store = options.store;
@@ -784,22 +784,22 @@ function createRedisSessionCheckpointStore(options) {
784
784
  const sessionRecordKey = (sessionId, id) => `${prefix}session:${sessionId}:${id}`;
785
785
  const sessionPrefix = (sessionId) => `${prefix}session:${sessionId}:`;
786
786
  const headKey = (sessionId) => `${prefix}head:${sessionId}`;
787
- const saveRecord = async (record) => {
788
- const serialized = JSON.stringify(record);
787
+ const saveRecord = async (record2) => {
788
+ const serialized = JSON.stringify(record2);
789
789
  await Promise.all([
790
- store.set(byIdKey(record.id), serialized, ttlMs),
790
+ store.set(byIdKey(record2.id), serialized, ttlMs),
791
791
  store.set(
792
- sessionRecordKey(record.sessionId, record.id),
792
+ sessionRecordKey(record2.sessionId, record2.id),
793
793
  serialized,
794
794
  ttlMs
795
795
  ),
796
- store.set(headKey(record.sessionId), record.id, ttlMs)
796
+ store.set(headKey(record2.sessionId), record2.id, ttlMs)
797
797
  ]);
798
798
  };
799
- const deleteRecord = async (record) => {
799
+ const deleteRecord = async (record2) => {
800
800
  await Promise.all([
801
- store.del(byIdKey(record.id)),
802
- store.del(sessionRecordKey(record.sessionId, record.id))
801
+ store.del(byIdKey(record2.id)),
802
+ store.del(sessionRecordKey(record2.sessionId, record2.id))
803
803
  ]);
804
804
  };
805
805
  const getRecord = async (id) => {
@@ -815,7 +815,7 @@ function createRedisSessionCheckpointStore(options) {
815
815
  return raw ? JSON.parse(raw) : null;
816
816
  })
817
817
  );
818
- return records.filter((record) => Boolean(record)).sort((a, b) => a.turnIndex - b.turnIndex || a.createdAt - b.createdAt);
818
+ return records.filter((record2) => Boolean(record2)).sort((a, b) => a.turnIndex - b.turnIndex || a.createdAt - b.createdAt);
819
819
  };
820
820
  const prune = async (sessionId) => {
821
821
  const records = await sortedRecords(sessionId);
@@ -825,22 +825,22 @@ function createRedisSessionCheckpointStore(options) {
825
825
  const getHead = async (sessionId) => {
826
826
  const headId = await store.get(headKey(sessionId));
827
827
  if (!headId) return null;
828
- const record = await getRecord(headId);
829
- return record ? clone(record) : null;
828
+ const record2 = await getRecord(headId);
829
+ return record2 ? clone(record2) : null;
830
830
  };
831
831
  return {
832
832
  async list(sessionId) {
833
833
  return (await sortedRecords(sessionId)).map(toSummary);
834
834
  },
835
835
  async get(id) {
836
- const record = await getRecord(id);
837
- return record ? clone(record) : null;
836
+ const record2 = await getRecord(id);
837
+ return record2 ? clone(record2) : null;
838
838
  },
839
839
  getHead,
840
840
  async append(args) {
841
841
  const previousHead = await getHead(args.sessionId);
842
842
  const pendingRequests = args.pendingRequests ?? [];
843
- const record = {
843
+ const record2 = {
844
844
  id: createId("cp"),
845
845
  sessionId: args.sessionId,
846
846
  runId: args.runId,
@@ -862,9 +862,9 @@ function createRedisSessionCheckpointStore(options) {
862
862
  ...args.workflowVersion ? { workflowVersion: args.workflowVersion } : {},
863
863
  ...args.buildId ? { buildId: args.buildId } : {}
864
864
  };
865
- await saveRecord(record);
865
+ await saveRecord(record2);
866
866
  await prune(args.sessionId);
867
- return clone(record);
867
+ return clone(record2);
868
868
  },
869
869
  async rollbackTo(id) {
870
870
  const target = await getRecord(id);
@@ -876,15 +876,15 @@ function createRedisSessionCheckpointStore(options) {
876
876
  });
877
877
  const records = await sortedRecords(target.sessionId);
878
878
  const trailing = records.filter(
879
- (record) => record.turnIndex > target.turnIndex
879
+ (record2) => record2.turnIndex > target.turnIndex
880
880
  );
881
881
  const invalidatedStructuredStreamIds = uniqueStrings(
882
- trailing.flatMap((record) => record.effects.structuredStreamIds)
882
+ trailing.flatMap((record2) => record2.effects.structuredStreamIds)
883
883
  );
884
884
  const invalidatedInterruptTokens = uniqueStrings(
885
- trailing.flatMap((record) => [
886
- ...record.effects.interruptTokens,
887
- ...record.activePendingRequests.map((request) => request.token)
885
+ trailing.flatMap((record2) => [
886
+ ...record2.effects.interruptTokens,
887
+ ...record2.activePendingRequests.map((request) => request.token)
888
888
  ])
889
889
  );
890
890
  await Promise.all(trailing.map(deleteRecord));
@@ -947,19 +947,19 @@ var createSessionId = () => `session-${(0, import_node_crypto2.randomUUID)()}`;
947
947
  var createResumeToken2 = () => (0, import_node_crypto2.randomUUID)();
948
948
  var createRequestId = () => `human-${(0, import_node_crypto2.randomUUID)()}`;
949
949
  var clone2 = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
950
- var toSummary2 = (record) => ({
951
- id: record.id,
952
- sessionId: record.sessionId,
953
- turnIndex: record.turnIndex,
954
- createdAt: record.createdAt,
955
- nodes: [...record.nodes],
956
- workflow: record.workflow,
957
- ...record.label ? { label: record.label } : {},
958
- ...record.parentCheckpointId ? { parentCheckpointId: record.parentCheckpointId } : {},
959
- ...record.parentSessionId ? { parentSessionId: record.parentSessionId } : {},
960
- ...record.forkedFrom ? { forkedFrom: record.forkedFrom } : {},
961
- ...record.workflowVersion ? { workflowVersion: record.workflowVersion } : {},
962
- ...record.buildId ? { buildId: record.buildId } : {}
950
+ var toSummary2 = (record2) => ({
951
+ id: record2.id,
952
+ sessionId: record2.sessionId,
953
+ turnIndex: record2.turnIndex,
954
+ createdAt: record2.createdAt,
955
+ nodes: [...record2.nodes],
956
+ workflow: record2.workflow,
957
+ ...record2.label ? { label: record2.label } : {},
958
+ ...record2.parentCheckpointId ? { parentCheckpointId: record2.parentCheckpointId } : {},
959
+ ...record2.parentSessionId ? { parentSessionId: record2.parentSessionId } : {},
960
+ ...record2.forkedFrom ? { forkedFrom: record2.forkedFrom } : {},
961
+ ...record2.workflowVersion ? { workflowVersion: record2.workflowVersion } : {},
962
+ ...record2.buildId ? { buildId: record2.buildId } : {}
963
963
  });
964
964
  var uniqueStrings2 = (values) => Array.from(
965
965
  new Set(
@@ -973,49 +973,49 @@ function createInMemorySessionCheckpointStore(options) {
973
973
  const byId = /* @__PURE__ */ new Map();
974
974
  const bySession = /* @__PURE__ */ new Map();
975
975
  const headBySession = /* @__PURE__ */ new Map();
976
- const sortedRecords = (sessionId) => (bySession.get(sessionId) ?? []).map((id) => byId.get(id)).filter((record) => Boolean(record)).sort((a, b) => a.turnIndex - b.turnIndex || a.createdAt - b.createdAt);
977
- const saveRecord = (record) => {
978
- byId.set(record.id, clone2(record));
979
- const ids = bySession.get(record.sessionId) ?? [];
980
- if (!ids.includes(record.id)) ids.push(record.id);
981
- bySession.set(record.sessionId, ids);
982
- headBySession.set(record.sessionId, record.id);
976
+ const sortedRecords = (sessionId) => (bySession.get(sessionId) ?? []).map((id) => byId.get(id)).filter((record2) => Boolean(record2)).sort((a, b) => a.turnIndex - b.turnIndex || a.createdAt - b.createdAt);
977
+ const saveRecord = (record2) => {
978
+ byId.set(record2.id, clone2(record2));
979
+ const ids = bySession.get(record2.sessionId) ?? [];
980
+ if (!ids.includes(record2.id)) ids.push(record2.id);
981
+ bySession.set(record2.sessionId, ids);
982
+ headBySession.set(record2.sessionId, record2.id);
983
983
  };
984
- const deleteRecord = (record) => {
985
- byId.delete(record.id);
984
+ const deleteRecord = (record2) => {
985
+ byId.delete(record2.id);
986
986
  bySession.set(
987
- record.sessionId,
987
+ record2.sessionId,
988
988
  /* v8 ignore next -- deleteRecord only receives records that came from this session index. */
989
- (bySession.get(record.sessionId) ?? []).filter((id) => id !== record.id)
989
+ (bySession.get(record2.sessionId) ?? []).filter((id) => id !== record2.id)
990
990
  );
991
991
  };
992
992
  const prune = (sessionId) => {
993
993
  const records = sortedRecords(sessionId);
994
994
  const overflow = Math.max(0, records.length - maxCheckpointsPerSession);
995
- for (const record of records.slice(0, overflow)) {
996
- deleteRecord(record);
995
+ for (const record2 of records.slice(0, overflow)) {
996
+ deleteRecord(record2);
997
997
  }
998
998
  };
999
999
  const getHead = async (sessionId) => {
1000
1000
  const headId = headBySession.get(sessionId);
1001
1001
  if (!headId) return null;
1002
- const record = byId.get(headId);
1003
- return record ? clone2(record) : null;
1002
+ const record2 = byId.get(headId);
1003
+ return record2 ? clone2(record2) : null;
1004
1004
  };
1005
1005
  return {
1006
1006
  async list(sessionId) {
1007
1007
  return sortedRecords(sessionId).map(toSummary2);
1008
1008
  },
1009
1009
  async get(id) {
1010
- const record = byId.get(id);
1011
- return record ? clone2(record) : null;
1010
+ const record2 = byId.get(id);
1011
+ return record2 ? clone2(record2) : null;
1012
1012
  },
1013
1013
  getHead,
1014
1014
  async append(args) {
1015
1015
  const previousHead = await getHead(args.sessionId);
1016
1016
  const turnIndex = previousHead ? previousHead.turnIndex + 1 : 0;
1017
1017
  const pendingRequests = args.pendingRequests ?? [];
1018
- const record = {
1018
+ const record2 = {
1019
1019
  id: createCheckpointId(),
1020
1020
  sessionId: args.sessionId,
1021
1021
  runId: args.runId,
@@ -1037,9 +1037,9 @@ function createInMemorySessionCheckpointStore(options) {
1037
1037
  ...args.workflowVersion ? { workflowVersion: args.workflowVersion } : {},
1038
1038
  ...args.buildId ? { buildId: args.buildId } : {}
1039
1039
  };
1040
- saveRecord(record);
1040
+ saveRecord(record2);
1041
1041
  prune(args.sessionId);
1042
- return clone2(record);
1042
+ return clone2(record2);
1043
1043
  },
1044
1044
  async rollbackTo(id) {
1045
1045
  const target = byId.get(id);
@@ -1052,18 +1052,18 @@ function createInMemorySessionCheckpointStore(options) {
1052
1052
  }
1053
1053
  const records = sortedRecords(target.sessionId);
1054
1054
  const trailing = records.filter(
1055
- (record) => record.turnIndex > target.turnIndex
1055
+ (record2) => record2.turnIndex > target.turnIndex
1056
1056
  );
1057
1057
  const invalidatedStructuredStreamIds = uniqueStrings2(
1058
- trailing.flatMap((record) => record.effects.structuredStreamIds)
1058
+ trailing.flatMap((record2) => record2.effects.structuredStreamIds)
1059
1059
  );
1060
1060
  const invalidatedInterruptTokens = uniqueStrings2(
1061
- trailing.flatMap((record) => [
1062
- ...record.effects.interruptTokens,
1063
- ...record.activePendingRequests.map((request) => request.token)
1061
+ trailing.flatMap((record2) => [
1062
+ ...record2.effects.interruptTokens,
1063
+ ...record2.activePendingRequests.map((request) => request.token)
1064
1064
  ])
1065
1065
  );
1066
- for (const record of trailing) deleteRecord(record);
1066
+ for (const record2 of trailing) deleteRecord(record2);
1067
1067
  headBySession.set(target.sessionId, target.id);
1068
1068
  return {
1069
1069
  sessionId: target.sessionId,
@@ -1221,10 +1221,11 @@ async function restoreGraphSnapshot(saver, snapshot, threadId) {
1221
1221
 
1222
1222
  // src/graph/create-execution-graph.ts
1223
1223
  var import_promises = require("timers/promises");
1224
- var import_core2 = require("@kortyx/core");
1224
+ var import_core3 = require("@kortyx/core");
1225
+ var import_errors8 = require("@kortyx/core/errors");
1225
1226
  var import_hooks2 = require("@kortyx/hooks");
1226
- var import_utils = require("@kortyx/utils");
1227
- var import_langgraph2 = require("@langchain/langgraph");
1227
+ var import_utils2 = require("@kortyx/utils");
1228
+ var import_langgraph3 = require("@langchain/langgraph");
1228
1229
 
1229
1230
  // src/node-loader.ts
1230
1231
  var import_node_path = require("path");
@@ -1312,25 +1313,10 @@ var import_errors6 = require("@kortyx/core/errors");
1312
1313
  var import_hooks = require("@kortyx/hooks");
1313
1314
  var import_singletons = require("@langchain/core/singletons");
1314
1315
  var import_langgraph = require("@langchain/langgraph");
1315
- function assertSequentialWorkflow(workflow) {
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 = {}) {
1316
+ function createWorkflowCallService(config, _parent, execution = {}) {
1330
1317
  return async (args) => {
1331
1318
  const execute = async () => {
1332
1319
  (0, import_core.throwIfExecutionAborted)(execution.abortSignal);
1333
- if (parent) assertSequentialWorkflow(parent);
1334
1320
  const depth = (config.workflowCallDepth ?? 0) + 1;
1335
1321
  if (depth > 16)
1336
1322
  throw new import_errors6.WorkflowContractError(
@@ -1350,7 +1336,6 @@ function createWorkflowCallService(config, parent, execution = {}) {
1350
1336
  "The typed workflow contract does not match the registered definition."
1351
1337
  );
1352
1338
  }
1353
- assertSequentialWorkflow(workflow);
1354
1339
  const snapshot = args.snapshot;
1355
1340
  if (snapshot && snapshot.version !== workflow.version)
1356
1341
  throw new import_errors6.WorkflowContractError(
@@ -1363,6 +1348,7 @@ function createWorkflowCallService(config, parent, execution = {}) {
1363
1348
  let request;
1364
1349
  const invocationPath = `${config.invocationPath ?? "root"}/${encodeURIComponent(args.id)}:${args.invocationId}`;
1365
1350
  let childConfig = {
1351
+ resume: Boolean(snapshot),
1366
1352
  executionBranchId: config.executionBranchId,
1367
1353
  prepareChildTelemetry: config.prepareChildTelemetry,
1368
1354
  context: config.context,
@@ -1557,34 +1543,504 @@ function createWorkflowCallService(config, parent, execution = {}) {
1557
1543
  };
1558
1544
  }
1559
1545
 
1546
+ // src/graph/parallel-graph.ts
1547
+ var import_core2 = require("@kortyx/core");
1548
+ var import_errors7 = require("@kortyx/core/errors");
1549
+ var import_utils = require("@kortyx/utils");
1550
+ var import_langgraph2 = require("@langchain/langgraph");
1551
+ var WORK = "__kortyx_parallel_work";
1552
+ var WAIT = "__kortyx_parallel_wait";
1553
+ var FINISH = "__kortyx_parallel_finish";
1554
+ var KEY = "__kortyxParallelGraph";
1555
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1556
+ var clone3 = (value) => JSON.parse(JSON.stringify(value));
1557
+ var zeroUsage = () => ({ input: 0, output: 0, total: 0 });
1558
+ function hasParallelEdges(workflow) {
1559
+ const groups = /* @__PURE__ */ new Map();
1560
+ for (const [from, , condition] of workflow.edges) {
1561
+ const group = groups.get(from) ?? { plain: 0, conditional: 0 };
1562
+ if (condition?.when) group.conditional++;
1563
+ else group.plain++;
1564
+ groups.set(from, group);
1565
+ if (group.plain > 1 || group.plain && group.conditional) return true;
1566
+ }
1567
+ return false;
1568
+ }
1569
+ function addParallelGraphCoordinator(builder, workflow, config, execution, executors) {
1570
+ const ids = [...Object.keys(workflow.nodes), "__end__"];
1571
+ const incoming = new Map(ids.map((id) => [id, []]));
1572
+ const outgoing = /* @__PURE__ */ new Map();
1573
+ for (const [from, to, condition] of workflow.edges) {
1574
+ if (from === "__end__" || to === "__start__" || from !== "__start__" && !Object.hasOwn(workflow.nodes, from) || !incoming.has(to))
1575
+ throw new import_errors7.WorkflowContractError(
1576
+ `Invalid parallel graph edge '${from}' \u2192 '${to}'.`
1577
+ );
1578
+ if (!incoming.get(to)?.includes(from)) incoming.get(to)?.push(from);
1579
+ const edges = outgoing.get(from) ?? [];
1580
+ if (!edges.some((edge) => edge.to === to && edge.when === condition?.when))
1581
+ edges.push({ to, ...condition ? { when: condition.when } : {} });
1582
+ outgoing.set(from, edges);
1583
+ }
1584
+ const ancestors = /* @__PURE__ */ new Map([["__start__", /* @__PURE__ */ new Set()]]);
1585
+ const remaining = new Set(ids);
1586
+ const ordered = [];
1587
+ while (remaining.size) {
1588
+ const ready = [...remaining].filter(
1589
+ (id) => incoming.get(id)?.every((from) => ancestors.has(from))
1590
+ );
1591
+ if (!ready.length)
1592
+ throw new import_errors7.KortyxError(
1593
+ "GRAPH_CYCLE",
1594
+ "Parallel graph back-edges are not supported: use a sequential child workflow for loops.",
1595
+ {
1596
+ category: "workflow",
1597
+ retryable: false,
1598
+ safeMessage: "Parallel graph back-edges are not supported: use a sequential child workflow for loops."
1599
+ }
1600
+ );
1601
+ for (const id of ready) {
1602
+ const parents = incoming.get(id) ?? [];
1603
+ ancestors.set(
1604
+ id,
1605
+ new Set(
1606
+ parents.flatMap((from) => [from, ...ancestors.get(from) ?? []])
1607
+ )
1608
+ );
1609
+ remaining.delete(id);
1610
+ ordered.push(id);
1611
+ }
1612
+ }
1613
+ ids.splice(0, ids.length, ...ordered);
1614
+ const failure = (code, message, node, cause) => new import_errors7.KortyxError(code, message, {
1615
+ category: "workflow",
1616
+ retryable: false,
1617
+ safeMessage: message,
1618
+ context: { workflow: workflow.id, node },
1619
+ ...cause ? { cause } : {}
1620
+ });
1621
+ const mergeFields = (parents, node, property, journal) => {
1622
+ const merged = {};
1623
+ for (const parent of parents) {
1624
+ for (const [key, field] of Object.entries(parent[property])) {
1625
+ const previous = Object.hasOwn(merged, key) ? merged[key] : void 0;
1626
+ if (previous && previous.writer !== field.writer) {
1627
+ if (journal.nodes[previous.writer]?.ancestors.includes(field.writer))
1628
+ continue;
1629
+ if (!journal.nodes[field.writer]?.ancestors.includes(previous.writer))
1630
+ throw failure(
1631
+ "GRAPH_OUTPUT_CONFLICT",
1632
+ `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.`,
1633
+ node
1634
+ );
1635
+ }
1636
+ Object.defineProperty(merged, key, {
1637
+ value: field,
1638
+ enumerable: true,
1639
+ configurable: true,
1640
+ writable: true
1641
+ });
1642
+ }
1643
+ }
1644
+ return merged;
1645
+ };
1646
+ const values = (fields) => Object.fromEntries(
1647
+ Object.entries(fields).map(([key, field]) => [key, field.value])
1648
+ );
1649
+ const initialWorkflowState = (journal) => {
1650
+ const internal = journal.initialRuntime.__kortyx;
1651
+ return record(internal) && record(internal.workflowState) ? internal.workflowState : {};
1652
+ };
1653
+ const usage = (journal) => {
1654
+ const total = {
1655
+ ...zeroUsage(),
1656
+ ...journal.initialRuntime.tokenUsage
1657
+ };
1658
+ for (const node of Object.values(journal.nodes)) {
1659
+ const used = node.runtime.tokenUsage;
1660
+ if (used)
1661
+ for (const key of ["input", "output", "total"])
1662
+ total[key] += used[key] ?? 0;
1663
+ }
1664
+ return total;
1665
+ };
1666
+ const runtimePatch = (journal) => ({
1667
+ [KEY]: journal,
1668
+ tokenUsage: usage(journal)
1669
+ });
1670
+ const load = (state) => {
1671
+ const saved = state.runtime[KEY];
1672
+ if (saved && (config.resume === true || !config.executionRunId || saved.runId === config.executionRunId)) {
1673
+ const journal = clone3(saved);
1674
+ if (journal.version !== 1 || journal.workflowVersion !== workflow.version)
1675
+ throw new import_errors7.WorkflowContractError(
1676
+ "Parallel workflow changed since its execution was suspended."
1677
+ );
1678
+ return journal;
1679
+ }
1680
+ const initialRuntime = clone3(state.runtime);
1681
+ delete initialRuntime[KEY];
1682
+ return {
1683
+ version: 1,
1684
+ runId: config.executionRunId,
1685
+ workflowVersion: workflow.version,
1686
+ initialInput: state.input,
1687
+ initialData: clone3(state.data ?? {}),
1688
+ initialRuntime,
1689
+ nodes: Object.fromEntries(
1690
+ ids.map((id) => [
1691
+ id,
1692
+ {
1693
+ id,
1694
+ ancestors: [],
1695
+ status: "pending",
1696
+ responses: [],
1697
+ runtime: {},
1698
+ fields: {},
1699
+ workflowFields: {},
1700
+ runtimeFields: {},
1701
+ selected: []
1702
+ }
1703
+ ])
1704
+ )
1705
+ };
1706
+ };
1707
+ const terminal = (node) => ["completed", "failed", "skipped"].includes(node.status);
1708
+ builder.addNode(WORK, async (state) => {
1709
+ const journal = load(state);
1710
+ let control;
1711
+ const running = /* @__PURE__ */ new Map();
1712
+ for (const node of Object.values(journal.nodes))
1713
+ if (node.status === "limited") node.status = "pending";
1714
+ const start = async (id, parents) => {
1715
+ const node = journal.nodes[id];
1716
+ let privateState;
1717
+ try {
1718
+ node.ancestors = [
1719
+ ...new Set(
1720
+ parents.flatMap((parent) => [parent.id, ...parent.ancestors])
1721
+ )
1722
+ ];
1723
+ const failed = parents.find((parent) => parent.status === "failed");
1724
+ if (failed)
1725
+ throw failure(
1726
+ "GRAPH_DEPENDENCY_FAILED",
1727
+ `Node '${id}' did not run because required dependency '${failed.id}' failed.`,
1728
+ id,
1729
+ failed.failure
1730
+ );
1731
+ node.fields = mergeFields(parents, id, "fields", journal);
1732
+ node.workflowFields = mergeFields(
1733
+ parents,
1734
+ id,
1735
+ "workflowFields",
1736
+ journal
1737
+ );
1738
+ node.runtimeFields = mergeFields(parents, id, "runtimeFields", journal);
1739
+ if (id === "__end__") {
1740
+ node.status = "completed";
1741
+ return;
1742
+ }
1743
+ const data = (0, import_utils.deepMergeWithArrayOverwrite)(
1744
+ journal.initialData,
1745
+ values(node.fields)
1746
+ );
1747
+ const nodeInput = Object.keys(node.fields).length ? (0, import_utils.deepMergeWithArrayOverwrite)(
1748
+ record(journal.initialInput) ? journal.initialInput : { rawInput: journal.initialInput },
1749
+ values(node.fields)
1750
+ ) : journal.initialInput;
1751
+ const workflowState = (0, import_utils.deepMergeWithArrayOverwrite)(
1752
+ initialWorkflowState(journal),
1753
+ values(node.workflowFields)
1754
+ );
1755
+ const ownInternal = node.runtime.__kortyx;
1756
+ const inheritedRuntime = (0, import_utils.deepMergeWithArrayOverwrite)(
1757
+ journal.initialRuntime,
1758
+ values(node.runtimeFields)
1759
+ );
1760
+ privateState = {
1761
+ ...state,
1762
+ input: nodeInput === void 0 ? void 0 : clone3(nodeInput),
1763
+ data: clone3(data),
1764
+ runtime: clone3({
1765
+ ...inheritedRuntime,
1766
+ tokenUsage: zeroUsage(),
1767
+ ...node.runtime,
1768
+ __kortyx: {
1769
+ ...record(ownInternal) ? ownInternal : {},
1770
+ workflowState: record(ownInternal) && record(ownInternal.workflowState) ? ownInternal.workflowState : workflowState
1771
+ }
1772
+ }),
1773
+ conversationHistory: [],
1774
+ awaitingHumanInput: false
1775
+ };
1776
+ let index = 0;
1777
+ node.status = "running";
1778
+ const updates = await executors[id](
1779
+ privateState,
1780
+ (request) => {
1781
+ const position = index++;
1782
+ if (position < node.responses.length)
1783
+ return node.responses[position];
1784
+ node.request = request;
1785
+ throw new import_langgraph2.GraphInterrupt([]);
1786
+ },
1787
+ (nodeState) => {
1788
+ const snapshot = clone3(journal);
1789
+ snapshot.nodes[id].runtime = clone3(
1790
+ nodeState.runtime
1791
+ );
1792
+ for (const activation of Object.values(snapshot.nodes))
1793
+ if (activation.status === "running")
1794
+ activation.status = "pending";
1795
+ return {
1796
+ ...state,
1797
+ lastNode: id,
1798
+ input: nodeState.input,
1799
+ data: nodeState.data ?? {},
1800
+ conversationHistory: [
1801
+ ...state.conversationHistory,
1802
+ ...ids.flatMap(
1803
+ (nodeId) => snapshot.nodes[nodeId]?.history ?? []
1804
+ )
1805
+ ],
1806
+ runtime: { ...nodeState.runtime, ...runtimePatch(snapshot) }
1807
+ };
1808
+ }
1809
+ );
1810
+ node.runtime = updates.runtime ?? privateState.runtime;
1811
+ const delta = updates.__kortyxNodeData;
1812
+ const updatedData = updates.data;
1813
+ for (const key of Object.keys(delta))
1814
+ Object.defineProperty(node.fields, key, {
1815
+ value: { writer: id, value: updatedData?.[key] },
1816
+ enumerable: true,
1817
+ configurable: true,
1818
+ writable: true
1819
+ });
1820
+ const internal = node.runtime.__kortyx;
1821
+ for (const [key, value] of Object.entries(node.runtime)) {
1822
+ if ([KEY, "__kortyx", "tokenUsage"].includes(key)) continue;
1823
+ if (JSON.stringify(value) !== JSON.stringify(inheritedRuntime[key]))
1824
+ Object.defineProperty(node.runtimeFields, key, {
1825
+ value: { writer: id, value },
1826
+ enumerable: true,
1827
+ configurable: true,
1828
+ writable: true
1829
+ });
1830
+ }
1831
+ const updatedWorkflow = record(internal) && record(internal.workflowState) ? internal.workflowState : {};
1832
+ for (const [key, value] of Object.entries(updatedWorkflow)) {
1833
+ if (JSON.stringify(value) !== JSON.stringify(workflowState[key]))
1834
+ Object.defineProperty(node.workflowFields, key, {
1835
+ value: { writer: id, value },
1836
+ enumerable: true,
1837
+ configurable: true,
1838
+ writable: true
1839
+ });
1840
+ }
1841
+ node.history = updates.conversationHistory ?? [];
1842
+ node.selected = (outgoing.get(id) ?? []).filter(
1843
+ (edge) => !edge.when || edge.when === (updates.lastCondition ?? updates.lastIntent)
1844
+ ).map((edge) => edge.to);
1845
+ node.status = "completed";
1846
+ delete node.request;
1847
+ } catch (error) {
1848
+ const patch = error?.__kortyxHookStatePatch;
1849
+ if (privateState) node.runtime = { ...privateState.runtime, ...patch };
1850
+ if ((0, import_langgraph2.isGraphInterrupt)(error)) node.status = "waiting";
1851
+ else if ((0, import_core2.isExecutionLimitReached)(error)) {
1852
+ node.status = "limited";
1853
+ control ??= error;
1854
+ } else if ((0, import_core2.isExecutionCancelled)(error) || execution.abortSignal?.aborted) {
1855
+ control ??= error;
1856
+ } else if ((0, import_errors7.serializeFailure)(error).category === "persistence")
1857
+ control ??= error;
1858
+ else {
1859
+ node.failure = (0, import_errors7.serializeFailure)(error);
1860
+ node.failure.context = {
1861
+ ...node.failure.context,
1862
+ node: id,
1863
+ workflow: workflow.id
1864
+ };
1865
+ node.status = "failed";
1866
+ node.selected = (outgoing.get(id) ?? []).map((edge) => edge.to);
1867
+ }
1868
+ }
1869
+ };
1870
+ const launch = () => {
1871
+ if (control || execution.abortSignal?.aborted) return;
1872
+ for (const id of ids) {
1873
+ if (id === "__end__") continue;
1874
+ const node = journal.nodes[id];
1875
+ if (node.status !== "pending" || running.has(id)) continue;
1876
+ const sources = incoming.get(id) ?? [];
1877
+ if (!sources.every(
1878
+ (from) => from === "__start__" || terminal(journal.nodes[from])
1879
+ ))
1880
+ continue;
1881
+ const activeSources = sources.filter(
1882
+ (from) => from === "__start__" ? (outgoing.get(from) ?? []).some(
1883
+ (edge) => edge.to === id && (!edge.when || edge.when === (state.lastCondition ?? state.lastIntent))
1884
+ ) : journal.nodes[from]?.selected.includes(id)
1885
+ );
1886
+ if (!activeSources.length) {
1887
+ node.status = "skipped";
1888
+ continue;
1889
+ }
1890
+ const task = start(
1891
+ id,
1892
+ activeSources.filter((from) => from !== "__start__").map((from) => journal.nodes[from])
1893
+ );
1894
+ running.set(
1895
+ id,
1896
+ task.finally(() => running.delete(id))
1897
+ );
1898
+ }
1899
+ };
1900
+ try {
1901
+ while (true) {
1902
+ launch();
1903
+ if (!running.size) break;
1904
+ await Promise.race(running.values());
1905
+ }
1906
+ (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
1907
+ if (control) throw control;
1908
+ } catch (error) {
1909
+ await Promise.allSettled(running.values());
1910
+ if (error && typeof error === "object")
1911
+ Object.assign(error, { __kortyxHookStatePatch: runtimePatch(journal) });
1912
+ throw error;
1913
+ }
1914
+ journal.waitingNode = ids.find(
1915
+ (id) => journal.nodes[id]?.status === "waiting"
1916
+ );
1917
+ if (!journal.waitingNode) {
1918
+ const leaves = ids.filter(
1919
+ (id) => id !== "__end__" && journal.nodes[id]?.status !== "skipped" && !journal.nodes[id]?.selected.some((to) => to !== "__end__")
1920
+ );
1921
+ await start(
1922
+ "__end__",
1923
+ leaves.map((id) => journal.nodes[id])
1924
+ );
1925
+ }
1926
+ const end = journal.nodes.__end__;
1927
+ return {
1928
+ runtime: {
1929
+ ...end.status === "completed" ? values(end.runtimeFields) : {},
1930
+ ...runtimePatch(journal)
1931
+ },
1932
+ ...end.status === "completed" ? {
1933
+ input: Object.keys(end.fields).length ? (0, import_utils.deepMergeWithArrayOverwrite)(
1934
+ record(journal.initialInput) ? journal.initialInput : { rawInput: journal.initialInput },
1935
+ values(end.fields)
1936
+ ) : journal.initialInput,
1937
+ data: (0, import_utils.deepMergeWithArrayOverwrite)(
1938
+ journal.initialData,
1939
+ values(end.fields)
1940
+ ),
1941
+ conversationHistory: ids.flatMap(
1942
+ (id) => journal.nodes[id]?.history ?? []
1943
+ )
1944
+ } : {}
1945
+ };
1946
+ });
1947
+ builder.addNode(WAIT, (state) => {
1948
+ const journal = load(state);
1949
+ const id = journal.waitingNode;
1950
+ const node = id ? journal.nodes[id] : void 0;
1951
+ if (!id || !node?.request)
1952
+ throw new import_errors7.WorkflowContractError("Missing waiting parallel node.");
1953
+ const request = {
1954
+ ...node.request,
1955
+ meta: {
1956
+ ...node.request.meta,
1957
+ __kortyxResumeStatePatch: runtimePatch(journal)
1958
+ }
1959
+ };
1960
+ let response;
1961
+ try {
1962
+ response = (0, import_langgraph2.interrupt)(request);
1963
+ } catch (error) {
1964
+ config.emit?.("interrupt", {
1965
+ node: id,
1966
+ workflow: workflow.id,
1967
+ input: request
1968
+ });
1969
+ throw error;
1970
+ }
1971
+ node.responses.push(response);
1972
+ node.status = "pending";
1973
+ delete journal.waitingNode;
1974
+ return { runtime: runtimePatch(journal) };
1975
+ });
1976
+ builder.addNode(FINISH, (state) => {
1977
+ const journal = load(state);
1978
+ const failed = ids.filter((id) => journal.nodes[id]?.status === "failed");
1979
+ if (failed.length) {
1980
+ const relevant = [...failed].reverse().find((id) => id !== "__end__") ?? failed[0];
1981
+ const detail = journal.nodes[relevant]?.failure;
1982
+ const error = (0, import_errors7.errorFromFailure)({
1983
+ ...detail,
1984
+ results: failed.map((id) => ({
1985
+ status: "rejected",
1986
+ failure: journal.nodes[id]?.failure
1987
+ }))
1988
+ });
1989
+ Object.assign(error, { __kortyxHookStatePatch: runtimePatch(journal) });
1990
+ throw error;
1991
+ }
1992
+ return {
1993
+ runtime: {
1994
+ [KEY]: null,
1995
+ __kortyx: {
1996
+ workflowState: (0, import_utils.deepMergeWithArrayOverwrite)(
1997
+ initialWorkflowState(journal),
1998
+ values(journal.nodes.__end__?.workflowFields ?? {})
1999
+ )
2000
+ }
2001
+ }
2002
+ };
2003
+ });
2004
+ builder.addEdge("__start__", WORK);
2005
+ builder.addConditionalEdges(
2006
+ WORK,
2007
+ (state) => load(state).waitingNode ? "wait" : "finish",
2008
+ { wait: WAIT, finish: FINISH }
2009
+ );
2010
+ builder.addEdge(WAIT, WORK);
2011
+ builder.addEdge(FINISH, "__end__");
2012
+ }
2013
+
1560
2014
  // src/graph/create-execution-graph.ts
1561
2015
  async function createExecutionGraph(workflow, config, execution = {}) {
1562
- const StateAnnotation = import_langgraph2.Annotation.Root({
1563
- currentWorkflow: import_langgraph2.Annotation,
1564
- input: import_langgraph2.Annotation,
1565
- output: import_langgraph2.Annotation,
1566
- lastNode: import_langgraph2.Annotation,
1567
- lastCondition: import_langgraph2.Annotation,
1568
- lastIntent: import_langgraph2.Annotation,
1569
- runtime: (0, import_langgraph2.Annotation)({
1570
- reducer: (l, r) => (0, import_utils.deepMergeWithArrayOverwrite)(l ?? {}, r ?? {}),
2016
+ const StateAnnotation = import_langgraph3.Annotation.Root({
2017
+ currentWorkflow: import_langgraph3.Annotation,
2018
+ input: import_langgraph3.Annotation,
2019
+ output: import_langgraph3.Annotation,
2020
+ lastNode: import_langgraph3.Annotation,
2021
+ lastCondition: import_langgraph3.Annotation,
2022
+ lastIntent: import_langgraph3.Annotation,
2023
+ runtime: (0, import_langgraph3.Annotation)({
2024
+ reducer: (l, r) => (0, import_utils2.deepMergeWithArrayOverwrite)(l ?? {}, r ?? {}),
1571
2025
  default: () => ({})
1572
2026
  }),
1573
- config: import_langgraph2.Annotation,
1574
- transitionTo: import_langgraph2.Annotation,
1575
- data: (0, import_langgraph2.Annotation)({
1576
- reducer: (l, r) => (0, import_utils.deepMergeWithArrayOverwrite)(l ?? {}, r ?? {}),
2027
+ config: import_langgraph3.Annotation,
2028
+ transitionTo: import_langgraph3.Annotation,
2029
+ data: (0, import_langgraph3.Annotation)({
2030
+ reducer: (l, r) => (0, import_utils2.deepMergeWithArrayOverwrite)(l ?? {}, r ?? {}),
1577
2031
  default: () => ({})
1578
2032
  }),
1579
- conversationHistory: (0, import_langgraph2.Annotation)({
2033
+ conversationHistory: (0, import_langgraph3.Annotation)({
1580
2034
  reducer: (l, r) => [...l ?? [], ...r ?? []],
1581
2035
  default: () => []
1582
2036
  }),
1583
- awaitingHumanInput: import_langgraph2.Annotation
2037
+ awaitingHumanInput: import_langgraph3.Annotation
1584
2038
  });
1585
- const builder = new import_langgraph2.StateGraph(StateAnnotation);
2039
+ const builder = new import_langgraph3.StateGraph(StateAnnotation);
1586
2040
  const edgeApi = builder;
1587
2041
  const workflowName = workflow.id;
2042
+ const parallelGraph = hasParallelEdges(workflow);
2043
+ const nodeExecutors = {};
1588
2044
  const runtimeConfig = config;
1589
2045
  if (!runtimeConfig.emit) {
1590
2046
  runtimeConfig.emit = () => {
@@ -1626,22 +2082,32 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1626
2082
  nodeDef.behavior ?? void 0
1627
2083
  );
1628
2084
  const behavior = nodeConfig.behavior ?? {};
1629
- builder.addNode(nodeId, async (state) => {
1630
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
2085
+ const executeNode = async (state, awaitInput, snapshotGraph) => {
2086
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1631
2087
  const emitRuntimeEvent = (event, payload) => {
1632
2088
  runtimeConfig.emit(event, payload);
1633
2089
  };
2090
+ if (awaitInput)
2091
+ emitRuntimeEvent("status", {
2092
+ node: nodeId,
2093
+ message: `Processing node: ${nodeId}`
2094
+ });
1634
2095
  let suspension;
1635
2096
  let deniedAdmission;
1636
2097
  const hookNodeContext = {
1637
- completeResponse: runtimeConfig.workflowCallDepth ? void 0 : execution.completeResponse,
2098
+ completeResponse: runtimeConfig.workflowCallDepth ? void 0 : execution.completeResponse ? async (options, nodeState) => {
2099
+ await execution.completeResponse?.(
2100
+ options,
2101
+ snapshotGraph ? snapshotGraph(nodeState) : nodeState
2102
+ );
2103
+ } : void 0,
1638
2104
  abortSignal: execution.abortSignal,
1639
2105
  consumeExecution: (limit) => {
1640
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
2106
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1641
2107
  if (suspension) throw suspension;
1642
2108
  try {
1643
2109
  if (execution.budget)
1644
- (0, import_core2.consumeExecutionBudget)(execution.budget, limit);
2110
+ (0, import_core3.consumeExecutionBudget)(execution.budget, limit);
1645
2111
  } catch (error) {
1646
2112
  deniedAdmission = error;
1647
2113
  throw error;
@@ -1669,8 +2135,8 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1669
2135
  config: nodeConfig,
1670
2136
  emit: emitRuntimeEvent,
1671
2137
  awaitInterrupt: (interruptConfig) => {
1672
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
1673
- if (execution.budget) (0, import_core2.assertExecutionBudget)(execution.budget);
2138
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
2139
+ if (execution.budget) (0, import_core3.assertExecutionBudget)(execution.budget);
1674
2140
  const { kind, question } = interruptConfig;
1675
2141
  const isMulti = kind === "multi-choice" || interruptConfig.kind !== "text" && interruptConfig.multiple === true;
1676
2142
  const sharedInterruptFields = {
@@ -1692,14 +2158,15 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1692
2158
  };
1693
2159
  let resumed;
1694
2160
  try {
1695
- resumed = (0, import_langgraph2.interrupt)(payload);
2161
+ resumed = awaitInput ? awaitInput(payload) : (0, import_langgraph3.interrupt)(payload);
1696
2162
  } catch (error) {
1697
2163
  suspension = error;
1698
- emitRuntimeEvent("interrupt", {
1699
- node: nodeId,
1700
- workflow: workflowName,
1701
- input: payload
1702
- });
2164
+ if (!awaitInput)
2165
+ emitRuntimeEvent("interrupt", {
2166
+ node: nodeId,
2167
+ workflow: workflowName,
2168
+ input: payload
2169
+ });
1703
2170
  throw error;
1704
2171
  }
1705
2172
  if (isMulti) {
@@ -1728,11 +2195,11 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1728
2195
  let retryHookRuntime = null;
1729
2196
  while (attempt < maxAttempts) {
1730
2197
  try {
1731
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
2198
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1732
2199
  attempt++;
1733
2200
  const attemptState = retryHookRuntime && typeof retryHookRuntime === "object" ? {
1734
2201
  ...state,
1735
- runtime: (0, import_utils.deepMergeWithArrayOverwrite)(
2202
+ runtime: (0, import_utils2.deepMergeWithArrayOverwrite)(
1736
2203
  state.runtime ?? {},
1737
2204
  retryHookRuntime
1738
2205
  )
@@ -1750,12 +2217,12 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1750
2217
  input: state.input,
1751
2218
  params: nodeParams ?? {}
1752
2219
  });
1753
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
2220
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1754
2221
  if (deniedAdmission) throw deniedAdmission;
1755
2222
  if (suspension) throw suspension;
1756
2223
  return result;
1757
2224
  } catch (error) {
1758
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
2225
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1759
2226
  if (deniedAdmission) throw deniedAdmission;
1760
2227
  throw error;
1761
2228
  }
@@ -1794,21 +2261,21 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1794
2261
  } catch (err) {
1795
2262
  const patch = err?.__kortyxHookStatePatch;
1796
2263
  if (patch && typeof patch === "object") {
1797
- state.runtime = (0, import_utils.deepMergeWithArrayOverwrite)(
2264
+ state.runtime = (0, import_utils2.deepMergeWithArrayOverwrite)(
1798
2265
  state.runtime ?? {},
1799
2266
  patch
1800
2267
  );
1801
- retryHookRuntime = (0, import_utils.deepMergeWithArrayOverwrite)(
2268
+ retryHookRuntime = (0, import_utils2.deepMergeWithArrayOverwrite)(
1802
2269
  retryHookRuntime ?? {},
1803
2270
  patch
1804
2271
  );
1805
2272
  }
1806
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
1807
- if ((0, import_core2.isExecutionLimitReached)(err)) {
2273
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
2274
+ if ((0, import_core3.isExecutionLimitReached)(err)) {
1808
2275
  Object.assign(err, { nodeId, workflowId: workflowName });
1809
2276
  throw err;
1810
2277
  }
1811
- if ((0, import_core2.isExecutionCancelled)(err) || (0, import_langgraph2.isGraphInterrupt)(err)) throw err;
2278
+ if ((0, import_core3.isExecutionCancelled)(err) || (0, import_langgraph3.isGraphInterrupt)(err)) throw err;
1812
2279
  const hasMore = attempt < maxAttempts;
1813
2280
  const delayMs = behavior.retry?.delayMs ?? 0;
1814
2281
  if (hasMore && delayMs > 0) {
@@ -1818,12 +2285,12 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1818
2285
  if (!hasMore) throw err;
1819
2286
  }
1820
2287
  }
1821
- (0, import_core2.throwIfExecutionAborted)(execution.abortSignal);
2288
+ (0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
1822
2289
  const res = nodeResult ?? {};
1823
2290
  if (hookRuntimeUpdates) {
1824
2291
  res.infra = {
1825
2292
  ...res.infra ?? {},
1826
- runtime: (0, import_utils.deepMergeWithArrayOverwrite)(
2293
+ runtime: (0, import_utils2.deepMergeWithArrayOverwrite)(
1827
2294
  (res.infra?.runtime ?? {}) || {},
1828
2295
  hookRuntimeUpdates
1829
2296
  )
@@ -1835,6 +2302,19 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1835
2302
  emitRuntimeEvent("message", { node: nodeId, content: uiMessage });
1836
2303
  }
1837
2304
  if (res.transitionTo) {
2305
+ if (awaitInput)
2306
+ throw Object.assign(
2307
+ new import_errors8.KortyxError(
2308
+ "GRAPH_BRANCH_HANDOFF",
2309
+ "transitionTo cannot hand off from a parallel branch; useWorkflow returns to its caller.",
2310
+ {
2311
+ category: "workflow",
2312
+ retryable: false,
2313
+ safeMessage: "transitionTo cannot hand off from a parallel branch; useWorkflow returns to its caller."
2314
+ }
2315
+ ),
2316
+ { __kortyxHookStatePatch: hookRuntimeUpdates }
2317
+ );
1838
2318
  emitRuntimeEvent("transition", {
1839
2319
  node: nodeId,
1840
2320
  workflow: workflowName,
@@ -1847,17 +2327,17 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1847
2327
  updates.lastCondition = res.condition;
1848
2328
  if (typeof res.intent === "string") updates.lastIntent = res.intent;
1849
2329
  if (res.data && typeof res.data === "object") {
1850
- updates.input = (0, import_utils.deepMergeWithArrayOverwrite)(
2330
+ updates.input = (0, import_utils2.deepMergeWithArrayOverwrite)(
1851
2331
  toRecordInput(state.input),
1852
2332
  res.data
1853
2333
  );
1854
- updates.data = (0, import_utils.deepMergeWithArrayOverwrite)(
2334
+ updates.data = (0, import_utils2.deepMergeWithArrayOverwrite)(
1855
2335
  state.data ?? {},
1856
2336
  res.data
1857
2337
  );
1858
2338
  }
1859
2339
  if (res.infra?.runtime && typeof res.infra.runtime === "object") {
1860
- updates.runtime = (0, import_utils.deepMergeWithArrayOverwrite)(
2340
+ updates.runtime = (0, import_utils2.deepMergeWithArrayOverwrite)(
1861
2341
  state.runtime ?? {},
1862
2342
  res.infra.runtime
1863
2343
  );
@@ -1865,7 +2345,7 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1865
2345
  if (res.ui) {
1866
2346
  updates.ui = {
1867
2347
  message: typeof res.ui.message === "string" ? res.ui.message : state.ui?.message ?? "",
1868
- structured: (0, import_utils.deepMergeWithArrayOverwrite)(
2348
+ structured: (0, import_utils2.deepMergeWithArrayOverwrite)(
1869
2349
  state.ui?.structured ?? {},
1870
2350
  res.ui.structured ?? {}
1871
2351
  )
@@ -1890,8 +2370,20 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1890
2370
  data: payload
1891
2371
  });
1892
2372
  }
2373
+ if (awaitInput) updates.__kortyxNodeData = res.data ?? {};
2374
+ if (awaitInput)
2375
+ emitRuntimeEvent("status", {
2376
+ node: nodeId,
2377
+ message: `\u2705 Completed node: ${nodeId}`
2378
+ });
1893
2379
  return updates;
2380
+ };
2381
+ Object.defineProperty(nodeExecutors, nodeId, {
2382
+ value: executeNode,
2383
+ enumerable: true
1894
2384
  });
2385
+ if (!parallelGraph)
2386
+ builder.addNode(nodeId, (state) => executeNode(state));
1895
2387
  }
1896
2388
  const condGroups = {};
1897
2389
  const plainEdges = [];
@@ -1904,26 +2396,36 @@ async function createExecutionGraph(workflow, config, execution = {}) {
1904
2396
  plainEdges.push([from, to]);
1905
2397
  }
1906
2398
  }
1907
- for (const [from, to] of plainEdges) {
1908
- edgeApi.addEdge(from, to);
1909
- }
1910
- for (const [from, rules] of Object.entries(condGroups)) {
1911
- const mapping = Object.fromEntries(
1912
- rules.map((r) => [r.when, r.to])
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
2399
+ if (parallelGraph) {
2400
+ addParallelGraphCoordinator(
2401
+ builder,
2402
+ workflow,
2403
+ runtimeConfig,
2404
+ execution,
2405
+ nodeExecutors
1926
2406
  );
2407
+ } else {
2408
+ for (const [from, to] of plainEdges) {
2409
+ edgeApi.addEdge(from, to);
2410
+ }
2411
+ for (const [from, rules] of Object.entries(condGroups)) {
2412
+ const mapping = Object.fromEntries(
2413
+ rules.map((r) => [r.when, r.to])
2414
+ );
2415
+ const fallbackKey = `__default__:${from}`;
2416
+ mapping[fallbackKey] = "__end__";
2417
+ edgeApi.addConditionalEdges(
2418
+ from,
2419
+ (s) => {
2420
+ const trigger = s.lastCondition ?? s.lastIntent;
2421
+ if (trigger && Object.hasOwn(mapping, trigger)) {
2422
+ return trigger;
2423
+ }
2424
+ return fallbackKey;
2425
+ },
2426
+ mapping
2427
+ );
2428
+ }
1927
2429
  }
1928
2430
  const sessionId = typeof config.session === "object" && config.session && "id" in config.session && typeof config.session.id === "string" ? config.session.id : "__default__";
1929
2431
  const checkpointer = config.checkpointer ?? getCheckpointer(sessionId);
@@ -1959,8 +2461,8 @@ function makeRequestId(prefix = "req") {
1959
2461
  // src/registry/file-registry.ts
1960
2462
  var import_promises2 = require("fs/promises");
1961
2463
  var import_node_path2 = require("path");
1962
- var import_core3 = require("@kortyx/core");
1963
- var import_errors7 = require("@kortyx/core/errors");
2464
+ var import_core4 = require("@kortyx/core");
2465
+ var import_errors9 = require("@kortyx/core/errors");
1964
2466
  var DEFAULT_EXTENSIONS = [
1965
2467
  ".workflow.ts",
1966
2468
  ".workflow.mts",
@@ -1992,7 +2494,7 @@ function createFileWorkflowRegistry(options) {
1992
2494
  const modulePath = (0, import_node_path2.join)(workflowsDir, file);
1993
2495
  if (isDeclarativeWorkflowFile(file)) {
1994
2496
  const raw = await (0, import_promises2.readFile)(modulePath, "utf8");
1995
- const workflow = (0, import_core3.loadWorkflow)(raw);
2497
+ const workflow = (0, import_core4.loadWorkflow)(raw);
1996
2498
  workflow.metadata = {
1997
2499
  ...workflow.metadata ?? {},
1998
2500
  __filePath: modulePath
@@ -2034,7 +2536,7 @@ function createFileWorkflowRegistry(options) {
2034
2536
  const fallback = selectOptions?.fallbackId ?? fallbackId;
2035
2537
  const selected = workflows[id] ?? (fallback ? workflows[fallback] : null);
2036
2538
  if (!selected) {
2037
- throw new import_errors7.KortyxError(
2539
+ throw new import_errors9.KortyxError(
2038
2540
  "UNKNOWN_WORKFLOW",
2039
2541
  `No workflow found for id "${id}" and no fallback "${fallback}" workflow available`,
2040
2542
  {
@@ -2059,7 +2561,7 @@ function createFileWorkflowRegistry(options) {
2059
2561
  }
2060
2562
 
2061
2563
  // src/registry/in-memory-registry.ts
2062
- var import_errors8 = require("@kortyx/core/errors");
2564
+ var import_errors10 = require("@kortyx/core/errors");
2063
2565
  function createInMemoryWorkflowRegistry(workflows, options) {
2064
2566
  const byId = /* @__PURE__ */ new Map();
2065
2567
  for (const wf of workflows) byId.set(wf.id, wf);
@@ -2075,7 +2577,7 @@ function createInMemoryWorkflowRegistry(workflows, options) {
2075
2577
  const fallback = selectOptions?.fallbackId ?? fallbackId;
2076
2578
  const wf = byId.get(id) ?? (fallback ? byId.get(fallback) : void 0);
2077
2579
  if (!wf) {
2078
- throw new import_errors8.KortyxError(
2580
+ throw new import_errors10.KortyxError(
2079
2581
  "UNKNOWN_WORKFLOW",
2080
2582
  `Workflow "${id}" not found${fallback ? ` (fallback "${fallback}" missing too)` : ""}.`,
2081
2583
  {