@hexabot-ai/agentic 3.1.2-alpha.8 → 3.1.2-beta.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/README.md +13 -5
- package/dist/cjs/action/abstract-action.js +8 -3
- 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/timeout.js +63 -8
- package/dist/cjs/utils/workflow-definition-resources.js +73 -0
- package/dist/cjs/workflow-runner.js +40 -17
- package/dist/esm/action/abstract-action.js +8 -3
- 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/timeout.js +63 -8
- package/dist/esm/utils/workflow-definition-resources.js +68 -0
- 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/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/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-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/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/timeout.ts +78 -8
- package/src/utils/workflow-definition-resources.ts +111 -0
- package/src/workflow-event-emitter.ts +13 -6
- package/src/workflow-runner.ts +59 -19
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { BaseWorkflowContext } from '../context';
|
|
8
|
+
import { ParallelSuspensionError } from '../errors';
|
|
8
9
|
import type { RuntimeSuspensionRequest } from '../runner-runtime-control';
|
|
9
10
|
import {
|
|
10
11
|
StepType,
|
|
@@ -48,12 +49,14 @@ const createEnv = (): StepExecutorEnv => {
|
|
|
48
49
|
outputMapping: {},
|
|
49
50
|
inputParser: { parse: (value: unknown) => value } as any,
|
|
50
51
|
} as any;
|
|
51
|
-
|
|
52
|
-
return {
|
|
52
|
+
const env = {
|
|
53
53
|
compiled,
|
|
54
54
|
context: new TestContext(),
|
|
55
|
+
signal: new AbortController().signal,
|
|
55
56
|
runId: 'run-1',
|
|
56
|
-
buildInstanceStepInfo: jest.fn()
|
|
57
|
+
buildInstanceStepInfo: jest.fn((step: CompiledStep): StepInfo => {
|
|
58
|
+
return { id: step.id, name: step.label, type: step.type };
|
|
59
|
+
}),
|
|
57
60
|
markSnapshot: jest.fn(),
|
|
58
61
|
recordStepExecution: jest.fn(),
|
|
59
62
|
emit: jest.fn(),
|
|
@@ -68,115 +71,221 @@ const createEnv = (): StepExecutorEnv => {
|
|
|
68
71
|
captureTaskOutput: jest.fn(),
|
|
69
72
|
executeFlow: jest.fn(),
|
|
70
73
|
executeStep: jest.fn(),
|
|
71
|
-
|
|
74
|
+
fork: jest.fn(),
|
|
75
|
+
} as StepExecutorEnv;
|
|
76
|
+
|
|
77
|
+
env.fork = jest.fn((overrides) => {
|
|
78
|
+
const forked = { ...env, ...overrides } as StepExecutorEnv;
|
|
79
|
+
|
|
80
|
+
forked.executeStep = jest.fn((step, state, path) =>
|
|
81
|
+
(env.executeStep as jest.Mock)(step, state, path, forked.signal),
|
|
82
|
+
);
|
|
83
|
+
forked.executeFlow = jest.fn((steps, state, path, startIndex) =>
|
|
84
|
+
(env.executeFlow as jest.Mock)(
|
|
85
|
+
steps,
|
|
86
|
+
state,
|
|
87
|
+
path,
|
|
88
|
+
startIndex,
|
|
89
|
+
forked.signal,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return forked;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return env;
|
|
97
|
+
};
|
|
98
|
+
const createStep = (strategy: ParallelStep['strategy']): ParallelStep => ({
|
|
99
|
+
id: 'parallel',
|
|
100
|
+
type: StepType.Parallel,
|
|
101
|
+
label: 'parallel',
|
|
102
|
+
strategy,
|
|
103
|
+
steps: [createChild('a'), createChild('b')],
|
|
104
|
+
});
|
|
105
|
+
const flushPromises = async () => {
|
|
106
|
+
await Promise.resolve();
|
|
107
|
+
await Promise.resolve();
|
|
72
108
|
};
|
|
73
109
|
|
|
74
110
|
describe('executeParallel', () => {
|
|
75
|
-
it('
|
|
111
|
+
it('starts all children before waiting when using wait_all', async () => {
|
|
76
112
|
const env = createEnv();
|
|
77
113
|
const state = createState();
|
|
78
|
-
const step
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
114
|
+
const step = createStep('wait_all');
|
|
115
|
+
const started: string[] = [];
|
|
116
|
+
const resolvers: Array<() => void> = [];
|
|
117
|
+
|
|
118
|
+
env.executeStep = jest.fn((child: CompiledStep) => {
|
|
119
|
+
started.push(child.id);
|
|
120
|
+
|
|
121
|
+
return new Promise<void>((resolve) => {
|
|
122
|
+
resolvers.push(resolve);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
86
125
|
|
|
87
|
-
const
|
|
126
|
+
const resultPromise = executeParallel(env, step, state, [0]);
|
|
127
|
+
await flushPromises();
|
|
88
128
|
|
|
89
|
-
expect(
|
|
129
|
+
expect(started).toEqual(['a', 'b']);
|
|
90
130
|
expect(env.executeStep).toHaveBeenCalledTimes(2);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
0,
|
|
95
|
-
]);
|
|
96
|
-
expect(env.executeStep).toHaveBeenNthCalledWith(2, step.steps[1], state, [
|
|
97
|
-
0,
|
|
98
|
-
'parallel',
|
|
99
|
-
1,
|
|
100
|
-
]);
|
|
131
|
+
|
|
132
|
+
resolvers.forEach((resolve) => resolve());
|
|
133
|
+
await expect(resultPromise).resolves.toBeUndefined();
|
|
101
134
|
});
|
|
102
135
|
|
|
103
|
-
it('
|
|
136
|
+
it('merges wait_all output deltas in child-index order', async () => {
|
|
104
137
|
const env = createEnv();
|
|
105
138
|
const state = createState();
|
|
106
|
-
const step
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
139
|
+
const step = createStep('wait_all');
|
|
140
|
+
|
|
141
|
+
state.output.existing = 'keep';
|
|
142
|
+
env.executeStep = jest.fn(
|
|
143
|
+
async (child: CompiledStep, branchState: ExecutionState) => {
|
|
144
|
+
branchState.output.shared = child.id;
|
|
145
|
+
branchState.output[`${child.id}_only`] = true;
|
|
146
|
+
},
|
|
147
|
+
);
|
|
114
148
|
|
|
115
|
-
|
|
149
|
+
await executeParallel(env, step, state, []);
|
|
116
150
|
|
|
117
|
-
expect(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
expect(state.output).toEqual({
|
|
152
|
+
existing: 'keep',
|
|
153
|
+
shared: 'b',
|
|
154
|
+
a_only: true,
|
|
155
|
+
b_only: true,
|
|
156
|
+
});
|
|
123
157
|
});
|
|
124
158
|
|
|
125
|
-
it('
|
|
159
|
+
it('fails wait_all fast and aborts unfinished siblings', async () => {
|
|
126
160
|
const env = createEnv();
|
|
127
161
|
const state = createState();
|
|
128
|
-
const
|
|
129
|
-
|
|
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);
|
|
162
|
+
const step = createStep('wait_all');
|
|
163
|
+
const failure = new Error('branch failed');
|
|
143
164
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
165
|
+
env.executeStep = jest.fn(
|
|
166
|
+
(
|
|
167
|
+
child: CompiledStep,
|
|
168
|
+
_branchState: ExecutionState,
|
|
169
|
+
_path: Array<number | string>,
|
|
170
|
+
signal?: AbortSignal,
|
|
171
|
+
) => {
|
|
172
|
+
if (child.id === 'a') {
|
|
173
|
+
return Promise.reject(failure);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return new Promise<void>((_resolve, reject) => {
|
|
177
|
+
signal?.addEventListener('abort', () => reject(signal.reason), {
|
|
178
|
+
once: true,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
},
|
|
147
182
|
);
|
|
148
183
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
expect(env.
|
|
153
|
-
|
|
154
|
-
'
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
await expect(executeParallel(env, step, state, [])).rejects.toThrow(
|
|
185
|
+
'branch failed',
|
|
186
|
+
);
|
|
187
|
+
expect(env.markSnapshot).toHaveBeenCalledWith(
|
|
188
|
+
expect.objectContaining({ id: 'b' }),
|
|
189
|
+
'cancelled',
|
|
190
|
+
'branch failed',
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('wait_any starts every child, keeps only the winner output, and cancels losers', async () => {
|
|
195
|
+
const env = createEnv();
|
|
196
|
+
const state = createState();
|
|
197
|
+
const step = createStep('wait_any');
|
|
198
|
+
const started: string[] = [];
|
|
199
|
+
|
|
200
|
+
env.executeStep = jest.fn(
|
|
201
|
+
async (
|
|
202
|
+
child: CompiledStep,
|
|
203
|
+
branchState: ExecutionState,
|
|
204
|
+
_path: Array<number | string>,
|
|
205
|
+
signal?: AbortSignal,
|
|
206
|
+
) => {
|
|
207
|
+
started.push(child.id);
|
|
208
|
+
if (child.id === 'b') {
|
|
209
|
+
branchState.output.winner = child.id;
|
|
210
|
+
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return new Promise<void>((_resolve, reject) => {
|
|
215
|
+
signal?.addEventListener('abort', () => reject(signal.reason), {
|
|
216
|
+
once: true,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
await executeParallel(env, step, state, []);
|
|
223
|
+
|
|
224
|
+
expect(started).toEqual(['a', 'b']);
|
|
225
|
+
expect(state.output).toEqual({ winner: 'b' });
|
|
226
|
+
expect(env.markSnapshot).toHaveBeenCalledWith(
|
|
227
|
+
expect.objectContaining({ id: 'a' }),
|
|
228
|
+
'cancelled',
|
|
229
|
+
'Parallel wait_any winner selected.',
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('wait_any fails fast before any successful winner', async () => {
|
|
234
|
+
const env = createEnv();
|
|
235
|
+
const state = createState();
|
|
236
|
+
const step = createStep('wait_any');
|
|
237
|
+
|
|
238
|
+
env.executeStep = jest.fn(
|
|
239
|
+
(
|
|
240
|
+
child: CompiledStep,
|
|
241
|
+
branchState: ExecutionState,
|
|
242
|
+
_path: Array<number | string>,
|
|
243
|
+
signal?: AbortSignal,
|
|
244
|
+
) => {
|
|
245
|
+
if (child.id === 'a') {
|
|
246
|
+
return Promise.reject(new Error('no winner'));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
branchState.output.loser = true;
|
|
250
|
+
|
|
251
|
+
return new Promise<void>((_resolve, reject) => {
|
|
252
|
+
signal?.addEventListener('abort', () => reject(signal.reason), {
|
|
253
|
+
once: true,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
await expect(executeParallel(env, step, state, [])).rejects.toThrow(
|
|
260
|
+
'no winner',
|
|
261
|
+
);
|
|
262
|
+
expect(state.output).toEqual({});
|
|
263
|
+
expect(env.markSnapshot).toHaveBeenCalledWith(
|
|
264
|
+
expect.objectContaining({ id: 'b' }),
|
|
265
|
+
'cancelled',
|
|
266
|
+
'no winner',
|
|
267
|
+
);
|
|
157
268
|
});
|
|
158
269
|
|
|
159
|
-
it('
|
|
270
|
+
it('rejects suspensions returned by children', async () => {
|
|
160
271
|
const env = createEnv();
|
|
161
272
|
const state = createState();
|
|
273
|
+
const step = createStep('wait_all');
|
|
162
274
|
const innerSuspension: Suspension = {
|
|
163
275
|
step: { id: 'a', name: 'a', type: StepType.Task } as StepInfo,
|
|
164
276
|
continue: jest.fn().mockResolvedValue(undefined),
|
|
165
277
|
};
|
|
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
278
|
|
|
175
|
-
|
|
176
|
-
|
|
279
|
+
env.executeStep = jest.fn((child: CompiledStep) => {
|
|
280
|
+
if (child.id === 'a') {
|
|
281
|
+
return Promise.resolve(innerSuspension);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return Promise.resolve(undefined);
|
|
285
|
+
});
|
|
177
286
|
|
|
178
|
-
expect(
|
|
179
|
-
|
|
180
|
-
|
|
287
|
+
await expect(executeParallel(env, step, state, [])).rejects.toBeInstanceOf(
|
|
288
|
+
ParallelSuspensionError,
|
|
289
|
+
);
|
|
181
290
|
});
|
|
182
291
|
});
|