@kortyx/hooks 0.23.0 → 0.24.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 +21 -0
- package/README.md +14 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +304 -35
- package/dist/index.js.map +1 -1
- package/dist/internal.js +14 -8
- package/dist/internal.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.24.0](https://github.com/kortyx-io/kortyx/compare/hooks-v0.23.1...hooks-v0.24.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
|
+
## [0.23.1](https://github.com/kortyx-io/kortyx/compare/hooks-v0.23.0...hooks-v0.23.1) (2026-09-11)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **providers:** preserve reasoning across tool workflows ([#169](https://github.com/kortyx-io/kortyx/issues/169)) ([6e4e403](https://github.com/kortyx-io/kortyx/commit/6e4e403d104d83586e05aab799510bc4c35b0f49))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Dependencies
|
|
19
|
+
|
|
20
|
+
* The following workspace dependencies were updated
|
|
21
|
+
* dependencies
|
|
22
|
+
* @kortyx/providers bumped to 0.6.1
|
|
23
|
+
|
|
3
24
|
## [0.23.0](https://github.com/kortyx-io/kortyx/compare/hooks-v0.22.0...hooks-v0.23.0) (2026-09-11)
|
|
4
25
|
|
|
5
26
|
|
package/README.md
CHANGED
|
@@ -78,6 +78,19 @@ string ids. Register the same definitions with the agent.
|
|
|
78
78
|
Calls need stable ids. A child interrupt pauses the chain; resuming replays
|
|
79
79
|
the enclosing node and reuses saved child results. Keep effects before calls
|
|
80
80
|
idempotent. Fork/rollback retain nested checkpoints; use Redis for persistence
|
|
81
|
-
across server restarts.
|
|
81
|
+
across server restarts. Join concurrent children with `parallel`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const [first, second] = await parallel([
|
|
85
|
+
useWorkflow({ id: "first", workflow: researchWorkflow, input: { topic: "AI" } }),
|
|
86
|
+
useWorkflow({ id: "second", workflow: researchWorkflow, input: { topic: "Robotics" } }),
|
|
87
|
+
]);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The tuple follows input order. The join preserves all siblings before pausing and
|
|
91
|
+
presents waiting questions individually through the parent handle. Terminal group
|
|
92
|
+
failures throw `ParallelError`, whose `results` exposes each settled outcome.
|
|
93
|
+
Await groups sequentially within one node; children may contain nested groups.
|
|
94
|
+
Native `Promise.all` calls and parallel graph edges remain unsupported.
|
|
82
95
|
|
|
83
96
|
See [the execution and replay contract](../../docs/design-specs/child-workflows.md).
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,16 @@ type CompleteResponseOptions = {
|
|
|
13
13
|
/** Finish client delivery, leaving the node and workflow execution running. */
|
|
14
14
|
declare function completeResponse(options?: CompleteResponseOptions): Promise<void>;
|
|
15
15
|
|
|
16
|
+
/** All siblings have settled; results retain the order passed to parallel(). */
|
|
17
|
+
declare class ParallelError extends AggregateError {
|
|
18
|
+
readonly results: readonly PromiseSettledResult<unknown>[];
|
|
19
|
+
constructor(results: readonly PromiseSettledResult<unknown>[]);
|
|
20
|
+
}
|
|
21
|
+
/** Join concurrent child calls, preserving their snapshots before suspending. */
|
|
22
|
+
declare function parallel<const T extends readonly unknown[]>(values: T): Promise<{
|
|
23
|
+
-readonly [K in keyof T]: Awaited<T[K]>;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
16
26
|
type WorkflowCallOutcome = {
|
|
17
27
|
status: "completed";
|
|
18
28
|
data: Record<string, unknown>;
|
|
@@ -236,4 +246,4 @@ declare function useWorkflowState<T>(key: string, ...rest: [] | [T]): [T, StateS
|
|
|
236
246
|
/** Best effort lifecycle facts. Engine snapshots and interrupt tokens never leave the runtime. */
|
|
237
247
|
declare function emitWorkflowCall(telemetry: KortyxTelemetryConfig | undefined, type: KortyxTelemetryEventType, payload: Record<string, unknown>): void;
|
|
238
248
|
|
|
239
|
-
export { type CompleteResponseOptions, type HookRuntimeContext, KortyxTelemetryConfig, KortyxTelemetryEventType, KortyxTraceMetadata, ReasonTraceAdapter, type UseInterruptArgs, type UseReasonArgs, type UseReasonResult, type UseReasonStep, type UseReasonToolExecution, type UseStructuredDataArgs, WorkflowCallError, type WorkflowCallOutcome, type WorkflowCallService, completeResponse, createWorkflowHooks, emitWorkflowCall, runWithHookContext, useAbortSignal, useInterrupt, useNodeState, useReason, useRuntimeContext, useStructuredData, useWorkflow, useWorkflowState, workflowCallFingerprint };
|
|
249
|
+
export { type CompleteResponseOptions, type HookRuntimeContext, KortyxTelemetryConfig, KortyxTelemetryEventType, KortyxTraceMetadata, ParallelError, ReasonTraceAdapter, type UseInterruptArgs, type UseReasonArgs, type UseReasonResult, type UseReasonStep, type UseReasonToolExecution, type UseStructuredDataArgs, WorkflowCallError, type WorkflowCallOutcome, type WorkflowCallService, completeResponse, createWorkflowHooks, emitWorkflowCall, parallel, runWithHookContext, useAbortSignal, useInterrupt, useNodeState, useReason, useRuntimeContext, useStructuredData, useWorkflow, useWorkflowState, workflowCallFingerprint };
|
package/dist/index.js
CHANGED
|
@@ -19,10 +19,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
// src/index.ts
|
|
20
20
|
var index_exports = {};
|
|
21
21
|
__export(index_exports, {
|
|
22
|
+
ParallelError: () => ParallelError,
|
|
22
23
|
WorkflowCallError: () => WorkflowCallError,
|
|
23
24
|
completeResponse: () => completeResponse,
|
|
24
25
|
createWorkflowHooks: () => createWorkflowHooks,
|
|
25
26
|
emitWorkflowCall: () => emitWorkflowCall,
|
|
27
|
+
parallel: () => parallel,
|
|
26
28
|
runWithHookContext: () => runWithHookContext,
|
|
27
29
|
useAbortSignal: () => useAbortSignal,
|
|
28
30
|
useInterrupt: () => useInterrupt,
|
|
@@ -90,6 +92,11 @@ var createInternalContext = (ctx) => {
|
|
|
90
92
|
...ctx,
|
|
91
93
|
workflowCallActive: false,
|
|
92
94
|
workflowCallIds: /* @__PURE__ */ new Set(),
|
|
95
|
+
workflowTasks: [],
|
|
96
|
+
parallelJoins: [],
|
|
97
|
+
parallelGroup: void 0,
|
|
98
|
+
parallelIndex: 0,
|
|
99
|
+
workflowContextClosed: false,
|
|
93
100
|
nodeStateIndex: 0,
|
|
94
101
|
reasonCallIndex: 0,
|
|
95
102
|
currentNodeState,
|
|
@@ -161,10 +168,29 @@ function getReasonTraceAdapter() {
|
|
|
161
168
|
async function runWithHookContext(ctx, fn) {
|
|
162
169
|
const internal = createInternalContext(ctx);
|
|
163
170
|
try {
|
|
164
|
-
const result = await storage.run(internal,
|
|
171
|
+
const result = await storage.run(internal, async () => {
|
|
172
|
+
const drain = async () => {
|
|
173
|
+
let drained = -1;
|
|
174
|
+
while (drained !== internal.parallelJoins.length + internal.workflowTasks.length) {
|
|
175
|
+
drained = internal.parallelJoins.length + internal.workflowTasks.length;
|
|
176
|
+
await Promise.allSettled(internal.parallelJoins);
|
|
177
|
+
await Promise.all(internal.workflowTasks.map((task) => task.done));
|
|
178
|
+
}
|
|
179
|
+
internal.workflowContextClosed = true;
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
const result2 = await fn();
|
|
183
|
+
await drain();
|
|
184
|
+
if (internal.workflowControlError) throw internal.workflowControlError;
|
|
185
|
+
return result2;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
await drain();
|
|
188
|
+
throw internal.workflowControlError ?? error;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
165
191
|
cleanupCompletedReasonCheckpoints(internal);
|
|
166
192
|
for (const key of Object.keys(internal.currentNodeState.byKey)) {
|
|
167
|
-
if (key.startsWith("__useWorkflow:") || key === "__useWorkflowActivation") {
|
|
193
|
+
if (key.startsWith("__useWorkflow:") || key.startsWith("__parallel:") || key === "__useWorkflowActivation") {
|
|
168
194
|
internal.currentNodeState.byKey[key] = null;
|
|
169
195
|
internal.stateDirty = true;
|
|
170
196
|
}
|
|
@@ -426,6 +452,16 @@ var emitNodeEvent = (emitEvent, nodeId, event, payload) => {
|
|
|
426
452
|
...typeof nodeId === "string" && nodeId.length > 0 ? { node: nodeId } : {}
|
|
427
453
|
});
|
|
428
454
|
};
|
|
455
|
+
var resolveProviderOptions = (defaults, overrides) => {
|
|
456
|
+
const providerOptions = defaults || overrides ? { ...defaults, ...overrides } : void 0;
|
|
457
|
+
for (const [key, value] of Object.entries(overrides ?? {})) {
|
|
458
|
+
const previous = defaults?.[key];
|
|
459
|
+
if (previous && value && typeof previous === "object" && typeof value === "object" && !Array.isArray(previous) && !Array.isArray(value)) {
|
|
460
|
+
if (providerOptions) providerOptions[key] = { ...previous, ...value };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return providerOptions;
|
|
464
|
+
};
|
|
429
465
|
async function runReasonEngine(args) {
|
|
430
466
|
let stream = args.stream ?? args.model.options?.streaming ?? true;
|
|
431
467
|
const emit = args.emit ?? true;
|
|
@@ -435,14 +471,10 @@ async function runReasonEngine(args) {
|
|
|
435
471
|
const abortSignal = args.abortSignal ?? args.model.options?.abortSignal;
|
|
436
472
|
const reasoning = args.reasoning ?? args.model.options?.reasoning;
|
|
437
473
|
const responseFormat = args.responseFormat ?? args.model.options?.responseFormat;
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (previous && value && typeof previous === "object" && typeof value === "object" && !Array.isArray(previous) && !Array.isArray(value)) {
|
|
443
|
-
if (providerOptions) providerOptions[key] = { ...previous, ...value };
|
|
444
|
-
}
|
|
445
|
-
}
|
|
474
|
+
const providerOptions = resolveProviderOptions(
|
|
475
|
+
args.model.options?.providerOptions,
|
|
476
|
+
args.providerOptions
|
|
477
|
+
);
|
|
446
478
|
const tools = args.tools ?? args.model.options?.tools;
|
|
447
479
|
(0, import_core2.throwIfExecutionAborted)(abortSignal);
|
|
448
480
|
const model = args.model.provider.getModel(args.model.modelId, {
|
|
@@ -1871,6 +1903,7 @@ var runReasonToolLoop = async (args) => {
|
|
|
1871
1903
|
const tools = useReasonArgs.tools ?? [];
|
|
1872
1904
|
const toolByName = /* @__PURE__ */ new Map();
|
|
1873
1905
|
let validationCompleted = false;
|
|
1906
|
+
let separateOutput = false;
|
|
1874
1907
|
try {
|
|
1875
1908
|
for (const tool of tools) {
|
|
1876
1909
|
if (toolByName.has(tool.name)) {
|
|
@@ -1885,6 +1918,17 @@ var runReasonToolLoop = async (args) => {
|
|
|
1885
1918
|
"useReason tools cannot be combined with useReason interrupt mode yet. Use toolExecution.approval for tool approval."
|
|
1886
1919
|
);
|
|
1887
1920
|
}
|
|
1921
|
+
separateOutput = Boolean(
|
|
1922
|
+
useReasonArgs.model.provider.getModel(useReasonArgs.model.modelId, {
|
|
1923
|
+
...useReasonArgs.model.options,
|
|
1924
|
+
...useReasonArgs.reasoning !== void 0 ? { reasoning: useReasonArgs.reasoning } : {},
|
|
1925
|
+
...useReasonArgs.responseFormat ? { responseFormat: useReasonArgs.responseFormat } : {},
|
|
1926
|
+
providerOptions: resolveProviderOptions(
|
|
1927
|
+
useReasonArgs.model.options?.providerOptions,
|
|
1928
|
+
useReasonArgs.providerOptions
|
|
1929
|
+
) ?? {}
|
|
1930
|
+
}).requiresSeparateStructuredOutput
|
|
1931
|
+
);
|
|
1888
1932
|
validationCompleted = true;
|
|
1889
1933
|
} finally {
|
|
1890
1934
|
if (!validationCompleted) {
|
|
@@ -1892,6 +1936,8 @@ var runReasonToolLoop = async (args) => {
|
|
|
1892
1936
|
}
|
|
1893
1937
|
}
|
|
1894
1938
|
const toolDefinitions = toToolDefinitions(tools);
|
|
1939
|
+
let finalizing = checkpoint?.finalizing ?? false;
|
|
1940
|
+
let completed = false;
|
|
1895
1941
|
const maxSteps = Math.max(1, useReasonArgs.toolExecution?.maxSteps ?? 3);
|
|
1896
1942
|
const messages = checkpoint?.messages ?? createInitialMessages({
|
|
1897
1943
|
system: useReasonArgs.system,
|
|
@@ -1926,6 +1972,7 @@ var runReasonToolLoop = async (args) => {
|
|
|
1926
1972
|
messages,
|
|
1927
1973
|
steps,
|
|
1928
1974
|
approvedCalls,
|
|
1975
|
+
finalizing,
|
|
1929
1976
|
...pending ? { pending } : {}
|
|
1930
1977
|
};
|
|
1931
1978
|
ctx.stateDirty = true;
|
|
@@ -1949,11 +1996,12 @@ var runReasonToolLoop = async (args) => {
|
|
|
1949
1996
|
const stream = useReasonArgs.stream ?? useReasonArgs.model.options?.streaming ?? true;
|
|
1950
1997
|
const emit = useReasonArgs.emit ?? true;
|
|
1951
1998
|
let textStarted = false;
|
|
1952
|
-
const structuredChunk = useReasonArgs.outputSchema && stream && emit && shouldStreamStructured(useReasonArgs.structured) ? createStructuredOutputStreamer(useReasonArgs, id, opId) : void 0;
|
|
1999
|
+
const structuredChunk = useReasonArgs.outputSchema && (!separateOutput || finalizing) && stream && emit && shouldStreamStructured(useReasonArgs.structured) ? createStructuredOutputStreamer(useReasonArgs, id, opId) : void 0;
|
|
1953
2000
|
const step = pending ?? await reasonEngine(
|
|
1954
2001
|
{
|
|
1955
2002
|
...useReasonArgs,
|
|
1956
|
-
tools: toolDefinitions,
|
|
2003
|
+
tools: finalizing ? [] : toolDefinitions,
|
|
2004
|
+
...separateOutput && !finalizing ? { responseFormat: { type: "text" } } : {},
|
|
1957
2005
|
messages,
|
|
1958
2006
|
emit: false,
|
|
1959
2007
|
stream,
|
|
@@ -1996,8 +2044,48 @@ var runReasonToolLoop = async (args) => {
|
|
|
1996
2044
|
...step.warnings ? { warnings: step.warnings } : {}
|
|
1997
2045
|
});
|
|
1998
2046
|
if (toolCalls.length === 0) {
|
|
2047
|
+
if (separateOutput && !finalizing) {
|
|
2048
|
+
if (args.allowValidatedToolOutput && useReasonArgs.outputSchema) {
|
|
2049
|
+
try {
|
|
2050
|
+
finalOutput = parseReasonOutputWithSchema({
|
|
2051
|
+
text: step.text,
|
|
2052
|
+
schema: useReasonArgs.outputSchema,
|
|
2053
|
+
...step.finishReason ? { finishReason: step.finishReason } : {},
|
|
2054
|
+
label: "useReason output"
|
|
2055
|
+
});
|
|
2056
|
+
aggregatedWarnings = mergeWarnings(aggregatedWarnings, [
|
|
2057
|
+
{
|
|
2058
|
+
type: "compatibility",
|
|
2059
|
+
feature: "responseFormat",
|
|
2060
|
+
details: "Reused locally validated tool-phase output without an extra native-schema request. Set responseFormat.schema explicitly to require provider schema enforcement."
|
|
2061
|
+
}
|
|
2062
|
+
]);
|
|
2063
|
+
completed = true;
|
|
2064
|
+
break;
|
|
2065
|
+
} catch {
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
messages.push({
|
|
2069
|
+
role: "assistant",
|
|
2070
|
+
content: step.text,
|
|
2071
|
+
...step.continuation ? { continuation: step.continuation } : {}
|
|
2072
|
+
});
|
|
2073
|
+
messages.push({
|
|
2074
|
+
role: "user",
|
|
2075
|
+
content: "Return the final answer using the requested output schema and the tool results above."
|
|
2076
|
+
});
|
|
2077
|
+
finalizing = true;
|
|
2078
|
+
pending = void 0;
|
|
2079
|
+
save();
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
completed = true;
|
|
1999
2083
|
break;
|
|
2000
2084
|
}
|
|
2085
|
+
if (finalizing)
|
|
2086
|
+
throw new Error(
|
|
2087
|
+
"Provider requested a tool during schema-only finalization."
|
|
2088
|
+
);
|
|
2001
2089
|
if (!reused) {
|
|
2002
2090
|
allToolCalls.push(...toolCalls);
|
|
2003
2091
|
messages.push({
|
|
@@ -2141,26 +2229,26 @@ var runReasonToolLoop = async (args) => {
|
|
|
2141
2229
|
message: result2.content
|
|
2142
2230
|
});
|
|
2143
2231
|
}
|
|
2144
|
-
const
|
|
2145
|
-
if (
|
|
2232
|
+
const completed2 = toolResults.at(-1);
|
|
2233
|
+
if (completed2)
|
|
2146
2234
|
messages.push({
|
|
2147
2235
|
role: "tool",
|
|
2148
|
-
content:
|
|
2149
|
-
toolCallId:
|
|
2150
|
-
name:
|
|
2151
|
-
...
|
|
2236
|
+
content: completed2.content,
|
|
2237
|
+
toolCallId: completed2.toolCallId,
|
|
2238
|
+
name: completed2.name,
|
|
2239
|
+
...completed2.isError !== void 0 ? { isError: completed2.isError } : {}
|
|
2152
2240
|
});
|
|
2153
2241
|
save();
|
|
2154
2242
|
}
|
|
2155
2243
|
pending = void 0;
|
|
2156
2244
|
save();
|
|
2157
2245
|
}
|
|
2158
|
-
if (
|
|
2246
|
+
if (!completed) {
|
|
2159
2247
|
throw new Error(
|
|
2160
2248
|
`useReason tool loop reached maxSteps (${maxSteps}) before producing a final response.`
|
|
2161
2249
|
);
|
|
2162
2250
|
}
|
|
2163
|
-
if (useReasonArgs.outputSchema) {
|
|
2251
|
+
if (useReasonArgs.outputSchema && finalOutput === void 0) {
|
|
2164
2252
|
finalOutput = parseReasonOutputWithSchema({
|
|
2165
2253
|
text: finalText,
|
|
2166
2254
|
schema: useReasonArgs.outputSchema,
|
|
@@ -2244,6 +2332,9 @@ async function useReason(args) {
|
|
|
2244
2332
|
args.model.options?.abortSignal
|
|
2245
2333
|
);
|
|
2246
2334
|
(0, import_core5.throwIfExecutionAborted)(abortSignal);
|
|
2335
|
+
const allowValidatedToolOutput = Boolean(
|
|
2336
|
+
args.outputSchema && !(args.responseFormat ?? args.model.options?.responseFormat)
|
|
2337
|
+
);
|
|
2247
2338
|
const inferred = inferOutputFormat(
|
|
2248
2339
|
args.outputSchema,
|
|
2249
2340
|
args.responseFormat ?? args.model.options?.responseFormat
|
|
@@ -2296,6 +2387,7 @@ async function useReason(args) {
|
|
|
2296
2387
|
if (args.tools?.length) {
|
|
2297
2388
|
return runReasonToolLoop({
|
|
2298
2389
|
useReasonArgs: args,
|
|
2390
|
+
allowValidatedToolOutput,
|
|
2299
2391
|
checkpointKey,
|
|
2300
2392
|
initialWarnings: inferred.warnings,
|
|
2301
2393
|
...id ? { id } : {},
|
|
@@ -2596,9 +2688,161 @@ function useWorkflowState(key, ...rest) {
|
|
|
2596
2688
|
return [getValue(), setValue];
|
|
2597
2689
|
}
|
|
2598
2690
|
|
|
2691
|
+
// src/parallel.ts
|
|
2692
|
+
var import_core6 = require("@kortyx/core");
|
|
2693
|
+
var ParallelChildWaiting = class extends Error {
|
|
2694
|
+
};
|
|
2695
|
+
var ParallelError = class extends AggregateError {
|
|
2696
|
+
constructor(results) {
|
|
2697
|
+
super(
|
|
2698
|
+
results.flatMap(
|
|
2699
|
+
(result) => result.status === "rejected" ? [result.reason] : []
|
|
2700
|
+
),
|
|
2701
|
+
"One or more parallel operations failed."
|
|
2702
|
+
);
|
|
2703
|
+
this.results = results;
|
|
2704
|
+
this.name = "ParallelError";
|
|
2705
|
+
}
|
|
2706
|
+
results;
|
|
2707
|
+
};
|
|
2708
|
+
function registerWorkflowTask(id) {
|
|
2709
|
+
const ctx = getHookContext();
|
|
2710
|
+
if (ctx.workflowControlError) throw ctx.workflowControlError;
|
|
2711
|
+
if (ctx.workflowContextClosed)
|
|
2712
|
+
throw new Error("The workflow node activation has finished.");
|
|
2713
|
+
const group = ctx.parallelGroup;
|
|
2714
|
+
if (group?.restored && !group.record.calls.includes(id)) {
|
|
2715
|
+
throw new Error(`Parallel call membership changed during replay: '${id}'.`);
|
|
2716
|
+
}
|
|
2717
|
+
let finish;
|
|
2718
|
+
const task = {
|
|
2719
|
+
id,
|
|
2720
|
+
...group ? { group } : {},
|
|
2721
|
+
completed: false,
|
|
2722
|
+
done: new Promise((resolve) => {
|
|
2723
|
+
finish = resolve;
|
|
2724
|
+
}),
|
|
2725
|
+
finish: () => {
|
|
2726
|
+
task.completed = true;
|
|
2727
|
+
finish();
|
|
2728
|
+
}
|
|
2729
|
+
};
|
|
2730
|
+
ctx.workflowTasks.push(task);
|
|
2731
|
+
if (group && !group.record.calls.includes(id)) group.record.calls.push(id);
|
|
2732
|
+
return task;
|
|
2733
|
+
}
|
|
2734
|
+
function parallel(values) {
|
|
2735
|
+
const settled = Promise.allSettled(values);
|
|
2736
|
+
let ctx;
|
|
2737
|
+
try {
|
|
2738
|
+
ctx = getHookContext();
|
|
2739
|
+
} catch (error) {
|
|
2740
|
+
return Promise.reject(error);
|
|
2741
|
+
}
|
|
2742
|
+
const join = async () => {
|
|
2743
|
+
if (ctx.workflowControlError) throw ctx.workflowControlError;
|
|
2744
|
+
if (ctx.parallelGroup)
|
|
2745
|
+
throw new Error(
|
|
2746
|
+
"Await each parallel group before starting another in the same node."
|
|
2747
|
+
);
|
|
2748
|
+
const key = `__parallel:${ctx.parallelIndex++}`;
|
|
2749
|
+
const saved = ctx.currentNodeState.byKey[key];
|
|
2750
|
+
const tasks = ctx.workflowTasks.filter(
|
|
2751
|
+
(task) => !task.completed && !task.group
|
|
2752
|
+
);
|
|
2753
|
+
const ids = tasks.map((task) => task.id);
|
|
2754
|
+
if (saved && (saved.version !== 1 || ids.some((id) => !saved.calls.includes(id)))) {
|
|
2755
|
+
throw new Error("Parallel call membership changed during replay.");
|
|
2756
|
+
}
|
|
2757
|
+
const group = {
|
|
2758
|
+
key,
|
|
2759
|
+
record: saved ?? { version: 1, calls: ids, interrupts: [] },
|
|
2760
|
+
restored: Boolean(saved),
|
|
2761
|
+
ready: Promise.resolve()
|
|
2762
|
+
};
|
|
2763
|
+
ctx.currentNodeState.byKey[key] = group.record;
|
|
2764
|
+
ctx.stateDirty = true;
|
|
2765
|
+
ctx.parallelGroup = group;
|
|
2766
|
+
for (const task of tasks) task.group = group;
|
|
2767
|
+
const callRecord = (id) => ctx.currentNodeState.byKey[`__useWorkflow:${id}`];
|
|
2768
|
+
const bridge = async (callId, index) => {
|
|
2769
|
+
const interruption = callRecord(callId)?.interrupts[index];
|
|
2770
|
+
if (!interruption)
|
|
2771
|
+
throw new Error(
|
|
2772
|
+
"Parallel interrupt history does not match the saved child."
|
|
2773
|
+
);
|
|
2774
|
+
try {
|
|
2775
|
+
interruption.response = await awaitInterruptInternal({
|
|
2776
|
+
request: interruption.request,
|
|
2777
|
+
...interruption.request.meta ? { meta: interruption.request.meta } : {}
|
|
2778
|
+
});
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
ctx.workflowControlError = error;
|
|
2781
|
+
throw error;
|
|
2782
|
+
}
|
|
2783
|
+
};
|
|
2784
|
+
group.ready = (async () => {
|
|
2785
|
+
for (const entry of group.record.interrupts)
|
|
2786
|
+
await bridge(entry.callId, entry.index);
|
|
2787
|
+
})();
|
|
2788
|
+
try {
|
|
2789
|
+
const [ready, outcomes] = await Promise.all([
|
|
2790
|
+
Promise.allSettled([group.ready]),
|
|
2791
|
+
settled
|
|
2792
|
+
]);
|
|
2793
|
+
let drained = -1;
|
|
2794
|
+
while (drained !== ctx.workflowTasks.length) {
|
|
2795
|
+
drained = ctx.workflowTasks.length;
|
|
2796
|
+
await Promise.all(
|
|
2797
|
+
ctx.workflowTasks.filter((task) => task.group === group).map((task) => task.done)
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
if (ready[0]?.status === "rejected") throw ready[0].reason;
|
|
2801
|
+
(0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
|
|
2802
|
+
if (ctx.workflowControlError) throw ctx.workflowControlError;
|
|
2803
|
+
const rejected = outcomes.filter(
|
|
2804
|
+
(outcome) => outcome.status === "rejected"
|
|
2805
|
+
);
|
|
2806
|
+
const control = rejected.find(
|
|
2807
|
+
(outcome) => (0, import_core6.isExecutionCancelled)(outcome.reason) || (0, import_core6.isExecutionLimitReached)(outcome.reason)
|
|
2808
|
+
);
|
|
2809
|
+
if (control) {
|
|
2810
|
+
ctx.workflowControlError = control.reason;
|
|
2811
|
+
throw control.reason;
|
|
2812
|
+
}
|
|
2813
|
+
const actual = ctx.workflowTasks.filter((task) => task.group === group).map((task) => task.id);
|
|
2814
|
+
if (group.record.calls.length !== actual.length || group.record.calls.some((id) => !actual.includes(id))) {
|
|
2815
|
+
throw new Error("Parallel call membership changed during replay.");
|
|
2816
|
+
}
|
|
2817
|
+
for (const callId of group.record.calls) {
|
|
2818
|
+
const record = callRecord(callId);
|
|
2819
|
+
if (record?.status !== "interrupted") continue;
|
|
2820
|
+
const index = record.interrupts.length - 1;
|
|
2821
|
+
if (record.interrupts[index]?.response !== void 0) continue;
|
|
2822
|
+
group.record.interrupts.push({ callId, index });
|
|
2823
|
+
await bridge(callId, index);
|
|
2824
|
+
throw new Error(
|
|
2825
|
+
"Parallel interrupt unexpectedly resumed without replay."
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
if (rejected.length) throw new ParallelError(outcomes);
|
|
2829
|
+
return outcomes.map(
|
|
2830
|
+
(outcome) => outcome.value
|
|
2831
|
+
);
|
|
2832
|
+
} finally {
|
|
2833
|
+
ctx.parallelGroup = void 0;
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
const promise = join();
|
|
2837
|
+
ctx.parallelJoins.push(promise);
|
|
2838
|
+
void promise.catch(() => {
|
|
2839
|
+
});
|
|
2840
|
+
return promise;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2599
2843
|
// src/workflow.ts
|
|
2600
2844
|
var import_node_crypto5 = require("crypto");
|
|
2601
|
-
var
|
|
2845
|
+
var import_core7 = require("@kortyx/core");
|
|
2602
2846
|
|
|
2603
2847
|
// src/workflow-telemetry.ts
|
|
2604
2848
|
var import_node_crypto4 = require("crypto");
|
|
@@ -2684,16 +2928,30 @@ function createWorkflowHooks(workflows) {
|
|
|
2684
2928
|
};
|
|
2685
2929
|
}
|
|
2686
2930
|
async function useWorkflow(args) {
|
|
2931
|
+
const task = registerWorkflowTask(args.id);
|
|
2932
|
+
try {
|
|
2933
|
+
await Promise.resolve();
|
|
2934
|
+
if (task.group) await task.group.ready;
|
|
2935
|
+
return await runWorkflowCall(args, task);
|
|
2936
|
+
} catch (error) {
|
|
2937
|
+
if ((0, import_core7.isExecutionCancelled)(error) || (0, import_core7.isExecutionLimitReached)(error))
|
|
2938
|
+
getHookContext().workflowControlError = error;
|
|
2939
|
+
throw error;
|
|
2940
|
+
} finally {
|
|
2941
|
+
task.finish();
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
async function runWorkflowCall(args, task) {
|
|
2687
2945
|
const ctx = getHookContext();
|
|
2688
|
-
(0,
|
|
2946
|
+
(0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
|
|
2689
2947
|
const service = ctx.node.callWorkflow;
|
|
2690
2948
|
if (!service)
|
|
2691
2949
|
throw new Error("useWorkflow requires an agent workflow registry.");
|
|
2692
2950
|
if (!args.id.trim())
|
|
2693
2951
|
throw new Error("useWorkflow requires a stable, nonempty id.");
|
|
2694
|
-
if (ctx.workflowCallActive)
|
|
2952
|
+
if (ctx.workflowCallActive && !task.group)
|
|
2695
2953
|
throw new Error(
|
|
2696
|
-
"Concurrent workflow calls
|
|
2954
|
+
"Concurrent workflow calls require parallel([...]); otherwise await each call."
|
|
2697
2955
|
);
|
|
2698
2956
|
if (ctx.workflowCallIds.has(args.id))
|
|
2699
2957
|
throw new Error(
|
|
@@ -2714,6 +2972,11 @@ async function useWorkflow(args) {
|
|
|
2714
2972
|
});
|
|
2715
2973
|
const key = `__useWorkflow:${args.id}`;
|
|
2716
2974
|
let record = ctx.currentNodeState.byKey[key];
|
|
2975
|
+
if (record && record.parallelKey !== task.group?.key)
|
|
2976
|
+
throw new WorkflowCallError(
|
|
2977
|
+
workflow,
|
|
2978
|
+
"parallel call membership changed during replay"
|
|
2979
|
+
);
|
|
2717
2980
|
if (record && record.fingerprint !== fingerprint)
|
|
2718
2981
|
throw new WorkflowCallError(
|
|
2719
2982
|
workflow,
|
|
@@ -2724,11 +2987,12 @@ async function useWorkflow(args) {
|
|
|
2724
2987
|
fingerprint,
|
|
2725
2988
|
invocationId: (0, import_node_crypto5.randomUUID)(),
|
|
2726
2989
|
status: "running",
|
|
2727
|
-
interrupts: []
|
|
2990
|
+
interrupts: [],
|
|
2991
|
+
...task.group ? { parallelKey: task.group.key } : {}
|
|
2728
2992
|
};
|
|
2729
2993
|
ctx.currentNodeState.byKey[key] = record;
|
|
2730
2994
|
ctx.stateDirty = true;
|
|
2731
|
-
ctx.workflowCallActive = true;
|
|
2995
|
+
if (!task.group) ctx.workflowCallActive = true;
|
|
2732
2996
|
const telemetry = ctx.node.workflowCallTelemetry;
|
|
2733
2997
|
const activationKey = "__useWorkflowActivation";
|
|
2734
2998
|
ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto5.randomUUID)();
|
|
@@ -2764,7 +3028,7 @@ async function useWorkflow(args) {
|
|
|
2764
3028
|
};
|
|
2765
3029
|
const cached = record.status === "completed";
|
|
2766
3030
|
try {
|
|
2767
|
-
for (const interruption of record.interrupts) {
|
|
3031
|
+
for (const interruption of task.group ? [] : record.interrupts) {
|
|
2768
3032
|
interruption.response = await awaitInterruptInternal({
|
|
2769
3033
|
request: interruption.request,
|
|
2770
3034
|
...interruption.request.meta ? { meta: interruption.request.meta } : {}
|
|
@@ -2772,6 +3036,8 @@ async function useWorkflow(args) {
|
|
|
2772
3036
|
}
|
|
2773
3037
|
if (record.status === "failed")
|
|
2774
3038
|
throw new WorkflowCallError(workflow, record.error ?? "child failed");
|
|
3039
|
+
if (task.group && record.status === "interrupted" && record.interrupts.at(-1)?.response === void 0)
|
|
3040
|
+
throw new ParallelChildWaiting();
|
|
2775
3041
|
while (record.status !== "completed") {
|
|
2776
3042
|
let outcome;
|
|
2777
3043
|
try {
|
|
@@ -2790,13 +3056,13 @@ async function useWorkflow(args) {
|
|
|
2790
3056
|
...record.interrupts.length ? { response: record.interrupts.at(-1)?.response } : {}
|
|
2791
3057
|
});
|
|
2792
3058
|
} catch (error) {
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3059
|
+
const child = error;
|
|
3060
|
+
recordUsage(child?.__kortyxChildUsage);
|
|
3061
|
+
(0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
|
|
3062
|
+
if ((0, import_core7.isExecutionCancelled)(error)) throw error;
|
|
3063
|
+
if ((0, import_core7.isExecutionLimitReached)(error)) {
|
|
3064
|
+
if (child?.__kortyxChildSnapshot)
|
|
2798
3065
|
record.snapshot = child.__kortyxChildSnapshot;
|
|
2799
|
-
recordUsage(child.__kortyxChildUsage);
|
|
2800
3066
|
report("suspended", {
|
|
2801
3067
|
reason: "limit_reached",
|
|
2802
3068
|
limit: error.limitReached
|
|
@@ -2808,7 +3074,7 @@ async function useWorkflow(args) {
|
|
|
2808
3074
|
report("failed", { error: "Child workflow failed" });
|
|
2809
3075
|
throw new WorkflowCallError(workflow, record.error);
|
|
2810
3076
|
}
|
|
2811
|
-
(0,
|
|
3077
|
+
(0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
|
|
2812
3078
|
recordUsage(outcome.usage);
|
|
2813
3079
|
if (outcome.status === "completed") {
|
|
2814
3080
|
record.data = outcome.data;
|
|
@@ -2829,6 +3095,7 @@ async function useWorkflow(args) {
|
|
|
2829
3095
|
request: outcome.request
|
|
2830
3096
|
};
|
|
2831
3097
|
record.interrupts.push(interruption);
|
|
3098
|
+
if (task.group) throw new ParallelChildWaiting();
|
|
2832
3099
|
interruption.response = await awaitInterruptInternal({
|
|
2833
3100
|
request: interruption.request,
|
|
2834
3101
|
...interruption.request.meta ? { meta: interruption.request.meta } : {}
|
|
@@ -2842,15 +3109,17 @@ async function useWorkflow(args) {
|
|
|
2842
3109
|
data: JSON.parse(JSON.stringify(data))
|
|
2843
3110
|
};
|
|
2844
3111
|
} finally {
|
|
2845
|
-
ctx.workflowCallActive = false;
|
|
3112
|
+
if (!task.group) ctx.workflowCallActive = false;
|
|
2846
3113
|
}
|
|
2847
3114
|
}
|
|
2848
3115
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2849
3116
|
0 && (module.exports = {
|
|
3117
|
+
ParallelError,
|
|
2850
3118
|
WorkflowCallError,
|
|
2851
3119
|
completeResponse,
|
|
2852
3120
|
createWorkflowHooks,
|
|
2853
3121
|
emitWorkflowCall,
|
|
3122
|
+
parallel,
|
|
2854
3123
|
runWithHookContext,
|
|
2855
3124
|
useAbortSignal,
|
|
2856
3125
|
useInterrupt,
|