@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
|
@@ -3,40 +3,254 @@
|
|
|
3
3
|
* Copyright (c) 2025 Hexastack.
|
|
4
4
|
* Full terms: see LICENSE.md.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { getAbortReason, ParallelSuspensionError, throwIfAborted, WorkflowCancellationError, } from '../errors';
|
|
7
|
+
import { markStepsCancelled } from './skip-helpers';
|
|
8
8
|
/**
|
|
9
|
-
* Execute a set of steps
|
|
9
|
+
* Execute a set of child steps concurrently and resolve according to the configured strategy.
|
|
10
10
|
*
|
|
11
11
|
* @param env Executor environment providing helpers and workflow context.
|
|
12
12
|
* @param step The parallel step definition including strategy and children.
|
|
13
13
|
* @param state Mutable workflow execution state.
|
|
14
14
|
* @param path Path tokens locating this parallel block in the workflow.
|
|
15
|
-
* @
|
|
16
|
-
* @returns A suspension if a child pauses execution, otherwise void.
|
|
15
|
+
* @returns Always undefined; suspensions are rejected inside parallel blocks.
|
|
17
16
|
*/
|
|
18
|
-
export async function executeParallel(env, step, state, path
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
export async function executeParallel(env, step, state, path) {
|
|
18
|
+
if (step.steps.length === 0) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
throwIfAborted(env.signal);
|
|
22
|
+
const baseOutput = cloneRecord(state.output);
|
|
23
|
+
const baseContextState = cloneRecord(env.context.state);
|
|
24
|
+
const branches = step.steps.map((child, index) => launchBranch({
|
|
25
|
+
env,
|
|
26
|
+
child,
|
|
27
|
+
index,
|
|
28
|
+
path,
|
|
29
|
+
parentState: state,
|
|
30
|
+
baseOutput,
|
|
31
|
+
baseContextState,
|
|
32
|
+
}));
|
|
33
|
+
if (step.strategy === 'wait_any') {
|
|
34
|
+
return executeWaitAny(env, branches, state);
|
|
35
|
+
}
|
|
36
|
+
return executeWaitAll(env, branches, state);
|
|
37
|
+
}
|
|
38
|
+
async function executeWaitAll(env, branches, state) {
|
|
39
|
+
const pending = new Set(branches);
|
|
40
|
+
const outcomes = new Map();
|
|
41
|
+
while (pending.size > 0) {
|
|
42
|
+
const { branch, result } = await raceNext(pending);
|
|
43
|
+
pending.delete(branch);
|
|
44
|
+
outcomes.set(result.index, result);
|
|
45
|
+
if (result.status === 'failed' || result.status === 'cancelled') {
|
|
46
|
+
cancelBranches(env, pending, result.error, 'Parallel branch execution was cancelled.');
|
|
47
|
+
await settleBranches(branches);
|
|
48
|
+
throw result.error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
mergeBranchOutputs(state, outcomes);
|
|
52
|
+
}
|
|
53
|
+
async function executeWaitAny(env, branches, state) {
|
|
54
|
+
const pending = new Set(branches);
|
|
55
|
+
const outcomes = new Map();
|
|
56
|
+
while (pending.size > 0) {
|
|
57
|
+
const { branch, result } = await raceNext(pending);
|
|
58
|
+
pending.delete(branch);
|
|
59
|
+
outcomes.set(result.index, result);
|
|
60
|
+
if (result.status === 'fulfilled') {
|
|
61
|
+
cancelBranches(env, pending, new WorkflowCancellationError('Parallel wait_any winner selected.'), 'Parallel wait_any branch lost the race.');
|
|
62
|
+
await settleBranches(branches);
|
|
63
|
+
mergeBranchOutputs(state, new Map([[result.index, result]]));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
cancelBranches(env, pending, result.error, 'Parallel branch execution was cancelled.');
|
|
67
|
+
await settleBranches(branches);
|
|
68
|
+
throw result.error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function launchBranch({ env, child, index, path, parentState, baseOutput, baseContextState, }) {
|
|
72
|
+
const controller = createBranchController(env.signal);
|
|
73
|
+
const trackedOutput = createTrackedOutput(baseOutput);
|
|
74
|
+
const branchState = {
|
|
75
|
+
input: parentState.input,
|
|
76
|
+
output: trackedOutput.output,
|
|
77
|
+
iteration: parentState.iteration ? { ...parentState.iteration } : undefined,
|
|
78
|
+
accumulator: cloneValue(parentState.accumulator),
|
|
79
|
+
iterationStack: [...parentState.iterationStack],
|
|
80
|
+
};
|
|
81
|
+
const branchContext = createParallelBranchContext(env.context, cloneRecord(baseContextState));
|
|
82
|
+
const branchEnv = env.fork({
|
|
83
|
+
context: branchContext,
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
setCurrentStep: () => undefined,
|
|
86
|
+
});
|
|
87
|
+
const childPath = [...path, 'parallel', index];
|
|
88
|
+
const promise = runBranch(branchEnv, child, branchState, childPath, trackedOutput, index);
|
|
89
|
+
return {
|
|
90
|
+
index,
|
|
91
|
+
step: child,
|
|
92
|
+
iterationStack: [...parentState.iterationStack],
|
|
93
|
+
controller,
|
|
94
|
+
promise,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function runBranch(env, child, branchState, childPath, trackedOutput, index) {
|
|
98
|
+
try {
|
|
99
|
+
throwIfAborted(env.signal);
|
|
100
|
+
const suspension = await env.executeStep(child, branchState, childPath);
|
|
23
101
|
if (suspension) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
102
|
+
throw new ParallelSuspensionError();
|
|
103
|
+
}
|
|
104
|
+
throwIfAborted(env.signal);
|
|
105
|
+
return {
|
|
106
|
+
status: 'fulfilled',
|
|
107
|
+
index,
|
|
108
|
+
outputDelta: trackedOutput.getDelta(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (env.signal.aborted) {
|
|
113
|
+
return { status: 'cancelled', index, error: getAbortReason(env.signal) };
|
|
114
|
+
}
|
|
115
|
+
return { status: 'failed', index, error };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function raceNext(pending) {
|
|
119
|
+
return Promise.race([...pending].map((branch) => branch.promise.then((result) => ({ branch, result }))));
|
|
120
|
+
}
|
|
121
|
+
async function settleBranches(branches) {
|
|
122
|
+
await Promise.all(branches.map((branch) => branch.promise.catch(() => null)));
|
|
123
|
+
}
|
|
124
|
+
function cancelBranches(env, branches, reason, snapshotReason) {
|
|
125
|
+
for (const branch of branches) {
|
|
126
|
+
if (!branch.controller.signal.aborted) {
|
|
127
|
+
branch.controller.abort(reason instanceof Error
|
|
128
|
+
? reason
|
|
129
|
+
: new WorkflowCancellationError(String(reason)));
|
|
130
|
+
}
|
|
131
|
+
markStepsCancelled(env, [branch.step], branch.iterationStack, reason instanceof Error ? reason.message : snapshotReason);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function mergeBranchOutputs(state, outcomes) {
|
|
135
|
+
const ordered = [...outcomes.values()].sort((left, right) => {
|
|
136
|
+
return left.index - right.index;
|
|
137
|
+
});
|
|
138
|
+
for (const outcome of ordered) {
|
|
139
|
+
if (outcome.status !== 'fulfilled') {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
Object.assign(state.output, outcome.outputDelta);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function createBranchController(parentSignal) {
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
if (parentSignal.aborted) {
|
|
148
|
+
controller.abort(getAbortReason(parentSignal));
|
|
149
|
+
return controller;
|
|
150
|
+
}
|
|
151
|
+
parentSignal.addEventListener('abort', () => controller.abort(getAbortReason(parentSignal)), { once: true });
|
|
152
|
+
return controller;
|
|
153
|
+
}
|
|
154
|
+
function createTrackedOutput(baseOutput) {
|
|
155
|
+
const writtenKeys = new Set();
|
|
156
|
+
const target = cloneRecord(baseOutput);
|
|
157
|
+
const output = new Proxy(target, {
|
|
158
|
+
set(record, property, value) {
|
|
159
|
+
if (typeof property === 'string') {
|
|
160
|
+
writtenKeys.add(property);
|
|
161
|
+
}
|
|
162
|
+
return Reflect.set(record, property, value);
|
|
163
|
+
},
|
|
164
|
+
deleteProperty(record, property) {
|
|
165
|
+
if (typeof property === 'string') {
|
|
166
|
+
writtenKeys.add(property);
|
|
167
|
+
}
|
|
168
|
+
return Reflect.deleteProperty(record, property);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
output,
|
|
173
|
+
getDelta: () => Object.fromEntries([...writtenKeys]
|
|
174
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(target, key))
|
|
175
|
+
.map((key) => [key, target[key]])),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
class ParallelWorkflowRuntimeControl {
|
|
179
|
+
constructor(getParent) {
|
|
180
|
+
this.getParent = getParent;
|
|
181
|
+
}
|
|
182
|
+
get status() {
|
|
183
|
+
return this.getParent().status;
|
|
184
|
+
}
|
|
185
|
+
get resumeData() {
|
|
186
|
+
return this.getParent().resumeData;
|
|
187
|
+
}
|
|
188
|
+
suspend() {
|
|
189
|
+
return Promise.reject(new ParallelSuspensionError());
|
|
190
|
+
}
|
|
191
|
+
resume(data) {
|
|
192
|
+
this.getParent().resume(data);
|
|
193
|
+
}
|
|
194
|
+
getSnapshot() {
|
|
195
|
+
return this.getParent().getSnapshot();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function createParallelBranchContext(context, branchState) {
|
|
199
|
+
let state = branchState;
|
|
200
|
+
const workflow = new ParallelWorkflowRuntimeControl(() => context.workflow);
|
|
201
|
+
return new Proxy(context, {
|
|
202
|
+
get(target, property, receiver) {
|
|
203
|
+
if (property === 'state') {
|
|
204
|
+
return state;
|
|
205
|
+
}
|
|
206
|
+
if (property === 'workflow') {
|
|
207
|
+
return workflow;
|
|
37
208
|
}
|
|
38
|
-
|
|
209
|
+
if (property === 'snapshot') {
|
|
210
|
+
return () => cloneRecord(state);
|
|
211
|
+
}
|
|
212
|
+
if (property === 'attachWorkflowRuntime') {
|
|
213
|
+
return () => undefined;
|
|
214
|
+
}
|
|
215
|
+
return Reflect.get(target, property, receiver);
|
|
216
|
+
},
|
|
217
|
+
set(target, property, value, receiver) {
|
|
218
|
+
if (property === 'state') {
|
|
219
|
+
state = isRecord(value) ? value : {};
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return Reflect.set(target, property, value, receiver);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function cloneRecord(value) {
|
|
227
|
+
return cloneValue(value);
|
|
228
|
+
}
|
|
229
|
+
function cloneValue(value) {
|
|
230
|
+
if (value === undefined || value === null) {
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
if (typeof structuredClone === 'function') {
|
|
234
|
+
try {
|
|
235
|
+
return structuredClone(value);
|
|
39
236
|
}
|
|
237
|
+
catch {
|
|
238
|
+
// Fall through to JSON clone below.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
return JSON.parse(JSON.stringify(value));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
return [...value];
|
|
247
|
+
}
|
|
248
|
+
if (isRecord(value)) {
|
|
249
|
+
return { ...value };
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
40
252
|
}
|
|
41
|
-
|
|
253
|
+
}
|
|
254
|
+
function isRecord(value) {
|
|
255
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
42
256
|
}
|
|
@@ -20,6 +20,29 @@ const markStepSkipped = (env, step, iterationStack, reason) => {
|
|
|
20
20
|
break;
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
+
const markStepCancelled = (env, step, iterationStack, reason) => {
|
|
24
|
+
const stepInfo = env.buildInstanceStepInfo(step, iterationStack);
|
|
25
|
+
env.markSnapshot(stepInfo, 'cancelled', reason);
|
|
26
|
+
env.emit('hook:step:cancelled', {
|
|
27
|
+
runId: env.runId,
|
|
28
|
+
step: stepInfo,
|
|
29
|
+
error: new Error(reason ?? 'Step execution was cancelled.'),
|
|
30
|
+
});
|
|
31
|
+
switch (step.type) {
|
|
32
|
+
case StepType.Parallel:
|
|
33
|
+
step.steps.forEach((child) => markStepCancelled(env, child, iterationStack, reason));
|
|
34
|
+
break;
|
|
35
|
+
case StepType.Conditional:
|
|
36
|
+
step.branches.forEach((branch) => branch.steps.forEach((child) => markStepCancelled(env, child, iterationStack, reason)));
|
|
37
|
+
break;
|
|
38
|
+
case StepType.Loop:
|
|
39
|
+
case StepType.Task:
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
23
43
|
export const markStepsSkipped = (env, steps, iterationStack, reason) => {
|
|
24
44
|
steps.forEach((step) => markStepSkipped(env, step, iterationStack, reason));
|
|
25
45
|
};
|
|
46
|
+
export const markStepsCancelled = (env, steps, iterationStack, reason) => {
|
|
47
|
+
steps.forEach((step) => markStepCancelled(env, step, iterationStack, reason));
|
|
48
|
+
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Copyright (c) 2025 Hexastack.
|
|
4
4
|
* Full terms: see LICENSE.md.
|
|
5
5
|
*/
|
|
6
|
+
import { getAbortReason, throwIfAborted } from '../errors';
|
|
6
7
|
import { evaluateMapping } from '../workflow-values';
|
|
7
8
|
/**
|
|
8
9
|
* Execute a task step by running its task and handling suspension or output mapping.
|
|
@@ -20,6 +21,11 @@ export async function executeTaskStep(env, step, state, _path) {
|
|
|
20
21
|
throw new Error(`Task "${step.taskName}" is not defined.`);
|
|
21
22
|
}
|
|
22
23
|
const stepInfo = env.buildInstanceStepInfo(step, state.iterationStack);
|
|
24
|
+
if (env.signal.aborted) {
|
|
25
|
+
const error = getAbortReason(env.signal);
|
|
26
|
+
recordTaskCancellation(env, stepInfo, error);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
23
29
|
const scope = {
|
|
24
30
|
input: state.input,
|
|
25
31
|
context: env.context.state,
|
|
@@ -28,7 +34,12 @@ export async function executeTaskStep(env, step, state, _path) {
|
|
|
28
34
|
accumulator: state.accumulator,
|
|
29
35
|
};
|
|
30
36
|
const inputs = await evaluateMapping(task.inputs, scope);
|
|
31
|
-
env.
|
|
37
|
+
if (env.signal.aborted) {
|
|
38
|
+
const error = getAbortReason(env.signal);
|
|
39
|
+
recordTaskCancellation(env, stepInfo, error);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
const stepExecution = env.recordStepExecution(stepInfo, {
|
|
32
43
|
action: task.actionName,
|
|
33
44
|
status: 'running',
|
|
34
45
|
startedAt: Date.now(),
|
|
@@ -38,9 +49,14 @@ export async function executeTaskStep(env, step, state, _path) {
|
|
|
38
49
|
env.beginStepExecution?.(stepInfo.id);
|
|
39
50
|
env.setCurrentStep(stepInfo);
|
|
40
51
|
env.markSnapshot(stepInfo, 'running');
|
|
41
|
-
env.emit('hook:step:start', {
|
|
52
|
+
env.emit('hook:step:start', {
|
|
53
|
+
runId: env.runId,
|
|
54
|
+
step: stepInfo,
|
|
55
|
+
stepExecution,
|
|
56
|
+
});
|
|
42
57
|
try {
|
|
43
|
-
|
|
58
|
+
throwIfAborted(env.signal);
|
|
59
|
+
const actionPromise = Promise.resolve().then(() => task.action.run(inputs, env.context, task.settings, task.bindings, env.signal));
|
|
44
60
|
const outcome = await waitForTaskProgress(env, stepInfo.id, actionPromise);
|
|
45
61
|
if (outcome.type === 'completed') {
|
|
46
62
|
await completeTask(env, stepInfo.id, stepInfo, task, state, outcome.value);
|
|
@@ -51,6 +67,11 @@ export async function executeTaskStep(env, step, state, _path) {
|
|
|
51
67
|
recordTaskFailure(env, stepInfo, outcome.error);
|
|
52
68
|
throw outcome.error;
|
|
53
69
|
}
|
|
70
|
+
if (outcome.type === 'cancelled') {
|
|
71
|
+
env.clearStepSuspensions(stepInfo.id, outcome.error);
|
|
72
|
+
recordTaskCancellation(env, stepInfo, outcome.error);
|
|
73
|
+
throw outcome.error;
|
|
74
|
+
}
|
|
54
75
|
return buildSuspensionContinuation(env, stepInfo.id, stepInfo, task, state, actionPromise, outcome.request);
|
|
55
76
|
}
|
|
56
77
|
finally {
|
|
@@ -62,22 +83,45 @@ const waitForTaskProgress = async (env, stepId, actionPromise) => {
|
|
|
62
83
|
const suspension = env
|
|
63
84
|
.waitForStepSuspension(stepId)
|
|
64
85
|
.then((request) => ({ type: 'suspended', request }));
|
|
65
|
-
|
|
86
|
+
let cleanupCancellation = () => undefined;
|
|
87
|
+
const cancellation = new Promise((resolve) => {
|
|
88
|
+
if (env.signal.aborted) {
|
|
89
|
+
resolve({ type: 'cancelled', error: getAbortReason(env.signal) });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const onAbort = () => {
|
|
93
|
+
resolve({ type: 'cancelled', error: getAbortReason(env.signal) });
|
|
94
|
+
};
|
|
95
|
+
env.signal.addEventListener('abort', onAbort, { once: true });
|
|
96
|
+
cleanupCancellation = () => {
|
|
97
|
+
env.signal.removeEventListener('abort', onAbort);
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
return await Promise.race([completion, suspension, cancellation]);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
cleanupCancellation();
|
|
105
|
+
}
|
|
66
106
|
};
|
|
67
107
|
const completeTask = async (env, stepId, stepInfo, task, state, result) => {
|
|
68
108
|
await env.captureTaskOutput(task, state, result);
|
|
69
|
-
env.recordStepExecution(stepInfo, {
|
|
109
|
+
const stepExecution = env.recordStepExecution(stepInfo, {
|
|
70
110
|
status: 'completed',
|
|
71
111
|
endedAt: Date.now(),
|
|
72
112
|
output: result,
|
|
73
113
|
context: { after: env.context.snapshot() },
|
|
74
114
|
});
|
|
75
115
|
env.markSnapshot(stepInfo, 'completed');
|
|
76
|
-
env.emit('hook:step:success', {
|
|
116
|
+
env.emit('hook:step:success', {
|
|
117
|
+
runId: env.runId,
|
|
118
|
+
step: stepInfo,
|
|
119
|
+
stepExecution,
|
|
120
|
+
});
|
|
77
121
|
env.clearStepSuspensions(stepId);
|
|
78
122
|
};
|
|
79
123
|
const recordSuspension = (env, stepInfo, reason, data) => {
|
|
80
|
-
env.recordStepExecution(stepInfo, {
|
|
124
|
+
const stepExecution = env.recordStepExecution(stepInfo, {
|
|
81
125
|
status: 'suspended',
|
|
82
126
|
endedAt: Date.now(),
|
|
83
127
|
reason,
|
|
@@ -87,6 +131,7 @@ const recordSuspension = (env, stepInfo, reason, data) => {
|
|
|
87
131
|
env.emit('hook:step:suspended', {
|
|
88
132
|
runId: env.runId,
|
|
89
133
|
step: stepInfo,
|
|
134
|
+
stepExecution,
|
|
90
135
|
reason,
|
|
91
136
|
data,
|
|
92
137
|
});
|
|
@@ -137,14 +182,36 @@ const buildSuspensionContinuation = (env, stepId, stepInfo, task, state, actionP
|
|
|
137
182
|
};
|
|
138
183
|
};
|
|
139
184
|
const recordTaskFailure = (env, stepInfo, error) => {
|
|
140
|
-
|
|
185
|
+
const normalizedError = normalizeError(error);
|
|
186
|
+
const stepExecution = env.recordStepExecution(stepInfo, {
|
|
141
187
|
status: 'failed',
|
|
142
188
|
endedAt: Date.now(),
|
|
143
|
-
error:
|
|
189
|
+
error: normalizedError,
|
|
144
190
|
context: { after: env.context.snapshot() },
|
|
145
191
|
});
|
|
146
192
|
env.markSnapshot(stepInfo, 'failed', normalizeErrorMessage(error));
|
|
147
|
-
env.emit('hook:step:error', {
|
|
193
|
+
env.emit('hook:step:error', {
|
|
194
|
+
runId: env.runId,
|
|
195
|
+
step: stepInfo,
|
|
196
|
+
stepExecution,
|
|
197
|
+
error,
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
const recordTaskCancellation = (env, stepInfo, error) => {
|
|
201
|
+
const normalizedError = normalizeError(error);
|
|
202
|
+
const stepExecution = env.recordStepExecution(stepInfo, {
|
|
203
|
+
status: 'cancelled',
|
|
204
|
+
endedAt: Date.now(),
|
|
205
|
+
error: normalizedError,
|
|
206
|
+
context: { after: env.context.snapshot() },
|
|
207
|
+
});
|
|
208
|
+
env.markSnapshot(stepInfo, 'cancelled', normalizeErrorMessage(error));
|
|
209
|
+
env.emit('hook:step:cancelled', {
|
|
210
|
+
runId: env.runId,
|
|
211
|
+
step: stepInfo,
|
|
212
|
+
stepExecution,
|
|
213
|
+
error,
|
|
214
|
+
});
|
|
148
215
|
};
|
|
149
216
|
const normalizeError = (error) => error instanceof Error
|
|
150
217
|
? { message: error.message, stack: error.stack }
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
* Full terms: see LICENSE.md.
|
|
5
5
|
*/
|
|
6
6
|
import { executeLoop as runLoopExecutor, shouldStopLoop, updateAccumulator, } from './step-executors/loop-executor';
|
|
7
|
-
import { executeParallel as runParallelExecutor } from './step-executors/parallel-executor';
|
|
8
|
-
import { markStepsSkipped } from './step-executors/skip-helpers';
|
|
9
7
|
import { wrapSuspensionContinuation } from './step-executors/suspension-continuation';
|
|
10
8
|
import { StepType, } from './workflow-event-emitter';
|
|
11
9
|
/**
|
|
@@ -166,28 +164,7 @@ export function buildSuspensionForPath(deps, steps, state, targetPath, pathPrefi
|
|
|
166
164
|
};
|
|
167
165
|
}
|
|
168
166
|
if (step.type === StepType.Parallel) {
|
|
169
|
-
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
const childPath = rest.slice(1);
|
|
173
|
-
const childSuspension = buildSuspensionForPath(deps, step.steps, state, childPath, [...currentPath, 'parallel'], iterationDepth, suspensionMetadata);
|
|
174
|
-
if (!childSuspension) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
const childIndex = typeof childPath[0] === 'number' ? childPath[0] : 0;
|
|
178
|
-
return wrapSuspensionContinuation(childSuspension, async () => {
|
|
179
|
-
if (step.strategy === 'wait_any') {
|
|
180
|
-
if (childIndex + 1 < step.steps.length) {
|
|
181
|
-
markStepsSkipped(env, step.steps.slice(childIndex + 1), state.iterationStack ?? []);
|
|
182
|
-
}
|
|
183
|
-
return deps.executeFlow(steps, state, pathPrefix, current + 1);
|
|
184
|
-
}
|
|
185
|
-
const next = await runParallelExecutor(env, step, state, currentPath, childIndex + 1);
|
|
186
|
-
if (next) {
|
|
187
|
-
return wrapSuspensionContinuation(next, () => deps.executeFlow(steps, state, pathPrefix, current + 1));
|
|
188
|
-
}
|
|
189
|
-
return deps.executeFlow(steps, state, pathPrefix, current + 1);
|
|
190
|
-
});
|
|
167
|
+
return null;
|
|
191
168
|
}
|
|
192
169
|
if (step.type === StepType.Conditional) {
|
|
193
170
|
if (rest[0] !== 'branch' || typeof rest[1] !== 'number') {
|
package/dist/esm/utils/naming.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Copyright (c) 2025 Hexastack.
|
|
4
4
|
* Full terms: see LICENSE.md.
|
|
5
5
|
*/
|
|
6
|
-
const SNAKE_CASE_REGEX = /^[a-z0-9]+(?:_[a-z0-9]+)
|
|
6
|
+
export const SNAKE_CASE_REGEX = /^[a-z0-9]+(?:_[a-z0-9]+)*$/;
|
|
7
7
|
/**
|
|
8
8
|
* Verifies that names follow the snake_case convention required by the system.
|
|
9
9
|
*
|
|
@@ -3,34 +3,89 @@
|
|
|
3
3
|
* Copyright (c) 2025 Hexastack.
|
|
4
4
|
* Full terms: see LICENSE.md.
|
|
5
5
|
*/
|
|
6
|
+
import { getAbortReason, throwIfAborted } from '../errors';
|
|
6
7
|
/**
|
|
7
8
|
* Resolves after the specified duration; useful for retry delays.
|
|
8
9
|
*
|
|
9
10
|
* @param durationMs - Number of milliseconds to wait before resolving.
|
|
11
|
+
* @param signal - Optional signal used to cancel the wait.
|
|
10
12
|
* @returns A promise that resolves once the duration elapses.
|
|
11
13
|
*/
|
|
12
|
-
export const sleep = (durationMs
|
|
14
|
+
export const sleep = (durationMs, signal) => {
|
|
15
|
+
if (!signal) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
17
|
+
}
|
|
18
|
+
throwIfAborted(signal);
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const timer = setTimeout(() => {
|
|
21
|
+
signal.removeEventListener('abort', onAbort);
|
|
22
|
+
resolve();
|
|
23
|
+
}, durationMs);
|
|
24
|
+
const onAbort = () => {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
reject(getAbortReason(signal));
|
|
27
|
+
};
|
|
28
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
29
|
+
});
|
|
30
|
+
};
|
|
13
31
|
/**
|
|
14
32
|
* Wraps a promise and rejects if it does not settle within the timeout.
|
|
15
33
|
*
|
|
16
34
|
* @param promise - Operation that may take longer than the allowed timeout.
|
|
17
35
|
* @param timeoutMs - Maximum time in milliseconds to wait before rejecting.
|
|
36
|
+
* @param signal - Optional signal used to cancel the operation.
|
|
18
37
|
* @returns The result of the original promise when it resolves in time.
|
|
19
38
|
* @throws Error when the timeout is exceeded.
|
|
20
39
|
*/
|
|
21
|
-
export async function withTimeout(promise, timeoutMs) {
|
|
22
|
-
if (!timeoutMs) {
|
|
40
|
+
export async function withTimeout(promise, timeoutMs, signal) {
|
|
41
|
+
if (!timeoutMs && !signal) {
|
|
23
42
|
return promise;
|
|
24
43
|
}
|
|
25
44
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
let settled = false;
|
|
46
|
+
const timer = timeoutMs
|
|
47
|
+
? setTimeout(() => {
|
|
48
|
+
settled = true;
|
|
49
|
+
signal?.removeEventListener('abort', onAbort);
|
|
50
|
+
reject(new Error(`Step execution exceeded timeout of ${timeoutMs}ms`));
|
|
51
|
+
}, timeoutMs)
|
|
52
|
+
: undefined;
|
|
53
|
+
const onAbort = () => {
|
|
54
|
+
if (settled) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
settled = true;
|
|
58
|
+
if (timer) {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
reject(getAbortReason(signal));
|
|
62
|
+
};
|
|
63
|
+
if (signal) {
|
|
64
|
+
if (signal.aborted) {
|
|
65
|
+
onAbort();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
69
|
+
}
|
|
29
70
|
promise.then((value) => {
|
|
30
|
-
|
|
71
|
+
if (settled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
settled = true;
|
|
75
|
+
if (timer) {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
signal?.removeEventListener('abort', onAbort);
|
|
31
79
|
resolve(value);
|
|
32
80
|
}, (error) => {
|
|
33
|
-
|
|
81
|
+
if (settled) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
settled = true;
|
|
85
|
+
if (timer) {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
signal?.removeEventListener('abort', onAbort);
|
|
34
89
|
reject(error);
|
|
35
90
|
});
|
|
36
91
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2026 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
const getSettingsRef = (definition, settingsKey) => {
|
|
7
|
+
const settings = definition.settings;
|
|
8
|
+
if (!settings || typeof settings !== 'object') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const value = settings[settingsKey];
|
|
12
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Collect external resource IDs referenced by workflow definition defs.
|
|
16
|
+
*/
|
|
17
|
+
export const collectWorkflowDefinitionResourceRefs = (definition, descriptors) => {
|
|
18
|
+
const result = Object.fromEntries(descriptors.map((descriptor) => [descriptor.kind, new Set()]));
|
|
19
|
+
for (const def of Object.values(definition.defs ?? {})) {
|
|
20
|
+
for (const descriptor of descriptors) {
|
|
21
|
+
if (def.kind !== descriptor.kind) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const ref = getSettingsRef(def, descriptor.settingsKey);
|
|
25
|
+
if (ref) {
|
|
26
|
+
result[descriptor.kind].add(ref);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return Object.fromEntries(Object.entries(result).map(([kind, refs]) => [kind, Array.from(refs)]));
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Rewrite external resource IDs referenced by workflow definition defs.
|
|
34
|
+
*/
|
|
35
|
+
export const remapWorkflowDefinitionResourceRefs = (definition, descriptors, idMaps) => {
|
|
36
|
+
let didChange = false;
|
|
37
|
+
const nextDefs = Object.fromEntries(Object.entries(definition.defs ?? {}).map(([defName, def]) => {
|
|
38
|
+
const descriptor = descriptors.find((item) => item.kind === def.kind);
|
|
39
|
+
if (!descriptor) {
|
|
40
|
+
return [defName, def];
|
|
41
|
+
}
|
|
42
|
+
const currentRef = getSettingsRef(def, descriptor.settingsKey);
|
|
43
|
+
const nextRef = currentRef
|
|
44
|
+
? idMaps[descriptor.kind]?.[currentRef]
|
|
45
|
+
: undefined;
|
|
46
|
+
if (!currentRef || !nextRef || nextRef === currentRef) {
|
|
47
|
+
return [defName, def];
|
|
48
|
+
}
|
|
49
|
+
didChange = true;
|
|
50
|
+
return [
|
|
51
|
+
defName,
|
|
52
|
+
{
|
|
53
|
+
...def,
|
|
54
|
+
settings: {
|
|
55
|
+
...(def.settings ?? {}),
|
|
56
|
+
[descriptor.settingsKey]: nextRef,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
}));
|
|
61
|
+
if (!didChange) {
|
|
62
|
+
return definition;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
...definition,
|
|
66
|
+
defs: nextDefs,
|
|
67
|
+
};
|
|
68
|
+
};
|