@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
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
shouldStopLoop,
|
|
19
19
|
updateAccumulator,
|
|
20
20
|
} from '../step-executors/loop-executor';
|
|
21
|
-
import { executeParallel as runParallelExecutor } from '../step-executors/parallel-executor';
|
|
22
21
|
import type { StepExecutorEnv } from '../step-executors/types';
|
|
23
22
|
import {
|
|
24
23
|
parseSuspendedStepId,
|
|
@@ -40,11 +39,6 @@ jest.mock('../step-executors/loop-executor', () => ({
|
|
|
40
39
|
shouldStopLoop: jest.fn(),
|
|
41
40
|
}));
|
|
42
41
|
|
|
43
|
-
jest.mock('../step-executors/parallel-executor', () => ({
|
|
44
|
-
__esModule: true,
|
|
45
|
-
executeParallel: jest.fn(),
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
42
|
const mockedRunLoopExecutor = runLoopExecutor as jest.MockedFunction<
|
|
49
43
|
typeof runLoopExecutor
|
|
50
44
|
>;
|
|
@@ -54,9 +48,6 @@ const mockedUpdateAccumulator = updateAccumulator as jest.MockedFunction<
|
|
|
54
48
|
const mockedShouldStopLoop = shouldStopLoop as jest.MockedFunction<
|
|
55
49
|
typeof shouldStopLoop
|
|
56
50
|
>;
|
|
57
|
-
const mockedRunParallelExecutor = runParallelExecutor as jest.MockedFunction<
|
|
58
|
-
typeof runParallelExecutor
|
|
59
|
-
>;
|
|
60
51
|
|
|
61
52
|
class TestContext extends BaseWorkflowContext {
|
|
62
53
|
public eventEmitter: EventEmitterLike = { emit: jest.fn(), on: jest.fn() };
|
|
@@ -268,7 +259,7 @@ describe('rebuildSuspension', () => {
|
|
|
268
259
|
expect(deps.executeFlow).toHaveBeenCalledWith(compiled.flow, state, [], 1);
|
|
269
260
|
});
|
|
270
261
|
|
|
271
|
-
it('
|
|
262
|
+
it('does not rebuild suspensions inside parallel blocks', () => {
|
|
272
263
|
const firstTask = createTask('child_a');
|
|
273
264
|
const secondTask = createTask('child_b');
|
|
274
265
|
const childA: CompiledStep = {
|
|
@@ -308,39 +299,9 @@ describe('rebuildSuspension', () => {
|
|
|
308
299
|
state,
|
|
309
300
|
stepId: '0.parallel.1:child_b',
|
|
310
301
|
});
|
|
311
|
-
const nextSuspension = {
|
|
312
|
-
step: {
|
|
313
|
-
id: '0.parallel.2:child_c',
|
|
314
|
-
name: 'child_c',
|
|
315
|
-
type: StepType.Task,
|
|
316
|
-
},
|
|
317
|
-
continue: jest.fn().mockResolvedValue(undefined),
|
|
318
|
-
};
|
|
319
|
-
mockedRunParallelExecutor.mockResolvedValue(nextSuspension);
|
|
320
302
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
expect(deps.captureTaskOutput).toHaveBeenCalledWith(secondTask, state, {
|
|
324
|
-
payload: true,
|
|
325
|
-
});
|
|
326
|
-
expect(mockedRunParallelExecutor).toHaveBeenCalledWith(
|
|
327
|
-
expect.anything(),
|
|
328
|
-
parallelStep,
|
|
329
|
-
state,
|
|
330
|
-
[0],
|
|
331
|
-
2,
|
|
332
|
-
);
|
|
333
|
-
expect(result?.step.id).toBe(nextSuspension.step.id);
|
|
334
|
-
|
|
335
|
-
await result?.continue({ done: true });
|
|
336
|
-
|
|
337
|
-
expect(nextSuspension.continue).toHaveBeenCalledWith({ done: true });
|
|
338
|
-
expect(deps.executeFlow).toHaveBeenLastCalledWith(
|
|
339
|
-
compiled.flow,
|
|
340
|
-
state,
|
|
341
|
-
[],
|
|
342
|
-
1,
|
|
343
|
-
);
|
|
303
|
+
expect(suspension).toBeNull();
|
|
304
|
+
expect(deps.captureTaskOutput).not.toHaveBeenCalled();
|
|
344
305
|
});
|
|
345
306
|
|
|
346
307
|
it('resumes a loop suspension, updating accumulators and continuing execution', async () => {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2026 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WorkflowDefinition } from '../dsl.types';
|
|
8
|
+
import {
|
|
9
|
+
collectWorkflowDefinitionResourceRefs,
|
|
10
|
+
remapWorkflowDefinitionResourceRefs,
|
|
11
|
+
type WorkflowDefinitionResourceDescriptor,
|
|
12
|
+
} from '../utils/workflow-definition-resources';
|
|
13
|
+
|
|
14
|
+
const descriptors: WorkflowDefinitionResourceDescriptor[] = [
|
|
15
|
+
{ kind: 'memory', settingsKey: 'definition_id' },
|
|
16
|
+
{ kind: 'mcp', settingsKey: 'server_id' },
|
|
17
|
+
];
|
|
18
|
+
const definition: WorkflowDefinition = {
|
|
19
|
+
defs: {
|
|
20
|
+
profile_memory: {
|
|
21
|
+
kind: 'memory',
|
|
22
|
+
settings: {
|
|
23
|
+
definition_id: 'memory-1',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
duplicate_profile_memory: {
|
|
27
|
+
kind: 'memory',
|
|
28
|
+
settings: {
|
|
29
|
+
definition_id: 'memory-1',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
search_mcp: {
|
|
33
|
+
kind: 'mcp',
|
|
34
|
+
settings: {
|
|
35
|
+
server_id: 'mcp-1',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
greet: {
|
|
39
|
+
kind: 'task',
|
|
40
|
+
action: 'send_message',
|
|
41
|
+
bindings: {
|
|
42
|
+
memory: ['profile_memory'],
|
|
43
|
+
mcp: ['search_mcp'],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
flow: [{ do: 'greet' }],
|
|
48
|
+
outputs: { result: '=$output.greet' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe('workflow definition resource helpers', () => {
|
|
52
|
+
it('collects unique resource references by descriptor kind', () => {
|
|
53
|
+
expect(
|
|
54
|
+
collectWorkflowDefinitionResourceRefs(definition, descriptors),
|
|
55
|
+
).toEqual({
|
|
56
|
+
memory: ['memory-1'],
|
|
57
|
+
mcp: ['mcp-1'],
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('remaps resource references without mutating the original definition', () => {
|
|
62
|
+
const updated = remapWorkflowDefinitionResourceRefs(
|
|
63
|
+
definition,
|
|
64
|
+
descriptors,
|
|
65
|
+
{
|
|
66
|
+
memory: { 'memory-1': 'memory-local' },
|
|
67
|
+
mcp: { 'mcp-1': 'mcp-local' },
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(updated).not.toBe(definition);
|
|
72
|
+
expect(updated.defs.profile_memory.settings).toEqual({
|
|
73
|
+
definition_id: 'memory-local',
|
|
74
|
+
});
|
|
75
|
+
expect(updated.defs.duplicate_profile_memory.settings).toEqual({
|
|
76
|
+
definition_id: 'memory-local',
|
|
77
|
+
});
|
|
78
|
+
expect(updated.defs.search_mcp.settings).toEqual({
|
|
79
|
+
server_id: 'mcp-local',
|
|
80
|
+
});
|
|
81
|
+
expect(definition.defs.profile_memory.settings).toEqual({
|
|
82
|
+
definition_id: 'memory-1',
|
|
83
|
+
});
|
|
84
|
+
expect(definition.defs.search_mcp.settings).toEqual({
|
|
85
|
+
server_id: 'mcp-1',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns the original definition when no refs change', () => {
|
|
90
|
+
expect(
|
|
91
|
+
remapWorkflowDefinitionResourceRefs(definition, descriptors, {
|
|
92
|
+
memory: {},
|
|
93
|
+
mcp: {},
|
|
94
|
+
}),
|
|
95
|
+
).toBe(definition);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -9,6 +9,7 @@ import { z } from 'zod';
|
|
|
9
9
|
import { defineAction } from '../action/action';
|
|
10
10
|
import { BaseWorkflowContext } from '../context';
|
|
11
11
|
import type { Settings, WorkflowDefinition } from '../dsl.types';
|
|
12
|
+
import { ParallelSuspensionError } from '../errors';
|
|
12
13
|
import { compileWorkflow } from '../workflow-compiler';
|
|
13
14
|
import {
|
|
14
15
|
StepType,
|
|
@@ -298,6 +299,7 @@ describe('WorkflowRunner', () => {
|
|
|
298
299
|
});
|
|
299
300
|
const runner = new WorkflowRunner(compiled, { runId: 'run-1' });
|
|
300
301
|
const context = new TestContext({});
|
|
302
|
+
context.eventEmitter = emitter;
|
|
301
303
|
const result = await runner.start({
|
|
302
304
|
inputData: { items: [10, 20] },
|
|
303
305
|
context,
|
|
@@ -313,15 +315,13 @@ describe('WorkflowRunner', () => {
|
|
|
313
315
|
}
|
|
314
316
|
|
|
315
317
|
expect(firstExecute).toHaveBeenCalledTimes(1);
|
|
316
|
-
expect(secondExecute).
|
|
318
|
+
expect(secondExecute).toHaveBeenCalledTimes(1);
|
|
317
319
|
expect(echoExecute).toHaveBeenCalledTimes(3);
|
|
318
|
-
expect(
|
|
319
|
-
eventLog.filter((entry) => entry.includes('second_task')),
|
|
320
|
-
).toHaveLength(0);
|
|
320
|
+
expect(eventLog).toContain('start:second_task');
|
|
321
321
|
expect(runner.getSnapshot().status).toBe('finished');
|
|
322
322
|
const snapshots = runner.getSnapshot().actions;
|
|
323
323
|
expect(snapshots['0.parallel.0:first_task']?.status).toBe('completed');
|
|
324
|
-
expect(snapshots['0.parallel.1:second_task']?.status).toBe('
|
|
324
|
+
expect(snapshots['0.parallel.1:second_task']?.status).toBe('cancelled');
|
|
325
325
|
expect(snapshots['1.branch.0.0:branch_task']?.status).toBe('completed');
|
|
326
326
|
expect(snapshots['1.branch.1.0:second_task']?.status).toBe('skipped');
|
|
327
327
|
});
|
|
@@ -741,6 +741,59 @@ describe('WorkflowRunner', () => {
|
|
|
741
741
|
expect(() => context.workflow).toThrow();
|
|
742
742
|
});
|
|
743
743
|
|
|
744
|
+
it('fails when a parallel branch attempts to suspend', async () => {
|
|
745
|
+
const suspendAction = defineAction<
|
|
746
|
+
unknown,
|
|
747
|
+
{ reply: string },
|
|
748
|
+
TestContext,
|
|
749
|
+
Settings
|
|
750
|
+
>({
|
|
751
|
+
name: 'parallel_suspend_action',
|
|
752
|
+
inputSchema: z.any(),
|
|
753
|
+
outputSchema: z.object({ reply: z.string() }),
|
|
754
|
+
execute: async ({ context }) => {
|
|
755
|
+
await context.workflow.suspend({ reason: 'not_allowed' });
|
|
756
|
+
|
|
757
|
+
return { reply: 'unreachable' };
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
const definition: WorkflowDefinition = {
|
|
761
|
+
defs: createTaskDefs({
|
|
762
|
+
wait_step: { action: 'parallel_suspend_action' },
|
|
763
|
+
}),
|
|
764
|
+
flow: [
|
|
765
|
+
{
|
|
766
|
+
parallel: {
|
|
767
|
+
strategy: 'wait_all',
|
|
768
|
+
steps: [{ do: 'wait_step' }],
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
],
|
|
772
|
+
outputs: { reply: '=$output.wait_step.reply' },
|
|
773
|
+
};
|
|
774
|
+
const compiled = compileWorkflow(definition, {
|
|
775
|
+
actions: { parallel_suspend_action: suspendAction },
|
|
776
|
+
});
|
|
777
|
+
const runner = new WorkflowRunner(compiled, {
|
|
778
|
+
runId: 'run-parallel-suspend',
|
|
779
|
+
});
|
|
780
|
+
const result = await runner.start({
|
|
781
|
+
inputData: {},
|
|
782
|
+
context: new TestContext({}),
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
expect(result.status).toBe('failed');
|
|
786
|
+
if (result.status === 'failed') {
|
|
787
|
+
expect(result.error).toBeInstanceOf(ParallelSuspensionError);
|
|
788
|
+
}
|
|
789
|
+
expect(
|
|
790
|
+
runner.getSnapshot().actions['0.parallel.0:wait_step'],
|
|
791
|
+
).toMatchObject({
|
|
792
|
+
status: 'failed',
|
|
793
|
+
reason: 'workflow.suspend() is not supported inside a parallel block.',
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
744
797
|
it('accepts emitter-like implementations without Node EventEmitter', async () => {
|
|
745
798
|
const pingAction = defineAction<
|
|
746
799
|
unknown,
|
|
@@ -189,6 +189,7 @@ class FlakyDoubleAction extends DoubleAction {
|
|
|
189
189
|
context,
|
|
190
190
|
settings,
|
|
191
191
|
bindings,
|
|
192
|
+
signal,
|
|
192
193
|
}: ActionExecutionArgs<
|
|
193
194
|
z.infer<typeof InputSchema>,
|
|
194
195
|
DoubleContext,
|
|
@@ -199,7 +200,7 @@ class FlakyDoubleAction extends DoubleAction {
|
|
|
199
200
|
throw new Error('Intermittent failure');
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
return super.execute({ input, context, settings, bindings });
|
|
203
|
+
return super.execute({ input, context, settings, bindings, signal });
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
|
|
@@ -8,6 +8,7 @@ import { z, ZodType } from 'zod';
|
|
|
8
8
|
|
|
9
9
|
import { BaseWorkflowContext } from '../context';
|
|
10
10
|
import { BaseSettingsSchema } from '../dsl.types';
|
|
11
|
+
import { throwIfAborted } from '../errors';
|
|
11
12
|
import { assertSnakeCaseName } from '../utils/naming';
|
|
12
13
|
import { sleep, withTimeout } from '../utils/timeout';
|
|
13
14
|
|
|
@@ -112,6 +113,7 @@ export abstract class AbstractAction<
|
|
|
112
113
|
context: C,
|
|
113
114
|
settings?: Partial<RuntimeSettings<S>>,
|
|
114
115
|
bindings?: B,
|
|
116
|
+
signal?: AbortSignal,
|
|
115
117
|
): Promise<O> {
|
|
116
118
|
const input = this.parseInput(payload);
|
|
117
119
|
const parsedSettings = this.parseSettings(settings);
|
|
@@ -136,6 +138,10 @@ export abstract class AbstractAction<
|
|
|
136
138
|
const multiplier = retrySettings.multiplier;
|
|
137
139
|
|
|
138
140
|
while (attempt < maxAttempts) {
|
|
141
|
+
if (signal) {
|
|
142
|
+
throwIfAborted(signal);
|
|
143
|
+
}
|
|
144
|
+
|
|
139
145
|
try {
|
|
140
146
|
const result = await withTimeout(
|
|
141
147
|
this.execute({
|
|
@@ -143,8 +149,10 @@ export abstract class AbstractAction<
|
|
|
143
149
|
context,
|
|
144
150
|
settings: parsedSettings,
|
|
145
151
|
bindings: parsedBindings,
|
|
152
|
+
signal: signal ?? new AbortController().signal,
|
|
146
153
|
}),
|
|
147
154
|
timeoutMs,
|
|
155
|
+
signal,
|
|
148
156
|
);
|
|
149
157
|
|
|
150
158
|
return this.parseOutput(result);
|
|
@@ -166,7 +174,7 @@ export abstract class AbstractAction<
|
|
|
166
174
|
const jitteredDelay = Math.max(0, Math.round(delay * jitterFactor));
|
|
167
175
|
|
|
168
176
|
if (jitteredDelay > 0) {
|
|
169
|
-
await sleep(jitteredDelay);
|
|
177
|
+
await sleep(jitteredDelay, signal);
|
|
170
178
|
}
|
|
171
179
|
}
|
|
172
180
|
|
|
@@ -34,6 +34,7 @@ export interface ActionExecutionArgs<
|
|
|
34
34
|
context: C;
|
|
35
35
|
settings: RuntimeSettings<S>;
|
|
36
36
|
bindings: B;
|
|
37
|
+
signal: AbortSignal;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export interface Action<
|
|
@@ -59,6 +60,7 @@ export interface Action<
|
|
|
59
60
|
context: C,
|
|
60
61
|
settings?: Partial<RuntimeSettings<S>>,
|
|
61
62
|
bindings?: B,
|
|
63
|
+
signal?: AbortSignal,
|
|
62
64
|
): Promise<O>;
|
|
63
65
|
}
|
|
64
66
|
|
package/src/context.ts
CHANGED
|
@@ -42,6 +42,7 @@ export const WORKFLOW_RUN_STATUSES: WorkflowRunStatus[] = [
|
|
|
42
42
|
* - suspended: action paused via `workflow.suspend`; resumes to completed.
|
|
43
43
|
* - completed: action resolved (either immediately or after resume).
|
|
44
44
|
* - failed: action threw an error.
|
|
45
|
+
* - cancelled: action was aborted by the workflow scheduler.
|
|
45
46
|
* - skipped: control flow bypassed the step (e.g., alternate branch or wait_any short-circuit).
|
|
46
47
|
*/
|
|
47
48
|
export type ActionStatus =
|
|
@@ -50,6 +51,7 @@ export type ActionStatus =
|
|
|
50
51
|
| 'suspended'
|
|
51
52
|
| 'completed'
|
|
52
53
|
| 'failed'
|
|
54
|
+
| 'cancelled'
|
|
53
55
|
| 'skipped';
|
|
54
56
|
|
|
55
57
|
export interface SuspensionOptions {
|
package/src/dsl.types.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { z } from 'zod';
|
|
|
10
10
|
|
|
11
11
|
import type { BindingKindSchemas } from './bindings/base-binding';
|
|
12
12
|
import { validateAndResolveBindings } from './bindings/base-binding';
|
|
13
|
+
import { SNAKE_CASE_REGEX } from './utils/naming';
|
|
13
14
|
|
|
14
15
|
export const ExpressionStringSchema = z
|
|
15
16
|
.string()
|
|
@@ -330,7 +331,10 @@ export const WorkflowDefinitionSchema = z.strictObject({
|
|
|
330
331
|
inputs: InputsSchema.optional(),
|
|
331
332
|
context: z.record(z.string(), JsonValueSchema).optional(),
|
|
332
333
|
defaults: DefaultsSchema.optional(),
|
|
333
|
-
defs: z.record(
|
|
334
|
+
defs: z.record(
|
|
335
|
+
z.string().regex(SNAKE_CASE_REGEX, 'Name must be snake_case'),
|
|
336
|
+
DefDefinitionSchema,
|
|
337
|
+
),
|
|
334
338
|
flow: z.array(FlowStepSchema),
|
|
335
339
|
outputs: z.record(z.string(), ExpressionStringSchema),
|
|
336
340
|
});
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class WorkflowCancellationError extends Error {
|
|
8
|
+
constructor(message = 'Workflow execution was cancelled.') {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'WorkflowCancellationError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ParallelSuspensionError extends Error {
|
|
15
|
+
constructor() {
|
|
16
|
+
super('workflow.suspend() is not supported inside a parallel block.');
|
|
17
|
+
this.name = 'ParallelSuspensionError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const isWorkflowCancellationError = (
|
|
22
|
+
error: unknown,
|
|
23
|
+
): error is WorkflowCancellationError =>
|
|
24
|
+
error instanceof WorkflowCancellationError ||
|
|
25
|
+
(error instanceof Error && error.name === 'WorkflowCancellationError');
|
|
26
|
+
|
|
27
|
+
export const getAbortReason = (signal: AbortSignal): Error => {
|
|
28
|
+
if (signal.reason instanceof Error) {
|
|
29
|
+
return signal.reason;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (signal.reason !== undefined) {
|
|
33
|
+
return new WorkflowCancellationError(String(signal.reason));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new WorkflowCancellationError();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const throwIfAborted = (signal: AbortSignal): void => {
|
|
40
|
+
if (signal.aborted) {
|
|
41
|
+
throw getAbortReason(signal);
|
|
42
|
+
}
|
|
43
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -92,6 +92,8 @@ export type {
|
|
|
92
92
|
|
|
93
93
|
export { NonDeterministicWorkflowError } from './runner-runtime-control';
|
|
94
94
|
|
|
95
|
+
export { ParallelSuspensionError, WorkflowCancellationError } from './errors';
|
|
96
|
+
|
|
95
97
|
export {
|
|
96
98
|
compileValue,
|
|
97
99
|
evaluateMapping,
|
|
@@ -114,3 +116,11 @@ export {
|
|
|
114
116
|
} from './utils/naming';
|
|
115
117
|
|
|
116
118
|
export { sleep, withTimeout } from './utils/timeout';
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
collectWorkflowDefinitionResourceRefs,
|
|
122
|
+
remapWorkflowDefinitionResourceRefs,
|
|
123
|
+
type WorkflowDefinitionResourceDescriptor,
|
|
124
|
+
type WorkflowDefinitionResourceIdMaps,
|
|
125
|
+
type WorkflowDefinitionResourceRefs,
|
|
126
|
+
} from './utils/workflow-definition-resources';
|
|
@@ -48,10 +48,10 @@ const createEnv = (): StepExecutorEnv => {
|
|
|
48
48
|
outputMapping: {},
|
|
49
49
|
inputParser: { parse: (value: unknown) => value } as any,
|
|
50
50
|
} as any;
|
|
51
|
-
|
|
52
|
-
return {
|
|
51
|
+
const env = {
|
|
53
52
|
compiled,
|
|
54
53
|
context: new TestContext(),
|
|
54
|
+
signal: new AbortController().signal,
|
|
55
55
|
runId: 'run-1',
|
|
56
56
|
buildInstanceStepInfo: jest.fn(),
|
|
57
57
|
markSnapshot: jest.fn(),
|
|
@@ -68,7 +68,12 @@ const createEnv = (): StepExecutorEnv => {
|
|
|
68
68
|
captureTaskOutput: jest.fn(),
|
|
69
69
|
executeFlow: jest.fn(),
|
|
70
70
|
executeStep: jest.fn(),
|
|
71
|
-
|
|
71
|
+
fork: jest.fn(),
|
|
72
|
+
} as StepExecutorEnv;
|
|
73
|
+
|
|
74
|
+
env.fork = jest.fn((overrides) => ({ ...env, ...overrides }));
|
|
75
|
+
|
|
76
|
+
return env;
|
|
72
77
|
};
|
|
73
78
|
|
|
74
79
|
describe('executeConditional', () => {
|
|
@@ -47,10 +47,10 @@ const createEnv = (executeFlow: jest.Mock): StepExecutorEnv => {
|
|
|
47
47
|
outputMapping: {},
|
|
48
48
|
inputParser: { parse: (value: unknown) => value } as any,
|
|
49
49
|
} as any;
|
|
50
|
-
|
|
51
|
-
return {
|
|
50
|
+
const env = {
|
|
52
51
|
compiled,
|
|
53
52
|
context: new TestContext(),
|
|
53
|
+
signal: new AbortController().signal,
|
|
54
54
|
runId: 'run-loop',
|
|
55
55
|
buildInstanceStepInfo: jest.fn(),
|
|
56
56
|
markSnapshot: jest.fn(),
|
|
@@ -67,7 +67,12 @@ const createEnv = (executeFlow: jest.Mock): StepExecutorEnv => {
|
|
|
67
67
|
captureTaskOutput: jest.fn(),
|
|
68
68
|
executeFlow,
|
|
69
69
|
executeStep: jest.fn(),
|
|
70
|
-
|
|
70
|
+
fork: jest.fn(),
|
|
71
|
+
} as StepExecutorEnv;
|
|
72
|
+
|
|
73
|
+
env.fork = jest.fn((overrides) => ({ ...env, ...overrides }));
|
|
74
|
+
|
|
75
|
+
return env;
|
|
71
76
|
};
|
|
72
77
|
const createTaskStep = (id: string): CompiledStep => ({
|
|
73
78
|
id,
|