@hexabot-ai/agentic 3.0.1-alpha.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/LICENSE.md +120 -0
- package/README.md +231 -0
- package/dist/cjs/action/abstract-action.js +141 -0
- package/dist/cjs/action/action.js +47 -0
- package/dist/cjs/action/action.types.js +7 -0
- package/dist/cjs/bindings/base-binding.js +278 -0
- package/dist/cjs/context.js +79 -0
- package/dist/cjs/dsl.types.js +318 -0
- package/dist/cjs/index.js +52 -0
- package/dist/cjs/runner-runtime-control.js +343 -0
- package/dist/cjs/step-executors/conditional-executor.js +63 -0
- package/dist/cjs/step-executors/loop-executor.js +187 -0
- package/dist/cjs/step-executors/parallel-executor.js +51 -0
- package/dist/cjs/step-executors/skip-helpers.js +29 -0
- package/dist/cjs/step-executors/task-executor.js +155 -0
- package/dist/cjs/step-executors/types.js +7 -0
- package/dist/cjs/suspension-rebuilder.js +301 -0
- package/dist/cjs/utils/deferred.js +17 -0
- package/dist/cjs/utils/naming.js +48 -0
- package/dist/cjs/utils/timeout.js +42 -0
- package/dist/cjs/utils/workflow-definition.js +102 -0
- package/dist/cjs/workflow-compiler.js +212 -0
- package/dist/cjs/workflow-event-emitter.js +40 -0
- package/dist/cjs/workflow-runner.js +448 -0
- package/dist/cjs/workflow-types.js +7 -0
- package/dist/cjs/workflow-values.js +99 -0
- package/dist/cjs/workflow.js +243 -0
- package/dist/esm/action/abstract-action.js +137 -0
- package/dist/esm/action/action.js +44 -0
- package/dist/esm/action/action.types.js +6 -0
- package/dist/esm/bindings/base-binding.js +273 -0
- package/dist/esm/context.js +75 -0
- package/dist/esm/dsl.types.js +309 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/runner-runtime-control.js +338 -0
- package/dist/esm/step-executors/conditional-executor.js +60 -0
- package/dist/esm/step-executors/loop-executor.js +182 -0
- package/dist/esm/step-executors/parallel-executor.js +48 -0
- package/dist/esm/step-executors/skip-helpers.js +25 -0
- package/dist/esm/step-executors/task-executor.js +152 -0
- package/dist/esm/step-executors/types.js +6 -0
- package/dist/esm/suspension-rebuilder.js +296 -0
- package/dist/esm/utils/deferred.js +14 -0
- package/dist/esm/utils/naming.js +42 -0
- package/dist/esm/utils/timeout.js +37 -0
- package/dist/esm/utils/workflow-definition.js +98 -0
- package/dist/esm/workflow-compiler.js +208 -0
- package/dist/esm/workflow-event-emitter.js +36 -0
- package/dist/esm/workflow-runner.js +444 -0
- package/dist/esm/workflow-types.js +6 -0
- package/dist/esm/workflow-values.js +89 -0
- package/dist/esm/workflow.js +236 -0
- package/dist/types/action/abstract-action.d.ts +54 -0
- package/dist/types/action/abstract-action.d.ts.map +1 -0
- package/dist/types/action/action.d.ts +23 -0
- package/dist/types/action/action.d.ts.map +1 -0
- package/dist/types/action/action.types.d.ts +55 -0
- package/dist/types/action/action.types.d.ts.map +1 -0
- package/dist/types/bindings/base-binding.d.ts +52 -0
- package/dist/types/bindings/base-binding.d.ts.map +1 -0
- package/dist/types/context.d.ts +105 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/dsl.types.d.ts +220 -0
- package/dist/types/dsl.types.d.ts.map +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/runner-runtime-control.d.ts +64 -0
- package/dist/types/runner-runtime-control.d.ts.map +1 -0
- package/dist/types/step-executors/conditional-executor.d.ts +13 -0
- package/dist/types/step-executors/conditional-executor.d.ts.map +1 -0
- package/dist/types/step-executors/loop-executor.d.ts +33 -0
- package/dist/types/step-executors/loop-executor.d.ts.map +1 -0
- package/dist/types/step-executors/parallel-executor.d.ts +14 -0
- package/dist/types/step-executors/parallel-executor.d.ts.map +1 -0
- package/dist/types/step-executors/skip-helpers.d.ts +12 -0
- package/dist/types/step-executors/skip-helpers.d.ts.map +1 -0
- package/dist/types/step-executors/task-executor.d.ts +13 -0
- package/dist/types/step-executors/task-executor.d.ts.map +1 -0
- package/dist/types/step-executors/types.d.ts +24 -0
- package/dist/types/step-executors/types.d.ts.map +1 -0
- package/dist/types/suspension-rebuilder.d.ts +67 -0
- package/dist/types/suspension-rebuilder.d.ts.map +1 -0
- package/dist/types/utils/deferred.d.ts +7 -0
- package/dist/types/utils/deferred.d.ts.map +1 -0
- package/dist/types/utils/naming.d.ts +24 -0
- package/dist/types/utils/naming.d.ts.map +1 -0
- package/dist/types/utils/timeout.d.ts +17 -0
- package/dist/types/utils/timeout.d.ts.map +1 -0
- package/dist/types/utils/workflow-definition.d.ts +3 -0
- package/dist/types/utils/workflow-definition.d.ts.map +1 -0
- package/dist/types/workflow-compiler.d.ts +12 -0
- package/dist/types/workflow-compiler.d.ts.map +1 -0
- package/dist/types/workflow-event-emitter.d.ts +70 -0
- package/dist/types/workflow-event-emitter.d.ts.map +1 -0
- package/dist/types/workflow-runner.d.ts +179 -0
- package/dist/types/workflow-runner.d.ts.map +1 -0
- package/dist/types/workflow-types.d.ts +195 -0
- package/dist/types/workflow-types.d.ts.map +1 -0
- package/dist/types/workflow-values.d.ts +34 -0
- package/dist/types/workflow-values.d.ts.map +1 -0
- package/dist/types/workflow.d.ts +75 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/examples/defs-bindings-agent/actions/ai-agent.ts +110 -0
- package/examples/defs-bindings-agent/actions/caculate-score.ts +49 -0
- package/examples/defs-bindings-agent/actions/index.ts +15 -0
- package/examples/defs-bindings-agent/bindings.ts +20 -0
- package/examples/defs-bindings-agent/context.ts +32 -0
- package/examples/defs-bindings-agent/workflow.ts +67 -0
- package/examples/defs-bindings-agent/workflow.yml +28 -0
- package/examples/full/actions/await-user-input.ts +46 -0
- package/examples/full/actions/call-llm.ts +133 -0
- package/examples/full/actions/create-ticket.ts +46 -0
- package/examples/full/actions/decision-router.ts +83 -0
- package/examples/full/actions/get-calendar-events.ts +55 -0
- package/examples/full/actions/get-user-profile.ts +72 -0
- package/examples/full/actions/index.ts +33 -0
- package/examples/full/actions/query-memory.ts +45 -0
- package/examples/full/actions/search-web.ts +53 -0
- package/examples/full/actions/send-email.ts +50 -0
- package/examples/full/context.ts +41 -0
- package/examples/full/workflow.ts +75 -0
- package/examples/full/workflow.yml +359 -0
- package/examples/loop/actions/await-reply.ts +47 -0
- package/examples/loop/actions/index.ts +19 -0
- package/examples/loop/actions/send-text-message.ts +40 -0
- package/examples/loop/context.ts +32 -0
- package/examples/loop/workflow.ts +66 -0
- package/examples/loop/workflow.yml +66 -0
- package/examples/suspend-resume/actions/format-reply.ts +43 -0
- package/examples/suspend-resume/actions/index.ts +13 -0
- package/examples/suspend-resume/actions/wait-for-user.ts +51 -0
- package/examples/suspend-resume/context.ts +32 -0
- package/examples/suspend-resume/workflow.ts +79 -0
- package/examples/suspend-resume/workflow.yml +29 -0
- package/package.json +60 -0
- package/src/__tests__/parser.test.ts +95 -0
- package/src/__tests__/suspension-rebuilder.test.ts +361 -0
- package/src/__tests__/test-helpers.ts +36 -0
- package/src/__tests__/validation.test.ts +526 -0
- package/src/__tests__/workflow-compiler.test.ts +715 -0
- package/src/__tests__/workflow-definition-path.test.ts +381 -0
- package/src/__tests__/workflow-event-emitter.test.ts +50 -0
- package/src/__tests__/workflow-runner.test.ts +1397 -0
- package/src/__tests__/workflow-values.test.ts +132 -0
- package/src/__tests__/workflow.test.ts +320 -0
- package/src/action/__tests__/abstract-action-timing.test.ts +183 -0
- package/src/action/__tests__/action.test.ts +473 -0
- package/src/action/abstract-action.ts +215 -0
- package/src/action/action.ts +83 -0
- package/src/action/action.types.ts +93 -0
- package/src/bindings/base-binding.ts +508 -0
- package/src/context.ts +179 -0
- package/src/dsl.types.ts +473 -0
- package/src/index.ts +114 -0
- package/src/runner-runtime-control.ts +544 -0
- package/src/step-executors/conditional-executor.test.ts +192 -0
- package/src/step-executors/conditional-executor.ts +83 -0
- package/src/step-executors/loop-executor.test.ts +303 -0
- package/src/step-executors/loop-executor.ts +299 -0
- package/src/step-executors/parallel-executor.test.ts +182 -0
- package/src/step-executors/parallel-executor.ts +77 -0
- package/src/step-executors/skip-helpers.ts +68 -0
- package/src/step-executors/task-executor.test.ts +257 -0
- package/src/step-executors/task-executor.ts +248 -0
- package/src/step-executors/types.ts +70 -0
- package/src/suspension-rebuilder.ts +504 -0
- package/src/utils/deferred.ts +23 -0
- package/src/utils/naming.ts +55 -0
- package/src/utils/timeout.ts +48 -0
- package/src/utils/workflow-definition.ts +173 -0
- package/src/workflow-compiler.ts +317 -0
- package/src/workflow-event-emitter.ts +86 -0
- package/src/workflow-runner.ts +603 -0
- package/src/workflow-types.ts +207 -0
- package/src/workflow-values.ts +159 -0
- package/src/workflow.ts +387 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ActionSnapshot,
|
|
9
|
+
ActionStatus,
|
|
10
|
+
BaseWorkflowContext,
|
|
11
|
+
StepExecutionRecord,
|
|
12
|
+
WorkflowRunStatus,
|
|
13
|
+
WorkflowSnapshot,
|
|
14
|
+
} from './context';
|
|
15
|
+
import { RunnerRuntimeControl } from './runner-runtime-control';
|
|
16
|
+
import { executeConditional as runConditionalExecutor } from './step-executors/conditional-executor';
|
|
17
|
+
import { executeLoop as runLoopExecutor } from './step-executors/loop-executor';
|
|
18
|
+
import { executeParallel as runParallelExecutor } from './step-executors/parallel-executor';
|
|
19
|
+
import { executeTaskStep as runTaskExecutor } from './step-executors/task-executor';
|
|
20
|
+
import type { StepExecutorEnv } from './step-executors/types';
|
|
21
|
+
import {
|
|
22
|
+
rebuildSuspension,
|
|
23
|
+
type SuspensionRebuilderDeps,
|
|
24
|
+
} from './suspension-rebuilder';
|
|
25
|
+
import {
|
|
26
|
+
StepType,
|
|
27
|
+
type StepInfo,
|
|
28
|
+
type WorkflowEventMap,
|
|
29
|
+
} from './workflow-event-emitter';
|
|
30
|
+
import type {
|
|
31
|
+
CompiledStep,
|
|
32
|
+
CompiledTask,
|
|
33
|
+
CompiledWorkflow,
|
|
34
|
+
ExecutionState,
|
|
35
|
+
PersistedSuspension,
|
|
36
|
+
ResumeResult,
|
|
37
|
+
RunnerResumeArgs,
|
|
38
|
+
RunnerStartArgs,
|
|
39
|
+
StartResult,
|
|
40
|
+
Suspension,
|
|
41
|
+
WorkflowRunOptions,
|
|
42
|
+
} from './workflow-types';
|
|
43
|
+
import { evaluateMapping } from './workflow-values';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Executes a compiled workflow definition, tracking state, suspensions, and event hooks.
|
|
47
|
+
* Create a new instance per execution to avoid leaking state between runs.
|
|
48
|
+
*/
|
|
49
|
+
export class WorkflowRunner {
|
|
50
|
+
// Compiled workflow definition driving this runner.
|
|
51
|
+
private readonly compiled: CompiledWorkflow;
|
|
52
|
+
|
|
53
|
+
// External run identifier propagated through events for correlation.
|
|
54
|
+
private readonly runId?: string;
|
|
55
|
+
|
|
56
|
+
// Current lifecycle status of the workflow execution.
|
|
57
|
+
private status: WorkflowRunStatus = 'idle';
|
|
58
|
+
|
|
59
|
+
// Snapshots of action execution keyed by step id for inspection/resume.
|
|
60
|
+
private snapshots: Record<string, ActionSnapshot> = {};
|
|
61
|
+
|
|
62
|
+
// Detailed execution records for each task step, used by UI/telemetry.
|
|
63
|
+
private stepLog: Record<string, StepExecutionRecord> = {};
|
|
64
|
+
|
|
65
|
+
// Suspension state when execution is paused awaiting external input.
|
|
66
|
+
private suspension?: Suspension;
|
|
67
|
+
|
|
68
|
+
// Control surface exposed to actions to suspend/resume the workflow.
|
|
69
|
+
private runtimeControl?: RunnerRuntimeControl;
|
|
70
|
+
|
|
71
|
+
// Step currently being executed, used in event payloads and errors.
|
|
72
|
+
private currentStep?: StepInfo;
|
|
73
|
+
|
|
74
|
+
// Last payload provided to resume; exposed to actions via the context.
|
|
75
|
+
private lastResumeData?: unknown;
|
|
76
|
+
|
|
77
|
+
// Mutable execution state including input and outputs.
|
|
78
|
+
private state?: ExecutionState;
|
|
79
|
+
|
|
80
|
+
// Workflow context shared with actions for IO and side-effects.
|
|
81
|
+
private context?: BaseWorkflowContext;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a new runner for a compiled workflow definition.
|
|
85
|
+
*
|
|
86
|
+
* @param compiled The compiled workflow to execute.
|
|
87
|
+
* @param options Optional runner configuration such as run id.
|
|
88
|
+
*/
|
|
89
|
+
constructor(compiled: CompiledWorkflow, options?: WorkflowRunOptions) {
|
|
90
|
+
this.compiled = compiled;
|
|
91
|
+
this.runId = options?.runId;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the current state of the workflow run.
|
|
96
|
+
*
|
|
97
|
+
* @returns The state of the workflow execution.
|
|
98
|
+
*/
|
|
99
|
+
getState(): ExecutionState | undefined {
|
|
100
|
+
return this.state;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the current lifecycle status of the workflow run.
|
|
105
|
+
*
|
|
106
|
+
* @returns The status of the workflow execution.
|
|
107
|
+
*/
|
|
108
|
+
getStatus(): WorkflowRunStatus {
|
|
109
|
+
return this.status;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Snapshot the current workflow status and action states.
|
|
114
|
+
*
|
|
115
|
+
* @returns A snapshot representing the workflow and each action's status.
|
|
116
|
+
*/
|
|
117
|
+
getSnapshot(): WorkflowSnapshot {
|
|
118
|
+
return {
|
|
119
|
+
status: this.status,
|
|
120
|
+
actions: { ...this.snapshots },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Read the per-step execution records captured for UI/telemetry.
|
|
126
|
+
*
|
|
127
|
+
* @returns A shallow copy of the step execution log.
|
|
128
|
+
*/
|
|
129
|
+
getStepLog(): Record<string, StepExecutionRecord> {
|
|
130
|
+
return Object.fromEntries(
|
|
131
|
+
Object.entries(this.stepLog).map(([id, record]) => [
|
|
132
|
+
id,
|
|
133
|
+
{
|
|
134
|
+
...record,
|
|
135
|
+
context: record.context ? { ...record.context } : undefined,
|
|
136
|
+
error: record.error ? { ...record.error } : undefined,
|
|
137
|
+
},
|
|
138
|
+
]),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Read the step currently being executed.
|
|
144
|
+
*
|
|
145
|
+
* @returns Metadata for the in-flight step, if any.
|
|
146
|
+
*/
|
|
147
|
+
getCurrentStep(): StepInfo | undefined {
|
|
148
|
+
return this.currentStep;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Access the last payload supplied to a resume call.
|
|
153
|
+
*
|
|
154
|
+
* @returns The most recent resume data, or undefined if none.
|
|
155
|
+
*/
|
|
156
|
+
getLastResumeData(): unknown {
|
|
157
|
+
return this.lastResumeData;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Begin executing the workflow from the first step.
|
|
162
|
+
* Returns a status object describing whether execution finished, suspended, or failed.
|
|
163
|
+
*
|
|
164
|
+
* @param args Input payload and context to seed execution.
|
|
165
|
+
* @returns Execution result including status and snapshot.
|
|
166
|
+
*/
|
|
167
|
+
async start(args: RunnerStartArgs): Promise<StartResult> {
|
|
168
|
+
this.status = 'running';
|
|
169
|
+
this.snapshots = {};
|
|
170
|
+
this.stepLog = {};
|
|
171
|
+
this.suspension = undefined;
|
|
172
|
+
this.lastResumeData = undefined;
|
|
173
|
+
this.currentStep = undefined;
|
|
174
|
+
this.context = args.context;
|
|
175
|
+
this.state = {
|
|
176
|
+
input: this.compiled.inputParser.parse(args.inputData ?? {}),
|
|
177
|
+
output: {},
|
|
178
|
+
iterationStack: [],
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
this.runtimeControl = new RunnerRuntimeControl(this);
|
|
182
|
+
this.context.attachWorkflowRuntime(this.runtimeControl);
|
|
183
|
+
|
|
184
|
+
this.emit('hook:workflow:start', { runId: this.runId });
|
|
185
|
+
|
|
186
|
+
const state = this.state;
|
|
187
|
+
if (!state) {
|
|
188
|
+
throw new Error('Workflow state not initialized.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return this.runExecution(() =>
|
|
192
|
+
this.executeFlow(this.compiled.flow, state, []),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resume a previously suspended workflow using the supplied resume data.
|
|
198
|
+
*
|
|
199
|
+
* @param args Data provided to resume the suspended step.
|
|
200
|
+
* @returns Execution result including status and snapshot.
|
|
201
|
+
*/
|
|
202
|
+
async resume(args: RunnerResumeArgs): Promise<ResumeResult> {
|
|
203
|
+
if (this.status !== 'suspended' || !this.suspension) {
|
|
204
|
+
throw new Error('Cannot resume a workflow that is not suspended.');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!this.context || !this.state) {
|
|
208
|
+
throw new Error('Workflow state is not initialized.');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.status = 'running';
|
|
212
|
+
this.lastResumeData = args.resumeData;
|
|
213
|
+
|
|
214
|
+
const suspension: Suspension = this.suspension;
|
|
215
|
+
|
|
216
|
+
return this.runExecution(() => suspension.continue(args.resumeData));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Execute the provided workflow operation and normalize suspension, finish, and failure handling.
|
|
221
|
+
*
|
|
222
|
+
* @param execute Function that advances workflow execution and may return a suspension.
|
|
223
|
+
* @returns Result of the execution attempt.
|
|
224
|
+
*/
|
|
225
|
+
private async runExecution(
|
|
226
|
+
execute: () => Promise<Suspension | void>,
|
|
227
|
+
): Promise<StartResult> {
|
|
228
|
+
try {
|
|
229
|
+
const suspension = await execute();
|
|
230
|
+
|
|
231
|
+
if (suspension) {
|
|
232
|
+
this.suspension = suspension;
|
|
233
|
+
this.status = 'suspended';
|
|
234
|
+
this.emit('hook:workflow:suspended', {
|
|
235
|
+
runId: this.runId,
|
|
236
|
+
step: suspension.step,
|
|
237
|
+
reason: suspension.reason,
|
|
238
|
+
data: suspension.data,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
status: 'suspended',
|
|
243
|
+
step: suspension.step,
|
|
244
|
+
reason: suspension.reason,
|
|
245
|
+
data: suspension.data,
|
|
246
|
+
stepExecId: suspension.stepExecId,
|
|
247
|
+
suspendIndex: suspension.suspendIndex,
|
|
248
|
+
suspendKey: suspension.suspendKey,
|
|
249
|
+
awaitResults: suspension.awaitResults,
|
|
250
|
+
snapshot: this.getSnapshot(),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const output = await this.evaluateWorkflowOutputs();
|
|
255
|
+
this.status = 'finished';
|
|
256
|
+
this.suspension = undefined;
|
|
257
|
+
this.currentStep = undefined;
|
|
258
|
+
this.emit('hook:workflow:finish', { runId: this.runId, output });
|
|
259
|
+
if (!this.context) {
|
|
260
|
+
throw new Error('Workflow context is not attached.');
|
|
261
|
+
}
|
|
262
|
+
this.context.attachWorkflowRuntime(undefined);
|
|
263
|
+
|
|
264
|
+
return { status: 'finished', output, snapshot: this.getSnapshot() };
|
|
265
|
+
} catch (error) {
|
|
266
|
+
this.status = 'failed';
|
|
267
|
+
this.currentStep = undefined;
|
|
268
|
+
this.emit('hook:workflow:failure', { runId: this.runId, error });
|
|
269
|
+
this.context?.attachWorkflowRuntime(undefined);
|
|
270
|
+
|
|
271
|
+
return { status: 'failed', error, snapshot: this.getSnapshot() };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Rebuild a runner from persisted state, allowing hosts to resume after restarts.
|
|
277
|
+
*
|
|
278
|
+
* @param compiled The compiled workflow definition.
|
|
279
|
+
* @param options Persisted state and metadata needed to rebuild the runner.
|
|
280
|
+
* @returns A runner positioned to continue from the prior suspension or status.
|
|
281
|
+
*/
|
|
282
|
+
static async fromPersistedState(
|
|
283
|
+
compiled: CompiledWorkflow,
|
|
284
|
+
options: {
|
|
285
|
+
state: ExecutionState;
|
|
286
|
+
context: BaseWorkflowContext;
|
|
287
|
+
snapshot: WorkflowSnapshot;
|
|
288
|
+
suspension?: PersistedSuspension;
|
|
289
|
+
runId?: string;
|
|
290
|
+
lastResumeData?: unknown;
|
|
291
|
+
},
|
|
292
|
+
): Promise<WorkflowRunner> {
|
|
293
|
+
const runner = new WorkflowRunner(compiled, {
|
|
294
|
+
runId: options.runId,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
runner.state = {
|
|
298
|
+
input: options.state.input ?? {},
|
|
299
|
+
output: options.state.output ?? {},
|
|
300
|
+
iteration: options.state.iteration,
|
|
301
|
+
accumulator: options.state.accumulator,
|
|
302
|
+
iterationStack: [...(options.state.iterationStack ?? [])],
|
|
303
|
+
};
|
|
304
|
+
runner.context = options.context;
|
|
305
|
+
runner.snapshots = options.snapshot.actions ?? {};
|
|
306
|
+
runner.stepLog = {};
|
|
307
|
+
runner.status = options.snapshot.status ?? 'idle';
|
|
308
|
+
runner.lastResumeData = options.lastResumeData;
|
|
309
|
+
runner.runtimeControl = new RunnerRuntimeControl(runner);
|
|
310
|
+
options.context.attachWorkflowRuntime(runner.runtimeControl);
|
|
311
|
+
|
|
312
|
+
if (options.suspension) {
|
|
313
|
+
const suspension = rebuildSuspension(
|
|
314
|
+
runner.createSuspensionRebuilderDeps(),
|
|
315
|
+
{
|
|
316
|
+
state: runner.state,
|
|
317
|
+
stepId: options.suspension.stepId,
|
|
318
|
+
reason: options.suspension.reason ?? undefined,
|
|
319
|
+
data: options.suspension.data,
|
|
320
|
+
stepExecId: options.suspension.stepExecId,
|
|
321
|
+
suspendIndex: options.suspension.suspendIndex,
|
|
322
|
+
suspendKey: options.suspension.suspendKey,
|
|
323
|
+
awaitResults: options.suspension.awaitResults,
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
if (!suspension) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Unable to rebuild suspension for step ${options.suspension.stepId}`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
runner.suspension = suspension;
|
|
334
|
+
runner.status = 'suspended';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return runner;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Emit an event if an emitter is provided.
|
|
342
|
+
*
|
|
343
|
+
* @param event The event name to emit.
|
|
344
|
+
* @param payload The event payload.
|
|
345
|
+
*/
|
|
346
|
+
private emit<K extends keyof WorkflowEventMap>(
|
|
347
|
+
event: K,
|
|
348
|
+
payload: WorkflowEventMap[K],
|
|
349
|
+
) {
|
|
350
|
+
this.context?.eventEmitter?.emit(event, payload);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Evaluate and map the workflow outputs after all steps have completed.
|
|
355
|
+
* Throws if the internal state or context were not initialized.
|
|
356
|
+
*
|
|
357
|
+
* @returns The evaluated workflow outputs.
|
|
358
|
+
* @throws When state or context are missing.
|
|
359
|
+
*/
|
|
360
|
+
private async evaluateWorkflowOutputs(): Promise<Record<string, unknown>> {
|
|
361
|
+
if (!this.state || !this.context) {
|
|
362
|
+
throw new Error('Workflow state not initialized.');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return evaluateMapping(this.compiled.outputMapping, {
|
|
366
|
+
input: this.state.input,
|
|
367
|
+
context: this.context.state,
|
|
368
|
+
output: this.state.output,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Build a stable step id that reflects the current loop iteration stack.
|
|
374
|
+
*
|
|
375
|
+
* @param step The compiled step to annotate.
|
|
376
|
+
* @param iterationStack The loop stack representing nested iterations.
|
|
377
|
+
* @returns A step info object with an iteration-aware id.
|
|
378
|
+
*/
|
|
379
|
+
private buildInstanceStepInfo(
|
|
380
|
+
step: CompiledStep,
|
|
381
|
+
iterationStack: number[],
|
|
382
|
+
): StepInfo {
|
|
383
|
+
const suffix =
|
|
384
|
+
iterationStack.length > 0 ? `[${iterationStack.join('.')}]` : '';
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
id: `${step.id}${suffix}`,
|
|
388
|
+
name: step.label,
|
|
389
|
+
type: step.type,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Record an action snapshot for the given step id.
|
|
395
|
+
*
|
|
396
|
+
* @param step The step being updated.
|
|
397
|
+
* @param status The new snapshot status.
|
|
398
|
+
* @param reason Optional reason to include when marking failure/suspension.
|
|
399
|
+
*/
|
|
400
|
+
private markSnapshot(step: StepInfo, status: ActionStatus, reason?: string) {
|
|
401
|
+
this.snapshots[step.id] = {
|
|
402
|
+
id: step.id,
|
|
403
|
+
name: step.name,
|
|
404
|
+
status,
|
|
405
|
+
reason,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Record a detailed execution entry for the given step.
|
|
411
|
+
*
|
|
412
|
+
* @param step The step being updated.
|
|
413
|
+
* @param update Partial record fields to merge into the log.
|
|
414
|
+
*/
|
|
415
|
+
private recordStepExecution(
|
|
416
|
+
step: StepInfo,
|
|
417
|
+
update: Partial<StepExecutionRecord>,
|
|
418
|
+
) {
|
|
419
|
+
const existing = this.stepLog[step.id];
|
|
420
|
+
const base: StepExecutionRecord =
|
|
421
|
+
existing ??
|
|
422
|
+
({
|
|
423
|
+
id: step.id,
|
|
424
|
+
name: step.name,
|
|
425
|
+
status: 'pending',
|
|
426
|
+
} satisfies StepExecutionRecord);
|
|
427
|
+
const mergedContext =
|
|
428
|
+
existing?.context || update.context
|
|
429
|
+
? { ...existing?.context, ...update.context }
|
|
430
|
+
: undefined;
|
|
431
|
+
|
|
432
|
+
this.stepLog[step.id] = {
|
|
433
|
+
...base,
|
|
434
|
+
...update,
|
|
435
|
+
id: step.id,
|
|
436
|
+
name: step.name,
|
|
437
|
+
status: update.status ?? base.status,
|
|
438
|
+
context: mergedContext,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Construct the environment object passed to step executors.
|
|
444
|
+
*
|
|
445
|
+
* @returns A step executor environment bound to this runner.
|
|
446
|
+
* @throws When the workflow context is missing.
|
|
447
|
+
*/
|
|
448
|
+
private createExecutorEnv(): StepExecutorEnv {
|
|
449
|
+
if (!this.context) {
|
|
450
|
+
throw new Error('Workflow context is not attached.');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
compiled: this.compiled,
|
|
455
|
+
context: this.context,
|
|
456
|
+
runId: this.runId,
|
|
457
|
+
buildInstanceStepInfo: (step, iterationStack) =>
|
|
458
|
+
this.buildInstanceStepInfo(step, iterationStack),
|
|
459
|
+
markSnapshot: (step, status, reason) =>
|
|
460
|
+
this.markSnapshot(step, status, reason),
|
|
461
|
+
recordStepExecution: (step, update) =>
|
|
462
|
+
this.recordStepExecution(step, update),
|
|
463
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
464
|
+
setCurrentStep: (step?: StepInfo) => {
|
|
465
|
+
this.currentStep = step;
|
|
466
|
+
},
|
|
467
|
+
beginStepExecution: (stepId) => {
|
|
468
|
+
if (!this.runtimeControl) {
|
|
469
|
+
return `${stepId}#1`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return this.runtimeControl.beginStepExecution(stepId);
|
|
473
|
+
},
|
|
474
|
+
waitForStepSuspension: (stepId) => {
|
|
475
|
+
if (!this.runtimeControl) {
|
|
476
|
+
throw new Error('Workflow runtime control is not initialized.');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return this.runtimeControl.waitForStepSuspension(stepId);
|
|
480
|
+
},
|
|
481
|
+
clearStepSuspensions: (stepId, error) => {
|
|
482
|
+
this.runtimeControl?.clearStepSuspensions(stepId, error);
|
|
483
|
+
},
|
|
484
|
+
primeStepResumeData: (stepId, resumeData) => {
|
|
485
|
+
this.runtimeControl?.primeStepResumeData(stepId, resumeData);
|
|
486
|
+
},
|
|
487
|
+
prepareStepReplay: (seed) => {
|
|
488
|
+
this.runtimeControl?.prepareStepReplay(seed);
|
|
489
|
+
},
|
|
490
|
+
recordStepSuspendResult: (params) => {
|
|
491
|
+
this.runtimeControl?.recordStepSuspendResult(params);
|
|
492
|
+
},
|
|
493
|
+
captureTaskOutput: (task, state, result) =>
|
|
494
|
+
this.captureTaskOutput(task, state, result),
|
|
495
|
+
executeFlow: (steps, state, path, startIndex) =>
|
|
496
|
+
this.executeFlow(steps, state, path, startIndex),
|
|
497
|
+
executeStep: (step, state, path) => this.executeStep(step, state, path),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Build dependencies used to reconstruct a suspension from persisted state.
|
|
503
|
+
*
|
|
504
|
+
* @returns Dependency bag for suspension rebuilders.
|
|
505
|
+
*/
|
|
506
|
+
private createSuspensionRebuilderDeps(): SuspensionRebuilderDeps {
|
|
507
|
+
return {
|
|
508
|
+
compiled: this.compiled,
|
|
509
|
+
context: this.context,
|
|
510
|
+
runId: this.runId,
|
|
511
|
+
createExecutorEnv: () => this.createExecutorEnv(),
|
|
512
|
+
buildInstanceStepInfo: (step, iterationStack) =>
|
|
513
|
+
this.buildInstanceStepInfo(step, iterationStack),
|
|
514
|
+
captureTaskOutput: (task, state, result) =>
|
|
515
|
+
this.captureTaskOutput(task, state, result),
|
|
516
|
+
markSnapshot: (step, status, reason) =>
|
|
517
|
+
this.markSnapshot(step, status, reason),
|
|
518
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
519
|
+
executeFlow: (steps, state, path, startIndex) =>
|
|
520
|
+
this.executeFlow(steps, state, path, startIndex),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Walk a list of compiled steps, threading state and returning a suspension when encountered.
|
|
526
|
+
*
|
|
527
|
+
* @param steps The steps to execute sequentially.
|
|
528
|
+
* @param state Mutable execution state shared across steps.
|
|
529
|
+
* @param path Path tokens leading to the current step for tracing.
|
|
530
|
+
* @param startIndex Index to resume from within the flow.
|
|
531
|
+
* @returns A suspension if execution pauses, otherwise void.
|
|
532
|
+
*/
|
|
533
|
+
private async executeFlow(
|
|
534
|
+
steps: CompiledStep[],
|
|
535
|
+
state: ExecutionState,
|
|
536
|
+
path: Array<number | string>,
|
|
537
|
+
startIndex = 0,
|
|
538
|
+
): Promise<Suspension | void> {
|
|
539
|
+
// Walk the flow sequentially; if a step suspends, wrap its continuation so we resume at the same index.
|
|
540
|
+
for (let index = startIndex; index < steps.length; index += 1) {
|
|
541
|
+
const step = steps[index];
|
|
542
|
+
const stepPath = [...path, index];
|
|
543
|
+
const suspension = await this.executeStep(step, state, stepPath);
|
|
544
|
+
|
|
545
|
+
if (suspension) {
|
|
546
|
+
return {
|
|
547
|
+
...suspension,
|
|
548
|
+
continue: async (resumeData: unknown) => {
|
|
549
|
+
const next = await suspension.continue(resumeData);
|
|
550
|
+
if (next) {
|
|
551
|
+
return next;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return this.executeFlow(steps, state, path, index + 1);
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Execute a single compiled step by delegating to its executor.
|
|
565
|
+
*
|
|
566
|
+
* @param step The step to run.
|
|
567
|
+
* @param state The shared execution state.
|
|
568
|
+
* @param path Tokens describing the location of the step in the workflow.
|
|
569
|
+
* @returns A suspension if the step pauses execution, otherwise void.
|
|
570
|
+
*/
|
|
571
|
+
private async executeStep(
|
|
572
|
+
step: CompiledStep,
|
|
573
|
+
state: ExecutionState,
|
|
574
|
+
path: Array<number | string>,
|
|
575
|
+
): Promise<Suspension | void> {
|
|
576
|
+
const env = this.createExecutorEnv();
|
|
577
|
+
switch (step.type) {
|
|
578
|
+
case StepType.Task:
|
|
579
|
+
return runTaskExecutor(env, step, state, path);
|
|
580
|
+
case StepType.Parallel:
|
|
581
|
+
return runParallelExecutor(env, step, state, path);
|
|
582
|
+
case StepType.Conditional:
|
|
583
|
+
return runConditionalExecutor(env, step, state, path);
|
|
584
|
+
case StepType.Loop:
|
|
585
|
+
return runLoopExecutor(env, step, state, path);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Store the raw task result under the task name in the workflow output state.
|
|
591
|
+
* @reviewed
|
|
592
|
+
* @param task The task whose output is being captured.
|
|
593
|
+
* @param state Current execution state to mutate.
|
|
594
|
+
* @param result Raw result returned by the task action.
|
|
595
|
+
*/
|
|
596
|
+
private async captureTaskOutput(
|
|
597
|
+
task: CompiledTask,
|
|
598
|
+
state: ExecutionState,
|
|
599
|
+
result: unknown,
|
|
600
|
+
) {
|
|
601
|
+
state.output[task.name] = result;
|
|
602
|
+
}
|
|
603
|
+
}
|