@hexabot-ai/agentic 3.1.2-alpha.9 → 3.1.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/dist/cjs/action/abstract-action.js +8 -3
- package/dist/cjs/errors.js +41 -0
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/step-executors/parallel-executor.js +238 -24
- package/dist/cjs/step-executors/skip-helpers.js +25 -1
- package/dist/cjs/step-executors/task-executor.js +77 -10
- package/dist/cjs/suspension-rebuilder.js +1 -24
- package/dist/cjs/utils/timeout.js +63 -8
- package/dist/cjs/utils/workflow-definition-resources.js +73 -0
- package/dist/cjs/workflow-runner.js +40 -17
- package/dist/esm/action/abstract-action.js +8 -3
- package/dist/esm/errors.js +33 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/step-executors/parallel-executor.js +239 -25
- package/dist/esm/step-executors/skip-helpers.js +23 -0
- package/dist/esm/step-executors/task-executor.js +77 -10
- package/dist/esm/suspension-rebuilder.js +1 -24
- package/dist/esm/utils/timeout.js +63 -8
- package/dist/esm/utils/workflow-definition-resources.js +68 -0
- package/dist/esm/workflow-runner.js +40 -17
- package/dist/types/action/abstract-action.d.ts +1 -1
- package/dist/types/action/abstract-action.d.ts.map +1 -1
- package/dist/types/action/action.types.d.ts +2 -1
- package/dist/types/action/action.types.d.ts.map +1 -1
- package/dist/types/context.d.ts +2 -1
- package/dist/types/context.d.ts.map +1 -1
- package/dist/types/errors.d.ts +10 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/step-executors/parallel-executor.d.ts +3 -4
- package/dist/types/step-executors/parallel-executor.d.ts.map +1 -1
- package/dist/types/step-executors/skip-helpers.d.ts +1 -0
- package/dist/types/step-executors/skip-helpers.d.ts.map +1 -1
- package/dist/types/step-executors/task-executor.d.ts.map +1 -1
- package/dist/types/step-executors/types.d.ts +9 -1
- package/dist/types/step-executors/types.d.ts.map +1 -1
- package/dist/types/suspension-rebuilder.d.ts.map +1 -1
- package/dist/types/utils/timeout.d.ts +4 -2
- package/dist/types/utils/timeout.d.ts.map +1 -1
- package/dist/types/utils/workflow-definition-resources.d.ts +16 -0
- package/dist/types/utils/workflow-definition-resources.d.ts.map +1 -0
- package/dist/types/workflow-event-emitter.d.ts +12 -13
- package/dist/types/workflow-event-emitter.d.ts.map +1 -1
- package/dist/types/workflow-runner.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/suspension-rebuilder.test.ts +3 -42
- package/src/__tests__/workflow-definition-resources.test.ts +97 -0
- package/src/__tests__/workflow-runner.test.ts +58 -5
- package/src/action/__tests__/action.test.ts +2 -1
- package/src/action/abstract-action.ts +9 -1
- package/src/action/action.types.ts +2 -0
- package/src/context.ts +2 -0
- package/src/errors.ts +43 -0
- package/src/index.ts +10 -0
- package/src/step-executors/conditional-executor.test.ts +8 -3
- package/src/step-executors/loop-executor.test.ts +8 -3
- package/src/step-executors/parallel-executor.test.ts +191 -82
- package/src/step-executors/parallel-executor.ts +402 -34
- package/src/step-executors/skip-helpers.ts +41 -0
- package/src/step-executors/task-executor.test.ts +85 -21
- package/src/step-executors/task-executor.ts +94 -10
- package/src/step-executors/types.ts +14 -1
- package/src/suspension-rebuilder.ts +1 -53
- package/src/utils/timeout.ts +78 -8
- package/src/utils/workflow-definition-resources.ts +111 -0
- package/src/workflow-event-emitter.ts +13 -6
- package/src/workflow-runner.ts +59 -19
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
|
-
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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', {
|
|
55
|
+
env.emit('hook:step:start', {
|
|
56
|
+
runId: env.runId,
|
|
57
|
+
step: stepInfo,
|
|
58
|
+
stepExecution,
|
|
59
|
+
});
|
|
45
60
|
try {
|
|
46
|
-
|
|
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
|
-
|
|
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', {
|
|
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
|
-
|
|
188
|
+
const normalizedError = normalizeError(error);
|
|
189
|
+
const stepExecution = env.recordStepExecution(stepInfo, {
|
|
144
190
|
status: 'failed',
|
|
145
191
|
endedAt: Date.now(),
|
|
146
|
-
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', {
|
|
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 }
|