@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.
Files changed (69) hide show
  1. package/README.md +13 -5
  2. package/dist/cjs/action/abstract-action.js +8 -3
  3. package/dist/cjs/errors.js +41 -0
  4. package/dist/cjs/index.js +7 -1
  5. package/dist/cjs/step-executors/parallel-executor.js +238 -24
  6. package/dist/cjs/step-executors/skip-helpers.js +25 -1
  7. package/dist/cjs/step-executors/task-executor.js +77 -10
  8. package/dist/cjs/suspension-rebuilder.js +1 -24
  9. package/dist/cjs/utils/timeout.js +63 -8
  10. package/dist/cjs/utils/workflow-definition-resources.js +73 -0
  11. package/dist/cjs/workflow-runner.js +40 -17
  12. package/dist/esm/action/abstract-action.js +8 -3
  13. package/dist/esm/errors.js +33 -0
  14. package/dist/esm/index.js +2 -0
  15. package/dist/esm/step-executors/parallel-executor.js +239 -25
  16. package/dist/esm/step-executors/skip-helpers.js +23 -0
  17. package/dist/esm/step-executors/task-executor.js +77 -10
  18. package/dist/esm/suspension-rebuilder.js +1 -24
  19. package/dist/esm/utils/timeout.js +63 -8
  20. package/dist/esm/utils/workflow-definition-resources.js +68 -0
  21. package/dist/esm/workflow-runner.js +40 -17
  22. package/dist/types/action/abstract-action.d.ts +1 -1
  23. package/dist/types/action/abstract-action.d.ts.map +1 -1
  24. package/dist/types/action/action.types.d.ts +2 -1
  25. package/dist/types/action/action.types.d.ts.map +1 -1
  26. package/dist/types/context.d.ts +2 -1
  27. package/dist/types/context.d.ts.map +1 -1
  28. package/dist/types/errors.d.ts +10 -0
  29. package/dist/types/errors.d.ts.map +1 -0
  30. package/dist/types/index.d.ts +2 -0
  31. package/dist/types/index.d.ts.map +1 -1
  32. package/dist/types/step-executors/parallel-executor.d.ts +3 -4
  33. package/dist/types/step-executors/parallel-executor.d.ts.map +1 -1
  34. package/dist/types/step-executors/skip-helpers.d.ts +1 -0
  35. package/dist/types/step-executors/skip-helpers.d.ts.map +1 -1
  36. package/dist/types/step-executors/task-executor.d.ts.map +1 -1
  37. package/dist/types/step-executors/types.d.ts +9 -1
  38. package/dist/types/step-executors/types.d.ts.map +1 -1
  39. package/dist/types/suspension-rebuilder.d.ts.map +1 -1
  40. package/dist/types/utils/timeout.d.ts +4 -2
  41. package/dist/types/utils/timeout.d.ts.map +1 -1
  42. package/dist/types/utils/workflow-definition-resources.d.ts +16 -0
  43. package/dist/types/utils/workflow-definition-resources.d.ts.map +1 -0
  44. package/dist/types/workflow-event-emitter.d.ts +12 -13
  45. package/dist/types/workflow-event-emitter.d.ts.map +1 -1
  46. package/dist/types/workflow-runner.d.ts.map +1 -1
  47. package/package.json +1 -1
  48. package/src/__tests__/suspension-rebuilder.test.ts +3 -42
  49. package/src/__tests__/workflow-definition-resources.test.ts +97 -0
  50. package/src/__tests__/workflow-runner.test.ts +58 -5
  51. package/src/action/__tests__/action.test.ts +2 -1
  52. package/src/action/abstract-action.ts +9 -1
  53. package/src/action/action.types.ts +2 -0
  54. package/src/context.ts +2 -0
  55. package/src/errors.ts +43 -0
  56. package/src/index.ts +10 -0
  57. package/src/step-executors/conditional-executor.test.ts +8 -3
  58. package/src/step-executors/loop-executor.test.ts +8 -3
  59. package/src/step-executors/parallel-executor.test.ts +191 -82
  60. package/src/step-executors/parallel-executor.ts +402 -34
  61. package/src/step-executors/skip-helpers.ts +41 -0
  62. package/src/step-executors/task-executor.test.ts +85 -21
  63. package/src/step-executors/task-executor.ts +94 -10
  64. package/src/step-executors/types.ts +14 -1
  65. package/src/suspension-rebuilder.ts +1 -53
  66. package/src/utils/timeout.ts +78 -8
  67. package/src/utils/workflow-definition-resources.ts +111 -0
  68. package/src/workflow-event-emitter.ts +13 -6
  69. package/src/workflow-runner.ts +59 -19
package/README.md CHANGED
@@ -6,8 +6,8 @@ Typed runtime and YAML DSL for orchestrating multi-step AI/automation workflows.
6
6
  - Declarative workflow DSL in YAML or JS objects with JSONata expressions (prefixed by `=`) and clear scopes (`$input`, `$context`, `$output`, `$iteration`, `$accumulator`).
7
7
  - Type-safe actions built with `defineAction`, Zod-validated IO, and merged settings (timeouts, retries, plus action-specific options) inherited from workflow defaults.
8
8
  - Resumable execution via `WorkflowRunner` and `context.workflow.suspend`, plus snapshots for persistence and replay.
9
- - Flow primitives: sequential `do`, `parallel` blocks (`wait_all`/`wait_any`), `conditional` branches, and two loop variants: `loop.type: for_each` (iterables) and `loop.type: while` (pre-check condition).
10
- - Event emitter hooks for observability (`hook:workflow:start|finish|failure|suspended`, `hook:step:start|success|error|suspended|skipped`).
9
+ - Flow primitives: sequential `do`, truly concurrent `parallel` blocks (`wait_all`/`wait_any`), `conditional` branches, and two loop variants: `loop.type: for_each` (iterables) and `loop.type: while` (pre-check condition).
10
+ - Event emitter hooks for observability (`hook:workflow:start|finish|failure|suspended`, `hook:step:start|success|error|cancelled|suspended|skipped`).
11
11
 
12
12
  ## Installation
13
13
 
@@ -89,7 +89,11 @@ export const call_api = defineAction<
89
89
  description: 'Fetches data from an API',
90
90
  inputSchema: z.object({ id: z.string() }),
91
91
  outputSchema: z.object({ body: z.string() }),
92
- execute: async ({ input, context, settings, bindings }) => {
92
+ execute: async ({ input, context, settings, bindings, signal }) => {
93
+ if (signal.aborted) {
94
+ throw signal.reason;
95
+ }
96
+
93
97
  context.log('calling api', {
94
98
  id: input.id,
95
99
  timeout: settings.timeout_ms,
@@ -100,7 +104,9 @@ export const call_api = defineAction<
100
104
  });
101
105
  ```
102
106
 
103
- Settings are parsed and merged with workflow defaults before `execute` runs; retries and timeout wrappers are applied automatically. Awaiting `context.workflow.suspend(...)` pauses the workflow until `resume` is called.
107
+ Settings are parsed and merged with workflow defaults before `execute` runs; retries and timeout wrappers are applied automatically. Each action receives an `AbortSignal` as `signal`; long-running actions should stop promptly when it is aborted. Awaiting `context.workflow.suspend(...)` pauses the workflow until `resume` is called, except inside `parallel` branches where suspension fails with `ParallelSuspensionError`.
108
+
109
+ Parallel branches start concurrently with isolated `$output`, `$iteration`, `$accumulator`, and context state from the parallel entry point. `wait_all` merges successful branch output deltas in child order. `wait_any` advances with the first successful branch, aborts losing branches, and discards loser outputs.
104
110
 
105
111
  ## Running a workflow from YAML
106
112
 
@@ -203,12 +209,14 @@ export const await_user = defineAction<unknown, { reply?: string }, AppContext,
203
209
 
204
210
  `Workflow.run` throws an error with suspension details (`stepId`, `reason`, `data`) when this happens; `WorkflowRunner.start` instead returns `{ status: 'suspended', ... }` so hosts can persist state and resume later with `runner.resume`.
205
211
 
212
+ Suspending actions must not be placed inside `parallel` blocks.
213
+
206
214
  ## Events and observability
207
215
 
208
216
  `WorkflowRunner` can publish lifecycle hooks to any emitter-like object with `emit`/`on` (`WorkflowEventEmitterLike`); `WorkflowEventEmitter` is the built-in helper and mirrors these events:
209
217
 
210
218
  - `hook:workflow:start | finish | failure | suspended`
211
- - `hook:step:start | success | error | suspended | skipped`
219
+ - `hook:step:start | success | error | cancelled | suspended | skipped`
212
220
 
213
221
  Attach listeners to stream logs, emit metrics, or capture snapshots for debugging.
214
222
 
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.AbstractAction = void 0;
9
9
  const zod_1 = require("zod");
10
10
  const dsl_types_1 = require("../dsl.types");
11
+ const errors_1 = require("../errors");
11
12
  const naming_1 = require("../utils/naming");
12
13
  const timeout_1 = require("../utils/timeout");
13
14
  const BASE_SETTINGS_KEYS = Object.keys(dsl_types_1.BaseSettingsSchema.shape);
@@ -67,7 +68,7 @@ class AbstractAction {
67
68
  .parse(payload ?? {});
68
69
  return (settings ?? {});
69
70
  }
70
- async run(payload, context, settings, bindings) {
71
+ async run(payload, context, settings, bindings, signal) {
71
72
  const input = this.parseInput(payload);
72
73
  const parsedSettings = this.parseSettings(settings);
73
74
  const parsedBindings = (bindings ?? {});
@@ -89,13 +90,17 @@ class AbstractAction {
89
90
  const jitter = retrySettings.jitter;
90
91
  const multiplier = retrySettings.multiplier;
91
92
  while (attempt < maxAttempts) {
93
+ if (signal) {
94
+ (0, errors_1.throwIfAborted)(signal);
95
+ }
92
96
  try {
93
97
  const result = await (0, timeout_1.withTimeout)(this.execute({
94
98
  input,
95
99
  context,
96
100
  settings: parsedSettings,
97
101
  bindings: parsedBindings,
98
- }), timeoutMs);
102
+ signal: signal ?? new AbortController().signal,
103
+ }), timeoutMs, signal);
99
104
  return this.parseOutput(result);
100
105
  }
101
106
  catch (error) {
@@ -111,7 +116,7 @@ class AbstractAction {
111
116
  const jitterFactor = jitter > 0 ? 1 + (Math.random() * 2 - 1) * jitter : 1;
112
117
  const jitteredDelay = Math.max(0, Math.round(delay * jitterFactor));
113
118
  if (jitteredDelay > 0) {
114
- await (0, timeout_1.sleep)(jitteredDelay);
119
+ await (0, timeout_1.sleep)(jitteredDelay, signal);
115
120
  }
116
121
  }
117
122
  const nextDelay = currentDelay * multiplier;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /*
3
+ * Hexabot — Fair Core License (FCL-1.0-ALv2)
4
+ * Copyright (c) 2025 Hexastack.
5
+ * Full terms: see LICENSE.md.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.throwIfAborted = exports.getAbortReason = exports.isWorkflowCancellationError = exports.ParallelSuspensionError = exports.WorkflowCancellationError = void 0;
9
+ class WorkflowCancellationError extends Error {
10
+ constructor(message = 'Workflow execution was cancelled.') {
11
+ super(message);
12
+ this.name = 'WorkflowCancellationError';
13
+ }
14
+ }
15
+ exports.WorkflowCancellationError = WorkflowCancellationError;
16
+ class ParallelSuspensionError extends Error {
17
+ constructor() {
18
+ super('workflow.suspend() is not supported inside a parallel block.');
19
+ this.name = 'ParallelSuspensionError';
20
+ }
21
+ }
22
+ exports.ParallelSuspensionError = ParallelSuspensionError;
23
+ const isWorkflowCancellationError = (error) => error instanceof WorkflowCancellationError ||
24
+ (error instanceof Error && error.name === 'WorkflowCancellationError');
25
+ exports.isWorkflowCancellationError = isWorkflowCancellationError;
26
+ const getAbortReason = (signal) => {
27
+ if (signal.reason instanceof Error) {
28
+ return signal.reason;
29
+ }
30
+ if (signal.reason !== undefined) {
31
+ return new WorkflowCancellationError(String(signal.reason));
32
+ }
33
+ return new WorkflowCancellationError();
34
+ };
35
+ exports.getAbortReason = getAbortReason;
36
+ const throwIfAborted = (signal) => {
37
+ if (signal.aborted) {
38
+ throw (0, exports.getAbortReason)(signal);
39
+ }
40
+ };
41
+ exports.throwIfAborted = throwIfAborted;
package/dist/cjs/index.js CHANGED
@@ -19,7 +19,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
19
19
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
20
20
  };
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.withTimeout = exports.sleep = exports.toSnakeCase = exports.isSnakeCaseName = exports.assertSnakeCaseName = exports.createDeferred = exports.mergeSettings = exports.evaluateValue = exports.evaluateMapping = exports.compileValue = exports.NonDeterministicWorkflowError = exports.StepType = exports.WorkflowRunner = exports.WorkflowEventEmitter = exports.Workflow = exports.compileWorkflow = exports.WORKFLOW_RUN_STATUSES = exports.EWorkflowRunStatus = exports.BaseWorkflowContext = exports.defineAction = exports.AbstractAction = void 0;
22
+ exports.remapWorkflowDefinitionResourceRefs = exports.collectWorkflowDefinitionResourceRefs = exports.withTimeout = exports.sleep = exports.toSnakeCase = exports.isSnakeCaseName = exports.assertSnakeCaseName = exports.createDeferred = exports.mergeSettings = exports.evaluateValue = exports.evaluateMapping = exports.compileValue = exports.WorkflowCancellationError = exports.ParallelSuspensionError = exports.NonDeterministicWorkflowError = exports.StepType = exports.WorkflowRunner = exports.WorkflowEventEmitter = exports.Workflow = exports.compileWorkflow = exports.WORKFLOW_RUN_STATUSES = exports.EWorkflowRunStatus = exports.BaseWorkflowContext = exports.defineAction = exports.AbstractAction = void 0;
23
23
  var abstract_action_1 = require("./action/abstract-action");
24
24
  Object.defineProperty(exports, "AbstractAction", { enumerable: true, get: function () { return abstract_action_1.AbstractAction; } });
25
25
  var action_1 = require("./action/action");
@@ -38,6 +38,9 @@ var workflow_event_emitter_1 = require("./workflow-event-emitter");
38
38
  Object.defineProperty(exports, "StepType", { enumerable: true, get: function () { return workflow_event_emitter_1.StepType; } });
39
39
  var runner_runtime_control_1 = require("./runner-runtime-control");
40
40
  Object.defineProperty(exports, "NonDeterministicWorkflowError", { enumerable: true, get: function () { return runner_runtime_control_1.NonDeterministicWorkflowError; } });
41
+ var errors_1 = require("./errors");
42
+ Object.defineProperty(exports, "ParallelSuspensionError", { enumerable: true, get: function () { return errors_1.ParallelSuspensionError; } });
43
+ Object.defineProperty(exports, "WorkflowCancellationError", { enumerable: true, get: function () { return errors_1.WorkflowCancellationError; } });
41
44
  var workflow_values_1 = require("./workflow-values");
42
45
  Object.defineProperty(exports, "compileValue", { enumerable: true, get: function () { return workflow_values_1.compileValue; } });
43
46
  Object.defineProperty(exports, "evaluateMapping", { enumerable: true, get: function () { return workflow_values_1.evaluateMapping; } });
@@ -52,3 +55,6 @@ Object.defineProperty(exports, "toSnakeCase", { enumerable: true, get: function
52
55
  var timeout_1 = require("./utils/timeout");
53
56
  Object.defineProperty(exports, "sleep", { enumerable: true, get: function () { return timeout_1.sleep; } });
54
57
  Object.defineProperty(exports, "withTimeout", { enumerable: true, get: function () { return timeout_1.withTimeout; } });
58
+ var workflow_definition_resources_1 = require("./utils/workflow-definition-resources");
59
+ Object.defineProperty(exports, "collectWorkflowDefinitionResourceRefs", { enumerable: true, get: function () { return workflow_definition_resources_1.collectWorkflowDefinitionResourceRefs; } });
60
+ Object.defineProperty(exports, "remapWorkflowDefinitionResourceRefs", { enumerable: true, get: function () { return workflow_definition_resources_1.remapWorkflowDefinitionResourceRefs; } });
@@ -6,40 +6,254 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.executeParallel = executeParallel;
9
+ const errors_1 = require("../errors");
9
10
  const skip_helpers_1 = require("./skip-helpers");
10
- const suspension_continuation_1 = require("./suspension-continuation");
11
11
  /**
12
- * Execute a set of steps in parallel order, respecting the configured strategy.
12
+ * Execute a set of child steps concurrently and resolve according to the configured strategy.
13
13
  *
14
14
  * @param env Executor environment providing helpers and workflow context.
15
15
  * @param step The parallel step definition including strategy and children.
16
16
  * @param state Mutable workflow execution state.
17
17
  * @param path Path tokens locating this parallel block in the workflow.
18
- * @param startIndex Index to resume execution from when continuing.
19
- * @returns A suspension if a child pauses execution, otherwise void.
18
+ * @returns Always undefined; suspensions are rejected inside parallel blocks.
20
19
  */
21
- async function executeParallel(env, step, state, path, startIndex = 0) {
22
- for (let index = startIndex; index < step.steps.length; index += 1) {
23
- const child = step.steps[index];
24
- const childPath = [...path, 'parallel', index];
25
- const suspension = await env.executeStep(child, state, childPath);
20
+ async function executeParallel(env, step, state, path) {
21
+ if (step.steps.length === 0) {
22
+ return undefined;
23
+ }
24
+ (0, errors_1.throwIfAborted)(env.signal);
25
+ const baseOutput = cloneRecord(state.output);
26
+ const baseContextState = cloneRecord(env.context.state);
27
+ const branches = step.steps.map((child, index) => launchBranch({
28
+ env,
29
+ child,
30
+ index,
31
+ path,
32
+ parentState: state,
33
+ baseOutput,
34
+ baseContextState,
35
+ }));
36
+ if (step.strategy === 'wait_any') {
37
+ return executeWaitAny(env, branches, state);
38
+ }
39
+ return executeWaitAll(env, branches, state);
40
+ }
41
+ async function executeWaitAll(env, branches, state) {
42
+ const pending = new Set(branches);
43
+ const outcomes = new Map();
44
+ while (pending.size > 0) {
45
+ const { branch, result } = await raceNext(pending);
46
+ pending.delete(branch);
47
+ outcomes.set(result.index, result);
48
+ if (result.status === 'failed' || result.status === 'cancelled') {
49
+ cancelBranches(env, pending, result.error, 'Parallel branch execution was cancelled.');
50
+ await settleBranches(branches);
51
+ throw result.error;
52
+ }
53
+ }
54
+ mergeBranchOutputs(state, outcomes);
55
+ }
56
+ async function executeWaitAny(env, branches, state) {
57
+ const pending = new Set(branches);
58
+ const outcomes = new Map();
59
+ while (pending.size > 0) {
60
+ const { branch, result } = await raceNext(pending);
61
+ pending.delete(branch);
62
+ outcomes.set(result.index, result);
63
+ if (result.status === 'fulfilled') {
64
+ cancelBranches(env, pending, new errors_1.WorkflowCancellationError('Parallel wait_any winner selected.'), 'Parallel wait_any branch lost the race.');
65
+ await settleBranches(branches);
66
+ mergeBranchOutputs(state, new Map([[result.index, result]]));
67
+ return;
68
+ }
69
+ cancelBranches(env, pending, result.error, 'Parallel branch execution was cancelled.');
70
+ await settleBranches(branches);
71
+ throw result.error;
72
+ }
73
+ }
74
+ function launchBranch({ env, child, index, path, parentState, baseOutput, baseContextState, }) {
75
+ const controller = createBranchController(env.signal);
76
+ const trackedOutput = createTrackedOutput(baseOutput);
77
+ const branchState = {
78
+ input: parentState.input,
79
+ output: trackedOutput.output,
80
+ iteration: parentState.iteration ? { ...parentState.iteration } : undefined,
81
+ accumulator: cloneValue(parentState.accumulator),
82
+ iterationStack: [...parentState.iterationStack],
83
+ };
84
+ const branchContext = createParallelBranchContext(env.context, cloneRecord(baseContextState));
85
+ const branchEnv = env.fork({
86
+ context: branchContext,
87
+ signal: controller.signal,
88
+ setCurrentStep: () => undefined,
89
+ });
90
+ const childPath = [...path, 'parallel', index];
91
+ const promise = runBranch(branchEnv, child, branchState, childPath, trackedOutput, index);
92
+ return {
93
+ index,
94
+ step: child,
95
+ iterationStack: [...parentState.iterationStack],
96
+ controller,
97
+ promise,
98
+ };
99
+ }
100
+ async function runBranch(env, child, branchState, childPath, trackedOutput, index) {
101
+ try {
102
+ (0, errors_1.throwIfAborted)(env.signal);
103
+ const suspension = await env.executeStep(child, branchState, childPath);
26
104
  if (suspension) {
27
- return (0, suspension_continuation_1.wrapSuspensionContinuation)(suspension, async () => {
28
- if (step.strategy === 'wait_any') {
29
- if (index + 1 < step.steps.length) {
30
- (0, skip_helpers_1.markStepsSkipped)(env, step.steps.slice(index + 1), state.iterationStack);
31
- }
32
- return undefined;
33
- }
34
- return executeParallel(env, step, state, path, index + 1);
35
- });
36
- }
37
- if (step.strategy === 'wait_any') {
38
- if (index + 1 < step.steps.length) {
39
- (0, skip_helpers_1.markStepsSkipped)(env, step.steps.slice(index + 1), state.iterationStack);
105
+ throw new errors_1.ParallelSuspensionError();
106
+ }
107
+ (0, errors_1.throwIfAborted)(env.signal);
108
+ return {
109
+ status: 'fulfilled',
110
+ index,
111
+ outputDelta: trackedOutput.getDelta(),
112
+ };
113
+ }
114
+ catch (error) {
115
+ if (env.signal.aborted) {
116
+ return { status: 'cancelled', index, error: (0, errors_1.getAbortReason)(env.signal) };
117
+ }
118
+ return { status: 'failed', index, error };
119
+ }
120
+ }
121
+ async function raceNext(pending) {
122
+ return Promise.race([...pending].map((branch) => branch.promise.then((result) => ({ branch, result }))));
123
+ }
124
+ async function settleBranches(branches) {
125
+ await Promise.all(branches.map((branch) => branch.promise.catch(() => null)));
126
+ }
127
+ function cancelBranches(env, branches, reason, snapshotReason) {
128
+ for (const branch of branches) {
129
+ if (!branch.controller.signal.aborted) {
130
+ branch.controller.abort(reason instanceof Error
131
+ ? reason
132
+ : new errors_1.WorkflowCancellationError(String(reason)));
133
+ }
134
+ (0, skip_helpers_1.markStepsCancelled)(env, [branch.step], branch.iterationStack, reason instanceof Error ? reason.message : snapshotReason);
135
+ }
136
+ }
137
+ function mergeBranchOutputs(state, outcomes) {
138
+ const ordered = [...outcomes.values()].sort((left, right) => {
139
+ return left.index - right.index;
140
+ });
141
+ for (const outcome of ordered) {
142
+ if (outcome.status !== 'fulfilled') {
143
+ continue;
144
+ }
145
+ Object.assign(state.output, outcome.outputDelta);
146
+ }
147
+ }
148
+ function createBranchController(parentSignal) {
149
+ const controller = new AbortController();
150
+ if (parentSignal.aborted) {
151
+ controller.abort((0, errors_1.getAbortReason)(parentSignal));
152
+ return controller;
153
+ }
154
+ parentSignal.addEventListener('abort', () => controller.abort((0, errors_1.getAbortReason)(parentSignal)), { once: true });
155
+ return controller;
156
+ }
157
+ function createTrackedOutput(baseOutput) {
158
+ const writtenKeys = new Set();
159
+ const target = cloneRecord(baseOutput);
160
+ const output = new Proxy(target, {
161
+ set(record, property, value) {
162
+ if (typeof property === 'string') {
163
+ writtenKeys.add(property);
164
+ }
165
+ return Reflect.set(record, property, value);
166
+ },
167
+ deleteProperty(record, property) {
168
+ if (typeof property === 'string') {
169
+ writtenKeys.add(property);
170
+ }
171
+ return Reflect.deleteProperty(record, property);
172
+ },
173
+ });
174
+ return {
175
+ output,
176
+ getDelta: () => Object.fromEntries([...writtenKeys]
177
+ .filter((key) => Object.prototype.hasOwnProperty.call(target, key))
178
+ .map((key) => [key, target[key]])),
179
+ };
180
+ }
181
+ class ParallelWorkflowRuntimeControl {
182
+ constructor(getParent) {
183
+ this.getParent = getParent;
184
+ }
185
+ get status() {
186
+ return this.getParent().status;
187
+ }
188
+ get resumeData() {
189
+ return this.getParent().resumeData;
190
+ }
191
+ suspend() {
192
+ return Promise.reject(new errors_1.ParallelSuspensionError());
193
+ }
194
+ resume(data) {
195
+ this.getParent().resume(data);
196
+ }
197
+ getSnapshot() {
198
+ return this.getParent().getSnapshot();
199
+ }
200
+ }
201
+ function createParallelBranchContext(context, branchState) {
202
+ let state = branchState;
203
+ const workflow = new ParallelWorkflowRuntimeControl(() => context.workflow);
204
+ return new Proxy(context, {
205
+ get(target, property, receiver) {
206
+ if (property === 'state') {
207
+ return state;
208
+ }
209
+ if (property === 'workflow') {
210
+ return workflow;
40
211
  }
41
- return undefined;
212
+ if (property === 'snapshot') {
213
+ return () => cloneRecord(state);
214
+ }
215
+ if (property === 'attachWorkflowRuntime') {
216
+ return () => undefined;
217
+ }
218
+ return Reflect.get(target, property, receiver);
219
+ },
220
+ set(target, property, value, receiver) {
221
+ if (property === 'state') {
222
+ state = isRecord(value) ? value : {};
223
+ return true;
224
+ }
225
+ return Reflect.set(target, property, value, receiver);
226
+ },
227
+ });
228
+ }
229
+ function cloneRecord(value) {
230
+ return cloneValue(value);
231
+ }
232
+ function cloneValue(value) {
233
+ if (value === undefined || value === null) {
234
+ return value;
235
+ }
236
+ if (typeof structuredClone === 'function') {
237
+ try {
238
+ return structuredClone(value);
42
239
  }
240
+ catch {
241
+ // Fall through to JSON clone below.
242
+ }
243
+ }
244
+ try {
245
+ return JSON.parse(JSON.stringify(value));
246
+ }
247
+ catch {
248
+ if (Array.isArray(value)) {
249
+ return [...value];
250
+ }
251
+ if (isRecord(value)) {
252
+ return { ...value };
253
+ }
254
+ return value;
43
255
  }
44
- return undefined;
256
+ }
257
+ function isRecord(value) {
258
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
45
259
  }
@@ -5,7 +5,7 @@
5
5
  * Full terms: see LICENSE.md.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.markStepsSkipped = void 0;
8
+ exports.markStepsCancelled = exports.markStepsSkipped = void 0;
9
9
  const workflow_event_emitter_1 = require("../workflow-event-emitter");
10
10
  const markStepSkipped = (env, step, iterationStack, reason) => {
11
11
  const stepInfo = env.buildInstanceStepInfo(step, iterationStack);
@@ -23,7 +23,31 @@ const markStepSkipped = (env, step, iterationStack, reason) => {
23
23
  break;
24
24
  }
25
25
  };
26
+ const markStepCancelled = (env, step, iterationStack, reason) => {
27
+ const stepInfo = env.buildInstanceStepInfo(step, iterationStack);
28
+ env.markSnapshot(stepInfo, 'cancelled', reason);
29
+ env.emit('hook:step:cancelled', {
30
+ runId: env.runId,
31
+ step: stepInfo,
32
+ error: new Error(reason ?? 'Step execution was cancelled.'),
33
+ });
34
+ switch (step.type) {
35
+ case workflow_event_emitter_1.StepType.Parallel:
36
+ step.steps.forEach((child) => markStepCancelled(env, child, iterationStack, reason));
37
+ break;
38
+ case workflow_event_emitter_1.StepType.Conditional:
39
+ step.branches.forEach((branch) => branch.steps.forEach((child) => markStepCancelled(env, child, iterationStack, reason)));
40
+ break;
41
+ case workflow_event_emitter_1.StepType.Loop:
42
+ case workflow_event_emitter_1.StepType.Task:
43
+ break;
44
+ }
45
+ };
26
46
  const markStepsSkipped = (env, steps, iterationStack, reason) => {
27
47
  steps.forEach((step) => markStepSkipped(env, step, iterationStack, reason));
28
48
  };
29
49
  exports.markStepsSkipped = markStepsSkipped;
50
+ const markStepsCancelled = (env, steps, iterationStack, reason) => {
51
+ steps.forEach((step) => markStepCancelled(env, step, iterationStack, reason));
52
+ };
53
+ exports.markStepsCancelled = markStepsCancelled;
@@ -6,6 +6,7 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.executeTaskStep = executeTaskStep;
9
+ const errors_1 = require("../errors");
9
10
  const workflow_values_1 = require("../workflow-values");
10
11
  /**
11
12
  * Execute a task step by running its task and handling suspension or output mapping.
@@ -23,6 +24,11 @@ async function executeTaskStep(env, step, state, _path) {
23
24
  throw new Error(`Task "${step.taskName}" is not defined.`);
24
25
  }
25
26
  const stepInfo = env.buildInstanceStepInfo(step, state.iterationStack);
27
+ if (env.signal.aborted) {
28
+ const error = (0, errors_1.getAbortReason)(env.signal);
29
+ recordTaskCancellation(env, stepInfo, error);
30
+ throw error;
31
+ }
26
32
  const scope = {
27
33
  input: state.input,
28
34
  context: env.context.state,
@@ -31,7 +37,12 @@ async function executeTaskStep(env, step, state, _path) {
31
37
  accumulator: state.accumulator,
32
38
  };
33
39
  const inputs = await (0, workflow_values_1.evaluateMapping)(task.inputs, scope);
34
- env.recordStepExecution(stepInfo, {
40
+ if (env.signal.aborted) {
41
+ const error = (0, errors_1.getAbortReason)(env.signal);
42
+ recordTaskCancellation(env, stepInfo, error);
43
+ throw error;
44
+ }
45
+ const stepExecution = env.recordStepExecution(stepInfo, {
35
46
  action: task.actionName,
36
47
  status: 'running',
37
48
  startedAt: Date.now(),
@@ -41,9 +52,14 @@ async function executeTaskStep(env, step, state, _path) {
41
52
  env.beginStepExecution?.(stepInfo.id);
42
53
  env.setCurrentStep(stepInfo);
43
54
  env.markSnapshot(stepInfo, 'running');
44
- env.emit('hook:step:start', { runId: env.runId, step: stepInfo });
55
+ env.emit('hook:step:start', {
56
+ runId: env.runId,
57
+ step: stepInfo,
58
+ stepExecution,
59
+ });
45
60
  try {
46
- const actionPromise = Promise.resolve().then(() => task.action.run(inputs, env.context, task.settings, task.bindings));
61
+ (0, errors_1.throwIfAborted)(env.signal);
62
+ const actionPromise = Promise.resolve().then(() => task.action.run(inputs, env.context, task.settings, task.bindings, env.signal));
47
63
  const outcome = await waitForTaskProgress(env, stepInfo.id, actionPromise);
48
64
  if (outcome.type === 'completed') {
49
65
  await completeTask(env, stepInfo.id, stepInfo, task, state, outcome.value);
@@ -54,6 +70,11 @@ async function executeTaskStep(env, step, state, _path) {
54
70
  recordTaskFailure(env, stepInfo, outcome.error);
55
71
  throw outcome.error;
56
72
  }
73
+ if (outcome.type === 'cancelled') {
74
+ env.clearStepSuspensions(stepInfo.id, outcome.error);
75
+ recordTaskCancellation(env, stepInfo, outcome.error);
76
+ throw outcome.error;
77
+ }
57
78
  return buildSuspensionContinuation(env, stepInfo.id, stepInfo, task, state, actionPromise, outcome.request);
58
79
  }
59
80
  finally {
@@ -65,22 +86,45 @@ const waitForTaskProgress = async (env, stepId, actionPromise) => {
65
86
  const suspension = env
66
87
  .waitForStepSuspension(stepId)
67
88
  .then((request) => ({ type: 'suspended', request }));
68
- return Promise.race([completion, suspension]);
89
+ let cleanupCancellation = () => undefined;
90
+ const cancellation = new Promise((resolve) => {
91
+ if (env.signal.aborted) {
92
+ resolve({ type: 'cancelled', error: (0, errors_1.getAbortReason)(env.signal) });
93
+ return;
94
+ }
95
+ const onAbort = () => {
96
+ resolve({ type: 'cancelled', error: (0, errors_1.getAbortReason)(env.signal) });
97
+ };
98
+ env.signal.addEventListener('abort', onAbort, { once: true });
99
+ cleanupCancellation = () => {
100
+ env.signal.removeEventListener('abort', onAbort);
101
+ };
102
+ });
103
+ try {
104
+ return await Promise.race([completion, suspension, cancellation]);
105
+ }
106
+ finally {
107
+ cleanupCancellation();
108
+ }
69
109
  };
70
110
  const completeTask = async (env, stepId, stepInfo, task, state, result) => {
71
111
  await env.captureTaskOutput(task, state, result);
72
- env.recordStepExecution(stepInfo, {
112
+ const stepExecution = env.recordStepExecution(stepInfo, {
73
113
  status: 'completed',
74
114
  endedAt: Date.now(),
75
115
  output: result,
76
116
  context: { after: env.context.snapshot() },
77
117
  });
78
118
  env.markSnapshot(stepInfo, 'completed');
79
- env.emit('hook:step:success', { runId: env.runId, step: stepInfo });
119
+ env.emit('hook:step:success', {
120
+ runId: env.runId,
121
+ step: stepInfo,
122
+ stepExecution,
123
+ });
80
124
  env.clearStepSuspensions(stepId);
81
125
  };
82
126
  const recordSuspension = (env, stepInfo, reason, data) => {
83
- env.recordStepExecution(stepInfo, {
127
+ const stepExecution = env.recordStepExecution(stepInfo, {
84
128
  status: 'suspended',
85
129
  endedAt: Date.now(),
86
130
  reason,
@@ -90,6 +134,7 @@ const recordSuspension = (env, stepInfo, reason, data) => {
90
134
  env.emit('hook:step:suspended', {
91
135
  runId: env.runId,
92
136
  step: stepInfo,
137
+ stepExecution,
93
138
  reason,
94
139
  data,
95
140
  });
@@ -140,14 +185,36 @@ const buildSuspensionContinuation = (env, stepId, stepInfo, task, state, actionP
140
185
  };
141
186
  };
142
187
  const recordTaskFailure = (env, stepInfo, error) => {
143
- env.recordStepExecution(stepInfo, {
188
+ const normalizedError = normalizeError(error);
189
+ const stepExecution = env.recordStepExecution(stepInfo, {
144
190
  status: 'failed',
145
191
  endedAt: Date.now(),
146
- error: normalizeError(error),
192
+ error: normalizedError,
147
193
  context: { after: env.context.snapshot() },
148
194
  });
149
195
  env.markSnapshot(stepInfo, 'failed', normalizeErrorMessage(error));
150
- env.emit('hook:step:error', { runId: env.runId, step: stepInfo, error });
196
+ env.emit('hook:step:error', {
197
+ runId: env.runId,
198
+ step: stepInfo,
199
+ stepExecution,
200
+ error,
201
+ });
202
+ };
203
+ const recordTaskCancellation = (env, stepInfo, error) => {
204
+ const normalizedError = normalizeError(error);
205
+ const stepExecution = env.recordStepExecution(stepInfo, {
206
+ status: 'cancelled',
207
+ endedAt: Date.now(),
208
+ error: normalizedError,
209
+ context: { after: env.context.snapshot() },
210
+ });
211
+ env.markSnapshot(stepInfo, 'cancelled', normalizeErrorMessage(error));
212
+ env.emit('hook:step:cancelled', {
213
+ runId: env.runId,
214
+ step: stepInfo,
215
+ stepExecution,
216
+ error,
217
+ });
151
218
  };
152
219
  const normalizeError = (error) => error instanceof Error
153
220
  ? { message: error.message, stack: error.stack }