@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/CHANGELOG.md +7 -0
- package/dist/index.js +666 -164
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -205,9 +205,9 @@ function createInMemoryPendingRequestStore() {
|
|
|
205
205
|
},
|
|
206
206
|
async take(token) {
|
|
207
207
|
prune();
|
|
208
|
-
const
|
|
208
|
+
const record2 = store.get(token);
|
|
209
209
|
store.delete(token);
|
|
210
|
-
return
|
|
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 = (
|
|
765
|
-
id:
|
|
766
|
-
sessionId:
|
|
767
|
-
turnIndex:
|
|
768
|
-
createdAt:
|
|
769
|
-
nodes: [...
|
|
770
|
-
workflow:
|
|
771
|
-
...
|
|
772
|
-
...
|
|
773
|
-
...
|
|
774
|
-
...
|
|
775
|
-
...
|
|
776
|
-
...
|
|
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 (
|
|
788
|
-
const serialized = JSON.stringify(
|
|
787
|
+
const saveRecord = async (record2) => {
|
|
788
|
+
const serialized = JSON.stringify(record2);
|
|
789
789
|
await Promise.all([
|
|
790
|
-
store.set(byIdKey(
|
|
790
|
+
store.set(byIdKey(record2.id), serialized, ttlMs),
|
|
791
791
|
store.set(
|
|
792
|
-
sessionRecordKey(
|
|
792
|
+
sessionRecordKey(record2.sessionId, record2.id),
|
|
793
793
|
serialized,
|
|
794
794
|
ttlMs
|
|
795
795
|
),
|
|
796
|
-
store.set(headKey(
|
|
796
|
+
store.set(headKey(record2.sessionId), record2.id, ttlMs)
|
|
797
797
|
]);
|
|
798
798
|
};
|
|
799
|
-
const deleteRecord = async (
|
|
799
|
+
const deleteRecord = async (record2) => {
|
|
800
800
|
await Promise.all([
|
|
801
|
-
store.del(byIdKey(
|
|
802
|
-
store.del(sessionRecordKey(
|
|
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((
|
|
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
|
|
829
|
-
return
|
|
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
|
|
837
|
-
return
|
|
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
|
|
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(
|
|
865
|
+
await saveRecord(record2);
|
|
866
866
|
await prune(args.sessionId);
|
|
867
|
-
return clone(
|
|
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
|
-
(
|
|
879
|
+
(record2) => record2.turnIndex > target.turnIndex
|
|
880
880
|
);
|
|
881
881
|
const invalidatedStructuredStreamIds = uniqueStrings(
|
|
882
|
-
trailing.flatMap((
|
|
882
|
+
trailing.flatMap((record2) => record2.effects.structuredStreamIds)
|
|
883
883
|
);
|
|
884
884
|
const invalidatedInterruptTokens = uniqueStrings(
|
|
885
|
-
trailing.flatMap((
|
|
886
|
-
...
|
|
887
|
-
...
|
|
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 = (
|
|
951
|
-
id:
|
|
952
|
-
sessionId:
|
|
953
|
-
turnIndex:
|
|
954
|
-
createdAt:
|
|
955
|
-
nodes: [...
|
|
956
|
-
workflow:
|
|
957
|
-
...
|
|
958
|
-
...
|
|
959
|
-
...
|
|
960
|
-
...
|
|
961
|
-
...
|
|
962
|
-
...
|
|
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((
|
|
977
|
-
const saveRecord = (
|
|
978
|
-
byId.set(
|
|
979
|
-
const ids = bySession.get(
|
|
980
|
-
if (!ids.includes(
|
|
981
|
-
bySession.set(
|
|
982
|
-
headBySession.set(
|
|
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 = (
|
|
985
|
-
byId.delete(
|
|
984
|
+
const deleteRecord = (record2) => {
|
|
985
|
+
byId.delete(record2.id);
|
|
986
986
|
bySession.set(
|
|
987
|
-
|
|
987
|
+
record2.sessionId,
|
|
988
988
|
/* v8 ignore next -- deleteRecord only receives records that came from this session index. */
|
|
989
|
-
(bySession.get(
|
|
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
|
|
996
|
-
deleteRecord(
|
|
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
|
|
1003
|
-
return
|
|
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
|
|
1011
|
-
return
|
|
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
|
|
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(
|
|
1040
|
+
saveRecord(record2);
|
|
1041
1041
|
prune(args.sessionId);
|
|
1042
|
-
return clone2(
|
|
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
|
-
(
|
|
1055
|
+
(record2) => record2.turnIndex > target.turnIndex
|
|
1056
1056
|
);
|
|
1057
1057
|
const invalidatedStructuredStreamIds = uniqueStrings2(
|
|
1058
|
-
trailing.flatMap((
|
|
1058
|
+
trailing.flatMap((record2) => record2.effects.structuredStreamIds)
|
|
1059
1059
|
);
|
|
1060
1060
|
const invalidatedInterruptTokens = uniqueStrings2(
|
|
1061
|
-
trailing.flatMap((
|
|
1062
|
-
...
|
|
1063
|
-
...
|
|
1061
|
+
trailing.flatMap((record2) => [
|
|
1062
|
+
...record2.effects.interruptTokens,
|
|
1063
|
+
...record2.activePendingRequests.map((request) => request.token)
|
|
1064
1064
|
])
|
|
1065
1065
|
);
|
|
1066
|
-
for (const
|
|
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
|
|
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
|
|
1227
|
-
var
|
|
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
|
|
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 =
|
|
1563
|
-
currentWorkflow:
|
|
1564
|
-
input:
|
|
1565
|
-
output:
|
|
1566
|
-
lastNode:
|
|
1567
|
-
lastCondition:
|
|
1568
|
-
lastIntent:
|
|
1569
|
-
runtime: (0,
|
|
1570
|
-
reducer: (l, r) => (0,
|
|
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:
|
|
1574
|
-
transitionTo:
|
|
1575
|
-
data: (0,
|
|
1576
|
-
reducer: (l, r) => (0,
|
|
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,
|
|
2033
|
+
conversationHistory: (0, import_langgraph3.Annotation)({
|
|
1580
2034
|
reducer: (l, r) => [...l ?? [], ...r ?? []],
|
|
1581
2035
|
default: () => []
|
|
1582
2036
|
}),
|
|
1583
|
-
awaitingHumanInput:
|
|
2037
|
+
awaitingHumanInput: import_langgraph3.Annotation
|
|
1584
2038
|
});
|
|
1585
|
-
const builder = new
|
|
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
|
-
|
|
1630
|
-
(0,
|
|
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,
|
|
2106
|
+
(0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
|
|
1641
2107
|
if (suspension) throw suspension;
|
|
1642
2108
|
try {
|
|
1643
2109
|
if (execution.budget)
|
|
1644
|
-
(0,
|
|
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,
|
|
1673
|
-
if (execution.budget) (0,
|
|
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,
|
|
2161
|
+
resumed = awaitInput ? awaitInput(payload) : (0, import_langgraph3.interrupt)(payload);
|
|
1696
2162
|
} catch (error) {
|
|
1697
2163
|
suspension = error;
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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,
|
|
2198
|
+
(0, import_core3.throwIfExecutionAborted)(execution.abortSignal);
|
|
1732
2199
|
attempt++;
|
|
1733
2200
|
const attemptState = retryHookRuntime && typeof retryHookRuntime === "object" ? {
|
|
1734
2201
|
...state,
|
|
1735
|
-
runtime: (0,
|
|
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,
|
|
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,
|
|
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,
|
|
2264
|
+
state.runtime = (0, import_utils2.deepMergeWithArrayOverwrite)(
|
|
1798
2265
|
state.runtime ?? {},
|
|
1799
2266
|
patch
|
|
1800
2267
|
);
|
|
1801
|
-
retryHookRuntime = (0,
|
|
2268
|
+
retryHookRuntime = (0, import_utils2.deepMergeWithArrayOverwrite)(
|
|
1802
2269
|
retryHookRuntime ?? {},
|
|
1803
2270
|
patch
|
|
1804
2271
|
);
|
|
1805
2272
|
}
|
|
1806
|
-
(0,
|
|
1807
|
-
if ((0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
2330
|
+
updates.input = (0, import_utils2.deepMergeWithArrayOverwrite)(
|
|
1851
2331
|
toRecordInput(state.input),
|
|
1852
2332
|
res.data
|
|
1853
2333
|
);
|
|
1854
|
-
updates.data = (0,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
1963
|
-
var
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
2580
|
+
throw new import_errors10.KortyxError(
|
|
2079
2581
|
"UNKNOWN_WORKFLOW",
|
|
2080
2582
|
`Workflow "${id}" not found${fallback ? ` (fallback "${fallback}" missing too)` : ""}.`,
|
|
2081
2583
|
{
|