@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.
Files changed (82) hide show
  1. package/README.md +13 -5
  2. package/dist/cjs/action/abstract-action.js +8 -3
  3. package/dist/cjs/dsl.types.js +2 -1
  4. package/dist/cjs/errors.js +41 -0
  5. package/dist/cjs/index.js +7 -1
  6. package/dist/cjs/step-executors/parallel-executor.js +238 -24
  7. package/dist/cjs/step-executors/skip-helpers.js +25 -1
  8. package/dist/cjs/step-executors/task-executor.js +77 -10
  9. package/dist/cjs/suspension-rebuilder.js +1 -24
  10. package/dist/cjs/utils/naming.js +4 -4
  11. package/dist/cjs/utils/timeout.js +63 -8
  12. package/dist/cjs/utils/workflow-definition-resources.js +73 -0
  13. package/dist/cjs/workflow-compiler.js +0 -2
  14. package/dist/cjs/workflow-runner.js +40 -17
  15. package/dist/esm/action/abstract-action.js +8 -3
  16. package/dist/esm/dsl.types.js +2 -1
  17. package/dist/esm/errors.js +33 -0
  18. package/dist/esm/index.js +2 -0
  19. package/dist/esm/step-executors/parallel-executor.js +239 -25
  20. package/dist/esm/step-executors/skip-helpers.js +23 -0
  21. package/dist/esm/step-executors/task-executor.js +77 -10
  22. package/dist/esm/suspension-rebuilder.js +1 -24
  23. package/dist/esm/utils/naming.js +1 -1
  24. package/dist/esm/utils/timeout.js +63 -8
  25. package/dist/esm/utils/workflow-definition-resources.js +68 -0
  26. package/dist/esm/workflow-compiler.js +0 -2
  27. package/dist/esm/workflow-runner.js +40 -17
  28. package/dist/types/action/abstract-action.d.ts +1 -1
  29. package/dist/types/action/abstract-action.d.ts.map +1 -1
  30. package/dist/types/action/action.types.d.ts +2 -1
  31. package/dist/types/action/action.types.d.ts.map +1 -1
  32. package/dist/types/context.d.ts +2 -1
  33. package/dist/types/context.d.ts.map +1 -1
  34. package/dist/types/dsl.types.d.ts.map +1 -1
  35. package/dist/types/errors.d.ts +10 -0
  36. package/dist/types/errors.d.ts.map +1 -0
  37. package/dist/types/index.d.ts +2 -0
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/step-executors/parallel-executor.d.ts +3 -4
  40. package/dist/types/step-executors/parallel-executor.d.ts.map +1 -1
  41. package/dist/types/step-executors/skip-helpers.d.ts +1 -0
  42. package/dist/types/step-executors/skip-helpers.d.ts.map +1 -1
  43. package/dist/types/step-executors/task-executor.d.ts.map +1 -1
  44. package/dist/types/step-executors/types.d.ts +9 -1
  45. package/dist/types/step-executors/types.d.ts.map +1 -1
  46. package/dist/types/suspension-rebuilder.d.ts.map +1 -1
  47. package/dist/types/utils/naming.d.ts +1 -0
  48. package/dist/types/utils/naming.d.ts.map +1 -1
  49. package/dist/types/utils/timeout.d.ts +4 -2
  50. package/dist/types/utils/timeout.d.ts.map +1 -1
  51. package/dist/types/utils/workflow-definition-resources.d.ts +16 -0
  52. package/dist/types/utils/workflow-definition-resources.d.ts.map +1 -0
  53. package/dist/types/workflow-compiler.d.ts.map +1 -1
  54. package/dist/types/workflow-event-emitter.d.ts +12 -13
  55. package/dist/types/workflow-event-emitter.d.ts.map +1 -1
  56. package/dist/types/workflow-runner.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/__tests__/suspension-rebuilder.test.ts +3 -42
  59. package/src/__tests__/workflow-definition-resources.test.ts +97 -0
  60. package/src/__tests__/workflow-runner.test.ts +58 -5
  61. package/src/action/__tests__/action.test.ts +2 -1
  62. package/src/action/abstract-action.ts +9 -1
  63. package/src/action/action.types.ts +2 -0
  64. package/src/context.ts +2 -0
  65. package/src/dsl.types.ts +5 -1
  66. package/src/errors.ts +43 -0
  67. package/src/index.ts +10 -0
  68. package/src/step-executors/conditional-executor.test.ts +8 -3
  69. package/src/step-executors/loop-executor.test.ts +8 -3
  70. package/src/step-executors/parallel-executor.test.ts +191 -82
  71. package/src/step-executors/parallel-executor.ts +402 -34
  72. package/src/step-executors/skip-helpers.ts +41 -0
  73. package/src/step-executors/task-executor.test.ts +85 -21
  74. package/src/step-executors/task-executor.ts +94 -10
  75. package/src/step-executors/types.ts +14 -1
  76. package/src/suspension-rebuilder.ts +1 -53
  77. package/src/utils/naming.ts +1 -1
  78. package/src/utils/timeout.ts +78 -8
  79. package/src/utils/workflow-definition-resources.ts +111 -0
  80. package/src/workflow-compiler.ts +0 -3
  81. package/src/workflow-event-emitter.ts +13 -6
  82. 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('executes all children when using wait_all', async () => {
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: 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);
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 result = await executeParallel(env, step, state, [0]);
126
+ const resultPromise = executeParallel(env, step, state, [0]);
127
+ await flushPromises();
88
128
 
89
- expect(result).toBeUndefined();
129
+ expect(started).toEqual(['a', 'b']);
90
130
  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
- ]);
131
+
132
+ resolvers.forEach((resolve) => resolve());
133
+ await expect(resultPromise).resolves.toBeUndefined();
101
134
  });
102
135
 
103
- it('short-circuits on first completion when using wait_any', async () => {
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: 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);
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
- const result = await executeParallel(env, step, state, []);
149
+ await executeParallel(env, step, state, []);
116
150
 
117
- expect(result).toBeUndefined();
118
- expect(env.executeStep).toHaveBeenCalledTimes(1);
119
- expect(env.executeStep).toHaveBeenCalledWith(step.steps[0], state, [
120
- 'parallel',
121
- 0,
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('continues remaining steps after resuming from suspension (wait_all)', async () => {
159
+ it('fails wait_all fast and aborts unfinished siblings', async () => {
126
160
  const env = createEnv();
127
161
  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);
162
+ const step = createStep('wait_all');
163
+ const failure = new Error('branch failed');
143
164
 
144
- const suspension = await executeParallel(env, step, state, []);
145
- expect(suspension).toEqual(
146
- expect.objectContaining({ step: innerSuspension.step }),
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
- 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
- ]);
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('stops after resuming a suspended child when using wait_any', async () => {
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
- const suspension = await executeParallel(env, step, state, []);
176
- const result = await suspension?.continue('resume-data');
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(result).toBeUndefined();
179
- expect(innerSuspension.continue).toHaveBeenCalledWith('resume-data');
180
- expect(env.executeStep).toHaveBeenCalledTimes(1);
287
+ await expect(executeParallel(env, step, state, [])).rejects.toBeInstanceOf(
288
+ ParallelSuspensionError,
289
+ );
181
290
  });
182
291
  });