@kortyx/agent 0.20.1 → 0.21.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 +16 -0
- package/dist/index.js +216 -17
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.21.0](https://github.com/kortyx-io/kortyx/compare/agent-v0.20.1...agent-v0.21.0) (2026-09-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **hooks:** support durable parallel child workflows ([#171](https://github.com/kortyx-io/kortyx/issues/171)) ([fdf2d56](https://github.com/kortyx-io/kortyx/commit/fdf2d564d88e921988e8f3e20da9fe0c30bbcdf9))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @kortyx/runtime bumped to 0.16.0
|
|
16
|
+
* devDependencies
|
|
17
|
+
* @kortyx/hooks bumped to 0.24.0
|
|
18
|
+
|
|
3
19
|
## [0.20.1](https://github.com/kortyx-io/kortyx/compare/agent-v0.20.0...agent-v0.20.1) (2026-09-11)
|
|
4
20
|
|
|
5
21
|
|
package/dist/index.js
CHANGED
|
@@ -51,10 +51,12 @@ var require_dist = __commonJS({
|
|
|
51
51
|
var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
|
|
52
52
|
var index_exports2 = {};
|
|
53
53
|
__export2(index_exports2, {
|
|
54
|
+
ParallelError: () => ParallelError,
|
|
54
55
|
WorkflowCallError: () => WorkflowCallError,
|
|
55
56
|
completeResponse: () => completeResponse,
|
|
56
57
|
createWorkflowHooks: () => createWorkflowHooks,
|
|
57
58
|
emitWorkflowCall: () => emitWorkflowCall2,
|
|
59
|
+
parallel: () => parallel,
|
|
58
60
|
runWithHookContext: () => runWithHookContext,
|
|
59
61
|
useAbortSignal: () => useAbortSignal,
|
|
60
62
|
useInterrupt: () => useInterrupt,
|
|
@@ -118,6 +120,11 @@ var require_dist = __commonJS({
|
|
|
118
120
|
...ctx,
|
|
119
121
|
workflowCallActive: false,
|
|
120
122
|
workflowCallIds: /* @__PURE__ */ new Set(),
|
|
123
|
+
workflowTasks: [],
|
|
124
|
+
parallelJoins: [],
|
|
125
|
+
parallelGroup: void 0,
|
|
126
|
+
parallelIndex: 0,
|
|
127
|
+
workflowContextClosed: false,
|
|
121
128
|
nodeStateIndex: 0,
|
|
122
129
|
reasonCallIndex: 0,
|
|
123
130
|
currentNodeState,
|
|
@@ -189,10 +196,29 @@ var require_dist = __commonJS({
|
|
|
189
196
|
async function runWithHookContext(ctx, fn) {
|
|
190
197
|
const internal = createInternalContext(ctx);
|
|
191
198
|
try {
|
|
192
|
-
const result = await storage.run(internal,
|
|
199
|
+
const result = await storage.run(internal, async () => {
|
|
200
|
+
const drain = async () => {
|
|
201
|
+
let drained = -1;
|
|
202
|
+
while (drained !== internal.parallelJoins.length + internal.workflowTasks.length) {
|
|
203
|
+
drained = internal.parallelJoins.length + internal.workflowTasks.length;
|
|
204
|
+
await Promise.allSettled(internal.parallelJoins);
|
|
205
|
+
await Promise.all(internal.workflowTasks.map((task) => task.done));
|
|
206
|
+
}
|
|
207
|
+
internal.workflowContextClosed = true;
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
const result2 = await fn();
|
|
211
|
+
await drain();
|
|
212
|
+
if (internal.workflowControlError) throw internal.workflowControlError;
|
|
213
|
+
return result2;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
await drain();
|
|
216
|
+
throw internal.workflowControlError ?? error;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
193
219
|
cleanupCompletedReasonCheckpoints(internal);
|
|
194
220
|
for (const key of Object.keys(internal.currentNodeState.byKey)) {
|
|
195
|
-
if (key.startsWith("__useWorkflow:") || key === "__useWorkflowActivation") {
|
|
221
|
+
if (key.startsWith("__useWorkflow:") || key.startsWith("__parallel:") || key === "__useWorkflowActivation") {
|
|
196
222
|
internal.currentNodeState.byKey[key] = null;
|
|
197
223
|
internal.stateDirty = true;
|
|
198
224
|
}
|
|
@@ -2647,8 +2673,158 @@ ${tryStringify2(args.response)}`
|
|
|
2647
2673
|
};
|
|
2648
2674
|
return [getValue(), setValue];
|
|
2649
2675
|
}
|
|
2650
|
-
var import_node_crypto5 = require("crypto");
|
|
2651
2676
|
var import_core62 = require("@kortyx/core");
|
|
2677
|
+
var ParallelChildWaiting = class extends Error {
|
|
2678
|
+
};
|
|
2679
|
+
var ParallelError = class extends AggregateError {
|
|
2680
|
+
constructor(results) {
|
|
2681
|
+
super(
|
|
2682
|
+
results.flatMap(
|
|
2683
|
+
(result) => result.status === "rejected" ? [result.reason] : []
|
|
2684
|
+
),
|
|
2685
|
+
"One or more parallel operations failed."
|
|
2686
|
+
);
|
|
2687
|
+
this.results = results;
|
|
2688
|
+
this.name = "ParallelError";
|
|
2689
|
+
}
|
|
2690
|
+
results;
|
|
2691
|
+
};
|
|
2692
|
+
function registerWorkflowTask(id) {
|
|
2693
|
+
const ctx = getHookContext();
|
|
2694
|
+
if (ctx.workflowControlError) throw ctx.workflowControlError;
|
|
2695
|
+
if (ctx.workflowContextClosed)
|
|
2696
|
+
throw new Error("The workflow node activation has finished.");
|
|
2697
|
+
const group = ctx.parallelGroup;
|
|
2698
|
+
if (group?.restored && !group.record.calls.includes(id)) {
|
|
2699
|
+
throw new Error(`Parallel call membership changed during replay: '${id}'.`);
|
|
2700
|
+
}
|
|
2701
|
+
let finish;
|
|
2702
|
+
const task = {
|
|
2703
|
+
id,
|
|
2704
|
+
...group ? { group } : {},
|
|
2705
|
+
completed: false,
|
|
2706
|
+
done: new Promise((resolve2) => {
|
|
2707
|
+
finish = resolve2;
|
|
2708
|
+
}),
|
|
2709
|
+
finish: () => {
|
|
2710
|
+
task.completed = true;
|
|
2711
|
+
finish();
|
|
2712
|
+
}
|
|
2713
|
+
};
|
|
2714
|
+
ctx.workflowTasks.push(task);
|
|
2715
|
+
if (group && !group.record.calls.includes(id)) group.record.calls.push(id);
|
|
2716
|
+
return task;
|
|
2717
|
+
}
|
|
2718
|
+
function parallel(values) {
|
|
2719
|
+
const settled = Promise.allSettled(values);
|
|
2720
|
+
let ctx;
|
|
2721
|
+
try {
|
|
2722
|
+
ctx = getHookContext();
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
return Promise.reject(error);
|
|
2725
|
+
}
|
|
2726
|
+
const join = async () => {
|
|
2727
|
+
if (ctx.workflowControlError) throw ctx.workflowControlError;
|
|
2728
|
+
if (ctx.parallelGroup)
|
|
2729
|
+
throw new Error(
|
|
2730
|
+
"Await each parallel group before starting another in the same node."
|
|
2731
|
+
);
|
|
2732
|
+
const key = `__parallel:${ctx.parallelIndex++}`;
|
|
2733
|
+
const saved = ctx.currentNodeState.byKey[key];
|
|
2734
|
+
const tasks = ctx.workflowTasks.filter(
|
|
2735
|
+
(task) => !task.completed && !task.group
|
|
2736
|
+
);
|
|
2737
|
+
const ids = tasks.map((task) => task.id);
|
|
2738
|
+
if (saved && (saved.version !== 1 || ids.some((id) => !saved.calls.includes(id)))) {
|
|
2739
|
+
throw new Error("Parallel call membership changed during replay.");
|
|
2740
|
+
}
|
|
2741
|
+
const group = {
|
|
2742
|
+
key,
|
|
2743
|
+
record: saved ?? { version: 1, calls: ids, interrupts: [] },
|
|
2744
|
+
restored: Boolean(saved),
|
|
2745
|
+
ready: Promise.resolve()
|
|
2746
|
+
};
|
|
2747
|
+
ctx.currentNodeState.byKey[key] = group.record;
|
|
2748
|
+
ctx.stateDirty = true;
|
|
2749
|
+
ctx.parallelGroup = group;
|
|
2750
|
+
for (const task of tasks) task.group = group;
|
|
2751
|
+
const callRecord = (id) => ctx.currentNodeState.byKey[`__useWorkflow:${id}`];
|
|
2752
|
+
const bridge = async (callId, index) => {
|
|
2753
|
+
const interruption = callRecord(callId)?.interrupts[index];
|
|
2754
|
+
if (!interruption)
|
|
2755
|
+
throw new Error(
|
|
2756
|
+
"Parallel interrupt history does not match the saved child."
|
|
2757
|
+
);
|
|
2758
|
+
try {
|
|
2759
|
+
interruption.response = await awaitInterruptInternal({
|
|
2760
|
+
request: interruption.request,
|
|
2761
|
+
...interruption.request.meta ? { meta: interruption.request.meta } : {}
|
|
2762
|
+
});
|
|
2763
|
+
} catch (error) {
|
|
2764
|
+
ctx.workflowControlError = error;
|
|
2765
|
+
throw error;
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
2768
|
+
group.ready = (async () => {
|
|
2769
|
+
for (const entry of group.record.interrupts)
|
|
2770
|
+
await bridge(entry.callId, entry.index);
|
|
2771
|
+
})();
|
|
2772
|
+
try {
|
|
2773
|
+
const [ready, outcomes] = await Promise.all([
|
|
2774
|
+
Promise.allSettled([group.ready]),
|
|
2775
|
+
settled
|
|
2776
|
+
]);
|
|
2777
|
+
let drained = -1;
|
|
2778
|
+
while (drained !== ctx.workflowTasks.length) {
|
|
2779
|
+
drained = ctx.workflowTasks.length;
|
|
2780
|
+
await Promise.all(
|
|
2781
|
+
ctx.workflowTasks.filter((task) => task.group === group).map((task) => task.done)
|
|
2782
|
+
);
|
|
2783
|
+
}
|
|
2784
|
+
if (ready[0]?.status === "rejected") throw ready[0].reason;
|
|
2785
|
+
(0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
|
|
2786
|
+
if (ctx.workflowControlError) throw ctx.workflowControlError;
|
|
2787
|
+
const rejected = outcomes.filter(
|
|
2788
|
+
(outcome) => outcome.status === "rejected"
|
|
2789
|
+
);
|
|
2790
|
+
const control = rejected.find(
|
|
2791
|
+
(outcome) => (0, import_core62.isExecutionCancelled)(outcome.reason) || (0, import_core62.isExecutionLimitReached)(outcome.reason)
|
|
2792
|
+
);
|
|
2793
|
+
if (control) {
|
|
2794
|
+
ctx.workflowControlError = control.reason;
|
|
2795
|
+
throw control.reason;
|
|
2796
|
+
}
|
|
2797
|
+
const actual = ctx.workflowTasks.filter((task) => task.group === group).map((task) => task.id);
|
|
2798
|
+
if (group.record.calls.length !== actual.length || group.record.calls.some((id) => !actual.includes(id))) {
|
|
2799
|
+
throw new Error("Parallel call membership changed during replay.");
|
|
2800
|
+
}
|
|
2801
|
+
for (const callId of group.record.calls) {
|
|
2802
|
+
const record = callRecord(callId);
|
|
2803
|
+
if (record?.status !== "interrupted") continue;
|
|
2804
|
+
const index = record.interrupts.length - 1;
|
|
2805
|
+
if (record.interrupts[index]?.response !== void 0) continue;
|
|
2806
|
+
group.record.interrupts.push({ callId, index });
|
|
2807
|
+
await bridge(callId, index);
|
|
2808
|
+
throw new Error(
|
|
2809
|
+
"Parallel interrupt unexpectedly resumed without replay."
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
if (rejected.length) throw new ParallelError(outcomes);
|
|
2813
|
+
return outcomes.map(
|
|
2814
|
+
(outcome) => outcome.value
|
|
2815
|
+
);
|
|
2816
|
+
} finally {
|
|
2817
|
+
ctx.parallelGroup = void 0;
|
|
2818
|
+
}
|
|
2819
|
+
};
|
|
2820
|
+
const promise = join();
|
|
2821
|
+
ctx.parallelJoins.push(promise);
|
|
2822
|
+
void promise.catch(() => {
|
|
2823
|
+
});
|
|
2824
|
+
return promise;
|
|
2825
|
+
}
|
|
2826
|
+
var import_node_crypto5 = require("crypto");
|
|
2827
|
+
var import_core7 = require("@kortyx/core");
|
|
2652
2828
|
var import_node_crypto42 = require("crypto");
|
|
2653
2829
|
function emitWorkflowCall2(telemetry, type, payload) {
|
|
2654
2830
|
const base = telemetry?.correlation;
|
|
@@ -2730,16 +2906,30 @@ ${tryStringify2(args.response)}`
|
|
|
2730
2906
|
};
|
|
2731
2907
|
}
|
|
2732
2908
|
async function useWorkflow(args) {
|
|
2909
|
+
const task = registerWorkflowTask(args.id);
|
|
2910
|
+
try {
|
|
2911
|
+
await Promise.resolve();
|
|
2912
|
+
if (task.group) await task.group.ready;
|
|
2913
|
+
return await runWorkflowCall(args, task);
|
|
2914
|
+
} catch (error) {
|
|
2915
|
+
if ((0, import_core7.isExecutionCancelled)(error) || (0, import_core7.isExecutionLimitReached)(error))
|
|
2916
|
+
getHookContext().workflowControlError = error;
|
|
2917
|
+
throw error;
|
|
2918
|
+
} finally {
|
|
2919
|
+
task.finish();
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
async function runWorkflowCall(args, task) {
|
|
2733
2923
|
const ctx = getHookContext();
|
|
2734
|
-
(0,
|
|
2924
|
+
(0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
|
|
2735
2925
|
const service = ctx.node.callWorkflow;
|
|
2736
2926
|
if (!service)
|
|
2737
2927
|
throw new Error("useWorkflow requires an agent workflow registry.");
|
|
2738
2928
|
if (!args.id.trim())
|
|
2739
2929
|
throw new Error("useWorkflow requires a stable, nonempty id.");
|
|
2740
|
-
if (ctx.workflowCallActive)
|
|
2930
|
+
if (ctx.workflowCallActive && !task.group)
|
|
2741
2931
|
throw new Error(
|
|
2742
|
-
"Concurrent workflow calls
|
|
2932
|
+
"Concurrent workflow calls require parallel([...]); otherwise await each call."
|
|
2743
2933
|
);
|
|
2744
2934
|
if (ctx.workflowCallIds.has(args.id))
|
|
2745
2935
|
throw new Error(
|
|
@@ -2760,6 +2950,11 @@ ${tryStringify2(args.response)}`
|
|
|
2760
2950
|
});
|
|
2761
2951
|
const key = `__useWorkflow:${args.id}`;
|
|
2762
2952
|
let record = ctx.currentNodeState.byKey[key];
|
|
2953
|
+
if (record && record.parallelKey !== task.group?.key)
|
|
2954
|
+
throw new WorkflowCallError(
|
|
2955
|
+
workflow,
|
|
2956
|
+
"parallel call membership changed during replay"
|
|
2957
|
+
);
|
|
2763
2958
|
if (record && record.fingerprint !== fingerprint)
|
|
2764
2959
|
throw new WorkflowCallError(
|
|
2765
2960
|
workflow,
|
|
@@ -2770,11 +2965,12 @@ ${tryStringify2(args.response)}`
|
|
|
2770
2965
|
fingerprint,
|
|
2771
2966
|
invocationId: (0, import_node_crypto5.randomUUID)(),
|
|
2772
2967
|
status: "running",
|
|
2773
|
-
interrupts: []
|
|
2968
|
+
interrupts: [],
|
|
2969
|
+
...task.group ? { parallelKey: task.group.key } : {}
|
|
2774
2970
|
};
|
|
2775
2971
|
ctx.currentNodeState.byKey[key] = record;
|
|
2776
2972
|
ctx.stateDirty = true;
|
|
2777
|
-
ctx.workflowCallActive = true;
|
|
2973
|
+
if (!task.group) ctx.workflowCallActive = true;
|
|
2778
2974
|
const telemetry = ctx.node.workflowCallTelemetry;
|
|
2779
2975
|
const activationKey = "__useWorkflowActivation";
|
|
2780
2976
|
ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto5.randomUUID)();
|
|
@@ -2810,7 +3006,7 @@ ${tryStringify2(args.response)}`
|
|
|
2810
3006
|
};
|
|
2811
3007
|
const cached = record.status === "completed";
|
|
2812
3008
|
try {
|
|
2813
|
-
for (const interruption of record.interrupts) {
|
|
3009
|
+
for (const interruption of task.group ? [] : record.interrupts) {
|
|
2814
3010
|
interruption.response = await awaitInterruptInternal({
|
|
2815
3011
|
request: interruption.request,
|
|
2816
3012
|
...interruption.request.meta ? { meta: interruption.request.meta } : {}
|
|
@@ -2818,6 +3014,8 @@ ${tryStringify2(args.response)}`
|
|
|
2818
3014
|
}
|
|
2819
3015
|
if (record.status === "failed")
|
|
2820
3016
|
throw new WorkflowCallError(workflow, record.error ?? "child failed");
|
|
3017
|
+
if (task.group && record.status === "interrupted" && record.interrupts.at(-1)?.response === void 0)
|
|
3018
|
+
throw new ParallelChildWaiting();
|
|
2821
3019
|
while (record.status !== "completed") {
|
|
2822
3020
|
let outcome;
|
|
2823
3021
|
try {
|
|
@@ -2836,13 +3034,13 @@ ${tryStringify2(args.response)}`
|
|
|
2836
3034
|
...record.interrupts.length ? { response: record.interrupts.at(-1)?.response } : {}
|
|
2837
3035
|
});
|
|
2838
3036
|
} catch (error) {
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
3037
|
+
const child = error;
|
|
3038
|
+
recordUsage(child?.__kortyxChildUsage);
|
|
3039
|
+
(0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
|
|
3040
|
+
if ((0, import_core7.isExecutionCancelled)(error)) throw error;
|
|
3041
|
+
if ((0, import_core7.isExecutionLimitReached)(error)) {
|
|
3042
|
+
if (child?.__kortyxChildSnapshot)
|
|
2844
3043
|
record.snapshot = child.__kortyxChildSnapshot;
|
|
2845
|
-
recordUsage(child.__kortyxChildUsage);
|
|
2846
3044
|
report("suspended", {
|
|
2847
3045
|
reason: "limit_reached",
|
|
2848
3046
|
limit: error.limitReached
|
|
@@ -2854,7 +3052,7 @@ ${tryStringify2(args.response)}`
|
|
|
2854
3052
|
report("failed", { error: "Child workflow failed" });
|
|
2855
3053
|
throw new WorkflowCallError(workflow, record.error);
|
|
2856
3054
|
}
|
|
2857
|
-
(0,
|
|
3055
|
+
(0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
|
|
2858
3056
|
recordUsage(outcome.usage);
|
|
2859
3057
|
if (outcome.status === "completed") {
|
|
2860
3058
|
record.data = outcome.data;
|
|
@@ -2875,6 +3073,7 @@ ${tryStringify2(args.response)}`
|
|
|
2875
3073
|
request: outcome.request
|
|
2876
3074
|
};
|
|
2877
3075
|
record.interrupts.push(interruption);
|
|
3076
|
+
if (task.group) throw new ParallelChildWaiting();
|
|
2878
3077
|
interruption.response = await awaitInterruptInternal({
|
|
2879
3078
|
request: interruption.request,
|
|
2880
3079
|
...interruption.request.meta ? { meta: interruption.request.meta } : {}
|
|
@@ -2888,7 +3087,7 @@ ${tryStringify2(args.response)}`
|
|
|
2888
3087
|
data: JSON.parse(JSON.stringify(data))
|
|
2889
3088
|
};
|
|
2890
3089
|
} finally {
|
|
2891
|
-
ctx.workflowCallActive = false;
|
|
3090
|
+
if (!task.group) ctx.workflowCallActive = false;
|
|
2892
3091
|
}
|
|
2893
3092
|
}
|
|
2894
3093
|
}
|