@kortyx/runtime 0.10.3 → 0.11.1
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 +16 -0
- package/LICENSE +51 -43
- package/dist/index.d.ts +75 -1
- package/dist/index.js +385 -19
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
createFrameworkAdapterFromEnv: () => createFrameworkAdapterFromEnv,
|
|
27
27
|
createInMemoryFrameworkAdapter: () => createInMemoryFrameworkAdapter,
|
|
28
28
|
createInMemoryPendingRequestStore: () => createInMemoryPendingRequestStore,
|
|
29
|
+
createInMemorySessionCheckpointStore: () => createInMemorySessionCheckpointStore,
|
|
29
30
|
createInMemoryWorkflowRegistry: () => createInMemoryWorkflowRegistry,
|
|
30
31
|
createRedisFrameworkAdapter: () => createRedisFrameworkAdapter,
|
|
31
32
|
getCheckpointer: () => getCheckpointer,
|
|
@@ -112,7 +113,6 @@ function createInMemoryCheckpointSaver(options) {
|
|
|
112
113
|
);
|
|
113
114
|
}
|
|
114
115
|
const ns = nsKey(threadId, checkpointNs);
|
|
115
|
-
const prevId = latest.get(ns);
|
|
116
116
|
const prepared = (0, import_langgraph_checkpoint.copyCheckpoint)(checkpoint);
|
|
117
117
|
const entry = {
|
|
118
118
|
checkpoint: prepared,
|
|
@@ -123,9 +123,6 @@ function createInMemoryCheckpointSaver(options) {
|
|
|
123
123
|
const id = checkpoint.id;
|
|
124
124
|
checkpoints.set(chkKey(threadId, checkpointNs, id), entry);
|
|
125
125
|
latest.set(ns, id);
|
|
126
|
-
if (prevId && prevId !== id) {
|
|
127
|
-
checkpoints.delete(chkKey(threadId, checkpointNs, prevId));
|
|
128
|
-
}
|
|
129
126
|
return {
|
|
130
127
|
configurable: {
|
|
131
128
|
thread_id: threadId,
|
|
@@ -159,6 +156,15 @@ function createInMemoryCheckpointSaver(options) {
|
|
|
159
156
|
for (const k of Array.from(checkpoints.keys())) {
|
|
160
157
|
if (k.startsWith(`${threadId}`)) checkpoints.delete(k);
|
|
161
158
|
}
|
|
159
|
+
},
|
|
160
|
+
async deleteCheckpointWrites(threadId, checkpointNs, checkpointId) {
|
|
161
|
+
const stored = checkpoints.get(
|
|
162
|
+
chkKey(threadId, checkpointNs, checkpointId)
|
|
163
|
+
);
|
|
164
|
+
stored?.writes.clear();
|
|
165
|
+
},
|
|
166
|
+
async getLatestCheckpointId(threadId, checkpointNs = "") {
|
|
167
|
+
return latest.get(nsKey(threadId, checkpointNs));
|
|
162
168
|
}
|
|
163
169
|
};
|
|
164
170
|
return saver;
|
|
@@ -335,7 +341,6 @@ function createRedisCheckpointSaver(options) {
|
|
|
335
341
|
'Failed to put checkpoint: missing "thread_id" in config.configurable.'
|
|
336
342
|
);
|
|
337
343
|
}
|
|
338
|
-
const prevId = await store.get(latestKey(threadId, checkpointNs));
|
|
339
344
|
const prepared = (0, import_langgraph_checkpoint2.copyCheckpoint)(checkpoint);
|
|
340
345
|
const [[, chkBytes], [, metaBytes]] = await Promise.all([
|
|
341
346
|
serde.dumpsTyped(prepared),
|
|
@@ -350,10 +355,6 @@ function createRedisCheckpointSaver(options) {
|
|
|
350
355
|
};
|
|
351
356
|
await store.set(key, JSON.stringify(stored), ttlMs);
|
|
352
357
|
await store.set(latestKey(threadId, checkpointNs), checkpointId, ttlMs);
|
|
353
|
-
if (prevId && prevId !== checkpointId) {
|
|
354
|
-
await store.del(checkpointKey(threadId, checkpointNs, prevId));
|
|
355
|
-
await store.del(writesKey(threadId, checkpointNs, prevId));
|
|
356
|
-
}
|
|
357
358
|
return {
|
|
358
359
|
configurable: {
|
|
359
360
|
thread_id: threadId,
|
|
@@ -402,6 +403,12 @@ function createRedisCheckpointSaver(options) {
|
|
|
402
403
|
const wrKeys = await store.scanKeys(`${prefix}wr:${threadId}:`);
|
|
403
404
|
const latestKeys = await store.scanKeys(`${prefix}latest:${threadId}:`);
|
|
404
405
|
await store.delRaw([...chkKeys, ...wrKeys, ...latestKeys]);
|
|
406
|
+
},
|
|
407
|
+
async deleteCheckpointWrites(threadId, checkpointNs, checkpointId) {
|
|
408
|
+
await store.del(writesKey(threadId, checkpointNs, checkpointId));
|
|
409
|
+
},
|
|
410
|
+
async getLatestCheckpointId(threadId, checkpointNs = "") {
|
|
411
|
+
return await store.get(latestKey(threadId, checkpointNs)) ?? void 0;
|
|
405
412
|
}
|
|
406
413
|
};
|
|
407
414
|
return saver;
|
|
@@ -498,7 +505,7 @@ function createRedisClient(options) {
|
|
|
498
505
|
const payload = encodeCommand(cmd, args);
|
|
499
506
|
return new Promise((resolve2, reject) => {
|
|
500
507
|
inflight.push({ resolve: resolve2, reject });
|
|
501
|
-
socket
|
|
508
|
+
socket?.write(payload);
|
|
502
509
|
});
|
|
503
510
|
};
|
|
504
511
|
const ensureConnected = async () => {
|
|
@@ -521,12 +528,12 @@ function createRedisClient(options) {
|
|
|
521
528
|
});
|
|
522
529
|
s.on("error", (err) => {
|
|
523
530
|
ready = false;
|
|
524
|
-
while (inflight.length) inflight.shift()
|
|
531
|
+
while (inflight.length) inflight.shift()?.reject(err);
|
|
525
532
|
});
|
|
526
533
|
s.on("close", () => {
|
|
527
534
|
ready = false;
|
|
528
535
|
while (inflight.length)
|
|
529
|
-
inflight.shift()
|
|
536
|
+
inflight.shift()?.reject(new Error("Redis socket closed"));
|
|
530
537
|
});
|
|
531
538
|
await new Promise((resolve2, reject) => {
|
|
532
539
|
const timer = setTimeout(
|
|
@@ -670,6 +677,346 @@ function createRedisFrameworkStore(options) {
|
|
|
670
677
|
};
|
|
671
678
|
}
|
|
672
679
|
|
|
680
|
+
// src/framework/redis/session-checkpoint-store.ts
|
|
681
|
+
var import_node_crypto = require("crypto");
|
|
682
|
+
var DEFAULT_MAX_CHECKPOINTS_PER_SESSION = 50;
|
|
683
|
+
var clone = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
|
|
684
|
+
var uniqueStrings = (values) => Array.from(
|
|
685
|
+
new Set(
|
|
686
|
+
Array.from(values).filter(
|
|
687
|
+
(value) => typeof value === "string" && value.length > 0
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
);
|
|
691
|
+
var createId = (prefix) => {
|
|
692
|
+
return `${prefix}-${(0, import_node_crypto.randomUUID)()}`;
|
|
693
|
+
};
|
|
694
|
+
var createResumeToken = () => (0, import_node_crypto.randomUUID)();
|
|
695
|
+
var toSummary = (record) => ({
|
|
696
|
+
id: record.id,
|
|
697
|
+
sessionId: record.sessionId,
|
|
698
|
+
turnIndex: record.turnIndex,
|
|
699
|
+
createdAt: record.createdAt,
|
|
700
|
+
nodes: [...record.nodes],
|
|
701
|
+
workflow: record.workflow,
|
|
702
|
+
...record.label ? { label: record.label } : {},
|
|
703
|
+
...record.parentCheckpointId ? { parentCheckpointId: record.parentCheckpointId } : {},
|
|
704
|
+
...record.parentSessionId ? { parentSessionId: record.parentSessionId } : {},
|
|
705
|
+
...record.forkedFrom ? { forkedFrom: record.forkedFrom } : {},
|
|
706
|
+
...record.workflowVersion ? { workflowVersion: record.workflowVersion } : {},
|
|
707
|
+
...record.buildId ? { buildId: record.buildId } : {}
|
|
708
|
+
});
|
|
709
|
+
function createRedisSessionCheckpointStore(options) {
|
|
710
|
+
const store = options.store;
|
|
711
|
+
const ttlMs = options.ttlMs;
|
|
712
|
+
const prefix = options.prefix ?? "kortyx:session-cp:";
|
|
713
|
+
const maxCheckpointsPerSession = options.maxCheckpointsPerSession ?? DEFAULT_MAX_CHECKPOINTS_PER_SESSION;
|
|
714
|
+
const byIdKey = (id) => `${prefix}by-id:${id}`;
|
|
715
|
+
const sessionRecordKey = (sessionId, id) => `${prefix}session:${sessionId}:${id}`;
|
|
716
|
+
const sessionPrefix = (sessionId) => `${prefix}session:${sessionId}:`;
|
|
717
|
+
const headKey = (sessionId) => `${prefix}head:${sessionId}`;
|
|
718
|
+
const saveRecord = async (record) => {
|
|
719
|
+
const serialized = JSON.stringify(record);
|
|
720
|
+
await Promise.all([
|
|
721
|
+
store.set(byIdKey(record.id), serialized, ttlMs),
|
|
722
|
+
store.set(
|
|
723
|
+
sessionRecordKey(record.sessionId, record.id),
|
|
724
|
+
serialized,
|
|
725
|
+
ttlMs
|
|
726
|
+
),
|
|
727
|
+
store.set(headKey(record.sessionId), record.id, ttlMs)
|
|
728
|
+
]);
|
|
729
|
+
};
|
|
730
|
+
const deleteRecord = async (record) => {
|
|
731
|
+
await Promise.all([
|
|
732
|
+
store.del(byIdKey(record.id)),
|
|
733
|
+
store.del(sessionRecordKey(record.sessionId, record.id))
|
|
734
|
+
]);
|
|
735
|
+
};
|
|
736
|
+
const getRecord = async (id) => {
|
|
737
|
+
const raw = await store.get(byIdKey(id));
|
|
738
|
+
return raw ? JSON.parse(raw) : null;
|
|
739
|
+
};
|
|
740
|
+
const sortedRecords = async (sessionId) => {
|
|
741
|
+
const keys = await store.scanKeys(sessionPrefix(sessionId));
|
|
742
|
+
const records = await Promise.all(
|
|
743
|
+
keys.map(async (rawKey) => {
|
|
744
|
+
const logicalKey = rawKey.includes(sessionPrefix(sessionId)) ? rawKey.slice(rawKey.indexOf(sessionPrefix(sessionId))) : rawKey;
|
|
745
|
+
const raw = await store.get(logicalKey);
|
|
746
|
+
return raw ? JSON.parse(raw) : null;
|
|
747
|
+
})
|
|
748
|
+
);
|
|
749
|
+
return records.filter((record) => Boolean(record)).sort((a, b) => a.turnIndex - b.turnIndex || a.createdAt - b.createdAt);
|
|
750
|
+
};
|
|
751
|
+
const prune = async (sessionId) => {
|
|
752
|
+
const records = await sortedRecords(sessionId);
|
|
753
|
+
const overflow = Math.max(0, records.length - maxCheckpointsPerSession);
|
|
754
|
+
await Promise.all(records.slice(0, overflow).map(deleteRecord));
|
|
755
|
+
};
|
|
756
|
+
const getHead = async (sessionId) => {
|
|
757
|
+
const headId = await store.get(headKey(sessionId));
|
|
758
|
+
if (!headId) return null;
|
|
759
|
+
const record = await getRecord(headId);
|
|
760
|
+
return record ? clone(record) : null;
|
|
761
|
+
};
|
|
762
|
+
return {
|
|
763
|
+
async list(sessionId) {
|
|
764
|
+
return (await sortedRecords(sessionId)).map(toSummary);
|
|
765
|
+
},
|
|
766
|
+
async get(id) {
|
|
767
|
+
const record = await getRecord(id);
|
|
768
|
+
return record ? clone(record) : null;
|
|
769
|
+
},
|
|
770
|
+
getHead,
|
|
771
|
+
async append(args) {
|
|
772
|
+
const previousHead = await getHead(args.sessionId);
|
|
773
|
+
const pendingRequests = args.pendingRequests ?? [];
|
|
774
|
+
const record = {
|
|
775
|
+
id: createId("cp"),
|
|
776
|
+
sessionId: args.sessionId,
|
|
777
|
+
runId: args.runId,
|
|
778
|
+
...args.graphCheckpointId ? { graphCheckpointId: args.graphCheckpointId } : {},
|
|
779
|
+
turnIndex: previousHead ? previousHead.turnIndex + 1 : 0,
|
|
780
|
+
createdAt: Date.now(),
|
|
781
|
+
workflow: args.workflow,
|
|
782
|
+
state: clone(args.state),
|
|
783
|
+
nodes: uniqueStrings(args.nodes ?? []),
|
|
784
|
+
effects: {
|
|
785
|
+
structuredStreamIds: uniqueStrings(args.structuredStreamIds ?? []),
|
|
786
|
+
interruptTokens: uniqueStrings(
|
|
787
|
+
pendingRequests.map((request) => request.token)
|
|
788
|
+
)
|
|
789
|
+
},
|
|
790
|
+
activePendingRequests: clone(pendingRequests),
|
|
791
|
+
...previousHead ? { parentCheckpointId: previousHead.id } : {},
|
|
792
|
+
...args.label ? { label: args.label } : {},
|
|
793
|
+
...args.workflowVersion ? { workflowVersion: args.workflowVersion } : {},
|
|
794
|
+
...args.buildId ? { buildId: args.buildId } : {}
|
|
795
|
+
};
|
|
796
|
+
await saveRecord(record);
|
|
797
|
+
await prune(args.sessionId);
|
|
798
|
+
return clone(record);
|
|
799
|
+
},
|
|
800
|
+
async rollbackTo(id) {
|
|
801
|
+
const target = await getRecord(id);
|
|
802
|
+
if (!target) throw new Error(`Checkpoint "${id}" not found.`);
|
|
803
|
+
const records = await sortedRecords(target.sessionId);
|
|
804
|
+
const trailing = records.filter(
|
|
805
|
+
(record) => record.turnIndex > target.turnIndex
|
|
806
|
+
);
|
|
807
|
+
const invalidatedStructuredStreamIds = uniqueStrings(
|
|
808
|
+
trailing.flatMap((record) => record.effects.structuredStreamIds)
|
|
809
|
+
);
|
|
810
|
+
const invalidatedInterruptTokens = uniqueStrings(
|
|
811
|
+
trailing.flatMap((record) => [
|
|
812
|
+
...record.effects.interruptTokens,
|
|
813
|
+
...record.activePendingRequests.map((request) => request.token)
|
|
814
|
+
])
|
|
815
|
+
);
|
|
816
|
+
await Promise.all(trailing.map(deleteRecord));
|
|
817
|
+
await store.set(headKey(target.sessionId), target.id, ttlMs);
|
|
818
|
+
return {
|
|
819
|
+
sessionId: target.sessionId,
|
|
820
|
+
head: target.id,
|
|
821
|
+
invalidatedStructuredStreamIds,
|
|
822
|
+
invalidatedInterruptTokens,
|
|
823
|
+
activePendingRequests: clone(target.activePendingRequests)
|
|
824
|
+
};
|
|
825
|
+
},
|
|
826
|
+
async fork(id, options2) {
|
|
827
|
+
const source = await getRecord(id);
|
|
828
|
+
if (!source) throw new Error(`Checkpoint "${id}" not found.`);
|
|
829
|
+
const sessionId = options2?.newSessionId || createId("session");
|
|
830
|
+
const { parentCheckpointId: _parentCheckpointId, ...sourceRecord } = clone(source);
|
|
831
|
+
const forked = {
|
|
832
|
+
...sourceRecord,
|
|
833
|
+
id: createId("cp"),
|
|
834
|
+
sessionId,
|
|
835
|
+
parentSessionId: source.sessionId,
|
|
836
|
+
forkedFrom: source.id,
|
|
837
|
+
activePendingRequests: clone(source.activePendingRequests).map(
|
|
838
|
+
(request) => ({
|
|
839
|
+
...request,
|
|
840
|
+
token: createResumeToken(),
|
|
841
|
+
requestId: createId("human"),
|
|
842
|
+
sessionId
|
|
843
|
+
})
|
|
844
|
+
)
|
|
845
|
+
};
|
|
846
|
+
await saveRecord(forked);
|
|
847
|
+
return {
|
|
848
|
+
sessionId,
|
|
849
|
+
parentSessionId: source.sessionId,
|
|
850
|
+
forkedFrom: source.id,
|
|
851
|
+
checkpoint: clone(forked)
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/framework/session-checkpoints.ts
|
|
858
|
+
var import_node_crypto2 = require("crypto");
|
|
859
|
+
var DEFAULT_MAX_CHECKPOINTS_PER_SESSION2 = 50;
|
|
860
|
+
var createCheckpointId = () => `cp-${(0, import_node_crypto2.randomUUID)()}`;
|
|
861
|
+
var createSessionId = () => `session-${(0, import_node_crypto2.randomUUID)()}`;
|
|
862
|
+
var createResumeToken2 = () => (0, import_node_crypto2.randomUUID)();
|
|
863
|
+
var createRequestId = () => `human-${(0, import_node_crypto2.randomUUID)()}`;
|
|
864
|
+
var clone2 = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
|
|
865
|
+
var toSummary2 = (record) => ({
|
|
866
|
+
id: record.id,
|
|
867
|
+
sessionId: record.sessionId,
|
|
868
|
+
turnIndex: record.turnIndex,
|
|
869
|
+
createdAt: record.createdAt,
|
|
870
|
+
nodes: [...record.nodes],
|
|
871
|
+
workflow: record.workflow,
|
|
872
|
+
...record.label ? { label: record.label } : {},
|
|
873
|
+
...record.parentCheckpointId ? { parentCheckpointId: record.parentCheckpointId } : {},
|
|
874
|
+
...record.parentSessionId ? { parentSessionId: record.parentSessionId } : {},
|
|
875
|
+
...record.forkedFrom ? { forkedFrom: record.forkedFrom } : {},
|
|
876
|
+
...record.workflowVersion ? { workflowVersion: record.workflowVersion } : {},
|
|
877
|
+
...record.buildId ? { buildId: record.buildId } : {}
|
|
878
|
+
});
|
|
879
|
+
var uniqueStrings2 = (values) => Array.from(
|
|
880
|
+
new Set(
|
|
881
|
+
Array.from(values).filter(
|
|
882
|
+
(value) => typeof value === "string" && value.length > 0
|
|
883
|
+
)
|
|
884
|
+
)
|
|
885
|
+
);
|
|
886
|
+
function createInMemorySessionCheckpointStore(options) {
|
|
887
|
+
const maxCheckpointsPerSession = options?.maxCheckpointsPerSession ?? DEFAULT_MAX_CHECKPOINTS_PER_SESSION2;
|
|
888
|
+
const byId = /* @__PURE__ */ new Map();
|
|
889
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
890
|
+
const headBySession = /* @__PURE__ */ new Map();
|
|
891
|
+
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);
|
|
892
|
+
const saveRecord = (record) => {
|
|
893
|
+
byId.set(record.id, clone2(record));
|
|
894
|
+
const ids = bySession.get(record.sessionId) ?? [];
|
|
895
|
+
if (!ids.includes(record.id)) ids.push(record.id);
|
|
896
|
+
bySession.set(record.sessionId, ids);
|
|
897
|
+
headBySession.set(record.sessionId, record.id);
|
|
898
|
+
};
|
|
899
|
+
const deleteRecord = (record) => {
|
|
900
|
+
byId.delete(record.id);
|
|
901
|
+
bySession.set(
|
|
902
|
+
record.sessionId,
|
|
903
|
+
/* v8 ignore next -- deleteRecord only receives records that came from this session index. */
|
|
904
|
+
(bySession.get(record.sessionId) ?? []).filter((id) => id !== record.id)
|
|
905
|
+
);
|
|
906
|
+
};
|
|
907
|
+
const prune = (sessionId) => {
|
|
908
|
+
const records = sortedRecords(sessionId);
|
|
909
|
+
const overflow = Math.max(0, records.length - maxCheckpointsPerSession);
|
|
910
|
+
for (const record of records.slice(0, overflow)) {
|
|
911
|
+
deleteRecord(record);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
const getHead = async (sessionId) => {
|
|
915
|
+
const headId = headBySession.get(sessionId);
|
|
916
|
+
if (!headId) return null;
|
|
917
|
+
const record = byId.get(headId);
|
|
918
|
+
return record ? clone2(record) : null;
|
|
919
|
+
};
|
|
920
|
+
return {
|
|
921
|
+
async list(sessionId) {
|
|
922
|
+
return sortedRecords(sessionId).map(toSummary2);
|
|
923
|
+
},
|
|
924
|
+
async get(id) {
|
|
925
|
+
const record = byId.get(id);
|
|
926
|
+
return record ? clone2(record) : null;
|
|
927
|
+
},
|
|
928
|
+
getHead,
|
|
929
|
+
async append(args) {
|
|
930
|
+
const previousHead = await getHead(args.sessionId);
|
|
931
|
+
const turnIndex = previousHead ? previousHead.turnIndex + 1 : 0;
|
|
932
|
+
const pendingRequests = args.pendingRequests ?? [];
|
|
933
|
+
const record = {
|
|
934
|
+
id: createCheckpointId(),
|
|
935
|
+
sessionId: args.sessionId,
|
|
936
|
+
runId: args.runId,
|
|
937
|
+
...args.graphCheckpointId ? { graphCheckpointId: args.graphCheckpointId } : {},
|
|
938
|
+
turnIndex,
|
|
939
|
+
createdAt: Date.now(),
|
|
940
|
+
workflow: args.workflow,
|
|
941
|
+
state: clone2(args.state),
|
|
942
|
+
nodes: uniqueStrings2(args.nodes ?? []),
|
|
943
|
+
effects: {
|
|
944
|
+
structuredStreamIds: uniqueStrings2(args.structuredStreamIds ?? []),
|
|
945
|
+
interruptTokens: uniqueStrings2(
|
|
946
|
+
pendingRequests.map((request) => request.token)
|
|
947
|
+
)
|
|
948
|
+
},
|
|
949
|
+
activePendingRequests: clone2(pendingRequests),
|
|
950
|
+
...previousHead ? { parentCheckpointId: previousHead.id } : {},
|
|
951
|
+
...args.label ? { label: args.label } : {},
|
|
952
|
+
...args.workflowVersion ? { workflowVersion: args.workflowVersion } : {},
|
|
953
|
+
...args.buildId ? { buildId: args.buildId } : {}
|
|
954
|
+
};
|
|
955
|
+
saveRecord(record);
|
|
956
|
+
prune(args.sessionId);
|
|
957
|
+
return clone2(record);
|
|
958
|
+
},
|
|
959
|
+
async rollbackTo(id) {
|
|
960
|
+
const target = byId.get(id);
|
|
961
|
+
if (!target) {
|
|
962
|
+
throw new Error(`Checkpoint "${id}" not found.`);
|
|
963
|
+
}
|
|
964
|
+
const records = sortedRecords(target.sessionId);
|
|
965
|
+
const trailing = records.filter(
|
|
966
|
+
(record) => record.turnIndex > target.turnIndex
|
|
967
|
+
);
|
|
968
|
+
const invalidatedStructuredStreamIds = uniqueStrings2(
|
|
969
|
+
trailing.flatMap((record) => record.effects.structuredStreamIds)
|
|
970
|
+
);
|
|
971
|
+
const invalidatedInterruptTokens = uniqueStrings2(
|
|
972
|
+
trailing.flatMap((record) => [
|
|
973
|
+
...record.effects.interruptTokens,
|
|
974
|
+
...record.activePendingRequests.map((request) => request.token)
|
|
975
|
+
])
|
|
976
|
+
);
|
|
977
|
+
for (const record of trailing) deleteRecord(record);
|
|
978
|
+
headBySession.set(target.sessionId, target.id);
|
|
979
|
+
return {
|
|
980
|
+
sessionId: target.sessionId,
|
|
981
|
+
head: target.id,
|
|
982
|
+
invalidatedStructuredStreamIds,
|
|
983
|
+
invalidatedInterruptTokens,
|
|
984
|
+
activePendingRequests: clone2(target.activePendingRequests)
|
|
985
|
+
};
|
|
986
|
+
},
|
|
987
|
+
async fork(id, options2) {
|
|
988
|
+
const source = byId.get(id);
|
|
989
|
+
if (!source) {
|
|
990
|
+
throw new Error(`Checkpoint "${id}" not found.`);
|
|
991
|
+
}
|
|
992
|
+
const sessionId = options2?.newSessionId || createSessionId();
|
|
993
|
+
const { parentCheckpointId: _parentCheckpointId, ...sourceRecord } = clone2(source);
|
|
994
|
+
const forked = {
|
|
995
|
+
...sourceRecord,
|
|
996
|
+
id: createCheckpointId(),
|
|
997
|
+
sessionId,
|
|
998
|
+
parentSessionId: source.sessionId,
|
|
999
|
+
forkedFrom: source.id,
|
|
1000
|
+
activePendingRequests: clone2(source.activePendingRequests).map(
|
|
1001
|
+
(request) => ({
|
|
1002
|
+
...request,
|
|
1003
|
+
token: createResumeToken2(),
|
|
1004
|
+
requestId: createRequestId(),
|
|
1005
|
+
sessionId
|
|
1006
|
+
})
|
|
1007
|
+
)
|
|
1008
|
+
};
|
|
1009
|
+
saveRecord(forked);
|
|
1010
|
+
return {
|
|
1011
|
+
sessionId,
|
|
1012
|
+
parentSessionId: source.sessionId,
|
|
1013
|
+
forkedFrom: source.id,
|
|
1014
|
+
checkpoint: clone2(forked)
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
673
1020
|
// src/framework/adapter.ts
|
|
674
1021
|
function createInMemoryFrameworkAdapter(options) {
|
|
675
1022
|
const ttlMs = options?.ttlMs ?? 15 * 60 * 1e3;
|
|
@@ -678,6 +1025,9 @@ function createInMemoryFrameworkAdapter(options) {
|
|
|
678
1025
|
kind: "in-memory",
|
|
679
1026
|
ttlMs,
|
|
680
1027
|
pendingRequests: createInMemoryPendingRequestStore(),
|
|
1028
|
+
sessionCheckpoints: createInMemorySessionCheckpointStore({
|
|
1029
|
+
...options?.maxSessionCheckpoints !== void 0 ? { maxCheckpointsPerSession: options.maxSessionCheckpoints } : {}
|
|
1030
|
+
}),
|
|
681
1031
|
checkpointer,
|
|
682
1032
|
cleanupRun: async (runId) => {
|
|
683
1033
|
try {
|
|
@@ -701,6 +1051,12 @@ function createRedisFrameworkAdapter(options) {
|
|
|
701
1051
|
store,
|
|
702
1052
|
prefix: "kortyx:pending:"
|
|
703
1053
|
}),
|
|
1054
|
+
sessionCheckpoints: createRedisSessionCheckpointStore({
|
|
1055
|
+
store,
|
|
1056
|
+
ttlMs,
|
|
1057
|
+
prefix: "kortyx:session-cp:",
|
|
1058
|
+
...options.maxSessionCheckpoints !== void 0 ? { maxCheckpointsPerSession: options.maxSessionCheckpoints } : {}
|
|
1059
|
+
}),
|
|
704
1060
|
checkpointer: createRedisCheckpointSaver({
|
|
705
1061
|
store,
|
|
706
1062
|
ttlMs,
|
|
@@ -798,7 +1154,7 @@ function pickNodeExport(mod, exportName) {
|
|
|
798
1154
|
const fnExports = Object.entries(mod).filter(
|
|
799
1155
|
([, v]) => typeof v === "function"
|
|
800
1156
|
);
|
|
801
|
-
if (fnExports.length === 1) return fnExports[0][1];
|
|
1157
|
+
if (fnExports.length === 1) return fnExports[0]?.[1];
|
|
802
1158
|
const names = fnExports.map(([k]) => k).sort();
|
|
803
1159
|
throw new Error(
|
|
804
1160
|
`Node module must export a default function or exactly one named function (found: ${names.join(", ") || "none"}).`
|
|
@@ -852,6 +1208,7 @@ async function createExecutionGraph(workflow, config) {
|
|
|
852
1208
|
}
|
|
853
1209
|
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
854
1210
|
const traceAdapter = () => runtimeConfig.telemetry?.trace ?? runtimeConfig.reasonTrace;
|
|
1211
|
+
const telemetryCorrelation = () => runtimeConfig.telemetry?.correlation;
|
|
855
1212
|
const configContext = () => isRecord(runtimeConfig.context) ? runtimeConfig.context : void 0;
|
|
856
1213
|
const toRecordInput = (value) => {
|
|
857
1214
|
if (isRecord(value)) return value;
|
|
@@ -975,7 +1332,13 @@ async function createExecutionGraph(workflow, config) {
|
|
|
975
1332
|
const spanArgs = {
|
|
976
1333
|
name: "kortyx.node",
|
|
977
1334
|
attributes: {
|
|
1335
|
+
...typeof telemetryCorrelation()?.runId === "string" ? { runId: telemetryCorrelation()?.runId } : {},
|
|
1336
|
+
...typeof telemetryCorrelation()?.sessionId === "string" ? { sessionId: telemetryCorrelation()?.sessionId } : {},
|
|
978
1337
|
workflowId: workflowName,
|
|
1338
|
+
...typeof telemetryCorrelation()?.workflowRevisionId === "string" ? {
|
|
1339
|
+
workflowRevisionId: telemetryCorrelation()?.workflowRevisionId
|
|
1340
|
+
} : {},
|
|
1341
|
+
...typeof telemetryCorrelation()?.topologyHash === "string" ? { topologyHash: telemetryCorrelation()?.topologyHash } : {},
|
|
979
1342
|
nodeId,
|
|
980
1343
|
attempt,
|
|
981
1344
|
...typeof context?.userId === "string" ? { userId: context.userId } : {},
|
|
@@ -1032,6 +1395,8 @@ async function createExecutionGraph(workflow, config) {
|
|
|
1032
1395
|
}
|
|
1033
1396
|
if (res.transitionTo) {
|
|
1034
1397
|
emitRuntimeEvent("transition", {
|
|
1398
|
+
node: nodeId,
|
|
1399
|
+
workflow: workflowName,
|
|
1035
1400
|
transitionTo: res.transitionTo,
|
|
1036
1401
|
payload: res.data ?? {}
|
|
1037
1402
|
});
|
|
@@ -1141,18 +1506,18 @@ async function createExecutionGraph(workflow, config) {
|
|
|
1141
1506
|
}
|
|
1142
1507
|
|
|
1143
1508
|
// src/interrupt/tokens.ts
|
|
1144
|
-
var
|
|
1509
|
+
var import_node_crypto3 = require("crypto");
|
|
1145
1510
|
function makeResumeToken() {
|
|
1146
|
-
return (0,
|
|
1511
|
+
return (0, import_node_crypto3.randomUUID)();
|
|
1147
1512
|
}
|
|
1148
1513
|
function makeRequestId(prefix = "req") {
|
|
1149
|
-
return `${prefix}-${(0,
|
|
1514
|
+
return `${prefix}-${(0, import_node_crypto3.randomUUID)()}`;
|
|
1150
1515
|
}
|
|
1151
1516
|
|
|
1152
1517
|
// src/registry/file-registry.ts
|
|
1153
|
-
var import_core = require("@kortyx/core");
|
|
1154
1518
|
var import_promises = require("fs/promises");
|
|
1155
|
-
var
|
|
1519
|
+
var import_node_path2 = require("path");
|
|
1520
|
+
var import_core = require("@kortyx/core");
|
|
1156
1521
|
var DEFAULT_EXTENSIONS = [
|
|
1157
1522
|
".workflow.ts",
|
|
1158
1523
|
".workflow.mts",
|
|
@@ -1181,7 +1546,7 @@ function createFileWorkflowRegistry(options) {
|
|
|
1181
1546
|
const workflows = {};
|
|
1182
1547
|
for (const file of workflowFiles) {
|
|
1183
1548
|
try {
|
|
1184
|
-
const modulePath = (0,
|
|
1549
|
+
const modulePath = (0, import_node_path2.join)(workflowsDir, file);
|
|
1185
1550
|
if (isDeclarativeWorkflowFile(file)) {
|
|
1186
1551
|
const raw = await (0, import_promises.readFile)(modulePath, "utf8");
|
|
1187
1552
|
const workflow = (0, import_core.loadWorkflow)(raw);
|
|
@@ -1301,6 +1666,7 @@ async function buildInitialGraphState({
|
|
|
1301
1666
|
createFrameworkAdapterFromEnv,
|
|
1302
1667
|
createInMemoryFrameworkAdapter,
|
|
1303
1668
|
createInMemoryPendingRequestStore,
|
|
1669
|
+
createInMemorySessionCheckpointStore,
|
|
1304
1670
|
createInMemoryWorkflowRegistry,
|
|
1305
1671
|
createRedisFrameworkAdapter,
|
|
1306
1672
|
getCheckpointer,
|