@hexabot-ai/agentic 3.0.1-alpha.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/LICENSE.md +120 -0
- package/README.md +231 -0
- package/dist/cjs/action/abstract-action.js +141 -0
- package/dist/cjs/action/action.js +47 -0
- package/dist/cjs/action/action.types.js +7 -0
- package/dist/cjs/bindings/base-binding.js +278 -0
- package/dist/cjs/context.js +79 -0
- package/dist/cjs/dsl.types.js +318 -0
- package/dist/cjs/index.js +52 -0
- package/dist/cjs/runner-runtime-control.js +343 -0
- package/dist/cjs/step-executors/conditional-executor.js +63 -0
- package/dist/cjs/step-executors/loop-executor.js +187 -0
- package/dist/cjs/step-executors/parallel-executor.js +51 -0
- package/dist/cjs/step-executors/skip-helpers.js +29 -0
- package/dist/cjs/step-executors/task-executor.js +155 -0
- package/dist/cjs/step-executors/types.js +7 -0
- package/dist/cjs/suspension-rebuilder.js +301 -0
- package/dist/cjs/utils/deferred.js +17 -0
- package/dist/cjs/utils/naming.js +48 -0
- package/dist/cjs/utils/timeout.js +42 -0
- package/dist/cjs/utils/workflow-definition.js +102 -0
- package/dist/cjs/workflow-compiler.js +212 -0
- package/dist/cjs/workflow-event-emitter.js +40 -0
- package/dist/cjs/workflow-runner.js +448 -0
- package/dist/cjs/workflow-types.js +7 -0
- package/dist/cjs/workflow-values.js +99 -0
- package/dist/cjs/workflow.js +243 -0
- package/dist/esm/action/abstract-action.js +137 -0
- package/dist/esm/action/action.js +44 -0
- package/dist/esm/action/action.types.js +6 -0
- package/dist/esm/bindings/base-binding.js +273 -0
- package/dist/esm/context.js +75 -0
- package/dist/esm/dsl.types.js +309 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/runner-runtime-control.js +338 -0
- package/dist/esm/step-executors/conditional-executor.js +60 -0
- package/dist/esm/step-executors/loop-executor.js +182 -0
- package/dist/esm/step-executors/parallel-executor.js +48 -0
- package/dist/esm/step-executors/skip-helpers.js +25 -0
- package/dist/esm/step-executors/task-executor.js +152 -0
- package/dist/esm/step-executors/types.js +6 -0
- package/dist/esm/suspension-rebuilder.js +296 -0
- package/dist/esm/utils/deferred.js +14 -0
- package/dist/esm/utils/naming.js +42 -0
- package/dist/esm/utils/timeout.js +37 -0
- package/dist/esm/utils/workflow-definition.js +98 -0
- package/dist/esm/workflow-compiler.js +208 -0
- package/dist/esm/workflow-event-emitter.js +36 -0
- package/dist/esm/workflow-runner.js +444 -0
- package/dist/esm/workflow-types.js +6 -0
- package/dist/esm/workflow-values.js +89 -0
- package/dist/esm/workflow.js +236 -0
- package/dist/types/action/abstract-action.d.ts +54 -0
- package/dist/types/action/abstract-action.d.ts.map +1 -0
- package/dist/types/action/action.d.ts +23 -0
- package/dist/types/action/action.d.ts.map +1 -0
- package/dist/types/action/action.types.d.ts +55 -0
- package/dist/types/action/action.types.d.ts.map +1 -0
- package/dist/types/bindings/base-binding.d.ts +52 -0
- package/dist/types/bindings/base-binding.d.ts.map +1 -0
- package/dist/types/context.d.ts +105 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/dsl.types.d.ts +220 -0
- package/dist/types/dsl.types.d.ts.map +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/runner-runtime-control.d.ts +64 -0
- package/dist/types/runner-runtime-control.d.ts.map +1 -0
- package/dist/types/step-executors/conditional-executor.d.ts +13 -0
- package/dist/types/step-executors/conditional-executor.d.ts.map +1 -0
- package/dist/types/step-executors/loop-executor.d.ts +33 -0
- package/dist/types/step-executors/loop-executor.d.ts.map +1 -0
- package/dist/types/step-executors/parallel-executor.d.ts +14 -0
- package/dist/types/step-executors/parallel-executor.d.ts.map +1 -0
- package/dist/types/step-executors/skip-helpers.d.ts +12 -0
- package/dist/types/step-executors/skip-helpers.d.ts.map +1 -0
- package/dist/types/step-executors/task-executor.d.ts +13 -0
- package/dist/types/step-executors/task-executor.d.ts.map +1 -0
- package/dist/types/step-executors/types.d.ts +24 -0
- package/dist/types/step-executors/types.d.ts.map +1 -0
- package/dist/types/suspension-rebuilder.d.ts +67 -0
- package/dist/types/suspension-rebuilder.d.ts.map +1 -0
- package/dist/types/utils/deferred.d.ts +7 -0
- package/dist/types/utils/deferred.d.ts.map +1 -0
- package/dist/types/utils/naming.d.ts +24 -0
- package/dist/types/utils/naming.d.ts.map +1 -0
- package/dist/types/utils/timeout.d.ts +17 -0
- package/dist/types/utils/timeout.d.ts.map +1 -0
- package/dist/types/utils/workflow-definition.d.ts +3 -0
- package/dist/types/utils/workflow-definition.d.ts.map +1 -0
- package/dist/types/workflow-compiler.d.ts +12 -0
- package/dist/types/workflow-compiler.d.ts.map +1 -0
- package/dist/types/workflow-event-emitter.d.ts +70 -0
- package/dist/types/workflow-event-emitter.d.ts.map +1 -0
- package/dist/types/workflow-runner.d.ts +179 -0
- package/dist/types/workflow-runner.d.ts.map +1 -0
- package/dist/types/workflow-types.d.ts +195 -0
- package/dist/types/workflow-types.d.ts.map +1 -0
- package/dist/types/workflow-values.d.ts +34 -0
- package/dist/types/workflow-values.d.ts.map +1 -0
- package/dist/types/workflow.d.ts +75 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/examples/defs-bindings-agent/actions/ai-agent.ts +110 -0
- package/examples/defs-bindings-agent/actions/caculate-score.ts +49 -0
- package/examples/defs-bindings-agent/actions/index.ts +15 -0
- package/examples/defs-bindings-agent/bindings.ts +20 -0
- package/examples/defs-bindings-agent/context.ts +32 -0
- package/examples/defs-bindings-agent/workflow.ts +67 -0
- package/examples/defs-bindings-agent/workflow.yml +28 -0
- package/examples/full/actions/await-user-input.ts +46 -0
- package/examples/full/actions/call-llm.ts +133 -0
- package/examples/full/actions/create-ticket.ts +46 -0
- package/examples/full/actions/decision-router.ts +83 -0
- package/examples/full/actions/get-calendar-events.ts +55 -0
- package/examples/full/actions/get-user-profile.ts +72 -0
- package/examples/full/actions/index.ts +33 -0
- package/examples/full/actions/query-memory.ts +45 -0
- package/examples/full/actions/search-web.ts +53 -0
- package/examples/full/actions/send-email.ts +50 -0
- package/examples/full/context.ts +41 -0
- package/examples/full/workflow.ts +75 -0
- package/examples/full/workflow.yml +359 -0
- package/examples/loop/actions/await-reply.ts +47 -0
- package/examples/loop/actions/index.ts +19 -0
- package/examples/loop/actions/send-text-message.ts +40 -0
- package/examples/loop/context.ts +32 -0
- package/examples/loop/workflow.ts +66 -0
- package/examples/loop/workflow.yml +66 -0
- package/examples/suspend-resume/actions/format-reply.ts +43 -0
- package/examples/suspend-resume/actions/index.ts +13 -0
- package/examples/suspend-resume/actions/wait-for-user.ts +51 -0
- package/examples/suspend-resume/context.ts +32 -0
- package/examples/suspend-resume/workflow.ts +79 -0
- package/examples/suspend-resume/workflow.yml +29 -0
- package/package.json +60 -0
- package/src/__tests__/parser.test.ts +95 -0
- package/src/__tests__/suspension-rebuilder.test.ts +361 -0
- package/src/__tests__/test-helpers.ts +36 -0
- package/src/__tests__/validation.test.ts +526 -0
- package/src/__tests__/workflow-compiler.test.ts +715 -0
- package/src/__tests__/workflow-definition-path.test.ts +381 -0
- package/src/__tests__/workflow-event-emitter.test.ts +50 -0
- package/src/__tests__/workflow-runner.test.ts +1397 -0
- package/src/__tests__/workflow-values.test.ts +132 -0
- package/src/__tests__/workflow.test.ts +320 -0
- package/src/action/__tests__/abstract-action-timing.test.ts +183 -0
- package/src/action/__tests__/action.test.ts +473 -0
- package/src/action/abstract-action.ts +215 -0
- package/src/action/action.ts +83 -0
- package/src/action/action.types.ts +93 -0
- package/src/bindings/base-binding.ts +508 -0
- package/src/context.ts +179 -0
- package/src/dsl.types.ts +473 -0
- package/src/index.ts +114 -0
- package/src/runner-runtime-control.ts +544 -0
- package/src/step-executors/conditional-executor.test.ts +192 -0
- package/src/step-executors/conditional-executor.ts +83 -0
- package/src/step-executors/loop-executor.test.ts +303 -0
- package/src/step-executors/loop-executor.ts +299 -0
- package/src/step-executors/parallel-executor.test.ts +182 -0
- package/src/step-executors/parallel-executor.ts +77 -0
- package/src/step-executors/skip-helpers.ts +68 -0
- package/src/step-executors/task-executor.test.ts +257 -0
- package/src/step-executors/task-executor.ts +248 -0
- package/src/step-executors/types.ts +70 -0
- package/src/suspension-rebuilder.ts +504 -0
- package/src/utils/deferred.ts +23 -0
- package/src/utils/naming.ts +55 -0
- package/src/utils/timeout.ts +48 -0
- package/src/utils/workflow-definition.ts +173 -0
- package/src/workflow-compiler.ts +317 -0
- package/src/workflow-event-emitter.ts +86 -0
- package/src/workflow-runner.ts +603 -0
- package/src/workflow-types.ts +207 -0
- package/src/workflow-values.ts +159 -0
- package/src/workflow.ts +387 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
EvaluationScope,
|
|
9
|
+
ExecutionState,
|
|
10
|
+
ForEachLoopStep,
|
|
11
|
+
LoopStep,
|
|
12
|
+
Suspension,
|
|
13
|
+
WhileLoopStep,
|
|
14
|
+
} from '../workflow-types';
|
|
15
|
+
import { evaluateValue } from '../workflow-values';
|
|
16
|
+
|
|
17
|
+
import type { StepExecutorEnv } from './types';
|
|
18
|
+
|
|
19
|
+
type LoopScope = EvaluationScope;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Execute a loop step by iterating over input items and executing child steps.
|
|
23
|
+
*
|
|
24
|
+
* @param env Executor environment with helpers and workflow context.
|
|
25
|
+
* @param step The loop step configuration.
|
|
26
|
+
* @param state Mutable workflow execution state.
|
|
27
|
+
* @param path Path tokens locating this loop within the workflow.
|
|
28
|
+
* @param startIndex Index to resume iteration from when continuing.
|
|
29
|
+
* @returns A suspension if a child step pauses execution, otherwise void.
|
|
30
|
+
*/
|
|
31
|
+
export async function executeLoop(
|
|
32
|
+
env: StepExecutorEnv,
|
|
33
|
+
step: LoopStep,
|
|
34
|
+
state: ExecutionState,
|
|
35
|
+
path: Array<number | string>,
|
|
36
|
+
startIndex = 0,
|
|
37
|
+
): Promise<Suspension | void> {
|
|
38
|
+
if (step.loopType === 'for_each') {
|
|
39
|
+
return executeForEachLoop(env, step, state, path, startIndex);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return executeWhileLoop(env, step, state, path, startIndex);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function executeForEachLoop(
|
|
46
|
+
env: StepExecutorEnv,
|
|
47
|
+
step: ForEachLoopStep,
|
|
48
|
+
state: ExecutionState,
|
|
49
|
+
path: Array<number | string>,
|
|
50
|
+
startIndex: number,
|
|
51
|
+
): Promise<Suspension | void> {
|
|
52
|
+
const scope = {
|
|
53
|
+
input: state.input,
|
|
54
|
+
context: env.context.state,
|
|
55
|
+
output: state.output,
|
|
56
|
+
iteration: state.iteration,
|
|
57
|
+
accumulator: state.accumulator,
|
|
58
|
+
};
|
|
59
|
+
const items = await evaluateValue(step.forEach.in, scope);
|
|
60
|
+
const iterable = Array.isArray(items) ? items : [];
|
|
61
|
+
let accumulator = step.accumulate?.initial ?? state.accumulator;
|
|
62
|
+
|
|
63
|
+
for (let index = startIndex; index < iterable.length; index += 1) {
|
|
64
|
+
const item = iterable[index];
|
|
65
|
+
const iterationState: ExecutionState = {
|
|
66
|
+
...state,
|
|
67
|
+
iteration: { item, index },
|
|
68
|
+
accumulator,
|
|
69
|
+
iterationStack: [...state.iterationStack, index],
|
|
70
|
+
};
|
|
71
|
+
const suspension = await env.executeFlow(step.steps, iterationState, [
|
|
72
|
+
...path,
|
|
73
|
+
index,
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
if (suspension) {
|
|
77
|
+
return {
|
|
78
|
+
...suspension,
|
|
79
|
+
continue: async (resumeData: unknown) => {
|
|
80
|
+
const next = await suspension.continue(resumeData);
|
|
81
|
+
if (next) {
|
|
82
|
+
return next;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const postScope = buildScope(
|
|
86
|
+
env,
|
|
87
|
+
iterationState,
|
|
88
|
+
{ item, index },
|
|
89
|
+
accumulator,
|
|
90
|
+
);
|
|
91
|
+
accumulator = await updateAccumulator(step, postScope, accumulator);
|
|
92
|
+
|
|
93
|
+
const shouldStop = await shouldStopLoop(step, postScope);
|
|
94
|
+
if (shouldStop) {
|
|
95
|
+
state.accumulator = accumulator;
|
|
96
|
+
if (step.accumulate && step.name) {
|
|
97
|
+
state.output[step.name] = { [step.accumulate.as]: accumulator };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return executeForEachLoop(
|
|
104
|
+
env,
|
|
105
|
+
step,
|
|
106
|
+
{
|
|
107
|
+
...iterationState,
|
|
108
|
+
accumulator,
|
|
109
|
+
iterationStack: state.iterationStack,
|
|
110
|
+
},
|
|
111
|
+
path,
|
|
112
|
+
index + 1,
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const postScope = buildScope(
|
|
119
|
+
env,
|
|
120
|
+
iterationState,
|
|
121
|
+
{ item, index },
|
|
122
|
+
accumulator,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
accumulator = await updateAccumulator(step, postScope, accumulator);
|
|
126
|
+
|
|
127
|
+
const shouldStop = await shouldStopLoop(step, postScope);
|
|
128
|
+
if (shouldStop) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
state.output = iterationState.output;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
finalizeAccumulatorState(step, state, accumulator);
|
|
136
|
+
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function executeWhileLoop(
|
|
141
|
+
env: StepExecutorEnv,
|
|
142
|
+
step: WhileLoopStep,
|
|
143
|
+
state: ExecutionState,
|
|
144
|
+
path: Array<number | string>,
|
|
145
|
+
startIndex: number,
|
|
146
|
+
): Promise<Suspension | void> {
|
|
147
|
+
let accumulator = step.accumulate?.initial ?? state.accumulator;
|
|
148
|
+
|
|
149
|
+
for (let index = startIndex; ; index += 1) {
|
|
150
|
+
const conditionScope = buildScope(
|
|
151
|
+
env,
|
|
152
|
+
state,
|
|
153
|
+
{ item: undefined, index },
|
|
154
|
+
accumulator,
|
|
155
|
+
);
|
|
156
|
+
const shouldContinue = await evaluateValue(step.while, conditionScope);
|
|
157
|
+
if (!Boolean(shouldContinue)) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const iterationState: ExecutionState = {
|
|
162
|
+
...state,
|
|
163
|
+
iteration: { item: undefined, index },
|
|
164
|
+
accumulator,
|
|
165
|
+
iterationStack: [...state.iterationStack, index],
|
|
166
|
+
};
|
|
167
|
+
const suspension = await env.executeFlow(step.steps, iterationState, [
|
|
168
|
+
...path,
|
|
169
|
+
index,
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
if (suspension) {
|
|
173
|
+
return {
|
|
174
|
+
...suspension,
|
|
175
|
+
continue: async (resumeData: unknown) => {
|
|
176
|
+
const next = await suspension.continue(resumeData);
|
|
177
|
+
if (next) {
|
|
178
|
+
return next;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const postScope = buildScope(
|
|
182
|
+
env,
|
|
183
|
+
iterationState,
|
|
184
|
+
{ item: undefined, index },
|
|
185
|
+
accumulator,
|
|
186
|
+
);
|
|
187
|
+
accumulator = await updateAccumulator(step, postScope, accumulator);
|
|
188
|
+
|
|
189
|
+
return executeWhileLoop(
|
|
190
|
+
env,
|
|
191
|
+
step,
|
|
192
|
+
{
|
|
193
|
+
...iterationState,
|
|
194
|
+
accumulator,
|
|
195
|
+
iterationStack: state.iterationStack,
|
|
196
|
+
},
|
|
197
|
+
path,
|
|
198
|
+
index + 1,
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const postScope = buildScope(
|
|
205
|
+
env,
|
|
206
|
+
iterationState,
|
|
207
|
+
{ item: undefined, index },
|
|
208
|
+
accumulator,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
accumulator = await updateAccumulator(step, postScope, accumulator);
|
|
212
|
+
state.output = iterationState.output;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
finalizeAccumulatorState(step, state, accumulator);
|
|
216
|
+
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function finalizeAccumulatorState(
|
|
221
|
+
step: LoopStep,
|
|
222
|
+
state: ExecutionState,
|
|
223
|
+
accumulator: unknown,
|
|
224
|
+
): void {
|
|
225
|
+
if (step.accumulate && step.name) {
|
|
226
|
+
state.output[step.name] = { [step.accumulate.as]: accumulator };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (step.accumulate) {
|
|
230
|
+
state.accumulator = accumulator;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Update the loop accumulator using the configured merge expression.
|
|
236
|
+
*
|
|
237
|
+
* @param step The loop step containing accumulation settings.
|
|
238
|
+
* @param scope Current evaluation scope for expressions.
|
|
239
|
+
* @param previous Previous accumulator value to merge with.
|
|
240
|
+
* @returns The updated accumulator value.
|
|
241
|
+
*/
|
|
242
|
+
export async function updateAccumulator(
|
|
243
|
+
step: LoopStep,
|
|
244
|
+
scope: LoopScope,
|
|
245
|
+
previous: unknown,
|
|
246
|
+
): Promise<unknown> {
|
|
247
|
+
if (!step.accumulate) {
|
|
248
|
+
return previous;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return evaluateValue(step.accumulate.merge, {
|
|
252
|
+
...scope,
|
|
253
|
+
accumulator: previous,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Determine whether loop execution should stop based on the `until` condition.
|
|
259
|
+
*
|
|
260
|
+
* @param step The loop step configuration.
|
|
261
|
+
* @param scope Current evaluation scope for expressions.
|
|
262
|
+
* @returns True if the loop should stop, otherwise false.
|
|
263
|
+
*/
|
|
264
|
+
export async function shouldStopLoop(
|
|
265
|
+
step: LoopStep,
|
|
266
|
+
scope: LoopScope,
|
|
267
|
+
): Promise<boolean> {
|
|
268
|
+
if (step.loopType !== 'for_each' || !step.until) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = await evaluateValue(step.until, scope);
|
|
273
|
+
|
|
274
|
+
return Boolean(result);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Build the evaluation scope for loop iteration and accumulator updates.
|
|
279
|
+
*
|
|
280
|
+
* @param env Executor environment with workflow context.
|
|
281
|
+
* @param state Execution state for the current iteration.
|
|
282
|
+
* @param iteration The current iteration item and index.
|
|
283
|
+
* @param accumulator Accumulator value to expose to expressions.
|
|
284
|
+
* @returns An evaluation scope object.
|
|
285
|
+
*/
|
|
286
|
+
function buildScope(
|
|
287
|
+
env: StepExecutorEnv,
|
|
288
|
+
state: ExecutionState,
|
|
289
|
+
iteration: { item: unknown; index: number },
|
|
290
|
+
accumulator: unknown,
|
|
291
|
+
): LoopScope {
|
|
292
|
+
return {
|
|
293
|
+
input: state.input,
|
|
294
|
+
context: env.context.state,
|
|
295
|
+
output: state.output,
|
|
296
|
+
iteration,
|
|
297
|
+
accumulator,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseWorkflowContext } from '../context';
|
|
8
|
+
import type { RuntimeSuspensionRequest } from '../runner-runtime-control';
|
|
9
|
+
import {
|
|
10
|
+
StepType,
|
|
11
|
+
type EventEmitterLike,
|
|
12
|
+
type StepInfo,
|
|
13
|
+
} from '../workflow-event-emitter';
|
|
14
|
+
import type {
|
|
15
|
+
CompiledStep,
|
|
16
|
+
ExecutionState,
|
|
17
|
+
ParallelStep,
|
|
18
|
+
Suspension,
|
|
19
|
+
} from '../workflow-types';
|
|
20
|
+
|
|
21
|
+
import { executeParallel } from './parallel-executor';
|
|
22
|
+
import type { StepExecutorEnv } from './types';
|
|
23
|
+
|
|
24
|
+
class TestContext extends BaseWorkflowContext {
|
|
25
|
+
public eventEmitter: EventEmitterLike = { emit: jest.fn(), on: jest.fn() };
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super({});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const createState = (): ExecutionState => ({
|
|
33
|
+
input: {},
|
|
34
|
+
output: {},
|
|
35
|
+
iterationStack: [],
|
|
36
|
+
});
|
|
37
|
+
const createChild = (id: string): CompiledStep => ({
|
|
38
|
+
id,
|
|
39
|
+
type: StepType.Task,
|
|
40
|
+
label: id,
|
|
41
|
+
taskName: `task_${id}`,
|
|
42
|
+
});
|
|
43
|
+
const createEnv = (): StepExecutorEnv => {
|
|
44
|
+
const compiled = {
|
|
45
|
+
definition: {} as any,
|
|
46
|
+
tasks: {},
|
|
47
|
+
flow: [],
|
|
48
|
+
outputMapping: {},
|
|
49
|
+
inputParser: { parse: (value: unknown) => value } as any,
|
|
50
|
+
} as any;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
compiled,
|
|
54
|
+
context: new TestContext(),
|
|
55
|
+
runId: 'run-1',
|
|
56
|
+
buildInstanceStepInfo: jest.fn(),
|
|
57
|
+
markSnapshot: jest.fn(),
|
|
58
|
+
recordStepExecution: jest.fn(),
|
|
59
|
+
emit: jest.fn(),
|
|
60
|
+
setCurrentStep: jest.fn(),
|
|
61
|
+
waitForStepSuspension: jest
|
|
62
|
+
.fn()
|
|
63
|
+
.mockImplementation(
|
|
64
|
+
() => new Promise<RuntimeSuspensionRequest>(() => undefined),
|
|
65
|
+
),
|
|
66
|
+
clearStepSuspensions: jest.fn(),
|
|
67
|
+
primeStepResumeData: jest.fn(),
|
|
68
|
+
captureTaskOutput: jest.fn(),
|
|
69
|
+
executeFlow: jest.fn(),
|
|
70
|
+
executeStep: jest.fn(),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
describe('executeParallel', () => {
|
|
75
|
+
it('executes all children when using wait_all', async () => {
|
|
76
|
+
const env = createEnv();
|
|
77
|
+
const state = createState();
|
|
78
|
+
const step: ParallelStep = {
|
|
79
|
+
id: 'parallel',
|
|
80
|
+
type: StepType.Parallel,
|
|
81
|
+
label: 'parallel',
|
|
82
|
+
strategy: 'wait_all',
|
|
83
|
+
steps: [createChild('a'), createChild('b')],
|
|
84
|
+
};
|
|
85
|
+
env.executeStep = jest.fn().mockResolvedValue(undefined);
|
|
86
|
+
|
|
87
|
+
const result = await executeParallel(env, step, state, [0]);
|
|
88
|
+
|
|
89
|
+
expect(result).toBeUndefined();
|
|
90
|
+
expect(env.executeStep).toHaveBeenCalledTimes(2);
|
|
91
|
+
expect(env.executeStep).toHaveBeenNthCalledWith(1, step.steps[0], state, [
|
|
92
|
+
0,
|
|
93
|
+
'parallel',
|
|
94
|
+
0,
|
|
95
|
+
]);
|
|
96
|
+
expect(env.executeStep).toHaveBeenNthCalledWith(2, step.steps[1], state, [
|
|
97
|
+
0,
|
|
98
|
+
'parallel',
|
|
99
|
+
1,
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('short-circuits on first completion when using wait_any', async () => {
|
|
104
|
+
const env = createEnv();
|
|
105
|
+
const state = createState();
|
|
106
|
+
const step: ParallelStep = {
|
|
107
|
+
id: 'parallel',
|
|
108
|
+
type: StepType.Parallel,
|
|
109
|
+
label: 'parallel',
|
|
110
|
+
strategy: 'wait_any',
|
|
111
|
+
steps: [createChild('a'), createChild('b')],
|
|
112
|
+
};
|
|
113
|
+
env.executeStep = jest.fn().mockResolvedValue(undefined);
|
|
114
|
+
|
|
115
|
+
const result = await executeParallel(env, step, state, []);
|
|
116
|
+
|
|
117
|
+
expect(result).toBeUndefined();
|
|
118
|
+
expect(env.executeStep).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(env.executeStep).toHaveBeenCalledWith(step.steps[0], state, [
|
|
120
|
+
'parallel',
|
|
121
|
+
0,
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('continues remaining steps after resuming from suspension (wait_all)', async () => {
|
|
126
|
+
const env = createEnv();
|
|
127
|
+
const state = createState();
|
|
128
|
+
const innerSuspension: Suspension = {
|
|
129
|
+
step: { id: 'a', name: 'a', type: StepType.Task } as StepInfo,
|
|
130
|
+
continue: jest.fn().mockResolvedValue(undefined),
|
|
131
|
+
};
|
|
132
|
+
const step: ParallelStep = {
|
|
133
|
+
id: 'parallel',
|
|
134
|
+
type: StepType.Parallel,
|
|
135
|
+
label: 'parallel',
|
|
136
|
+
strategy: 'wait_all',
|
|
137
|
+
steps: [createChild('a'), createChild('b')],
|
|
138
|
+
};
|
|
139
|
+
env.executeStep = jest
|
|
140
|
+
.fn()
|
|
141
|
+
.mockResolvedValueOnce(innerSuspension)
|
|
142
|
+
.mockResolvedValueOnce(undefined);
|
|
143
|
+
|
|
144
|
+
const suspension = await executeParallel(env, step, state, []);
|
|
145
|
+
expect(suspension).toEqual(
|
|
146
|
+
expect.objectContaining({ step: innerSuspension.step }),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const result = await suspension?.continue('resume-data');
|
|
150
|
+
expect(result).toBeUndefined();
|
|
151
|
+
expect(innerSuspension.continue).toHaveBeenCalledWith('resume-data');
|
|
152
|
+
expect(env.executeStep).toHaveBeenCalledTimes(2);
|
|
153
|
+
expect(env.executeStep).toHaveBeenLastCalledWith(step.steps[1], state, [
|
|
154
|
+
'parallel',
|
|
155
|
+
1,
|
|
156
|
+
]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('stops after resuming a suspended child when using wait_any', async () => {
|
|
160
|
+
const env = createEnv();
|
|
161
|
+
const state = createState();
|
|
162
|
+
const innerSuspension: Suspension = {
|
|
163
|
+
step: { id: 'a', name: 'a', type: StepType.Task } as StepInfo,
|
|
164
|
+
continue: jest.fn().mockResolvedValue(undefined),
|
|
165
|
+
};
|
|
166
|
+
const step: ParallelStep = {
|
|
167
|
+
id: 'parallel',
|
|
168
|
+
type: StepType.Parallel,
|
|
169
|
+
label: 'parallel',
|
|
170
|
+
strategy: 'wait_any',
|
|
171
|
+
steps: [createChild('a'), createChild('b')],
|
|
172
|
+
};
|
|
173
|
+
env.executeStep = jest.fn().mockResolvedValueOnce(innerSuspension);
|
|
174
|
+
|
|
175
|
+
const suspension = await executeParallel(env, step, state, []);
|
|
176
|
+
const result = await suspension?.continue('resume-data');
|
|
177
|
+
|
|
178
|
+
expect(result).toBeUndefined();
|
|
179
|
+
expect(innerSuspension.continue).toHaveBeenCalledWith('resume-data');
|
|
180
|
+
expect(env.executeStep).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ExecutionState,
|
|
9
|
+
ParallelStep,
|
|
10
|
+
Suspension,
|
|
11
|
+
} from '../workflow-types';
|
|
12
|
+
|
|
13
|
+
import { markStepsSkipped } from './skip-helpers';
|
|
14
|
+
import type { StepExecutorEnv } from './types';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Execute a set of steps in parallel order, respecting the configured strategy.
|
|
18
|
+
*
|
|
19
|
+
* @param env Executor environment providing helpers and workflow context.
|
|
20
|
+
* @param step The parallel step definition including strategy and children.
|
|
21
|
+
* @param state Mutable workflow execution state.
|
|
22
|
+
* @param path Path tokens locating this parallel block in the workflow.
|
|
23
|
+
* @param startIndex Index to resume execution from when continuing.
|
|
24
|
+
* @returns A suspension if a child pauses execution, otherwise void.
|
|
25
|
+
*/
|
|
26
|
+
export async function executeParallel(
|
|
27
|
+
env: StepExecutorEnv,
|
|
28
|
+
step: ParallelStep,
|
|
29
|
+
state: ExecutionState,
|
|
30
|
+
path: Array<number | string>,
|
|
31
|
+
startIndex = 0,
|
|
32
|
+
): Promise<Suspension | void> {
|
|
33
|
+
for (let index = startIndex; index < step.steps.length; index += 1) {
|
|
34
|
+
const child = step.steps[index];
|
|
35
|
+
const childPath = [...path, 'parallel', index];
|
|
36
|
+
const suspension = await env.executeStep(child, state, childPath);
|
|
37
|
+
if (suspension) {
|
|
38
|
+
return {
|
|
39
|
+
...suspension,
|
|
40
|
+
continue: async (resumeData: unknown) => {
|
|
41
|
+
const next = await suspension.continue(resumeData);
|
|
42
|
+
if (next) {
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (step.strategy === 'wait_any') {
|
|
47
|
+
if (index + 1 < step.steps.length) {
|
|
48
|
+
markStepsSkipped(
|
|
49
|
+
env,
|
|
50
|
+
step.steps.slice(index + 1),
|
|
51
|
+
state.iterationStack,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return executeParallel(env, step, state, path, index + 1);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (step.strategy === 'wait_any') {
|
|
64
|
+
if (index + 1 < step.steps.length) {
|
|
65
|
+
markStepsSkipped(
|
|
66
|
+
env,
|
|
67
|
+
step.steps.slice(index + 1),
|
|
68
|
+
state.iterationStack,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ActionSnapshot } from '../context';
|
|
8
|
+
import {
|
|
9
|
+
StepType,
|
|
10
|
+
type StepInfo,
|
|
11
|
+
type WorkflowEventMap,
|
|
12
|
+
} from '../workflow-event-emitter';
|
|
13
|
+
import type { CompiledStep } from '../workflow-types';
|
|
14
|
+
|
|
15
|
+
type SkipStepEnv = {
|
|
16
|
+
runId?: string;
|
|
17
|
+
buildInstanceStepInfo: (
|
|
18
|
+
step: CompiledStep,
|
|
19
|
+
iterationStack: number[],
|
|
20
|
+
) => StepInfo;
|
|
21
|
+
markSnapshot: (
|
|
22
|
+
step: StepInfo,
|
|
23
|
+
status: ActionSnapshot['status'],
|
|
24
|
+
reason?: string,
|
|
25
|
+
) => void;
|
|
26
|
+
emit: <K extends keyof WorkflowEventMap>(
|
|
27
|
+
event: K,
|
|
28
|
+
payload: WorkflowEventMap[K],
|
|
29
|
+
) => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const markStepSkipped = (
|
|
33
|
+
env: SkipStepEnv,
|
|
34
|
+
step: CompiledStep,
|
|
35
|
+
iterationStack: number[],
|
|
36
|
+
reason?: string,
|
|
37
|
+
) => {
|
|
38
|
+
const stepInfo = env.buildInstanceStepInfo(step, iterationStack);
|
|
39
|
+
env.markSnapshot(stepInfo, 'skipped', reason);
|
|
40
|
+
env.emit('hook:step:skipped', { runId: env.runId, step: stepInfo, reason });
|
|
41
|
+
|
|
42
|
+
switch (step.type) {
|
|
43
|
+
case StepType.Parallel:
|
|
44
|
+
step.steps.forEach((child) =>
|
|
45
|
+
markStepSkipped(env, child, iterationStack, reason),
|
|
46
|
+
);
|
|
47
|
+
break;
|
|
48
|
+
case StepType.Conditional:
|
|
49
|
+
step.branches.forEach((branch) =>
|
|
50
|
+
branch.steps.forEach((child) =>
|
|
51
|
+
markStepSkipped(env, child, iterationStack, reason),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
break;
|
|
55
|
+
case StepType.Loop:
|
|
56
|
+
case StepType.Task:
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const markStepsSkipped = (
|
|
62
|
+
env: SkipStepEnv,
|
|
63
|
+
steps: CompiledStep[],
|
|
64
|
+
iterationStack: number[],
|
|
65
|
+
reason?: string,
|
|
66
|
+
) => {
|
|
67
|
+
steps.forEach((step) => markStepSkipped(env, step, iterationStack, reason));
|
|
68
|
+
};
|