@kortyx/agent 0.20.0 → 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 CHANGED
@@ -1,5 +1,33 @@
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
+
19
+ ## [0.20.1](https://github.com/kortyx-io/kortyx/compare/agent-v0.20.0...agent-v0.20.1) (2026-09-11)
20
+
21
+
22
+ ### Dependencies
23
+
24
+ * The following workspace dependencies were updated
25
+ * dependencies
26
+ * @kortyx/providers bumped to 0.6.1
27
+ * @kortyx/runtime bumped to 0.15.2
28
+ * devDependencies
29
+ * @kortyx/hooks bumped to 0.23.1
30
+
3
31
  ## [0.20.0](https://github.com/kortyx-io/kortyx/compare/agent-v0.19.0...agent-v0.20.0) (2026-09-11)
4
32
 
5
33
 
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, fn);
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
  }
@@ -432,6 +458,16 @@ var require_dist = __commonJS({
432
458
  ...typeof nodeId === "string" && nodeId.length > 0 ? { node: nodeId } : {}
433
459
  });
434
460
  };
461
+ var resolveProviderOptions = (defaults, overrides) => {
462
+ const providerOptions = defaults || overrides ? { ...defaults, ...overrides } : void 0;
463
+ for (const [key, value] of Object.entries(overrides ?? {})) {
464
+ const previous = defaults?.[key];
465
+ if (previous && value && typeof previous === "object" && typeof value === "object" && !Array.isArray(previous) && !Array.isArray(value)) {
466
+ if (providerOptions) providerOptions[key] = { ...previous, ...value };
467
+ }
468
+ }
469
+ return providerOptions;
470
+ };
435
471
  async function runReasonEngine(args) {
436
472
  let stream = args.stream ?? args.model.options?.streaming ?? true;
437
473
  const emit = args.emit ?? true;
@@ -441,14 +477,10 @@ var require_dist = __commonJS({
441
477
  const abortSignal = args.abortSignal ?? args.model.options?.abortSignal;
442
478
  const reasoning = args.reasoning ?? args.model.options?.reasoning;
443
479
  const responseFormat = args.responseFormat ?? args.model.options?.responseFormat;
444
- const defaults = args.model.options?.providerOptions;
445
- const providerOptions = defaults || args.providerOptions ? { ...defaults, ...args.providerOptions } : void 0;
446
- for (const [key, value] of Object.entries(args.providerOptions ?? {})) {
447
- const previous = defaults?.[key];
448
- if (previous && value && typeof previous === "object" && typeof value === "object" && !Array.isArray(previous) && !Array.isArray(value)) {
449
- if (providerOptions) providerOptions[key] = { ...previous, ...value };
450
- }
451
- }
480
+ const providerOptions = resolveProviderOptions(
481
+ args.model.options?.providerOptions,
482
+ args.providerOptions
483
+ );
452
484
  const tools = args.tools ?? args.model.options?.tools;
453
485
  (0, import_core22.throwIfExecutionAborted)(abortSignal);
454
486
  const model = args.model.provider.getModel(args.model.modelId, {
@@ -1861,6 +1893,7 @@ ${tryStringify2(args.response)}`
1861
1893
  const tools = useReasonArgs.tools ?? [];
1862
1894
  const toolByName = /* @__PURE__ */ new Map();
1863
1895
  let validationCompleted = false;
1896
+ let separateOutput = false;
1864
1897
  try {
1865
1898
  for (const tool of tools) {
1866
1899
  if (toolByName.has(tool.name)) {
@@ -1875,6 +1908,17 @@ ${tryStringify2(args.response)}`
1875
1908
  "useReason tools cannot be combined with useReason interrupt mode yet. Use toolExecution.approval for tool approval."
1876
1909
  );
1877
1910
  }
1911
+ separateOutput = Boolean(
1912
+ useReasonArgs.model.provider.getModel(useReasonArgs.model.modelId, {
1913
+ ...useReasonArgs.model.options,
1914
+ ...useReasonArgs.reasoning !== void 0 ? { reasoning: useReasonArgs.reasoning } : {},
1915
+ ...useReasonArgs.responseFormat ? { responseFormat: useReasonArgs.responseFormat } : {},
1916
+ providerOptions: resolveProviderOptions(
1917
+ useReasonArgs.model.options?.providerOptions,
1918
+ useReasonArgs.providerOptions
1919
+ ) ?? {}
1920
+ }).requiresSeparateStructuredOutput
1921
+ );
1878
1922
  validationCompleted = true;
1879
1923
  } finally {
1880
1924
  if (!validationCompleted) {
@@ -1882,6 +1926,8 @@ ${tryStringify2(args.response)}`
1882
1926
  }
1883
1927
  }
1884
1928
  const toolDefinitions = toToolDefinitions(tools);
1929
+ let finalizing = checkpoint?.finalizing ?? false;
1930
+ let completed = false;
1885
1931
  const maxSteps = Math.max(1, useReasonArgs.toolExecution?.maxSteps ?? 3);
1886
1932
  const messages = checkpoint?.messages ?? createInitialMessages({
1887
1933
  system: useReasonArgs.system,
@@ -1916,6 +1962,7 @@ ${tryStringify2(args.response)}`
1916
1962
  messages,
1917
1963
  steps,
1918
1964
  approvedCalls,
1965
+ finalizing,
1919
1966
  ...pending ? { pending } : {}
1920
1967
  };
1921
1968
  ctx.stateDirty = true;
@@ -1939,11 +1986,12 @@ ${tryStringify2(args.response)}`
1939
1986
  const stream = useReasonArgs.stream ?? useReasonArgs.model.options?.streaming ?? true;
1940
1987
  const emit = useReasonArgs.emit ?? true;
1941
1988
  let textStarted = false;
1942
- const structuredChunk = useReasonArgs.outputSchema && stream && emit && shouldStreamStructured(useReasonArgs.structured) ? createStructuredOutputStreamer(useReasonArgs, id, opId) : void 0;
1989
+ const structuredChunk = useReasonArgs.outputSchema && (!separateOutput || finalizing) && stream && emit && shouldStreamStructured(useReasonArgs.structured) ? createStructuredOutputStreamer(useReasonArgs, id, opId) : void 0;
1943
1990
  const step = pending ?? await reasonEngine(
1944
1991
  {
1945
1992
  ...useReasonArgs,
1946
- tools: toolDefinitions,
1993
+ tools: finalizing ? [] : toolDefinitions,
1994
+ ...separateOutput && !finalizing ? { responseFormat: { type: "text" } } : {},
1947
1995
  messages,
1948
1996
  emit: false,
1949
1997
  stream,
@@ -1986,8 +2034,48 @@ ${tryStringify2(args.response)}`
1986
2034
  ...step.warnings ? { warnings: step.warnings } : {}
1987
2035
  });
1988
2036
  if (toolCalls.length === 0) {
2037
+ if (separateOutput && !finalizing) {
2038
+ if (args.allowValidatedToolOutput && useReasonArgs.outputSchema) {
2039
+ try {
2040
+ finalOutput = parseReasonOutputWithSchema({
2041
+ text: step.text,
2042
+ schema: useReasonArgs.outputSchema,
2043
+ ...step.finishReason ? { finishReason: step.finishReason } : {},
2044
+ label: "useReason output"
2045
+ });
2046
+ aggregatedWarnings = mergeWarnings(aggregatedWarnings, [
2047
+ {
2048
+ type: "compatibility",
2049
+ feature: "responseFormat",
2050
+ details: "Reused locally validated tool-phase output without an extra native-schema request. Set responseFormat.schema explicitly to require provider schema enforcement."
2051
+ }
2052
+ ]);
2053
+ completed = true;
2054
+ break;
2055
+ } catch {
2056
+ }
2057
+ }
2058
+ messages.push({
2059
+ role: "assistant",
2060
+ content: step.text,
2061
+ ...step.continuation ? { continuation: step.continuation } : {}
2062
+ });
2063
+ messages.push({
2064
+ role: "user",
2065
+ content: "Return the final answer using the requested output schema and the tool results above."
2066
+ });
2067
+ finalizing = true;
2068
+ pending = void 0;
2069
+ save();
2070
+ continue;
2071
+ }
2072
+ completed = true;
1989
2073
  break;
1990
2074
  }
2075
+ if (finalizing)
2076
+ throw new Error(
2077
+ "Provider requested a tool during schema-only finalization."
2078
+ );
1991
2079
  if (!reused) {
1992
2080
  allToolCalls.push(...toolCalls);
1993
2081
  messages.push({
@@ -2131,26 +2219,26 @@ ${tryStringify2(args.response)}`
2131
2219
  message: result2.content
2132
2220
  });
2133
2221
  }
2134
- const completed = toolResults.at(-1);
2135
- if (completed)
2222
+ const completed2 = toolResults.at(-1);
2223
+ if (completed2)
2136
2224
  messages.push({
2137
2225
  role: "tool",
2138
- content: completed.content,
2139
- toolCallId: completed.toolCallId,
2140
- name: completed.name,
2141
- ...completed.isError !== void 0 ? { isError: completed.isError } : {}
2226
+ content: completed2.content,
2227
+ toolCallId: completed2.toolCallId,
2228
+ name: completed2.name,
2229
+ ...completed2.isError !== void 0 ? { isError: completed2.isError } : {}
2142
2230
  });
2143
2231
  save();
2144
2232
  }
2145
2233
  pending = void 0;
2146
2234
  save();
2147
2235
  }
2148
- if (steps.length >= maxSteps && steps.at(-1)?.toolCalls.length) {
2236
+ if (!completed) {
2149
2237
  throw new Error(
2150
2238
  `useReason tool loop reached maxSteps (${maxSteps}) before producing a final response.`
2151
2239
  );
2152
2240
  }
2153
- if (useReasonArgs.outputSchema) {
2241
+ if (useReasonArgs.outputSchema && finalOutput === void 0) {
2154
2242
  finalOutput = parseReasonOutputWithSchema({
2155
2243
  text: finalText,
2156
2244
  schema: useReasonArgs.outputSchema,
@@ -2232,6 +2320,9 @@ ${tryStringify2(args.response)}`
2232
2320
  args.model.options?.abortSignal
2233
2321
  );
2234
2322
  (0, import_core52.throwIfExecutionAborted)(abortSignal);
2323
+ const allowValidatedToolOutput = Boolean(
2324
+ args.outputSchema && !(args.responseFormat ?? args.model.options?.responseFormat)
2325
+ );
2235
2326
  const inferred = inferOutputFormat(
2236
2327
  args.outputSchema,
2237
2328
  args.responseFormat ?? args.model.options?.responseFormat
@@ -2284,6 +2375,7 @@ ${tryStringify2(args.response)}`
2284
2375
  if (args.tools?.length) {
2285
2376
  return runReasonToolLoop({
2286
2377
  useReasonArgs: args,
2378
+ allowValidatedToolOutput,
2287
2379
  checkpointKey,
2288
2380
  initialWarnings: inferred.warnings,
2289
2381
  ...id ? { id } : {},
@@ -2581,8 +2673,158 @@ ${tryStringify2(args.response)}`
2581
2673
  };
2582
2674
  return [getValue(), setValue];
2583
2675
  }
2584
- var import_node_crypto5 = require("crypto");
2585
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");
2586
2828
  var import_node_crypto42 = require("crypto");
2587
2829
  function emitWorkflowCall2(telemetry, type, payload) {
2588
2830
  const base = telemetry?.correlation;
@@ -2664,16 +2906,30 @@ ${tryStringify2(args.response)}`
2664
2906
  };
2665
2907
  }
2666
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) {
2667
2923
  const ctx = getHookContext();
2668
- (0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
2924
+ (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
2669
2925
  const service = ctx.node.callWorkflow;
2670
2926
  if (!service)
2671
2927
  throw new Error("useWorkflow requires an agent workflow registry.");
2672
2928
  if (!args.id.trim())
2673
2929
  throw new Error("useWorkflow requires a stable, nonempty id.");
2674
- if (ctx.workflowCallActive)
2930
+ if (ctx.workflowCallActive && !task.group)
2675
2931
  throw new Error(
2676
- "Concurrent workflow calls are not supported; await each call."
2932
+ "Concurrent workflow calls require parallel([...]); otherwise await each call."
2677
2933
  );
2678
2934
  if (ctx.workflowCallIds.has(args.id))
2679
2935
  throw new Error(
@@ -2694,6 +2950,11 @@ ${tryStringify2(args.response)}`
2694
2950
  });
2695
2951
  const key = `__useWorkflow:${args.id}`;
2696
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
+ );
2697
2958
  if (record && record.fingerprint !== fingerprint)
2698
2959
  throw new WorkflowCallError(
2699
2960
  workflow,
@@ -2704,11 +2965,12 @@ ${tryStringify2(args.response)}`
2704
2965
  fingerprint,
2705
2966
  invocationId: (0, import_node_crypto5.randomUUID)(),
2706
2967
  status: "running",
2707
- interrupts: []
2968
+ interrupts: [],
2969
+ ...task.group ? { parallelKey: task.group.key } : {}
2708
2970
  };
2709
2971
  ctx.currentNodeState.byKey[key] = record;
2710
2972
  ctx.stateDirty = true;
2711
- ctx.workflowCallActive = true;
2973
+ if (!task.group) ctx.workflowCallActive = true;
2712
2974
  const telemetry = ctx.node.workflowCallTelemetry;
2713
2975
  const activationKey = "__useWorkflowActivation";
2714
2976
  ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto5.randomUUID)();
@@ -2744,7 +3006,7 @@ ${tryStringify2(args.response)}`
2744
3006
  };
2745
3007
  const cached = record.status === "completed";
2746
3008
  try {
2747
- for (const interruption of record.interrupts) {
3009
+ for (const interruption of task.group ? [] : record.interrupts) {
2748
3010
  interruption.response = await awaitInterruptInternal({
2749
3011
  request: interruption.request,
2750
3012
  ...interruption.request.meta ? { meta: interruption.request.meta } : {}
@@ -2752,6 +3014,8 @@ ${tryStringify2(args.response)}`
2752
3014
  }
2753
3015
  if (record.status === "failed")
2754
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();
2755
3019
  while (record.status !== "completed") {
2756
3020
  let outcome;
2757
3021
  try {
@@ -2770,13 +3034,13 @@ ${tryStringify2(args.response)}`
2770
3034
  ...record.interrupts.length ? { response: record.interrupts.at(-1)?.response } : {}
2771
3035
  });
2772
3036
  } catch (error) {
2773
- (0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
2774
- if ((0, import_core62.isExecutionCancelled)(error)) throw error;
2775
- if ((0, import_core62.isExecutionLimitReached)(error)) {
2776
- const child = error;
2777
- if (child.__kortyxChildSnapshot)
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)
2778
3043
  record.snapshot = child.__kortyxChildSnapshot;
2779
- recordUsage(child.__kortyxChildUsage);
2780
3044
  report("suspended", {
2781
3045
  reason: "limit_reached",
2782
3046
  limit: error.limitReached
@@ -2788,7 +3052,7 @@ ${tryStringify2(args.response)}`
2788
3052
  report("failed", { error: "Child workflow failed" });
2789
3053
  throw new WorkflowCallError(workflow, record.error);
2790
3054
  }
2791
- (0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
3055
+ (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
2792
3056
  recordUsage(outcome.usage);
2793
3057
  if (outcome.status === "completed") {
2794
3058
  record.data = outcome.data;
@@ -2809,6 +3073,7 @@ ${tryStringify2(args.response)}`
2809
3073
  request: outcome.request
2810
3074
  };
2811
3075
  record.interrupts.push(interruption);
3076
+ if (task.group) throw new ParallelChildWaiting();
2812
3077
  interruption.response = await awaitInterruptInternal({
2813
3078
  request: interruption.request,
2814
3079
  ...interruption.request.meta ? { meta: interruption.request.meta } : {}
@@ -2822,7 +3087,7 @@ ${tryStringify2(args.response)}`
2822
3087
  data: JSON.parse(JSON.stringify(data))
2823
3088
  };
2824
3089
  } finally {
2825
- ctx.workflowCallActive = false;
3090
+ if (!task.group) ctx.workflowCallActive = false;
2826
3091
  }
2827
3092
  }
2828
3093
  }