@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
@@ -9,8 +9,6 @@ exports.parseSuspendedStepId = parseSuspendedStepId;
9
9
  exports.rebuildSuspension = rebuildSuspension;
10
10
  exports.buildSuspensionForPath = buildSuspensionForPath;
11
11
  const loop_executor_1 = require("./step-executors/loop-executor");
12
- const parallel_executor_1 = require("./step-executors/parallel-executor");
13
- const skip_helpers_1 = require("./step-executors/skip-helpers");
14
12
  const suspension_continuation_1 = require("./step-executors/suspension-continuation");
15
13
  const workflow_event_emitter_1 = require("./workflow-event-emitter");
16
14
  /**
@@ -171,28 +169,7 @@ function buildSuspensionForPath(deps, steps, state, targetPath, pathPrefix, iter
171
169
  };
172
170
  }
173
171
  if (step.type === workflow_event_emitter_1.StepType.Parallel) {
174
- if (rest[0] !== 'parallel') {
175
- return null;
176
- }
177
- const childPath = rest.slice(1);
178
- const childSuspension = buildSuspensionForPath(deps, step.steps, state, childPath, [...currentPath, 'parallel'], iterationDepth, suspensionMetadata);
179
- if (!childSuspension) {
180
- return null;
181
- }
182
- const childIndex = typeof childPath[0] === 'number' ? childPath[0] : 0;
183
- return (0, suspension_continuation_1.wrapSuspensionContinuation)(childSuspension, async () => {
184
- if (step.strategy === 'wait_any') {
185
- if (childIndex + 1 < step.steps.length) {
186
- (0, skip_helpers_1.markStepsSkipped)(env, step.steps.slice(childIndex + 1), state.iterationStack ?? []);
187
- }
188
- return deps.executeFlow(steps, state, pathPrefix, current + 1);
189
- }
190
- const next = await (0, parallel_executor_1.executeParallel)(env, step, state, currentPath, childIndex + 1);
191
- if (next) {
192
- return (0, suspension_continuation_1.wrapSuspensionContinuation)(next, () => deps.executeFlow(steps, state, pathPrefix, current + 1));
193
- }
194
- return deps.executeFlow(steps, state, pathPrefix, current + 1);
195
- });
172
+ return null;
196
173
  }
197
174
  if (step.type === workflow_event_emitter_1.StepType.Conditional) {
198
175
  if (rest[0] !== 'branch' || typeof rest[1] !== 'number') {
@@ -5,10 +5,10 @@
5
5
  * Full terms: see LICENSE.md.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.isSnakeCaseName = void 0;
8
+ exports.isSnakeCaseName = exports.SNAKE_CASE_REGEX = void 0;
9
9
  exports.assertSnakeCaseName = assertSnakeCaseName;
10
10
  exports.toSnakeCase = toSnakeCase;
11
- const SNAKE_CASE_REGEX = /^[a-z0-9]+(?:_[a-z0-9]+)+$/;
11
+ exports.SNAKE_CASE_REGEX = /^[a-z0-9]+(?:_[a-z0-9]+)*$/;
12
12
  /**
13
13
  * Verifies that names follow the snake_case convention required by the system.
14
14
  *
@@ -17,7 +17,7 @@ const SNAKE_CASE_REGEX = /^[a-z0-9]+(?:_[a-z0-9]+)+$/;
17
17
  * @throws Error when the name is not snake_case.
18
18
  */
19
19
  function assertSnakeCaseName(name, entity) {
20
- if (!SNAKE_CASE_REGEX.test(name)) {
20
+ if (!exports.SNAKE_CASE_REGEX.test(name)) {
21
21
  throw new Error(`${entity} name must be snake_case with at least one underscore. Received: "${name}"`);
22
22
  }
23
23
  }
@@ -27,7 +27,7 @@ function assertSnakeCaseName(name, entity) {
27
27
  * @param value - Text to evaluate.
28
28
  * @returns `true` when the value is snake_case; otherwise `false`.
29
29
  */
30
- const isSnakeCaseName = (value) => SNAKE_CASE_REGEX.test(value);
30
+ const isSnakeCaseName = (value) => exports.SNAKE_CASE_REGEX.test(value);
31
31
  exports.isSnakeCaseName = isSnakeCaseName;
32
32
  /**
33
33
  * Converts arbitrary text into snake_case for use in workflow entities.
@@ -7,35 +7,90 @@
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.sleep = void 0;
9
9
  exports.withTimeout = withTimeout;
10
+ const errors_1 = require("../errors");
10
11
  /**
11
12
  * Resolves after the specified duration; useful for retry delays.
12
13
  *
13
14
  * @param durationMs - Number of milliseconds to wait before resolving.
15
+ * @param signal - Optional signal used to cancel the wait.
14
16
  * @returns A promise that resolves once the duration elapses.
15
17
  */
16
- const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
18
+ const sleep = (durationMs, signal) => {
19
+ if (!signal) {
20
+ return new Promise((resolve) => setTimeout(resolve, durationMs));
21
+ }
22
+ (0, errors_1.throwIfAborted)(signal);
23
+ return new Promise((resolve, reject) => {
24
+ const timer = setTimeout(() => {
25
+ signal.removeEventListener('abort', onAbort);
26
+ resolve();
27
+ }, durationMs);
28
+ const onAbort = () => {
29
+ clearTimeout(timer);
30
+ reject((0, errors_1.getAbortReason)(signal));
31
+ };
32
+ signal.addEventListener('abort', onAbort, { once: true });
33
+ });
34
+ };
17
35
  exports.sleep = sleep;
18
36
  /**
19
37
  * Wraps a promise and rejects if it does not settle within the timeout.
20
38
  *
21
39
  * @param promise - Operation that may take longer than the allowed timeout.
22
40
  * @param timeoutMs - Maximum time in milliseconds to wait before rejecting.
41
+ * @param signal - Optional signal used to cancel the operation.
23
42
  * @returns The result of the original promise when it resolves in time.
24
43
  * @throws Error when the timeout is exceeded.
25
44
  */
26
- async function withTimeout(promise, timeoutMs) {
27
- if (!timeoutMs) {
45
+ async function withTimeout(promise, timeoutMs, signal) {
46
+ if (!timeoutMs && !signal) {
28
47
  return promise;
29
48
  }
30
49
  return new Promise((resolve, reject) => {
31
- const timer = setTimeout(() => {
32
- reject(new Error(`Step execution exceeded timeout of ${timeoutMs}ms`));
33
- }, timeoutMs);
50
+ let settled = false;
51
+ const timer = timeoutMs
52
+ ? setTimeout(() => {
53
+ settled = true;
54
+ signal?.removeEventListener('abort', onAbort);
55
+ reject(new Error(`Step execution exceeded timeout of ${timeoutMs}ms`));
56
+ }, timeoutMs)
57
+ : undefined;
58
+ const onAbort = () => {
59
+ if (settled) {
60
+ return;
61
+ }
62
+ settled = true;
63
+ if (timer) {
64
+ clearTimeout(timer);
65
+ }
66
+ reject((0, errors_1.getAbortReason)(signal));
67
+ };
68
+ if (signal) {
69
+ if (signal.aborted) {
70
+ onAbort();
71
+ return;
72
+ }
73
+ signal.addEventListener('abort', onAbort, { once: true });
74
+ }
34
75
  promise.then((value) => {
35
- clearTimeout(timer);
76
+ if (settled) {
77
+ return;
78
+ }
79
+ settled = true;
80
+ if (timer) {
81
+ clearTimeout(timer);
82
+ }
83
+ signal?.removeEventListener('abort', onAbort);
36
84
  resolve(value);
37
85
  }, (error) => {
38
- clearTimeout(timer);
86
+ if (settled) {
87
+ return;
88
+ }
89
+ settled = true;
90
+ if (timer) {
91
+ clearTimeout(timer);
92
+ }
93
+ signal?.removeEventListener('abort', onAbort);
39
94
  reject(error);
40
95
  });
41
96
  });
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ /*
3
+ * Hexabot — Fair Core License (FCL-1.0-ALv2)
4
+ * Copyright (c) 2026 Hexastack.
5
+ * Full terms: see LICENSE.md.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.remapWorkflowDefinitionResourceRefs = exports.collectWorkflowDefinitionResourceRefs = void 0;
9
+ const getSettingsRef = (definition, settingsKey) => {
10
+ const settings = definition.settings;
11
+ if (!settings || typeof settings !== 'object') {
12
+ return null;
13
+ }
14
+ const value = settings[settingsKey];
15
+ return typeof value === 'string' && value.trim() ? value : null;
16
+ };
17
+ /**
18
+ * Collect external resource IDs referenced by workflow definition defs.
19
+ */
20
+ const collectWorkflowDefinitionResourceRefs = (definition, descriptors) => {
21
+ const result = Object.fromEntries(descriptors.map((descriptor) => [descriptor.kind, new Set()]));
22
+ for (const def of Object.values(definition.defs ?? {})) {
23
+ for (const descriptor of descriptors) {
24
+ if (def.kind !== descriptor.kind) {
25
+ continue;
26
+ }
27
+ const ref = getSettingsRef(def, descriptor.settingsKey);
28
+ if (ref) {
29
+ result[descriptor.kind].add(ref);
30
+ }
31
+ }
32
+ }
33
+ return Object.fromEntries(Object.entries(result).map(([kind, refs]) => [kind, Array.from(refs)]));
34
+ };
35
+ exports.collectWorkflowDefinitionResourceRefs = collectWorkflowDefinitionResourceRefs;
36
+ /**
37
+ * Rewrite external resource IDs referenced by workflow definition defs.
38
+ */
39
+ const remapWorkflowDefinitionResourceRefs = (definition, descriptors, idMaps) => {
40
+ let didChange = false;
41
+ const nextDefs = Object.fromEntries(Object.entries(definition.defs ?? {}).map(([defName, def]) => {
42
+ const descriptor = descriptors.find((item) => item.kind === def.kind);
43
+ if (!descriptor) {
44
+ return [defName, def];
45
+ }
46
+ const currentRef = getSettingsRef(def, descriptor.settingsKey);
47
+ const nextRef = currentRef
48
+ ? idMaps[descriptor.kind]?.[currentRef]
49
+ : undefined;
50
+ if (!currentRef || !nextRef || nextRef === currentRef) {
51
+ return [defName, def];
52
+ }
53
+ didChange = true;
54
+ return [
55
+ defName,
56
+ {
57
+ ...def,
58
+ settings: {
59
+ ...(def.settings ?? {}),
60
+ [descriptor.settingsKey]: nextRef,
61
+ },
62
+ },
63
+ ];
64
+ }));
65
+ if (!didChange) {
66
+ return definition;
67
+ }
68
+ return {
69
+ ...definition,
70
+ defs: nextDefs,
71
+ };
72
+ };
73
+ exports.remapWorkflowDefinitionResourceRefs = remapWorkflowDefinitionResourceRefs;
@@ -9,7 +9,6 @@ exports.compileWorkflow = void 0;
9
9
  const zod_1 = require("zod");
10
10
  const base_binding_1 = require("./bindings/base-binding");
11
11
  const dsl_types_1 = require("./dsl.types");
12
- const naming_1 = require("./utils/naming");
13
12
  const workflow_event_emitter_1 = require("./workflow-event-emitter");
14
13
  const workflow_values_1 = require("./workflow-values");
15
14
  /** Build a stable identifier for a step using its path and label. */
@@ -103,7 +102,6 @@ const compileTasks = (definition, options) => {
103
102
  }
104
103
  assertActionsBound(taskDefinitions, options.actions);
105
104
  for (const [taskName, task] of Object.entries(taskDefinitions)) {
106
- (0, naming_1.assertSnakeCaseName)(taskName, 'action');
107
105
  const action = options.actions[task.action];
108
106
  const settingsPayload = (0, workflow_values_1.mergeSettings)(defaultSettings, task.settings);
109
107
  const parsedSettings = action.parseSettings(settingsPayload);
@@ -7,6 +7,7 @@
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.WorkflowRunner = void 0;
9
9
  const context_1 = require("./context");
10
+ const errors_1 = require("./errors");
10
11
  const runner_runtime_control_1 = require("./runner-runtime-control");
11
12
  const conditional_executor_1 = require("./step-executors/conditional-executor");
12
13
  const loop_executor_1 = require("./step-executors/loop-executor");
@@ -122,7 +123,8 @@ class WorkflowRunner {
122
123
  if (!state) {
123
124
  throw new Error('Workflow state not initialized.');
124
125
  }
125
- return this.runExecution(() => this.executeFlow(this.compiled.flow, state, []));
126
+ const env = this.createExecutorEnv();
127
+ return this.runExecution(() => this.executeFlow(this.compiled.flow, state, [], 0, env));
126
128
  }
127
129
  /**
128
130
  * Resume a previously suspended workflow using the supplied resume data.
@@ -316,7 +318,7 @@ class WorkflowRunner {
316
318
  const mergedContext = existing?.context || update.context
317
319
  ? { ...existing?.context, ...update.context }
318
320
  : undefined;
319
- this.stepLog[step.id] = {
321
+ const nextRecord = {
320
322
  ...base,
321
323
  ...update,
322
324
  id: step.id,
@@ -324,6 +326,12 @@ class WorkflowRunner {
324
326
  status: update.status ?? base.status,
325
327
  context: mergedContext,
326
328
  };
329
+ this.stepLog[step.id] = nextRecord;
330
+ return {
331
+ ...nextRecord,
332
+ context: nextRecord.context ? { ...nextRecord.context } : undefined,
333
+ error: nextRecord.error ? { ...nextRecord.error } : undefined,
334
+ };
327
335
  }
328
336
  /**
329
337
  * Construct the environment object passed to step executors.
@@ -331,21 +339,28 @@ class WorkflowRunner {
331
339
  * @returns A step executor environment bound to this runner.
332
340
  * @throws When the workflow context is missing.
333
341
  */
334
- createExecutorEnv() {
335
- if (!this.context) {
342
+ createExecutorEnv(overrides = {}) {
343
+ const context = overrides.context ?? this.context;
344
+ if (!context) {
336
345
  throw new Error('Workflow context is not attached.');
337
346
  }
338
- return {
347
+ const signal = overrides.signal ?? new AbortController().signal;
348
+ const setCurrentStep = overrides.setCurrentStep ??
349
+ ((step) => {
350
+ this.currentStep = step;
351
+ });
352
+ const captureTaskOutput = overrides.captureTaskOutput ??
353
+ ((task, state, result) => this.captureTaskOutput(task, state, result));
354
+ const executorEnv = {
339
355
  compiled: this.compiled,
340
- context: this.context,
356
+ context,
357
+ signal,
341
358
  runId: this.runId,
342
359
  buildInstanceStepInfo: (step, iterationStack) => this.buildInstanceStepInfo(step, iterationStack),
343
360
  markSnapshot: (step, status, reason) => this.markSnapshot(step, status, reason),
344
361
  recordStepExecution: (step, update) => this.recordStepExecution(step, update),
345
362
  emit: (event, payload) => this.emit(event, payload),
346
- setCurrentStep: (step) => {
347
- this.currentStep = step;
348
- },
363
+ setCurrentStep,
349
364
  beginStepExecution: (stepId) => {
350
365
  if (!this.runtimeControl) {
351
366
  return `${stepId}#1`;
@@ -370,10 +385,17 @@ class WorkflowRunner {
370
385
  recordStepSuspendResult: (params) => {
371
386
  this.runtimeControl?.recordStepSuspendResult(params);
372
387
  },
373
- captureTaskOutput: (task, state, result) => this.captureTaskOutput(task, state, result),
374
- executeFlow: (steps, state, path, startIndex) => this.executeFlow(steps, state, path, startIndex),
375
- executeStep: (step, state, path) => this.executeStep(step, state, path),
388
+ captureTaskOutput,
389
+ executeFlow: (steps, state, path, startIndex) => this.executeFlow(steps, state, path, startIndex, executorEnv),
390
+ executeStep: (step, state, path) => this.executeStep(step, state, path, executorEnv),
391
+ fork: (forkOverrides) => this.createExecutorEnv({
392
+ context: forkOverrides.context ?? context,
393
+ signal: forkOverrides.signal ?? signal,
394
+ setCurrentStep: forkOverrides.setCurrentStep ?? setCurrentStep,
395
+ captureTaskOutput: forkOverrides.captureTaskOutput ?? captureTaskOutput,
396
+ }),
376
397
  };
398
+ return executorEnv;
377
399
  }
378
400
  /**
379
401
  * Build dependencies used to reconstruct a suspension from persisted state.
@@ -402,14 +424,15 @@ class WorkflowRunner {
402
424
  * @param startIndex Index to resume from within the flow.
403
425
  * @returns A suspension if execution pauses, otherwise void.
404
426
  */
405
- async executeFlow(steps, state, path, startIndex = 0) {
427
+ async executeFlow(steps, state, path, startIndex = 0, env = this.createExecutorEnv()) {
406
428
  // Walk the flow sequentially; if a step suspends, wrap its continuation so we resume at the same index.
407
429
  for (let index = startIndex; index < steps.length; index += 1) {
430
+ (0, errors_1.throwIfAborted)(env.signal);
408
431
  const step = steps[index];
409
432
  const stepPath = [...path, index];
410
- const suspension = await this.executeStep(step, state, stepPath);
433
+ const suspension = await this.executeStep(step, state, stepPath, env);
411
434
  if (suspension) {
412
- return (0, suspension_continuation_1.wrapSuspensionContinuation)(suspension, () => this.executeFlow(steps, state, path, index + 1));
435
+ return (0, suspension_continuation_1.wrapSuspensionContinuation)(suspension, () => this.executeFlow(steps, state, path, index + 1, env));
413
436
  }
414
437
  }
415
438
  return undefined;
@@ -422,8 +445,8 @@ class WorkflowRunner {
422
445
  * @param path Tokens describing the location of the step in the workflow.
423
446
  * @returns A suspension if the step pauses execution, otherwise void.
424
447
  */
425
- async executeStep(step, state, path) {
426
- const env = this.createExecutorEnv();
448
+ async executeStep(step, state, path, env = this.createExecutorEnv()) {
449
+ (0, errors_1.throwIfAborted)(env.signal);
427
450
  switch (step.type) {
428
451
  case workflow_event_emitter_1.StepType.Task:
429
452
  return (0, task_executor_1.executeTaskStep)(env, step, state, path);
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { z } from 'zod';
7
7
  import { BaseSettingsSchema } from '../dsl.types';
8
+ import { throwIfAborted } from '../errors';
8
9
  import { assertSnakeCaseName } from '../utils/naming';
9
10
  import { sleep, withTimeout } from '../utils/timeout';
10
11
  const BASE_SETTINGS_KEYS = Object.keys(BaseSettingsSchema.shape);
@@ -64,7 +65,7 @@ export class AbstractAction {
64
65
  .parse(payload ?? {});
65
66
  return (settings ?? {});
66
67
  }
67
- async run(payload, context, settings, bindings) {
68
+ async run(payload, context, settings, bindings, signal) {
68
69
  const input = this.parseInput(payload);
69
70
  const parsedSettings = this.parseSettings(settings);
70
71
  const parsedBindings = (bindings ?? {});
@@ -86,13 +87,17 @@ export class AbstractAction {
86
87
  const jitter = retrySettings.jitter;
87
88
  const multiplier = retrySettings.multiplier;
88
89
  while (attempt < maxAttempts) {
90
+ if (signal) {
91
+ throwIfAborted(signal);
92
+ }
89
93
  try {
90
94
  const result = await withTimeout(this.execute({
91
95
  input,
92
96
  context,
93
97
  settings: parsedSettings,
94
98
  bindings: parsedBindings,
95
- }), timeoutMs);
99
+ signal: signal ?? new AbortController().signal,
100
+ }), timeoutMs, signal);
96
101
  return this.parseOutput(result);
97
102
  }
98
103
  catch (error) {
@@ -108,7 +113,7 @@ export class AbstractAction {
108
113
  const jitterFactor = jitter > 0 ? 1 + (Math.random() * 2 - 1) * jitter : 1;
109
114
  const jitteredDelay = Math.max(0, Math.round(delay * jitterFactor));
110
115
  if (jitteredDelay > 0) {
111
- await sleep(jitteredDelay);
116
+ await sleep(jitteredDelay, signal);
112
117
  }
113
118
  }
114
119
  const nextDelay = currentDelay * multiplier;
@@ -7,6 +7,7 @@ import jsonata from 'jsonata';
7
7
  import { parse as parseYaml } from 'yaml';
8
8
  import { z } from 'zod';
9
9
  import { validateAndResolveBindings } from './bindings/base-binding';
10
+ import { SNAKE_CASE_REGEX } from './utils/naming';
10
11
  export const ExpressionStringSchema = z
11
12
  .string()
12
13
  .regex(/^=/, 'Expression strings must start with "="')
@@ -227,7 +228,7 @@ export const WorkflowDefinitionSchema = z.strictObject({
227
228
  inputs: InputsSchema.optional(),
228
229
  context: z.record(z.string(), JsonValueSchema).optional(),
229
230
  defaults: DefaultsSchema.optional(),
230
- defs: z.record(z.string(), DefDefinitionSchema),
231
+ defs: z.record(z.string().regex(SNAKE_CASE_REGEX, 'Name must be snake_case'), DefDefinitionSchema),
231
232
  flow: z.array(FlowStepSchema),
232
233
  outputs: z.record(z.string(), ExpressionStringSchema),
233
234
  });
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Hexabot — Fair Core License (FCL-1.0-ALv2)
3
+ * Copyright (c) 2025 Hexastack.
4
+ * Full terms: see LICENSE.md.
5
+ */
6
+ export class WorkflowCancellationError extends Error {
7
+ constructor(message = 'Workflow execution was cancelled.') {
8
+ super(message);
9
+ this.name = 'WorkflowCancellationError';
10
+ }
11
+ }
12
+ export class ParallelSuspensionError extends Error {
13
+ constructor() {
14
+ super('workflow.suspend() is not supported inside a parallel block.');
15
+ this.name = 'ParallelSuspensionError';
16
+ }
17
+ }
18
+ export const isWorkflowCancellationError = (error) => error instanceof WorkflowCancellationError ||
19
+ (error instanceof Error && error.name === 'WorkflowCancellationError');
20
+ export const getAbortReason = (signal) => {
21
+ if (signal.reason instanceof Error) {
22
+ return signal.reason;
23
+ }
24
+ if (signal.reason !== undefined) {
25
+ return new WorkflowCancellationError(String(signal.reason));
26
+ }
27
+ return new WorkflowCancellationError();
28
+ };
29
+ export const throwIfAborted = (signal) => {
30
+ if (signal.aborted) {
31
+ throw getAbortReason(signal);
32
+ }
33
+ };
package/dist/esm/index.js CHANGED
@@ -10,7 +10,9 @@ export * from './dsl.types';
10
10
  export { compileWorkflow, Workflow, WorkflowEventEmitter, WorkflowRunner, } from './workflow';
11
11
  export { StepType } from './workflow-event-emitter';
12
12
  export { NonDeterministicWorkflowError } from './runner-runtime-control';
13
+ export { ParallelSuspensionError, WorkflowCancellationError } from './errors';
13
14
  export { compileValue, evaluateMapping, evaluateValue, mergeSettings, } from './workflow-values';
14
15
  export { createDeferred } from './utils/deferred';
15
16
  export { assertSnakeCaseName, isSnakeCaseName, toSnakeCase, } from './utils/naming';
16
17
  export { sleep, withTimeout } from './utils/timeout';
18
+ export { collectWorkflowDefinitionResourceRefs, remapWorkflowDefinitionResourceRefs, } from './utils/workflow-definition-resources';