@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,208 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { mountTaskBindings, validateAndResolveBindings, } from './bindings/base-binding';
|
|
8
|
+
import { extractTaskDefinitions as extractTaskDefinitionsFromDefs } from './dsl.types';
|
|
9
|
+
import { assertSnakeCaseName } from './utils/naming';
|
|
10
|
+
import { StepType } from './workflow-event-emitter';
|
|
11
|
+
import { compileValue, mergeSettings, } from './workflow-values';
|
|
12
|
+
/** Build a stable identifier for a step using its path and label. */
|
|
13
|
+
const buildStepId = (path, label) => {
|
|
14
|
+
const pathPart = path.length > 0 ? path.join('.') : 'root';
|
|
15
|
+
return `${pathPart}:${label}`;
|
|
16
|
+
};
|
|
17
|
+
/** Convert workflow input field metadata to a zod schema. */
|
|
18
|
+
const inputFieldToZod = (field) => {
|
|
19
|
+
let schema;
|
|
20
|
+
switch (field.type) {
|
|
21
|
+
case 'string':
|
|
22
|
+
schema = z.string();
|
|
23
|
+
break;
|
|
24
|
+
case 'number':
|
|
25
|
+
schema = z.number();
|
|
26
|
+
break;
|
|
27
|
+
case 'integer':
|
|
28
|
+
schema = z.int();
|
|
29
|
+
break;
|
|
30
|
+
case 'boolean':
|
|
31
|
+
schema = z.boolean();
|
|
32
|
+
break;
|
|
33
|
+
case 'array':
|
|
34
|
+
schema = z.array(field.items ? inputFieldToZod(field.items) : z.any());
|
|
35
|
+
break;
|
|
36
|
+
case 'object': {
|
|
37
|
+
const properties = {};
|
|
38
|
+
if (field.properties) {
|
|
39
|
+
for (const [name, child] of Object.entries(field.properties)) {
|
|
40
|
+
properties[name] = inputFieldToZod(child).optional();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
schema =
|
|
44
|
+
Object.keys(properties).length > 0
|
|
45
|
+
? z.strictObject(properties).partial()
|
|
46
|
+
: z.record(z.string(), z.any());
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
default:
|
|
50
|
+
schema = z.any();
|
|
51
|
+
}
|
|
52
|
+
if (field.enum) {
|
|
53
|
+
schema = schema.refine((value) => field.enum?.some((allowed) => allowed === value), {
|
|
54
|
+
message: `Value must be one of: ${field.enum.join(', ')}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return schema;
|
|
58
|
+
};
|
|
59
|
+
/** Assemble a parser for the workflow top-level input payload. */
|
|
60
|
+
const buildInputParser = (schema) => {
|
|
61
|
+
if (!schema || Object.keys(schema).length === 0) {
|
|
62
|
+
return z.looseObject({});
|
|
63
|
+
}
|
|
64
|
+
const shape = {};
|
|
65
|
+
for (const [name, field] of Object.entries(schema)) {
|
|
66
|
+
shape[name] = inputFieldToZod(field).optional();
|
|
67
|
+
}
|
|
68
|
+
return z.strictObject(shape).partial();
|
|
69
|
+
};
|
|
70
|
+
/** Compile a raw mapping object into expression-aware value mappings. */
|
|
71
|
+
const compileMapping = (values, options) => {
|
|
72
|
+
if (!values) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => [
|
|
76
|
+
key,
|
|
77
|
+
compileValue(value, options),
|
|
78
|
+
]));
|
|
79
|
+
};
|
|
80
|
+
/** Ensure every task references a provided action implementation. */
|
|
81
|
+
const assertActionsBound = (tasks, actions) => {
|
|
82
|
+
const missing = Object.values(tasks)
|
|
83
|
+
.map((task) => task.action)
|
|
84
|
+
.filter((actionName) => !Object.prototype.hasOwnProperty.call(actions, actionName));
|
|
85
|
+
if (missing.length > 0) {
|
|
86
|
+
throw new Error(`No action implementations provided for: ${missing.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/** Parse settings, compile inputs, and bind actions for each task. */
|
|
90
|
+
const compileTasks = (definition, options) => {
|
|
91
|
+
const compiled = {};
|
|
92
|
+
const defaultSettings = definition.defaults?.settings;
|
|
93
|
+
const taskDefinitions = extractTaskDefinitionsFromDefs(definition.defs);
|
|
94
|
+
const bindingValidation = validateAndResolveBindings(definition, {
|
|
95
|
+
bindingKinds: options.bindingKinds,
|
|
96
|
+
actions: options.actions,
|
|
97
|
+
});
|
|
98
|
+
if (bindingValidation.errors.length > 0) {
|
|
99
|
+
throw new Error(`Workflow bindings validation failed: ${bindingValidation.errors.join('; ')}`);
|
|
100
|
+
}
|
|
101
|
+
assertActionsBound(taskDefinitions, options.actions);
|
|
102
|
+
for (const [taskName, task] of Object.entries(taskDefinitions)) {
|
|
103
|
+
assertSnakeCaseName(taskName, 'action');
|
|
104
|
+
const action = options.actions[task.action];
|
|
105
|
+
const settingsPayload = mergeSettings(defaultSettings, task.settings);
|
|
106
|
+
const parsedSettings = action.parseSettings(settingsPayload);
|
|
107
|
+
compiled[taskName] = {
|
|
108
|
+
name: taskName,
|
|
109
|
+
actionName: task.action,
|
|
110
|
+
definition: task,
|
|
111
|
+
action,
|
|
112
|
+
inputs: compileMapping(task.inputs, options) ?? {},
|
|
113
|
+
settings: parsedSettings,
|
|
114
|
+
bindings: mountTaskBindings(task.bindings, bindingValidation.resolvedDefs, options.bindingKinds),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return compiled;
|
|
118
|
+
};
|
|
119
|
+
/** Recursively compile flow steps into executable nodes with generated ids. */
|
|
120
|
+
const compileFlowSteps = (steps, path = [], options) => steps.map((step, index) => {
|
|
121
|
+
const stepPath = [...path, index];
|
|
122
|
+
if ('do' in step) {
|
|
123
|
+
const label = step.do;
|
|
124
|
+
return {
|
|
125
|
+
type: StepType.Task,
|
|
126
|
+
id: buildStepId(stepPath, step.do),
|
|
127
|
+
label,
|
|
128
|
+
taskName: step.do,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if ('parallel' in step) {
|
|
132
|
+
const label = step.parallel.description ?? 'parallel';
|
|
133
|
+
return {
|
|
134
|
+
type: StepType.Parallel,
|
|
135
|
+
id: buildStepId(stepPath, 'parallel'),
|
|
136
|
+
label,
|
|
137
|
+
description: step.parallel.description,
|
|
138
|
+
strategy: step.parallel.strategy ?? 'wait_all',
|
|
139
|
+
steps: compileFlowSteps(step.parallel.steps, [...stepPath, 'parallel'], options),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if ('conditional' in step) {
|
|
143
|
+
const label = step.conditional.description ?? 'conditional';
|
|
144
|
+
const branches = step.conditional.when.map((branch, branchIdx) => ({
|
|
145
|
+
id: buildStepId([...stepPath, 'branch', branchIdx], 'conditional'),
|
|
146
|
+
condition: 'condition' in branch
|
|
147
|
+
? compileValue(branch.condition, options)
|
|
148
|
+
: undefined,
|
|
149
|
+
steps: compileFlowSteps(branch.steps, [...stepPath, 'branch', branchIdx], options),
|
|
150
|
+
}));
|
|
151
|
+
return {
|
|
152
|
+
type: StepType.Conditional,
|
|
153
|
+
id: buildStepId(stepPath, 'conditional'),
|
|
154
|
+
label,
|
|
155
|
+
description: step.conditional.description,
|
|
156
|
+
branches,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const loop = step.loop;
|
|
160
|
+
const label = loop.name ?? 'loop';
|
|
161
|
+
const commonLoop = {
|
|
162
|
+
type: StepType.Loop,
|
|
163
|
+
id: buildStepId(stepPath, label),
|
|
164
|
+
label,
|
|
165
|
+
name: loop.name,
|
|
166
|
+
description: loop.description,
|
|
167
|
+
accumulate: loop.accumulate
|
|
168
|
+
? {
|
|
169
|
+
as: loop.accumulate.as,
|
|
170
|
+
initial: loop.accumulate.initial,
|
|
171
|
+
merge: compileValue(loop.accumulate.merge, options),
|
|
172
|
+
}
|
|
173
|
+
: undefined,
|
|
174
|
+
steps: compileFlowSteps(loop.steps, [...stepPath, loop.name ?? 'loop'], options),
|
|
175
|
+
};
|
|
176
|
+
if (loop.type === 'for_each') {
|
|
177
|
+
return {
|
|
178
|
+
...commonLoop,
|
|
179
|
+
loopType: 'for_each',
|
|
180
|
+
forEach: {
|
|
181
|
+
item: loop.for_each.item,
|
|
182
|
+
in: compileValue(loop.for_each.in, options),
|
|
183
|
+
},
|
|
184
|
+
maxConcurrency: loop.max_concurrency,
|
|
185
|
+
until: loop.until ? compileValue(loop.until, options) : undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
...commonLoop,
|
|
190
|
+
loopType: 'while',
|
|
191
|
+
while: compileValue(loop.while, options),
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
/** Compile a workflow definition into structures consumable by the runtime. */
|
|
195
|
+
export const compileWorkflow = (definition, options) => {
|
|
196
|
+
const inputParser = buildInputParser(definition.inputs?.schema);
|
|
197
|
+
const tasks = compileTasks(definition, options);
|
|
198
|
+
const flow = compileFlowSteps(definition.flow, [], options);
|
|
199
|
+
const outputMapping = compileMapping(definition.outputs, options) ?? {};
|
|
200
|
+
return {
|
|
201
|
+
definition,
|
|
202
|
+
tasks,
|
|
203
|
+
flow,
|
|
204
|
+
outputMapping,
|
|
205
|
+
inputParser,
|
|
206
|
+
defaultSettings: definition.defaults?.settings,
|
|
207
|
+
};
|
|
208
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
export var StepType;
|
|
7
|
+
(function (StepType) {
|
|
8
|
+
StepType["Task"] = "task";
|
|
9
|
+
StepType["Parallel"] = "parallel";
|
|
10
|
+
StepType["Conditional"] = "conditional";
|
|
11
|
+
StepType["Loop"] = "loop";
|
|
12
|
+
})(StepType || (StepType = {}));
|
|
13
|
+
/**
|
|
14
|
+
* Minimal, browser-friendly event emitter that preserves the typed payloads
|
|
15
|
+
* exposed by {@link WorkflowEventMap}. It supports the subset of the Node.js
|
|
16
|
+
* EventEmitter API that the runtime relies on (`emit` and `on`).
|
|
17
|
+
*/
|
|
18
|
+
export class WorkflowEventEmitter {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.listeners = new Map();
|
|
21
|
+
}
|
|
22
|
+
emit(event, payload) {
|
|
23
|
+
const eventListeners = this.listeners.get(event);
|
|
24
|
+
if (!eventListeners || eventListeners.size === 0) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
eventListeners.forEach((listener) => listener(payload));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
on(event, listener) {
|
|
31
|
+
const listeners = this.listeners.get(event) ?? new Set();
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
this.listeners.set(event, listeners);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { RunnerRuntimeControl } from './runner-runtime-control';
|
|
7
|
+
import { executeConditional as runConditionalExecutor } from './step-executors/conditional-executor';
|
|
8
|
+
import { executeLoop as runLoopExecutor } from './step-executors/loop-executor';
|
|
9
|
+
import { executeParallel as runParallelExecutor } from './step-executors/parallel-executor';
|
|
10
|
+
import { executeTaskStep as runTaskExecutor } from './step-executors/task-executor';
|
|
11
|
+
import { rebuildSuspension, } from './suspension-rebuilder';
|
|
12
|
+
import { StepType, } from './workflow-event-emitter';
|
|
13
|
+
import { evaluateMapping } from './workflow-values';
|
|
14
|
+
/**
|
|
15
|
+
* Executes a compiled workflow definition, tracking state, suspensions, and event hooks.
|
|
16
|
+
* Create a new instance per execution to avoid leaking state between runs.
|
|
17
|
+
*/
|
|
18
|
+
export class WorkflowRunner {
|
|
19
|
+
/**
|
|
20
|
+
* Create a new runner for a compiled workflow definition.
|
|
21
|
+
*
|
|
22
|
+
* @param compiled The compiled workflow to execute.
|
|
23
|
+
* @param options Optional runner configuration such as run id.
|
|
24
|
+
*/
|
|
25
|
+
constructor(compiled, options) {
|
|
26
|
+
// Current lifecycle status of the workflow execution.
|
|
27
|
+
this.status = 'idle';
|
|
28
|
+
// Snapshots of action execution keyed by step id for inspection/resume.
|
|
29
|
+
this.snapshots = {};
|
|
30
|
+
// Detailed execution records for each task step, used by UI/telemetry.
|
|
31
|
+
this.stepLog = {};
|
|
32
|
+
this.compiled = compiled;
|
|
33
|
+
this.runId = options?.runId;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get the current state of the workflow run.
|
|
37
|
+
*
|
|
38
|
+
* @returns The state of the workflow execution.
|
|
39
|
+
*/
|
|
40
|
+
getState() {
|
|
41
|
+
return this.state;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the current lifecycle status of the workflow run.
|
|
45
|
+
*
|
|
46
|
+
* @returns The status of the workflow execution.
|
|
47
|
+
*/
|
|
48
|
+
getStatus() {
|
|
49
|
+
return this.status;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Snapshot the current workflow status and action states.
|
|
53
|
+
*
|
|
54
|
+
* @returns A snapshot representing the workflow and each action's status.
|
|
55
|
+
*/
|
|
56
|
+
getSnapshot() {
|
|
57
|
+
return {
|
|
58
|
+
status: this.status,
|
|
59
|
+
actions: { ...this.snapshots },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read the per-step execution records captured for UI/telemetry.
|
|
64
|
+
*
|
|
65
|
+
* @returns A shallow copy of the step execution log.
|
|
66
|
+
*/
|
|
67
|
+
getStepLog() {
|
|
68
|
+
return Object.fromEntries(Object.entries(this.stepLog).map(([id, record]) => [
|
|
69
|
+
id,
|
|
70
|
+
{
|
|
71
|
+
...record,
|
|
72
|
+
context: record.context ? { ...record.context } : undefined,
|
|
73
|
+
error: record.error ? { ...record.error } : undefined,
|
|
74
|
+
},
|
|
75
|
+
]));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read the step currently being executed.
|
|
79
|
+
*
|
|
80
|
+
* @returns Metadata for the in-flight step, if any.
|
|
81
|
+
*/
|
|
82
|
+
getCurrentStep() {
|
|
83
|
+
return this.currentStep;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Access the last payload supplied to a resume call.
|
|
87
|
+
*
|
|
88
|
+
* @returns The most recent resume data, or undefined if none.
|
|
89
|
+
*/
|
|
90
|
+
getLastResumeData() {
|
|
91
|
+
return this.lastResumeData;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Begin executing the workflow from the first step.
|
|
95
|
+
* Returns a status object describing whether execution finished, suspended, or failed.
|
|
96
|
+
*
|
|
97
|
+
* @param args Input payload and context to seed execution.
|
|
98
|
+
* @returns Execution result including status and snapshot.
|
|
99
|
+
*/
|
|
100
|
+
async start(args) {
|
|
101
|
+
this.status = 'running';
|
|
102
|
+
this.snapshots = {};
|
|
103
|
+
this.stepLog = {};
|
|
104
|
+
this.suspension = undefined;
|
|
105
|
+
this.lastResumeData = undefined;
|
|
106
|
+
this.currentStep = undefined;
|
|
107
|
+
this.context = args.context;
|
|
108
|
+
this.state = {
|
|
109
|
+
input: this.compiled.inputParser.parse(args.inputData ?? {}),
|
|
110
|
+
output: {},
|
|
111
|
+
iterationStack: [],
|
|
112
|
+
};
|
|
113
|
+
this.runtimeControl = new RunnerRuntimeControl(this);
|
|
114
|
+
this.context.attachWorkflowRuntime(this.runtimeControl);
|
|
115
|
+
this.emit('hook:workflow:start', { runId: this.runId });
|
|
116
|
+
const state = this.state;
|
|
117
|
+
if (!state) {
|
|
118
|
+
throw new Error('Workflow state not initialized.');
|
|
119
|
+
}
|
|
120
|
+
return this.runExecution(() => this.executeFlow(this.compiled.flow, state, []));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resume a previously suspended workflow using the supplied resume data.
|
|
124
|
+
*
|
|
125
|
+
* @param args Data provided to resume the suspended step.
|
|
126
|
+
* @returns Execution result including status and snapshot.
|
|
127
|
+
*/
|
|
128
|
+
async resume(args) {
|
|
129
|
+
if (this.status !== 'suspended' || !this.suspension) {
|
|
130
|
+
throw new Error('Cannot resume a workflow that is not suspended.');
|
|
131
|
+
}
|
|
132
|
+
if (!this.context || !this.state) {
|
|
133
|
+
throw new Error('Workflow state is not initialized.');
|
|
134
|
+
}
|
|
135
|
+
this.status = 'running';
|
|
136
|
+
this.lastResumeData = args.resumeData;
|
|
137
|
+
const suspension = this.suspension;
|
|
138
|
+
return this.runExecution(() => suspension.continue(args.resumeData));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Execute the provided workflow operation and normalize suspension, finish, and failure handling.
|
|
142
|
+
*
|
|
143
|
+
* @param execute Function that advances workflow execution and may return a suspension.
|
|
144
|
+
* @returns Result of the execution attempt.
|
|
145
|
+
*/
|
|
146
|
+
async runExecution(execute) {
|
|
147
|
+
try {
|
|
148
|
+
const suspension = await execute();
|
|
149
|
+
if (suspension) {
|
|
150
|
+
this.suspension = suspension;
|
|
151
|
+
this.status = 'suspended';
|
|
152
|
+
this.emit('hook:workflow:suspended', {
|
|
153
|
+
runId: this.runId,
|
|
154
|
+
step: suspension.step,
|
|
155
|
+
reason: suspension.reason,
|
|
156
|
+
data: suspension.data,
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
status: 'suspended',
|
|
160
|
+
step: suspension.step,
|
|
161
|
+
reason: suspension.reason,
|
|
162
|
+
data: suspension.data,
|
|
163
|
+
stepExecId: suspension.stepExecId,
|
|
164
|
+
suspendIndex: suspension.suspendIndex,
|
|
165
|
+
suspendKey: suspension.suspendKey,
|
|
166
|
+
awaitResults: suspension.awaitResults,
|
|
167
|
+
snapshot: this.getSnapshot(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const output = await this.evaluateWorkflowOutputs();
|
|
171
|
+
this.status = 'finished';
|
|
172
|
+
this.suspension = undefined;
|
|
173
|
+
this.currentStep = undefined;
|
|
174
|
+
this.emit('hook:workflow:finish', { runId: this.runId, output });
|
|
175
|
+
if (!this.context) {
|
|
176
|
+
throw new Error('Workflow context is not attached.');
|
|
177
|
+
}
|
|
178
|
+
this.context.attachWorkflowRuntime(undefined);
|
|
179
|
+
return { status: 'finished', output, snapshot: this.getSnapshot() };
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
this.status = 'failed';
|
|
183
|
+
this.currentStep = undefined;
|
|
184
|
+
this.emit('hook:workflow:failure', { runId: this.runId, error });
|
|
185
|
+
this.context?.attachWorkflowRuntime(undefined);
|
|
186
|
+
return { status: 'failed', error, snapshot: this.getSnapshot() };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Rebuild a runner from persisted state, allowing hosts to resume after restarts.
|
|
191
|
+
*
|
|
192
|
+
* @param compiled The compiled workflow definition.
|
|
193
|
+
* @param options Persisted state and metadata needed to rebuild the runner.
|
|
194
|
+
* @returns A runner positioned to continue from the prior suspension or status.
|
|
195
|
+
*/
|
|
196
|
+
static async fromPersistedState(compiled, options) {
|
|
197
|
+
const runner = new WorkflowRunner(compiled, {
|
|
198
|
+
runId: options.runId,
|
|
199
|
+
});
|
|
200
|
+
runner.state = {
|
|
201
|
+
input: options.state.input ?? {},
|
|
202
|
+
output: options.state.output ?? {},
|
|
203
|
+
iteration: options.state.iteration,
|
|
204
|
+
accumulator: options.state.accumulator,
|
|
205
|
+
iterationStack: [...(options.state.iterationStack ?? [])],
|
|
206
|
+
};
|
|
207
|
+
runner.context = options.context;
|
|
208
|
+
runner.snapshots = options.snapshot.actions ?? {};
|
|
209
|
+
runner.stepLog = {};
|
|
210
|
+
runner.status = options.snapshot.status ?? 'idle';
|
|
211
|
+
runner.lastResumeData = options.lastResumeData;
|
|
212
|
+
runner.runtimeControl = new RunnerRuntimeControl(runner);
|
|
213
|
+
options.context.attachWorkflowRuntime(runner.runtimeControl);
|
|
214
|
+
if (options.suspension) {
|
|
215
|
+
const suspension = rebuildSuspension(runner.createSuspensionRebuilderDeps(), {
|
|
216
|
+
state: runner.state,
|
|
217
|
+
stepId: options.suspension.stepId,
|
|
218
|
+
reason: options.suspension.reason ?? undefined,
|
|
219
|
+
data: options.suspension.data,
|
|
220
|
+
stepExecId: options.suspension.stepExecId,
|
|
221
|
+
suspendIndex: options.suspension.suspendIndex,
|
|
222
|
+
suspendKey: options.suspension.suspendKey,
|
|
223
|
+
awaitResults: options.suspension.awaitResults,
|
|
224
|
+
});
|
|
225
|
+
if (!suspension) {
|
|
226
|
+
throw new Error(`Unable to rebuild suspension for step ${options.suspension.stepId}`);
|
|
227
|
+
}
|
|
228
|
+
runner.suspension = suspension;
|
|
229
|
+
runner.status = 'suspended';
|
|
230
|
+
}
|
|
231
|
+
return runner;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Emit an event if an emitter is provided.
|
|
235
|
+
*
|
|
236
|
+
* @param event The event name to emit.
|
|
237
|
+
* @param payload The event payload.
|
|
238
|
+
*/
|
|
239
|
+
emit(event, payload) {
|
|
240
|
+
this.context?.eventEmitter?.emit(event, payload);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Evaluate and map the workflow outputs after all steps have completed.
|
|
244
|
+
* Throws if the internal state or context were not initialized.
|
|
245
|
+
*
|
|
246
|
+
* @returns The evaluated workflow outputs.
|
|
247
|
+
* @throws When state or context are missing.
|
|
248
|
+
*/
|
|
249
|
+
async evaluateWorkflowOutputs() {
|
|
250
|
+
if (!this.state || !this.context) {
|
|
251
|
+
throw new Error('Workflow state not initialized.');
|
|
252
|
+
}
|
|
253
|
+
return evaluateMapping(this.compiled.outputMapping, {
|
|
254
|
+
input: this.state.input,
|
|
255
|
+
context: this.context.state,
|
|
256
|
+
output: this.state.output,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Build a stable step id that reflects the current loop iteration stack.
|
|
261
|
+
*
|
|
262
|
+
* @param step The compiled step to annotate.
|
|
263
|
+
* @param iterationStack The loop stack representing nested iterations.
|
|
264
|
+
* @returns A step info object with an iteration-aware id.
|
|
265
|
+
*/
|
|
266
|
+
buildInstanceStepInfo(step, iterationStack) {
|
|
267
|
+
const suffix = iterationStack.length > 0 ? `[${iterationStack.join('.')}]` : '';
|
|
268
|
+
return {
|
|
269
|
+
id: `${step.id}${suffix}`,
|
|
270
|
+
name: step.label,
|
|
271
|
+
type: step.type,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Record an action snapshot for the given step id.
|
|
276
|
+
*
|
|
277
|
+
* @param step The step being updated.
|
|
278
|
+
* @param status The new snapshot status.
|
|
279
|
+
* @param reason Optional reason to include when marking failure/suspension.
|
|
280
|
+
*/
|
|
281
|
+
markSnapshot(step, status, reason) {
|
|
282
|
+
this.snapshots[step.id] = {
|
|
283
|
+
id: step.id,
|
|
284
|
+
name: step.name,
|
|
285
|
+
status,
|
|
286
|
+
reason,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Record a detailed execution entry for the given step.
|
|
291
|
+
*
|
|
292
|
+
* @param step The step being updated.
|
|
293
|
+
* @param update Partial record fields to merge into the log.
|
|
294
|
+
*/
|
|
295
|
+
recordStepExecution(step, update) {
|
|
296
|
+
const existing = this.stepLog[step.id];
|
|
297
|
+
const base = existing ??
|
|
298
|
+
{
|
|
299
|
+
id: step.id,
|
|
300
|
+
name: step.name,
|
|
301
|
+
status: 'pending',
|
|
302
|
+
};
|
|
303
|
+
const mergedContext = existing?.context || update.context
|
|
304
|
+
? { ...existing?.context, ...update.context }
|
|
305
|
+
: undefined;
|
|
306
|
+
this.stepLog[step.id] = {
|
|
307
|
+
...base,
|
|
308
|
+
...update,
|
|
309
|
+
id: step.id,
|
|
310
|
+
name: step.name,
|
|
311
|
+
status: update.status ?? base.status,
|
|
312
|
+
context: mergedContext,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Construct the environment object passed to step executors.
|
|
317
|
+
*
|
|
318
|
+
* @returns A step executor environment bound to this runner.
|
|
319
|
+
* @throws When the workflow context is missing.
|
|
320
|
+
*/
|
|
321
|
+
createExecutorEnv() {
|
|
322
|
+
if (!this.context) {
|
|
323
|
+
throw new Error('Workflow context is not attached.');
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
compiled: this.compiled,
|
|
327
|
+
context: this.context,
|
|
328
|
+
runId: this.runId,
|
|
329
|
+
buildInstanceStepInfo: (step, iterationStack) => this.buildInstanceStepInfo(step, iterationStack),
|
|
330
|
+
markSnapshot: (step, status, reason) => this.markSnapshot(step, status, reason),
|
|
331
|
+
recordStepExecution: (step, update) => this.recordStepExecution(step, update),
|
|
332
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
333
|
+
setCurrentStep: (step) => {
|
|
334
|
+
this.currentStep = step;
|
|
335
|
+
},
|
|
336
|
+
beginStepExecution: (stepId) => {
|
|
337
|
+
if (!this.runtimeControl) {
|
|
338
|
+
return `${stepId}#1`;
|
|
339
|
+
}
|
|
340
|
+
return this.runtimeControl.beginStepExecution(stepId);
|
|
341
|
+
},
|
|
342
|
+
waitForStepSuspension: (stepId) => {
|
|
343
|
+
if (!this.runtimeControl) {
|
|
344
|
+
throw new Error('Workflow runtime control is not initialized.');
|
|
345
|
+
}
|
|
346
|
+
return this.runtimeControl.waitForStepSuspension(stepId);
|
|
347
|
+
},
|
|
348
|
+
clearStepSuspensions: (stepId, error) => {
|
|
349
|
+
this.runtimeControl?.clearStepSuspensions(stepId, error);
|
|
350
|
+
},
|
|
351
|
+
primeStepResumeData: (stepId, resumeData) => {
|
|
352
|
+
this.runtimeControl?.primeStepResumeData(stepId, resumeData);
|
|
353
|
+
},
|
|
354
|
+
prepareStepReplay: (seed) => {
|
|
355
|
+
this.runtimeControl?.prepareStepReplay(seed);
|
|
356
|
+
},
|
|
357
|
+
recordStepSuspendResult: (params) => {
|
|
358
|
+
this.runtimeControl?.recordStepSuspendResult(params);
|
|
359
|
+
},
|
|
360
|
+
captureTaskOutput: (task, state, result) => this.captureTaskOutput(task, state, result),
|
|
361
|
+
executeFlow: (steps, state, path, startIndex) => this.executeFlow(steps, state, path, startIndex),
|
|
362
|
+
executeStep: (step, state, path) => this.executeStep(step, state, path),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Build dependencies used to reconstruct a suspension from persisted state.
|
|
367
|
+
*
|
|
368
|
+
* @returns Dependency bag for suspension rebuilders.
|
|
369
|
+
*/
|
|
370
|
+
createSuspensionRebuilderDeps() {
|
|
371
|
+
return {
|
|
372
|
+
compiled: this.compiled,
|
|
373
|
+
context: this.context,
|
|
374
|
+
runId: this.runId,
|
|
375
|
+
createExecutorEnv: () => this.createExecutorEnv(),
|
|
376
|
+
buildInstanceStepInfo: (step, iterationStack) => this.buildInstanceStepInfo(step, iterationStack),
|
|
377
|
+
captureTaskOutput: (task, state, result) => this.captureTaskOutput(task, state, result),
|
|
378
|
+
markSnapshot: (step, status, reason) => this.markSnapshot(step, status, reason),
|
|
379
|
+
emit: (event, payload) => this.emit(event, payload),
|
|
380
|
+
executeFlow: (steps, state, path, startIndex) => this.executeFlow(steps, state, path, startIndex),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Walk a list of compiled steps, threading state and returning a suspension when encountered.
|
|
385
|
+
*
|
|
386
|
+
* @param steps The steps to execute sequentially.
|
|
387
|
+
* @param state Mutable execution state shared across steps.
|
|
388
|
+
* @param path Path tokens leading to the current step for tracing.
|
|
389
|
+
* @param startIndex Index to resume from within the flow.
|
|
390
|
+
* @returns A suspension if execution pauses, otherwise void.
|
|
391
|
+
*/
|
|
392
|
+
async executeFlow(steps, state, path, startIndex = 0) {
|
|
393
|
+
// Walk the flow sequentially; if a step suspends, wrap its continuation so we resume at the same index.
|
|
394
|
+
for (let index = startIndex; index < steps.length; index += 1) {
|
|
395
|
+
const step = steps[index];
|
|
396
|
+
const stepPath = [...path, index];
|
|
397
|
+
const suspension = await this.executeStep(step, state, stepPath);
|
|
398
|
+
if (suspension) {
|
|
399
|
+
return {
|
|
400
|
+
...suspension,
|
|
401
|
+
continue: async (resumeData) => {
|
|
402
|
+
const next = await suspension.continue(resumeData);
|
|
403
|
+
if (next) {
|
|
404
|
+
return next;
|
|
405
|
+
}
|
|
406
|
+
return this.executeFlow(steps, state, path, index + 1);
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Execute a single compiled step by delegating to its executor.
|
|
415
|
+
*
|
|
416
|
+
* @param step The step to run.
|
|
417
|
+
* @param state The shared execution state.
|
|
418
|
+
* @param path Tokens describing the location of the step in the workflow.
|
|
419
|
+
* @returns A suspension if the step pauses execution, otherwise void.
|
|
420
|
+
*/
|
|
421
|
+
async executeStep(step, state, path) {
|
|
422
|
+
const env = this.createExecutorEnv();
|
|
423
|
+
switch (step.type) {
|
|
424
|
+
case StepType.Task:
|
|
425
|
+
return runTaskExecutor(env, step, state, path);
|
|
426
|
+
case StepType.Parallel:
|
|
427
|
+
return runParallelExecutor(env, step, state, path);
|
|
428
|
+
case StepType.Conditional:
|
|
429
|
+
return runConditionalExecutor(env, step, state, path);
|
|
430
|
+
case StepType.Loop:
|
|
431
|
+
return runLoopExecutor(env, step, state, path);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Store the raw task result under the task name in the workflow output state.
|
|
436
|
+
* @reviewed
|
|
437
|
+
* @param task The task whose output is being captured.
|
|
438
|
+
* @param state Current execution state to mutate.
|
|
439
|
+
* @param result Raw result returned by the task action.
|
|
440
|
+
*/
|
|
441
|
+
async captureTaskOutput(task, state, result) {
|
|
442
|
+
state.output[task.name] = result;
|
|
443
|
+
}
|
|
444
|
+
}
|