@hexabot-ai/agentic 3.1.2-alpha.9 → 3.1.2-beta.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/README.md +13 -5
- package/dist/cjs/action/abstract-action.js +8 -3
- package/dist/cjs/dsl.types.js +2 -1
- package/dist/cjs/errors.js +41 -0
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/step-executors/parallel-executor.js +238 -24
- package/dist/cjs/step-executors/skip-helpers.js +25 -1
- package/dist/cjs/step-executors/task-executor.js +77 -10
- package/dist/cjs/suspension-rebuilder.js +1 -24
- package/dist/cjs/utils/naming.js +4 -4
- package/dist/cjs/utils/timeout.js +63 -8
- package/dist/cjs/utils/workflow-definition-resources.js +73 -0
- package/dist/cjs/workflow-compiler.js +0 -2
- package/dist/cjs/workflow-runner.js +40 -17
- package/dist/esm/action/abstract-action.js +8 -3
- package/dist/esm/dsl.types.js +2 -1
- package/dist/esm/errors.js +33 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/step-executors/parallel-executor.js +239 -25
- package/dist/esm/step-executors/skip-helpers.js +23 -0
- package/dist/esm/step-executors/task-executor.js +77 -10
- package/dist/esm/suspension-rebuilder.js +1 -24
- package/dist/esm/utils/naming.js +1 -1
- package/dist/esm/utils/timeout.js +63 -8
- package/dist/esm/utils/workflow-definition-resources.js +68 -0
- package/dist/esm/workflow-compiler.js +0 -2
- package/dist/esm/workflow-runner.js +40 -17
- package/dist/types/action/abstract-action.d.ts +1 -1
- package/dist/types/action/abstract-action.d.ts.map +1 -1
- package/dist/types/action/action.types.d.ts +2 -1
- package/dist/types/action/action.types.d.ts.map +1 -1
- package/dist/types/context.d.ts +2 -1
- package/dist/types/context.d.ts.map +1 -1
- package/dist/types/dsl.types.d.ts.map +1 -1
- package/dist/types/errors.d.ts +10 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/step-executors/parallel-executor.d.ts +3 -4
- package/dist/types/step-executors/parallel-executor.d.ts.map +1 -1
- package/dist/types/step-executors/skip-helpers.d.ts +1 -0
- package/dist/types/step-executors/skip-helpers.d.ts.map +1 -1
- package/dist/types/step-executors/task-executor.d.ts.map +1 -1
- package/dist/types/step-executors/types.d.ts +9 -1
- package/dist/types/step-executors/types.d.ts.map +1 -1
- package/dist/types/suspension-rebuilder.d.ts.map +1 -1
- package/dist/types/utils/naming.d.ts +1 -0
- package/dist/types/utils/naming.d.ts.map +1 -1
- package/dist/types/utils/timeout.d.ts +4 -2
- package/dist/types/utils/timeout.d.ts.map +1 -1
- package/dist/types/utils/workflow-definition-resources.d.ts +16 -0
- package/dist/types/utils/workflow-definition-resources.d.ts.map +1 -0
- package/dist/types/workflow-compiler.d.ts.map +1 -1
- package/dist/types/workflow-event-emitter.d.ts +12 -13
- package/dist/types/workflow-event-emitter.d.ts.map +1 -1
- package/dist/types/workflow-runner.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/suspension-rebuilder.test.ts +3 -42
- package/src/__tests__/workflow-definition-resources.test.ts +97 -0
- package/src/__tests__/workflow-runner.test.ts +58 -5
- package/src/action/__tests__/action.test.ts +2 -1
- package/src/action/abstract-action.ts +9 -1
- package/src/action/action.types.ts +2 -0
- package/src/context.ts +2 -0
- package/src/dsl.types.ts +5 -1
- package/src/errors.ts +43 -0
- package/src/index.ts +10 -0
- package/src/step-executors/conditional-executor.test.ts +8 -3
- package/src/step-executors/loop-executor.test.ts +8 -3
- package/src/step-executors/parallel-executor.test.ts +191 -82
- package/src/step-executors/parallel-executor.ts +402 -34
- package/src/step-executors/skip-helpers.ts +41 -0
- package/src/step-executors/task-executor.test.ts +85 -21
- package/src/step-executors/task-executor.ts +94 -10
- package/src/step-executors/types.ts +14 -1
- package/src/suspension-rebuilder.ts +1 -53
- package/src/utils/naming.ts +1 -1
- package/src/utils/timeout.ts +78 -8
- package/src/utils/workflow-definition-resources.ts +111 -0
- package/src/workflow-compiler.ts +0 -3
- package/src/workflow-event-emitter.ts +13 -6
- package/src/workflow-runner.ts +59 -19
|
@@ -4,67 +4,435 @@
|
|
|
4
4
|
* Full terms: see LICENSE.md.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { BaseWorkflowContext, WorkflowRuntimeControl } from '../context';
|
|
8
|
+
import {
|
|
9
|
+
getAbortReason,
|
|
10
|
+
ParallelSuspensionError,
|
|
11
|
+
throwIfAborted,
|
|
12
|
+
WorkflowCancellationError,
|
|
13
|
+
} from '../errors';
|
|
7
14
|
import type {
|
|
15
|
+
CompiledStep,
|
|
8
16
|
ExecutionState,
|
|
9
17
|
ParallelStep,
|
|
10
18
|
Suspension,
|
|
11
19
|
} from '../workflow-types';
|
|
12
20
|
|
|
13
|
-
import {
|
|
14
|
-
import { wrapSuspensionContinuation } from './suspension-continuation';
|
|
21
|
+
import { markStepsCancelled } from './skip-helpers';
|
|
15
22
|
import type { StepExecutorEnv } from './types';
|
|
16
23
|
|
|
24
|
+
type BranchResult =
|
|
25
|
+
| { status: 'fulfilled'; index: number; outputDelta: Record<string, unknown> }
|
|
26
|
+
| { status: 'failed'; index: number; error: unknown }
|
|
27
|
+
| { status: 'cancelled'; index: number; error: Error };
|
|
28
|
+
|
|
29
|
+
type BranchHandle = {
|
|
30
|
+
index: number;
|
|
31
|
+
step: CompiledStep;
|
|
32
|
+
iterationStack: number[];
|
|
33
|
+
controller: AbortController;
|
|
34
|
+
promise: Promise<BranchResult>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type TrackedOutput = {
|
|
38
|
+
output: Record<string, unknown>;
|
|
39
|
+
getDelta: () => Record<string, unknown>;
|
|
40
|
+
};
|
|
41
|
+
|
|
17
42
|
/**
|
|
18
|
-
* Execute a set of steps
|
|
43
|
+
* Execute a set of child steps concurrently and resolve according to the configured strategy.
|
|
19
44
|
*
|
|
20
45
|
* @param env Executor environment providing helpers and workflow context.
|
|
21
46
|
* @param step The parallel step definition including strategy and children.
|
|
22
47
|
* @param state Mutable workflow execution state.
|
|
23
48
|
* @param path Path tokens locating this parallel block in the workflow.
|
|
24
|
-
* @
|
|
25
|
-
* @returns A suspension if a child pauses execution, otherwise void.
|
|
49
|
+
* @returns Always undefined; suspensions are rejected inside parallel blocks.
|
|
26
50
|
*/
|
|
27
51
|
export async function executeParallel(
|
|
28
52
|
env: StepExecutorEnv,
|
|
29
53
|
step: ParallelStep,
|
|
30
54
|
state: ExecutionState,
|
|
31
55
|
path: Array<number | string>,
|
|
32
|
-
startIndex = 0,
|
|
33
56
|
): Promise<Suspension | void> {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
if (step.steps.length === 0) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throwIfAborted(env.signal);
|
|
62
|
+
|
|
63
|
+
const baseOutput = cloneRecord(state.output);
|
|
64
|
+
const baseContextState = cloneRecord(env.context.state);
|
|
65
|
+
const branches = step.steps.map((child, index) =>
|
|
66
|
+
launchBranch({
|
|
67
|
+
env,
|
|
68
|
+
child,
|
|
69
|
+
index,
|
|
70
|
+
path,
|
|
71
|
+
parentState: state,
|
|
72
|
+
baseOutput,
|
|
73
|
+
baseContextState,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (step.strategy === 'wait_any') {
|
|
78
|
+
return executeWaitAny(env, branches, state);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return executeWaitAll(env, branches, state);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function executeWaitAll(
|
|
85
|
+
env: StepExecutorEnv,
|
|
86
|
+
branches: BranchHandle[],
|
|
87
|
+
state: ExecutionState,
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
const pending = new Set(branches);
|
|
90
|
+
const outcomes = new Map<number, BranchResult>();
|
|
91
|
+
|
|
92
|
+
while (pending.size > 0) {
|
|
93
|
+
const { branch, result } = await raceNext(pending);
|
|
94
|
+
pending.delete(branch);
|
|
95
|
+
outcomes.set(result.index, result);
|
|
96
|
+
|
|
97
|
+
if (result.status === 'failed' || result.status === 'cancelled') {
|
|
98
|
+
cancelBranches(
|
|
99
|
+
env,
|
|
100
|
+
pending,
|
|
101
|
+
result.error,
|
|
102
|
+
'Parallel branch execution was cancelled.',
|
|
103
|
+
);
|
|
104
|
+
await settleBranches(branches);
|
|
105
|
+
throw result.error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
mergeBranchOutputs(state, outcomes);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function executeWaitAny(
|
|
113
|
+
env: StepExecutorEnv,
|
|
114
|
+
branches: BranchHandle[],
|
|
115
|
+
state: ExecutionState,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
const pending = new Set(branches);
|
|
118
|
+
const outcomes = new Map<number, BranchResult>();
|
|
119
|
+
|
|
120
|
+
while (pending.size > 0) {
|
|
121
|
+
const { branch, result } = await raceNext(pending);
|
|
122
|
+
pending.delete(branch);
|
|
123
|
+
outcomes.set(result.index, result);
|
|
124
|
+
|
|
125
|
+
if (result.status === 'fulfilled') {
|
|
126
|
+
cancelBranches(
|
|
127
|
+
env,
|
|
128
|
+
pending,
|
|
129
|
+
new WorkflowCancellationError('Parallel wait_any winner selected.'),
|
|
130
|
+
'Parallel wait_any branch lost the race.',
|
|
131
|
+
);
|
|
132
|
+
await settleBranches(branches);
|
|
133
|
+
mergeBranchOutputs(state, new Map([[result.index, result]]));
|
|
134
|
+
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
cancelBranches(
|
|
139
|
+
env,
|
|
140
|
+
pending,
|
|
141
|
+
result.error,
|
|
142
|
+
'Parallel branch execution was cancelled.',
|
|
143
|
+
);
|
|
144
|
+
await settleBranches(branches);
|
|
145
|
+
throw result.error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function launchBranch({
|
|
150
|
+
env,
|
|
151
|
+
child,
|
|
152
|
+
index,
|
|
153
|
+
path,
|
|
154
|
+
parentState,
|
|
155
|
+
baseOutput,
|
|
156
|
+
baseContextState,
|
|
157
|
+
}: {
|
|
158
|
+
env: StepExecutorEnv;
|
|
159
|
+
child: CompiledStep;
|
|
160
|
+
index: number;
|
|
161
|
+
path: Array<number | string>;
|
|
162
|
+
parentState: ExecutionState;
|
|
163
|
+
baseOutput: Record<string, unknown>;
|
|
164
|
+
baseContextState: Record<string, unknown>;
|
|
165
|
+
}): BranchHandle {
|
|
166
|
+
const controller = createBranchController(env.signal);
|
|
167
|
+
const trackedOutput = createTrackedOutput(baseOutput);
|
|
168
|
+
const branchState: ExecutionState = {
|
|
169
|
+
input: parentState.input,
|
|
170
|
+
output: trackedOutput.output,
|
|
171
|
+
iteration: parentState.iteration ? { ...parentState.iteration } : undefined,
|
|
172
|
+
accumulator: cloneValue(parentState.accumulator),
|
|
173
|
+
iterationStack: [...parentState.iterationStack],
|
|
174
|
+
};
|
|
175
|
+
const branchContext = createParallelBranchContext(
|
|
176
|
+
env.context,
|
|
177
|
+
cloneRecord(baseContextState),
|
|
178
|
+
);
|
|
179
|
+
const branchEnv = env.fork({
|
|
180
|
+
context: branchContext,
|
|
181
|
+
signal: controller.signal,
|
|
182
|
+
setCurrentStep: () => undefined,
|
|
183
|
+
});
|
|
184
|
+
const childPath = [...path, 'parallel', index];
|
|
185
|
+
const promise = runBranch(
|
|
186
|
+
branchEnv,
|
|
187
|
+
child,
|
|
188
|
+
branchState,
|
|
189
|
+
childPath,
|
|
190
|
+
trackedOutput,
|
|
191
|
+
index,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
index,
|
|
196
|
+
step: child,
|
|
197
|
+
iterationStack: [...parentState.iterationStack],
|
|
198
|
+
controller,
|
|
199
|
+
promise,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function runBranch(
|
|
204
|
+
env: StepExecutorEnv,
|
|
205
|
+
child: CompiledStep,
|
|
206
|
+
branchState: ExecutionState,
|
|
207
|
+
childPath: Array<number | string>,
|
|
208
|
+
trackedOutput: TrackedOutput,
|
|
209
|
+
index: number,
|
|
210
|
+
): Promise<BranchResult> {
|
|
211
|
+
try {
|
|
212
|
+
throwIfAborted(env.signal);
|
|
213
|
+
|
|
214
|
+
const suspension = await env.executeStep(child, branchState, childPath);
|
|
38
215
|
if (suspension) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return executeParallel(env, step, state, path, index + 1);
|
|
53
|
-
});
|
|
216
|
+
throw new ParallelSuspensionError();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throwIfAborted(env.signal);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
status: 'fulfilled',
|
|
223
|
+
index,
|
|
224
|
+
outputDelta: trackedOutput.getDelta(),
|
|
225
|
+
};
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (env.signal.aborted) {
|
|
228
|
+
return { status: 'cancelled', index, error: getAbortReason(env.signal) };
|
|
54
229
|
}
|
|
55
230
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
231
|
+
return { status: 'failed', index, error };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function raceNext(
|
|
236
|
+
pending: Set<BranchHandle>,
|
|
237
|
+
): Promise<{ branch: BranchHandle; result: BranchResult }> {
|
|
238
|
+
return Promise.race(
|
|
239
|
+
[...pending].map((branch) =>
|
|
240
|
+
branch.promise.then((result) => ({ branch, result })),
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function settleBranches(branches: BranchHandle[]): Promise<void> {
|
|
246
|
+
await Promise.all(branches.map((branch) => branch.promise.catch(() => null)));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function cancelBranches(
|
|
250
|
+
env: StepExecutorEnv,
|
|
251
|
+
branches: Iterable<BranchHandle>,
|
|
252
|
+
reason: unknown,
|
|
253
|
+
snapshotReason: string,
|
|
254
|
+
): void {
|
|
255
|
+
for (const branch of branches) {
|
|
256
|
+
if (!branch.controller.signal.aborted) {
|
|
257
|
+
branch.controller.abort(
|
|
258
|
+
reason instanceof Error
|
|
259
|
+
? reason
|
|
260
|
+
: new WorkflowCancellationError(String(reason)),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
markStepsCancelled(
|
|
265
|
+
env,
|
|
266
|
+
[branch.step],
|
|
267
|
+
branch.iterationStack,
|
|
268
|
+
reason instanceof Error ? reason.message : snapshotReason,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function mergeBranchOutputs(
|
|
274
|
+
state: ExecutionState,
|
|
275
|
+
outcomes: Map<number, BranchResult>,
|
|
276
|
+
): void {
|
|
277
|
+
const ordered = [...outcomes.values()].sort((left, right) => {
|
|
278
|
+
return left.index - right.index;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
for (const outcome of ordered) {
|
|
282
|
+
if (outcome.status !== 'fulfilled') {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
Object.assign(state.output, outcome.outputDelta);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function createBranchController(parentSignal: AbortSignal): AbortController {
|
|
291
|
+
const controller = new AbortController();
|
|
292
|
+
|
|
293
|
+
if (parentSignal.aborted) {
|
|
294
|
+
controller.abort(getAbortReason(parentSignal));
|
|
295
|
+
|
|
296
|
+
return controller;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
parentSignal.addEventListener(
|
|
300
|
+
'abort',
|
|
301
|
+
() => controller.abort(getAbortReason(parentSignal)),
|
|
302
|
+
{ once: true },
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
return controller;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function createTrackedOutput(
|
|
309
|
+
baseOutput: Record<string, unknown>,
|
|
310
|
+
): TrackedOutput {
|
|
311
|
+
const writtenKeys = new Set<string>();
|
|
312
|
+
const target = cloneRecord(baseOutput);
|
|
313
|
+
const output = new Proxy(target, {
|
|
314
|
+
set(record, property, value) {
|
|
315
|
+
if (typeof property === 'string') {
|
|
316
|
+
writtenKeys.add(property);
|
|
63
317
|
}
|
|
64
318
|
|
|
65
|
-
return
|
|
319
|
+
return Reflect.set(record, property, value);
|
|
320
|
+
},
|
|
321
|
+
deleteProperty(record, property) {
|
|
322
|
+
if (typeof property === 'string') {
|
|
323
|
+
writtenKeys.add(property);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return Reflect.deleteProperty(record, property);
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
output,
|
|
332
|
+
getDelta: () =>
|
|
333
|
+
Object.fromEntries(
|
|
334
|
+
[...writtenKeys]
|
|
335
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(target, key))
|
|
336
|
+
.map((key) => [key, target[key]]),
|
|
337
|
+
),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
class ParallelWorkflowRuntimeControl implements WorkflowRuntimeControl {
|
|
342
|
+
constructor(private readonly getParent: () => WorkflowRuntimeControl) {}
|
|
343
|
+
|
|
344
|
+
get status() {
|
|
345
|
+
return this.getParent().status;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
get resumeData() {
|
|
349
|
+
return this.getParent().resumeData;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
suspend<T = unknown>(): Promise<T> {
|
|
353
|
+
return Promise.reject(new ParallelSuspensionError());
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
resume(data?: unknown): void {
|
|
357
|
+
this.getParent().resume(data);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
getSnapshot() {
|
|
361
|
+
return this.getParent().getSnapshot();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function createParallelBranchContext(
|
|
366
|
+
context: BaseWorkflowContext,
|
|
367
|
+
branchState: Record<string, unknown>,
|
|
368
|
+
): BaseWorkflowContext {
|
|
369
|
+
let state = branchState;
|
|
370
|
+
const workflow = new ParallelWorkflowRuntimeControl(() => context.workflow);
|
|
371
|
+
|
|
372
|
+
return new Proxy(context, {
|
|
373
|
+
get(target, property, receiver) {
|
|
374
|
+
if (property === 'state') {
|
|
375
|
+
return state;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (property === 'workflow') {
|
|
379
|
+
return workflow;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (property === 'snapshot') {
|
|
383
|
+
return () => cloneRecord(state);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (property === 'attachWorkflowRuntime') {
|
|
387
|
+
return () => undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return Reflect.get(target, property, receiver);
|
|
391
|
+
},
|
|
392
|
+
set(target, property, value, receiver) {
|
|
393
|
+
if (property === 'state') {
|
|
394
|
+
state = isRecord(value) ? value : {};
|
|
395
|
+
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return Reflect.set(target, property, value, receiver);
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function cloneRecord(value: Record<string, unknown>): Record<string, unknown> {
|
|
405
|
+
return cloneValue(value) as Record<string, unknown>;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function cloneValue<T>(value: T): T {
|
|
409
|
+
if (value === undefined || value === null) {
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (typeof structuredClone === 'function') {
|
|
414
|
+
try {
|
|
415
|
+
return structuredClone(value);
|
|
416
|
+
} catch {
|
|
417
|
+
// Fall through to JSON clone below.
|
|
66
418
|
}
|
|
67
419
|
}
|
|
68
420
|
|
|
69
|
-
|
|
421
|
+
try {
|
|
422
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
423
|
+
} catch {
|
|
424
|
+
if (Array.isArray(value)) {
|
|
425
|
+
return [...value] as T;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (isRecord(value)) {
|
|
429
|
+
return { ...value } as T;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
437
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
70
438
|
}
|
|
@@ -57,6 +57,38 @@ const markStepSkipped = (
|
|
|
57
57
|
break;
|
|
58
58
|
}
|
|
59
59
|
};
|
|
60
|
+
const markStepCancelled = (
|
|
61
|
+
env: SkipStepEnv,
|
|
62
|
+
step: CompiledStep,
|
|
63
|
+
iterationStack: number[],
|
|
64
|
+
reason?: string,
|
|
65
|
+
) => {
|
|
66
|
+
const stepInfo = env.buildInstanceStepInfo(step, iterationStack);
|
|
67
|
+
env.markSnapshot(stepInfo, 'cancelled', reason);
|
|
68
|
+
env.emit('hook:step:cancelled', {
|
|
69
|
+
runId: env.runId,
|
|
70
|
+
step: stepInfo,
|
|
71
|
+
error: new Error(reason ?? 'Step execution was cancelled.'),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
switch (step.type) {
|
|
75
|
+
case StepType.Parallel:
|
|
76
|
+
step.steps.forEach((child) =>
|
|
77
|
+
markStepCancelled(env, child, iterationStack, reason),
|
|
78
|
+
);
|
|
79
|
+
break;
|
|
80
|
+
case StepType.Conditional:
|
|
81
|
+
step.branches.forEach((branch) =>
|
|
82
|
+
branch.steps.forEach((child) =>
|
|
83
|
+
markStepCancelled(env, child, iterationStack, reason),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
break;
|
|
87
|
+
case StepType.Loop:
|
|
88
|
+
case StepType.Task:
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
60
92
|
|
|
61
93
|
export const markStepsSkipped = (
|
|
62
94
|
env: SkipStepEnv,
|
|
@@ -66,3 +98,12 @@ export const markStepsSkipped = (
|
|
|
66
98
|
) => {
|
|
67
99
|
steps.forEach((step) => markStepSkipped(env, step, iterationStack, reason));
|
|
68
100
|
};
|
|
101
|
+
|
|
102
|
+
export const markStepsCancelled = (
|
|
103
|
+
env: SkipStepEnv,
|
|
104
|
+
steps: CompiledStep[],
|
|
105
|
+
iterationStack: number[],
|
|
106
|
+
reason?: string,
|
|
107
|
+
) => {
|
|
108
|
+
steps.forEach((step) => markStepCancelled(env, step, iterationStack, reason));
|
|
109
|
+
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Action } from '../action/action.types';
|
|
8
|
-
import { BaseWorkflowContext } from '../context';
|
|
8
|
+
import { BaseWorkflowContext, type StepExecutionRecord } from '../context';
|
|
9
9
|
import type { RuntimeSuspensionRequest } from '../runner-runtime-control';
|
|
10
10
|
import { createDeferred } from '../utils/deferred';
|
|
11
11
|
import {
|
|
@@ -72,26 +72,59 @@ const createCompiled = (task: CompiledTask): CompiledWorkflow =>
|
|
|
72
72
|
const createEnv = (
|
|
73
73
|
compiled: CompiledWorkflow,
|
|
74
74
|
stepInfo: StepInfo,
|
|
75
|
-
): StepExecutorEnv =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
75
|
+
): StepExecutorEnv => {
|
|
76
|
+
const stepRecords: Record<string, StepExecutionRecord> = {};
|
|
77
|
+
const env = {
|
|
78
|
+
compiled,
|
|
79
|
+
context: new TestContext(),
|
|
80
|
+
signal: new AbortController().signal,
|
|
81
|
+
runId: 'run-123',
|
|
82
|
+
buildInstanceStepInfo: jest.fn().mockReturnValue(stepInfo),
|
|
83
|
+
markSnapshot: jest.fn(),
|
|
84
|
+
recordStepExecution: jest.fn((step, update) => {
|
|
85
|
+
const existing =
|
|
86
|
+
stepRecords[step.id] ??
|
|
87
|
+
({
|
|
88
|
+
id: step.id,
|
|
89
|
+
name: step.name,
|
|
90
|
+
status: 'pending',
|
|
91
|
+
} satisfies StepExecutionRecord);
|
|
92
|
+
const context =
|
|
93
|
+
existing.context || update.context
|
|
94
|
+
? { ...existing.context, ...update.context }
|
|
95
|
+
: undefined;
|
|
96
|
+
const nextRecord = {
|
|
97
|
+
...existing,
|
|
98
|
+
...update,
|
|
99
|
+
id: step.id,
|
|
100
|
+
name: step.name,
|
|
101
|
+
status: update.status ?? existing.status,
|
|
102
|
+
context,
|
|
103
|
+
} satisfies StepExecutionRecord;
|
|
104
|
+
|
|
105
|
+
stepRecords[step.id] = nextRecord;
|
|
106
|
+
|
|
107
|
+
return nextRecord;
|
|
108
|
+
}),
|
|
109
|
+
emit: jest.fn(),
|
|
110
|
+
setCurrentStep: jest.fn(),
|
|
111
|
+
waitForStepSuspension: jest
|
|
112
|
+
.fn()
|
|
113
|
+
.mockImplementation(
|
|
114
|
+
() => new Promise<RuntimeSuspensionRequest>(() => undefined),
|
|
115
|
+
),
|
|
116
|
+
clearStepSuspensions: jest.fn(),
|
|
117
|
+
primeStepResumeData: jest.fn(),
|
|
118
|
+
captureTaskOutput: jest.fn().mockResolvedValue(undefined),
|
|
119
|
+
executeFlow: jest.fn(),
|
|
120
|
+
executeStep: jest.fn(),
|
|
121
|
+
fork: jest.fn(),
|
|
122
|
+
} as StepExecutorEnv;
|
|
123
|
+
|
|
124
|
+
env.fork = jest.fn((overrides) => ({ ...env, ...overrides }));
|
|
125
|
+
|
|
126
|
+
return env;
|
|
127
|
+
};
|
|
95
128
|
const step: TaskStep = {
|
|
96
129
|
id: '0:test_task',
|
|
97
130
|
type: StepType.Task,
|
|
@@ -122,6 +155,7 @@ describe('executeTaskStep', () => {
|
|
|
122
155
|
env.context,
|
|
123
156
|
task.settings,
|
|
124
157
|
task.bindings,
|
|
158
|
+
env.signal,
|
|
125
159
|
);
|
|
126
160
|
expect(env.setCurrentStep).toHaveBeenNthCalledWith(1, stepInfo);
|
|
127
161
|
expect(env.markSnapshot).toHaveBeenNthCalledWith(1, stepInfo, 'running');
|
|
@@ -153,10 +187,18 @@ describe('executeTaskStep', () => {
|
|
|
153
187
|
expect(env.emit).toHaveBeenCalledWith('hook:step:start', {
|
|
154
188
|
runId: 'run-123',
|
|
155
189
|
step: stepInfo,
|
|
190
|
+
stepExecution: expect.objectContaining({
|
|
191
|
+
id: stepInfo.id,
|
|
192
|
+
status: 'running',
|
|
193
|
+
}),
|
|
156
194
|
});
|
|
157
195
|
expect(env.emit).toHaveBeenCalledWith('hook:step:success', {
|
|
158
196
|
runId: 'run-123',
|
|
159
197
|
step: stepInfo,
|
|
198
|
+
stepExecution: expect.objectContaining({
|
|
199
|
+
id: stepInfo.id,
|
|
200
|
+
status: 'completed',
|
|
201
|
+
}),
|
|
160
202
|
});
|
|
161
203
|
expect(env.setCurrentStep).toHaveBeenLastCalledWith(undefined);
|
|
162
204
|
});
|
|
@@ -215,6 +257,16 @@ describe('executeTaskStep', () => {
|
|
|
215
257
|
'suspended',
|
|
216
258
|
'awaiting_user',
|
|
217
259
|
);
|
|
260
|
+
expect(env.emit).toHaveBeenCalledWith('hook:step:suspended', {
|
|
261
|
+
runId: 'run-123',
|
|
262
|
+
step: stepInfo,
|
|
263
|
+
stepExecution: expect.objectContaining({
|
|
264
|
+
id: stepInfo.id,
|
|
265
|
+
status: 'suspended',
|
|
266
|
+
}),
|
|
267
|
+
reason: 'awaiting_user',
|
|
268
|
+
data: { channel: 'sms' },
|
|
269
|
+
});
|
|
218
270
|
|
|
219
271
|
await suspension?.continue({ reply: 'Sure' });
|
|
220
272
|
|
|
@@ -228,6 +280,14 @@ describe('executeTaskStep', () => {
|
|
|
228
280
|
output: { reply: 'SURE' },
|
|
229
281
|
}),
|
|
230
282
|
);
|
|
283
|
+
expect(env.emit).toHaveBeenCalledWith('hook:step:success', {
|
|
284
|
+
runId: 'run-123',
|
|
285
|
+
step: stepInfo,
|
|
286
|
+
stepExecution: expect.objectContaining({
|
|
287
|
+
id: stepInfo.id,
|
|
288
|
+
status: 'completed',
|
|
289
|
+
}),
|
|
290
|
+
});
|
|
231
291
|
});
|
|
232
292
|
|
|
233
293
|
it('marks failure and rethrows errors from the task', async () => {
|
|
@@ -250,6 +310,10 @@ describe('executeTaskStep', () => {
|
|
|
250
310
|
expect(env.emit).toHaveBeenCalledWith('hook:step:error', {
|
|
251
311
|
runId: 'run-123',
|
|
252
312
|
step: stepInfo,
|
|
313
|
+
stepExecution: expect.objectContaining({
|
|
314
|
+
id: stepInfo.id,
|
|
315
|
+
status: 'failed',
|
|
316
|
+
}),
|
|
253
317
|
error: expect.any(Error),
|
|
254
318
|
});
|
|
255
319
|
expect(env.setCurrentStep).toHaveBeenLastCalledWith(undefined);
|